目录
快慢指针的思想
快慢指针的思想并不难理解。正如它的名字一样,快慢指针就是指两个指针,一个跑得快,一个跑得慢。这里所谓的快和慢指的是移动的步长,比如每次让一个指针向前走两步,而让另一个指针向前走一步,那么前面那个指针就是快指针,后面的就是慢指针。
P.S. 在这篇blog中,快慢指针也可广义地指一个在前、一个在后的指针,下面将要讲到的leetcode #19就是使用了这种方法。
Leetcode #19
题目描述
思路1:哈希
一种比较简单的想法是建立一个指针数组,使得每个链表结点的序号与结点本身有一个映射关系。因为在该题中有可能删除头结点(如示例2),所以我们可以用一个指向head的prehead结点作为指针数组的第0项。随后,我们可以根据数学关系取倒数第n项。然后使前一个结点指向后一个结点即可。代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* mp[32]; //题目最大size是30,多一个头结点,多一个nullptr
int i=0;
ListNode* curr = new ListNode(-1, head);
while(curr){
mp[i] = curr;
i++;
curr = curr->next;
}
mp[i] = nullptr;
mp[i-n-1]->next = mp[i-n+1];
return mp[0]->next;
}
};
思路2:快慢指针
另外一种方法呢,就是本文介绍的快慢指针了。我们让快指针先起跑,我们说的倒数第n个,其实也就是和最后一个结点相差n-1个的结点。那么,一开始我们让fast和slow指针都指向头结点,然后让fast指针跑n-1步,此时fast指针和slow指针就相差n-1个结点了。接下来每次让fast和slow指针都前进一步,当fast到达最后一个结点时(即 fast->next 为nullptr时),slow 指针即为倒数第n个结点。因为我们要对slow进行删除操作,所以在每一次循环中,我们都要把slow前一个结点(即prev)记录下来,最后直接让 prev->next 指向 slow->next 即可。
P.S. 最后要注意删除头结点的特殊情况。当然,可以通过设置一个新的头结点指向head来解决这个问题,但是由于笔者较懒,就不改进这个代码了2333。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *fast, *slow;
ListNode* ans = head;
fast = slow = head;
ListNode* prev = slow;
for(int i=1; i<n; i++){
fast = fast->next;
}
while(fast->next){
fast = fast->next;
prev = slow;
slow = slow->next;
}
if(prev != slow) prev->next = slow->next;
else ans = slow->next; //特殊情况,删除头结点
return ans;
}
};
事实上,这种做法并没有在时间复杂度上比第一种做法好多少,不过是一个很好的理解快慢指针的例子。
Leetcode #876
题目描述
思路与代码
与#19非常相似,但有些许不同。这一次我们用的快慢指针是真正意义上的“快”和“慢”了,快指针一次走两步,而慢指针一次走一步。这种找中间元素的情况,经常需要分奇偶讨论。据题意可得,在偶数结点的情况下,我们会返回中间两个靠后的哪一个结点。那么我们来分析一下,对于奇数结点的情况,结点移动的过程如下图所示:
可见,总结点数为奇数时,当fast指向最后一个结点时(即 fast->next == nullptr),slow正好指向中间的结点。接下来我们看一下结点总数为偶数的情况:
可见,当总结点数为偶数时,fast指向nullptr时,slow正好指向题目要求的“中间结点”。根据这种思路,我们可以编写出如下代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* middleNode(ListNode* head) {
if(!head || !head->next) return head;
ListNode* fast = head;
ListNode* slow = head;
while(fast && fast->next){
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
Leetcode #141
题目描述
思路1:哈希
很容易想到,我们可以建立一个链表结点到bool型或int型的map,随后遍历整个链表,如果在遍历过程中,某一元素已经被访问过,那么这个链表必定是有环的。代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head == NULL) return false;
unordered_map<ListNode*, bool> mp;
while(head != NULL){
if(mp[head]) return true;
mp[head] = true;
head = head->next;
}
return false;
}
};
思路二:快慢指针
这个方法非常有趣。先给出结论:对于存在环的链表,快慢指针一定会在某一时刻相遇。
先给出比较通俗的理解:我们知道,快指针每一次会比慢指针多走一步,那么如果在一个环里,快指针一定可以追上慢指针(因为在一个环中,快指针与慢指针之间差的步数是有限的)。
笔者在这里尝试给出较为数学一点的证明。
要证明快慢指针会相遇,即证明: ∃ s ∈ Z \exist s\in Z ∃s∈Z,使得slow指针走到第 s − 1 s-1 s−1步时,与fast会相遇。
假设整个链表的结点数为
n
n
n,环中包含的结点数为
k
k
k,
s
s
s为slow结点的序号(绝对序号,即总共走了
s
−
1
s-1
s−1步),
f
f
f为fast结点的绝对序号。那么,根据快指针每一次比慢指针多走一步,慢指针走了
s
−
1
s-1
s−1步后,快指针比慢指针多走了
s
−
1
s-1
s−1步,我们有:
f
−
s
=
s
−
1
f-s = s-1
f−s=s−1
以环的入口为第一个结点,假设slow与fast在环的第
m
m
m个结点相遇。有以下关系:
[
f
−
(
n
−
k
)
]
%
k
=
m
[
s
−
(
n
−
k
)
]
%
k
=
m
[f-(n-k)] \%k = m\\ [s-(n-k)]\%k = m
[f−(n−k)]%k=m[s−(n−k)]%k=m
假设fast走了
a
a
a圈,slow走了
b
b
b圈,那么:
f
−
(
n
−
k
)
=
a
k
+
m
s
−
(
n
−
k
)
=
b
k
+
m
f-(n-k) = ak+m\\ s-(n-k)=bk+m
f−(n−k)=ak+ms−(n−k)=bk+m
联立以上式子,可得:
s
=
(
a
−
b
)
k
+
1
s=(a-b)k+1
s=(a−b)k+1
显然,fast一定比slow先跑完一整圈,所以
a
>
b
a>b
a>b,则当slow走了
(
a
−
b
)
k
(a-b)k
(a−b)k步之后,fast和slow会相遇。
总而言之,fast会追上slow。代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head == NULL || head->next == NULL) return false;
ListNode* fast = head->next;
ListNode* slow = head;
while(fast != slow){
if(fast == NULL || fast->next == NULL) return false;
fast = fast->next->next;
slow = slow->next;
}
return true;
}
};
Leetcode #142
题目描述
思路1:哈希
建立链表结点到bool型变量的map,标记某结点是否已经访问过,出现过的第一个已访问过的结点即为所求。代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
unordered_map<ListNode*, bool> mp;
if(head == NULL || head->next == NULL) return NULL;
while(head != NULL){
if(mp[head]) return head;
mp[head] = true;
head = head->next;
}
return NULL;
}
};
思路2:快慢指针+双指针
这道题的思路基本都是从数学推导上得来的,这里leetcode官网给出的解答就挺不错的。示意图如下:
这里的
a
,
b
,
c
a, b, c
a,b,c指的是对应段的长度,即结点数。因为我们知道,对于总路程来讲,fast指针走过的路程是slow指针的两倍,那么有如下关系:
a
+
n
(
b
+
c
)
+
b
=
2
(
a
+
b
)
a+n(b+c)+b = 2(a+b)
a+n(b+c)+b=2(a+b)
化简可得:
a
=
c
+
(
n
−
1
)
(
b
+
c
)
.
a = c+(n-1)(b+c).
a=c+(n−1)(b+c).也就是说,如果让一个新结点从头结点出发,slow从相遇点出发,那么它们一定会在入环点相遇。(因为
c
c
c为从相遇点到入环点的距离,而
(
b
+
c
)
(b+c)
(b+c)就是整个环的长度,不管加多少个,并不会改变在环内的位置)
思路如上,代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(head == NULL) return NULL;
ListNode *fast, *slow;
fast = slow = head;
while(fast != NULL){
slow = slow->next;
if(fast->next == NULL) return NULL;
fast = fast->next->next;
if(fast == slow){
ListNode* tmp = head;
while(tmp != slow){
tmp = tmp->next;
slow = slow->next;
}
return slow;
}
}
return NULL;
}
};
Leetcode #1721
题目描述
思路与代码
找到正数第k个结点并不难,一路往下循环即可,找倒数第k个结点的过程依旧使用之前提到的快慢指针的方法。最后的交换过程需要注意,相邻两个结点的交换和不相邻结点的交换略有不同,此外,如果交换的结点是head结点,那么我们不能返回head结点。
代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapNodes(ListNode* head, int k) {
ListNode *fast, *slow, *prev1, *prev2, *tmp1, *tmp2, *curr;
fast = slow = tmp1 = tmp2 = curr = head;
ListNode *tmps1, *tmps2;
prev1 = new ListNode(-1, head);
prev2 = new ListNode(-1, head);
int cnt = 0, cnt2 = 0;
//找到正数第k个结点
while(curr != nullptr){
cnt++;
if(cnt == k){
tmp1 = curr;
break;
}
prev1 = prev1->next;
curr = curr->next;
}
//快慢指针找倒数第k个结点
for(int i=0; i<k; i++){
fast = fast->next;
}
cnt2 = 1;
while(fast){
prev2 = slow;
slow = slow->next;
fast = fast->next;
cnt2++;
}
tmp2 = slow;
if(cnt2 < k){ //如果倒数第k个结点在整数第k个结点的前面,交换tmp1, tmp2 和 prev1, prev2
tmp2 = tmp1;
tmp1 = slow;
curr = prev1;
prev1 = prev2;
prev2 = curr;
}
bool flag = false; //标记发生改变的是否为head结点
if(tmp1 == tmp2) return head;
else{
if(prev1->next == head) flag = true;
tmps1 = tmp1->next;
tmps2 = tmp2->next;
if(prev2 == tmp1){ //如果两个结点相邻
prev2->next = tmps2;
prev1->next = tmps1;
tmp2->next = tmp1;
}
else{
prev1->next = tmp2;
prev2->next = tmp1;
tmp2->next = tmps1;
tmp1->next = tmps2;
}
}
return flag ? prev1->next : head;
}
};
总结
快慢指针可以用于寻找链表中点,寻找倒数第k个结点,判断是否有环等等,感觉贼有趣。如果上述内容有什么错误,还请各位看官直接留言,一定仔细检查勘正。