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)。
自己分析其他問題以提高分析能力。別忘了考慮不同的條件。如果很難對所有情況進行分析,請考慮最糟糕的情況。