【萬字】鏈表算法面試?看我就夠了!

1 引言

單鏈表的操做算法是筆試面試中較爲常見的題目。本文將着重介紹平時面試中常見的關於鏈表的應用題目,但願對大家有幫助 ^_^c++

2 輸出單鏈表倒數第 K 個節點

2.1 問題描述

題目:輸入一個單鏈表,輸出此鏈表中的倒數第 K 個節點。(去除頭結點,節點計數從 1 開始)面試

2.2 兩次遍歷法

2.2.1 解題思想

(1)遍歷單鏈表,遍歷同時得出鏈表長度 N 。 (2)再次從頭遍歷,訪問至第 N - K 個節點爲所求節點。算法

2.2.2 圖解過程

圖 1

2.2.3 代碼實現
/*計算鏈表長度*/
int listLength(ListNode* pHead){
    int count = 0;
    ListNode* pCur = pHead->next;
    if(pCur == NULL){
        printf("error");
    }
    while(pCur){
        count++;
        pCur = pCur->pNext;
    }
    return count;
}
/*查找第k個節點的值*/
ListNode* searchNodeK(ListNode* pHead, int k){
    int i = 0;
    ListNode* pCur = pHead; 
    //計算鏈表長度
    int len = listLength(pHead);
    if(k > len){
        printf("error");
    }
    //循環len-k+1次
    for(i=0; i < len-k+1; i++){
        pCur  = pCur->next;
    }
    return pCur;//返回倒數第K個節點
}    
複製代碼

採用這種遍歷方式須要兩次遍歷鏈表,時間複雜度爲O(n*2)。可見這種方式最爲簡單,也較好理解,可是效率低下。緩存

2.3 遞歸法

2.3.1 解題思想

(1)定義num = k (2)使用遞歸方式遍歷至鏈表末尾。 (3)由末尾開始返回,每返回一次 num 減 1 (4)當 num 爲 0 時,便可找到目標節點bash

2.3.2 圖解過程

圖 2

2.3.3 代碼實現
int num;//定義num值
ListNode* findKthTail(ListNode* pHead, int k) {
        num = k;
        if(pHead == NULL)
            return NULL;
        //遞歸調用
        ListNode* pCur = findKthTail(pHead->next, k);
        if(pCur != NULL)
            return pCur;
        else{
            num--;// 遞歸返回一次,num值減1
            if(num == 0)
                return pHead;//返回倒數第K個節點
            return NULL;
        }
}
複製代碼

使用遞歸的方式實現仍然須要兩次遍歷鏈表,時間複雜度爲O(n*2)數據結構

2.4 雙指針法

2.4.1 解題思想

(1)定義兩個指針 p1 和 p2 分別指向鏈表頭節點。 (2)p1 前進 K 個節點,則 p1 與 p2 相距 K 個節點。 (3)p1,p2 同時前進,每次前進 1 個節點。 (4)當 p1 指向到達鏈表末尾,因爲 p1 與 p2 相距 K 個節點,則 p2 指向目標節點。函數

2.4.2 圖解過程

圖 3

圖 4

2.4.3 代碼實現
ListNode* findKthTail(ListNode *pHead, int K){
    if (NULL == pHead || K == 0)
        return NULL;
    //p1,p2均指向頭節點
    ListNode *p1 = pHead;
    ListNode *p2 = pHead;
    //p1先出發,前進K個節點
    for (int i = 0; i < K; i++) {
        if (p1)//防止k大於鏈表節點的個數
            p1 = p1->_next;
        else
            return NULL;
    }

    while (p1)//若是p1沒有到達鏈表結尾,則p1,p2繼續遍歷
    {
        p1 = p1->_next;
        p2 = p2->_next;
    }
    return p2;//當p1到達末尾時,p2正好指向倒數第K個節點
}
複製代碼

能夠看出使用雙指針法只需遍歷鏈表一次,這種方法更爲高效時間複雜度爲O(n),一般筆試題目中要考的也是這種方法。oop

3 鏈表中存在環問題

3.1 判斷鏈表是否有環

單鏈表中的環是指鏈表末尾的節點的 next 指針不爲 NULL ,而是指向了鏈表中的某個節點,致使鏈表中出現了環形結構。動畫

鏈表中有環示意圖: ui

圖 5

鏈表的末尾節點 8 指向了鏈表中的節點 3,致使鏈表中出現了環形結構。 對於鏈表是不是由有環的判斷方法有哪些呢?

3.1.1 窮舉比較法
3.1.1.1 解題思想

(1)遍歷鏈表,記錄已訪問的節點。 (2)將當前節點與以前以及訪問過的節點比較,如有相同節點則有環。 不然,不存在環。

這種窮舉比較思想簡單,可是效率過於低下,尤爲是當鏈表節點數目較多,在進行比較時花費大量時間,時間複雜度大體在 O(n^2)。這種方法天然不是出題人的理想答案。若是筆試面試中使用這種方法,估計就要跪了,忘了這種方法吧

3.1.2 哈希緩存法

既然在窮舉遍歷時,元素比較過程花費大量時間,那麼有什麼辦法能夠提升比較速度呢?

3.1.2.1 解題思想

(1)首先建立一個以節點 ID 爲鍵的 HashSe t集合,用來存儲曾經遍歷過的節點。 (2)從頭節點開始,依次遍歷單鏈表的每個節點。 (3)每遍歷到一個新節點,就用新節點和 HashSet 集合當中存儲的節點做比較,若是發現 HashSet 當中存在相同節點 ID,則說明鏈表有環,若是 HashSet 當中不存在相同的節點 ID,就把這個新節點 ID 存入 HashSet ,以後進入下一節點,繼續重複剛纔的操做。

假設從鏈表頭節點到入環點的距離是 a ,鏈表的環長是 r 。而每一次 HashSet 查找元素的時間複雜度是 O(1), 因此整體的時間複雜度是 1 * ( a + r ) = a + r,能夠簡單理解爲 O(n) 。而算法的空間複雜度仍是 a + r - 1,能夠簡單地理解成 O(n) 。

3.1.3 快慢指針法
3.1.3.1 解題思想

(1)定義兩個指針分別爲 slow,fast,而且將指針均指向鏈表頭節點。 (2)規定,slow 指針每次前進 1 個節點,fast 指針每次前進兩個節點。 (3)當 slow 與 fast 相等,且兩者均不爲空,則鏈表存在環。

3.1.3.2 圖解過程

無環過程:

圖 6

圖 7

圖 8

經過圖解過程能夠看出,若表中不存在環形,fast 與 slow 指針只能在鏈表末尾相遇。

有環過程:

圖 9

圖 10

圖 11

圖解過程能夠看出,若鏈表中存在環,則快慢指針必然能在環中相遇。這就比如在環形跑道中進行龜兔賽跑。因爲兔子速度大於烏龜速度,則必然會出現兔子與烏龜再次相遇狀況。所以,當出現快慢指針相等時,且兩者不爲NULL,則代表鏈表存在環。

3.1.3.3 代碼實現
bool isExistLoop(ListNode* pHead) {  
    ListNode* fast;//慢指針,每次前進一個節點
    ListNode* slow;//快指針,每次前進2個節點 
    slow = fast = pHead ;  //兩個指針均指向鏈表頭節點
    //當沒有到達鏈表結尾,則繼續前進
    while (slow != NULL && fast -> next != NULL)  {  
        slow = slow -> next ; //慢指針前進一個節點
        fast = fast -> next -> next ; //快指針前進兩個節點
        if (slow == fast)  //若兩個指針相遇,且均不爲NULL則存在環
            return true ;  
    }  
    //到達末尾仍然沒有相遇,則不存在環
    return false ;  
}  
複製代碼

3.2 定位環入口

在 3.1 節中,已經實現了鏈表中是否有環的判斷方法。那麼,當鏈表中存在環,如何肯定環的入口節點呢?

3.2.1 解題思想

slow 指針每次前進一個節點,故 slow 與 fast 相遇時,slow 尚未遍歷完整個鏈表。設 slow 走過節點數爲 s,fast 走過節點數爲 2s。設環入口點距離頭節點爲 a,slow 與 fast 首次相遇點距離入口點爲 b,環的長度爲 r。 則有: s = a + b; 2s = n * r + a + b; n 表明fast指針已經在環中循環的圈數。 則推出: s = n * r; 意味着slow指針走過的長度爲環的長度整數倍。

若鏈表頭節點到環的末尾節點度爲 L,slow 與 fast 的相遇節點距離環入口節點爲 X。 則有: a+X = s = n * r = (n - 1) * r + (L - a); a = (n - 1) * r + (L - a - X); 上述等式能夠看出: 從 slow 與 fast 相遇點出發一個指針 p1,請進 (L - a - X) 步,則此指針到達入口節點。同時指針 p2 從頭結點出發,前進 a 步。當 p1 與 p2 相遇時,此時 p1 與 p2 均指向入口節點。

例如圖3.1所示鏈表: slow 走過節點 s = 6; fast 走過節點 2s = 12; 環入口節點據流頭節點 a = 3; 相遇點距離頭節點 X = 3; L = 8; r = 6; 能夠得出 a = (n - 1) * r + (L - a - X)結果成立。

3.2.2 圖解過程

圖 12

圖 13

3.2.3 代碼實現
//找到環中的相遇節點
ListNode* getMeetingNode(ListNode* pHead) // 假設爲帶頭節點的單鏈表 {
    ListNode* fast;//慢指針,每次前進一個節點
    ListNode* slow;//快指針,每次前進2個節點 
    slow = fast = pHead ;  //兩個指針均指向鏈表頭節點
    //當沒有到達鏈表結尾,則繼續前進
    while (slow != NULL && fast -> next != NULL){  
        slow = slow -> next ; //慢指針前進一個節點
        fast = fast -> next -> next ; //快指針前進兩個節點
        if (slow == fast)  //若兩個指針相遇,且均不爲NULL則存在環
            return slow;  
    }  

    //到達末尾仍然沒有相遇,則不存在環
    return NULL ;
}
//找出環的入口節點
ListNode* getEntryNodeOfLoop(ListNode* pHead){
    ListNode* meetingNode = getMeetingNode(pHead); // 先找出環中的相遇節點
    if (meetingNode == NULL)
        return NULL;
    ListNode* p1 = meetingNode;
    ListNode* p2 = pHead;
    while (p1 != p2) // p1和p2以相同的速度向前移動,當p2指向環的入口節點時,p1已經圍繞着環走了n圈又回到了入口節點。
    {
        p1 = p1->next;
        p2 = p2->next;
    }
    //返回入口節點
    return p1;
}
複製代碼

3.3 計算環長度

3.3.1 解題思想

在3.1中找到了 slow 與 fast 的相遇節點,令 solw 與 fast 指針從相遇節點出發,按照以前的前進規則,當 slow 與fast 再次相遇時,slow 走過的長度正好爲環的長度。

3.3.2 圖解過程

圖 14

圖 15

3.3.3 代碼實現
int getLoopLength(ListNode* head){
    ListNode* slow = head;
    ListNode* fast = head;
    while ( fast && fast->next ){
        slow = slow->next;
        fast = fast->next->next;
        if ( slow == fast )//第一次相遇
            break;
    }
    //slow與fast繼續前進
    slow = slow->next;
    fast = fast->next->next;
    int length = 1;       //環長度
    while ( fast != slow )//再次相遇
    {
        slow = slow->next;
        fast = fast->next->next;
        length ++;        //累加
    }
    //當slow與fast再次相遇,獲得環長度
    return length;
}
複製代碼

4 使用鏈表實現大數加法

4.1 問題描述

兩個用鏈表表明的整數,其中每一個節點包含一個數字。數字存儲按照在原來整數中相反的順序,使得第一個數字位於鏈表的開頭。寫出一個函數將兩個整數相加,用鏈表形式返回和。

例如: 輸入: 3->1->5->null 5->9->2->null, 輸出: 8->0->8->null

4.2 代碼實現

ListNode* numberAddAsList(ListNode* l1, ListNode* l2) {
        ListNode *ret = l1, *pre = l1;
        int up = 0;
        while (l1 != NULL && l2 != NULL) {
            //數值相加
            l1->val = l1->val + l2->val + up;
            //計算是否有進位
            up = l1->val / 10;
            //保留計算結果的個位
            l1->val %= 10;
            //記錄當前節點位置
            pre = l1;
            //同時向後移位
            l1 = l1->next;
            l2 = l2->next;
        }
        //若l1到達末尾,說明l1長度小於l2
        if (l1 == NULL)
            //pre->next指向l2的當前位置
            pre->next = l2;
        //l1指針指向l2節點當前位置
        l1 = pre->next;
        //繼續計算剩餘節點
        while (l1 != NULL) {
            l1->val = l1->val + up;
            up = l1->val / 10;
            l1->val %= 10;
            pre = l1;
            l1 = l1->next;
        }

        //最高位計算有進位,則新建一個節點保留最高位
        if (up != 0) {
            ListNode *tmp = new ListNode(up);
            pre->next = tmp;
        }
        //返回計算結果鏈表
        return ret;
}
複製代碼

5 有序鏈表合併

5.1 問題描述

題目:將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的。

示例: 輸入: 1->2->4, 1->3->4 輸出: 1->1->2->3->4->4

5.2 算法流程

5.3 通常方案

5.3.1 解題思想

(1)對空鏈表存在的狀況進行處理,假如 pHead1 爲空則返回 pHead2 ,pHead2 爲空則返回 pHead1。(兩個都爲空此狀況在pHead1爲空已經被攔截) (2)在兩個鏈表無空鏈表的狀況下肯定第一個結點,比較鏈表1和鏈表2的第一個結點的值,將值小的結點保存下來爲合併後的第一個結點。而且把第一個結點爲最小的鏈表向後移動一個元素。 (3)繼續在剩下的元素中選擇小的值,鏈接到第一個結點後面,並不斷next將值小的結點鏈接到第一個結點後面,直到某一個鏈表爲空。 (4)當兩個鏈表長度不一致時,也就是比較完成後其中一個鏈表爲空,此時須要把另一個鏈表剩下的元素都鏈接到第一個結點的後面。

5.3.2 代碼實現
ListNode* mergeTwoOrderedLists(ListNode* pHead1, ListNode* pHead2){
    ListNode* pTail = NULL;//指向新鏈表的最後一個結點 pTail->next去鏈接
    ListNode* newHead = NULL;//指向合併後鏈表第一個結點
    if (NULL == pHead1){
        return pHead2;
    }else if(NULL == pHead2){
        return pHead1;
    }else{
        //肯定頭指針
        if ( pHead1->data < pHead2->data){
            newHead = pHead1;
            pHead1 = pHead1->next;//指向鏈表的第二個結點
        }else{
            newHead = pHead2;
            pHead2 = pHead2->next;
        }
        pTail = newHead;//指向第一個結點
        while ( pHead1 && pHead2) {
            if ( pHead1->data <= pHead2->data ){
                pTail->next = pHead1;  
                pHead1 = pHead1->next;
            }else {
                pTail->next = pHead2;
                pHead2 = pHead2->next;
            }
            pTail = pTail->next;

        }
        if(NULL == pHead1){
            pTail->next = pHead2;
        }else if(NULL == pHead2){
            pTail->next = pHead1;
        }
        return newHead;
}
複製代碼

5.4 遞歸方案

5.4.1 解題思想

(1)對空鏈表存在的狀況進行處理,假如 pHead1 爲空則返回 pHead2 ,pHead2 爲空則返回 pHead1。 (2)比較兩個鏈表第一個結點的大小,肯定頭結點的位置 (3)頭結點肯定後,繼續在剩下的結點中選出下一個結點去連接到第二步選出的結點後面,而後在繼續重複(2 )(3) 步,直到有鏈表爲空。

5.4.2 代碼實現
ListNode* mergeTwoOrderedLists(ListNode* pHead1, ListNode* pHead2){
    ListNode* newHead = NULL;
    if (NULL == pHead1){
        return pHead2;
    }else if(NULL ==pHead2){
        return pHead2;
    }else{
        if (pHead1->data < pHead2->data){
            newHead = pHead1;
            newHead->next = mergeTwoOrderedLists(pHead1->next, pHead2);
        }else{
            newHead = pHead2;
            newHead->next = mergeTwoOrderedLists(pHead1, pHead2->next);
         }
        return newHead;
    }   
}
複製代碼

6 刪除鏈表中節點,要求時間複雜度爲 O(1)

6.1 問題描述

給定一個單鏈表中的表頭和一個等待被刪除的節點。請在 O(1) 時間複雜度刪除該鏈表節點。並在刪除該節點後,返回表頭。

示例: 給定 1->2->3->4,和節點 3,返回 1->2->4。

6.2 解題思想

在以前介紹的單鏈表刪除節點中,最普通的方法就是遍歷鏈表,複雜度爲O(n)。 若是咱們把刪除節點的下一個結點的值賦值給要刪除的結點,而後刪除這個結點,這至關於刪除了須要刪除的那個結點。由於咱們很容易獲取到刪除節點的下一個節點,因此複雜度只須要O(1)。

示例 單鏈表:1->2->3->4->NULL 若要刪除節點 3 。第一步將節點3的下一個節點的值4賦值給當前節點。變成 1->2->4->4->NULL,而後將就 4 這個結點刪除,就達到目的了。 1->2->4->NULL

若是刪除的節點的是頭節點,把頭結點指向 NULL。 若是刪除的節點的是尾節點,那隻能從頭遍歷到頭節點的上一個結點。

6.3 圖解過程

圖 16

6.4 代碼實現

void deleteNode(ListNode **pHead, ListNode* pDelNode) {
        if(pDelNode == NULL)
            return;
        if(pDelNode->next != NULL){
            ListNode *pNext = pDelNode->next;
            //下一個節點值賦給待刪除節點
            pDelNode->val   =  pNext->val;
            //待刪除節點指針指後面第二個節點
            pDelNode->next  = pNext->next;
            //刪除待刪除節點的下一個節點
            delete pNext;
            pNext = NULL;
        }else if(*pHead == pDelNode)//刪除的節點是頭節點
         {
            delete pDelNode;
            pDelNode= NULL;
            *pHead = NULL;
        } else//刪除的是尾節點
        {
            ListNode *pNode = *pHead;
            while(pNode->next != pDelNode) {
                pNode = pNode->next;
            }
            pNode->next = NULL;
            delete pDelNode;
            pDelNode= NULL;
        }
    }
複製代碼

7 從尾到頭打印鏈表

7.1 問題描述

輸入一個鏈表,按鏈表值從尾到頭的順序返回一個 ArrayList 。

7.2 解法

初看題目意思就是輸出的時候鏈表尾部的元素放在前面,鏈表頭部的元素放在後面。這不就是 先進後出,後進先出 麼。

什麼數據結構符合這個要求?

動畫 2

7.2.1 代碼實現

class Solution {
public:
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> value;
        ListNode *p=NULL;
        p=head;
        stack<int> stk;
        while(p!=NULL){
            stk.push(p->val);
            p=p->next;
        }
        while(!stk.empty()){
            value.push_back(stk.top());
            stk.pop();
        }
        return value;
    }
};
複製代碼

7.3 解法二

第二種方法也比較容易想到,經過鏈表的構造,若是將末尾的節點存儲以後,剩餘的鏈表處理方式仍是不變,因此可使用遞歸的形式進行處理。

7.3.1 代碼實現

class Solution {
public:
    vector<int> value;
    vector<int> printListFromTailToHead(ListNode* head) {
        ListNode *p=NULL;
        p=head;
        if(p!=NULL){
            if(p->next!=NULL){
                printListFromTailToHead(p->next);
            }
            value.push_back(p->val);
        }
        return value;
    }
};
複製代碼

8 反轉鏈表

8.1 題目描述

反轉一個單鏈表。

示例:

輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
複製代碼

進階: 你能夠迭代或遞歸地反轉鏈表。你可否用兩種方法解決這道題?

8.2 解題思路

設置三個節點precurnext

  • (1)每次查看cur節點是否爲NULL,若是是,則結束循環,得到結果
  • (2)若是cur節點不是爲NULL,則先設置臨時變量nextcur的下一個節點
  • (3)讓cur的下一個節點變成指向pre,然後pre移動curcur移動到next
  • (4)重複(1)(2)(3)

動畫演示

206.Reverse Linked List

8.3 代碼實現

8.3.1 迭代方式
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = NULL;
        ListNode* cur = head;
        while(cur != NULL){
            ListNode* next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
};
複製代碼
8.3.2 遞歸的方式處理
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 遞歸終止條件
        if(head == NULL || head->next == NULL)
            return head;

        ListNode* rhead = reverseList(head->next);
        // head->next此刻指向head後面的鏈表的尾節點
        // head->next->next = head把head節點放在了尾部
        head->next->next = head;
        head->next = NULL;
        return rhead;
    }
};
複製代碼

End

最近文章點贊量有點少,若是文章對你有幫助的話,麻煩點個贊~

相關文章
相關標籤/搜索