链表算法题解
1. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
当 l1
和 l2
都不是空链表时,判断 l1
和 l2
哪一个链表的头节点的值更小,将较小值的节点添加到结果链表里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。
在循环终止的时候, l1
和 l2
至多有一个是非空的。将非空链表接在合并链表的后面,并返回合并链表即可。
var mergeTwoLists = function (list1, list2) {
const list = new ListNode(0)
let tail = list
while (list1 && list2) {
if (list1.val > list2.val) {
tail.next = list2
list2 = list2.next
} else {
tail.next = list1
list1 = list1.next
}
tail = tail.next
}
tail.next = list1 || list2;
return list.next;
};
2. 删除排序链表中的重复元素
给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
输入:head = [1,1,2,3,3] 输出:[1,2,3]
指定cur
指针指向头部 head
,当 cur
和 cur.next
的存在为循环结束条件,当二者有一个不存在时说明链表没有去重复的必要了,当 cur.val
和 cur.next.val
相等时说明需要去重,则将 cur
的下一个指针指向下一个的下一个,这样就能达到去重复的效果,如果不相等则 cur
移动到下一个位置继续循环。
var deleteDuplicates = function(head) {
let curr = head
while(curr && curr.next){
if(curr.val === curr.next.val){
curr.next = curr.next.next
} else {
curr = curr.next
}
}
return head
};
3. 环形链表
给你一个链表的头节点 head
,判断链表中是否有环。如果链表中存在环 ,则返回 true
。 否则,返回 false
。
输入:head = [1,2,3,4,5], pos = 3 输出:true 解释:链表中有一个环,其尾部连接到第三个节点。
定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head
,而快指针在位置 head.next
。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
var hasCycle = function (head) {
if (head == null || head.next == null) {
return false;
}
let slow = head
let fast = head.next
while (slow != fast) {
if (slow === null || fast === null) {
return false
}
slow = slow.next
if (!fast.next) {
return false
}
fast = fast.next.next
}
return true
};
4. 相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
输入:listA = [1,2,3,4,5], listB = [2,3,4,5] 输出:Intersected at 3 解释:相交节点的值为 3 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [1,2,3,4,5],链表 B 为 [2,3,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 1 个节点。
设第一个公共节点为 node
,**链表 headA
**的节点数量为 a
,**链表 headB
**的节点数量为b
,两链表的公共尾部的节点数量为 c
,则有:
- 头节点
headA
到node
前,共有a - c
个节点; - 头节点
headB
到node
前,共有b - c
个节点;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kvkvb8E0-1660211264123)(E:\Web\test\Notes\数据结构与算法\链表\images\相交链表.png)]
考虑构建两个节点指针 A
, B
分别指向两链表头节点 headA
, headB
,做如下操作:
-
指针 A 先遍历完链表
headA
,再开始遍历链表headB
,当走到node
时,共走步数为:a + (b - c)
-
指针 B 先遍历完链表
headB
,再开始遍历链表headA
,当走到node
时,共走步数为:b + (a - c)
如下式所示,此时指针 A
, B
重合,并有两种情况:
a + (b - c) = b + (a - c)
- 若两链表 有 公共尾部 (即
c > 0
) :指针A
,B
同时指向第一个公共节点node
。
- 若两链表 无 公共尾部 (即
c = 0
) :指针A
,B
同时指向null
。
因此返回A
或者B
即可。
var getIntersectionNode = function (headA, headB) {
let nA = headA,nB = headB
while (nA != nB) {
nA = nA === null ? headB : nA.next
nB = nB === null ? headA : nB.next
}
return nA
};
5. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807
将两个链表看成是相同长度的进行遍历,如果一个链表较短则在前面补 0,比如 987 + 23 = 987 + 023 = 1010
每一位计算的同时需要考虑上一位的进位问题,而当前位计算结束后同样需要更新进位值
如果两个链表全部遍历完毕后,进位值为 1,则在新链表最前方添加节点 1
var addTwoNumbers = function(l1, l2) {
let pre = new ListNode(0)
let cur = pre
let carry = 0
while(l1 !== null || l2 !== null || carry !== 0){
l1Val = l1 !== null ? l1.val : 0
l2Val = l2 !== null ? l2.val : 0
sumVal = l1Val+l2Val+carry
carry = parseInt(sumVal/10)
const sumNode = new ListNode(sumVal%10)
cur.next = sumNode
cur = sumNode
if(l1 !== null) l1 = l1.next
if(l2 !== null) l2 = l2.next
}
return pre.next
};
6. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
- 设定双指针
l1
和l2
,初始都指向虚拟节点head
- 移动
l2,直到
l1
与l2
之间相隔的元素个数为n
- 同时移动
l1
与l2
,直到l2
指向的为NULL
- 将
l1
的下一个节点指向下下个节点
var removeNthFromEnd = function (head, n) {
let l1 = head, l2 = head
// 让l2和l1相差n个节点
for (let i = 0; i < n; i++) {
if (l2.next) {
l2 = l2.next
} else {
return head.next
}
}
// 两个指针同步往后移动,一直到l2为null时
while (l2.next !== null) {
l1 = l1.next
l2 = l2.next
}
l1.next = l1.next.next
return head
};
7. 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
输入:head = [1,2,3,4] 输出:[2,1,4,3]
这里需要用到3个指针,l0
、l1
、l2
,其中l0
表示l1
的前一个指针。
假设链表为 :1->2->3->4
这里,我们新建一个节点来表示头节点,方便后续初始化链表,于是链表变为:
0->1->2->3->4
迭代时,每次处理两个节点,所以有
- 第一轮
l1
指向1,l2
指向2,l0
指向0。 - 第二轮
l1
指向3,l2
指向4,l0
指向1。 - 第三轮
l1
指向null
(迭代完成)
我们通过 l1.next = l2.next
,l2.next = l1
就交换了两个节点,于是1->2
变为 2->1
。但是这里有一个细节,原l0
指向的下一个节点为1,这边需要初始化链表,将它的指向修改为2,完成第一轮后的链表如下:
0->2->1->3->4
此时,l0
指向0
,l1
指向1
,l2
指向2
。当然,我们在链表修改完成后指针也需要l0
初始化,将它变为下一轮循环体的前一个节点,也就是3->4
的前一个节点1
,方便下一个循环。
思路图:
细节实现图:
var swapPairs = function (head) {
let pre = new ListNode(0)
pre.next = head
let l1 = pre, l2 = pre, l0 = pre
while (l1.next !== null && l1.next.next !== null) {
// l1移动一步,l2移动两步,l0暂时不动
l1 = l1.next
l2 = l1.next
// 进行节点交换
l1.next = l2.next
l2.next = l1
//初始化链表,让l0指向交换后的前一个节点(0->1->2 交换后 0->1且2->1 ,需要让0->2 )
l0.next = l2
//初始化指针,让l0、l2同时指向l1
l2 = l1
l0 = l1
}
return pre.next
};
8. 旋转链表
给你一个链表的头节点 head
,旋转链表,将链表每个节点向右移动 k
个位置。
输入:head = [1,2,3,4,5], k = 2 输出:[4,5,1,2,3]
这里需要先算下长度count
,同时也让指针(tail
)停留在最后一个结点处,然后算出新的k
(k=k % count
), 即是将要旋转的结点个数。
细节:当 k==0
时,链表旋转完成后其实是没有变化的,无需执行后边的算法直接返回链表就行。
接下来,形成闭环;即让尾节点(tail
)指向head,这里它向右边旋转k
个则等同于让倒数第k
个成为新的头节点,所以再从当前节点(tail
)走一次,找到倒数第count-k
的前一个节点,将它的next
指向为null
即完成了链表的旋转。
细节:需要更新头节点
流程实现图:
var rotateRight = function (head, k) {
if(head === null || head.next === null || k===0) return head
let tail = head
let count = 1
while (tail.next) {
tail = tail.next
count++
}
k = k % count
if (k === 0) return head
tail.next = head
let index = 0
while (index++ < count - k) {
tail = tail.next
}
let newHead = tail.next
tail.next = null
return newHead
};
9. 合并K个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6
在了解了如何合并两个有序链表后的基础上,我们使用一个变量 target
来维护以及合并链表,第 i
次循环就把第 i
个链表和 target
合并,答案保存到 target
中。
如何合并两个升序链表请查看 目录1
var mergeKLists = function (lists) {
if (lists.length == 0) return null
if (lists.length === 1) return lists[0]
let target = null
for (let i = 0; i < lists.length; i++) {
target = merge(target, lists[i])
}
return target
};
function merge(list1, list2) {
const list = new ListNode(0)
let tail = list
while (list1 && list2) {
if (list1.val > list2.val) {
tail.next = list2
list2 = list2.next
} else {
tail.next = list1
list1 = list1.next
}
tail = tail.next
}
tail.next = list1 || list2;
return list.next;
}
10. K 个一组翻转链表
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5] 输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5]
- 将链表通过k分为一个个的子链表(需要翻转的链表部分)
- 需记录翻转链表前驱和后继,方便翻转完成后把已翻转部分和未翻转部分连接起来
- 初始需要两个变量
pre
和end
,pre
代表待翻转链表的前驱,end
代表待翻转链表的末尾 - 通过
pre
可以找到待翻转链表的开头start,通过end
找到待翻转链表的后继 - 过程中,当
end
无法进行的时候代表链表已完成反转
var reverseKGroup = function (head, k) {
let list = new ListNode(0)
list.next = head
let pre = list, end = list
while (end.next) {
for (let i = 0; i < k && end; i++) {
end = end.next
}
if (end == null) break;
// 确认start 和 end 后
let start = pre.next,next = end.next
end.next = null
pre.next = reverse(start)
start.next = next
pre = start
end = pre
}
return list.next
};
function reverse(head) {
let pre = null, curr = head
while (curr) {
let next = curr.next
curr.next = pre
pre = curr
curr = next
}
return pre
}