通常链表操作指针操作,可能还不止一个,还要防止被边界条件坑一下,其实,这种费力不讨好的操作,可以被 递归 替代,递归本的质是暴力穷举,算不上算法,只能是编码的实现方式。对电脑来说,两者都是要求它老老实实干活,但是 递归 对于人脑更好理解。
一. 基础操作
0.快慢指针 正确性证明
快慢指针是一种常见的解法,但是我对快慢指针总是不相信:在连续的世界快慢指针一定会相遇,但是在离散世界是有可能永远不会相遇的。
这里有证明:
https://zhuanlan.zhihu.com/p/60736361
<线性同余方程 >
是数学定理,他说 当 快指针步长为2,慢指针步长为1时,不管圈大小,起点如何都会追上!这里不在细究,但是终于相信。
1. 由数组生成链表
1)递归生成,代码更简单
function ListNode(val, next) {this.val = (val === undefined ? 0 : val)this.next = (next === undefined ? null : next)}function getListFromArray(arr) {if (!arr.length) {return null;}const head = new ListNode(arr.shift());head.next = getListFromArray(arr);return head;}
2)常规生成
dumyHead 的漂亮亮相
function ListNode(val, next) {this.val = (val===undefined ? 0 : val)this.next = (next===undefined ? null : next)}function List(nums) {if (nums.length === 0) {return null;}let dumyHead = new ListNode();let p = dumyHead;for (let num of nums) {let cur = new ListNode(num, null);p.next = cur;p = p.next;}return dumyHead.next;}new List([4,2,1,3]);
如果不用 dummyhead 就会很繁琐,代码也不好理解:
function List(nums) {if (nums.length === 0) {return null;}let head;let pre;for (let num of nums) {let cur = new ListNode(num, null);if (!pre) {head = cur;pre = cur;} else {pre.next = cur;pre = cur;}}return head;}
2. 翻转链表
剑指 Offer 24. 反转链表
抛开繁琐的而且很容错的指针操作,用 递归 简直清爽太多了。
function reverseList(head) {function recur(head, pre) {if (head === null) {return pre;}let newHead = recur(head.next, head);head.next = pre; // head 是逆转之后链表的尾部return newHead;}return recur(head, null);}
25. K 个一组翻转链表
在 24 翻转链表的基础上:
var reverseKGroup = function(head, k) {function reverseList(head) {if (!head) {return null;}let p = head;for (let i = 1; i < k; i++) {if (p.next) {p = p.next} else { // 不够k个,原样返回return head;}}let nextHead = p.next;p.next = null;let [_head, _tail] = _reverseList(head);_tail.next = reverseList(nextHead);return _head;};return reverseList(head);};function _reverseList(head) {function recur(head, pre) {if (head === null) {return pre;}let _newHead = recur(head.next, head);head.next = pre; // head 是逆转之后链表的尾部return _newHead;}let newHead = recur(head, null);return [newHead, head];}
删除元素
83. 删除排序链表中的重复元素
还是使用 递归的方式去做,安全而保险
var deleteDuplicates = function(head) {function dfs(node) {if (!node) {return;}while (node.next && (node.val === node.next.val)) { // 前序操作!!node.next = node.next.next;}dfs(node.next);}dfs(head);return head;};
而使用传统指针就麻烦多了。
var deleteDuplicates = function(head) {if (!head || !head.next) {return head;}let p = head;while(p && p.next) { // 不知道要循环多少次,就用 whileif (p.val === p.next.val) { // 有重复就删除p.next = p.next.next;} else { // else 特别关键,只有不同时,才移动指针,否则一直比p = p.next;}}return head;};
82. 删除排序链表中的重复元素 II
感觉细节太多了:要区分是否有重复区间,有就要删除
var deleteDuplicates = function(head) {let dummyHead = new ListNode(); // 安装假头,以防第一个元素就是重复元素dummyHead.next = head;let q = dummyHead;let p = head;let toDelete = false;while (p && p.next) {while (p.next && p.val === p.next.val) {toDelete = true;p = p.next;}if (toDelete) {q.next = p.next;p = p.next; // 一开始丢了这个toDelete = false;} else {q = p;p = p.next;}}return dummyHead.next;};
上面是引入了第三方变量,其实还有一种方式:
var deleteDuplicates = function(head) {let dummyHead = new ListNode(); // 安装假头,以防第一个元素就是重复元素dummyHead.next = head;let q = dummyHead;let p = head;while (p && p.next) {while (p.next && p.val === p.next.val) { // 跳出 while 循环时,p指向最后一个重复元素p = p.next;}// if (toDelete) {if (q.next !== p) { // p 有移动过,代表有重复元素q.next = p.next;p = p.next; // 一开始丢了这个// toDelete = false;} else {q = p;p = p.next;}}return dummyHead.next;};
其实指针操作极其容易出错,其实可以使用递归的操作:(很棒!)
同样要区分是否有 重复区间:
var deleteDuplicates = function(head) {if (!head) {return null;}let dummyHead = new ListNode(); // 安装假头,以防第一个元素就是重复元素dummyHead.next = head;if (head.next && (head.val === head.next.val)) {while (head.next && (head.val === head.next.val)) {head = head.next;}dummyHead.next = deleteDuplicates(head.next); // 处理1} else {head.next = deleteDuplicates(head.next); // 处理2}return dummyHead.next;};
合并链表
合并两个有序链表
递归写法:(很简洁)
function mergeTwoList(l1, l2) {if (!(l1 && l2)) {return l1 || l2;}if (l1.val < l2.val) {l1.next = mergeTwoList(l1.next, l2);return l1;} else {l2.next = mergeTwoList(l1, l2.next);return l2;}}
对比一般写法:(很复杂,容易错)
function mergeTwoList(l1, l2) { // 合并 lists 的套路:一个 while 循环,加四个 if 条件const dummyHead = new ListNode();let p = dummyHead;while (l1 || l2) { // 两个链表还没遍历完if (l1 && l2) {if (l1.val < l2.val) {p.next = l1;l1 = l1.next;} else {p.next = l2;l2 = l2.next}} else if (l1 && !l2) {p.next = l1;l1 = l1.next;} else if (!l1 && l2) {p.next = l2;l2 = l2.next;}p = p.next;}p = null; // 结尾return dummyHead.next;}
23. 合并K个升序链表
var mergeKLists = function(lists) {let n = lists.length;if (n === 1) {return lists[0]}if (n === 0) {return null;}const finalList = lists.reduce((pre, cur) => mergeTwoList(pre, cur));return finalList;};
148. Sort List

归并排序的基调定下来之后
1)先使用快慢指针把 链表分成两部分
2)然后递归,最后知道链表长度 <= 1 即可
3)归并
var sortList = function(head) {if (!head) {return null;}if (!head.next) {return head;}let slow = head;let fast = head.next;while (fast) {if (!fast.next) {break;}slow = slow.next;fast = fast.next.next;}let l2 = slow.nextslow.next = null;let l1 = head;l1 = sortList(l1);l2 = sortList(l2)return mergeTwoList(l1, l2);};
二、else
2. 两数相加
var addTwoNumbers = function(l1, l2) {let head = new ListNode(0); // 假头let p = head;let p1 = l1;let p2 = l2;let carry = 0;while (p1 || p2 || carry) {sum = (p1 ? p1.val : 0) + (p2 ? p2.val : 0) + carry;carry = (sum >= 10) ? 1 : 0;sum = sum % 10;// 有了假头,才能串联的经典操作p.next = new ListNode(sum);p = p.next;p1 = p1 ? p1.next : null;p2 = p2 ? p2.next : null;}return head.next;};
24. 两两交换链表中的节点
链表操作,指针很多,边界值要考虑清楚。
var swapPairs = function(head) {if (!head || !head.next) {return head;}let dummyHead = new ListNode();dummyHead.next = head;let parent = dummyHead;let p1 = parent.next;let p2 = p1 ? p1.next : null;let child = p2 ? p2.next : null;while (p1 && p2) {parent.next = p2;p2.next = p1;p1.next = child;parent = p1;p1 = child;p2 = p1 ? p1.next : null;child = p2 ? p2.next : null;}return dummyHead.next;};
剑指 Offer 22. 链表中倒数第k个节点
总结 while 循环的写法:
var getKthFromEnd = function(head, k) { // 链表就是过河的卒子不能后退let p = head;let q = p;while ((k - 1) && p) { // 多保护条件p = p.next;k--;}while (p && p.next) { // 指针在移动时,临界退出是否指向最后一个??q = q.next;p = p.next}return q;};

这样p指向尾部null时,q 会错过它原来的节点。
解决方式很简单,一开始P 和 q 拉的距离加1就行了。
86. 分隔链表
拼接链表,注意老链表关系的终结。
var partition = function(head, x) {let smallList = new ListNode();let bigList = new ListNode();let smallHead = smallList;let bigHead = bigList;while (head) {if (head.val < x) {smallHead.next = head;smallHead = smallHead.next;} else {bigHead.next = head;bigHead = bigHead.next;}head = head.next;}bigHead.next = null; // 堵上漏洞,这个 next 指向原来链表某个节点,会在新链表中构成环smallHead.next = bigList.next;return smallList.next;};
138. 复制带随机指针的链表
1)机械的做法:
两次遍历:第一次,记录老节点和新节点的映射,第二次遍历,将老节点的社会关系告诉新节点
var copyRandomList = function(head) { // 忽略了map这种结构,可以以object为keyif (head === null) {return null;}const cachedNode = new Map();let p = head;let p_copy = null;while(p) {p_copy = new ListNode(p.val);cachedNode.set(p, p_copy);p = p.next;}// let new_head = null;p = head;p_copy = null;while (p) {p_copy = cachedNode.get(p);p_copy.next = p.next ? cachedNode.get(p.next) : null; // 这里有问题p_copy.random = p.random ? cachedNode.get(p.random) : null;p = p.next;// if (!new_head) {// new_head = p_copy;// }}// return new_head;return cachedNode.get(head); // 这个取法很巧妙}
2)递归算法
很简单,也使用map作为已经完成复制的记录,不断递归(复制节点值,以及关系)
var copyRandomList = function(head, cachedNodeMap = new Map()) { // 忽略了map这种结构,可以以object为keyif (head === null) {return null;}if (!cachedNodeMap.get(head)) { // 没有被复制过let copy = new ListNode(head.val);cachedNodeMap.set(head, copy); // 先设置,这很关键copy.next = copyRandomList(head.next, cachedNodeMap);copy.random = copyRandomList(head.random, cachedNodeMap);}// p.next = head.next ? new ListNode(head.next) : null;// p.random = head.random ? new ListNode(head.random) : null;return cachedNodeMap.get(head);}
对于递归算法,我一开始不放心的是会不会遍历链表全部的问题。其实,由于 当前指针 p 有 两个 指向:p.next 和 p.random,p.random 乱指虽然不保证全部遍历,但是 p.next 一定会保证的!
1485. 克隆含随机指针的二叉树
和 138 一样的解题思路
var copyRandomBinaryTree = function(root) {let visitedMap = new Map();function dfs(node) { // 递归返回的是 复制后的nodeif (!node) {return node;}// 先序操作if (visitedMap.has(node)) {return visitedMap.get(node);}const newNode = new NodeCopy(node.val); // 题意要求新 classvisitedMap.set(node, newNode); // 顺序放在这里才对// 这个三个大哥的顺序无所谓。newNode.left = dfs(node.left);newNode.random = dfs(node.random);newNode.right = dfs(node.right);// visitedMap.set(node, newNode);return newNode;}return dfs(root);};
三、需要记忆的特殊题目
160. 相交链表


相交链表 和 平行链表的区别:
平行链表,在各自链表被遍历到末尾时,折回另一个链表的头部返回折时,不会相交。
var getIntersectionNode = function(headA, headB) {if (!(headA && headB)) { // 有一个为空链表,则不相交return null;}let pA = headA;let pB = headB;let pA_switched = false;let pB_switched = false;while (pA !== pB) {if (pA_switched && pB_switched) {pA = pA.next;pB = pB.next;if (!pA && !pB) {return null;}continue;}if (pA.next) {pA = pA.next;} else {// pA = headB;if (!pA_switched) {pA = headB;pA_switched = true;}}if (pB.next) {pB = pB.next;} else {// pB = headA;if (!pB_switched) {pB = headA;pB_switched = true;}}}return pA;};
我这写的很复杂,但是现实中,我可能就写成这个球像了。
上面冗长的代码,都是没有考虑到下面这一点,不相交的链表,交换方向都指向末尾 null 时,可以终止 while 循环:
因此,也无需考虑是否交换过方向:
var getIntersectionNode = function(headA, headB) {if (!(headA && headB)) { // 有一个为空链表,则不相交return null;}let pA = headA;let pB = headB;while (pA !== pB) { // 不会陷入死循环:如果两条链平行的话,pa pb 都是null,会跳出whileif (pA) {pA = pA.next;} else {pA = headB;}if (pB) {pB = pB.next;} else {pB = headA;}}return pA;};
上述解法,有一种情况被掩盖了:
那就是 两个相交列表,在第一次遍历就完成结果了。
142. 环形链表 II
找环的节点
https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode-solution/
上面的等式能看出来是什么意思?能看出来吗?下面是结论:

