著作權,轉載請註明出處,謝謝!
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)。參考代碼如下:
// 求單鏈表中結點的個數unsigned int GetListLength(ListNode * pHead){if(pHead == NULL)return 0;unsigned int nLength = 0;ListNode * pCurrent = pHead;while(pCurrent != NULL){nLength++;pCurrent = pCurrent->m_pNext;}return nLength;}
2. 將單鏈表反轉
從頭到尾遍曆原鏈表,每遍曆一個結點,將其摘下放在新鏈表的最前端。注意鏈表為空白和只有一個結點的情況。時間複雜度為O(n)。參考代碼如下:
// 反轉單鏈表ListNode * ReverseList(ListNode * pHead){ // 如果鏈表為空白或只有一個結點,無需反轉,直接返回原鏈表頭指標if(pHead == NULL || pHead->m_pNext == NULL) return pHead;ListNode * pReversedHead = NULL; // 反轉後的新鏈表頭指標,初始為NULLListNode * pCurrent = pHead;while(pCurrent != NULL){ListNode * pTemp = pCurrent;pCurrent = pCurrent->m_pNext;pTemp->m_pNext = pReversedHead; // 將當前結點摘下,插入新鏈表的最前端pReversedHead = pTemp;}return pReversedHead;}
3. 尋找單鏈表中的倒數第K個結點(k > 0)
最普遍的方法是,先統計單鏈表中結點的個數,然後再找到第(n-k)個結點。注意鏈表為空白,k為0,k為1,k大於鏈表中節點個數時的情況。時間複雜度為O(n)。代碼略。
這裡主要講一下另一個思路,這種思路在其他題目中也會有應用。
主要思路就是使用兩個指標,先讓前面的指標走到正向第k個結點,這樣前後兩個指標的距離差是k-1,之後前後兩個指標一起向前走,前面的指標走到最後一個結點時,後面指標所指結點就是倒數第k個結點。
參考代碼如下:
// 尋找單鏈表中倒數第K個結點ListNode * RGetKthNode(ListNode * pHead, unsigned int k) // 函數名前面的R代表反向{if(k == 0 || pHead == NULL) // 這裡k的計數是從1開始的,若k為0或鏈表為空白返回NULLreturn NULL;ListNode * pAhead = pHead;ListNode * pBehind = pHead;while(k > 1 && pAhead != NULL) // 前面的指標先走到正向第k個結點{pAhead = pAhead->m_pNext;k--;}if(k > 1 || pAhead == NULL) // 結點個數小於k,返回NULLreturn NULL;while(pAhead->m_pNext != NULL) // 前後兩個指標一起向前走,直到前面的指標指向最後一個結點{pBehind = pBehind->m_pNext;pAhead = pAhead->m_pNext;}return pBehind; // 後面的指標所指結點就是倒數第k個結點}
4. 尋找單鏈表的中間結點
此題可應用於上一題類似的思想。也是設定兩個指標,只不過這裡是,兩個指標同時向前走,前面的指標每次走兩步,後面的指標每次走一步,前面的指標走到最後一個結點時,後面的指標所指結點就是中間結點,即第(n/2+1)個結點。注意鏈表為空白,鏈表結點個數為1和2的情況。時間複雜度O(n)。參考代碼如下:
// 擷取單鏈表中間結點,若鏈表長度為n(n>0),則返回第n/2+1個結點ListNode * GetMiddleNode(ListNode * pHead){if(pHead == NULL || pHead->m_pNext == NULL) // 鏈表為空白或只有一個結點,返回頭指標return pHead;ListNode * pAhead = pHead;ListNode * pBehind = pHead;while(pAhead->m_pNext != NULL) // 前面指標每次走兩步,直到指向最後一個結點,後面指標每次走一步{pAhead = pAhead->m_pNext;pBehind = pBehind->m_pNext;if(pAhead->m_pNext != NULL)pAhead = pAhead->m_pNext;}return pBehind; // 後面的指標所指結點即為中間結點}
5. 從尾到頭列印單鏈表
對於這種顛倒順序的問題,我們應該就會想到棧,後進先出。所以,這一題要麼自己使用棧,要麼讓系統使用棧,也就是遞迴。注意鏈表為空白的情況。時間複雜度為O(n)。參考代碼如下:
自己使用棧:
// 從尾到頭列印鏈表,使用棧void RPrintList(ListNode * pHead){std::stack<ListNode *> s;ListNode * pNode = pHead;while(pNode != NULL){s.push(pNode);pNode = pNode->m_pNext;}while(!s.empty()){pNode = s.top();printf("%d\t", pNode->m_nKey);s.pop();}}
使用遞迴函式:
// 從尾到頭列印鏈表,使用遞迴void RPrintList(ListNode * pHead){if(pHead == NULL){return;}else{RPrintList(pHead->m_pNext);printf("%d\t", pHead->m_nKey);}}
6. 已知兩個單鏈表pHead1 和pHead2 各自有序,把它們合并成一個鏈表依然有序
這個類似歸併排序。尤其注意兩個鏈表都為空白,和其中一個為空白時的情況。只需要O(1)的空間。時間複雜度為O(max(len1, len2))。參考代碼如下:
// 合并兩個有序鏈表ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2){if(pHead1 == NULL)return pHead2;if(pHead2 == NULL)return pHead1;ListNode * pHeadMerged = NULL;if(pHead1->m_nKey < pHead2->m_nKey){pHeadMerged = pHead1;pHeadMerged->m_pNext = NULL;pHead1 = pHead1->m_pNext;}else{pHeadMerged = pHead2;pHeadMerged->m_pNext = NULL;pHead2 = pHead2->m_pNext;}ListNode * pTemp = pHeadMerged;while(pHead1 != NULL && pHead2 != NULL){if(pHead1->m_nKey < pHead2->m_nKey){pTemp->m_pNext = pHead1;pHead1 = pHead1->m_pNext;pTemp = pTemp->m_pNext;pTemp->m_pNext = NULL;}else{pTemp->m_pNext = pHead2;pHead2 = pHead2->m_pNext;pTemp = pTemp->m_pNext;pTemp->m_pNext = NULL;}}if(pHead1 != NULL)pTemp->m_pNext = pHead1;else if(pHead2 != NULL)pTemp->m_pNext = pHead2;return pHeadMerged;}
也有如下遞迴解法:
ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2){if(pHead1 == NULL)return pHead2;if(pHead2 == NULL)return pHead1;ListNode * pHeadMerged = NULL;if(pHead1->m_nKey < pHead2->m_nKey){pHeadMerged = pHead1;pHeadMerged->m_pNext = MergeSortedList(pHead1->m_pNext, pHead2);}else{pHeadMerged = pHead2;pHeadMerged->m_pNext = MergeSortedList(pHead1, pHead2->m_pNext);}return pHeadMerged;}
7. 判斷一個單鏈表中是否有環
這裡也是用到兩個指標。如果一個鏈表中有環,也就是說用一個指標去遍曆,是永遠走不到頭的。因此,我們可以用兩個指標去遍曆,一個指標一次走兩步,一個指標一次走一步,如果有環,兩個指標肯定會在環中相遇。時間複雜度為O(n)。參考代碼如下:
bool HasCircle(ListNode * pHead){ListNode * pFast = pHead; // 快指標每次前進兩步ListNode * pSlow = pHead; // 慢指標每次前進一步while(pFast != NULL && pFast->m_pNext != NULL){pFast = pFast->m_pNext->m_pNext;pSlow = pSlow->m_pNext;if(pSlow == pFast) // 相遇,存在環return true;}return false;}
8. 判斷兩個單鏈表是否相交
如果兩個鏈表相交於某一節點,那麼在這個相交節點之後的所有節點都是兩個鏈表所共有的。也就是說,如果兩個鏈表相交,那麼最後一個節點肯定是共有的。先遍曆第一個鏈表,記住最後一個節點,然後遍曆第二個鏈表,到最後一個節點時和第一個鏈表的最後一個節點做比較,如果相同,則相交,否則不相交。時間複雜度為O(len1+len2),因為只需要一個額外指標儲存最後一個節點地址,空間複雜度為O(1)。參考代碼如下:
bool IsIntersected(ListNode * pHead1, ListNode * pHead2){ if(pHead1 == NULL || pHead2 == NULL) return false;ListNode * pTail1 = pHead1;while(pTail1->m_pNext != NULL)pTail1 = pTail1->m_pNext;ListNode * pTail2 = pHead2;while(pTail2->m_pNext != NULL)pTail2 = pTail2->m_pNext;return pTail1 == pTail2;}
9. 求兩個單鏈表相交的第一個節點
對第一個鏈表遍曆,計算長度len1,同時儲存最後一個節點的地址。
對第二個鏈表遍曆,計算長度len2,同時檢查最後一個節點是否和第一個鏈表的最後一個節點相同,若不相同,不相交,結束。
兩個鏈表均從前端節點開始,假設len1大於len2,那麼將第一個鏈表先遍曆len1-len2個節點,此時兩個鏈表當前節點到第一個相交節點的距離就相等了,然後一起向後遍曆,知道兩個節點的地址相同。
時間複雜度,O(len1+len2)。參考代碼如下:
ListNode* GetFirstCommonNode(ListNode * pHead1, ListNode * pHead2){if(pHead1 == NULL || pHead2 == NULL)return NULL;int len1 = 1;ListNode * pTail1 = pHead1;while(pTail1->m_pNext != NULL){pTail1 = pTail1->m_pNext;len1++;}int len2 = 1;ListNode * pTail2 = pHead2;while(pTail2->m_pNext != NULL){pTail2 = pTail2->m_pNext;len2++;}if(pTail1 != pTail2) // 不相交直接返回NULLreturn NULL;ListNode * pNode1 = pHead1;ListNode * pNode2 = pHead2; // 先對齊兩個鏈表的當前結點,使之到尾節點的距離相等if(len1 > len2){int k = len1 - len2;while(k--)pNode1 = pNode1->m_pNext;}else{int k = len2 - len1;while(k--)pNode2 = pNode2->m_pNext;}while(pNode1 != pNode2){pNode1 = pNode1->m_pNext;pNode2 = pNode2->m_pNext;} return pNode1;}
10. 已知一個單鏈表中存在環,求進入環中的第一個節點
首先判斷是否存在環,若不存在結束。在環中的一個節點處斷開(當然函數結束時不能破壞原鏈表),這樣就形成了兩個相交的單鏈表,求進入環中的第一個節點也就轉換成了求兩個單鏈表相交的第一個節點。參考代碼如下:
ListNode* GetFirstNodeInCircle(ListNode * pHead){if(pHead == NULL || pHead->m_pNext == NULL)return NULL;ListNode * pFast = pHead;ListNode * pSlow = pHead;while(pFast != NULL && pFast->m_pNext != NULL){pSlow = pSlow->m_pNext;pFast = pFast->m_pNext->m_pNext;if(pSlow == pFast)break;}if(pFast == NULL || pFast->m_pNext == NULL)return NULL;// 將環中的此節點作為假設的尾節點,將它變成兩個單鏈表相交問題ListNode * pAssumedTail = pSlow; ListNode * pHead1 = pHead;ListNode * pHead2 = pAssumedTail->m_pNext;ListNode * pNode1, * pNode2;int len1 = 1;ListNode * pNode1 = pHead1;while(pNode1 != pAssumedTail){pNode1 = pNode1->m_pNext;len1++;}int len2 = 1;ListNode * pNode2 = pHead2;while(pNode2 != pAssumedTail){pNode2 = pNode2->m_pNext;len2++;}pNode1 = pHead1;pNode2 = pHead2;// 先對齊兩個鏈表的當前結點,使之到尾節點的距離相等if(len1 > len2){int k = len1 - len2;while(k--)pNode1 = pNode1->m_pNext;}else{int k = len2 - len1;while(k--)pNode2 = pNode2->m_pNext;}while(pNode1 != pNode2){pNode1 = pNode1->m_pNext;pNode2 = pNode2->m_pNext;} return pNode1;}
11. 給出一單鏈表頭指標pHead和一節點指標pToBeDeleted,O(1)時間複雜度刪除節點pToBeDeleted
對於刪除節點,我們普通的思路就是讓該節點的前一個節點指向該節點的下一個節點,這種情況需要遍曆找到該節點的前一個節點,時間複雜度為O(n)。對於鏈表,鏈表中的每個節點結構都是一樣的,所以我們可以把該節點的下一個節點的資料複製到該節點,然後刪除下一個節點即可。要注意最後一個節點的情況,這個時候只能用常見的方法來操作,先找到前一個節點,但總體的平均時間複雜度還是O(1)。參考代碼如下:
void Delete(ListNode * pHead, ListNode * pToBeDeleted){if(pToBeDeleted == NULL)return;if(pToBeDeleted->m_pNext != NULL){pToBeDeleted->m_nKey = pToBeDeleted->m_pNext->m_nKey; // 將下一個節點的資料複製到本節點,然後刪除下一個節點ListNode * temp = pToBeDeleted->m_pNext;pToBeDeleted->m_pNext = pToBeDeleted->m_pNext->m_pNext;delete temp;}else // 要刪除的是最後一個節點{if(pHead == pToBeDeleted) // 鏈表中只有一個節點的情況{pHead = NULL;delete pToBeDeleted;}else{ListNode * pNode = pHead;while(pNode->m_pNext != pToBeDeleted) // 找到倒數第二個節點pNode = pNode->m_pNext;pNode->m_pNext = NULL;delete pToBeDeleted;}}}