這十幾個鏈表題型面試必考(詳細解析)

寫在前邊

若是你和小鹿同樣,剛開始對鏈表的操做代碼實現很懵的話,不妨按照小鹿通過一個月的時間對鏈表相關操做以及題型的整理總結,由淺入深進行適當的練習,我相信,當你真正的練習完這些題目,不但會讓你放下對鏈表心理上的困惑,並且對你學習其餘數據結構有很大的信心和幫助!javascript

一、學習建議

小鹿不建議你一口氣去看完這篇全部的題目和練習,給本身制定一個小計劃,我當初整理該題目的時候,天天都計劃認真整理一到題目,把每道題分析透,這樣才能達到最好的吸取效果。java

二、學習路徑

本篇分爲三個階段,基礎練習階段、進階練習階段、增強練習階段。node

1)基礎練習階段git

首先進行第一個階段以前,你已經對鏈表的基礎知識可以熟練掌握,可是對於沒有動手寫過鏈表代碼,那麼你從第一階段最基礎的開始進行。確保每個基礎點要親自動手用本身熟悉的語言寫出來,雖然本篇中基本都是 javascript 代碼實現的,可是算法思路是一成不變的,若是遇到困難能夠自行百度或谷歌,也能夠下方給我進行留言。github

2)進階練習階段面試

若是你對上述的鏈表基本代碼已經徹底熟練掌握了,那麼恭喜你能夠進行下一個階段,進階階段,這一階段增長的難度就是鏈表的操做是對於實際問題來解決的,因此很是鍛鍊你對問題的分析能力和解決能力,也考驗你對代碼的全面性、魯棒性。這一階段很是的重要,下面的每道題我都作出了詳細的分析。算法

3)增強練習階段編程

若是上述的進階練習階段的題型你都瞭如指掌了,那麼不妨咱們實戰一下,LeetCode 匯聚了不少面試的題型,因此我在上邊整理了幾個經典的題目,你能夠嘗試着解答它們,相關題目的代碼以及解題思路我都整理好了。這一階段的題目小鹿會在後期不斷的更新,這些題目你可以徹底掌握,鏈表對你來講小菜一碟了。緩存


1、鏈表基礎練習(階段一)

本身首相嘗試着一個個攻破下方的鏈表中最基礎的操做,相關代碼我也整理好了(先本身嘗試着去解決哦)。數據結構


2、鏈表進階練習(階段二)

一、單鏈表從尾到頭打印

題目:輸入一個鏈表的頭結點,從尾到頭反過來打印出每一個節點的值。

1.1 問題分析與解決

▉ 問題分析

1)看到題目第一想到的就是反轉鏈表在打印輸出,一種反轉鏈表的方法,可是這種方法改變了原有的鏈表結構。

缺點:使得鏈表的結構發生改變了。若是不改變鏈表結構應該怎麼解決?

2)從問題中能夠得出,咱們想要從尾到頭打印鏈表,正常狀況下是從頭至尾打印的,咱們就會想到最後的數據先打印,開始的數據最後打印,有種「先進後出」的特色,咱們就能想到用「棧」這種結構,用棧來實現。

缺點:代碼不夠簡潔。

優勢:魯棒性好(在不肯定的狀況下,程序仍然能夠正確的執行)。

3)提到棧這種數據結構,咱們就會想到「遞歸」的實現就是用棧這種數據結構實現的。既然棧能實現,那麼遞歸也能實現。

缺點:若是鏈表很長,遞歸深度很深,致使堆棧溢出。

優勢:代碼簡潔、明瞭。

▉ 算法思路

得出如下幾種實現方式:

  • 反轉鏈表法
  • 棧實現
  • 遞歸實現

1)反轉鏈表實現:

從尾到頭輸出鏈表的內容,通常的思路就是將鏈表反轉過來,而後從頭至尾輸出數據。

2)棧實現

從頭至尾遍歷單鏈表,將數據存儲按照順序存儲到棧中。而後遍歷整個棧,打印輸出數據。

2)遞歸實現:

能夠經過遞歸的方式來實現單鏈表從尾到頭依次輸出,遞歸過程涉及到「遞」和「歸」,反轉鏈表輸出數據,正式利用了循環「遞」的過程,因此數據先從頭部輸出,那麼遞歸採用的是「歸」的過程來輸出內容,輸出當前結點先要輸出當前節點的下一節點。

▉ 測試用例

在寫代碼以前,要想好測試用例才能寫出健全、魯棒性的代碼,也是爲了考慮到邊界狀況,每每也是整個程序最致命的地方,若是考慮不全面,就會出現 bug,致使程序崩潰。

測試用例:

1)輸入空鏈表;

2)輸入的鏈表只有一個結點;

3)輸入的鏈表有多個結點。

▉ 代碼實現:反轉鏈表法
//定義結點
class Node{
    constructor(data){
        this.data = data;
        this.next = null;
    }
}
//定義鏈表
class LinkedList{
    constructor(){
        this.head = new Node('head');
    }
    
    // 功能:單鏈表反轉
    // 步驟:
    // 一、定義三個指針(pre=null/next/current)
    // 二、判斷鏈表是否可反轉(頭節點是否爲空、是否有第二個結點)
    // 三、尾指針指向第一個結點的 next
    // 四、尾指針向前移動
    // 五、當前指針(current)向後移動
    // 六、將 head 指向單轉好的結點
    reverseList = () =>{
        //聲明三個指針
        let current = this.head; //當前指針指向頭節點
        let pre = null;//尾指針
        let next;//指向當前指針的下一個指針

        //判斷單鏈表是否符合反轉的條件(一個結點以上)?
        if(this.head == null || this.head.next == null) return -1;

        //開始反轉
        while(current !== null){
            next = current.next;
            current.next = pre;
            pre = current;
            current = next;
        }
        this.head = pre;
    }
    
    //輸出結點
    print = () =>{
        let currentNode = this.head
        //若是結點不爲空
        while(currentNode !== null){
            console.log(currentNode.data)
            currentNode = currentNode.next;
        }
    }
}
複製代碼
▉ 代碼實現:循環棧
//方法三:棧實現
const tailToHeadOutput = (currentNode)=>{
    let stack = [];
    //遍歷鏈表,將數據入棧
    while(currentNode !== null){
        stack.push(currentNode.data);
        currentNode = currentNode.next;
    }
    //遍歷棧,數據出棧
    while(stack.length !== 0){
        console.log(stack.pop());
    }
}
複製代碼
▉ 代碼實現:遞歸
// 步驟:
// 一、判斷是否爲空鏈表
// 二、終止條件(下一結點爲空)
// 三、遞歸打印下一結點信息
const tailToHeadOutput = (head)=>{
	// 判斷是否空鏈表
	if(head !== null){
        // 判斷下一結點是否爲空
	    if(head.next !== null){
            // 下一結點不爲空,先輸出下一結點
	        tailToHeadOutput(head.next)
	    }
	    console.log(head.data);
	}else{
	    console.log("空鏈表");
	}
}
複製代碼
▉ 性能分析

反轉鏈表實現:

  • 時間複雜度:O(n)。須要遍歷整個鏈表,時間複雜度爲 O(n)。
  • 空間複雜度:O(1)。不須要額外的棧存儲空間,空間複雜度爲 O(1)。

循環棧實現:

  • 時間複雜度:O(n)。須要遍歷整個鏈表,時間複雜度爲 O(n)。
  • 空間複雜度:O(n)。須要額外的棧存儲空間,空間複雜度爲 O(n)。

遞歸實現:

  • 時間複雜度:O(n)。須要遍歷整個鏈表,時間複雜度爲 O(n)。
  • 空間複雜度:O(n)。須要額外的棧存儲空間,空間複雜度爲 O(n)。

2.2 小結

▉ 考察內容

1)對單鏈表的基本操做。

2)代碼的魯棒性。

3)循環、遞歸、棧的靈活運用。

▉ 擴展思考:循環和遞歸

適用條件:若是須要進行屢次計算相同的問題,將採用循環或遞歸的方式。

遞歸的優勢:代碼簡潔。

遞歸的缺點:

1)堆棧溢出:函數調用自身,函數的臨時變量是壓棧的操做,當函數執行完,棧才清空,若是遞歸的規模過大,在函數內部一直執行函數的自身調用,臨時變量一直壓棧,系統棧或虛擬機棧內存小,致使堆棧溢出。

2)重複計算:遞歸會出現不少的重複計算問題,重複計算對程序的性能有很大影響,致使消耗時間成指數增加,可是能夠經過散列表的方式解決。

3)高空間複雜度:遞歸的每次函數調用都要涉及到在內存開闢空間,壓棧、出棧等操做,即耗時又耗費空間,致使遞歸的效率並不如循環的效率。

擴展:

1)遞歸—棧:遞歸的本質是棧,一般用棧循環解決的問題適合於遞歸。

2)遞歸-動態規劃:動態規劃解決問題常常用遞歸的思路分析問題。關於遞歸重複計算問題,咱們一般使用自下而上的解決思路(動態規劃)來解決遞歸重複計算的問題。

▉ 注意事項:

1)涉及到循環解決的問題,能夠想想能不能使用遞歸來解決。

2)用遞歸解決必定要銘記遞歸的缺點帶來的性能問題。

3)遞歸解決的問題,能不能用動態規劃來解決,使得性能更高。

4)用到棧這種數據結構,想想遞歸是否能夠實現呢。


二、刪除鏈表結點

題目:在 O(1)的時間複雜度內刪除鏈表節點。

給定單向鏈表的頭指針和一個節點指針,定義一個函數在 O(1)時間內刪除該節點。

2.1 問題分析與解決

▉ 問題分析

1)想必看到單鏈表刪除節點的題,第一想到的就是刪除鏈表結點須要以 O(n)時間複雜度遍歷鏈表找到該結點的前結點,而後以 O(1)時間複雜度進行刪除,時間複雜度爲O(n)。而題目中的確實總體要求時間複雜度爲 O(1)。

2)怎麼才能達到 O(1)的時間複雜度刪除鏈表?若是不遍歷不就能夠了?若是直接刪除的時間複雜度爲 O(1),前提是咱們須要知道前結點才能作到。咱們就會想怎麼作到不用遍歷數據才能獲取到前結點呢?並且必須保證時間複雜度爲 O(1)。

3)可是必須讓本身多想一步就是若是刪除的結點是尾結點怎麼操做,若是刪除的鏈表結點只有一個結點,便是尾結點又是頭結點怎麼辦?

▉ 算法思路

得出如下幾種實現方式:

  • 交換結點法

1)這一有種技巧很難想到,就是我把當前結點的數據與下一結點的數據進行交換,刪除下一結點不就能夠達到時間複雜度爲O(1)了嗎。並且咱們知道當前結點就是下一結點的前節點,perfect。

2)針對以上兩種特殊狀況,若是是尾結點,沒有下一結點,咱們就從頭遍歷鏈表刪除節點;若是便是尾結點又是頭結點,那麼刪除頭結點,並置於 null。

▉ 測試用例
  1. 輸入空鏈表;

2)在多個結點鏈表中刪除中間結點;

3)在多個鏈表中刪除頭結點;

4)在多個鏈表總刪除尾結點;

5)在只有一個結點鏈表中刪除惟一結點;

▉ 代碼實現
// 定義結點
class Node{
    constructor(data){
        this.data = data;
        this.next = null;
    }
}
// 定義鏈表
class LinkedList{
    constructor(){
        this.head = new Node('head');
    }

    //根據 value 查找結點
    findByValue = (value) =>{
        let currentNode = this.head;
        while(currentNode !== null && currentNode.data !== value){
            currentNode = currentNode.next;
        }
        //判斷該結點是否找到
        console.log(currentNode)
        return currentNode === null ? -1 : currentNode;
    }

    //插入元素(指定元素向後插入)
    insert = (value,element) =>{
        //先查找該元素
        let currentNode = this.findByValue(element);
        //若是沒有找到
        if(currentNode == -1){
            console.log("未找到插入位置!")
            return;
        }
        let newNode = new Node(value);
        newNode.next = currentNode.next;
        currentNode.next = newNode;
    }

    //遍歷全部結點
    print = () =>{
        let currentNode = this.head
        //若是結點不爲空
        while(currentNode !== null){
            console.log(currentNode.data)
            currentNode = currentNode.next;
        }
    }

    // 刪除節點(核心代碼)
    deleteNode = node =>{
        // 判斷當前查找的結點是否爲 null
        if(node == null) return -1;
        // 一、查找刪除的結點
        let d_node = this.findByValue(parseInt(node.data))
        // 二、判斷該結點是否爲尾結點
        if(d_node.next == null){
            // 從新遍歷鏈表
            let p = null;
            let current = this.head;
            while(current.next !== null){
                p = current;
                current = current.next;
            }
            // 尾結點置爲 null
            p.next = null;
        }else{
            // 三、將刪除結點的值與下一結點交換
            d_node.data = d_node.next.data;
            // 四、刪除下一結點
            d_node.next = d_node.next.next;
        }
    }
}

// 測試 
sortedList1 = new LinkedList()
sortedList1.insert(1, 'head')
sortedList1.insert(2, 1)
sortedList1.insert(3, 2)
sortedList1.insert(4, 3)
sortedList1.print();
console.log('------------------------------刪除指定結點----------------------------')
let dnode = new Node('1')
sortedList1.deleteNode(dnode)
sortedList1.print();
複製代碼
▉ 性能分析
  • 時間複雜度:O(1)。通過上述的方法,刪除一個鏈表的結點,除了刪除一個鏈表的尾結點以外,其餘刪除節點的時間複雜度爲 O(1),獲取刪除的結點的前一結點,時間複雜度爲 O(1),刪除節點的時間複雜度爲 O(1)。只有刪除尾結點才須要遍歷整個鏈表,但大部分刪除節點是 O(1)的。使用分析時間複雜度的一個方法攤還分析,將刪除節點的時間複雜度平均分到其餘大部分狀況下,因此平均時間複雜度爲 O(1)。

  • 空間複雜度:O(1)。不須要額外的內存空間。

2.2 小結

▉ 內容考察

1)對單鏈表的刪除基本操做。

2)對問題的有創新思惟的解決能力:能不能將複雜問題的根源用另外一種思惟去優化。

3)問題考慮的全面性:考慮到問題出現的各類特殊狀況,以及邊界問題。


三、鏈表中的倒數第 K 個結點

題目:輸入一個鏈表,輸出該鏈表中倒數第 K 個節點。爲符合大多數人的習慣,從 1 開始計數,即鏈表的尾結點是倒數第一個節點。

3.1 問題分析與解決

▉ 問題分析

1)看到這個題的第一想法就是從鏈表頭遍歷到鏈表尾部,而後尾部倒數 k 個數,由於是單鏈表,因此倒數並不能實現,想法行不通。

2)那咱們只能將思路轉移到頭結點開始,怎麼才能從頭結點開始遍歷到倒數第 k 個結點呢?大致咱們能夠得出至少須要遍歷兩次鏈表。

3)上述能不能再優化呢?遍歷一次鏈表就能夠完成查找?

▉ 算法思路

得出如下幾種實現方式:

  • 兩次遍歷法
  • 一次遍歷法

前提條件:

1)不要忘記判斷單鏈表是否爲環型結構

兩次遍歷法:

1)有一個規律就是鏈表的長度 n 減去 k 加 1 就是倒數第 k 個數據。因此須要遍歷鏈表獲得鏈表的長度,而後再遍歷兩次找到鏈表的倒數第 k 個數據。整個過程須要遍歷兩遍鏈表。

一次遍歷法:

1)那咱們就用到雙指針,第一個指針指向第一個結點,第二個指針指向 k - 1 個結點,同時向前移動,直到第二個節點指向尾結點位置,第一個節點就指向了倒數第 k 結點。遍歷一遍鏈表就完成查找。

▉ 測試用例

1)k 的取值範圍(0 < k < n);輸入不在範圍內的數據。

2)輸入空鏈表。

3)查找倒數第 k 結點爲頭結點/尾結點。

▉ 代碼實現
// 定義結點
class Node{
    constructor(data){
        this.data = data;
        this.next = null;
    }
}
// 定義鏈表
class LinkedList{
    constructor(){
        this.head = new Node('head');
    }

    //根據 value 查找結點
    findByValue = (value) =>{
        let currentNode = this.head;
        while(currentNode !== null && currentNode.data !== value){
            currentNode = currentNode.next;
        }
        //判斷該結點是否找到
        console.log(currentNode)
        return currentNode === null ? -1 : currentNode;
    }

    //插入元素(指定元素向後插入)
    insert = (value,element) =>{
        //先查找該元素
        let currentNode = this.findByValue(element);
        //若是沒有找到
        if(currentNode == -1){
            console.log("未找到插入位置!")
            return;
        }
        let newNode = new Node(value);
        newNode.next = currentNode.next;
        currentNode.next = newNode;
    }

    //遍歷全部結點
    print = () =>{
        let currentNode = this.head
        //若是結點不爲空
        while(currentNode !== null){
            console.log(currentNode.data)
            currentNode = currentNode.next;
        }
    }

    // 檢測單鏈表是否爲環
    checkCircle = ()=>{
        // 判斷是否爲空鏈表
        if(this.head == null) return fast;
        // 定義快慢指針
        let fast = this.head.next;
        let low = this.head;
        //進行循環判斷(當前 fast 結點/fast 移動兩步後的結點是否爲 null)
        while(fast !== null && fast.next !== null){
            // fast 指針向前移動兩步
            fast = fast.next.next;
            // low 指針向前移動一步
            low = low.next;
            // 若是爲環,總有一天會相遇
            if(fast === low) return true;
        } 
        return false;
    }

    // 查找倒數第 k 結點
    findByIndexFromEnd = k =>{
        //判斷 k 是否大於0
        if(k < 1) return 'k 的大小不在搜索範圍內';
        // 檢測是否爲環
        if(this.checkCircle()) return false;
        // 定義兩個指針進行遍歷
        let current = this.head;
        let fast = current;
        let low = current;

        let pos = 0;
        for(let i = 1;i <= k - 1;i++){
            if(fast.next !== null){
                fast = fast.next;
            }else{
                // k 的大小超出鏈表大小的範圍
                return 'k 的大小超出鏈表的範圍';
            }
        }

        // low 和 fast 指針同時移動
        while(fast.next !== null){
            fast = fast.next;
            low = low.next;
        }

        // 返回倒數第 k 結點
        return low;
    }
}
// 測試
const list = new LinkedList();
list.insert('1','head');
// list.insert('2','1');
// list.insert('3','2');
// list.insert('4','3');
// list.insert('5','4');
// list.insert('6','5');
list.print();
console.log('-------------------查找倒數第 k 結點----------------')
console.log(list.findByIndexFromEnd(8)); 
複製代碼
▉ 性能分析

兩次遍歷法:

  • 時間複雜度:O(k*n)。當 k 趨近於 n 時,最壞時間複雜度爲 O(n^2)。
  • 空間複雜度:O(1)。不須要額外的內存空間。

一次遍歷法:

  • 時間複雜度:O(n)。只須要遍歷一次單鏈表,因此時間複雜度爲O(n)。
  • 空間複雜度:O(1)。不須要額外的內存空間。

3.2 小結

▉ 內容考察

1)對單鏈表的基本操做。

2)代碼的全面性、魯棒性。

▉ 注意事項

1)當咱們用一個指針不能解決時,想想兩個指針可否解決?

▉ 相關題目

1)求中間結點

2)求倒數第 k 個結點

3)檢測環的存在


四、反轉鏈表

題目:定義一個函數,輸入一個鏈表的頭結點,反轉該鏈表並輸出反轉鏈表的頭結點。

4.1 問題分析與解決

▉ 問題分析

反轉鏈表的咱們第一可以想到的方法就是最經常使用的方法,聲明三個指針,把頭結點變爲尾結點,而後下一結點拼接到尾結點的頭部,一次類推。說白了就是就是直接將鏈表指針反轉就能夠實現反轉鏈表。

▉ 算法思路

1)定義三個指針,分別爲 Pnext、pre、current,current 存儲當前結點, pre 指向反轉好的結點的頭結點,Pnext 存儲下一結點信息。

2)判斷當前結點是否能夠反轉(是否爲空鏈表或鏈表大於 1 個結點)?

步驟:

1)Pnext 指針存儲下一結點 。

2)當前結點的 next 結點是否爲 null (爲 null 的話當前結點就是最後的一個結點),若是爲 null,將當前節點賦值爲 head 頭指針(斷裂處)。

3)將 pre 指針指向的結點賦值當前節點 current 的下一結點 next。

4)而後讓 pre 指針指向當前節點 current。

5)current 繼續遍歷, 當前節點指向 current 指向 Pnext。

遞歸法(重點分析):

1)先肯定終止條件:當下一結點爲 null 時,返回當前節點;

2)判斷當前的鏈表是否爲 null;

3)遞歸找到尾結點,將其存儲爲頭結點。

4)此時遞歸的層次是第二層遞歸,因此要設置爲頭結點的下一結點就是當前第二層結點,而且將第二節點的下一結點設置爲 bull。

▉ 測試用例

1)鏈表是空鏈表。

2)當前鏈表的長度小於等於 1。

3)輸入長度大於 1 的鏈表。

▉ 代碼實現
var reverseList = function(head) {
    // 判斷當前鏈表是否爲空鏈表
    if(head == null) return null;

    // 定義三個指針
    let [current,prev,next] = [head,null,null];

    while(current !== null){
        //一、存儲下一結點
        next = current.next;
        if(next == null){
            head = current;
        }
        current.next = prev;
        prev = current;
        current = next;
    }
    return head;
};
複製代碼
▉ 遞歸法
const reverseList = (head)=>{
    //若是鏈表爲空或者鏈表中只有一個元素
    if(head == null || head.next == null){
        return head;
    }else{
        //先反轉後面的鏈表,走到鏈表的末端結點
        let newhead = reverseList(head.next);
        //再將當前節點設置爲後面節點的後續節點
        head.next.next = head;
        head.next = null;
        return newhead;
    }
}
複製代碼
▉ 性能分析
  • 時間複雜度:O(n)。只需遍歷整個鏈表就能夠完成反轉,時間複雜度爲 O(n)。
  • 空間複雜度:O(1)。只須要常量級的空間,空間複雜度爲 O(1)。

4.2 小結

▉ 內容考察

1)對單鏈表的基本操做。

2)對指針操做順序的邏輯性考察。

3)考察思惟的全面性以及代碼的魯棒性。

▉ 注意事項

1)邊界條件。

2)寫代碼以前想好測試用例,寫完代碼一一驗證測試用例的正確性。


五、合併兩個有序鏈表

題目:輸入兩個遞增排序的鏈表,合併這兩個鏈表並使新鏈表中的節點仍然是遞增排序的。

5.1 問題分析與解決

▉ 問題分析

1)合併兩個鏈表,常常犯的錯誤就是沒有弄清除指針的指向,致使鏈表合併的時候斷裂以及代碼全面性考慮的不全,也就是代碼的魯棒性存在問題。

2)遞歸。每次都要比較兩個結點大小,是否可使用遞歸來解決呢?

▉ 算法思路

通常解決法:

1)合併兩個鏈表,首先須要兩個指針,分別指向兩個鏈表。

2)比較兩個指針指向結點元素的大小,小的結點添加到新鏈表,而後指針向後移動繼續比較。

3)直到其中一個鏈表沒有結點了,另外一個鏈表存在結點,將剩餘的結點加入到新鏈表的尾部,完成合並。

遞歸法:(知足遞歸的三個條件)

比較當前結點大小先比較下一結點的大小。

1)結點之間的比較能夠分的子問題爲每一個節點的比較。

2)終止條件:其中一個鏈表結點爲 null。

3)子問題和總問題具備相同的解決思路。

▉ 測試用例

1)輸入兩個空鏈表。

2)其中一個鏈表爲空鏈表。

3)輸入兩個完整的鏈表。

▉ 代碼實現
// 功能:兩個有序鏈表的合併
// 步驟:
// 一、判斷兩個鏈表是否爲 null,並將鏈表賦予臨時變量
// 二、聲明合併鏈表,經過 currentNode 指向當前結點
// 三、兩個鏈表比較大小,數值小的添加到合併鏈表中,合併鏈表進行指針移動
// 四、將鏈表剩餘數據添加到合併鏈表後邊
const mergeSortList = (listA,listB) =>{
    //判斷鏈表是否爲空
    if(listA === null) return false;
    if(listB === null) return false;
    let a = listA;
    let b = listB;

    //聲明合併鏈表,經過 currentNode 指向當前結點
    let resultList = undefined

    //兩個鏈表比較大小,數值小的添加到合併鏈表中,合併鏈表進行指針移動
    if (a.data < b.data) {
        resultList = a
        a = a.next
    } else {
        resultList = b
        b = b.next
    }
    let currentNode = resultList;
    while (a !== null && b !== null) {
        if (a.data < b.data) {
            currentNode.next = a
            a = a.next
        } else {
            currentNode.next = b
            b = b.next
        }
        currentNode = currentNode.next
    } 

    // 將鏈表剩餘數據添加到合併鏈表後邊 
    if(a !== null){
        currentNode.next = a;
    }else{
        currentNode.next = b;
    }
    //返回合併鏈表
    return resultList;
}
複製代碼
▉ 遞歸實現
var mergeTwoLists = function(l1, l2) {
    let result = null;
    //終止條件
    if(l1 == null) return l2;
    if(l2 == null) return l1;

    //判斷數值大小遞歸
    if(l1.val < l2.val){
        result = l1;
        result.next = mergeTwoLists(l1.next,l2);
    }else{
        result = l2;
        result.next = mergeTwoLists(l2.next,l1);
    }

    //返回結果
    return result;
};
複製代碼
▉ 代碼測試
//定義結點
class Node{
    constructor(data){
        this.data = data;
        this.next = null;
    }
}

//定義鏈表
class LinkedList{
    constructor(){
        this.head = new Node('head');
    }

    //根據 value 查找結點
    findByValue = (value) =>{
        let currentNode = this.head;
        while(currentNode !== null && currentNode.data !== value){
            currentNode = currentNode.next;
        }
        //判斷該結點是否找到
        console.log(currentNode)
        return currentNode === null ? -1 : currentNode;
    }

    //插入元素(指定元素向後插入)
    insert = (value,element) =>{
        //先查找該元素
        let currentNode = this.findByValue(element);
        //若是沒有找到
        if(currentNode == -1){
            console.log("未找到插入位置!")
            return;
        }
        let newNode = new Node(value);
        newNode.next = currentNode.next;
        currentNode.next = newNode;
    }

    //遍歷全部結點
    print = () =>{
        let currentNode = this.head
        //若是結點不爲空
        while(currentNode !== null){
            console.log(currentNode.data)
            currentNode = currentNode.next;
        }
    }
}
// 合併兩個鏈表
var mergeSortList = function(l1, l2) {
    let result = null;
    //終止條件
    if(l1 == null) return l2;
    if(l2 == null) return l1;

    //判斷數值大小遞歸
    if(l1.val < l2.val){
        result = l1;
        result.next = mergeSortList(l1.next,l2);
    }else{
        result = l2;
        result.next = mergeSortList(l2.next,l1);
    }

    //返回結果
    return result;
};

// 測試
sortedList1 = new LinkedList()
sortedList1.insert(9, 'head')
sortedList1.insert(8, 'head')
sortedList1.insert(7, 'head')
sortedList1.insert(6, 'head')
sortedList1.print();
sortedList2 = new LinkedList()
sortedList2.insert(21, 'head')
sortedList2.insert(20, 'head')
sortedList2.insert(19, 'head')
sortedList2.insert(18, 'head')
sortedList2.print();
console.log('----------------合併兩個有序的鏈表----------------')
let resultList = mergeSortList(sortedList1.head.next,sortedList2.head.next)
while (resultList !== null) {
    console.log(resultList.date);
    resultList = resultList.next;
}
複製代碼
▉ 性能分析
  • 時間複雜度:O(n)。n 爲較短的鏈表的長度。
  • 空間複雜度:O(n+m)。須要額外的 n+m(兩個鏈表長度之和) 大小的空間來存儲合併的結點。

5.2 小結

▉ 內容考察

1)對鏈表的基本操做。

2)寫代碼考慮問題的全面性和魯棒性。

▉ 注意事項

1)遞歸實現,注意遞歸解決問題的三個缺點。

  • 堆棧溢出
  • 重複數據
  • 高空間複雜度

3、LeetCode 增強練習階段(階段三)

若是你對基本的鏈表操做已經掌握,想進一步提升對鏈表熟練度的操做,能夠練習一下 LeetCode 題目。每道題我都作了詳細的解析,如:問題分析、算法思路、代碼實現、考查內容等,有關鏈表的相關題目會不斷更新......


4、鏈表總結

作了大量有關鏈表的題型以後,對鏈表的操做作一個總結和覆盤,對鏈表有一個總體的把握和從新的認識。

一、結構上

1)存儲鏈表的內存空間是不連續的,全部須要使用指針將這些零碎內存空間鏈接起來,致使須要經過指針來進行操做,這也是爲何鏈表中大多數都是關於指針的操做的緣由。

2)鏈表在結構上有兩個特殊的地方就是鏈表頭和鏈表尾,不少操做都要對鏈表頭和鏈表尾進行特殊處理,因此咱們能夠藉助哨兵思想(在鏈表頭添加一個哨兵),這樣帶頭的鏈表能夠簡化問題的解決。

二、操做上

1)遞歸:鏈表中的不少操做都是能夠用遞歸來進行解決的,由於鏈表的每一個結點都有着相同的結構,再加上解決的問題能夠分解爲子問題進行解決。因此在鏈表中遞歸編程技巧仍是很是經常使用的。如:從尾到頭打印鏈表、合併兩個有序鏈表、反轉鏈表等。

2)雙指針:鏈表中大部分都是進行指針操做,鏈表屬於線性表結構(形如一條線的結構),不少問題可使用雙指針來解決,也是很是經常使用到的。如:查找倒數第K 結點、求鏈表的中間結點等。

三、性能上

1)鏈表正是由於存儲空間不連續,對 CPU 緩存不友好,隨時訪問只能從頭遍歷鏈表,時間複雜度爲 O(n),可是鏈表的這種結構也有個好處就是。能夠動態的申請內存空間,不須要提早申請。

2)指針的存儲是須要額外的內存空間的,若是存儲的數據遠大於存儲指針的內存空間,能夠進行忽略。

做者:小鹿 座右銘:追求平淡不平凡,一輩子追求作一個不甘平凡的碼農! 本文首發於 Github ,轉載請說明出處。 我的公衆號:一個不甘平凡的碼農。
相關文章
相關標籤/搜索