1. 程式人生 > >單鏈表查詢倒數第k個節點

單鏈表查詢倒數第k個節點

題目:輸入一個單向連結串列,輸出該連結串列中倒數第k個結點。連結串列的倒數第0個結點為連結串列的尾指標。連結串列結點定義如下:

struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};

分析:為了得到倒數第k個結點,很自然的想法是先走到連結串列的尾端,再從尾端回溯k步。可是輸入的是單向連結串列,只有從前往後的指標而沒有從後往前的指標。因此我們需要開啟我們的思路。

既然不能從尾結點開始遍歷這個連結串列,我們還是把思路回到頭結點上來。假設整個連結串列有n個結點,那麼倒數第k個結點是從頭結點開始的第n-k-1個結點(從0開始計數)。如果我們能夠得到連結串列中結點的個數

n,那我們只要從頭結點開始往後走n-k-1步就可以了。如何得到結點數n?這個不難,只需要從頭開始遍歷連結串列,每經過一個結點,計數器加一就行了。

這種思路的時間複雜度是O(n),但需要遍歷連結串列兩次。第一次得到連結串列中結點個數n,第二次得到從頭結點開始的第n-k-1個結點即倒數第k個結點。

如果連結串列的結點數不多,這是一種很好的方法。但如果輸入的連結串列的結點個數 很多,有可能不能一次性把整個連結串列都從硬碟讀入實體記憶體,那麼遍歷兩遍意味著一個結點需要兩次從硬碟讀入到實體記憶體。我們知道把資料從硬碟讀入到記憶體是非 常耗時間的操作。我們能不能把連結串列遍歷的次數減少到1?如果可以,將能有效地提高程式碼執行的時間效率。

如果我們在遍歷時維持兩個指標,第一個指標從連結串列的頭指標開始遍歷,在第k-1步之前,第二個指標保持不動;在第k-1步開始,第二個指標也開始從連結串列的頭指標開始遍歷。由於兩個指標的距離保持在k-1,當第一個(走在前面的)指標到達連結串列的尾結點時,第二個指標(走在後面的)指標正好是倒數第k個結點。

這種思路只需要遍歷連結串列一次。對於很長的連結串列,只需要把每個結點從硬碟匯入到記憶體一次。因此這一方法的時間效率前面的方法要高。

思路一的參考程式碼:

///////////////////////////////////////////////////////////////////////
// Find the kth node from the tail of a list
// Input: pListHead - the head of list

//k- the distance to the tail
// Output: the kth node from the tail of a list
///////////////////////////////////////////////////////////////////////
ListNode* FindKthToTail_Solution1(ListNode* pListHead, unsigned int k)
{
if(pListHead == NULL)
return NULL;

// count the nodes number in the list
ListNode *pCur = pListHead;
unsigned int nNum = 0;
while(pCur->m_pNext != NULL)
{
pCur = pCur->m_pNext;
nNum ;
}

// if the number of nodes in the list is less than k
// do nothing
if(nNum < k)
return NULL;

// the kth node from the tail of a list 
// is the (n - k)th node from the head
pCur = pListHead;
for(unsigned int i = 0; i < nNum - k; i)
pCur = pCur->m_pNext;
return pCur;
}

思路二的參考程式碼:

///////////////////////////////////////////////////////////////////////
// Find the kth node from the tail of a list
// Input: pListHead - the head of list
//k- the distance to the tail
// Output: the kth node from the tail of a list
///////////////////////////////////////////////////////////////////////
ListNode* FindKthToTail_Solution2(ListNode* pListHead, unsigned int k)
{
if(pListHead == NULL)
return NULL;

ListNode *pAhead = pListHead;
ListNode *pBehind = NULL;
for(unsigned int i = 0; i < k; i)
{
if(pAhead->m_pNext != NULL)
pAhead = pAhead->m_pNext;
else
{
// if the number of nodes in the list is less than k, 
// do nothing
return NULL;
}
}
pBehind = pListHead;

// the distance between pAhead and pBehind is k
// when pAhead arrives at the tail, p
// Behind is at the kth node from the tail
while(pAhead->m_pNext != NULL)
{
pAhead = pAhead->m_pNext;
pBehind = pBehind->m_pNext;
}

return pBehind;
}

討論:這道題的程式碼有大量的指標操作。在軟體開發中,錯誤的指標操作是大部分問題的根源。因此每個公司都希望程式設計師在操作指標時有良好的習慣,比如使用指標之前判斷是不是空指標。這些都是程式設計的細節,但如果這些細節把握得不好,很有可能就會和心儀的公司失之交臂。

另外,這兩種思路對應的程式碼都含有迴圈。含有迴圈的程式碼經常出的問題是在迴圈結束條件的判斷。是該用小於還是小於等於?是該用k還是該用k-1?由於題目要求的是從0開始計數,而我們的習慣思維是從1開始計數,因此首先要想好這些邊界條件再開始編寫程式碼,再者要在編寫完程式碼之後再用邊界值、邊界值減1、邊界值加1都執行一次(在紙上寫程式碼就只能在心裡運行了)。

擴充套件:和這道題類似的題目還有:輸入一個單向連結串列。如果該連結串列的結點數為奇數,輸出中間的結點;如果連結串列結點數為偶數,輸出中間兩個結點前面的一個。如果各位感興趣,請自己分析並編寫程式碼。