鏈表是最基本的數據結構,面試官也經常用鏈表來考察面試者的基本能力,並且鏈表相關的操做相對而言比較簡單,也適合考察寫代碼的能力。鏈表的操做也離不開指針,指針又很容易致使出錯。綜合多方面的緣由,鏈表題目在面試中佔據着很重要的地位。本文對鏈表相關的面試題作了較爲全面的整理,但願能對找工做的同窗有所幫助。前端
鏈表結點聲明以下:面試
struct ListNode
{
int m_nKey;
ListNode * m_pNext;數據結構
};函數
題目列表:spa
1. 求單鏈表中結點的個數
2. 將單鏈表反轉
3. 查找單鏈表中的倒數第K個結點(k > 0)
4. 查找單鏈表的中間結點
5. 從尾到頭打印單鏈表
6. 已知兩個單鏈表pHead1 和pHead2 各自有序,把它們合併成一個鏈表依然有序
7. 判斷一個單鏈表中是否有環
8. 判斷兩個單鏈表是否相交
9. 求兩個單鏈表相交的第一個節點
10. 已知一個單鏈表中存在環,求進入環中的第一個節點
11. 給出一單鏈表頭指針pHead和一節點指針pToBeDeleted,O(1)時間複雜度刪除節點pToBeDeleted.net
詳細解答指針
這是最最基本的了,應該可以迅速寫出正確的代碼,注意檢查鏈表是否爲空。時間複雜度爲O(n)。參考代碼以下:blog
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 }
從頭至尾遍歷原鏈表,每遍歷一個結點,將其摘下放在新鏈表的最前端。注意鏈表爲空和只有一個結點的狀況。時間複雜度爲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 }
最廣泛的方法是,先統計單鏈表中結點的個數,而後再找到第(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) // 結點個數小於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 }
此題可應用於上一題相似的思想。也是設置兩個指針,只不過這裏是,兩個指針同時向前走,前面的指針每次走兩步,後面的指針每次走一步,前面的指針走到最後一個結點時,後面的指針所指結點就是中間結點,即第(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 }
對於這種顛倒順序的問題,咱們應該就會想到棧,後進先出。因此,這一題要麼本身使用棧,要麼讓系統使用棧,也就是遞歸。注意鏈表爲空的狀況。時間複雜度爲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 }
這裏也是用到兩個指針。若是一個鏈表中有環,也就是說用一個指針去遍歷,是永遠走不到頭的。所以,咱們能夠用兩個指針去遍歷,一個指針一次走兩步,一個指針一次走一步,若是有環,兩個指針確定會在環中相遇。時間複雜度爲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 }
若是兩個鏈表相交於某一節點,那麼在這個相交節點以後的全部節點都是兩個鏈表所共有的。也就是說,若是兩個鏈表相交,那麼最後一個節點確定是共有的。先遍歷第一個鏈表,記住最後一個節點,而後遍歷第二個鏈表,到最後一個節點時和第一個鏈表的最後一個節點作比較,若是相同,則相交,不然不相交。時間複雜度爲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 }
首先判斷是否存在環,若不存在結束。在環中的一個節點處斷開(固然函數結束時不能破壞原鏈表),這樣就造成了兩個相交的單鏈表,求進入環中的第一個節點也就轉換成了求兩個單鏈表相交的第一個節點。參考代碼以下:
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 } 28 }