LeetCode--链表2-双指针问题
思考问题:
判断一个链表是否有环
列举几种情况:
graph LR
A-->B
B-->C
C-->D
D-->E
E-->C
graph LR
A-->B
B-->A
你可能已经使用哈希表提出了解决方案。但是,使用双指针技巧有一个更有效的解决方案。在阅读接下来的内容之前,试着自己仔细考虑一下。
想象一下,有两个速度不同的跑步者。如果他们在直路上行驶,快跑者将首先到达目的地。但是,如果它们在圆形跑道上跑步,那么快跑者如果继续跑步就会追上慢跑者。
这正是我们在链表中使用两个速度不同的指针时会遇到的情况:
如果没有环,快指针将停在链表的末尾。
如果有环,快指针最终将与慢指针相遇。
所以剩下的问题是:
这两个指针的适当速度应该是多少?
一个安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。
那其他选择呢?它们有用吗?它们会更高效吗?
题1 环形链表1(简单)
/**
* 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) {
// 当链表中没有节点或只有一个节点时 false
if(!head || !head->next)
return false;
// 两个节点的初始值都是head
ListNode* p1 = head;
ListNode* p2 = head;
//
while( p1->next && p2->next)
{
if(p1->next == p2->next->next)
return true;
p1 = p1->next;
p2 = p2->next->next;
if( !p1 || !p2 )
return false;
}
return false;
}
};
题2 环形链表2(中等题)
自己的思路:
巴拉巴拉
然而
自己的思路并不对...
看看人家的想法好了
graph LR
0-->1
1-->2
2-->3
3-->4
4-->5
5-->2
剑指offer上这道题的思路,主要就是运用双指针,起点不同。
设环内节点个数为n,那就一个从0节点出发,另一个从第n+1个节点出发。
相遇处,就是入口处。
说白了就是带环的相遇问题。
所以这道题需要解决几个问题
- 确定链表是否有环
- 确定链表内节点个数
- 确定入口节点
/**
* 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) {
int num = counter(head);
if ( num == 0)
return NULL;
ListNode* pa = head;
ListNode* pb = head;
for (int i = 0 ; i < num; i++)
{
pb = pb->next;
}
while (pa->next && pa->next)
{
if(pa == pb)
return pb;
pa = pa->next;
pb = pb->next;
}
return NULL;
}
int counter( ListNode* head )
{
// 链表为空或链表中只有一个节点-->不存在环-返回0
if( !head || !head->next )
return 0;
// 设置双指针
ListNode* p1 = head;
ListNode* p2 = head;
//
int count = 0;
while( p1->next && p2->next )
{
// 若p1和P2即将相遇,重新赋值,并开始计数
if( p1->next == p2->next->next)
{
p1 = p1->next;
p2 = p1->next;
count = 2;
while(p2->next)
{
if( p1 == p2 )
{
return count;
}
p2 = p2->next;
count ++;
}
}
p1 = p1->next ;
p2 = p2->next->next;
if(!p1||!p2)
return 0;
}
return 0;
}
};
超出时间限制-
修改为下面的代码,通过了测试;
修改内容:
- 避免了双重的while循环
- 避免while的循环的终止条件是真值
/**
* 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* pmeet = meeting(head);
if ( pmeet == NULL)
return NULL;
// 确定环内节点的个数
int count = 1 ;
ListNode* p1 = pmeet;
while( p1->next != pmeet )
{
p1 = p1->next;
count ++;
}
// 确定环的入口节点
ListNode* pa = head;
ListNode* pb = head;
for (int i = 0 ; i < count; i++)
{
pb = pb->next;
}
while ( pa != pb )
{
pa = pa->next;
pb = pb->next;
}
return pa;
}
// 确定快慢指针相遇的节点
ListNode* meeting (ListNode* head )
{
// 链表为空或链表中只有一个节点-->不存在环-返回0
if( !head || !head->next )
return NULL;
// 设置双指针
ListNode* p1 = head;
ListNode* p2 = head;
//
ListNode* meet = head;
while( p1->next && p2->next )
{
// 若p1和P2即将相遇,重新赋值,并开始计数
if( p1->next == p2->next->next)
{
meet = p1->next;
return meet;
}
p1 = p1->next ;
p2 = p2->next->next;
if(!p1||!p2)
return NULL;
}
return NULL;
}
};
题3 相交链表
graph LR
A-->B
B-->C
C-->F
E-->F
D-->E
F-->G
G-->H
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
// 如果两个链表其中任意一个为空就不会有相交节点
if( !headA || !headB )
return NULL;
// 两个链表从头节点就相交了
if( headA == headB )
return headA;
ListNode* pa = headA;
ListNode* pb = headB;
// 求两个链表的长度
int numa = counter(headA);
int numb = counter(headB);
// 哪一个链表长,其指针就往前步进长度差的步长
int step = 0;
if ( numa >= numb )
{
step = numa - numb;
for(int i = 0; i < step ; ++i)
{
pa = pa->next;
}
}
else
{
step = numb - numa;
for(int j = 0 ; j < step; ++j)
{
pb = pb->next;
}
}
// 定位第一个相同的节点
while ( pa && pb && (pa != pb) )
{
pa = pa->next;
pb = pb->next;
}
return pb;
// 第二种循环的写法
/*
while ( pa && pb )
{
if ( pa == pb )
return pa;
pa = pa->next;
pb = pb->next;
}
return NULL;
*/
}
// 返回单链表中的节点数
int counter(ListNode* head)
{
ListNode* p = head;
int count = 1;
if( !p )
return 0;
if( !p->next )
return 1;
while( p->next )
{
p = p->next;
++count;
}
return count;
}
};
题4 删除链表中的倒数第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) {
//
if( !head || n <= 0 )
return NULL;
// 建立一个辅助栈
stack<ListNode*> nodes;
// 遍历链表,依次放入栈中
ListNode* p = head;
while(p)
{
nodes.push(p);
p = p->next;
}
if(n == 1)
{
nodes.pop();
ListNode* pend = nodes.top();
pend->next = nullptr;
return head;
}
// 遍历栈中的节点到第n-1个节点
int i = 1;
while ( i != n-1 && n > 1 )
{
if(nodes.empty())
return NULL;
nodes.pop();
++i;
}
ListNode* pe = nodes.top();
nodes.pop();
nodes.pop();
ListNode* ps = nodes.top();
ps->next = pe;
return head;
}
};
测试用例通过,但是提交解答报错
Line 152: Char 17: runtime error: reference binding to misaligned address 0xbebebebebebec0b6 for type 'struct ListNode *', which requires 8 byte alignment (stl_deque.h)
修改后代码如下:
/**
* 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 || n <= 0 )
return NULL;
// 建立一个辅助栈
stack<ListNode*> nodes;
// 遍历链表,依次放入栈中
ListNode* p = head;
while(p)
{
nodes.push(p);
p = p->next;
}
// 倒数第1个节点
if(n == 1)
{
nodes.pop();
if(nodes.empty())
return NULL;
ListNode* pend = nodes.top();
pend->next = NULL;
return head;
}
// 倒数n-1个节点之前的节点出栈
int i = 1;
while ( i != n-1 && n >= 2 )
{
if(nodes.empty())
return NULL;
nodes.pop();
++i;
}
// 得到第n-1个节点,使其出栈
ListNode* pe = nodes.top();
nodes.pop();
// 第n个节点出栈
nodes.pop();
// 如果倒数第n个节点之前再无节点,head = pe
if(nodes.empty())
{
head = pe;
return head;
}
ListNode* ps = nodes.top();
ps->next = pe;
return head;
}
};
使用辅助栈的时候,代码的鲁棒性要十分注意
出栈后,栈是否为空一定要注意!!!
小结 - 链表中的双指针问题
代码模板:
// Initialize slow & fast pointers
ListNode* slow = head;
ListNode* fast = head;
/**
* Change this condition to fit specific problem.
* Attention: remember to avoid null-pointer error
**/
while (slow && fast && fast->next) {
slow = slow->next; // move slow pointer one step each time
fast = fast->next->next; // move fast pointer two steps each time
if (slow == fast) { // change this condition to fit specific problem
return true;
}
}
return false; // change return value to fit specific problem
提示
它与我们在数组中学到的内容类似。但它可能更棘手而且更容易出错。你应该注意以下几点:
-
在调用 next 字段之前,始终检查节点是否为空。
获取空节点的下一个节点将导致空指针错误。例如,在我们运行 fast = fast.next.next 之前,需要检查 fast 和 fast.next 不为空。 -
仔细定义循环的结束条件。运行几个示例,以确保你的结束条件不会导致无限循环。在定义结束条件时,你必须考虑我们的第一点提示。
复杂度分析
空间复杂度分析容易。如果只使用指针,而不使用任何其他额外的空间,那么空间复杂度将是 O(1)。但是,时间复杂度的分析比较困难。为了得到答案,我们需要分析运行循环的次数。
在前面的查找循环示例中,假设我们每次移动较快的指针 2 步,每次移动较慢的指针 1 步。
如果没有循环,快指针需要 N/2 次才能到达链表的末尾,其中 N 是链表的长度。
如果存在循环,则快指针需要 M 次才能赶上慢指针,其中 M 是列表中循环的长度。
显然,M <= N 。所以我们将循环运行 N 次。对于每次循环,我们只需要常量级的时间。因此,该算法的时间复杂度总共为 O(N)。
自己分析其他问题以提高分析能力。别忘了考虑不同的条件。如果很难对所有情况进行分析,请考虑最糟糕的情况。