实际上,双指针是一个很笼统的概念。只要在解题时用到了两个指针(链表指针、数组下标皆可),都可以叫做双指针方法。根据两个指针运动方式的不同,双指针方法可以分成同向指针、对向指针、快慢指针等。
当双指针方法遇到链表问题,我们主要使用快慢指针方法。很多时候链表操作遇到疑难杂症时,可以使用快慢指针,减少算法所需的时间或者空间。
标题链表问题中的双指针
我们知道,链表数据类型的特点决定了它只能单向顺序访问,而不能逆向遍历或随机访问(按下标访问)。很多时候,我们需要使用快慢指针的技巧来实现一定程序的逆向遍历,或者减少遍历的次数。这就是为什么快慢指针常用于链表问题。
链表问题中的快慢指针又可以细分为多个不同类型。下面将依次介绍快慢指针的三个应用场景。
例题 1:链表中间的节点
问题描述:
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
题解:
这道题最朴素的做法是,先遍历一次,计算链表的长度,进而计算链表中间结点的下标(注意偶数结点的时候,得到的是中间的第二个结点),然后再遍历一次,来到所要求结点的位置。
代码:
class Solution {
public:
ListNode* middleNode(ListNode* head) {
vector<ListNode*> A = {head};
while (A.back()->next != NULL)
A.push_back(A.back()->next);
return A[A.size() / 2];
}
};
更优的解法是使用快慢指针,慢指针走一步,快指针走两步,这样当快指针走到结尾的时候慢指针刚好是中间。
代码:
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
例题二:删除链表中第N个节点
问题描述:
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:
给定的 n 保证是有效的。
题解:
寻找链表的倒数第 N个元素的常规做法是,先遍历一遍链表,计算链表的长度 K。这样就可以计算出要找第K-N 个元素,再遍历一遍链表即可。
这里同样可以使用快慢指针方法减少一次遍历。不过这次两个指针速度相同,只是间隔一定距离。快指针先前进 N个元素,然后两个指针同样速度前进。这样当快指针到达链表尾部时,慢指针正好到达链表的倒数第 N个元素。
代码:
方法一:先遍历求出长度,再从前往后遍历找到并删除
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
int len=1;
ListNode* temp=head;
while(temp->next){
temp=temp->next;
len++;
}
if(n==len) return head->next; //如果是第一个,则直接返回head->next;
int m=len-n-1; //倒数第n个,就是从头开始第len-n+1个,但是我们要保存他的前一个数字,并且数字是从1开始的;这里减了一个2
ListNode* pre=head,* now=head->next;
while(m>0){
pre=pre->next;
now=now->next;
m--;
}
pre->next=now->next;
return head;
}
};
方法一 的优化:
class Solution
{
public:
ListNode *removeNthFromEnd(ListNode *head, int n)
{
vector<ListNode *> v;
ListNode* cur=head;
while(true){
v.push_back(cur);
cur=cur->next;
if(cur==NULL){
break;
}
}
int total=v.size();
int pos=total-n+1;
if(n==total){
head=head->next;
return head;
}
if(n==1){
v[total-2]->next=NULL;
return head;
}
v[pos-2]->next=v[pos];
return head;
}
};
方法二:快慢指针
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n)
{
if(!head |!head->next) return NULL;
ListNode* fast = head;
ListNode* slow = head;
for(int i = 0;i < n;i++)
{
fast = fast->next;
}
if(!fast)
{
return head->next;
}
while(fast -> next != NULL)
{
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return head;
}
};
例题三:前文写过的环形链表,这里不再赘述
代码如下:
/**
* 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 || !head->next)
{
return false;
}
ListNode * fast = head;
ListNode * slow = head;
while(fast && fast ->next)
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast)
{
return true;
}
}
return false;
}
};
例题4 环形链表Ⅱ
问题描述:
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。
题解:
假设非环部分的长度是x,从环起点到相遇点的长度是y。环的长度是c。
现在走的慢的那个指针走过的长度肯定是x+n1c+y,走的快的那个指针的速度是走的慢的那个指针速度的两倍。这意味着走的快的那个指针走的长度是2(x+n1c+y)。
还有一个约束就是走的快的那个指针比走的慢的那个指针多走的路程一定是环长度的整数倍。根据上面那个式子可以知道2(x+n1c+y)-x+n1c+y=x+n1c+y=n2c。
所以有x+y=(n2-n1)*c,这意味着什么?我们解读下这个数学公式:非环部分的长度+环起点到相遇点之间的长度就是环的整数倍。这在数据结构上的意义是什么?现在我们知道两个指针都在离环起点距离是y的那个相遇点,而现在x+y是环长度的整数倍,这意味着他们从相遇点再走x距离就刚刚走了很多圈,这意味着他们如果从相遇点再走x就到了起点。
那怎么才能再走x步呢?答:让一个指针从头部开始走,另一个指针从相遇点走,等这两个指针相遇那就走了x步此时就是环的起点。
代码:
/**
* 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) {
ListNode* fastPtr=head, *slowPtr=head;
// 让fast与slow指针第一次相遇
while (fastPtr!=NULL && fastPtr->next!=NULL)
{
fastPtr = fastPtr->next->next;
slowPtr = slowPtr->next;
if (fastPtr==slowPtr)
{
// 从相遇点再走“非环部分长度”一定可以再次走到环起点
fastPtr = head;
while (fastPtr != slowPtr)
{
fastPtr = fastPtr->next;
slowPtr = slowPtr->next;
}
return fastPtr;
break;
}
}
return nullptr;
}
};