【LeetCode】->链表->通向链表自由之路

    科技2022-07-11  92

    LeetCode - 链表

    Ⅰ 前言Ⅱ 删除链表倒数第 n 个结点(#19)Ⅲ 两个有序链表的合并 (#21)Ⅳ 链表中环的检测(#141)Ⅴ 单链表反转(#206)Ⅵ 求链表的中间节点(#876)

    Ⅰ 前言

    在我数据结构与算法的链表讲解中,我留下了几个链表必须要掌握的操作,掌握看了它们,就基本可以实现 “链表自由” 了。

    关于链表的详细讲解,可以跳转到我下面的文章。

    【数据结构与算法】->数据结构->链表->LRU缓存淘汰算法的实现

    这几个操作分别对应了 LeetCode 上的几道题,我们一起来看看。

    Ⅱ 删除链表倒数第 n 个结点(#19)

    这道题的具体描述如下👇

    这个题我最开始的想法是用散列表先将这些结点按顺序存储起来,这样可以直接根据键值来找到需要删除的点,并且,也只需要遍历一次,就可以将所有结点存入散列表中,后面直接根据键值来取就好了。

    想法是好的,最后时间性能上打败了百分之六十多的人,让我意识到这个程序写得是多么蠢。

    这个题其实还可以用一个小技巧来做,就是快慢指针。我们先让一个快指针走 n 次,然后这时候慢指针和快指针一起移动,这样当快指针移动到终点,也就是这条链的尾结点的时候,慢指针刚好到要删除的地方。

    为了方便对链表进行操作,我引入了一个哨兵,这样如果要删除的结点是头结点,也不需要做特殊处理。我们直接来看代码。👇

    import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /* * @lc app=leetcode.cn id=19 lang=java * * [19] 删除链表的倒数第N个节点 */ // @lc code=start /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode pre = new ListNode(0); ListNode fastPoint = pre; ListNode slowPoint = pre; pre.next = head; while (n != 0) { fastPoint = fastPoint.next; n--; } while (fastPoint.next != null) { fastPoint = fastPoint.next; slowPoint = slowPoint.next; } slowPoint.next = slowPoint.next.next; return pre.next; } } // @lc code=end

    可以看到性能提高了非常多。

    Ⅲ 两个有序链表的合并 (#21)

    我先将这道题的具体描述贴上。

    这道题大家很容易想到利用 while 循环,然后对两个有序链表进行逐节点的比较,这个思路比较简单,我一开始也是想不到什么好方法,只能用这种笨蛋解法。我先将我的笨蛋解法贴出来,思路很简单,大家看一下就很容易理解了。

    /* * @lc app=leetcode.cn id=21 lang=java * * [21] 合并两个有序链表 */ // @lc code=start /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode head = new ListNode(); ListNode tmp = head; ListNode p = l1; ListNode q = l2; while (p != null && q != null) { ListNode cur = new ListNode(); if (p.val < q.val) { cur.val = p.val; p = p.next; } else { cur.val = q.val; q = q.next; } tmp.next = cur; tmp = cur; } while (p != null) { ListNode cur = new ListNode(p.val); tmp.next = cur; tmp = cur; p = p.next; } while (q != null) { ListNode cur = new ListNode(q.val); tmp.next = cur; tmp = cur; q = q.next; } return head.next; } } // @lc code=end

    思路就是先对两个有序链表进行比较,直到一个链表到尽头了。那么如果还有链表没有被遍历完,因为是顺序的,所以直接将它余下的元素加入到最后的链表中即可。

    这个笨蛋解法的耗时是多少呢?

    可以看到,时间复杂度和空间复杂度都非常高。

    我们这个解法每一次比较之后,都要建立一个新的结点,在 while 循环中不仅浪费了很多时间,也浪费了大量的空间,其实没必要,我们再看看一个更聪明的解法,用递归来解决这个问题。

    题目的部分我不再给出,只贴出作答的部分👇

    class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) { return l2; } if (l2 == null) { return l1; } if (l1.val < l2.val) { l1.next = mergeTwoLists(l1.next, l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } } }

    代码比前面的少了非常多,我们再看看性能👇

    通过这个递归,最后返回的就是最小的结点,我们也不需要再建立额外的空间,直接将原先的结点连接起来就好。

    Ⅳ 链表中环的检测(#141)

    同样的,我先把题目贴出。

    看到这个题我首先想到的就是用一个set集合或者hash table来存储,遍历整个链表,如果有相同的结点存进去,那就说明有环,返回 true,否则返回 false。但是,同样的,我首先可以想到的算法一定不是最好的算法。

    我们注意题目里的进阶要求,空间复杂度为 O(1)。那就肯定不可以用 set 集合了,那样复杂度就变成 O(n) 了。

    这时候,我们就要用到一个非常巧妙的解决方法,也是我们上面提到过的 快慢指针。那这个快慢指针要怎么用呢?

    大家肯定对我们初中学过的追及问题有印象吧?如果你比你追及的对象速度快,那你就一定可以追到它。对应到这道题,我们让快指针每次往前走两格,慢指针每次往前走一格,如果链表中有环的话,快慢指针最后就会进入环中,相当于两个人开始在操场跑圈,那快的人一定会追上慢的人,两个人一定能相见。

    放在这个题中,也是同样的思路。我们最后就检测快指针有没有追到慢指针,如果追到了,说明一定有环,如果没有追到,那链表中一定没有环。根据这个思路,我们来写代码。

    /* * @lc app=leetcode.cn id=141 lang=java * * [141] 环形链表 */ // @lc code=start /** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public boolean hasCycle(ListNode head) { ListNode fast = head; ListNode slow = head; while (fast != null && fast.next != null) { if (fast.next == slow) { return true; } fast = fast.next.next; slow = slow.next; } return false; } } // @lc code=end

    这里我还想直接截个图,因为不仅我的编辑器太漂亮了,这个解法也狠漂亮,非常简洁。

    Ⅴ 单链表反转(#206)

    具体题目如下👇

    进阶的要求是用迭代或者递归来解决,那我们就不要考虑用一个容器比如 ArrayList 来存放然后反转这种算法了,我们直接来看比较复杂的迭代和递归。

    这两个算法在我之前的文章中写过,大家有兴趣可以跳转过去看。

    【程序员必修数学课】->基础思想篇->迭代法

    【程序员必修数学课】->基础思想篇->递归(上)->泛化数学归纳

    【程序员必修数学课】->基础思想篇->递归(下)->分而治之&从归并排序到MapReduce

    首先我们先看递归。

    /* * @lc app=leetcode.cn id=206 lang=java * * [206] 反转链表 */ // @lc code=start /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode reverseList(ListNode head) { if (head == null || head.next == null) { return head; } ListNode cur = reverseList(head.next); head.next.next = head; head.next = null; return cur; } } // @lc code=end

    最复杂的地方是下面这句

    这其实就是一个反转的过程。

    拿上图 4 个这个结点来举例子。

    head.next.next 指的就是 4.next.next 也就是 5.next,它等于 head,也就是指向了自己,这个语句就实现了结点 5 指向了 4,然后我们再把 head.next = null,也就是 4 指向 5 给断开,这就实现了反转。

    整个递归的过程,返回的 cur 就是 5,也就是最后一个结点,这样所有的反转完成之后,返回的 5 就相当于是这个反转链表的头结点了,也就完成了整个的反转过程。

    我们来看看递归的性能👇

    时间是很快,但是空间消耗是很大的,因为每一层递归都要消耗系统堆栈空间,所以占用的内存很大。

    我们再来看迭代。

    public ListNode reverseList(ListNode head) { ListNode pre = null; ListNode cur = head; ListNode tmp = null; while (cur != null) { tmp = cur.next; cur.next = pre; pre = cur; cur = tmp; } return pre; }

    迭代法实现的思想就是每遍历到一个结点,用 cur 标记,然后 pre 是 cur 前面的结点,使得 cur 指向 pre,这样遍历完,到 cur 指向 null 的时候,pre 正好指向尾结点,这样就实现了反转。

    迭代法的思路不难理解,大家可以用代码做个参考。

    迭代法实现的性能也是很强大,空间消耗比递归法少很多👇

    Ⅵ 求链表的中间节点(#876)

    我们先看看这道题的具体描述。

    我们要求一个链表的中间结点。经过前面几道题,我们知道了有一个很巧妙的遍历方法,就是快慢指针,这道题简直就是为快慢指针而设的。

    要求中间结点,那我们令快指针和慢指针都指向头结点,然后开始遍历。快指针一次走两格,慢指针一次走一格,这样直到快指针遍历到链表末端。如果是奇数个,那快指针刚好指向尾结点,fast.next = null。如果是偶数个,那快指针就指向了尾结点的下一个结点,也就是空,即 fast = null。

    所以,我们可以根据这两个边界条件,来判断快指针是否遍历完了整个链表。当快指针遍历完链表之后,由于它每次都比慢指针多走一步,所以慢指针最后指向的就是我们要求的中间结点。

    我们直接来看代码👇

    /* * @lc app=leetcode.cn id=876 lang=java * * [876] 链表的中间结点 */ // @lc code=start /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode middleNode(ListNode head) { ListNode fast = head; ListNode slow = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; } return slow; } } // @lc code=end

    为方便大家看,我再截个图👇

    性能测试如下👇

    OK,这就是链表的几个重要操作,掌握熟练这些题,我们就在通向链表自由之路上前进了一大截。

    Processed: 0.017, SQL: 8