單鏈表
- 單鏈表問題與思路
找出單鏈表的倒數(shù)第K個元素(僅允許遍歷一遍鏈表)
使用指針追趕的方法。定義兩個指針fast和slow,fast先走K步,然后fast和slow同時繼續(xù)走。當(dāng)fast到鏈表尾部時,slow指向倒數(shù)第K個。注意要考慮鏈表長度應(yīng)該大于K找出單鏈表的中間元素(僅允許遍歷一遍鏈表)
使用指針追趕的方法。fast每次走一步,slow每次走兩步。當(dāng)fast到鏈表尾部時,slow指向鏈表的中間元素。
判斷單鏈表是否有環(huán)?
方法一:使用指針追趕的方法。slow指針每次走一步,fast指針每次走兩步。如存在環(huán),則兩者相遇;如不存在環(huán),fast遇到NULL退出。
方法二:使用p、q兩個指針,p總是向前走,但q每次都從頭開始走。如何知道環(huán)的長度?
記錄下碰撞點(或者找在環(huán)中任意一結(jié)點都可以),讓slow從碰撞點開始,繞著環(huán)走一圈,再次到碰撞點的位置時,所走過的結(jié)點數(shù)就是環(huán)的長度s。如何找到環(huán)的入口?
分別從碰撞點、頭指針開始走,相遇的那個點就是連接點。-
判斷兩個鏈表(無環(huán))是否相交?
方法一:采用暴力的方法,遍歷兩個鏈表,在遍歷的過程中進行比較,看節(jié)點是否相同。方法二:兩鏈表一旦相交,相交節(jié)點一定有相同的內(nèi)存地址,因此利用內(nèi)存地址建立哈希表,如此通過判斷兩個鏈表中是否存在內(nèi)存地址相同的節(jié)點判斷兩個鏈表是否相交。具體做法是:遍歷第一個鏈表,并利用地址建立哈希表,遍歷第二個鏈表,看看地址哈希值是否和第一個表中的節(jié)點地址值有相同即可判斷兩個鏈表是否相交。時間復(fù)雜度O((length(A)+ length(B))
方法三:問題轉(zhuǎn)化法。先遍歷第一個鏈表到其尾部,然后將尾部的原本指向NULL的next指針指向第二個鏈表。這樣兩個鏈表就合成了一個鏈表,問題轉(zhuǎn)變?yōu)榕袛嘈碌逆湵硎欠裼协h(huán)?
方法四:一旦兩個鏈表相交,那么兩個鏈表從相交節(jié)點開始到尾節(jié)點一定都是相同的節(jié)點。所以,如果他們相交的話,那么他們最后的一個節(jié)點一定是相同的,因此分別遍歷到兩個鏈表的尾部,然后判斷他們是否相同。
如何知道兩個單鏈表(可能有環(huán))是否相交
思路:根據(jù)兩個鏈表是否有環(huán)來分別處理,若相交這個環(huán)屬于兩個鏈表共有
(1)如果兩個鏈表都沒有環(huán)。
(2)一個有環(huán),一個沒環(huán)。肯定不相交
(3)兩個都有環(huán)。
①求出A的環(huán)入口,判斷其是否在B鏈表上,如果在,則相交。
② 在A鏈表上,使用指針追趕的方法,找到兩個指針碰撞點,之后判斷碰撞點是否在B鏈表上。如果在,則相交。尋找兩個相交鏈表的第一個公共節(jié)點
方法一:最簡單的方法就是先順序訪問其中一個鏈表,在每訪問一個節(jié)點時,都對另外一個鏈表進行遍歷,直到找到一個相等的節(jié)點位置,如果鏈表長度分別是m,n 則時間復(fù)雜度為O(mn)。
方法二:(兩種情況)
① 相交的點,在環(huán)外。如果兩個鏈表有公共節(jié)點,那么該公共節(jié)點之后的所有節(jié)點都是兩個鏈表所共有的,所以長度一定也是相等的,如果兩個鏈表的總長度是相等的,那么我們對兩個鏈表進行遍歷,則一定同時到達(dá)第一個公共節(jié)點。但是鏈表的長度實際上不一定相同,所以計算出兩個鏈表的長度之差n,然后讓長的那個鏈表先移動n步,短的鏈表再開始向后遍歷,這樣他們一定同時到達(dá)第一個公共節(jié)點,我們只需要在向后移動的時候比較兩個鏈表的節(jié)點是否相等就可以獲得第一個公共節(jié)點。時間復(fù)雜度是O(m+n)。
② 相交的點在環(huán)內(nèi)。當(dāng)交點在環(huán)中時,此時的交點可以是A鏈表中的環(huán)入口點,也可以是B鏈表中環(huán)入口點。這是因為如果把B看出一個完整的鏈表,而A指向了B鏈表,則此時交點是A的環(huán)入口點。反之交點是鏈表B的環(huán)入口點。
思路:根據(jù)上述分析,可以直接求出A的環(huán)入口點或者B的環(huán)入口點就可以了。
- 單鏈表代碼實現(xiàn)
鏈表結(jié)點聲明如下:
struct ListNode{
int m_nKey;
ListNode * m_pNext;
};
- 求單鏈表中結(jié)點的個數(shù)
這是最最基本的了,應(yīng)該能夠迅速寫出正確的代碼,注意檢查鏈表是否為空。時間復(fù)雜度為O(n)。參考代碼如下:
1. // 求單鏈表中結(jié)點的個數(shù)
2. unsigned int GetListLength(ListNode * pHead)
3. {
4. if(pHead == NULL)
5. return 0;
6.
7. unsigned int nLength = 0;
8. ListNode * pCurrent = pHead;
9. while(pCurrent != NULL)
10. {
11. nLength++;
12. pCurrent = pCurrent->m_pNext;
13. }
14. return nLength;
15. }
- 找到單鏈表的中間節(jié)點:
SListNode* FindMidNode(SListNode* pHead)//pHead是鏈表的頭節(jié)點
{
SListNode* fast = pHead;
SListNode* slow = pHead;
while (fast->next)
{
slow = slow->next;
fast = fast->next;
if (fast->next)
{
fast = fast->next;
}
else
{
break;
}
}
return slow;
}
- 判斷一個鏈表是否帶環(huán),若帶返回環(huán)的入口點(較難)
上面已經(jīng)說過,通過快慢指針可以找到單鏈表中的任何節(jié)點,這也是通過驗證的。那么原理上也可以判斷鏈表是否帶環(huán):
如果帶環(huán),快慢指針一定會在某個節(jié)點處相遇。(fast一直在環(huán)里轉(zhuǎn),總有一個時刻,slow追上fast,相遇)
1. SListNode* WhetherRing(SListNode* pHead)//判斷是否帶環(huán),若帶,返回相遇節(jié)點
2. {
3. if (pHead == NULL||pHead->next == NULL)
4. return NULL;
5. SListNode* fast = pHead;
6. SListNode* slow = pHead;
7. while (fast)
8. {
9. slow = slow->next;
10. fast = fast->next;
11. if (fast)
12. {
13. fast = fast->next;
14. }
15. else
16. {
17. return NULL;//沒有環(huán)
18. }
19. if (fast == slow)
20. {
21. return slow;//有環(huán)
22. }
23. }
24. return NULL;
25. }
如果鏈表帶環(huán)的話,我們就可以求出相遇的節(jié)點。然后從相遇節(jié)點處斷開帶環(huán)鏈表,一個指針從鏈表投節(jié)點處開始往后遍歷,一個指針從斷開處往后遍歷,相遇節(jié)點處就是環(huán)的入口點
1. SListNode* GetEnterNode(SListNode* pHead)//找到鏈表環(huán)入口節(jié)點在哪(重點)
2. {
3. SListNode* start = pHead;
4. SListNode* tmp = WhetherRing(pHead);
5. while (start != tmp)
6. {
7. start = start->next;
8. tmp = tmp->next;
9. }
10. return start;
11. }
- 將單鏈表反轉(zhuǎn)
從頭到尾遍歷原鏈表,每遍歷一個結(jié)點,將其摘下放在新鏈表的最前端。注意鏈表為空和只有一個結(jié)點的情況。時間復(fù)雜度為O(n)。參考代碼如下:
1. // 反轉(zhuǎn)單鏈表
2. ListNode * ReverseList(ListNode * pHead)
3. {
4. // 如果鏈表為空或只有一個結(jié)點,無需反轉(zhuǎn),直接返回原鏈表頭指針
5. if(pHead == NULL || pHead->m_pNext == NULL)
6. return pHead;
7.
8. ListNode * pReversedHead = NULL; // 反轉(zhuǎn)后的新鏈表頭指針,初始為NULL
9. ListNode * pCurrent = pHead;
10. while(pCurrent != NULL)
11. {
12. ListNode * pTemp = pCurrent;
13. pCurrent = pCurrent->m_pNext;
14. pTemp->m_pNext = pReversedHead; // 將當(dāng)前結(jié)點摘下,插入新鏈表的最前端
15. pReversedHead = pTemp;
16. }
17. return pReversedHead;
18. }
- 查找單鏈表中的倒數(shù)第K個結(jié)點(k > 0)
最普遍的方法是,先統(tǒng)計單鏈表中結(jié)點的個數(shù),然后再找到第(n-k)個結(jié)點。注意鏈表為空,k為0,k為1,k大于鏈表中節(jié)點個數(shù)時的情況。時間復(fù)雜度為O(n)。代碼略。
這里主要講一下另一個思路,這種思路在其他題目中也會有應(yīng)用。
主要思路就是使用兩個指針,先讓前面的指針走到正向第k個結(jié)點,這樣前后兩個指針的距離差是k-1,之后前后兩個指針一起向前走,前面的指針走到最后一個結(jié)點時,后面指針?biāo)附Y(jié)點就是倒數(shù)第k個結(jié)點。
參考代碼如下:
1. // 查找單鏈表中倒數(shù)第K個結(jié)點
2. ListNode * RGetKthNode(ListNode * pHead, unsigned int k) // 函數(shù)名前面的R代表反向
3. {
4. if(k == 0 || pHead == NULL) // 這里k的計數(shù)是從1開始的,若k為0或鏈表為空返回NULL
5. return NULL;
6.
7. ListNode * pAhead = pHead;
8. ListNode * pBehind = pHead;
9. while(k > 1 && pAhead != NULL) // 前面的指針先走到正向第k個結(jié)點
10. {
11. pAhead = pAhead->m_pNext;
12. k--;
13. }
14. if(k > 1 || pAhead == NULL) // 結(jié)點個數(shù)小于k,返回NULL
15. return NULL;
16. while(pAhead->m_pNext != NULL) // 前后兩個指針一起向前走,直到前面的指針指向最后一個結(jié)點
17. {
18. pBehind = pBehind->m_pNext;
19. pAhead = pAhead->m_pNext;
20. }
21. return pBehind; // 后面的指針?biāo)附Y(jié)點就是倒數(shù)第k個結(jié)點
22. }
- 從尾到頭打印單鏈表
對于這種顛倒順序的問題,我們應(yīng)該就會想到棧,后進先出。所以,這一題要么自己使用棧,要么讓系統(tǒng)使用棧,也就是遞歸。注意鏈表為空的情況。時間復(fù)雜度為O(n)。參考代碼如下:
自己使用棧:
1. // 從尾到頭打印鏈表,使用棧
2. void RPrintList(ListNode * pHead)
3. {
4. std::stack<ListNode *> s;
5. ListNode * pNode = pHead;
6. while(pNode != NULL)
7. {
8. s.push(pNode);
9. pNode = pNode->m_pNext;
10. }
11. while(!s.empty())
12. {
13. pNode = s.top();
14. printf("%d\t", pNode->m_nKey);
15. s.pop();
16. }
17. }
使用遞歸函數(shù):
1. // 從尾到頭打印鏈表,使用遞歸
2. void RPrintList(ListNode * pHead)
3. {
4. if(pHead == NULL)
5. {
6. return;
7. }
8. else
9. {
10. RPrintList(pHead->m_pNext);
11. printf("%d\t", pHead->m_nKey);
12. }
13. }
- 已知兩個單鏈表pHead1 和pHead2 各自有序,把它們合并成一個鏈表依然有序
這個類似歸并排序。尤其注意兩個鏈表都為空,和其中一個為空時的情況。只需要O(1)的空間。時間復(fù)雜度為O(max(len1, len2))。參考代碼如下:
1. ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2)
2. {
3. if(pHead1 == NULL)
4. return pHead2;
5. if(pHead2 == NULL)
6. return pHead1;
7. ListNode * pHeadMerged = NULL;
8. if(pHead1->m_nKey < pHead2->m_nKey)
9. {
10. pHeadMerged = pHead1;
11. pHeadMerged->m_pNext = MergeSortedList(pHead1->m_pNext, pHead2);
12. }
13. else
14. {
15. pHeadMerged = pHead2;
16. pHeadMerged->m_pNext = MergeSortedList(pHead1, pHead2->m_pNext);
17. }
18. return pHeadMerged;
19. }
- 判斷一個單鏈表中是否有環(huán)
這里也是用到兩個指針。如果一個鏈表中有環(huán),也就是說用一個指針去遍歷,是永遠(yuǎn)走不到頭的。因此,我們可以用兩個指針去遍歷,一個指針一次走兩步,一個指針一次走一步,如果有環(huán),兩個指針肯定會在環(huán)中相遇。時間復(fù)雜度為O(n)。參考代碼如下:
1. bool HasCircle(ListNode * pHead)
2. {
3. ListNode * pFast = pHead; // 快指針每次前進兩步
4. ListNode * pSlow = pHead; // 慢指針每次前進一步
5. while(pFast != NULL && pFast->m_pNext != NULL)
6. {
7. pFast = pFast->m_pNext->m_pNext;
8. pSlow = pSlow->m_pNext;
9. if(pSlow == pFast) // 相遇,存在環(huán)
10. return true;
11. }
12. return false;
13. }
- 給出一單鏈表頭指針pHead和一節(jié)點指針pToBeDeleted,O(1)時間復(fù)雜度刪除節(jié)點pToBeDeleted
對于刪除節(jié)點,我們普通的思路就是讓該節(jié)點的前一個節(jié)點指向該節(jié)點的下一個節(jié)點,這種情況需要遍歷找到該節(jié)點的前一個節(jié)點,時間復(fù)雜度為O(n)。對于鏈表,鏈表中的每個節(jié)點結(jié)構(gòu)都是一樣的,所以我們可以把該節(jié)點的下一個節(jié)點的數(shù)據(jù)復(fù)制到該節(jié)點,然后刪除下一個節(jié)點即可。要注意最后一個節(jié)點的情況,這個時候只能用常見的方法來操作,先找到前一個節(jié)點,但總體的平均時間復(fù)雜度還是O(1)。參考代碼如下:
1. void Delete(ListNode * pHead, ListNode * pToBeDeleted)
2. {
3. if(pToBeDeleted == NULL)
4. return;
5. if(pToBeDeleted->m_pNext != NULL)
6. {
7. pToBeDeleted->m_nKey = pToBeDeleted->m_pNext->m_nKey; // 將下一個節(jié)點的數(shù)據(jù)復(fù)制到本節(jié)點,然后刪除下一個節(jié)點
8. ListNode * temp = pToBeDeleted->m_pNext;
9. pToBeDeleted->m_pNext = pToBeDeleted->m_pNext->m_pNext;
10. delete temp;
11. }
12. else // 要刪除的是最后一個節(jié)點
13. {
14. if(pHead == pToBeDeleted) // 鏈表中只有一個節(jié)點的情況
15. {
16. pHead = NULL;
17. delete pToBeDeleted;
18. }
19. else
20. {
21. ListNode * pNode = pHead;
22. while(pNode->m_pNext != pToBeDeleted) // 找到倒數(shù)第二個節(jié)點
23. pNode = pNode->m_pNext;
24. pNode->m_pNext = NULL;
25. delete pToBeDeleted;
26. }
27. }
28. }