資料結構知否知否系列之 — 線性表的順序與鏈式儲存篇(8000 多字長

lvxfcjf發表於2021-09-09

從不浪費時間的人,沒有工夫抱怨時間不夠。 —— 傑弗遜

線性表是由 n 個資料元素組成的有限序列,也是最基本、最簡單、最常用的一種資料結構。

作者簡介:五月君,Nodejs Developer,熱愛技術、喜歡分享的 90 後青年,公眾號「Nodejs技術棧」,Github 開源專案

前言

本篇文章歷時一週多,差不多花費了兩個週末的時間,在書寫的過程中更多的還是在思考每一種線性表的演算法實現,連結串列的指標域部分對於不理解指標或者物件引用的童鞋,在閱讀程式碼的時候可能會濛濛的,本篇文章程式碼部分採用的 JavaScript 程式語言,但是實現思想是相通的,如果你用 Java、Python 等也可做參考,如文章有理解錯誤之處歡迎在下方評論區指正。

認識線性表

根據線性表的定義,可得出幾個關鍵詞:n 個資料元素有限序列,也就是說它是有長度限制的且元素之間是有序的,在多個元素之間,第一個元素無前驅,最後一個元素無後繼,中間元素有且只有一個前驅和後繼。

舉一個與大家都息息相關的十二生肖例子,以“子(鼠)” 開頭,“亥(豬)”結尾,其中間的每個生肖也都有其前驅和後繼,圖例如下所示:

圖片描述

下面再介紹一個複雜的線性表,其一個元素由多個資料項構成,例如,我們的班級名單,含學生的學號、姓名、年齡、性別等資訊,圖例如下所示:

圖片描述

線性表兩種儲存結構

線性表有兩種儲存結構,一種為順序結構儲存,稱為順序表;另一種為鏈式形式儲存,稱為連結串列,連結串列根據指標域的不同,連結串列分為單向連結串列、雙向連結串列、迴圈連結串列等。詳細的內容會在後面展開講解。

順序表

順序表是在計算機記憶體中以陣列的形式儲存的線性表,是指用一組地址連續的儲存單元依次儲存資料元素的線性結構。

線上性表裡順序表相對更容易些,因此也先從順序表講起,透過實現編碼的方式帶著大家從零開始實現一個順序表,網上很多教程大多都是以 C 語言為例子,其實現思想都是相通的,這裡採用 JavaScript 編碼實現。

實現步驟

  1. Constructor(capacity): 初始化順序表記憶體空間,設定順序表的容量
  2. isEmpty(): 檢查順序表是否為空,是否有元素
  3. isOverflow(): 檢查順序表空間是否已滿
  4. getElement(i): 返回順序表中第 i 個資料元素的值
  5. locateElement(e): 返回順序表中第 1 個與 e 滿足關係的元素,不存在,則返回值為 -1
  6. priorElement(e): 在順序表中返回指定元素的前驅
  7. nextElement(e): 在順序表中返回指定元素的後繼
  8. listInsert(i, e): 在順序表中第 i 個位置之前插入新的資料元素 e
  9. listDelete(i): 刪除順序表的第 i 個資料元素,並返回其值
  10. clear(): 清空順序表元素,記憶體空間還是保留的
  11. destroy(): 銷燬順序表,同時記憶體也要回收(通常高階語言都會有自動回收機制,在 C 語言中這時就需要手動回收)
  12. traversing(): 遍歷輸出順序表元素

初始化順序表空間

在建構函式的 constructor 裡進行宣告,傳入 capacity 初始化順序表空間同時初始化順序表的元素長度(length)為 0。

/**
 * 
 * @param { Number } capacity 棧空間容量
 */
constructor(capacity) {
    if (!capacity) {
        throw new Error('The capacity field is required!');
    }

    this.capacity = capacity;
    this.list = new Array(capacity);
    this.length = 0; // 初始化順序表元素長度
}

順序表是否為空檢查

定義 isEmpty() 方法返回順序表是否為空,根據 length 順序表元素進行判斷。

isEmpty() {
    return this.length === 0 ? true : false;
}

順序表是否溢位檢查

定義 isOverflow() 方法返回順序表空間是否溢位,根據順序表元素長度和初始化的空間容量進行判斷。

isOverflow() {
    return this.length === this.capacity;
}

查詢指定位置元素

返回順序表中第 i 個資料元素的值

getElement(i) {
    if (i < 0 || i > this.length) {
        return false;
    }

    return this.list[i];
}

查詢元素的第一個位置索引

返回順序表中第 1 個與 e 滿足關係的元素,存在則返回其索引值;不存在,則返回值為 -1

locateElement(e) {
    for (let i=0; i<this.length; i++) {
        if (this.list[i] === e) {
            return i;
        }
    }

    return -1;
}

在順序表中返回指定元素的前驅

這裡就用到了上面定義的 locateElement 函式,先找到元素對應的索引位置,如果前驅就取前一個位置,後繼就取後一個位置,在這之前先校驗當前元素的索引位置是否存在合法。

priorElement(e) {
    const i = this.locateElement(e);

    if (i === -1) {
        return false;
    }

    if (i === 0) { // 沒有前驅
        return false;
    }

    return this.list[i - 1]; // 返回前驅(即前一個元素)
}

在順序表中返回指定元素的後繼

nextElement(e) {
    const i = this.locateElement(e);

    if (i === -1) {
        return false;
    }

    if (i === this.length - 1) { // 為最後一個元素,沒有後繼
        return false;
    }

    return this.list[i + 1]; // 返回後繼(即後 一個元素)
}

插入元素

在順序表中第 i 個位置之前插入新的資料元素 e,在插入之前先進行元素位置後移,插入之後順序表元素的長度要加 1。

舉個例子,我們去火車站取票,恰逢人多大家都在排隊,突然來一個美女或者帥哥對你說我的車次馬上要開車了,你可能同意了,此時你的位置及你後面的童鞋就要後移一位了,也許你會聽到一些聲音,怎麼回事呀?怎麼插隊了呀,其實後面的人有的也不清楚什麼原因 “233”,看一個圖

圖片描述

演算法實現如下:

listInsert(i, e) {
    if (i < 0 || i > this.length) {
        return false; // 不合法的 i 值
    }

    for (let k=this.length; k>=i; k--) { // 元素位置後移 1 位
        this.list[k + 1] = this.list[k];
    }

    this.list[i] = e;
    this.length++;

    return true;
}

刪除元素

刪除順序表的第 i 個資料元素,並返回其值,與插入相反,需要將刪除位置之後的元素進行前移,最後將順序表元素長度減 1。

同樣以火車站取票的例子說明,如果大家都正在排隊取票,突然你前面一個妹子有急事臨時走了,那麼你及你後面的童鞋就要前進一步,圖例如下所示:

圖片描述

演算法實現如下:

 listDelete(i) {
    if (i < 0 || i >= this.length) {
        return false; // 不合法的 i 值
    }

    const e = this.list[i];

    for (let j=i+1; j<this.length; j++) { // 元素位置前移 1 位
        this.list[j - 1] = this.list[j];
    }

    this.length--;

    return e;
}

清除順序表元素

這裡有幾種實現,你也可以把順序表的空間進行初始化,或者把 length 棧位置設為 0 也可。

clear() {
    this.length = 0;
}

順序表銷燬

在一些高階語言中都會有垃圾回收機制,例如 JS 中只要當前物件不再持有引用,下次垃圾回收來臨時將會被回收。不清楚的可以看看我之前寫的

destroy() {
    this.list = null;
}

順序表元素遍歷

定義 traversing() 方法對順序表的元素進行遍歷輸出。

traversing(isBottom = false){
    const arr = [];

    for (let i=0; i < this.length; i++) {
        arr.push(this.list[i])
    }

    console.log(arr.join('|'));
}

做一些測試

做下測試分別看下插入、刪除、遍歷等操作,其它的功能大家在練習的過程中可自行實踐。

const [e1, e2, e3, e4, e5] = [3, 6, 1, 8, 7];
const list = new SequenceTable(10);
list.listInsert(0, e1);
list.listInsert(1, e2);
list.listInsert(2, e3);
list.listInsert(3, e4);
list.listInsert(1, e5);
list.traversing(); // 3|7|6|1|8

console.log(list.priorElement(3) ? '有前驅' : '無前驅'); // 無前驅
console.log(list.priorElement(6) ? '有前驅' : '無前驅'); // 有前驅
console.log(list.nextElement(3) ? '有後繼' : '無後繼'); // 有後繼
console.log(list.nextElement(8) ? '有後繼' : '無後繼'); // 無後繼

list.listDelete(0); // 3
list.traversing(); // 7|6|1|8

順序表的執行機制原始碼地址如下:


順序表優缺點總結

插入、刪除元素如果是在最後一個位置時間複雜度為 O(1),如果是在第一個(或其它非最後一個)位置,此時時間複雜度為 O(1),就要移動所有的元素向後或向前,時間複雜度為 O(n),當順序表的長度越大,插入和刪除操作可能就需要大量的移動操作。

對於存取操作,可以快速存取順序表中任意位置元素,時間複雜度為 O(1)。

連結串列

連結串列(Linked list)是一種常見的基礎資料結構,是一種線性表,但是並不會按線性的順序儲存資料,而是在每一個節點裡存到下一個節點的指標(Pointer)。由於不必須按順序儲存,連結串列在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是連結串列查詢一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)

使用連結串列結構可以克服陣列連結串列需要預先知道資料大小的缺點,連結串列結構可以充分利用計算機記憶體空間,實現靈活的記憶體動態管理。但是連結串列失去了陣列隨機讀取的優點,同時連結串列由於增加了節點的指標域,空間開銷比較大。

單向連結串列

連結串列中最簡單的一種是單向連結串列,它包含兩個域,一個資訊域和一個指標域。這個連結指向列表中的下一個節點,而最後一個節點則指向一個空值,圖例如下:

圖片描述

除了單向連結串列之外還有雙向連結串列、迴圈連結串列,在學習這些之前先從單向連結串列開始,因此,這裡會完整講解單向連結串列的實現,其它的幾種後續都會在這個基礎之上進行改造。

單向連結串列實現步驟

  1. Constructor(): 建構函式,初始化
  2. isEmpty(): 檢查連結串列是否為空,是否有元素
  3. length(): 獲取連結串列長度
  4. getElement(i): 返回連結串列中第 i 個資料元素的值
  5. locateElement(e): 返回連結串列中第 1 個與 e 滿足關係的元素,不存在,則返回值為 -1
  6. priorElement(e): 在連結串列中返回指定元素的前驅
  7. nextElement(e): 在連結串列中返回指定元素的後繼
  8. insertTail(e): 連結串列尾部插入元素
  9. insert(i, e): 在連結串列中第 i 個位置之前插入新的資料元素 e
  10. delete(i): 刪除連結串列的第 i 個資料元素,並返回其值
  11. traversing(): 遍歷輸出連結串列元素

初始化連結串列

在建構函式的 constructor 裡進行宣告,無需傳入引數,分別對以下幾個屬性和方法做了宣告:

  • node: 定義 node 方法,它包含一個 element 屬性,即新增到列表的值,及另一個 next 屬性,指向列表中下一個節點項的指標
  • length: 連結串列元素長度
  • head: 在 head 變數中儲存第一個節點的引用

當我們例項化一個 SingleList 物件時 head 指向為 null 及 length 預設等於 0,程式碼示例如下:

class SingleList {
    constructor() {
        this.node = function(element) {
            return {
                element,
                next: null, 
            }
        };

        this.length = 0;
        this.head = null;
    }
}

連結串列是否為空檢查

定義 isEmpty() 方法返回連結串列是否為空,根據連結串列的 length 進行判斷。

isEmpty() {
    return this.length === 0 ? true : false;
}

返回連結串列長度

同樣使用連結串列的 length 即可

length() {
    return this.length;
}

連結串列尾部插入元素

連結串列 SingleList 尾部增加元素,需要考慮兩種情況:一種是連結串列(head)為空,直接賦值新增第一個元素,另一種情況就是連結串列不為空,找到連結串列最後一個節點在其尾部增加新的節點(node)即可。

第一種情況,假設我們插入一個元素 1,此時由於連結串列為空,就會走到(行 {2})程式碼處,示意圖如下:

圖片描述

第二種情況,假設我們再插入一個元素 2,此時連結串列頭部 head 指向不為空,走到(行 {3})程式碼處,透過 while 迴圈直到找到最後一個節點,也就是當 current.next = null 時說明已經達到連結串列尾部了,接下來我們要做的就是將 current.next 指向想要新增到連結串列的節點,示意圖如下:

圖片描述

演算法實現如下:

insertTail(e) {
    let node = this.node(e); // {1}
    let current;

    if (this.head === null) { // 列表中還沒有元素 {2}
        this.head = node;
    } else { // {3}
        current = this.head;

        while (current.next) { // 下個節點存在
            current = current.next;
        }

        current.next = node;
    }

    this.length++;
}

連結串列指定位置插入元素

實現連結串列的 insert 方法,在任意位置插入資料,同樣分為兩種情況,以下一一進行介紹。

如果是連結串列的第一個位置,很簡單看程式碼塊(行 {1})處,將 node.next 設定為 current(連結串列中的第一個元素),此時的 node 就是我們想要的值,接下來將 node 的引用改為 head(node、head 這兩個變數此時在堆記憶體中的地址是相同的),示意圖如下所示:

圖片描述

如果要插入的元素不是連結串列第一個位置,透過 for 迴圈,從連結串列的第一個位置開始迴圈,定位到要插入的目標位置,for 迴圈中的變數 previous(行 {3})是對想要插入新元素位置之前的一個物件引用,current(行 {4})是對想要插入新元素位置之後的一個物件引用,清楚這個關係之後開始連結,我們本次要插入的節點 node.next 與 current(行 {5})進行連結,之後 previous.next 指向 node(行 {6})。

圖片描述

演算法實現如下:

/**
 * 在任意位置插入元素
 * @param { Number } i 插入的元素位置
 * @param { * } e 插入的元素
 */
insert(i, e) {
    if (i < 0 || i > this.length) {
        return false;
    }
    
    let node = this.node(e);
    let current = this.head;
    let previous;

    if (i === 0) { // {1}
        node.next = current;
        this.head = node;
    } else { // {2}
        for (let k=0; k<i; k++) {
            previous = current; // {3}
            current = current.next; // 儲存當前節點的下一個節點 {4}
        }

        node.next = current; // {5}
        previous.next = node; // 注意,這塊涉及到物件的引用關係 {6}
    }

    this.length++;
    return true;
}

移除指定位置的元素

定義 delete(i) 方法實現移除任意位置的元素,同樣也有兩種情況,第一種就是移除第一個元素(行 {1})處,第二種就是移除第一個元素以外的任一元素,透過 for 迴圈,從連結串列的第一個位置開始迴圈,定位到要刪除的目標位置,for 迴圈中的變數 previous(行 {2})是對想要刪除元素位置之前的一個物件引用,current(行 {3})是對想要刪除元素位置之後的一個物件引用,要從列表中移除元素,需要做的就是將 previous.next 與 current.next 進行連結,那麼當前元素會被丟棄於計算機記憶體中,等待垃圾回收器回收處理。

關於記憶體管理和垃圾回收機制的知識可參考文章

透過一張圖,來看下刪除一個元素的過程:

圖片描述

演算法實現如下:

delete(i) {
    // 要刪除的元素位置不能超過連結串列的最後一位
    if (i < 0 || i >= this.length) {
        return false;
    }

    let current = this.head;
    let previous;

    if (i === 0) { // {1}
        this.head = current.next;
    } else {
        for (let k=0; k<i; k++) {
            previous = current; // {2}
            current = current.next; // {3}
        }

        previous.next = current.next;
    }

    this.length--;
    return current.element;
}

獲取指定位置元素

定義 getElement(i) 方法獲取指定位置元素,類似於 delete 方法可做參考,在鎖定位置目標後,返回當前的元素即可 previous.element。

getElement(i) {
    if (i < 0 || i >= this.length) {
        return false;
    }

    let current = this.head;
    let previous;

    for (let k=0; k<=i; k++) {
        previous = current
        current = current.next;
    }

    return previous.element;
}

查詢元素的第一個位置索引

返回連結串列中第 1 個與 e 滿足關係的元素,存在則返回其索引值;不存在,則返回值為 -1

locateElement(e) {
    let current = this.head;
    let index = 0;

    while (current.next) { // 下個節點存在
        if (index === 0) {
            if (current.element === e) {
                return index;
            }
        }

        current = current.next;
        index++;

        if (current.element === e) {
            return index;
        }
    }

    return -1;
}

在連結串列中返回指定元素的前驅

如果是第一個元素,是沒有前驅的直接返回 false,否則的話,需要遍歷連結串列,定位到目標元素返回其前驅即當前元素的上一個元素,如果在連結串列中沒有找到,則返回 false。

priorElement(e) {
    let current = this.head;
    let previous;

    if (current.element === e) { // 第 0 個節點
        return false; // 沒有前驅
    } else {
        while (current.next) { // 下個節點存在
            previous = current;
            current = current.next;

            if (current.element === e) {
                return previous.element;
            }
        }
    }

    return false;
}

在連結串列中返回指定元素的後繼

nextElement(e) {
    let current = this.head;

    while (current.next) { // 下個節點存在
        if (current.element === e) {
            return current.next.element;
        }

        current = current.next;
    }

    return false;
}

連結串列元素遍歷

定義 traversing() 方法對連結串列的元素進行遍歷輸出,主要是將 elment 轉為字串拼接輸出。

traversing(){
    //console.log(JSON.stringify(this.head));
    let current = this.head,
    string = '';

    while (current) {
        string += current.element + ' ';
        current = current.next;
    }

    console.log(string);

    return string;
}

單向連結串列與順序表優缺點比較

  • 查詢:單向連結串列時間複雜度為 O(n);順序表時間複雜度為 O(1)
  • 插入與刪除:單向連結串列時間複雜度為 O(1);順序表需要移動元素時間複雜度為 O(n)
  • 空間效能:單向連結串列無需預先分配儲存空間;順序表需要預先分配記憶體空間,大了浪費,小了易溢位

單向連結串列原始碼地址如下:


雙向連結串列

雙向連結串列也叫雙連結串列。與單向連結串列的區別是雙向連結串列中不僅有指向後一個節點的指標,還有指向前一個節點的指標。這樣可以從任何一個節點訪問前一個節點,當然也可以訪問後一個節點,以至整個連結串列。

圖片描述

雙向連結串列是基於單向連結串列的擴充套件,很多操作與單向連結串列還是相同的,在建構函式中我們要增加 prev 指向前一個元素的指標和 tail 用來儲存最後一個元素的引用,可以從尾到頭反向查詢,重點修改插入、刪除方法。

修改初始化連結串列

constructor() {
    this.node = function(element) {
        return {
            element,
            next: null, 
            prev: null, // 新增
        }
    };

    this.length = 0;
    this.head = null;
    this.tail = null; // 新增
}

修改連結串列指定位置插入元素

在雙向連結串列中我們需要控制 prev 和 next 兩個指標,比單向連結串列要複雜些,這裡可能會出現三種情況:

情況一:連結串列頭部新增

如果是在連結串列的第一個位置插入元素,當 head 頭部指標為 null 時,將 head 和 tail 都指向 node 節點即可,如果 head 頭部節點不為空,將 node.next 的下一個元素為 current,那麼同樣 current 的上個元素就為 node(current.prev = node),node 就為第一個元素且 prev(node.prev = null)為空,最後我們將 head 指向 node。

假設我們當前連結串列僅有一個元素 b,我們要在第一個位置插入元素 a,圖例如下:

圖片描述

情況二:連結串列尾部新增

這又是一種特殊的情況連結串列尾部新增,這時候我們要改變 current 的指向為 tail(引用最後一個元素),開始連結把 current 的 next 指向我們要新增的節點 node,同樣 node 的上個節點 prev 就為 current,最後我們將 tail 指向 node。

繼續上面的例子,我們在連結串列尾部在增加一個元素 d

圖片描述

情況三:非連結串列頭部、尾部的任意位置新增

這個和單向連結串列插入那塊是一樣的思路,不清楚的,在回頭去看下,只不過增加了節點的向前一個元素的引用,current.prev 指向 node,node.prev 指向 previous。

繼續上面的例子,在元素 d 的位置插入元素 c,那麼 d 就會變成 c 的下一個元素,圖例如下:

圖片描述

演算法實現如下:

insert(i, e) {
    if (i < 0 || i > this.length) {
        return false;
    }
    
    let node = this.node(e);
    let current = this.head;
    let previous;

    if (i === 0) { // 有修改
        if (current) {
            node.next = current;
            current.prev = node;
            this.head = node;
        } else {
            this.head = this.tail = node;
        }
    } else if (i === this.length) { // 新增加
        current = this.tail;
        current.next = node;
        node.prev = current;
        this.tail = node;
    } else {
        for (let k=0; k<i; k++) {
            previous = current;
            current = current.next; // 儲存當前節點的下一個節點
        }

        node.next = current;
        previous.next = node; // 注意,這塊涉及到物件的引用關係

        current.prev = node; // 新增加
        node.prev = previous; // 新增加
    }

    this.length++;
    return true;
}

移除連結串列元素

雙向連結串列中移除元素同插入一樣,需要考慮三種情況,下面分別看下各自實現:

情況一:連結串列頭部移除

current 是連結串列中第一個元素的引用,對於移除第一個元素,我們讓 head = current 的下一個元素,即 current.next,這在單向連結串列中就已經完成了,但是雙向連結串列我們還要修改節點的上一個指標域,再次判斷當前連結串列長度是否等於 1,如果僅有一個元素,刪除之後連結串列就為空了,那麼 tail 也要置為 null,如果不是一個元素,將 head 的 prev 設定為 null,圖例如下所示:

圖片描述

情況二:連結串列尾部移除

改變 current 的指向為 tail(引用最後一個元素),在這是 tail 的引用為 current 的上個元素,即最後一個元素的前一個元素,最後再將 tail 的下一個元素 next 設定為 null,圖例如下所示:

圖片描述

情況三:連結串列尾部移除

這個和單向連結串列刪除那塊是一樣的思路,不清楚的,在回頭去看下,只增加了 current.next.prev = previous 當前節點的下一個節點的 prev 指標域等於當前節點的上一個節點 previous,圖例如下所示:

圖片描述

演算法實現如下:

delete(i) {
    // 要刪除的元素位置不能超過連結串列的最後一位
    if (i < 0 || i >= this.length) {
        return false;
    }

    let current = this.head;
    let previous;

    if (i === 0) {
        this.head = current.next;

        if (this.length === 1) {
            this.tail = null; 
        } else {
            this.head.prev = null;
        }
    } else if (i === this.length -1) {
        current = this.tail;
        this.tail = current.prev;
        this.tail.next = null;
    } else {
        for (let k=0; k<i; k++) {
            previous = current;
            current = current.next;
        }

        previous.next = current.next;
        current.next.prev = previous; // 新增加
    }

    this.length--;
    return current.element;
}

雙向連結串列原始碼地址如下:


迴圈連結串列

在單向連結串列和雙向連結串列中,如果一個節點沒有前驅或後繼該節點的指標域就指向為 null,迴圈連結串列中最後一個節點 tail.next 不會指向 null 而是指向第一個節點 head,同樣雙向引用中 head.prev 也會指向 tail 元素,如下圖所示:

圖片描述

可以看出迴圈連結串列可以將整個連結串列形成一個環,既可以向單向連結串列那樣只有單向引用,也可以向雙向連結串列那樣擁有雙向引用。

以下基於單向連結串列一節的程式碼進行改造

尾部插入元素

對於環形連結串列的節點插入與單向連結串列的方式不同,如果當前節點為空,當前節點的 next 值不指向為 null,指向 head。如果頭部節點不為空,遍歷到尾部節點,注意這裡不能在用 current.next 為空進行判斷了,否則會進入死迴圈,我們需要判斷當前節點的下個節點是否等於頭部節點,演算法實現如下所示:

insertTail(e) {
    let node = this.node(e);
    let current;

    if (this.head === null) { // 列表中還沒有元素
        this.head = node;
        node.next = this.head; // 新增
    } else {
        current = this.head;

        while (current.next !== this.head) { // 下個節點存在
            current = current.next;
        }

        current.next = node;
        node.next = this.head; // 新增,尾節點指向頭節點
    }

    this.length++;
}

連結串列任意位置插入元素

實現同連結串列尾部插入相似,注意:將新節點插入在原連結串列頭部之前,首先,要將新節點的指標指向原連結串列頭節點,並遍歷整個連結串列找到連結串列尾部,將連結串列尾部指標指向新增節點,圖例如下:

圖片描述

演算法實現如下所示:

insert(i, e) {
    if (i < 0 || i > this.length) {
        return false;
    }
    
    let node = this.node(e);
    let current = this.head;
    let previous;

    if (i === 0) {
        if (this.head === null) { // 新增
            this.head = node;
            node.next = this.head;
        } else {
            node.next = current;
            const lastElement = this.getNodeAt(this.length - 1);
            this.head = node;
            // 新增,更新最後一個元素的頭部引用
            lastElement.next = this.head
        }
    } else {
        for (let k=0; k<i; k++) {
            previous = current;
            current = current.next; // 儲存當前節點的下一個節
        }

        node.next = current;
        previous.next = node; // 注意,這塊涉及到物件的引用關係
    }

    this.length++;
    return true;
}

移除指定位置元素

與之前不同的是,如果刪除第一個節點,先判斷連結串列在僅有一個節點的情況下直接將 head 置為 null,否則不僅僅只有一個節點的情況下,首先將連結串列頭指標移動到下一個節點,同時將最後一個節點的指標指向新的連結串列頭部

圖片描述

演算法實現如下所示:

delete(i) {
    // 要刪除的元素位置不能超過連結串列的最後一位
    if (i < 0 || i >= this.length) {
        return false;
    }

    let current = this.head;
    let previous;

    if (i === 0) {
        if (this.length === 1) {
            this.head = null;
        } else {
            const lastElement = this.getNodeAt(this.length - 1);
            this.head = current.next;
            lastElement.next = this.head;
            current = lastElement;
        }
    } else {
        for (let k=0; k<i; k++) {
            previous = current;
            current = current.next;
        }

        previous.next = current.next;
    }

    this.length--;
    return current.element;
}

最後在遍歷的時候也要注意,不能在根據 current.next 是否為空來判斷連結串列是否結束,可以根據連結串列元素長度或者 current.next 是否等於頭節點來判斷,本節原始碼實現連結如下所示:

https://github.com/Q-Angelo/project-training/tree/master/algorithm/circular-linked-list.js

總結

本節主要講解的是線性表,從順序表->單向連結串列->雙向連結串列->迴圈連結串列,這個過程也是循序漸進的,前兩個講的很詳細,雙向連結串列與迴圈連結串列透過與前兩個不同的地方進行比較針對性的進行了講解,另外學習線性表也是學習其它資料結構的基礎,資料結構特別是涉及到一些實現演算法的時候,有時候並不是看一遍就能理解的,總之多實踐多思考

Reference

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3486/viewspace-2823709/,如需轉載,請註明出處,否則將追究法律責任。

相關文章