通常链表操作指针操作,可能还不止一个,还要防止被边界条件坑一下,其实,这种费力不讨好的操作,可以被 递归 替代,递归本的质是暴力穷举,算不上算法,只能是编码的实现方式。对电脑来说,两者都是要求它老老实实干活,但是 递归 对于人脑更好理解。

一. 基础操作

0.快慢指针 正确性证明

快慢指针是一种常见的解法,但是我对快慢指针总是不相信:在连续的世界快慢指针一定会相遇,但是在离散世界是有可能永远不会相遇的。

这里有证明:
https://zhuanlan.zhihu.com/p/60736361
image.png
<线性同余方程 >
是数学定理,他说 当 快指针步长为2,慢指针步长为1时,不管圈大小,起点如何都会追上!这里不在细究,但是终于相信

1. 由数组生成链表

1)递归生成,代码更简单

  1. function ListNode(val, next) {
  2. this.val = (val === undefined ? 0 : val)
  3. this.next = (next === undefined ? null : next)
  4. }
  5. function getListFromArray(arr) {
  6. if (!arr.length) {
  7. return null;
  8. }
  9. const head = new ListNode(arr.shift());
  10. head.next = getListFromArray(arr);
  11. return head;
  12. }

2)常规生成

dumyHead 的漂亮亮相

  1. function ListNode(val, next) {
  2. this.val = (val===undefined ? 0 : val)
  3. this.next = (next===undefined ? null : next)
  4. }
  5. function List(nums) {
  6. if (nums.length === 0) {
  7. return null;
  8. }
  9. let dumyHead = new ListNode();
  10. let p = dumyHead;
  11. for (let num of nums) {
  12. let cur = new ListNode(num, null);
  13. p.next = cur;
  14. p = p.next;
  15. }
  16. return dumyHead.next;
  17. }
  18. new List([4,2,1,3]);

如果不用 dummyhead 就会很繁琐,代码也不好理解:

  1. function List(nums) {
  2. if (nums.length === 0) {
  3. return null;
  4. }
  5. let head;
  6. let pre;
  7. for (let num of nums) {
  8. let cur = new ListNode(num, null);
  9. if (!pre) {
  10. head = cur;
  11. pre = cur;
  12. } else {
  13. pre.next = cur;
  14. pre = cur;
  15. }
  16. }
  17. return head;
  18. }

2. 翻转链表

剑指 Offer 24. 反转链表

抛开繁琐的而且很容错的指针操作,用 递归 简直清爽太多了。

  1. function reverseList(head) {
  2. function recur(head, pre) {
  3. if (head === null) {
  4. return pre;
  5. }
  6. let newHead = recur(head.next, head);
  7. head.next = pre; // head 是逆转之后链表的尾部
  8. return newHead;
  9. }
  10. return recur(head, null);
  11. }

25. K 个一组翻转链表

在 24 翻转链表的基础上:

  1. var reverseKGroup = function(head, k) {
  2. function reverseList(head) {
  3. if (!head) {
  4. return null;
  5. }
  6. let p = head;
  7. for (let i = 1; i < k; i++) {
  8. if (p.next) {
  9. p = p.next
  10. } else { // 不够k个,原样返回
  11. return head;
  12. }
  13. }
  14. let nextHead = p.next;
  15. p.next = null;
  16. let [_head, _tail] = _reverseList(head);
  17. _tail.next = reverseList(nextHead);
  18. return _head;
  19. };
  20. return reverseList(head);
  21. };
  22. function _reverseList(head) {
  23. function recur(head, pre) {
  24. if (head === null) {
  25. return pre;
  26. }
  27. let _newHead = recur(head.next, head);
  28. head.next = pre; // head 是逆转之后链表的尾部
  29. return _newHead;
  30. }
  31. let newHead = recur(head, null);
  32. return [newHead, head];
  33. }

删除元素

83. 删除排序链表中的重复元素

还是使用 递归的方式去做,安全而保险

  1. var deleteDuplicates = function(head) {
  2. function dfs(node) {
  3. if (!node) {
  4. return;
  5. }
  6. while (node.next && (node.val === node.next.val)) { // 前序操作!!
  7. node.next = node.next.next;
  8. }
  9. dfs(node.next);
  10. }
  11. dfs(head);
  12. return head;
  13. };

而使用传统指针就麻烦多了。

  1. var deleteDuplicates = function(head) {
  2. if (!head || !head.next) {
  3. return head;
  4. }
  5. let p = head;
  6. while(p && p.next) { // 不知道要循环多少次,就用 while
  7. if (p.val === p.next.val) { // 有重复就删除
  8. p.next = p.next.next;
  9. } else { // else 特别关键,只有不同时,才移动指针,否则一直比
  10. p = p.next;
  11. }
  12. }
  13. return head;
  14. };

82. 删除排序链表中的重复元素 II

感觉细节太多了:要区分是否有重复区间,有就要删除

  1. var deleteDuplicates = function(head) {
  2. let dummyHead = new ListNode(); // 安装假头,以防第一个元素就是重复元素
  3. dummyHead.next = head;
  4. let q = dummyHead;
  5. let p = head;
  6. let toDelete = false;
  7. while (p && p.next) {
  8. while (p.next && p.val === p.next.val) {
  9. toDelete = true;
  10. p = p.next;
  11. }
  12. if (toDelete) {
  13. q.next = p.next;
  14. p = p.next; // 一开始丢了这个
  15. toDelete = false;
  16. } else {
  17. q = p;
  18. p = p.next;
  19. }
  20. }
  21. return dummyHead.next;
  22. };

上面是引入了第三方变量,其实还有一种方式:

  1. var deleteDuplicates = function(head) {
  2. let dummyHead = new ListNode(); // 安装假头,以防第一个元素就是重复元素
  3. dummyHead.next = head;
  4. let q = dummyHead;
  5. let p = head;
  6. while (p && p.next) {
  7. while (p.next && p.val === p.next.val) { // 跳出 while 循环时,p指向最后一个重复元素
  8. p = p.next;
  9. }
  10. // if (toDelete) {
  11. if (q.next !== p) { // p 有移动过,代表有重复元素
  12. q.next = p.next;
  13. p = p.next; // 一开始丢了这个
  14. // toDelete = false;
  15. } else {
  16. q = p;
  17. p = p.next;
  18. }
  19. }
  20. return dummyHead.next;
  21. };

其实指针操作极其容易出错,其实可以使用递归的操作:(很棒!)
同样要区分是否有 重复区间:

  1. var deleteDuplicates = function(head) {
  2. if (!head) {
  3. return null;
  4. }
  5. let dummyHead = new ListNode(); // 安装假头,以防第一个元素就是重复元素
  6. dummyHead.next = head;
  7. if (head.next && (head.val === head.next.val)) {
  8. while (head.next && (head.val === head.next.val)) {
  9. head = head.next;
  10. }
  11. dummyHead.next = deleteDuplicates(head.next); // 处理1
  12. } else {
  13. head.next = deleteDuplicates(head.next); // 处理2
  14. }
  15. return dummyHead.next;
  16. };

合并链表

合并两个有序链表

递归写法:(很简洁)

  1. function mergeTwoList(l1, l2) {
  2. if (!(l1 && l2)) {
  3. return l1 || l2;
  4. }
  5. if (l1.val < l2.val) {
  6. l1.next = mergeTwoList(l1.next, l2);
  7. return l1;
  8. } else {
  9. l2.next = mergeTwoList(l1, l2.next);
  10. return l2;
  11. }
  12. }

对比一般写法:(很复杂,容易错)

  1. function mergeTwoList(l1, l2) { // 合并 lists 的套路:一个 while 循环,加四个 if 条件
  2. const dummyHead = new ListNode();
  3. let p = dummyHead;
  4. while (l1 || l2) { // 两个链表还没遍历完
  5. if (l1 && l2) {
  6. if (l1.val < l2.val) {
  7. p.next = l1;
  8. l1 = l1.next;
  9. } else {
  10. p.next = l2;
  11. l2 = l2.next
  12. }
  13. } else if (l1 && !l2) {
  14. p.next = l1;
  15. l1 = l1.next;
  16. } else if (!l1 && l2) {
  17. p.next = l2;
  18. l2 = l2.next;
  19. }
  20. p = p.next;
  21. }
  22. p = null; // 结尾
  23. return dummyHead.next;
  24. }

23. 合并K个升序链表

  1. var mergeKLists = function(lists) {
  2. let n = lists.length;
  3. if (n === 1) {
  4. return lists[0]
  5. }
  6. if (n === 0) {
  7. return null;
  8. }
  9. const finalList = lists.reduce((pre, cur) => mergeTwoList(pre, cur));
  10. return finalList;
  11. };

148. Sort List

image.png
归并排序的基调定下来之后
1)先使用快慢指针把 链表分成两部分
2)然后递归,最后知道链表长度 <= 1 即可
3)归并

  1. var sortList = function(head) {
  2. if (!head) {
  3. return null;
  4. }
  5. if (!head.next) {
  6. return head;
  7. }
  8. let slow = head;
  9. let fast = head.next;
  10. while (fast) {
  11. if (!fast.next) {
  12. break;
  13. }
  14. slow = slow.next;
  15. fast = fast.next.next;
  16. }
  17. let l2 = slow.next
  18. slow.next = null;
  19. let l1 = head;
  20. l1 = sortList(l1);
  21. l2 = sortList(l2)
  22. return mergeTwoList(l1, l2);
  23. };

结果还可以:
image.png
代码写了一大坨,感觉很冗长。

二、else

2. 两数相加

  1. var addTwoNumbers = function(l1, l2) {
  2. let head = new ListNode(0); // 假头
  3. let p = head;
  4. let p1 = l1;
  5. let p2 = l2;
  6. let carry = 0;
  7. while (p1 || p2 || carry) {
  8. sum = (p1 ? p1.val : 0) + (p2 ? p2.val : 0) + carry;
  9. carry = (sum >= 10) ? 1 : 0;
  10. sum = sum % 10;
  11. // 有了假头,才能串联的经典操作
  12. p.next = new ListNode(sum);
  13. p = p.next;
  14. p1 = p1 ? p1.next : null;
  15. p2 = p2 ? p2.next : null;
  16. }
  17. return head.next;
  18. };

24. 两两交换链表中的节点

链表操作,指针很多,边界值要考虑清楚。

  1. var swapPairs = function(head) {
  2. if (!head || !head.next) {
  3. return head;
  4. }
  5. let dummyHead = new ListNode();
  6. dummyHead.next = head;
  7. let parent = dummyHead;
  8. let p1 = parent.next;
  9. let p2 = p1 ? p1.next : null;
  10. let child = p2 ? p2.next : null;
  11. while (p1 && p2) {
  12. parent.next = p2;
  13. p2.next = p1;
  14. p1.next = child;
  15. parent = p1;
  16. p1 = child;
  17. p2 = p1 ? p1.next : null;
  18. child = p2 ? p2.next : null;
  19. }
  20. return dummyHead.next;
  21. };

剑指 Offer 22. 链表中倒数第k个节点

总结 while 循环的写法:

  1. var getKthFromEnd = function(head, k) { // 链表就是过河的卒子不能后退
  2. let p = head;
  3. let q = p;
  4. while ((k - 1) && p) { // 多保护条件
  5. p = p.next;
  6. k--;
  7. }
  8. while (p && p.next) { // 指针在移动时,临界退出是否指向最后一个??
  9. q = q.next;
  10. p = p.next
  11. }
  12. return q;
  13. };

image.png
这样p指向尾部null时,q 会错过它原来的节点。
解决方式很简单,一开始P 和 q 拉的距离加1就行了。
image.png

86. 分隔链表

拼接链表,注意老链表关系的终结。

  1. var partition = function(head, x) {
  2. let smallList = new ListNode();
  3. let bigList = new ListNode();
  4. let smallHead = smallList;
  5. let bigHead = bigList;
  6. while (head) {
  7. if (head.val < x) {
  8. smallHead.next = head;
  9. smallHead = smallHead.next;
  10. } else {
  11. bigHead.next = head;
  12. bigHead = bigHead.next;
  13. }
  14. head = head.next;
  15. }
  16. bigHead.next = null; // 堵上漏洞,这个 next 指向原来链表某个节点,会在新链表中构成环
  17. smallHead.next = bigList.next;
  18. return smallList.next;
  19. };

138. 复制带随机指针的链表

1)机械的做法:
两次遍历:第一次,记录老节点和新节点的映射,第二次遍历,将老节点的社会关系告诉新节点

  1. var copyRandomList = function(head) { // 忽略了map这种结构,可以以object为key
  2. if (head === null) {
  3. return null;
  4. }
  5. const cachedNode = new Map();
  6. let p = head;
  7. let p_copy = null;
  8. while(p) {
  9. p_copy = new ListNode(p.val);
  10. cachedNode.set(p, p_copy);
  11. p = p.next;
  12. }
  13. // let new_head = null;
  14. p = head;
  15. p_copy = null;
  16. while (p) {
  17. p_copy = cachedNode.get(p);
  18. p_copy.next = p.next ? cachedNode.get(p.next) : null; // 这里有问题
  19. p_copy.random = p.random ? cachedNode.get(p.random) : null;
  20. p = p.next;
  21. // if (!new_head) {
  22. // new_head = p_copy;
  23. // }
  24. }
  25. // return new_head;
  26. return cachedNode.get(head); // 这个取法很巧妙
  27. }

2)递归算法
很简单,也使用map作为已经完成复制的记录,不断递归(复制节点值,以及关系)

  1. var copyRandomList = function(head, cachedNodeMap = new Map()) { // 忽略了map这种结构,可以以object为key
  2. if (head === null) {
  3. return null;
  4. }
  5. if (!cachedNodeMap.get(head)) { // 没有被复制过
  6. let copy = new ListNode(head.val);
  7. cachedNodeMap.set(head, copy); // 先设置,这很关键
  8. copy.next = copyRandomList(head.next, cachedNodeMap);
  9. copy.random = copyRandomList(head.random, cachedNodeMap);
  10. }
  11. // p.next = head.next ? new ListNode(head.next) : null;
  12. // p.random = head.random ? new ListNode(head.random) : null;
  13. return cachedNodeMap.get(head);
  14. }

对于递归算法,我一开始不放心的是会不会遍历链表全部的问题。其实,由于 当前指针 p 有 两个 指向:p.next 和 p.random,p.random 乱指虽然不保证全部遍历,但是 p.next 一定会保证的!

1485. 克隆含随机指针的二叉树

和 138 一样的解题思路

  1. var copyRandomBinaryTree = function(root) {
  2. let visitedMap = new Map();
  3. function dfs(node) { // 递归返回的是 复制后的node
  4. if (!node) {
  5. return node;
  6. }
  7. // 先序操作
  8. if (visitedMap.has(node)) {
  9. return visitedMap.get(node);
  10. }
  11. const newNode = new NodeCopy(node.val); // 题意要求新 class
  12. visitedMap.set(node, newNode); // 顺序放在这里才对
  13. // 这个三个大哥的顺序无所谓。
  14. newNode.left = dfs(node.left);
  15. newNode.random = dfs(node.random);
  16. newNode.right = dfs(node.right);
  17. // visitedMap.set(node, newNode);
  18. return newNode;
  19. }
  20. return dfs(root);
  21. };

三、需要记忆的特殊题目

160. 相交链表

image.png
image.png

相交链表 和 平行链表的区别:
平行链表,在各自链表被遍历到末尾时,折回另一个链表的头部返回折时,不会相交。
image.png

  1. var getIntersectionNode = function(headA, headB) {
  2. if (!(headA && headB)) { // 有一个为空链表,则不相交
  3. return null;
  4. }
  5. let pA = headA;
  6. let pB = headB;
  7. let pA_switched = false;
  8. let pB_switched = false;
  9. while (pA !== pB) {
  10. if (pA_switched && pB_switched) {
  11. pA = pA.next;
  12. pB = pB.next;
  13. if (!pA && !pB) {
  14. return null;
  15. }
  16. continue;
  17. }
  18. if (pA.next) {
  19. pA = pA.next;
  20. } else {
  21. // pA = headB;
  22. if (!pA_switched) {
  23. pA = headB;
  24. pA_switched = true;
  25. }
  26. }
  27. if (pB.next) {
  28. pB = pB.next;
  29. } else {
  30. // pB = headA;
  31. if (!pB_switched) {
  32. pB = headA;
  33. pB_switched = true;
  34. }
  35. }
  36. }
  37. return pA;
  38. };

我这写的很复杂,但是现实中,我可能就写成这个球像了。
image.png
上面冗长的代码,都是没有考虑到下面这一点,不相交的链表,交换方向都指向末尾 null 时,可以终止 while 循环:
image.png
因此,也无需考虑是否交换过方向:

  1. var getIntersectionNode = function(headA, headB) {
  2. if (!(headA && headB)) { // 有一个为空链表,则不相交
  3. return null;
  4. }
  5. let pA = headA;
  6. let pB = headB;
  7. while (pA !== pB) { // 不会陷入死循环:如果两条链平行的话,pa pb 都是null,会跳出while
  8. if (pA) {
  9. pA = pA.next;
  10. } else {
  11. pA = headB;
  12. }
  13. if (pB) {
  14. pB = pB.next;
  15. } else {
  16. pB = headA;
  17. }
  18. }
  19. return pA;
  20. };

上述解法,有一种情况被掩盖了:
那就是 两个相交列表,在第一次遍历就完成结果了。

142. 环形链表 II

找环的节点

https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode-solution/
image.png
上面的等式能看出来是什么意思?能看出来吗?下面是结论:
image.png