理论基础
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
链表的类型
单链表:上面的就是单链表
双链表
- 每个节点有两个指针域,一个指向下一个节点,一个指向上一个节点
- 既可以向前查询也可以向后查询
循环链表
链表的存储方式
数组是在内存中是连续分布的,但链表在内存中不是连续分布的
链表是通过指针域的指针链接在内存中各个节点,所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理
图中所示链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起
链表的定义
面试的时候,要自己手写链表
Go定义链表节点方式如下所示:
1
2
3
4
5
|
// 单链表
type ListNode struct {
Val int // 节点上存储的元素
Next *ListNode // 指向下一个节点的指针
}
|
链表的操作
删除节点:删除D节点,如图所示:
只要将C节点的next指针指向E节点就可以了
添加节点
可以看出链表的增添和删除都是O(1)操作,不会影响到其他节点
但如果要删除最后一个节点,则需要从头节点查找到最后一个节点的前一个节点然后通过next指针进行删除操作,查找的时间复杂度是O(n)
性能分析
链表和数组的特性对比,如图所示:
- 数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组
- 链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景
移除链表元素
203. 移除链表元素 - 力扣(LeetCode)
给定一个链表的头节点和一个目标值,删除链表中所有节点值为目标值的节点,返回新的头节点
由于本题头节点含值,所有为了保证操作统一性,添加虚拟头节点
从虚拟节点开始遍历,逐个检查当前节点的下一节点是否为目标值
注意:
- 保证当前节点不为空且当前节点的下一节点也不为空
- 若当前节点的下一节点是目标值,修改当前节点的指针域后,下已遍历仍从当前节点出发判断其下一节点
- 最后返回虚拟节点的指针域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func removeElements(head *ListNode, val int) *ListNode {
dummyHead := new(ListNode)
dummyHead.Next = head
cur := dummyHead // 从虚拟头节点开始
for cur != nil && cur.Next != nil { // 当前节点及其下一节点不为空
if cur.Next.Val == val { // 当前节点的下一节点为目标值
cur.Next = cur.Next.Next // 修改当前节点的指针域
} else {
cur = cur.Next // 遍历下一节点
}
}
return dummyHead.Next // 返回虚拟头节点的指针域
}
|
设计链表
707. 设计链表 - 力扣(LeetCode)
设计并实现自己的单/双链表:
- 若实现单链表,则节点应具备两个属性:
val
和 next
,val
是当前节点的值,next
是指向下一个节点的指针/引用;若实现双向链表,则还需要属性 prev
以指示链表中的上一个节点
- 假设链表中的所有节点下标从 0 开始
- 链表定义、节点定义(自己写)、构造函数、获取节点值、添加头节点、添加尾节点、添加节点到指定节点前,删除节点
单链表
注意:
- 单链表节点可以不用定义,直接用
ListNode
复用力扣的全局变量,若想自己定义,可以定义节点名为Node
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
type MyLinkedList struct {
head *ListNode
length int
}
func Constructor() MyLinkedList {
dummyNode := new(ListNode)
myLinkedList := new(MyLinkedList)
myLinkedList.head = dummyNode
myLinkedList.length = 1
return *myLinkedList
}
// 获取指定下标索引的节点值
func (this *MyLinkedList) Get(index int) int {
index += 1 // 与添加虚拟头节点后的链表下标匹配
cur := this.head // 定义当前节点为虚拟头节点
for i := 0; i < this.length; i++ {
if i == index { // 找到指定下标节点
return cur.Val // 返回目标节点值
}
if cur.Next != nil {
cur = cur.Next // 更新为下一节点
} else {
return -1 // 遍历到最后一个节点都未找到目标值
}
}
return -1
}
// 添加头节点
func (this *MyLinkedList) AddAtHead(val int) {
newNode := new(ListNode)
newNode.Val = val
if this.head.Next != nil { // 虚拟头节点后有节点
newNode.Next = this.head.Next
this.head.Next = newNode
} else { // 虚拟头节点后无节点
this.head.Next = newNode
newNode.Next = nil
}
this.length++ // 更新链表长度
}
// 添加尾节点
func (this *MyLinkedList) AddAtTail(val int) {
newNode := new(ListNode)
newNode.Val = val
newNode.Next = nil
cur := this.head
for i := 0; i < this.length-1; i++ { // 找到最后一个节点
cur = cur.Next
}
cur.Next = newNode
this.length++ // 更新链表长度
}
// 添加节点到指定索引节点之前
func (this *MyLinkedList) AddAtIndex(index int, val int) {
if index == this.length-1 { // 刚好等于链表实际节点长度
this.AddAtTail(val) // 插入尾节点
return
}
newNode := new(ListNode)
newNode.Val = val
cur := this.head
for cur != nil && cur.Next != nil { // 遍历链表
if index == 0 { // 找到目标节点是当前节点的下一节点
newNode.Next = cur.Next
cur.Next = newNode
this.length++ // 更新链表长度
break
}
index-- // 更新索引
cur = cur.Next
}
}
// 删除指定下标节点
func (this *MyLinkedList) DeleteAtIndex(index int) {
cur := this.head
for cur != nil && cur.Next != nil {
if index == 0 { // 找到目标节点是当前节点的下一节点
cur.Next = cur.Next.Next
this.length-- // 更新链表长度
break
}
index-- // 更新索引
cur = cur.Next
}
}
|
- 时间复杂度: 涉及
index
的相关操作为 O(index),其余为 O(1)
- 空间复杂度: O(n)
双向链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
type MyListNode struct {
Val int
Next *MyListNode
Prev *MyListNode
}
type MyLinkedList struct {
dummyHead *MyListNode
dummyTail *MyListNode
Length int
}
func Constructor() MyLinkedList {
dummyHead := new(MyListNode)
dummyTail := new(MyListNode)
dummyHead.Prev = dummyTail
dummyHead.Next = dummyTail
dummyTail.Prev = dummyHead
dummyTail.Next = dummyHead
myLinkedList := MyLinkedList{
dummyHead: dummyHead,
dummyTail: dummyTail,
}
return myLinkedList
}
func (this *MyLinkedList) Get(index int) int {
if index >= this.Length {
return -1
}
cur := this.dummyHead.Next
for index != 0 {
index--
cur = cur.Next
}
return cur.Val
}
func (this *MyLinkedList) AddAtHead(val int) {
node := &MyListNode{Val: val}
node.Prev = this.dummyHead
node.Next = this.dummyHead.Next
this.dummyHead.Next = node
node.Next.Prev = node
this.Length++
}
func (this *MyLinkedList) AddAtTail(val int) {
node := &MyListNode{Val: val}
node.Prev = this.dummyTail.Prev
node.Next = this.dummyTail
this.dummyTail.Prev = node
node.Prev.Next = node
this.Length++
}
func (this *MyLinkedList) AddAtIndex(index int, val int) {
if index > this.Length {
return
}
node := &MyListNode{Val: val}
cur := this.dummyHead.Next
if index == this.Length {
this.AddAtTail(val)
return
} else {
for index != 0 {
cur = cur.Next
index--
}// cur此时指向要插入的位置
node.Next = cur
node.Prev = cur.Prev
cur.Prev = node
node.Prev.Next = node
}
this.Length++
}
func (this *MyLinkedList) DeleteAtIndex(index int) {
if index >= this.Length {
return
}
cur := this.dummyHead.Next
for index != 0 {
cur = cur.Next
index--
}
cur.Prev.Next = cur.Next
cur.Next.Prev = cur.Prev
cur.Next = nil
cur.Prev = nil
this.Length--
}
|
- 时间复杂度: 涉及
index
的相关操作为 O(index),其余为 O(1)
- 空间复杂度: O(n)
反转链表
206. 反转链表 - 力扣(LeetCode)
JZ24 反转链表_牛客题霸_牛客网
给定单链表的头节点 head
,返回反转后的链表
双指针,一个保存上一个节点,另一个遍历链表,边遍历边反转
反转:保存当前节点的下一个节点,将当前节点指向上一个节点,更新上一个节点为当前节点,当前节点前进
1
2
3
4
5
6
7
8
9
10
11
|
func reverseList(head *ListNode) *ListNode {
var pre *ListNode // 保存上一个节点(结构体指针初始化为nil)
cur := head // 当前节点为头节点
for cur != nil { // 遍历到最后一个节点
temp := cur.Next // 保存当前节点指针域
cur.Next = pre // 更新当前节点指针域
pre = cur // 更新保存的上一节点
cur = temp // 更新保存的当前节点
}
return pre
}
|
两两交换链表中的节点
24. 两两交换链表中的节点 - 力扣(LeetCode)
给定一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)
由于要返回头节点,所以定义虚拟头节点,保持操作统一性,也方便返回头节点
由于俩节点交换后还要更新这俩节点前面一个节点的指针域,所以用双指针
注意:先更新上一节点再更新当前节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func swapPairs(head *ListNode) *ListNode {
dummyNode := new(ListNode)
dummyNode.Next = head
cur := head // 从头节点出发
pre := dummyNode // 保存上一节点为虚拟头节点
for cur != nil && cur.Next != nil { // 遍历到倒数第二个节点
temp := cur.Next // 保存当前节点的下一个节点
cur.Next = cur.Next.Next // 更新当前节点指针域
temp.Next = cur // 更新保存的当前节点的下一个节点的指针域
pre.Next = temp // 更新这俩节点的上一节点的指针域
pre = cur // 更新上一节点
cur = cur.Next // 更新当前节点
}
return dummyNode.Next
}
|
删除链表的倒数第 N 个结点
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
给定一个链表和整数n
,删除链表的倒数第 n
个结点,并且返回链表的头结点
由于要返回头节点,所以定义虚拟头节点,保持操作统一性,也方便返回头节点
双指针法:先让快指针走n
步,此时快慢指针之间距离为n
。然后快慢指针同时移动,当快指针指向节点的下一个为 nil
时,慢指针指向的节点的后一个节点就是要删除的节点。一遍遍历,时间复杂度为O(n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummyNode := new(ListNode)
dummyNode.Next = head
pre, cur := dummyNode, dummyNode
for cur != nil {
if cur.Next == nil { // 遍历到尾节点
temp := pre.Next.Next // 保存要删除节点的下一节点
pre.Next = temp // 删除慢指针指向的下一节点
break
}
if n != 0 {
cur = cur.Next // 快指针前进
n--
} else {
pre = pre.Next // 慢指针前进
cur = cur.Next // 快指针前进
}
}
return dummyNode.Next
}
|
链表相交
面试题 02.07. 链表相交
160. 相交链表 - 力扣(LeetCode)
JZ52 两个链表的第一个公共结点_牛客题霸_牛客网
给定两个单链表的头节点 headA
和 headB
,返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
思路一:求长度,同时出发
双指针分别指向两个链表平齐的地方同时开始遍历,当两个指针指向的节点相同时,则找到了相交的起始节点,当有至少一条链表遍历结束仍未找到,则返回nil
要先遍历两个链表求出长度,因为长链的指针要从与短链开头相同位置的节点出发才能保持同时前进
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
func getIntersectionNode(headA, headB *ListNode) *ListNode {
cur := headA
lengthA, lengthB := 0, 0
for cur != nil { // 遍历到尾节点
lengthA++
cur = cur.Next
}
cur = headB
for cur != nil { // 遍历到尾节点
lengthB++
cur = cur.Next
}
differ := 0 // 俩链表长度差值
longerList := max(lengthA, lengthB)
idxA, idxB := headA, headB // 初始化俩指针分别指向头节点
if longerList == lengthB { // B链表更长
differ = lengthB - lengthA
for ; differ != 0; differ-- { // 移动B链表指针到与A平齐
idxB = idxB.Next
}
} else { // A链表更长
differ = lengthA - lengthB
for ; differ != 0; differ-- { // 移动A链表指针到与B平齐
idxA = idxA.Next
}
}
for idxA != idxB {
if idxA == nil || idxB == nil { // 至少一个链表遍历结束
return nil // 未找到公共节点
}
idxA = idxA.Next
idxB = idxB.Next
}
return idxA
}
|
思路二:双指针
保证两个指针走过的路径长度相同:结束的一方,让其从另一条链头继续;相当于每个指针最终都走过一遍两条链;最终两指针走过路径长度相同,若两链无焦点,则两指针最终都会为空,若有焦点,则会在焦点相遇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func getIntersectionNode(headA, headB *ListNode) *ListNode {
l1, l2 := headA, headB
for l1 != l2 {
if l1 != nil {
l1 = l1.Next
} else {
l1 = headB
}
if l2 != nil {
l2 = l2.Next
} else {
l2 = headA
}
}
return l1
}
|
环形链表 II
142. 环形链表 II - 力扣(LeetCode)
JZ23 链表中环的入口结点_牛客题霸_牛客网
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
哈希表:定义一个哈希表逐个遍历节点,每遍历到一个节点就查字典,若查到则说明是环中回到起点,若遍历完都没查到则说明链中无环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func detectCycle(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return nil
}
cur := head
nodeMap := make(map[*ListNode]bool)
for cur != nil { // 逐个遍历到最后一个节点
if _, ok := nodeMap[cur]; ok { // 遇到重复节点
return cur
}
nodeMap[cur] = true // 将节点存入字典
cur = cur.Next // 遍历下一节点
}
return nil
}
|
快慢指针+数学:
判断是否有环:快指针先走两步,慢指针再走一步,重复此步骤;若链中无环,则始终无法相遇,快指针最终为nil
;若有环,则慢指针最终会遇上指针,两个指针最终会在环中某节点相遇,因为相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合
若有环如何找到环入口:
此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
假设从头结点到环入口的节点数为x
,环入口到俩指针相遇点的节点数为y
,从相遇点再到环入口的节点数为 z

相遇时:
- slow 指针走过的节点数为:
x + y
,
- fast 指针走过的节点数:
x + n (y + z) + y
,
n
为fast
指针在环内走了n
圈才遇到slow
指针, (y+z)
为一圈的节点数
因为fast
指针是一步走两个节点,slow 指针一步走一个节点, 所以fast
指针走过的节点数 = slow
指针走过的节点数 * 2
,得
x + y + n (y + z) = (x + y) * 2
两边消掉一个 (x+y) 得
n (y + z) = x + y
因为要找环形的入口,那么要求的是x
x = n (y + z) - y
再从n(y+z)
中提出一个(y+z)
,整理公式之后得
x = (n - 1) (y + z) + z
注意这里n ≥ 1
的,因为fast
指针至少要多走一圈才能相遇slow
指针
这个公式的意义:
- 当
n=1
时,意味着fast
指针在环形里转了一圈之后,就遇到了slow
指针了
此时公式为 x = z,这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是环形入口的节点
- 当
n>1
时,意味着fast
指针在环内转n
圈之后才遇到slow
指针
其实这种情况和n=1
的效果是一样的,一样可以通过双指针法找到环形的入口节点,只不过index1
指针在环里多转了(n-1)
圈,然后再遇到index2
,相遇点依然是环形的入口节点
注意:
- slow,fast初始要都从头节点出发,不然俩节点会一直相差一个单位
- slow, fast更新不能写在for中,因为初始一定相等,直接返回头节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func detectCycle(head *ListNode) *ListNode {
slow, fast := head, head
for fast != nil && fast.Next != nil { // 遍历到最后一个节点的前一个节点
slow = slow.Next //慢指针前进一步
fast = fast.Next.Next // 快指针前进两步
if slow == fast { // 快慢指针相遇
idx1 := slow // 从相遇点出发
idx2 := head // 从链表头出发
for idx1 != idx2 { // 俩指针每次前进一步
idx1 = idx1.Next
idx2 = idx2.Next
}
return idx1 // 俩指针相遇=>入环点
}
}
return nil // 遍历链表无环
}
|
附加
删除排序链表中的重复元素
83. 删除排序链表中的重复元素 - 力扣(LeetCode)
给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次
方法一:单指针 逐个删除重复节点
1
2
3
4
5
6
7
8
9
10
|
func deleteDuplicates(head *ListNode) *ListNode {
cur := head
for cur != nil {
for cur.Next != nil && cur.Val == cur.Next.Val {
cur.Next = cur.Next.Next
}
cur = cur.Next
}
return head
}
|
方法二:双指针 找到新元素后一次性删除中间的重复节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
func deleteDuplicates(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
pre := head
cur := head.Next
for cur != nil {
for cur != nil && cur.Val == pre.Val {
cur = cur.Next
}
pre.Next = cur
if cur == nil {
break
}
pre = cur
cur = cur.Next
}
return head
}
|
附加
删除排序链表中的重复元素 II
82. 删除排序链表中的重复元素 II - 力扣(LeetCode)
JZ76 删除链表中重复的结点_牛客题霸_牛客网
只要有元素重复,就将该元素全部删除
- 链表头结点可能被删除,所以要用 dummy node 辅助删除逻辑一致
- 若当前元素与下一元素相同,则当前指针后移不断寻找相同元素,直到当前元素与下一元素不同;
- 此时注意要区分是有重复元素的不同,还是无重复元素的不同:若有重复元素,则删除重复元素,前一元素指针不动;若无重复元素,则更新前一元素指针;
- 最后更新当前元素指针为前一元素指针的下一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func deleteDuplicates(head *ListNode) *ListNode {
dummyNode := &ListNode{Val: -101, Next: head}
pre, cur := dummyNode, dummyNode.Next
for cur != nil {
findDup := false
for cur.Next != nil && cur.Val == cur.Next.Val {
findDup = true
cur = cur.Next
}
if findDup {
// 有重复元素:删除重复元素
pre.Next = cur.Next
} else {
// 无重复元素:更新前一元素指针
pre = pre.Next
}
// 更新当前元素指针为前一元素指针的下一个
cur = pre.Next
}
return dummyNode.Next
}
|
反转链表 II
92. 反转链表 II - 力扣(LeetCode)
反转给定位置的链表节点
- 为了处理头节点是起始点的情况,引入虚拟头节点指向头节点
- 遍历节点至指定结束位置,记录反转起始和结束点,以及起始前一个和结束后一个节点;
- 反转指定链表节点后,重新连接起始前一个和结束后一个节点
- 遇到只有一个头节点或反转一个节点的情况直接返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
func reverseBetween(head *ListNode, left int, right int) *ListNode {
if head.Next == nil || left == right {
return head
}
dummyNode := &ListNode{Next: head}
end := dummyNode
var preS, start, nextE *ListNode
for i := 0; i < right; i++ {
// 记录反转的起始节点和其前一个节点
if i == left-1 {
preS = end
start = end.Next
}
end = end.Next
}
// 记录反转的最后一个节点的下一个节点
nextE = end.Next
reverse(start, nextE)
preS.Next = end
start.Next = nextE
return dummyNode.Next
}
func reverse(head, nextE *ListNode) {
var pre *ListNode
cur := head
for cur != nextE {
temp := cur.Next
cur.Next = pre
pre = cur
cur = temp
}
}
|
合并两个有序链表
21. 合并两个有序链表 - 力扣(LeetCode)
JZ25 合并两个排序的链表_牛客题霸_牛客网
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
dummyNode := new(ListNode)
pre := dummyNode
// 遍历两个链表直到一条结束
for list1 != nil && list2 != nil {
if list1.Val < list2.Val {
pre.Next = list1
list1 = list1.Next
} else {
pre.Next = list2
list2 = list2.Next
}
pre = pre.Next
}
// 将剩下的链表直接连接到最后面
if list1 == nil {
pre.Next = list2
} else {
pre.Next = list1
}
return dummyNode.Next
}
|
分隔链表
86. 分隔链表 - 力扣(LeetCode)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func partition(head *ListNode, x int) *ListNode {
// 两个链表的虚拟头节点:一个小于目标值,一个大于等于目标值的
dummyNode1, dummyNode2 := new(ListNode), new(ListNode)
list1, list2 := dummyNode1, dummyNode2
// 得到两个链表:一个小于目标值,一个大于等于目标值
for head != nil {
if head.Val < x {
list1.Next = head
list1 = list1.Next
} else {
list2.Next = head
list2 = list2.Next
}
head = head.Next
}
// 将大于目标值的链表的末节点NEXT域置空
list2.Next = nil
// 将大于目标值的链表的头节点接到小于目标值的链表的尾
list1.Next = dummyNode2.Next
// 返回小于目标值的链表的头节点
return dummyNode1.Next
}
|
排序链表
148. 排序链表 - 力扣(LeetCode)
方法一:开辟额外空间存储节点值排序后重新分配节点值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func sortList(head *ListNode) *ListNode {
nums := make([]int, 0)
cur := head
for cur != nil {
nums = append(nums, cur.Val)
cur = cur.Next
}
sort.Ints(nums)
cur = head
for i := 0; i < len(nums); i++ {
cur.Val = nums[i]
cur = cur.Next
}
return head
}
|
- 时间复杂度:$O(n)$
- 空间复杂度:$O(n)$
方法二:归并排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
func sortList(head *ListNode) *ListNode {
return mergeSort(head)
}
// 归并排序
func mergeSort(head *ListNode) *ListNode {
// 无节点或只有一个节点
if head == nil || head.Next == nil {
return head
}
// 快慢指针法找中点
slow, fast := head, head.Next
// 快指针为空(奇)或next域为空(偶)时,慢指针指向中点
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
// 断开中间节点
temp := slow.Next
slow.Next = nil
// 向左右递归
left := mergeSort(head)
right := mergeSort(temp)
// 合并
return merge(left, right)
}
func merge(list1, list2 *ListNode) *ListNode {
dummyNode := new(ListNode)
cur := dummyNode
// 遍历两条链比较节点值,升序连接到新链,直到一条链结束
for list1 != nil && list2 != nil {
if list1.Val < list2.Val {
cur.Next = list1
list1 = list1.Next
} else {
cur.Next = list2
list2 = list2.Next
}
cur = cur.Next
}
// 将剩余节点直接连接到新链尾
if list1 == nil {
cur.Next = list2
} else {
cur.Next = list1
}
return dummyNode.Next
}
|
- 时间复杂度:$O(nlogn)$
- 空间复杂度:$O(1)$
重排链表
143. 重排链表 - 力扣(LeetCode)
寻找链表中点 + 链表逆序 + 合并链表:
-
找到原链表的中点断开
-
将原链表的右半端反转
-
将两条链合并
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
func reorderList(head *ListNode) {
if head.Next == nil {
return
}
// 找链表中点并断开
slow, fast := head, head.Next
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
list2Head := slow.Next
slow.Next = nil
// 反转右半部分链表
var pre *ListNode
for list2Head != nil {
temp := list2Head.Next
list2Head.Next = pre
pre = list2Head
list2Head = temp
}
// 合并
list1, list2 := head.Next, pre
cur := head
toggle := true
for list1 != nil && list2 != nil {
// 切换器为真用右半部分
if toggle {
cur.Next = list2
cur = cur.Next
list2 = list2.Next
} else {
cur.Next = list1
cur = cur.Next
list1 = list1.Next
}
toggle = !toggle
}
if list1 != nil {
cur.Next = list1
} else {
cur.Next = list2
}
return
}
|
环形链表
141. 环形链表 - 力扣(LeetCode)
思路:快慢指针,快慢指针相同则有环,证明:如果有环每走一步快慢指针距离会减 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func hasCycle(head *ListNode) bool {
if head == nil || head.Next == nil {
return false
}
slow, fast := head, head.Next
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}
|
回文链表
234. 回文链表 - 力扣(LeetCode)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
func isPalindrome(head *ListNode) bool {
// 找中点
slow, fast := head, head.Next
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
// 中点断开
list2 := slow.Next
slow.Next = nil
// 反转右半链表
var pre *ListNode
for list2 != nil {
temp := list2.Next
list2.Next = pre
pre = list2
list2 = temp
}
// 逐个对比俩链表节点
for head != nil && pre != nil {
if head.Val != pre.Val {
return false
}
head = head.Next
pre = pre.Next
}
return true
}
|
随机链表的复制
138. 随机链表的复制 - 力扣(LeetCode)
JZ35 复杂链表的复制_牛客题霸_牛客网
思路一:哈希表存储节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func copyRandomList(head *Node) *Node {
oldCur := head
NewDummyNode := new(Node)
NewPre := NewDummyNode
mp := make(map[*Node]*Node)
// 复制旧节点,保存旧节点与新节点的一一映射
for oldCur != nil {
newNode := &Node{Val: oldCur.Val, Next: oldCur.Next, Random: oldCur.Random}
mp[oldCur] = newNode
NewPre.Next = newNode
NewPre = NewPre.Next
oldCur = oldCur.Next
}
// 遍历新链表,用字典值替换Random域
newCur := NewDummyNode.Next
for newCur != nil {
newCur.Random = mp[newCur.Random]
newCur = newCur.Next
}
return NewDummyNode.Next
}
|
- 时间复杂度:$O(n)$
- 空间复杂度:$O(n)$
思路二:复制节点跟在原节点后面 最后新旧节点分离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
func copyRandomList(head *Node) *Node {
if head == nil {
return head
}
// 复制节点跟在旧节点后面, Random域置空
cur := head
for cur != nil {
clone := &Node{Val: cur.Val, Next: cur.Next}
cur.Next = clone
cur = clone.Next
}
// 处理Random域:新节点random域 = 旧节点random域后的新节点
cur = head
for cur != nil {
if cur.Random != nil {
cur.Next.Random = cur.Random.Next
}
cur = cur.Next.Next
}
// 分离新旧节点
cur = head
cloneHead := head.Next
for cur != nil && cur.Next != nil {
temp := cur.Next
cur.Next = cur.Next.Next
cur = temp
}
return cloneHead
}
|
- 时间复杂度:$O(n)$
- 空间复杂度:$O(1)$
合并零之间的节点
2181. 合并零之间的节点 - 力扣(LeetCode)
给定一个链表的头节点 ,链表的 开端 和 末尾 的节点值为0,中间也含有0节点,要求将相邻两个0节点间的所有节点合并成一个节点,值为累加值,而且要将所有0节点删除, 返回修改后链表的头节点
思路:创建指针指向头节点,代表答案链表尾节点;遍历链表,每遇到非0节点就累加值到答案链表尾节点,每遇到0节点就让答案链表尾节点指针前进一个单位,同时清空新尾节点的值;遍历结束后断开答案链表尾节点之后的节点,返回头节点即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func mergeNodes(head *ListNode) *ListNode {
// tail指向答案链表的末尾节点
tail := head
// 从第二个节点遍历到倒数第二个节点
for cur := head.Next; cur.Next != nil; cur = cur.Next {
// 判断当前节点值是否非0
if cur.Val != 0 {
// 将当前节点值累加到答案链表尾节点中
tail.Val += cur.Val
} else {
// 更新答案链表尾节点(前进一个)
tail = tail.Next
// 清空当前答案链表尾节点值
tail.Val = 0
}
}
// 断开答案链表后面的节点
tail.Next = nil
// 返回头节点
return head
}
|
- 时间复杂度:$O(n)$
- 空间复杂度:$O(1)$
两数相加
2. 两数相加 - 力扣(LeetCode)
给两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。将两个数相加,并以相同形式返回一个表示和的链表。这两个数都不会以 0 开头
思路:顺序遍历俩链表,直到俩链表都遍历结束且无进位,遍历时直接用求得两节点和的个位作为新节点值
注意:不能读出完整整数后再相加,因为测试用例数字过大会溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
dummyNode := new(ListNode)
cur := dummyNode
sum := 0
// 循环直到链表全部遍历结束且无进位值
for l1 != nil || l2 != nil || sum != 0 {
if l1 != nil {
// l1值加到进位
sum += l1.Val
}
if l2 != nil {
// l2值加到进位
sum += l2.Val
}
// 和的个位作为新节点值
node := &ListNode{Val: sum % 10}
// 新节点入链
cur.Next = node
// 移动指针
cur = cur.Next
// 更新sum为进位值
sum /= 10
if l1 != nil {
// l1向右移动
l1 = l1.Next
}
if l2 != nil {
// l2向右移动
l2 = l2.Next
}
}
return dummyNode.Next
}
// 写法二
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
dummyNode := new(ListNode)
cur := dummyNode
cur1, cur2 := l1, l2
sum := 0
for cur1 != nil || cur2 != nil {
if cur1 != nil {
sum += cur1.Val
cur1 = cur1.Next
}
if cur2 != nil {
sum += cur2.Val
cur2 = cur2.Next
}
node := &ListNode{Val: sum%10}
cur.Next = node
cur = cur.Next
sum /= 10
}
if sum != 0 {
node := &ListNode{Val: sum}
cur.Next = node
}
return dummyNode.Next
}
|
- 时间复杂度:$O(n+m)$
- 空间复杂度:$O(1)$
K 个一组翻转链表
25. K 个一组翻转链表 - 力扣(LeetCode)
给定链表的头节点和一个整数k
,每 k
个节点一组进行翻转,返回修改后的链表。k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么将最后剩余的节点保持原有顺序。不能只是单纯的改变节点内部的值,而是需要实际进行节点交换
思路一:遍历链表每遇到k个结点就断开其尾,翻转后,重新连接首节点与前置节点,尾节点与后置节点;核心思路和两两一组反转一样,重要的就是
- 组内单链表反转模板
- 维护上一组最后一个和下一组第一个节点。
- 思考如何用变量表示反转后当前组的第一个和最后一个节点
- 最后调整组之间的连接关系,然后移动到下一组继续处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
func reverseKGroup(head *ListNode, k int) *ListNode {
dummyNode := &ListNode{Next: head}
pre := dummyNode
cur := head
count := 1
tempHead := head
for cur != nil {
if count == k {
temp := cur.Next
cur.Next = nil
reverse(tempHead)
pre.Next = cur
pre = tempHead
tempHead.Next = temp
tempHead = temp
cur = temp
count = 1
} else {
count++
cur = cur.Next
}
}
return dummyNode.Next
}
func reverse(head *ListNode) {
cur := head
var pre *ListNode
for cur != nil {
temp := cur.Next
cur.Next = pre
pre = cur
cur = temp
}
}
|
- 时间复杂度:$O(n)$
- 空间复杂度:$O(1)$
合并 K 个升序链表
23. 合并 K 个升序链表 - 力扣(LeetCode)
给定一个链表数组,每个链表都已经按升序排列。要求将所有链表合并到一个升序链表中,返回合并后的链表
思路一:将所有值存入容器,再对容器内值排序,重新赋值回链表,并将链表相连
思路二:链表归并排序(推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0 {
return nil
}
if len(lists) <= 1 {
return lists[0]
}
left := mergeKLists(lists[:len(lists)/2])
right := mergeKLists(lists[len(lists)/2:])
return merge(left, right)
}
func merge(list1, list2 *ListNode) *ListNode {
dummyNode := new(ListNode)
pre := dummyNode
for list1 != nil && list2 != nil {
if list1.Val <= list2.Val {
pre.Next = list1
pre = pre.Next
list1 = list1.Next
} else {
pre.Next = list2
pre = pre.Next
list2 = list2.Next
}
}
if list1 != nil {
pre.Next = list1
}
if list2 != nil {
pre.Next = list2
}
return dummyNode.Next
}
|
- 时间复杂度:$O(nlogk)$
- 空间复杂度:$O(n)$
LRU 缓存
146. LRU 缓存 - 力扣(LeetCode)
思路:双向链表+哈希表
注意:
- 需要虚拟头节点保持操作一致性
- 链表中也要记录
key
,用来在字典中删除头节点记录
- 查到记录后要将该节点移至尾节点
- 插入新数据前检查是否已存在,存在的话更新即可,更新后移至尾节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
type DListNode struct {
Key, Val int
Pre *DListNode
Next *DListNode
}
type LRUCache struct {
DummyNode *DListNode
Mp map[int]*DListNode
Capacity int
}
func Constructor(capacity int) LRUCache {
newLRUCache := LRUCache{
DummyNode: new(DListNode),
Mp: make(map[int]*DListNode),
Capacity: capacity,
}
newLRUCache.DummyNode.Pre = newLRUCache.DummyNode
newLRUCache.DummyNode.Next = newLRUCache.DummyNode
return newLRUCache
}
func (this *LRUCache) Get(key int) int {
if node, ok := this.Mp[key]; ok {
// 移到尾节点
this.moveTail(node)
return node.Val
}
return -1
}
func (this *LRUCache) Put(key int, value int) {
// 判是否为更新
if this.Get(key) != -1 {
// 更新节点值
this.Mp[key].Val = value
// 移到尾节点
this.moveTail(this.Mp[key])
} else {
// 新值作为尾节点插入
newNode := &DListNode{
Key: key,
Val: value,
Pre: this.DummyNode.Pre,
Next: this.DummyNode,
}
// 超容淘汰LRU(头节点)
if this.Capacity == len(this.Mp) {
delete(this.Mp, this.DummyNode.Next.Key)
this.DummyNode.Next.Next.Pre = this.DummyNode
this.DummyNode.Next = this.DummyNode.Next.Next
}
this.DummyNode.Pre.Next = newNode
this.DummyNode.Pre = newNode
// 计入哈希表
this.Mp[key] = newNode
}
}
func (this *LRUCache) moveTail(node *DListNode) {
// 修改node前一节点的next域
node.Pre.Next = node.Next
// 修改node后一节点的pre域
node.Next.Pre = node.Pre
// 修改node的next pre域
node.Pre = this.DummyNode.Pre
node.Next = this.DummyNode
// 修改原尾节点的next域
this.DummyNode.Pre.Next = node
// 修改虚拟头节点的pre域
this.DummyNode.Pre = node
}
// 写法二
type Node struct {
Key int
Val int
Pre *Node
Next *Node
}
type LRUCache struct {
dummyNode *Node
mp map[int]*Node
capacity int
}
func Constructor(capacity int) LRUCache {
lruCache := LRUCache{
dummyNode: new(Node),
mp: make(map[int]*Node),
capacity: capacity,
}
lruCache.dummyNode.Pre = lruCache.dummyNode
lruCache.dummyNode.Next = lruCache.dummyNode
return lruCache
}
func (this *LRUCache) Get(key int) int {
if node, ok := this.mp[key]; ok {
// 该节点移至链尾(使用过)
deleteNode(node)
setTail(this.dummyNode, node)
// 返回该节点值
return node.Val
}
return -1
}
func (this *LRUCache) Put(key int, value int) {
if node, ok := this.mp[key]; ok {
// 修改节点值
node.Val = value
// 该节点移至链尾(使用过)
deleteNode(node)
setTail(this.dummyNode, node)
} else {
if len(this.mp) >= this.capacity {
// 移出链首
delete(this.mp, this.dummyNode.Next.Key)
deleteNode(this.dummyNode.Next)
}
// 新节点插入链尾
node := &Node{Key: key, Val: value}
setTail(this.dummyNode, node)
this.mp[key] = node
}
}
func deleteNode(node *Node) {
node.Pre.Next = node.Next
node.Next.Pre = node.Pre
node.Pre = nil
node.Next = nil
}
func setTail(dummyNode, node *Node) {
node.Pre = dummyNode.Pre
node.Next = dummyNode
dummyNode.Pre.Next = node
dummyNode.Pre = node
}
|
旋转链表
61. 旋转链表 - 力扣(LeetCode)
给定一个链表的头节点 head
,旋转链表,将链表每个节点向右移动 k
个位置。
思路一:旋转数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
func rotateRight(head *ListNode, k int) *ListNode {
if head == nil {
return nil
}
nums := make([]int, 0)
cur := head
for cur != nil {
nums = append(nums, cur.Val)
cur = cur.Next
}
// 旋转数组
k %= len(nums)
slices.Reverse(nums)
slices.Reverse(nums[:k])
slices.Reverse(nums[k:])
// 更新链表
cur = head
i := 0
for cur != nil {
cur.Val = nums[i]
i++
cur = cur.Next
}
return head
}
|
思路二:倒数k个节点连接到链首
注意:
- 单独处理无节点和单节点
- 先求长度,再对
k
取余
- 单独处理
k == 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
func rotateRight(head *ListNode, k int) *ListNode {
if head == nil {
return nil
}
// 统计链表长度
cur := head
cnt := 1
for cur.Next != nil {
cnt++
cur = cur.Next
} // 此时cur指向最后一个节点
// 更新k
k %= cnt
// 找倒数第k个节点
dummyNode := &ListNode{Next: head}
slow, fast := dummyNode, head
for fast != nil {
if k <= 0 {
slow = slow.Next
}
fast = fast.Next
k--
} // 此时slow指向倒数第k+1个节点
// 将倒数k个节点连接到链首
cur.Next = dummyNode.Next
dummyNode.Next = slow.Next
slow.Next = nil
return dummyNode.Next
}
// 写法二
func rotateRight(head *ListNode, k int) *ListNode {
if head == nil || head.Next == nil {
return head
}
length := 0
cur := head
for cur != nil {
cur = cur.Next
length++
}
k %= length
if k == 0 {
return head
}
// 最后k个节点移至开头
slow, fast := head, head
for fast != nil && fast.Next != nil {
if k <= 0 {
slow = slow.Next
}
fast = fast.Next
k--
}
temp := slow.Next
slow.Next = nil
fast.Next = head
return temp
}
|
思路三:成环后断开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
func rotateRight(head *ListNode, k int) *ListNode {
if head == nil {
return nil
}
// 统计链表长度
cur := head
cnt := 1
for cur.Next != nil {
cnt++
cur = cur.Next
} // 此时cur指向最后一个节点
// 连成环
cur.Next = head
// 更新k
k %= cnt
k = cnt - k - 1
// 第k个节点后作为首断开环
cur = head
for k != 0 {
cur = cur.Next
k--
}
rHead := cur.Next
cur.Next = nil
return rHead
}
|
删除链表的中间节点
2095. 删除链表的中间节点 - 力扣(LeetCode)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func deleteMiddle(head *ListNode) *ListNode {
if head.Next == nil {
return nil
}
var pre *ListNode
slow, fast := head, head
for fast != nil && fast.Next != nil {
pre = slow
slow = slow.Next
fast = fast.Next.Next
}
pre.Next = slow.Next
return head
}
|
奇偶链表
328. 奇偶链表 - 力扣(LeetCode)
给定单链表的头节点 head
,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。必须在 O(1)
的额外空间复杂度和 O(n)
的时间复杂度下解决这个问题
注意:链表节点数为偶数时,需要cur.Next != nil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
func oddEvenList(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
dummyNode1, dummyNode2 := &ListNode{}, &ListNode{}
cur1, cur2 := head, head.Next
dummyNode1.Next, dummyNode2.Next = cur1, cur2
for cur1 != nil && cur1.Next != nil && cur2 != nil && cur2.Next != nil {
cur1.Next = cur1.Next.Next
cur2.Next = cur2.Next.Next
cur1 = cur1.Next
cur2 = cur2.Next
}
cur1.Next = dummyNode2.Next
return dummyNode1.Next
}
// 写法二
func oddEvenList(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
dummyNode1 := new(ListNode)
dummyNode2 := new(ListNode)
cur1 := dummyNode1
cur2 := dummyNode2
cur := head
sign := true
for cur != nil {
if sign {
cur1.Next = cur
cur1 = cur1.Next
} else {
cur2.Next = cur
cur2 = cur2.Next
}
temp := cur.Next
cur.Next = nil
cur = temp
sign = !sign
}
cur1.Next = dummyNode2.Next
return dummyNode1.Next
}
|
链表最大孪生和
2130. 链表最大孪生和 - 力扣(LeetCode)
给定一个长度为偶数的链表的头节点 head
,返回链表的 最大孪生和。最大孪生和:比方说长度为 4 的链表中所有的孪生节点,节点 0
是节点 3
的孪生节点,节点 1
是节点 2
的孪生节点
思路:快慢指针边找中点边反转一半,再从中心向两边遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func pairSum(head *ListNode) int {
// 找中点并反转前半段
slow, fast := head, head.Next
var pre *ListNode
for fast != nil && fast.Next != nil {
fast = fast.Next.Next
temp := slow.Next
slow.Next = pre
pre = slow
slow = temp
} // 此时slow指向前半部分尾且未反转
// 从中间向两边遍历链表
cur2 := slow.Next
slow.Next = pre
res := 0
for cur2 != nil || slow != nil {
res = max(res, cur2.Val+slow.Val)
cur2 = cur2.Next
slow = slow.Next
}
return res
}
|
LCR 123. 图书整理 I
LCR 123. 图书整理 I - 力扣(LeetCode)
JZ6 从尾到头打印链表_牛客题霸_牛客网
从尾到头打印链表
1
2
3
4
5
6
7
8
9
10
|
func reverseBookList(head *ListNode) []int {
cur := head
res := []int{}
for cur != nil {
res = append(res, cur.Val)
cur = cur.Next
}
slices.Reverse(res)
return res
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func reverseBookList(head *ListNode) []int {
if head == nil {
return []int{}
}
if head.Next == nil {
return []int{head.Val}
}
var pre *ListNode
cur := head
for cur != nil {
temp := cur.Next
cur.Next = pre
pre = cur
cur = temp
}
res := []int{}
for pre != nil {
res = append(res, pre.Val)
pre = pre.Next
}
return res
}
|