LeetCode--鏈表2-雙指針問題


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個節點出發。
相遇處,就是入口處。
說白了就是帶環的相遇問題。

所以這道題需要解決幾個問題

  1. 確定鏈表是否有環
  2. 確定鏈表內節點個數
  3. 確定入口節點
/**
 * 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;
    }
};

超出時間限制-
修改為下面的代碼,通過了測試;

修改內容:
  1. 避免了雙重的while循環
  2. 避免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

提示

它與我們在數組中學到的內容類似。但它可能更棘手而且更容易出錯。你應該注意以下幾點:

  1. 在調用 next 字段之前,始終檢查節點是否為空。
    獲取空節點的下一個節點將導致空指針錯誤。例如,在我們運行 fast = fast.next.next 之前,需要檢查 fast 和 fast.next 不為空。

  2. 仔細定義循環的結束條件。運行幾個示例,以確保你的結束條件不會導致無限循環。在定義結束條件時,你必須考慮我們的第一點提示。

復雜度分析

空間復雜度分析容易。如果只使用指針,而不使用任何其他額外的空間,那么空間復雜度將是 O(1)。但是,時間復雜度的分析比較困難。為了得到答案,我們需要分析運行循環的次數。

在前面的查找循環示例中,假設我們每次移動較快的指針 2 步,每次移動較慢的指針 1 步。

如果沒有循環,快指針需要 N/2 次才能到達鏈表的末尾,其中 N 是鏈表的長度。
如果存在循環,則快指針需要 M 次才能趕上慢指針,其中 M 是列表中循環的長度。

顯然,M <= N 。所以我們將循環運行 N 次。對於每次循環,我們只需要常量級的時間。因此,該算法的時間復雜度總共為 O(N)。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM