輕鬆搞定面試中的連結串列題目

lostinai發表於2014-03-14

版權所有,轉載請註明出處,謝謝!
http://blog.csdn.net/walkinginthewind/article/details/7393134

連結串列是最基本的資料結構,面試官也常常用連結串列來考察面試者的基本能力,而且連結串列相關的操作相對而言比較簡單,也適合考察寫程式碼的能力。連結串列的操作也離不開指標,指標又很容易導致出錯。綜合多方面的原因,連結串列題目在面試中佔據著很重要的地位。本文對連結串列相關的面試題做了較為全面的整理,希望能對找工作的同學有所幫助。

連結串列結點宣告如下:

struct ListNode
{
    int m_nKey;
    ListNode * m_pNext;

};

題目列表:

1. 求單連結串列中結點的個數
2. 將單連結串列反轉
3. 查詢單連結串列中的倒數第K個結點(k > 0)
4. 查詢單連結串列的中間結點
5. 從尾到頭列印單連結串列
6. 已知兩個單連結串列pHead1 和pHead2 各自有序,把它們合併成一個連結串列依然有序
7. 判斷一個單連結串列中是否有環
8. 判斷兩個單連結串列是否相交
9. 求兩個單連結串列相交的第一個節點
10. 已知一個單連結串列中存在環,求進入環中的第一個節點
11. 給出一單連結串列頭指標pHead和一節點指標pToBeDeleted,O(1)時間複雜度刪除節點pToBeDeleted


詳細解答

1. 求單連結串列中結點的個數

這是最最基本的了,應該能夠迅速寫出正確的程式碼,注意檢查連結串列是否為空。時間複雜度為O(n)。參考程式碼如下:

  1. // 求單連結串列中結點的個數  
  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. }  


2. 將單連結串列反轉

從頭到尾遍歷原連結串列,每遍歷一個結點,將其摘下放在新連結串列的最前端。注意連結串列為空和只有一個結點的情況。時間複雜度為O(n)。參考程式碼如下:

  1. // 反轉單連結串列  
  2. ListNode * ReverseList(ListNode * pHead)  
  3. {  
  4.         // 如果連結串列為空或只有一個結點,無需反轉,直接返回原連結串列頭指標  
  5.     if(pHead == NULL || pHead->m_pNext == NULL)    
  6.         return pHead;  
  7.   
  8.     ListNode * pReversedHead = NULL; // 反轉後的新連結串列頭指標,初始為NULL  
  9.     ListNode * pCurrent = pHead;  
  10.     while(pCurrent != NULL)  
  11.     {  
  12.         ListNode * pTemp = pCurrent;  
  13.         pCurrent = pCurrent->m_pNext;  
  14.         pTemp->m_pNext = pReversedHead; // 將當前結點摘下,插入新連結串列的最前端  
  15.         pReversedHead = pTemp;  
  16.     }  
  17.     return pReversedHead;  
  18. }  

3. 查詢單連結串列中的倒數第K個結點(k > 0)

最普遍的方法是,先統計單連結串列中結點的個數,然後再找到第(n-k)個結點。注意連結串列為空,k為0,k為1,k大於連結串列中節點個數時的情況。時間複雜度為O(n)。程式碼略。

這裡主要講一下另一個思路,這種思路在其他題目中也會有應用。

主要思路就是使用兩個指標,先讓前面的指標走到正向第k個結點,這樣前後兩個指標的距離差是k-1,之後前後兩個指標一起向前走,前面的指標走到最後一個結點時,後面指標所指結點就是倒數第k個結點。

參考程式碼如下:

  1. // 查詢單連結串列中倒數第K個結點  
  2. ListNode * RGetKthNode(ListNode * pHead, unsigned int k) // 函式名前面的R代表反向  
  3. {  
  4.     if(k == 0 || pHead == NULL) // 這裡k的計數是從1開始的,若k為0或連結串列為空返回NULL  
  5.         return NULL;  
  6.   
  7.     ListNode * pAhead = pHead;  
  8.     ListNode * pBehind = pHead;  
  9.     while(k > 1 && pAhead != NULL) // 前面的指標先走到正向第k個結點  
  10.     {  
  11.         pAhead = pAhead->m_pNext;  
  12.         k--;  
  13.     }  
  14.     if(k > 1 || pAhead == NULL)     // 結點個數小於k,返回NULL  
  15.         return NULL;  
  16.     while(pAhead->m_pNext != NULL)  // 前後兩個指標一起向前走,直到前面的指標指向最後一個結點  
  17.     {  
  18.         pBehind = pBehind->m_pNext;  
  19.         pAhead = pAhead->m_pNext;  
  20.     }  
  21.     return pBehind;  // 後面的指標所指結點就是倒數第k個結點  
  22. }  

4. 查詢單連結串列的中間結點

此題可應用於上一題類似的思想。也是設定兩個指標,只不過這裡是,兩個指標同時向前走,前面的指標每次走兩步,後面的指標每次走一步,前面的指標走到最後一個結點時,後面的指標所指結點就是中間結點,即第(n/2+1)個結點。注意連結串列為空,連結串列結點個數為1和2的情況。時間複雜度O(n)。參考程式碼如下:

  1. // 獲取單連結串列中間結點,若連結串列長度為n(n>0),則返回第n/2+1個結點  
  2. ListNode * GetMiddleNode(ListNode * pHead)  
  3. {  
  4.     if(pHead == NULL || pHead->m_pNext == NULL) // 連結串列為空或只有一個結點,返回頭指標  
  5.         return pHead;  
  6.   
  7.     ListNode * pAhead = pHead;  
  8.     ListNode * pBehind = pHead;  
  9.     while(pAhead->m_pNext != NULL) // 前面指標每次走兩步,直到指向最後一個結點,後面指標每次走一步  
  10.     {  
  11.         pAhead = pAhead->m_pNext;  
  12.         pBehind = pBehind->m_pNext;  
  13.         if(pAhead->m_pNext != NULL)  
  14.             pAhead = pAhead->m_pNext;  
  15.     }  
  16.     return pBehind; // 後面的指標所指結點即為中間結點  
  17. }  


5. 從尾到頭列印單連結串列

對於這種顛倒順序的問題,我們應該就會想到棧,後進先出。所以,這一題要麼自己使用棧,要麼讓系統使用棧,也就是遞迴。注意連結串列為空的情況。時間複雜度為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. }  

使用遞迴函式:

  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. }  


6. 已知兩個單連結串列pHead1 和pHead2 各自有序,把它們合併成一個連結串列依然有序

這個類似歸併排序。尤其注意兩個連結串列都為空,和其中一個為空時的情況。只需要O(1)的空間。時間複雜度為O(max(len1, len2))。參考程式碼如下:

  1. // 合併兩個有序連結串列  
  2. ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2)  
  3. {  
  4.     if(pHead1 == NULL)  
  5.         return pHead2;  
  6.     if(pHead2 == NULL)  
  7.         return pHead1;  
  8.     ListNode * pHeadMerged = NULL;  
  9.     if(pHead1->m_nKey < pHead2->m_nKey)  
  10.     {  
  11.         pHeadMerged = pHead1;  
  12.         pHeadMerged->m_pNext = NULL;  
  13.         pHead1 = pHead1->m_pNext;  
  14.     }  
  15.     else  
  16.     {  
  17.         pHeadMerged = pHead2;  
  18.         pHeadMerged->m_pNext = NULL;  
  19.         pHead2 = pHead2->m_pNext;  
  20.     }  
  21.     ListNode * pTemp = pHeadMerged;  
  22.     while(pHead1 != NULL && pHead2 != NULL)  
  23.     {  
  24.         if(pHead1->m_nKey < pHead2->m_nKey)  
  25.         {  
  26.             pTemp->m_pNext = pHead1;  
  27.             pHead1 = pHead1->m_pNext;  
  28.             pTemp = pTemp->m_pNext;  
  29.             pTemp->m_pNext = NULL;  
  30.         }  
  31.         else  
  32.         {  
  33.             pTemp->m_pNext = pHead2;  
  34.             pHead2 = pHead2->m_pNext;  
  35.             pTemp = pTemp->m_pNext;  
  36.             pTemp->m_pNext = NULL;  
  37.         }  
  38.     }  
  39.     if(pHead1 != NULL)  
  40.         pTemp->m_pNext = pHead1;  
  41.     else if(pHead2 != NULL)  
  42.         pTemp->m_pNext = pHead2;  
  43.     return pHeadMerged;  
  44. }  

也有如下遞迴解法:

  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. }  


7. 判斷一個單連結串列中是否有環

這裡也是用到兩個指標。如果一個連結串列中有環,也就是說用一個指標去遍歷,是永遠走不到頭的。因此,我們可以用兩個指標去遍歷,一個指標一次走兩步,一個指標一次走一步,如果有環,兩個指標肯定會在環中相遇。時間複雜度為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) // 相遇,存在環  
  10.             return true;  
  11.     }  
  12.     return false;  
  13. }  


8. 判斷兩個單連結串列是否相交

如果兩個連結串列相交於某一節點,那麼在這個相交節點之後的所有節點都是兩個連結串列所共有的。也就是說,如果兩個連結串列相交,那麼最後一個節點肯定是共有的。先遍歷第一個連結串列,記住最後一個節點,然後遍歷第二個連結串列,到最後一個節點時和第一個連結串列的最後一個節點做比較,如果相同,則相交,否則不相交。時間複雜度為O(len1+len2),因為只需要一個額外指標儲存最後一個節點地址,空間複雜度為O(1)。參考程式碼如下:

  1. bool IsIntersected(ListNode * pHead1, ListNode * pHead2)  
  2. {  
  3.         if(pHead1 == NULL || pHead2 == NULL)  
  4.                 return false;  
  5.   
  6.     ListNode * pTail1 = pHead1;  
  7.     while(pTail1->m_pNext != NULL)  
  8.         pTail1 = pTail1->m_pNext;  
  9.   
  10.     ListNode * pTail2 = pHead2;  
  11.     while(pTail2->m_pNext != NULL)  
  12.         pTail2 = pTail2->m_pNext;  
  13.     return pTail1 == pTail2;  
  14. }  

9. 求兩個單連結串列相交的第一個節點
對第一個連結串列遍歷,計算長度len1,同時儲存最後一個節點的地址。
對第二個連結串列遍歷,計算長度len2,同時檢查最後一個節點是否和第一個連結串列的最後一個節點相同,若不相同,不相交,結束。
兩個連結串列均從頭節點開始,假設len1大於len2,那麼將第一個連結串列先遍歷len1-len2個節點,此時兩個連結串列當前節點到第一個相交節點的距離就相等了,然後一起向後遍歷,知道兩個節點的地址相同。

時間複雜度,O(len1+len2)。參考程式碼如下:

  1. ListNode* GetFirstCommonNode(ListNode * pHead1, ListNode * pHead2)  
  2. {  
  3.     if(pHead1 == NULL || pHead2 == NULL)  
  4.         return NULL;  
  5.   
  6.     int len1 = 1;  
  7.     ListNode * pTail1 = pHead1;  
  8.     while(pTail1->m_pNext != NULL)  
  9.     {  
  10.         pTail1 = pTail1->m_pNext;  
  11.         len1++;  
  12.     }  
  13.   
  14.     int len2 = 1;  
  15.     ListNode * pTail2 = pHead2;  
  16.     while(pTail2->m_pNext != NULL)  
  17.     {  
  18.         pTail2 = pTail2->m_pNext;  
  19.         len2++;  
  20.     }  
  21.   
  22.     if(pTail1 != pTail2) // 不相交直接返回NULL  
  23.         return NULL;  
  24.   
  25.     ListNode * pNode1 = pHead1;  
  26.     ListNode * pNode2 = pHead2;  
  27.         // 先對齊兩個連結串列的當前結點,使之到尾節點的距離相等  
  28.     if(len1 > len2)  
  29.     {  
  30.         int k = len1 - len2;  
  31.         while(k--)  
  32.             pNode1 = pNode1->m_pNext;  
  33.     }  
  34.     else  
  35.     {  
  36.         int k = len2 - len1;  
  37.         while(k--)  
  38.             pNode2 = pNode2->m_pNext;  
  39.     }  
  40.     while(pNode1 != pNode2)  
  41.     {  
  42.         pNode1 = pNode1->m_pNext;  
  43.         pNode2 = pNode2->m_pNext;  
  44.     }  
  45.         return pNode1;  
  46. }  

10. 已知一個單連結串列中存在環,求進入環中的第一個節點

首先判斷是否存在環,若不存在結束。在環中的一個節點處斷開(當然函式結束時不能破壞原連結串列),這樣就形成了兩個相交的單連結串列,求進入環中的第一個節點也就轉換成了求兩個單連結串列相交的第一個節點。參考程式碼如下:

  1. ListNode* GetFirstNodeInCircle(ListNode * pHead)  
  2. {  
  3.     if(pHead == NULL || pHead->m_pNext == NULL)  
  4.         return NULL;  
  5.   
  6.     ListNode * pFast = pHead;  
  7.     ListNode * pSlow = pHead;  
  8.     while(pFast != NULL && pFast->m_pNext != NULL)  
  9.     {  
  10.         pSlow = pSlow->m_pNext;  
  11.         pFast = pFast->m_pNext->m_pNext;  
  12.         if(pSlow == pFast)  
  13.             break;  
  14.     }  
  15.     if(pFast == NULL || pFast->m_pNext == NULL)  
  16.         return NULL;  
  17.   
  18.     // 將環中的此節點作為假設的尾節點,將它變成兩個單連結串列相交問題  
  19.     ListNode * pAssumedTail = pSlow;   
  20.     ListNode * pHead1 = pHead;  
  21.     ListNode * pHead2 = pAssumedTail->m_pNext;  
  22.   
  23.     ListNode * pNode1, * pNode2;  
  24.     int len1 = 1;  
  25.     ListNode * pNode1 = pHead1;  
  26.     while(pNode1 != pAssumedTail)  
  27.     {  
  28.         pNode1 = pNode1->m_pNext;  
  29.         len1++;  
  30.     }  
  31.       
  32.     int len2 = 1;  
  33.     ListNode * pNode2 = pHead2;  
  34.     while(pNode2 != pAssumedTail)  
  35.     {  
  36.         pNode2 = pNode2->m_pNext;  
  37.         len2++;  
  38.     }  
  39.   
  40.     pNode1 = pHead1;  
  41.     pNode2 = pHead2;  
  42.     // 先對齊兩個連結串列的當前結點,使之到尾節點的距離相等  
  43.     if(len1 > len2)  
  44.     {  
  45.         int k = len1 - len2;  
  46.         while(k--)  
  47.             pNode1 = pNode1->m_pNext;  
  48.     }  
  49.     else  
  50.     {  
  51.         int k = len2 - len1;  
  52.         while(k--)  
  53.             pNode2 = pNode2->m_pNext;  
  54.     }  
  55.     while(pNode1 != pNode2)  
  56.     {  
  57.         pNode1 = pNode1->m_pNext;  
  58.         pNode2 = pNode2->m_pNext;  
  59.     }  
  60.     return pNode1;  
  61. }  


11. 給出一單連結串列頭指標pHead和一節點指標pToBeDeleted,O(1)時間複雜度刪除節點pToBeDeleted

對於刪除節點,我們普通的思路就是讓該節點的前一個節點指向該節點的下一個節點,這種情況需要遍歷找到該節點的前一個節點,時間複雜度為O(n)。對於連結串列,連結串列中的每個節點結構都是一樣的,所以我們可以把該節點的下一個節點的資料複製到該節點,然後刪除下一個節點即可。要注意最後一個節點的情況,這個時候只能用常見的方法來操作,先找到前一個節點,但總體的平均時間複雜度還是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; // 將下一個節點的資料複製到本節點,然後刪除下一個節點  
  8.         ListNode * temp = pToBeDeleted->m_pNext;  
  9.         pToBeDeleted->m_pNext = pToBeDeleted->m_pNext->m_pNext;  
  10.         delete temp;  
  11.     }  
  12.     else // 要刪除的是最後一個節點  
  13.     {  
  14.         if(pHead == pToBeDeleted) // 連結串列中只有一個節點的情況  
  15.         {  
  16.             pHead = NULL;  
  17.             delete pToBeDeleted;  
  18.         }  
  19.         else  
  20.         {  
  21.             ListNode * pNode = pHead;  
  22.             while(pNode->m_pNext != pToBeDeleted) // 找到倒數第二個節點  
  23.                 pNode = pNode->m_pNext;  
  24.             pNode->m_pNext = NULL;  
  25.             delete pToBeDeleted;  
  26.         }     
  27.     }  

相關文章