JavaScript資料結構 之 連結串列

TDX發表於2019-06-26

一、什麼是連結串列 

連結串列是一種鏈式儲存的線性表,是由一組節點組成的集合,每一個節點都儲存了下一個節點的地址;指向另一個節點的引用叫鏈;和陣列中的元素記憶體地址是連續的相比,連結串列中的所有元素的記憶體地址不一定是連續的。結構模擬如圖:

一般來說,說到連結串列,就要提下陣列,一般連結串列都是和陣列進行對比。

在很多程式語言中,陣列的長度時固定的,所以陣列中的增加和刪除比較麻煩,需要頻繁的移動陣列中的其他元素。

然而,JavaScript中的陣列並不存在上述問題,JS中的陣列相對其他語言使用上更方便,因為JS中的陣列本質是一個類似陣列的物件,這就使得JS的陣列雖然使用更方便,但比其他語言(C++、Java、C#)的陣列效率要低。

所以,在實際應用中如果發現陣列很慢,就可以考慮使用連結串列來替代它。除了對資料的隨機訪問,連結串列幾乎可以用在任何可以使用一維陣列的情況中。如果需要隨機訪問,陣列仍然是更好的選擇。

 

二、連結串列的設計

為了對連結串列更好的使用,我們設計了類LinkedList, 對連結串列中節點的增刪改查方法進行了封裝。結構如圖:

其中size和head為LinkedList建構函式私有屬性,size記錄連結串列中有多少個節點,head指向連結串列的頭結點。

根據需要對外暴露了以下方法(可以根據需要自定義其他方法):

 單向LinkedList完整設計程式碼:

/**
 * 自定義連結串列:對外公開的方法有
 * append(element) 在連結串列最後追加節點
 * insert(index, element) 根據索引index, 在索引位置插入節點
 * remove(element)  刪除節點
 * removeAt(index)  刪除指定索引節點
 * removeAll(element) 刪除所有匹配的節點
 * set(index, element) 根據索引,修改對應索引的節點值
 * get(index)  根據索引獲取節點資訊
 * indexOf(element) 獲取某個節點的索引位置
 * clear()  清空所有節點
 * length()   返回節點長度
 * print() 列印所有節點資訊
 * toString() 列印所有節點資訊,同print
 * */
const LinkedList = function(){
    let head = null;
    let size = 0;   //記錄連結串列元素個數

    //Node模型
    function LinkNode(element, next){
        this.element = element;
        this.next = next;
    }

    //元素越界檢查, 越界丟擲異常
    function outOfBounds(index){
        if (index < 0 || index >= size){
            throw("抱歉,目標位置不存在!");
        }
    }

    //根據索引,獲取目標物件
    function node(index){
        outOfBounds(index);

        let obj = head;
        for (let i = 0; i < index; i++){
            obj = obj.next;
        }

        return obj;
    }

    //新增一個元素
     function append(element){
        if (size == 0){
            head = new LinkNode(element, null);
        }
        else{
            let obj = node(size-1);
            obj.next = new LinkNode(element, null);
        }
         size++;
    }

    //插入一個元素
     function insert(index, element){
        if (index == 0){
            head = new LinkNode(element, head);
        }
        else{
            let obj = node(index-1);
            obj.next = new LinkNode(element, obj.next);
        }
         size++;
    }

    //修改元素
    function set(index, element){
        let obj = node(index);
        obj.element = element;
    }

    //根據值移除節點元素
    function remove(element){
        if (size < 1) return null;

        if (head.element == element){
            head = head.next;
            size--;
            return element;
        }
        else{
            let temp = head;
            while(temp.next){
                if (temp.next.element == element){
                    temp.next = temp.next.next;
                    size--;
                    return element;
                }
                else{
                    temp = temp.next;
                }
            }
        }
        return null;
    }

    //根據索引移除節點
     function removeAt(index){
         outOfBounds(index);
         let element = null;

         if (index == 0){
             element = head.element;
             head = head.next;
         }
         else{
             let prev = node(index-1);
             element = prev.next.element;
             prev.next = prev.next.next;
         }
         size--;
        return element;
    }

    //移除連結串列裡面的所有匹配值element的元素
     function removeAll(element){

        let virHead = new LinkNode(null, head); //建立一個虛擬頭結點,head為次節點
         let tempNode = virHead, ele = null;

         while(tempNode.next){
             if (tempNode.next.element == element){
                 tempNode.next = tempNode.next.next;
                 size--;
                 ele = element;
             }
             else{
                tempNode = tempNode.next;
             }
         }

         //重新賦值
         head = virHead.next;

        return ele;
    }

    //獲取某個元素
    function get(index){
        return node(index).element;
    }

    //獲取元素索引
    function indexOf(element){
        let obj = head, index = -1;

        for (let i = 0; i < size; i++){
            if (obj.element == element){
                index = i;
                break;
            }
            obj = obj.next;
        }
        return index;
    }

    //清除所有元素
    function clear(){
        head = null;
        size = 0;
    }

    //屬性轉字串
    function getObjString(obj){

        let str = "";

        if (obj instanceof Array){
            str += "[";
            for (let i = 0; i < obj.length; i++){
                str += getObjString(obj[i]);
            }
            str = str.substring(0, str.length - 2);
            str += "], "
        }
        else if (obj instanceof Object){
            str += "{";
            for (var key in obj){
                let item = obj[key];
                str += "\"" + key + "\": " + getObjString(item);
            }
            str = str.substring(0, str.length-2);
            str += "}, "
        }
        else if (typeof obj == "string"){
            str += "\"" + obj + "\"" + ", ";
        }
        else{
            str += obj + ", ";
        }

        return str;
    }
    function toString(){
        let str = "", obj = head;
        for (let i = 0; i < size; i++){
            str += getObjString(obj.element);
            obj = obj.next;
        }
        if (str.length > 0) str = str.substring(0, str.length -2);
        return str;
    }
    //列印所有元素
    function print(){
        console.log(this.toString())
    }

    //對外公開方法
    this.append = append;
    this.insert = insert;
    this.remove = remove;
    this.removeAt = removeAt;
    this.removeAll = removeAll;
    this.set = set;
    this.get = get;
    this.indexOf = indexOf;
    this.length = function(){
        return size;
    }
    this.clear = clear;
    this.print = print;
    this.toString = toString;
}


////測試
// let obj = new LinkedList();
// let obj1 = { title: "全明星比賽", stores: [{name: "張飛vs岳飛", store: "2:3"}, { name: "關羽vs秦瓊", store: "5:5"}]};
//
// obj.append(99);
// obj.append("hello")
// obj.append(true)
// obj.insert(3, obj1);
// obj.insert(0, [12, false, "Good", 81]);
// obj.print();
// console.log("obj1.index: ", obj.indexOf(obj1));
// obj.remove(0);
// obj.removeAll(obj1);
// obj.print();

////測試2
console.log("\n\n......test2.....")
var obj2 = new LinkedList();
obj2.append(8); obj2.insert(1,99); obj2.append('abc'); obj2.append(8); obj2.append(false);
obj2.append(12); obj2.append(8); obj2.append('123'); obj2.append(8);
obj2.print();
obj2.removeAll(8); //刪除所有8
obj2.print();
View Code

 

另外,可以在LinkedList中增加一個虛擬節點,即在頭結點之前增加一個節點,一直保留,結構如圖:

這裡程式碼就不提供了,在上一份連結串列程式碼中的removeAll(刪除連結串列中指定值的所有節點)方法中有用到虛擬頭結點, 下面的練習題中也有應用到虛擬頭結點,應用場景還是蠻多的。

 

三、連結串列練習題

推薦一個神奇的網站,可以以動畫的方式演示各種資料結構增刪改查變化,先來張展示連結串列的增刪效果圖看看:

網址:https://visualgo.net/zh

 

接下來做幾個連結串列的練習題,題目來自力扣,可以先自己先做一下,看看自己得分,再對比下官方提供的程式碼demo

3.1 刪除排序連結串列中的重複元素_第83題

參考demo:

/**
 * 給定一個排序連結串列,刪除所有重複的元素,使得每個元素只出現一次。
 示例 1:
 輸入: 1->1->2
 輸出: 1->2

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

 力扣得分:
 執行用時 :108 ms, 在所有 JavaScript 提交中擊敗77.12%的使用者
 記憶體消耗 :37.4 MB, 在所有 JavaScript 提交中擊敗了5.03%的使用者
 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {

    let virHead = new ListNode(0); //增加一個虛擬節點
    virHead.next = head;
    let temp = virHead, obj = {};

    while(temp.next){
        if (obj[temp.next.val]){ //表示為重複節點,刪除這個節點
            temp.next = temp.next.next;
        }
        else{ //
            obj[temp.next.val] = 1;
            temp = temp.next;
        }
    }
    return virHead.next;
}

//測試
var obj = new ListNode(1);
obj.next = new ListNode(2);
obj.next.next = new ListNode(1);
obj.next.next.next = new ListNode(3);
obj.next.next.next.next = new ListNode(1);
obj.next.next.next.next.next = new ListNode(2);
obj.next.next.next.next.next.next = new ListNode(3);
console.log(obj);
console.log(".>>>>>>刪除重複節點:")
console.log(deleteDuplicates(obj));
View Code

 

3.2 判斷是否環形連結串列_第141題

參考demo:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    //快慢指標,快指標每次走兩步,慢指標每次走一步
    let obj1 = head, obj2 = head; //obj1快指標,obj2為慢指標

    while(obj2){
      obj2 = obj2.next;

      if (obj1){
          obj1 = obj1.next;
      }

      if (obj1){
          obj1 = obj1.next;
      }

      if (obj2 == obj1 && obj1) return true;
    }
    return false;
};

function ListNode(val){
    this.val = val;
    this.next = null;
}

//測試
console.log(">>>>>>環形連結串列》》測試》》")
let node1 = new ListNode(1);
let node2 = new ListNode(2);
let node3 = new ListNode(3);
let node4 = new ListNode(4);

node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2;

let res = hasCycle(node1);
console.log("res: ", res);
View Code

 

3.3 移除連結串列中給定值的所有元素_第203題

 

參考demo1:

/**
 刪除連結串列中等於給定值 val 的所有節點。
 示例:
 輸入: 1->2->6->3->4->5->6, val = 6
 輸出: 1->2->3->4->5

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * 在力扣中得分:耗時160ms, 打敗Javascript中17.87%; 記憶體消耗37.5M, 打敗JavaScript中24.79% , 更優化的寫法是?
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    let newHead = null, curNode = null;
    while(head){
        if (head.val != val){
            if (curNode){
                curNode.next = new ListNode(head.val);
                curNode = curNode.next;
            }
            else{
                curNode = new ListNode(head.val);
                newHead = curNode;
            }
        }
        head = head.next;
    }
    return newHead;
}

function ListNode(val){
    this.val = val;
    this.next = null;
}


//測試
console.log(">>>>移除連結串列元素測試》》》")
var node = new ListNode(1);
node.next = new ListNode(2);
// node.next.next = new ListNode(5);
// node.next.next.next = new ListNode(4);
// node.next.next.next.next = new ListNode(6);
// node.next.next.next.next.next = new ListNode(8);
// node.next.next.next.next.next.next = new ListNode(4);

// var newNode = removeElements(node, 6);
// console.log(newNode);

var newNode = removeElements(node, 2);
console.log(newNode);
View Code

參考demo2:

/**
 刪除連結串列中等於給定值 val 的所有節點。
 示例:
 輸入: 1->2->6->3->4->5->6, val = 6
 輸出: 1->2->3->4->5

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/** 第二種寫法
 * 在力扣中得分:耗時112ms, 打敗Javascript中90.28%; 記憶體消耗37.5M, 打敗JavaScript中24.79%
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    if (!head) return head;

    let newHead = new ListNode(-1);
    newHead.next = head; //把head作為newHead的下一個
    let tmpNode = newHead;

    while(tmpNode.next){
        if (tmpNode.next.val == val){
            tmpNode.next = tmpNode.next.next;
        }
        else{
            tmpNode = tmpNode.next;
        }
    }
    return newHead.next; //返回newHead的下一個,就是我們想要的結果
}

function ListNode(val){
    this.val = val;
    this.next = null;
}


//測試
console.log(">>>>移除連結串列元素測試》》》")
var node = new ListNode(1);
node.next = new ListNode(2);
// node.next.next = new ListNode(5);
// node.next.next.next = new ListNode(4);
// node.next.next.next.next = new ListNode(6);
// node.next.next.next.next.next = new ListNode(8);
// node.next.next.next.next.next.next = new ListNode(4);

// var newNode = removeElements(node, 6);
// console.log(newNode);

var newNode = removeElements(node, 2);
console.log(newNode);
View Code

 

3.4 反轉連結串列_第206題

 

參考demo1_迭代方式:

/*
 反轉一個單連結串列。使用迭代方式實現
 示例:
 輸入: 1->2->3->4->5->NULL
 輸出: 5->4->3->2->1->NULL

 力扣中測試執行用時 : 76 ms, 在所有 JavaScript 提交中擊敗了97.74%的使用者
 記憶體消耗 :36 MB, 在所有 JavaScript 提交中擊敗了6.92%的使用者
 * */

function ListNode(val){
    this.val = val;
    this.next = null;
}
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let newHead = null;
    while(head){
        let tmpNode= newHead;
        newHead = new ListNode(head.val);
        newHead.next = tmpNode;
        head = head.next;
    }
    return newHead;
}


////測試
var node = new ListNode(9);
node.next = new ListNode(99);
node.next.next = new ListNode(999);
node.next.next.next = new ListNode(33);

console.log("原連結串列:", node);
console.log(".....反轉....")
console.log(reverseList(node))
View Code

參考demo2_遞迴方式:

/*
 反轉一個單連結串列。 使用遞迴方式實現
 示例:
 輸入: 1->2->3->4->5->NULL
 輸出: 5->4->3->2->1->NULL

 力扣測試得分:
 執行用時 :80 ms, 在所有 JavaScript 提交中擊敗了95.56%的使用者
 記憶體消耗 :36.3 MB, 在所有 JavaScript 提交中擊敗了5.03%的使用者
* */

function ListNode(val){
    this.val = val;
    this.next = null;
}
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    return getNewNode(head).first;
}

/**
 * 遞迴,好繞啊:
 * 推演:加入2->3->4->5 遞迴:
 * @param node
 */
function getNewNode(node){

    if (!node) return {first: null, cur: null };

    var cur = new ListNode(node.val);

    ////一直遞迴遞迴,拿到原連結串列最後一個元素開始返回
    var res = getNewNode(node.next);

    if (res.first) {
        res.cur.next = cur; //設定

        return {
            first: res.first, //反轉連結串列的第一個元素
            cur: cur
        }
    }

    console.log("666_node.val: ", node.val);
    /**
     * 原連結串列最後一個元素會執行到這裡,最後一個元素作為反轉連結串列的第一個元素返回
     */

    return {
        first: cur, //反轉連結串列的第一個元素
        cur: cur    //每次遞迴返回的一個元素
    };
}

//測試
var node = new ListNode(2);
node.next = new ListNode(3);
node.next.next = new ListNode(4);
node.next.next.next = new ListNode(5);
console.log("\n\n*****原連結串列****")
console.log(node);
console.log("......反轉.....")
console.log(reverseList(node));
View Code

 

3.5 查詢連結串列的中間結點_第876題

參考程式碼demo1_迭代方式:

/**
 * 給定一個帶有頭結點 head 的非空單連結串列,返回連結串列的中間結點。
 如果有兩個中間結點,則返回第二個中間結點。

 示例 1:
 輸入:[1,2,3,4,5]
 輸出:此列表中的結點 3 (序列化形式:[3,4,5])
 返回的結點值為 3 。 (測評系統對該結點序列化表述是 [3,4,5])。
 注意,我們返回了一個 ListNode 型別的物件 ans,這樣:
 ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

 示例 2:
 輸入:[1,2,3,4,5,6]
 輸出:此列表中的結點 4 (序列化形式:[4,5,6])
 由於該列表有兩個中間結點,值分別為 3 和 4,我們返回第二個結點。
  

 提示:
 給定連結串列的結點數介於 1 和 100 之間。

 力扣得分:
 執行用時 :108 ms, 在所有 JavaScript 提交中擊敗了19.44%的使用者
 記憶體消耗 :33.6 MB, 在所有 JavaScript 提交中擊敗了74.60%的使用者
 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var middleNode = function(head) {

    if (!head) return head;

    let arr = [];
    while(head){
        arr.push(head);
        head = head.next;
    }

    let len = arr.length;
    return len % 2 == 0 ? arr[len/2] : arr[(len-1)/2];
};

//測試
var obj = new ListNode(1), temp = obj;
for (let i = 0; i < 6; i++){
    temp.next = new ListNode(2+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));
View Code

參考程式碼demo2_快慢指標:

/**
 * 給定一個帶有頭結點 head 的非空單連結串列,返回連結串列的中間結點。
 如果有兩個中間結點,則返回第二個中間結點。

 示例 1:
 輸入:[1,2,3,4,5]
 輸出:此列表中的結點 3 (序列化形式:[3,4,5])
 返回的結點值為 3 。 (測評系統對該結點序列化表述是 [3,4,5])。
 注意,我們返回了一個 ListNode 型別的物件 ans,這樣:
 ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

 示例 2:
 輸入:[1,2,3,4,5,6]
 輸出:此列表中的結點 4 (序列化形式:[4,5,6])
 由於該列表有兩個中間結點,值分別為 3 和 4,我們返回第二個結點。
  

 提示:
 給定連結串列的結點數介於 1 和 100 之間。

 力扣得分:
 執行用時 :120 ms, 在所有 JavaScript 提交中擊敗了12.22%的使用者
 記憶體消耗 :34.1 MB, 在所有 JavaScript 提交中擊敗了11.11%的使用者

 官方答案,官方這個確實簡潔:
 let slow = fast = head;
 while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
 return slow;

 官方力扣得分:
 執行用時 :64 ms, 在所有 JavaScript 提交中擊敗了99.44%的使用者
 記憶體消耗 :34.1 MB, 在所有 JavaScript 提交中擊敗了11.11%的使用者

 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/** 用快慢指標來處理下
 * @param {ListNode} head
 * @return {ListNode}
 */
var middleNode = function(head) {
    // let slow = head, fast = head;
    // while(slow){
    //     if (fast){
    //         fast = fast.next;
    //         if (fast){
    //             fast = fast.next;
    //         }
    //         else{
    //             return slow;
    //         }
    //     }
    //     else{
    //         return slow;
    //     }
    //     slow = slow.next;
    // }
    // return head;

    //官方答案:簡潔明瞭
    let slow = fast = head;
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
};

//測試
var obj = new ListNode(1), temp = obj;
for (let i = 0; i < 6; i++){
    temp.next = new ListNode(2+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));

obj = new ListNode(90), temp = obj;
for (let i = 0; i < 5; i++){
    temp.next = new ListNode(91+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));
View Code

 

參考Demo地址:https://github.com/xiaotanit/Tan_DataStruct

相關文章