算法通关 - 数组和链表

算法学习方法
  • 坚持、刻意练习
  • 练习缺陷、弱点地方
  • 不舒服、枯燥是正常的
  • LeetCode做题要考虑时间复杂度,尽量做到最优解
  • 经常反馈,LeetCode每道题后面的solution和discuss都会有别人的解法,可以学习别人的优秀方法。
常用的数据结构
  • 数组
  • 堆栈/队列
  • 优先队列
  • 链表(单链表/双链表)
  • 哈希表
  • 树/二叉树
  • 二叉搜索树
时间及空间复杂度
big o.png
主定理
主定理.png
数组和链表

数组是内存中一段连续的存储区域 ,通过下标可以随机访问数组中的任意元素,所以查询较快。而插入元素的话就需要先将插入位置后面的所有元素向后移动一位,然后再插入新元素,所以数组增删慢。

链表在内存中不是连续的,每个元素除了自己的值之外还有一个指向下一个元素的指针。插入元素只需要移动指针的指向即可,但是查询元素的话需要从头开始移动指针,直到找到要查找的元素。

链表包括单链表和双向链表。单链表,只有 next. 双链表, 不仅有 next, 还有 previous.

数组随机访问某个元素的时间复杂度是O(1)。查找的话,如果无序数组就是o(n),如果有序就可以用二分查找时间复杂度是 O(logn) 。增删的时间复杂度是O(n) 。链表查询的时间复杂度是O(n) ,增删的时间复杂度是O(1)

1. 查找旋转数组的最小数字

例如:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3,4,5,1,2} 为数组 {1,2,3,4,5} 的一个旋转,该数组的最小值为 1

方法一:遍历一遍数组,找到最小值后退出

public static int getTheMin(int nums[]) {
    if (nums == null || nums.length == 0) {
        throw new RuntimeException("input error!");
    }
    int result = nums[0];
    for (int i = 0; i < nums.length - 1; i++) {
        if (nums[i + 1] < nums[i]) {
            result = nums[i + 1];
            break;
        }
    }
    return result;
}

方法二:数组已经是有序的,只是做了一个旋转,所以我们可以考虑二分查找。

可以设定两个下标 low 和 high,并设定 mid = (low + high)/2,我们自然就可以找到数组中间的元素 array[mid],如果中间的元素位于前面的递增数组,那么它应该大于或者等于 low 下标对应的元素,此时数组中最小的元素应该位于该元素的后面,我们可以把 low 下标指向该中间元素,这样可以缩小查找的范围。同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于 high 下标对应的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们就可以把 high 下标更新到中位数的下标,移动之后的 high 下标对应的元素仍然在后面的递增子数组中。不管是更新 low 还是 high,我们的查找范围都会缩小为原来的一半,接下来我们再用更新的下标去重复新一轮的查找。直到最后两个下标相邻,即high - low = 1,也就是我们的循环结束条件。

public static int getTheMin(int nums[]) {
        if (nums == null || nums.length == 0) {
            throw new RuntimeException("input error!");
        }
        // 如果只有一个元素,直接返回
        if (nums.length == 1)
            return nums[0];
        int result = nums[0];
        int low = 0, high = nums.length - 1;
        int mid;
        // 确保 low 下标对应的值在左边的递增子数组,high 对应的值在右边递增子数组
        while (nums[low] >= nums[high]) {
            // 确保循环结束条件
            if (high - low == 1) {
                return nums[high];
            }
            // 取中间位置
            mid = (low + high) / 2;
            // 代表中间元素在左边递增子数组
            if (nums[mid] >= nums[low]) {
                low = mid;
            } else {
                high = mid;
            }
        }
        return result;
}
2.调整数组顺序使奇数位于偶数前面

例如:输入一个整型数组,实现一个函数来调整该数组中的数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分,希望时间复杂度尽量小

方法一:从头到尾扫描一遍数组,然后遇到奇数就移动到最前面

private static int[] orderArray(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            // 遇到奇数就放到最前面
            if (Math.abs(arr[i]) % 2 == 1) {
                int temp = arr[i];
                // 先把 i 前面的都向后移动一个位置
                for (int j = i; j > 0; j--) {
                    arr[j] = arr[j - 1];
                }
                arr[0] = temp;
            }
        }
        return arr;
}

方法二:我们只需要维护两个下标值,让一个下标值从前往后遍历,另外一个下标值从后往前遍历,当发现第一个下标值对应到偶数,第二个下标值对应到奇数的时候,我们就直接对调两个值。直到第一个下标到了第二个下标的后面的时候退出循环。

private static int[] orderArray(int[] arr) {
    int odd = 0, even = arr.length - 1;
    // 循环结束条件为 odd >= even
    while (odd < even) {
        // 第一个下标为偶数的时候停止
        while (odd < even && Math.abs(arr[odd]) % 2 != 0) {
            odd++;
        }
        // 第二个下标为奇数的时候停止
        while (odd < even && Math.abs(arr[even]) % 2 == 0) {
            even--;
        }

        // 找到后对调两个值
        int temp = arr[odd];
        arr[odd] = arr[even];
        arr[even] = temp;
    }
    return arr;
}
3.反转一个单链表(LeetCode - 206)

例如:

input: 1 --> 2 --> 3 --> 4      output: 4 --> 3 --> 2 --> 1

方法一:迭代

在遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。最后返回新的头引用。

public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode nextTemp = curr.next;
        curr.next = prev;
        prev = curr;
        curr = nextTemp;
    }
    return prev;
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

方法二:递归
递归版本关键在于反向工作。假设列表的其余部分已经被反转,现在该如何反转它前面的部分?

若从节点 k+1到 m 已经被反转,而我们正处于k。我们希望 k+1的下一个节点next指向k 。所以,

k.next.next = k。要注意的是第一个节点头结点head的下一个必须指向 Ø 。如果忽略了这一点,你的链表中可能会产生循环。

public ListNode reverseList(ListNode head) {
    if(head == null || head.next == null) return head;
    ListNode p = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return p;
}
  • 假设 n 是列表的长度,那么时间复杂度为 O(n)
  • 空间复杂度:O(n) 由于使用递归,将会使用隐式栈空间。递归深度可能会达到 n 层。
4.交换链表相邻元素(LeetCode - 24)

例如:对于给定链表中的元素,每相邻的两个元素互相加换。如果元素个数是奇数个,则最后一个元素就不用交换了。

input: 1 --> 2 --> 3 --> 4          output: 2 --> 1 --> 4 --> 3
input: 1 --> 2 --> 3 --> 4 --> 5    output: 2 --> 1 --> 4 --> 3 --> 5

递归解法:

public ListNode swapPairs(ListNode head) {
        if(head == null || head.next == null){
            return head;
        }
        ListNode next = head.next;
        head.next = swapPairs(next.next);
        next.next = head;
        return next;
}

非递归解法:

public ListNode swapPairs(ListNode head) {
        ListNode pre = new ListNode(0);
        pre.next = head;
        ListNode temp = pre;
        while(temp.next != null && temp.next.next != null) {
            ListNode start = temp.next;
            ListNode end = temp.next.next;
            temp.next = end;
            start.next = end.next;
            end.next = start;
            temp = start;
        }
        return pre.next;
}
5.判断一个链表是否有环(LeetCode -141)

方法一:哈希表

思路:可以通过检查一个结点此前是否被访问过来判断链表是否为环形链表。我们遍历所有结点并在哈希表中存储每个结点的引用(或内存地址)。如果当前结点为空结点 null(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,并且该链表不是环形链表。如果当前结点的引用已经存在于哈希表中,那么返回 true,表示该链表为环形链表。

public boolean hasCycle(ListNode head) {
    Set<ListNode> nodesSeen = new HashSet<>();
    while (head != null) {
        if (nodesSeen.contains(head)) {
            return true;
        } else {
            nodesSeen.add(head);
        }
        head = head.next;
    }
    return false;
}
  • 时间复杂度为 O(n)
  • 空间复杂度为 O(n)

方法二:龟兔赛跑法

思路:如果链表有环的话,那么和两个运动员在环形跑道上赛跑是一个道理。如果两人速度不同,那么他们最终会相遇。所以,我们可以使用具有 不同速度 的快、慢两个指针遍历链表,空间复杂度可以被降低至 O(1)。慢指针每次移动一步,而快指针每次移动两步。如果列表中不存在环,最终快指针将会最先到达尾部,此时我们可以返回 false

public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    ListNode slow = head;
    ListNode quick = head.next;
    while (slow != quick) {
        if (quick == null || quick.next == null) {
            return false;
        }
        slow = slow.next;
        quick = quick.next.next;
    }
    return true;
}
  • 时间复杂度为 O(n)
  • 空间复杂度为 O(1)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容