【譯】JavaScript數據結構(3):單向鏈表與雙向鏈表

翻譯:瘋狂的技術宅
英文https://code.tutsplus.com/art...
說明:本文翻譯自系列文章《Data Structures With JavaScript》,總共爲四篇,原做者是在美國硅谷工做的工程師 Cho S. Kim 。這是本系列的第三篇。javascript

說明:本專欄文章首發於公衆號:jingchengyideng 。java

計算機科學中最多見的兩種數據結構是單鏈表和雙鏈表。node

在我學習這些數據結構的時候,曾經問個人同伴在生活中有沒有相似的概念。我所聽到的例子是購物清單和火車。可是我最終明白了,這些類比是不許確的,購物清單更相似隊列,火車則更像是一個數組。數組

隨着時間的推移,我終於發現了一個可以準確類比單鏈表和雙向鏈表的例子:尋寶遊戲。 若是你對尋寶遊戲和鏈表之間的關係感到好奇,請繼續往下讀。數據結構

單鏈表

在計算機科學中,單鏈表是一種數據結構,保存了一系列連接的節點。 每一個節點中包含數據和一個可指向另外一個節點的指針。ide

單鏈列表的節點很是相似於尋寶遊戲中的步驟。 每一個步驟都包含一條消息(例如「您已到達法國」)和指向下一步驟的指針(例如「訪問這些經緯度座標」)。 當咱們開始對這些單獨的步驟進行排序並造成一系列步驟時,就是在玩一個尋寶遊戲。函數

如今咱們對單鏈表有了一個基本的概念,接下來討論單鏈表的操做工具

單鏈表的操做

由於單鏈表包含節點,這二者的構造函數能夠是兩個獨立的構造函數,因此咱們須要些構造函數:NodeSinglyList學習

Node

  • data 存儲數據this

  • next 指向鏈表中下一個節點的指針

SinglyList

  • _length 用於表示鏈表中的節點數量

  • head 分配一個節點做爲鏈表的頭

  • add(value) 向鏈表中添加一個節點

  • searchNodeAt(position) 找到在列表中指定位置 n 上的節點

  • remove(position) 刪除指定位置的節點

單鏈表的實現

在實現時,咱們首先定義一個名爲Node的構造函數,而後定義一個名爲SinglyList的構造函數。

Node 的每一個實例都應該可以存儲數據而且可以指向另一個節點。 要實現此功能,咱們將分別建立兩個屬性:datanext

function Node(data) {
    this.data = data;
    this.next = null;
}

下一步咱們定義SinglyList:

function SinglyList() {
    this._length = 0;
    this.head = null;
}

SinglyList 的每一個實例有兩個屬性:_lengthhead。前者保存鏈表中的節點數,後者指向鏈表的頭部,鏈表前面的節點。因爲新建立的singlylist實例不包含任何節點,因此head的默認值是null_length的默認值是 0

單鏈表的方法

咱們須要定義能夠從鏈表中添加、查找和刪除節點的方法。先從添加節點開始。

方法1/3: add(value)

太棒了,如今咱們來實現將節點添加到鏈表的功能。

SinglyList.prototype.add = function(value) {
    var node = new Node(value),
        currentNode = this.head;
 
    // 1st use-case: an empty list 
    if (!currentNode) {
        this.head = node;
        this._length++;
         
        return node;
    }
 
    // 2nd use-case: a non-empty list
    while (currentNode.next) {
        currentNode = currentNode.next;
    }
 
    currentNode.next = node;
 
    this._length++;
     
    return node;
};

把節點添加到鏈表會涉及不少步驟。先從方法開始。 咱們使用add(value)的參數來建立一個節點的新實例,該節點被分配給名爲node的變量。咱們還聲明瞭一個名爲currentNode的變量,並將其初始化爲鏈表的_head。 若是鏈表中尚未節點,那麼head的值爲null

實現了這一點以後,咱們將處理兩種狀況。

第一種狀況考慮將節點添加到空的鏈表中,若是head沒有指向任何節點的話,那麼將該node指定爲鏈表的頭,同時鏈表的長度加一,並返回node

第二種狀況考慮將節點添加到飛空鏈表。咱們進入while循環,在每次循環中,判斷currentNode.next是否指向下一個節點。(第一次循環時,CurrentNode指向鏈表的頭部。)

若是答案是否認的,咱們會把currentnode.next指向新添加的節點,並返回node

若是答案是確定的,就進入while循環。 在循環體中,咱們將currentNode從新賦值給currentNode.next。 重複這個過程,直到currentNode.next再也不指向任何。換句話說,currentNode指向鏈表中的最後一個節點。

while循環結束後,使currentnode.next指向新添加的節點,同時_length加1,最後返回node

方法2/3: searchNodeAt(position)

如今咱們能夠將節點添加到鏈表中了,可是尚未辦法找到特定位置的節點。下面添加這個功能。建立一個名爲searchNodeAt(position) 的方法,它接受一個名爲 position 的參數。這個參數是個整數,用來表示鏈表中的位置n。

SinglyList.prototype.searchNodeAt = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 1,
        message = {failure: 'Failure: non-existent node in this list.'};
 
    // 1st use-case: an invalid position 
    if (length === 0 || position < 1 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: a valid position 
    while (count < position) {
        currentNode = currentNode.next;
        count++;
    }
 
    return currentNode;
};

if中檢查第一種狀況:參數非法。

若是傳給searchNodeAt(position)的索引是有效的,那麼咱們執行第二種狀況 —— while循環。 在while的每次循環中,指向頭的currentNode被從新指向鏈表中的下一個節點。

這個循環不斷執行,一直到count等於position

方法3/3: remove(position)

最後一個方法是remove(position)

SinglyList.prototype.remove = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 0,
        message = {failure: 'Failure: non-existent node in this list.'},
        beforeNodeToDelete = null,
        nodeToDelete = null,
        deletedNode = null;
 
    // 1st use-case: an invalid position
    if (position < 0 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: the first node is removed
    if (position === 1) {
        this.head = currentNode.next;
        deletedNode = currentNode;
        currentNode = null;
        this._length--;
         
        return deletedNode;
    }
 
    // 3rd use-case: any other node is removed
    while (count < position) {
        beforeNodeToDelete = currentNode;
        nodeToDelete = currentNode.next;
        count++;
    }
 
    beforeNodeToDelete.next = nodeToDelete.next;
    deletedNode = nodeToDelete;
    nodeToDelete = null;
    this._length--;
 
    return deletedNode;
};

咱們要實現的remove(position)涉及三種狀況:

  1. 無效的位置做爲參數傳遞。

  2. 第一個位置(鏈表的的`head)做爲參數的傳遞。

  3. 一個合法的位置(不是第一個位置)做爲參數的傳遞。

前兩種狀況是最簡單的處理。 關於第一種狀況,若是鏈表爲空或傳入的位置不存在,則會拋出錯誤。

第二種狀況處理鏈表中第一個節點的刪除,這也是頭節點。 若是是這種狀況,就執行下面的邏輯:

  1. 頭被從新賦值給currentNode.next

  2. deletedNode指向currentNode

  3. currentNode被從新賦值爲null。

  4. 將的鏈表的長度減1。

  5. 返回deletedNode

第三種狀況是最難理解的。 其複雜性在於咱們要在每一次循環中操做兩個節點的必要性。 在每次循環中,須要處理要刪除的節點和它前面的節點。當循環到要被刪除的位置的節點時,循環終止。

在這一點上,咱們涉及到三個節點:
beforeNodeToDelete, nodeToDelete, 和 deletedNode。刪除nodeToDelete以前,必須先把它的next的值賦給beforeNodeToDeletenext,若是不清楚這一步驟的目的,能夠提醒本身有一個節點負責連接其先後的其餘節點,只須要刪除這個節點,就能夠把鏈表斷開。

接下來,咱們將deletedNode賦值給nodeToDelete。 而後咱們將nodeToDelete的值設置爲null,將列表的長度減1,最後返回deletedNode

單向鏈表的完整實現

如下是單向鏈表的完整實現:

function Node(data) {
    this.data = data;
    this.next = null;
}
 
function SinglyList() {
    this._length = 0;
    this.head = null;
}
 
SinglyList.prototype.add = function(value) {
    var node = new Node(value),
        currentNode = this.head;
 
    // 1st use-case: an empty list
    if (!currentNode) {
        this.head = node;
        this._length++;
 
        return node;
    }
 
    // 2nd use-case: a non-empty list
    while (currentNode.next) {
        currentNode = currentNode.next;
    }
 
    currentNode.next = node;
 
    this._length++;
     
    return node;
};
 
SinglyList.prototype.searchNodeAt = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 1,
        message = {failure: 'Failure: non-existent node in this list.'};
 
    // 1st use-case: an invalid position
    if (length === 0 || position < 1 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: a valid position
    while (count < position) {
        currentNode = currentNode.next;
        count++;
    }
 
    return currentNode;
};
 
SinglyList.prototype.remove = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 0,
        message = {failure: 'Failure: non-existent node in this list.'},
        beforeNodeToDelete = null,
        nodeToDelete = null,
        deletedNode = null;
 
    // 1st use-case: an invalid position
    if (position < 0 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: the first node is removed
    if (position === 1) {
        this.head = currentNode.next;
        deletedNode = currentNode;
        currentNode = null;
        this._length--;
         
        return deletedNode;
    }
 
    // 3rd use-case: any other node is removed
    while (count < position) {
        beforeNodeToDelete = currentNode;
        nodeToDelete = currentNode.next;
        count++;
    }
 
    beforeNodeToDelete.next = nodeToDelete.next;
    deletedNode = nodeToDelete;
    nodeToDelete = null;
    this._length--;
 
    return deletedNode;
};

從單鏈表到雙鏈表

咱們已經完整的實現了單鏈表,這真是極好的。如今能夠在一個佔用費連續的空間的鏈表結構中,進行添加、刪除和查找節點的操做了。

然而如今全部的操做都是從鏈表的起始位置開始,並運行到鏈表的結尾。換句話說,它們是單向的。

可能在某些狀況下咱們但願操做是雙向的。若是你考慮了這種可能性,那麼你剛纔就是描述了一個雙向鏈表。

雙向鏈表

雙向鏈表具備單鏈表的全部功能,並將其擴展爲在鏈表中能夠進行雙向遍歷。 換句話說,咱們可從鏈表中第一個節點遍歷到到最後一個節點;也能夠從最後一個節點遍歷到第一個節點。

在本節中,咱們將重點關注雙向鏈表和單鏈列表之間的差別。

雙向鏈表的操做

咱們的鏈表將包括兩個構造函數:NodeDoublyList。看看他們是怎樣運做的。

Node

  • data 存儲數據。

  • next 指向鏈表中下一個節點的指針。

  • previous 指向鏈表中前一個節點的指針。

DoublyList

  • _length 保存鏈表中節點的個數

  • head 指定一個節點做爲鏈表的頭節點

  • tail 指定一個節點做爲鏈表的尾節點

  • add(value) 向鏈表中添加一個節點

  • searchNodeAt(position) 找到在列表中指定位置 n 上的節點

  • remove(position) 刪除鏈表中指定位置上的節點

雙向鏈表的實現

如今開始寫代碼!

在實現中,將會建立一個名爲Node的構造函數:

function Node(value) {
    this.data = value;
    this.previous = null;
    this.next = null;
}

想要實現雙向鏈表的雙向遍歷,咱們須要指向鏈表兩個方向的屬性。這些屬性被命名爲previousnext

接下來,咱們須要實現DoublyList並添加三個屬性:_lengthheadtail

與單鏈表不一樣,雙向鏈表包含對鏈表開頭和結尾節點的引用。 因爲DoublyList剛被實例化時並不包含任何節點,因此headtail的默認值都被設置爲null

function DoublyList() {
    this._length = 0;
    this.head = null;
    this.tail = null;
}

雙向鏈表的方法

接下來咱們討論如下方法:add(value), remove(position), 和 searchNodeAt(position)。全部這些方法都用於單鏈表; 然而,它們必須備重寫爲能夠雙向遍歷。

方法1/3 add(value)

DoublyList.prototype.add = function(value) {
    var node = new Node(value);
 
    if (this._length) {
        this.tail.next = node;
        node.previous = this.tail;
        this.tail = node;
    } else {
        this.head = node;
        this.tail = node;
    }
 
    this._length++;
     
    return node;
};

在這個方法中,存在兩種可能。首先,若是鏈表是空的,則給它的head tail分配節點。其次,若是鏈表中已經存在節點,則查找鏈表的尾部並把心節點分配給tail.next;一樣,咱們須要配置新的尾部以供進行雙向遍歷。換句話說,咱們須要把tail.previous設置爲原來的尾部。

方法2/3 searchNodeAt(position)

searchNodeAt(position)的實現與單鏈表相同。 若是你忘記了如何實現它,請經過下面的代碼回憶:

DoublyList.prototype.searchNodeAt = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 1,
        message = {failure: 'Failure: non-existent node in this list.'};
 
    // 1st use-case: an invalid position 
    if (length === 0 || position < 1 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: a valid position 
    while (count < position) {
        currentNode = currentNode.next;
        count++;
    }
 
    return currentNode;
};

方法3/3 remove(position)

理解這個方法是最具挑戰性的。我先寫出代碼,而後再解釋它。

DoublyList.prototype.remove = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 1,
        message = {failure: 'Failure: non-existent node in this list.'},
        beforeNodeToDelete = null,
        nodeToDelete = null,
        deletedNode = null;
 
    // 1st use-case: an invalid position
    if (length === 0 || position < 1 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: the first node is removed
    if (position === 1) {
        this.head = currentNode.next;
 
        // 2nd use-case: there is a second node
        if (!this.head) {
            this.head.previous = null;
        // 2nd use-case: there is no second node
        } else {
            this.tail = null;
        }
 
    // 3rd use-case: the last node is removed
    } else if (position === this._length) {
        this.tail = this.tail.previous;
        this.tail.next = null;
    // 4th use-case: a middle node is removed
    } else {
        while (count < position) {
            currentNode = currentNode.next;
            count++;
        }
 
        beforeNodeToDelete = currentNode.previous;
        nodeToDelete = currentNode;
        afterNodeToDelete = currentNode.next;
 
        beforeNodeToDelete.next = afterNodeToDelete;
        afterNodeToDelete.previous = beforeNodeToDelete;
        deletedNode = nodeToDelete;
        nodeToDelete = null;
    }
 
    this._length--;
 
    return message.success;
};

remove(position) 處理如下四種狀況:

  1. 若是remove(position)的參數傳遞的位置存在, 將會拋出一個錯誤。

  2. 若是remove(position)的參數傳遞的位置是鏈表的第一個節點(head),將把head賦值給deletedNode ,而後把head從新分配到鏈表中的下一個節點。 此時,咱們必須考慮鏈表中否存在多個節點。 若是答案爲否,頭部將被分配爲null,以後進入if-else語句的if部分。 在if的代碼中,還必須將tail設置爲null —— 換句話說,咱們返回到一個空的雙向鏈表的初始狀態。若是刪除列表中的第一個節點,而且鏈表中存在多個節點,那麼咱們輸入if-else語句的else部分。 在這種狀況下,咱們必須正確地將headprevious屬性設置爲null —— 在鏈表的頭前面是沒有節點的。

  3. 若是remove(position)的參數傳遞的位置是鏈表的尾部,首先把tail賦值給deletedNode,而後tail被從新賦值爲尾部以前的那個節點,最後新尾部後面沒有其餘節點,須要將其next值設置爲null

  4. 這裏發生了不少事情,因此我將重點關注邏輯,而不是每一行代碼。 一旦CurrentNode指向的節點是將要被remove(position)刪除的節點時,就退出while循環。這時咱們把nodeToDelete以後的節點從新賦值給beforeNodeToDelete.next。相應的,
    nodeToDelete以前的節點從新賦值給afterNodeToDelete.previous。——換句話說,咱們把指向已刪除節點的指針,改成指向正確的節點。最後,把nodeToDelete 賦值爲null

最後,把鏈表的長度減1,返回deletedNode

雙向鏈表的完整實現

function Node(value) {
    this.data = value;
    this.previous = null;
    this.next = null;
}
 
function DoublyList() {
    this._length = 0;
    this.head = null;
    this.tail = null;
}
 
DoublyList.prototype.add = function(value) {
    var node = new Node(value);
 
    if (this._length) {
        this.tail.next = node;
        node.previous = this.tail;
        this.tail = node;
    } else {
        this.head = node;
        this.tail = node;
    }
 
    this._length++;
 
    return node;
};
 
DoublyList.prototype.searchNodeAt = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 1,
        message = {failure: 'Failure: non-existent node in this list.'};
 
    // 1st use-case: an invalid position
    if (length === 0 || position < 1 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: a valid position
    while (count < position) {
        currentNode = currentNode.next;
        count++;
    }
 
    return currentNode;
};
 
DoublyList.prototype.remove = function(position) {
    var currentNode = this.head,
        length = this._length,
        count = 1,
        message = {failure: 'Failure: non-existent node in this list.'},
        beforeNodeToDelete = null,
        nodeToDelete = null,
        deletedNode = null;
 
    // 1st use-case: an invalid position
    if (length === 0 || position < 1 || position > length) {
        throw new Error(message.failure);
    }
 
    // 2nd use-case: the first node is removed
    if (position === 1) {
        this.head = currentNode.next;
 
        // 2nd use-case: there is a second node
        if (!this.head) {
            this.head.previous = null;
        // 2nd use-case: there is no second node
        } else {
            this.tail = null;
        }
 
    // 3rd use-case: the last node is removed
    } else if (position === this._length) {
        this.tail = this.tail.previous;
        this.tail.next = null;
    // 4th use-case: a middle node is removed
    } else {
        while (count < position) {
            currentNode = currentNode.next;
            count++;
        }
 
        beforeNodeToDelete = currentNode.previous;
        nodeToDelete = currentNode;
        afterNodeToDelete = currentNode.next;
 
        beforeNodeToDelete.next = afterNodeToDelete;
        afterNodeToDelete.previous = beforeNodeToDelete;
        deletedNode = nodeToDelete;
        nodeToDelete = null;
    }
 
    this._length--;
 
    return message.success;
};

總結

本文中已經介紹了不少信息。 若是其中任何地方看起來使人困惑,就再讀一遍並查看代碼。若是它最終對你有所幫助,我會感到自豪。你剛剛揭開了一個單鏈表和雙向鏈表的祕密,能夠把這些數據結構添加到本身的編碼工具彈藥庫中!

歡迎掃描二維碼關注公衆號,天天推送我翻譯的技術文章。

圖片描述