前端學習 資料結構與演算法 快速入門 系列 —— 連結串列

彭加李 發表於 2021-09-22
前端 演算法 資料結構

連結串列

連結串列資料結構

前面我們已經學習了陣列資料結構,但是從陣列頭部或中間插入元素,或者移除元素的成本比較高,因為需要移動元素。

就像這樣:

// 從頭部插入元素
Array.prototype.insertFirst = function (v) {
    for (let i = this.length; i >= 1; i--) {
        this[i] = this[i - 1]
    }
    this[0] = v
}

連結串列不同於陣列,連結串列中的元素在記憶體中不是連續放置的,每個元素由一個儲存元素本身的節點和一個指向下一個元素的引用(也稱為指標)組成。就像這樣:

              Node             Node                      Node
head -> [value | next] -> [value | next] -> ... -> [value | next(undefined)]

要想訪問連結串列中的元素,需要從起點(表頭)開始迭代,直到找到所需要的元素。

就像尋寶遊戲,給你一個起始線索,得到第二個線索,在得到下一個線索...。要得到中間線索的唯一方法就是從起點(第一條線索)順著尋找。

相對於陣列,連結串列的一個好處是,新增或移除元素的時候不需要移動其他元素,無論是從頭部、尾部還是中間來新增或移除。

比較典型的例子就是火車,非常容易增加一節車廂或移除一個車廂,只需要改變一下車廂的掛鉤即可。

建立連結串列

理解了連結串列,我們就要開始實現我們的資料結構,以下是 LinkedList 的骨架:

// 連結串列中的節點
class Node{
    constructor(element){
        this.element = element
        this.next = undefined
    }
}

// 預設的相等的函式
function defaultEquals(a, b){
    return Object.is(a, b)
}

// 連結串列類
class LinkedList{
    constructor(equalsFn = defaultEquals){
        this.count = 0
        this.head = undefined
        this.equalsFn = equalsFn
    }
}

連結串列中的方法:

  • push(element),向連結串列尾部新增一個新元素

  • insert(element, index),在任意位置插入新元素。插入成功返回 true,否則返回 false

  • remove(element) 移除特定元素。返回刪除的元素

  • removeAt(index) 從任意位置移除元素,並返回刪除的元素。索引從 0 開始

  • size() 返回連結串列中元素的個數

  • isEmpty() 如果連結串列不包含任何元素,返回 true,否則返回 false

  • toString() 返回表示連結串列的字串

  • indexOf(element) 返回元素在連結串列中的索引。如果沒有該元素,則返回 -1

  • getElementAt(index) 取得特定位置的元素。如果不存在這樣的元素,則返回 undefined。

    • getNodeAt(index) 取得特定位置的節點。和 getElementAt(index) 唯一不同是返回值,前者返回 node,後者返回 element。

Tip:理解了什麼是連結串列,比較容易想到的方法有

  • 插入和刪除:pushinsertremoveremoveAt
  • 其他:sizeisEmptytoString

向連結串列尾部新增一個新元素

第一種實現,需要依賴於另外兩個方法:

push(element) {
    // 封裝成節點
    const node = new Node(element)
    if(this.isEmpty()){
        this.head = node
        this.count++
        return
    }
    const lastNode = this.getNodeAt(this.count - 1)
    lastNode.next = node
    this.count++
}

第二種實現,不依賴其他方法:

push(element) {
    // 封裝成節點
    const node = new Node(element)
    if(this.head === undefined){
        this.head = node
        this.count++
        return
    }
    // 取得最後一個節點,並將其引用指向新的節點
    let lastNode = this.head
    while(lastNode.next){
        lastNode = lastNode.next
    }
    lastNode.next = node
    this.count++
}

從任意位置移除元素

removeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能刪除的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current
        // 刪除第一項
        if (Object.is(index, 0)) {
            current = this.head
            this.head = current.next
        } else {  // 刪除中間項或最後一項
            let prev = this.getNodeAt(index - 1)
            current = prev.next
            prev.next = current.next
        }
        this.count--
        return current.element
    }

如果不需要依賴 getNodeAt 方法,可以這樣:

removeAt(index) {
    ...

    let current = this.head
    // 刪除第一項
    if (Object.is(index, 0)) {
        this.head = current.next
    } else {  // 刪除中間項或最後一項
        let prev
        while(index--){
            prev = current
            current = prev.next
        }
        prev.next = current.next
    }

    ...
}

取得特定位置的節點

首先排除無效的索引(index),邏輯和 removeAt 中的相同:

getNodeAt(index) {
    // index 必須是自然數,即 0、1、2...
    const isNaturalNumber = Number.isInteger(index) && index >= 0
    const outOfBounds = index >= this.count
    // 處理不能取得節點的情況:非自然數、index 出界,都不做處理
    if (!isNaturalNumber || outOfBounds) {
        return
    }

    let current = this.head
    while (index--) {
        current = current.next
    }
    return current
}

在任意位置插入新元素

insert(element, index) {
    // 引數不合法
    const isNaturalNumber = Number.isInteger(index) && index >= 0
    const outOfBounds = index > this.count
    if (!isNaturalNumber || outOfBounds) {
        return
    }
    
    const newNode = new Node(element)
    // 連結串列為空
    if (Object.is(this.head, undefined)) {
        this.head = newNode
    } else if (Object.is(index, 0)) { // 插入第一項
        newNode.next = this.head
        this.head = newNode
    } else if (Object.is(index, this.count)) { // 插入最後一項
        const lastNode = this.getNodeAt(index - 1)
        lastNode.next = newNode
    } else { // 插入中間
        const prev = this.getNodeAt(index - 1)
        newNode.next = prev.next
        prev.next = newNode
    }
    this.count++
}

其中前兩種邏輯可以合併,後面插入最後一項以及插入中間也可以合併成一個邏輯:

insert(element, index) {
    ...
    const newNode = new Node(element)
    if (Object.is(index, 0)) { // 連結串列為空 & 插入第一項
        newNode.next = this.head
        this.head = newNode
    } else { // 插入中間以及插入最後一項
        const prev = this.getNodeAt(index - 1)
        newNode.next = prev.next
        prev.next = newNode
    }
    ...
}

元素在連結串列中的索引

indexOf(element) {
    let result = -1
    let current = this.head
    let { count } = this
    let i = 0
    while (i < count) {
        if (this.equalsFn(current.element, element)) {
            result = i
            break
        }
        current = current.next
        i++
    }
    return result
}

改成 for 迴圈更顯簡潔:

indexOf(element) {
    let current = this.head
    for(let i = 0, count = this.count; i < count; i++){
        if (this.equalsFn(current.element, element)) {
            return i
        }
        current = current.next
    }
    return -1
}

其他方法

Tip:剩下的幾個方法都比較簡單,就放在一起介紹

  • 從連結串列中移除一個元素
remove(element){
    const index = this.indexOf(element)
    return this.removeAt(index)
}
  • size()isEmpty()
size(){
    return this.count
}
isEmpty(){
    return this.count === 0
}
  • 返回表示連結串列的字串
toString(){
    if(this.isEmpty()){
        return ''
    }

    let elements = []
    let current = this.head
    while(current){
        elements.push(current.element)
        current = current.next
    }
    return elements.join(',')
}

使用 LinkedList 類

class Node {
    constructor(element) {
        this.element = element
        this.next = undefined
    }
}

function defaultEquals(a, b) {
    return Object.is(a, b)
}

/**
 * 連結串列
 * @class LinkedList
 */
class LinkedList {
    constructor(equalsFn = defaultEquals) {
        this.count = 0
        this.head = undefined
        this.equalsFn = equalsFn
    }
    // 向連結串列尾部新增一個新元素
    push(element) {
        // 封裝成節點
        const node = new Node(element)
        if (this.head === undefined) {
            this.head = node
            this.count++
            return
        }
        // 取得最後一個節點,並將其引用指向新的節點
        let lastNode = this.head
        while (lastNode.next) {
            lastNode = lastNode.next
        }
        lastNode.next = node
        this.count++
    }
    // 從連結串列中刪除特定位置的元素
    removeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能刪除的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current = this.head
        // 刪除第一項
        if (Object.is(index, 0)) {
            this.head = current.next
        } else {  // 刪除中間項或最後一項
            let prev
            while (index--) {
                prev = current
                current = prev.next
            }
            prev.next = current.next
        }
        this.count--
        return current.element
    }
    getNodeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能刪除的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current = this.head
        while (index--) {
            current = current.next
        }
        return current
    }
    getElementAt(index) {
        const result = this.getNodeAt(index)
        return result ? result.element : result
    }

    // 向連結串列特定位置插入一個新元素
    insert(element, index) {
        // 引數不合法
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index > this.count
        if (!isNaturalNumber || outOfBounds) {
            return false
        }

        const newNode = new Node(element)
        if (Object.is(index, 0)) { // 連結串列為空 & 插入第一項
            newNode.next = this.head
            this.head = newNode
        } else { // 插入中間以及插入最後一項
            const prev = this.getNodeAt(index - 1)
            newNode.next = prev.next
            prev.next = newNode
        }
        this.count++
        return true
    }

    indexOf(element) {
        let current = this.head
        for (let i = 0, count = this.count; i < count; i++) {
            if (this.equalsFn(current.element, element)) {
                return i
            }
            current = current.next
        }
        return -1
    }

    remove(element) {
        const index = this.indexOf(element)
        return this.removeAt(index)
    }

    size() {
        return this.count
    }

    isEmpty() {
        return this.count === 0
    }

    toString() {
        if (this.isEmpty()) {
            return ''
        }

        let elements = []
        let current = this.head
        while (current) {
            elements.push(current.element)
            current = current.next
        }
        return elements.join(',')
    }
}
class Dog {
    constructor(name) {
        this.name = name
    }
    toString() {
        return this.name
    }
}

let linkedlist = new LinkedList()
console.log(linkedlist.isEmpty()) // true
linkedlist.push(1)
console.log(linkedlist.isEmpty()) // false
linkedlist.push(2)
console.log(linkedlist.toString()) // 1,2

linkedlist.insert(3, 0) // 在索引 0 處插入元素 3
linkedlist.insert(4, 3)
linkedlist.insert(5, 5) // 插入失敗
console.log(linkedlist.toString()) // 3,1,2,4

let i = linkedlist.indexOf(3)
let j = linkedlist.indexOf(4)
console.log('i: ', i) // i:  0
console.log('j: ', j) // j:  3

let dog1 = new Dog('a')
linkedlist.push(dog1)
console.log(linkedlist.toString()) // 3,1,2,4,a
let k = linkedlist.indexOf(dog1)
console.log('k: ', k) // k:  4
let m = linkedlist.getElementAt(0)
console.log('m: ', m) // m:  3
linkedlist.remove(dog1)
linkedlist.removeAt(0)
console.log(linkedlist.toString()) // 1,2,4
let l = linkedlist.size()
console.log('l: ', l) // l:  3

雙向連結串列

連結串列有多種型別,雙向連結串列提供兩種迭代方法:從頭到尾,或者從尾到頭。

在雙向連結串列中,相對於連結串列,每一項都新增了一個 prev 引用。還有 tail 引用,用於從尾部迭代到頭部。就像這樣:

                      Node                   Node                      node           
head -> [prev(undefined) | value | next] <->  ... <-> [prev | value | next(undefined)] <- tail

Tip:在單向連結串列中,如果錯過了要找的元素,就需要重新回到起點,重新迭代。而雙向連結串列則沒有這個問題

建立雙向連結串列

我們先從最基礎的開始,建立 DoubleNode 類和 DoubleLinkedList 類:

// 雙向連結串列中的節點
class DoubleNode extends Node {
    constructor(element) {
        super(element)
        this.prev = undefined
    }
}

// 雙向連結串列
class DoubleLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals) {
        super(equalsFn)
        this.tail = undefined
    }
}

:因為雙向連結串列對於連結串列來說,只是增加了從尾部遍歷到頭部的特性,所以雙向連結串列的方法和連結串列的方法其實是相同的(也是 10 個方法)

在任意位置插入新元素

我們直接在 LinkedList 類中 insert 方法的基礎上修改一下即可:

  • 對於不能插入的情況,即“引數不合法”部分,無需修改
  • 節點的建立,改為 DoubleNode
  • 插入分4種情況
insert(element, index) {
    // 引數不合法
    const isNaturalNumber = Number.isInteger(index) && index >= 0
    const outOfBounds = index > this.count
    if (!isNaturalNumber || outOfBounds) {
        return false
    }

    const newNode = new DoubleNode(element)
    // 連結串列為空
    if (Object.is(this.head, undefined)) {
        this.head = newNode
        this.tail = newNode
    } else if (Object.is(index, 0)) { // 插入第一項
        newNode.next = this.head
        this.head.prev = newNode
        this.head = newNode
    } else if (Object.is(index, this.count)) { // 插入最後一項
        newNode.prev = this.tail
        this.tail.next = newNode
        this.tail = newNode
    } else { // 插入中間
        const prev = this.getNodeAt(index - 1)
        newNode.next = prev.next
        prev.next.prev = newNode
        prev.next = newNode
        newNode.prev = prev
    }
    this.count++
    return true
}

從任意位置移除元素

我們直接在 LinkedList 類中 removeAt 方法的基礎上修改一下即可:

  • 對於不能刪除的情況,即“引數不合法”部分,無需修改
  • 刪除的場景從2種改為4種
// 從連結串列中刪除特定位置的元素
removeAt(index) {
    // index 必須是自然數,即 0、1、2...
    const isNaturalNumber = Number.isInteger(index) && index >= 0
    const outOfBounds = index >= this.count
    // 處理不能刪除的情況:非自然數、index 出界,都不做處理
    if (!isNaturalNumber || outOfBounds) {
        return
    }

    let current = this.head

    // 第一項&唯一
    if (Object.is(this.size(), 1)) {
        this.head = undefined
        this.tail = undefined
    } else if (Object.is(index, 0)) { // 第一項
        this.head = current.next
        current.next.prev = undefined
    } else if (Object.is(index, this.size() - 1)) { // 最後一項
        this.tail = this.tail.prev
        this.tail.next = undefined
    } else { // 中間項
        current = this.getNodeAt(index)
        const prev = current.prev
        const next = current.next
        prev.next = next
        next.prev = prev
    }
    this.count--
    return current.element
}

其他方法

  • push() 實現比較簡單:
// 在 LinkedList 的 push 方法基礎上修改即可
// 也是分連結串列為空和不為空的情況
// 這個方法還可以呼叫 insert 實現
push(element) {
    // 封裝成節點
    const newNode = new DoubleNode(element)
    if (this.isEmpty()) {
        this.head = newNode
        this.tail = newNode
        this.count++
        return
    }
    this.tail.next = newNode
    newNode.prev = this.tail
    this.tail = newNode
    this.count++
}
  • getNodeAt() 其實可以使用 LinkedList 中的 getNodeAt() 方法,這裡稍微優化一下:
// 如果 index 大於 count/2,就可以從尾部開始迭代,而不是從頭開始(這樣就能迭代更少的元素)
getNodeAt(index) {
    // index 必須是自然數,即 0、1、2...
    const isNaturalNumber = Number.isInteger(index) && index >= 0
    const outOfBounds = index >= this.count
    // 處理不能取得節點的情況:非自然數、index 出界,都不做處理
    if (!isNaturalNumber || outOfBounds) {
        return
    }

    let current = this.head
    let isPositiveOrder = index < (this.count / 2)
    let indexMethod = 'next'
    let count = index
    if (!isPositiveOrder) {
        count = this.count - 1 - index
        indexMethod = 'prev'
    }
    while (count--) {
        current = current[indexMethod]
    }
    return current
}

剩餘的方法直接呼叫父類:indexOftoStringremovesizeisEmptygetElementAt

使用 DoubleLinkedList 類

/**
 * 雙向連結串列中的節點
 *
 * @class DoubleNode
 * @extends {Node}
 */
class DoubleNode extends Node {
    constructor(element) {
        super(element)
        this.prev = undefined
    }
}

/**
 * 雙向連結串列
 * 此類有10個方法,6個來自父類,4個重寫了父類的方法
 * @class DoubleLinkedList
 * @extends {LinkedList}
 */
class DoubleLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals) {
        super(equalsFn)
        this.tail = undefined
    }
    // 向連結串列特定位置插入一個新元素
    insert(element, index) {
        // 引數不合法
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index > this.count
        if (!isNaturalNumber || outOfBounds) {
            return false
        }

        const newNode = new DoubleNode(element)
        // 連結串列為空
        if (Object.is(this.head, undefined)) {
            this.head = newNode
            this.tail = newNode
        } else if (Object.is(index, 0)) { // 插入第一項
            newNode.next = this.head
            this.head.prev = newNode
            this.head = newNode
        } else if (Object.is(index, this.count)) { // 插入最後一項
            newNode.prev = this.tail
            this.tail.next = newNode
            this.tail = newNode
        } else { // 插入中間
            const prev = this.getNodeAt(index - 1)
            newNode.next = prev.next
            prev.next.prev = newNode
            prev.next = newNode
            newNode.prev = prev
        }
        this.count++
        return true
    }
    // 從連結串列中刪除特定位置的元素
    removeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能刪除的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current = this.head

        // 第一項&唯一
        if (Object.is(this.size(), 1)) {
            this.head = undefined
            this.tail = undefined
        } else if (Object.is(index, 0)) { // 第一項
            this.head = current.next
            current.next.prev = undefined
        } else if (Object.is(index, this.size() - 1)) { // 最後一項
            this.tail = this.tail.prev
            this.tail.next = undefined
        } else { // 中間項
            current = this.getNodeAt(index)
            const prev = current.prev
            const next = current.next
            prev.next = next
            next.prev = prev
        }
        this.count--
        return current.element
    }
    // 在 LinkedList 的 push 方法基礎上修改即可
    // 也是分連結串列為空和不為空的情況
    // 這個方法還可以呼叫 insert 實現
    push(element) {
        // 封裝成節點
        const newNode = new DoubleNode(element)
        if (this.isEmpty()) {
            this.head = newNode
            this.tail = newNode
            this.count++
            return
        }
        this.tail.next = newNode
        newNode.prev = this.tail
        this.tail = newNode
        this.count++
    }
    // 其實可以使用 LinkedList 中的 getNodeAt() 方法,這裡稍微優化一下
    // 如果 index 大於 count/2,就可以從尾部開始迭代,而不是從頭開始(這樣就能迭代更少的元素)
    getNodeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能取得節點的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current = this.head
        let isPositiveOrder = index < (this.count / 2)
        let indexMethod = 'next'
        let count = index
        if (!isPositiveOrder) {
            count = this.count - 1 - index
            indexMethod = 'prev'
        }
        while (count--) {
            current = current[indexMethod]
        }
        return current
    }
}

:下面的測試程式碼和 LinkedList 中的幾乎相同,唯一不同的是將 new LinkedList() 改為 new DoubleLinkedList()

class Dog {
    constructor(name) {
        this.name = name
    }
    toString() {
        return this.name
    }
}

let linkedlist = new DoubleLinkedList()
console.log(linkedlist.isEmpty()) // true
linkedlist.push(1)
console.log(linkedlist.isEmpty()) // false
linkedlist.push(2)
console.log(linkedlist.toString()) // 1,2

linkedlist.insert(3, 0) // 在索引 0 處插入元素 3
linkedlist.insert(4, 3)
linkedlist.insert(5, 5) // 插入失敗
console.log(linkedlist.toString()) // 3,1,2,4

let i = linkedlist.indexOf(3)
let j = linkedlist.indexOf(4)
console.log('i: ', i) // i:  0
console.log('j: ', j) // j:  3

let dog1 = new Dog('a')
linkedlist.push(dog1)
console.log(linkedlist.toString()) // 3,1,2,4,a
let k = linkedlist.indexOf(dog1)
console.log('k: ', k) // k:  4
let m = linkedlist.getElementAt(0)
console.log('m: ', m) // m:  3
linkedlist.remove(dog1)
linkedlist.removeAt(0)
console.log(linkedlist.toString()) // 1,2,4
let l = linkedlist.size()
console.log('l: ', l) // l:  3

迴圈連結串列

迴圈連結串列可以基於單項鍊表,也可以基於雙向連結串列。

以單項鍊表為基礎,只需要將連結串列中最後一個節點的 next 指向第一個節點,就是迴圈連結串列

如果以雙向連結串列為基礎,則需要將最後一個節點的 next 指向第一個節點,第一個節點的 prev 指向最後一個節點

基於單項鍊表的迴圈連結串列

直接繼承 LinkedList,不需要增加額外的屬性,就像這樣:

class CircularLinkedList extends LinkedList{
    constructor(equalsFn = defaultEquals) {
        super(equalsFn)
    }
}

Tip:剩餘部分,可自行重寫相應的方法即可,筆者就不在展開。

有序連結串列

有序連結串列是指保持元素有序的連結串列結構。

所以我們只需要繼承 LinkedList 類,並重寫和插入相關的兩個方法即可:

// 預設比較的方法
function defaultCompare(a, b) {
    return a - b
}

/**
 * 有序連結串列
 * 重寫2個方法,保證元素插入到正確的位置,保證連結串列的有序性
 * @class SortedLinkedList
 * @extends {LinkedList}
 */
class SortedLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
        super(equalsFn)
        this.compareFn = compareFn
    }
    push(element) {
        this.insert(element)
    }
    // 不允許在任意位置插入
    insert(element) {
        if (this.isEmpty()) {
            return super.insert(element, 0)
        }

        let current = this.head
        let position = 0
        for (let count = this.size(); position < count; position++) {
            if (this.compareFn(element, current.element) < 0) {
                return super.insert(element, position)
            }
            current = current.next
        }
        return super.insert(element, position)
    }
}

Tip:其中 defaultEqualsLinkedList 類中已經實現過:

function defaultEquals(a, b) {
    return Object.is(a, b)
}

測試程式碼如下:

let linkedlist = new SortedLinkedList()
linkedlist.insert(3)
console.log(linkedlist.toString()) // 3
linkedlist.insert(2)
linkedlist.insert(1)
linkedlist.push(0)
console.log(linkedlist.toString()) // 0,1,2,3

基於連結串列的棧

我們可以使用連結串列作為內部資料結構來建立其他資料結構,例如棧、佇列等

比如我們用 LinkedList 作為 Stack 的內部資料結構,用於建立 StackLinkedList

/**
 * 基於連結串列的棧
 *
 * @class StackLinkedList
 */
class StackLinkedList {
    constructor() {
        this.items = new LinkedList()
    }
    push(...values) {
        values.forEach(item => {
            this.items.push(item)
        })
    }
    toString() {
        return this.items.toString()
    }
    // todo 其他方法 
}
let stack = new StackLinkedList()
stack.push(1, 3, 5)
console.log(stack.toString()) // 1,3,5

Tip:我們還可以對 LinkedList 類優化,儲存一個指向尾部元素的引用

連結串列完整程式碼

Tip:筆者是在 node 環境下進行

LinkedList.js


/**
 * 連結串列的節點
 *
 * @class Node
 */
class Node {
    constructor(element) {
        this.element = element
        this.next = undefined
    }
}

function defaultEquals(a, b) {
    return Object.is(a, b)
}

/**
 * 連結串列
 * @class LinkedList
 */
class LinkedList {
    constructor(equalsFn = defaultEquals) {
        this.count = 0
        this.head = undefined
        this.equalsFn = equalsFn
    }
    // 向連結串列尾部新增一個新元素
    push(element) {
        // 封裝成節點
        const node = new Node(element)
        if (this.head === undefined) {
            this.head = node
            this.count++
            return
        }
        // 取得最後一個節點,並將其引用指向新的節點
        let lastNode = this.head
        while (lastNode.next) {
            lastNode = lastNode.next
        }
        lastNode.next = node
        this.count++
    }
    // 從連結串列中刪除特定位置的元素
    removeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能刪除的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current = this.head
        // 刪除第一項
        if (Object.is(index, 0)) {
            this.head = current.next
        } else {  // 刪除中間項或最後一項
            let prev
            while (index--) {
                prev = current
                current = prev.next
            }
            prev.next = current.next
        }
        this.count--
        return current.element
    }
    getNodeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能取得節點的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current = this.head
        while (index--) {
            current = current.next
        }
        return current
    }
    getElementAt(index) {
        const result = this.getNodeAt(index)
        return result ? result.element : result
    }

    // 向連結串列特定位置插入一個新元素
    insert(element, index) {
        // 引數不合法
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index > this.count
        if (!isNaturalNumber || outOfBounds) {
            return false
        }

        const newNode = new Node(element)
        if (Object.is(index, 0)) { // 連結串列為空 & 插入第一項
            newNode.next = this.head
            this.head = newNode
        } else { // 插入中間以及插入最後一項
            const prev = this.getNodeAt(index - 1)
            newNode.next = prev.next
            prev.next = newNode
        }
        this.count++
        return true
    }

    indexOf(element) {
        let current = this.head
        for (let i = 0, count = this.count; i < count; i++) {
            if (this.equalsFn(current.element, element)) {
                return i
            }
            current = current.next
        }
        return -1
    }

    remove(element) {
        const index = this.indexOf(element)
        return this.removeAt(index)
    }

    size() {
        return this.count
    }

    isEmpty() {
        return this.count === 0
    }

    toString() {
        if (this.isEmpty()) {
            return ''
        }

        let elements = []
        let current = this.head
        while (current) {
            elements.push(current.element)
            current = current.next
        }
        return elements.join(',')
    }
}


/**
 * 雙向連結串列中的節點
 *
 * @class DoubleNode
 * @extends {Node}
 */
class DoubleNode extends Node {
    constructor(element) {
        super(element)
        this.prev = undefined
    }
}

/**
 * 雙向連結串列
 * 此類有10個方法,6個來自父類,4個重寫了父類的方法
 * @class DoubleLinkedList
 * @extends {LinkedList}
 */
class DoubleLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals) {
        super(equalsFn)
        this.tail = undefined
    }
    // 向連結串列特定位置插入一個新元素
    insert(element, index) {
        // 引數不合法
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index > this.count
        if (!isNaturalNumber || outOfBounds) {
            return false
        }

        const newNode = new DoubleNode(element)
        // 連結串列為空
        if (Object.is(this.head, undefined)) {
            this.head = newNode
            this.tail = newNode
        } else if (Object.is(index, 0)) { // 插入第一項
            newNode.next = this.head
            this.head.prev = newNode
            this.head = newNode
        } else if (Object.is(index, this.count)) { // 插入最後一項
            newNode.prev = this.tail
            this.tail.next = newNode
            this.tail = newNode
        } else { // 插入中間
            const prev = this.getNodeAt(index - 1)
            newNode.next = prev.next
            prev.next.prev = newNode
            prev.next = newNode
            newNode.prev = prev
        }
        this.count++
        return true
    }
    // 從連結串列中刪除特定位置的元素
    removeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能刪除的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current = this.head

        // 第一項&唯一
        if (Object.is(this.size(), 1)) {
            this.head = undefined
            this.tail = undefined
        } else if (Object.is(index, 0)) { // 第一項
            this.head = current.next
            current.next.prev = undefined
        } else if (Object.is(index, this.size() - 1)) { // 最後一項
            this.tail = this.tail.prev
            this.tail.next = undefined
        } else { // 中間項
            current = this.getNodeAt(index)
            const prev = current.prev
            const next = current.next
            prev.next = next
            next.prev = prev
        }
        this.count--
        return current.element
    }
    // 在 LinkedList 的 push 方法基礎上修改即可
    // 也是分連結串列為空和不為空的情況
    // 這個方法還可以呼叫 insert 實現
    push(element) {
        // 封裝成節點
        const newNode = new DoubleNode(element)
        if (this.isEmpty()) {
            this.head = newNode
            this.tail = newNode
            this.count++
            return
        }
        this.tail.next = newNode
        newNode.prev = this.tail
        this.tail = newNode
        this.count++
    }
    // 其實可以使用 LinkedList 中的 getNodeAt() 方法,這裡稍微優化一下
    // 如果 index 大於 count/2,就可以從尾部開始迭代,而不是從頭開始(這樣就能迭代更少的元素)
    getNodeAt(index) {
        // index 必須是自然數,即 0、1、2...
        const isNaturalNumber = Number.isInteger(index) && index >= 0
        const outOfBounds = index >= this.count
        // 處理不能取得節點的情況:非自然數、index 出界,都不做處理
        if (!isNaturalNumber || outOfBounds) {
            return
        }

        let current = this.head
        let isPositiveOrder = index < (this.count / 2)
        let indexMethod = 'next'
        let count = index
        if (!isPositiveOrder) {
            count = this.count - 1 - index
            indexMethod = 'prev'
        }
        while (count--) {
            current = current[indexMethod]
        }
        return current
    }
}


/**
 * 迴圈連結串列
 * todo
 * @class CircularLinkedList
 * @extends {LinkedList}
 */
class CircularLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals) {
        super(equalsFn)
    }
}

// 預設比較的方法
function defaultCompare(a, b) {
    return a - b
}

/**
 * 有序連結串列
 * 重寫2個方法,保證元素插入到正確的位置,保證連結串列的有序性
 * @class SortedLinkedList
 * @extends {LinkedList}
 */
class SortedLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
        super(equalsFn)
        this.compareFn = compareFn
    }
    push(element) {
        this.insert(element)
    }
    // 不允許在任意位置插入
    insert(element) {
        if (this.isEmpty()) {
            return super.insert(element, 0)
        }

        let current = this.head
        let position = 0
        for (let count = this.size(); position < count; position++) {
            if (this.compareFn(element, current.element) < 0) {
                return super.insert(element, position)
            }
            current = current.next
        }
        return super.insert(element, position)
    }
}

/**
 * 基於連結串列的棧
 *
 * @class StackLinkedList
 */
class StackLinkedList {
    constructor() {
        this.items = new LinkedList()
    }
    push(...values) {
        values.forEach(item => {
            this.items.push(item)
        })
    }
    toString() {
        return this.items.toString()
    }
    // todo 其他方法 
}
module.exports = { LinkedList, DoubleLinkedList, SortedLinkedList, StackLinkedList }

test.js

const { LinkedList, DoubleLinkedList, SortedLinkedList, StackLinkedList } = require('./LinkedList')

let stack = new StackLinkedList()
stack.push(1, 3, 5)
console.log(stack.toString()) // 1,3,5