Day1: 单链表的六大解题思路
21. 合并两个有序链表
生成一个哑节点current,比较list1和 list2 指针,将较小的值的指针拼接在current后,current指针前进,最后将剩余的指针全拼在current后。
利用哑节点可以避免处理空指针的情况,减少不必要的麻烦。
var mergeTwoLists = function(list1, list2) {// 生成哑节点let dummyNode = new ListNode(-1)let current = dummyNodewhile (list1 && list2) {// 将较小的值的指针拼接在current后if (list1.val < list2.val) {current.next = list1list1 = list1.next} else {current.next = list2list2 = list2.next}// 指针继续前进current = current.next}// 将剩余的指针全拼在current后current.next = list1 || list2return dummyNode.next};
23.合并k个链表
最小堆
由于js没有堆的数据结构,所以将链表所有节点存入一个数组中,再将数组排序,再从数组重新生成一个完整链表
优先队列
pq中的元素个数最多是k,所以一次poll或者add方法的时间复杂度是O(logk);所有的链表节点都会被加入和弹出pq,所以算法整体的时间复杂度是
**O(Nlogk)**,其中**k**是链表的条数,**N**是这些链表的节点总数。
var mergeKLists = function(lists) {let arr = []// 将所有【链表节点】存入数组,不能对lists使用flat的数组方法lists.map(i => {let node = iwhile (node) {arr.push(node.val)node = node.next}})// 排序arr.sort((a, b) => a - b)let dummyNode = new ListNode(-1)let current = dummyNode// 重新生成链表arr.map(num => {current.next = new ListNode(num)current = current.next})return dummyNode.next};
归并合并排序链表
输入k个排序链表,将其拆分成前k/2个链表和后k/2个链表,这前k/2个链表和后k/2个链表分别合并成两个排序的链表,再将两个排序的链表合并,所有链表就都合并了。
- 下面代码中递归调用栈的深度为O(logn),所以空间复杂度为O(logn)
- 因为使用的是归并排序的思路,所以它的时间复杂度为O(nlogn)
var mergeKLists = function(lists) {if (!lists.length) {return null}/* 合并两个链表为升序链表 */var mergeTwoLists = function(list1, list2) {// 生成哑节点let dummyNode = new ListNode(-1)let current = dummyNodewhile (list1 && list2) {// 将较小的值的指针拼接在current后if (list1.val < list2.val) {current.next = list1list1 = list1.next} else {current.next = list2list2 = list2.next}// 指针继续前进current = current.next}// 将剩余的指针全拼在current后current.next = list1 || list2return dummyNode.next};const mergeLists = (lists, start = 0, end = lists.length) => {// 仅有一个节点时直接返回 lists, 无需拆分if (start + 1 === end) {return lists[start]}// 拆分成两个链表,分别进行排序const mid = Math.floor((start + end)/2) // 也可写作 const mid = (start + end) >> 1const l1 = mergeLists(lists, start, mid)const l2 = mergeLists(lists, mid, end)return mergeTwoLists(l1, l2)}// 前闭后开区间return mergeLists(lists, 0, lists.length)};
19.删除链表的倒数第 N 个结点
生成哑结点,让快指针先走N步,快指针到达时,慢指针到达倒数N + 1结点处,再删除
var removeNthFromEnd = function(head, n) {let dummyNode = new ListNode(-1)dummyNode.next = headlet fast = dummyNodelet slow = dummyNode// 快指针先走 N 步while (n --) {fast = fast.next}// 快指针到达终点,慢指针到达倒数N + 1处while (fast && fast.next) {fast = fast.nextslow = slow.next}// 删除slow.next = slow.next.nextreturn dummyNode.next};
876. 链表的中间结点
设置快慢指针,让快指针速度是慢指针的2倍,快指针到达时,慢指针刚好在链表中点。
var middleNode = function(head) {let slow = fast = headwhile (fast && fast.next) {fast = fast.next.nextslow = slow.next}return slow};
环形链表II - 环位置
设置快(2)慢(1)指针,若快慢指针相遇,则为环形
可以看到,当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置
var detectCycle = function(head) {let slow = headlet fast = headlet isLoop = falsewhile (fast && fast.next) {fast = fast.next.nextslow = slow.nextif (slow === fast) {// 快慢指针相遇,则为环形isLoop = true// 跳出循环break}}// 确认是环形,找出posif (isLoop) {slow = headwhile (slow !== fast) {slow = slow.nextfast = fast.next}}return isLoop ? slow : null};
160. 相交链表
我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,
让 p2 遍历完链表 B 之后开始遍历链表 A,
这样相当于「逻辑上」两条链表接在了一起。
如果这样进行拼接,就可以让 p1 和 p2 同时进入公共部分,也就是同时到达相交节点
var getIntersectionNode = function(headA, headB) {let p1 = headAlet p2 = headBwhile (p1 != p2) {// 遍历完后遍历另一支链表p1 = p1 ? p1.next : headBp2 = p2 ? p2.next : headA}return p1};
Day2: 递归反转链表
206. 反转整个链表
递归
将递归函数定义为:输入一个结点head,将以【head为起点】的链表反转,返回反转后的头节点.
递归函数需要base case,当链表仅有一个节点时,反转后也是自己,直接返回即可
var reverseList = function(head) {// 链表仅有一个节点,直接返回if (head === null || head.next === null) {return head}// 将head.next为起点的链表反转,返回反转后的头结点const last = reverseList(head.next)head.next.next = headhead.next = nullreturn last};
迭代

var reverseList = function(head) {let pre = nulllet current = headwhile (current) {// 临时变量存储未反转的链表起点const temp = current.nextcurrent.next = prepre = currentcurrent = temp}return pre};
反转链表的前n个节点
递归函数定义: 反转以head为起点的前n个节点链表,返回反转后的头节点
basecase:
/*** A -> B -> C -> D* A <- B C -> D* A -> C*/let rest = nullvar reverseN = function(head, n) {if (n === 1) {// 返回当前节点// 并记录 n + 1 的节点位置(未反转部分的链表起点)rest = head.nextreturn head}// 反转 head.next 为起点的 n - 1 个节点const last = reverseN(head.next, n - 1)head.next.next = head// 反转后的节点和未反转的节点连接起来head.next = restreturn last};
92.反转链表的一部分节点
basecase:当 left 为 1 时,相当于反转前N个节点。
反转head起始的[left,right]区间就相当于反转 head.next 起始的[left - 1, right - 1]区间
var reverseBetween = function(head, left, right) {if (left === 1) {return revereN(head, right)}// 以 head.next 为下标 1,那就是 反转head.next为起点的 left - 1, right - 1 区间head.next = reverseBetween(head.next, left - 1, right - 1)return head};// 反转前 N 个链表,相当于 left = 1 开始let rest = nullconst revereN = (head, n) => {if (n === 1) {rest = head.nextreturn head}const last = revereN(head.next, n - 1)head.next.next = headhead.next = restreturn last}
25. K个一组反转链表
记录下每k个一组的起始节点和结束节点,不足 k 个的直接返回head。
将反转起始节点和结束节点的链表并拼接到剩余未反转的链表上
var reverseKGroup = function(head, k) {// 用start 和 end 记录每k个需要反转的起始节点、结束起点let start = end = head// 不足 k 个即end 到达了 null,直接返回 headfor (let i = 0; i < k; i ++) {if (end === null) {return head}// 得到第 k 个节点 endend = end.next}// 反转 start 到 end 间的链表const last = reverse(start, end)start.next = reverseKGroup(end, k)return last};// 前面的反转整个链表:其实就是反转 head 到 null 间的链表// 迭代const reverse = (start, end = null) => {let pre = nulllet current = startwhile (current !== end) {const temp = current.nextcurrent.next = prepre = currentcurrent = temp}return pre}
234.回文链表
找出中点,将中点后的节点反转,记录为右指针,比较左指针(起始)和右指针的节点值,不同则返回false
// 反转链表const reverse = (head) => {if (head === null || head.next == null) {return head}const last = reverse(head.next)head.next.next = headhead.next = nullreturn last}var isPalindrome = function(head) {// 找出中点let fast = slow = headwhile (fast && fast.next) {fast = fast.next.nextslow = slow.next}// 注意奇数链表,中点多走一步if (fast !== null) {slow = slow.next}let left = headlet right = reverse(slow)while (right) {if (left.val != right.val) {return false}left = left.nextright = right.next}return true};
