目录
本系列文章仅是 GitHub 大神 @halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C++转化。原书请自行下载学习。
本篇文章涉及新手应该优先刷的几道经典链表算法题,以后会更新“二刷”“三刷”等等,上一篇文章《一刷链表(上)》在这里。
LeetCode #876:Middle of the Linked List 链表的中间节点
给你单链表的头节点 head
,请你找出并返回链表的中间节点。
如果有两个中间节点,则返回第二个中间节点。
问题的关键在于我们无法得到链表的长度 n
,常规的解法是两次遍历。正如我们在上一篇文章中的寻找倒数第 k
个节点的解决思路,我们让两个指针 fast
和 slow
分别指向链表头节点 head
,每当慢指针 slow
前进一步,快指针 fast
就前进两步,那么当 fast
走向链表末尾的同时,slow
恰好指向链表的中点。
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* fast = head, *slow = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
利用这一思路,我们可以解决链表是否成环的问题。
LeetCode #141:Linked List Cycle 环形链表
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
同样地,每当慢指针 slow
前进一步,快指针 fast
就前进两步,如果过程中 fast
与 slow
出现追及(相遇),则证明该链表包含环。
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* fast = head, *slow = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true;
}
return false;
}
};
更多例子:找出环的起点
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改链表。
假设快慢指针相遇时,slow
走了 k
步,那么 fast
一定走了 2k
步;fast
相对于 slow
多走的 k
实际上是环长度的整数倍。
因此,假设相遇点距环起点的距离为 m
,从相遇点继续前进 k - m
步恰好到达环的起点,而环的起点距头节点 head
的距离也为 k - m
。此时我们只要将两个指针中的任意一个指针重新指向 head
,两个指针则同速前进,k - m
步后一定会相遇,相遇之处便是环的起点。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head, *slow = head;
while (fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) break;
}
//没有环
if (fast == nullptr || fast->next == nullptr) return nullptr;
slow = head;
while (slow != fast) {
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
LeetCode #160:Intersection of Two Linked Lists 相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据保证整个链式结构中不存在环。
注意,函数返回结果后,链表必须保持其原始结构。
自定义评测:
评测系统的输入如下(你设计的程序不适用此输入):
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为 0listA
- 第一个链表listB
- 第二个链表skipA
- 在listA
中(从头节点开始)跳到交叉节点的节点数skipB
- 在listB
中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA
和 headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被视作正确答案。
哈希集合解法
遍历链表 A,使用哈希集合存储链表 A 中的每个节点,随后和另一条链表对比,判断是否有节点在哈希集合中,第一个在哈希集合的节点就是所谓的相交节点 c1
。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
unordered_set<ListNode *> visited;
ListNode *temp = headA;
while (temp != nullptr) {
visited.emplace(temp); //插入链表 A 的元素,也可以用 insert() 方法
temp = temp->next;
}
temp = headB;
while (temp != nullptr) {
if (visited.count(temp)) { //判断链表 B 中的元素是否存在于哈希集合中
return temp;
}
temp = temp->next;
}
return nullptr;
}
};
该算法的时间复杂度为 O ( n + m ) \ O(n+m) O(n+m) ,空间复杂度为 O ( n ) \ O(n) O(n) 。
双指针解法
双指针的算法可以将空间复杂度降至 O ( 1 ) \ O(1) O(1) 。
由于两条链表长度可能不一致,两条链表之间的节点无法对应,如果只是让两个指针 p1
和 p2
分别在两条链表上前进,并不能同时走到相交节点。因此,我们可以让 p1
遍历完 listA
后开始遍历 listB
,相应地,p2
遍历完 listB
后也开始遍历 listA
,这样两条链表在逻辑上接在了一起,消除了两者间的长度差,p1
和 p2
就可以同时进入公共部分,即相交节点 c1
。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* p1 = headA, *p2 = headB;
while (p1 != p2) {
// p1 走到链表 A 末尾,转到链表 B
if (p1 == nullptr) p1 = headB;
else p1 = p1->next;
// p2 走到链表 B 末尾,转到链表 A
if (p2 == nullptr) p2 = headA;
else p2 = p2->next;
}
return p1;
}
};
另一种双指针解法
若两条链表相交,则必然存在一段公共段,也就意味着,走到公共段之前的对比没有必要。两条链表最终相交,显然末端对齐的两个链表中,两条链表可能会因为长度差而无法完全按照索引一一对应。那么,我们可以让长链表的指针 p1
先走两个链表长度的差值,接着短链表的指针 p2
定位到与长链表长度相同的位置,二者开始同步向后遍历,比较链表节点是否一致。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* p1 = headA, *p2 = headB;
int lenA = 0, lenB = 0;
while (p1 != nullptr) { //求链表 A 的长度
lenA++;
p1 = p1->next;
}
while (p2 != nullptr) { //求链表 B 的长度
lenB++;
p2 = p2->next;
}
//重置两个指针
p1 = headA, p2 = headB;
//确保 p1 为长链表的指针
if (lenB > lenA) {
int tempLen = lenA;
lenA = lenB, lenB = tempLen;
ListNode* tempNode = p1;
p1 = p2, p2 = tempNode;
}
//求长度差
int gap = lenA - lenB;
//让 p1 和 p2 在同一起点上(末端对齐)
while (gap-- > 0) {
p1 = p1->next;
}
//遍历 p1 和 p2,遇到相同则直接返回
while ( p1 != nullptr) {
if (p1 == p2) {
return p1;
}
p1 = p1->next;
p2 = p2->next;
}
return nullptr;
}
};
受到环链表启发的解法
如果把两条链表首尾相连,那么「寻找两条链表的交点」的问题转换成了前面提到的「寻找环起点」的问题:
需要注意,找到相交节点后应当将环断开,以避免返回错误结果。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* p = headB;
//找到链表 B 的最后一个节点,即两链表公共部分的最后一个节点
while (p->next != nullptr) p = p->next;
//将公共链条的最后一个节点与 headB 头节点相连构成一个环
p->next = headB;
//找出环的起点,即链表 A 和 B 的相交节点
ListNode* res = detectCycle(headA);
//断开环
p->next = nullptr;
return res;
}
private:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head, *slow = head;
while (fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) break;
}
//没有环
if (fast == nullptr || fast->next == nullptr) return nullptr;
slow = head;
while (slow != fast) {
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
LeetCode #707:Design Linked List 设计链表
实现链表的查找、头插法、尾插法、通用插入、删除操作:
get(index)
:获取链表中第index
个节点的值。如果索引无效,则返回 -1。addAtHead(val)
:在链表的第一个元素之前添加一个值为val
的节点。插入后,新节点将成为链表的第一个节点。addAtTail(val)
:将值为val
的节点追加到链表的最后一个元素。addAtIndex(index,val)
:在链表中的第index
个节点之前添加值为val
的节点。如果index
等于链表的长度,则该节点将附加到链表的末尾。如果index
大于链表长度,则不会插入节点;如果index
小于 0,则在头部插入节点。deleteAtIndex(index)
:如果索引index
有效,则删除链表中的第index
个节点。
单向链表的实现
实现单向链表,即每个节点仅存储本身的值和后继节点。除此之外,我们还需要一个哨兵(
s
e
n
t
i
n
e
l
\ sentinel
sentinel)节点作为头节点,和一个 length
参数保存有效节点数:
先定义一个简单的节点类 Node
:
struct Node {
int val;
Node* next;
Node(int x) : val(x), next(nullptr) {} //构造函数
};
链表类里初始化头节点和链表长度:
class MyLinkedList {
private:
Node* head; //头节点,不存储有效数据,仅作为哨兵节点
int length; //链表长度
public:
MyLinkedList() {
head = new Node(0); //初始化头节点为哨兵节点
length = 0;
}
};
实现 get(index)
时,先判断有效性,再通过循环来找到对应的节点的值:
int get(int index) {
if (index < 0 || index >= length) {
return -1;
}
Node* curr = head->next; //从第一个有效节点开始遍历
for (int i = 0; i < index; ++i) {
curr = curr->next;
}
return curr->val;
}
实现 addAtIndex(index, val)
时,如果 index
是有效值,则需要找到原来下标为 index
的节点的前驱节点 curr
,并创建新节点 newNode
,将 newNode
的后继节点设为 curr
的后继节点,将 curr
的后继节点更新为 newNode
,这样就将 newNode
插入到了链表中。最后需要更新 length
:
void addAtIndex(int index, int val) {
if (index > length) return;
if (index < 0) index = 0;
Node* newNode = new Node(val);
Node* curr = head;
//找到第index个节点的前驱节点
for (int i = 0; i < index; ++i) {
curr = curr->next;
}
newNode->next = curr->next;
curr->next = newNode;
//插入节点后,链表长度 + 1
length++;
}
addAtHead(val)
相当于 addAtIndex(0, val)
,addAtTail(val)
相当于 addAtIndex(length, val)
:
void addAtHead(int val) {
addAtIndex(0, val);
}
void addAtTail(int val) {
addAtIndex(length, val);
}
实现 deleteAtIndex(index)
,先判断参数有效性,然后找到下标为 index
的节点的前驱节点 curr
,通过将 curr
的后继节点更新为 curr
的后继节点的后继节点,来达到删除节点的效果。同时也要更新 length
:
void deleteAtIndex(int index) {
if (index < 0 || index >= length) return;
Node* curr = head;
//找到第 index 个节点的前驱节点
for (int i = 0; i < index; ++i) {
curr = curr->next;
}
Node* toDelete = curr->next;
curr->next = toDelete->next;
//释放被删除节点的内存
delete toDelete;
//删除节点后,链表长度 - 1
length--;
}
最终代码实现如下:
//定义单节点结构体
struct Node {
int val;
Node* next;
Node(int _val) : val(_val), next(nullptr) {}
};
class MyLinkedList {
private:
Node* head; //头节点,不存储有效数据,仅作为哨兵节点
int length; //链表长度
public:
MyLinkedList() {
head = new Node(0); //初始化头节点为哨兵节点
length = 0;
}
int get(int index) {
if (index < 0 || index >= length) {
return -1;
}
Node* curr = head->next; //从第一个有效节点开始遍历
for (int i = 0; i < index; ++i) {
curr = curr->next;
}
return curr->val;
}
void addAtHead(int val) {
addAtIndex(0, val);
}
void addAtTail(int val) {
addAtIndex(length, val);
}
void addAtIndex(int index, int val) {
if (index > length) return;
if (index < 0) index = 0;
Node* newNode = new Node(val);
Node* curr = head;
//找到第index个节点的前驱节点
for (int i = 0; i < index; ++i) {
curr = curr->next;
}
newNode->next = curr->next;
curr->next = newNode;
//插入节点后,链表长度 + 1
length++;
}
void deleteAtIndex(int index) {
if (index < 0 || index >= length) return;
Node* curr = head;
//找到第 index 个节点的前驱节点
for (int i = 0; i < index; ++i) {
curr = curr->next;
}
Node* toDelete = curr->next;
curr->next = toDelete->next;
//释放被删除节点的内存
delete toDelete;
//删除节点后,链表长度 - 1
length--;
}
};
双向链表的实现
实现双向链表,即每个节点要存储本身的值、后继节点和前驱节点。除此之外,需要一个哨兵节点作为头节点 head
和一个哨兵节点作为尾节点 tail
,同时仍需要一个 size
参数保存有效节点数:
先定义节点类 DListNode
:
struct DListNode {
int val;
DLinkListNode *prev, *next;
DLinkListNode(int _val) : val(_val), prev(nullptr), next(nullptr) {}
};
初始化头节点 head
和 size
:
class MyLinkedList {
private:
int size;
DLinkListNode *head;
DLinkListNode *tail;
public:
MyLinkedList() {
this->size = 0;
this->head = new DLinkListNode(0);
this->tail = new DLinkListNode(0);
head->next = tail;
tail->prev = head;
}
};
实现 get(index)
时,先判断有效性,再通过循环来找到对应的节点的值:
int get(int index) {
if (index < 0 || index >= size) return -1;
DLinkListNode *curr;
if (index + 1 < size - index) {
curr = head;
for (int i = 0; i <= index; i++) {
curr = curr->next;
}
} else {
curr = tail;
for (int i = 0; i < size - index; i++) {
curr = curr->prev;
}
}
return curr->val;
}
同理实现增添删改的功能:
void addAtHead(int val) {
addAtIndex(0, val);
}
void addAtTail(int val) {
addAtIndex(size, val);
}
void addAtIndex(int index, int val) {
if (index > size) return;
index = max(0, index);
DLinkListNode *pred, *succ;
if (index < size - index) {
pred = head;
for (int i = 0; i < index; i++) pred = pred->next;
succ = pred->next;
} else {
succ = tail;
for (int i = 0; i < size - index; i++) succ = succ->prev;
pred = succ->prev;
}
size++;
DLinkListNode *newNode = new DLinkListNode(val);
newNode->prev = pred;
newNode->next = succ;
pred->next = newNode;
succ->prev = newNode;
}
void deleteAtIndex(int index) {
if (index < 0 || index >= size) return;
DLinkListNode *pred, *succ;
if (index < size - index) {
pred = head;
for (int i = 0; i < index; i++) pred = pred->next;
succ = pred->next->next;
} else {
succ = tail;
for (int i = 0; i < size - index - 1; i++) succ = succ->prev;
pred = succ->prev->prev;
}
size--;
DLinkListNode *p = pred->next;
pred->next = succ;
succ->prev = pred;
delete p;
}
最终代码实现如下:
struct DListNode {
int val;
DLinkListNode *prev, *next;
DLinkListNode(int _val) : val(_val), prev(nullptr), next(nullptr) {}
};
class MyLinkedList {
private:
int size;
DLinkListNode *head;
DLinkListNode *tail;
public:
MyLinkedList() {
this->size = 0;
this->head = new DLinkListNode(0);
this->tail = new DLinkListNode(0);
head->next = tail;
tail->prev = head;
}
int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
DLinkListNode *curr;
if (index + 1 < size - index) {
curr = head;
for (int i = 0; i <= index; i++) {
curr = curr->next;
}
} else {
curr = tail;
for (int i = 0; i < size - index; i++) {
curr = curr->prev;
}
}
return curr->val;
}
void addAtHead(int val) {
addAtIndex(0, val);
}
void addAtTail(int val) {
addAtIndex(size, val);
}
void addAtIndex(int index, int val) {
if (index > size) return;
index = max(0, index);
DLinkListNode *pred, *succ;
if (index < size - index) {
pred = head;
for (int i = 0; i < index; i++) pred = pred->next;
succ = pred->next;
} else {
succ = tail;
for (int i = 0; i < size - index; i++) succ = succ->prev;
pred = succ->prev;
}
size++;
DLinkListNode *newNode = new DLinkListNode(val);
newNode->prev = pred;
newNode->next = succ;
pred->next = newNode;
succ->prev = newNode;
}
};