搞定面試中的鏈表題目

鏈表是最基本的數據結構,面試官也常常用鏈表來考察面試者的基本能力,而且鏈表相關的操作相對而言比較簡單,也適合考察寫代碼的能力。鏈表的操作也 離不開指針,指針又很容易導致出錯。綜合多方面的原因,鏈表題目在面試中佔據着很重要的地位。本文對鏈表相關的面試題做了較爲全面的整理,希望能對找工作 的同學有所幫助。
鏈表結點聲明如下:

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

題目列表:

詳細解答

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; // 反轉後的新鏈表頭指針,初始爲NULL
    ListNode * 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或鏈表爲空返回NULL
        return NULL;

    ListNode * pAhead = pHead;
    ListNode * pBehind = pHead;
    while(k > 1 && pAhead != NULL) // 前面的指針先走到正向第k個結點
    {
        pAhead = pAhead->m_pNext;
        k--;
    }
    if(k > 1 || pAhead == NULL)     // 結點個數小於k,返回NULL
        return 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) // 不相交直接返回NULL
        return 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;
        }   
    }
}

原文以及參考文獻

  1. http://blog.csdn.net/walkinginthewind/article/details/7393134

  2. 劍指offer

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章