三、链表
链表是数据结构的基础部分。
1. 环形链表
环形链表是指在链表中包含一个环形的链表。如下图所示
环形链表的性质:
① 设环的长度为 R R R,快指针fast的移动是慢指针 s l o w slow slow的两倍,两者距离为 P P P,如图所示。当慢指针 s l o w slow slow进入入口点后 t t t时间内,快指针走了 2 t 2t 2t个节点,慢指针走了 t t t个节点。 当慢指针与快指针相遇时有 S + 2 t − t = n R S+2t-t=nR S+2t−t=nR即 s + t = n R s+t=nR s+t=nR
② 当两个指针在cross点相遇时,慢指针走了 S + L S+L S+L,快指针走了 L + S + n R L+S+nR L+S+nR,又因为快指针fast的移动是慢指针 s l o w slow slow的两倍,所以 2 ( L + S ) = L + S + n R 2(L+S)=L+S+nR 2(L+S)=L+S+nR,即 L + S = n R L+S=nR L+S=nR,当n=1时, L = R − S L=R-S L=R−S,所以 如果采用两个指针,一个从表头出发,一个从相遇点出发,那么它们将同时到达环入口。即二者相等时便是环入口节点
💗判断链表中是否带环
🐟 ①. 141. 环形链表
给定一个链表,判断链表中是否有环。为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
输入 | 输出 | 解释 |
---|---|---|
head = [3,2,0,-4], pos = 1 | true | 链表中有一个环,其尾部连接到第二个节点。 |
思路1:通常采用双指针的方法,当快指针fast
和慢指针slow
进入循环后,总会相遇,如果相遇,则说明链表中有环。如果没有相遇,则说明链表中没有环。
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *s=head; //s为慢指针
ListNode *t=head; //t为快指针
while(t!=NULL && t->next!=NULL){
s=s->next;
t=t->next->next; //快指针每次比慢指针多走一步
if(s==t) //快指针与慢指针相遇
return true;
}
return false;
}
};
思路2: 利用哈希表,遍历所有结点并在map
中存储每个结点的引用(或内存地址),如果当前结点的引用已经存在于map
中,说明链表中存在环,则返回true。
class Solution {
public:
bool hasCycle(ListNode *head) {
if(!head)
return false;
map<ListNode*, int> key;
ListNode* pcur = head;
while(pcur){
if(key.find(pcur) != key.end()){ //判断当前节点的引用是否存在于map中
return true;
}
else
++key[pcur]; //若不在,则将当前节点加入到map中
pcur = pcur->next;
}
return false;
}
};
💗 带环链表的环入口
在环形链表当中,计算出环形的入口节点。
🐟 ②. 142. 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
输入 | 输出 | 解释 |
---|---|---|
head = [3,2,0,-4], pos = 1 | tail connects to node index 1 | 链表中有一个环,其尾部连接到第二个节点。 |
思路: 根据循环链表的性质②,采用双指针,先计算出双指针的相遇点,然后分别从相遇点和起始点出发,当两个指针相遇时为入环的第一个节点。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(head==NULL)
return NULL;
ListNode *fast=head;
ListNode *slow=head;
while(fast!=NULL && fast->next!=NULL){ //计算双指针的相遇点
fast=fast->next->next;
slow=slow->next;
if(slow==fast) //此时slow指针为相遇点
break;
}
if(fast==NULL || fast->next==NULL)
return NULL;
ListNode *cross=slow;
ListNode *headcross=head;
while(headcross!=cross){ //分别从相遇点和起始点出发,当两个指针相遇时为入环的第一个节点
headcross=headcross->next;
cross=cross->next;
}
return headcross;
}
};
💗 环形链表的环的长度
2. 相交链表
两个链表在某一结点处相交。如下图所示
相交链表的性质:
从图中可以看出,当A与B相交时,A走了 L a L_a La路程,B走了 L b L_b Lb路程,然后一起走了 L L L路程。为了求出其相交结点,当A走到终点后,从B开始走,当B走到终点后,从A开始走。根据路程循环,则有 L a + L + L b = L b + L + L a L_a+L+L_b=L_b+L+L_a La+L+Lb=Lb+L+La,此时两指针在相交点相遇。
💗 找到相交链表相交点
🐟 ①. 160. 相交链表
编写一个程序,找到两个单链表相交的起始节点。
输入 | 输出 | 解释 |
---|---|---|
Reference of the node with value = 8 | 相交节点的值为 8 |
思路: 根据相交链表的性质,分别设置两个指针A,B,当A走到终点后,从B开始走,当B走到终点后,从A开始走。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA==nullptr|| headB==nullptr)
return nullptr;
ListNode* hA=headA;
ListNode* hB=headB;
while(hA!=hB){
hA = hA==NULL ? headB : hA->next; //当A走到终点后,从B开始走
hB = hB==NULL ? headA : hB->next; //当B走到终点后,从A开始走
}
return hA;
}
};
3.反转链表
将链表翻转。如下图所示:
💗 反转链表
🐟 ①. 206. 反转链表
输入 | 输出 | 解释 |
---|---|---|
1->2->3->4->5->NULL | 5->4->3->2->1->NULL |
思路: 利用双指针迭代,第一个指针叫 pre
,最初是指向 null 的。第二个指针 cur
指向 head,然后不断遍历 cur
。每次迭代到 cur
,都将 cur
的 next 指向 pre
,然后 pre
和 cur
前进一位。
都迭代完了(cur 变成 null 了),pre
就是最后一个节点了。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==NULL)
return NULL;
ListNode *pre=NULL;
ListNode *cur=head;
while(cur!=NULL){
ListNode *temp=cur->next;
cur->next=pre;
pre=cur; //交换指针,指向转换后链表的头部,pre前进一位
cur=temp; //交换指针,指向转换前链表的头部,cur前进一位
}
return pre;
}
};
4. 链表中间结点
💗 链表中间结点
🐟 ①. 876. 链表的中间结点
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点
输入 | 输出 | 解释 |
---|---|---|
[1,2,3,4,5] | 此列表中的结点 3 |
**思路:**利用快慢双指针,快指针移动速率是慢指针的两倍,当快指针移动到表尾时,慢指针正好移动到链表中间结点。
class Solution {
public:
ListNode* middleNode(ListNode* head) {
if(head==NULL)
return NULL;
ListNode *fast=head;
ListNode *slow=head;
while(fast!=NULL && fast->next!=NULL){
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
};