連結串列-單連結串列實現

致于数据科学家的小陈發表於2024-06-03

關於連結串列

要儲存多個元素的時候, 陣列/列表 是最為常用的資料結構, 幾乎每個程式語言都實現了陣列或者列表.

但這種結構的缺點是, 通常陣列大小是固定的, 即便類似 js Array 或者 Python 中的 list, 當我們從中間插入或者刪除元素時成本很高.

陣列特點是: 訪問快 (有索引), 中間或者頭部插入效率慢, 需要連續儲存.

而連結串列這種結構, 裡面的元素不是連續放置的. 每個元素由一個儲存元素本身的節點 和 指向下一個元素的引用 (指標/連結) 組成. 其好處在於, 新增或者刪除元素, 不需要移動其他元素, 但經常需要指標從頭節點去遍歷查詢.

連結串列的特點是: 訪問效率低 (都要遍歷), 中間或頭部插入效率高 (不用移動其他元素), 分散儲存.

建立連結串列

要建立連結串列, 首先我們需要建立一個 Node 類來表示每個節點元素, 其儲存自身的值 和 一個指標屬性, 預設指向 null.

// 節點類
class Node {
  constructor(element) {
    this.element = element
    this.next = null
  }
}

然後是連結串列的初始狀態, 用一個變數 count 來動態記錄連結串列大小, 用 head 來指向頭結點物件, 預設是 null.

// 連結串列類
class LinkedList {
  constructor(equalsFn = defaultEquals) {
    this.count = 0        // 連結串列元素數量
    this.head = null     // 頭結點元素的引用
  }
}

連結串列常用方法

基本的方法包括獲取連結串列的大小, 插入元素, 查詢元素, 連結串列檢視等.

  • size() 獲取連結串列的大小
  • isEmpty() 判斷連結串列是否為空
  • getHead() 獲取頭結點的值
  • toString() 以字元形式列印連結串列元素
  • getElement(index) 根據位置查詢對應節點元素值

  • indexOf(element) 獲取元素的位置, 不存在則返回 -1
  • push(element) 從連結串列尾部新增元素
  • insert(index, element) 從連結串列任意位置插入元素 (前插)
  • removeAt(index) 根據位置刪除元素
  • remove(element) 根據元素值

共用方法

  • size() 獲取連結串列的大小
  • isEmpty() 判斷連結串列是否為空
  • getHead() 獲取頭結點的值
  • toString() 以字元形式列印連結串列元素
  • getElement(index) 根據位置查詢對應節點元素值
// 節點類
class Node {
    constructor(element) {
        this.element = element
        this.nex = null 
    }
}

// 連結串列類
class LinkedList {
    constructor() {
        this.count = 0
        this.head = null
    }
    // 基礎公用方法, 連結串列大小, 頭節點, 列印值 ...
    size() {
        return this.count
    }
    
    isEmpty() {
        return this.count == 0
    }
    
    getHead() {
        // 這裡的 element 就是 Node 的值
        return this.head.element
    }
    
    toString() {
        if (this.head == null) return undefined
        
        let str = `${this.head.element}`
        // current 指向頭結點的下一個元素, 然後開始移動, 直至為空節點
        let current = this.head.next
        for (let i = 1; i < this.size() && current != null; i++) {
            str = `${str}, ${current.element}`
            current = current.next
        }
        return str
    }
    
    // 根據節點位置索引, 查詢並返回該節點物件
    // 這個方法使用頻繁, 不論是查詢, 增刪節點都會用到
    getElementAt(index) {
        // 越界檢查
        if (index < 0 || index > this.count) return undefined
        // 從連結串列頭部開始迭代 0 -> index, 取出元素值即可
        let node = this.head
        for (let i = 0; i < index && node != null) {
            node = node.next
        }
        return node 
    }
}

鏈尾插入元素

  • 如果是空連結串列, 直接讓 head 指向該元素
  • 如果連結串列非空, 則移動指標到鏈尾, 指向該元素
  • 最後要記得更新連結串列長度 +1
// 鏈尾新增元素
push(element) {
    // 例項化節點元素 和 宣告 current 指標, 預設指向 head 節點
    const node = new Node(element)
    let current = this.head

    // 當連結串列為空時, 讓連結串列 head 指向 node 即可
    if (this.head == null) {
        this.head = node
    } else {
        // 移動指標到最後, 然後新增元素
        while (current.next != null) {
            current = current.next
        }
    }
    // 迴圈結束後, 只是指標處於鏈尾, 將其 next 指向新元素即可
    current.next = node
}
// 最後一定要記得更新連結串列長度哦
this.count += 1

任意位置新增元素

這就要用到之前定義的 getElementAt(index) 方法了, 然後要根據插入位置做不同處理.

當插入的位置是 0, 即從頭結點插入時:

node -> [];  [node]
或者: note -> [1, 2, 3] => [node, 1, 2, 3]

則:  this.head = node; node.next = current

當從中間插入時, 則要先找到這個元素, 並對其前, 後 元素進行先斷鏈, 新增元素後再連結的操作:

node -> [1, 2, 3, 4], 要在位置 2 的位置插入:

[1, 2, node, 3, 4]

先獲取目標位置的前一個元素 previous = p(2-1) = 2;
然後目標位置的元素 current = previous.next = 3;

然後斷鏈插入操作, 讓 current 往後 "挪" 一下即可.
previous.next = node
node.next = current

  // 任意位置插入元素
  insert(index, element) {
    // 越界檢查
    if (index < 0 || index > this.count) return false 

    // 建立節點物件, 並判斷是在哪個位置新增
    const node = new Node(element)
    
    if (index == 0) {
      // 如果是第一個位置新增, 則讓 this.head -> node -> head_old 即可
      node.next = this.head 
      this.head = node
    } else {
      // 獲取插入位置的前一個節點 previous, 以當前節點 current
      const previous = this.getElementAt(index - 1)
      const current = previous.next
      // 斷鏈插入 node 以後, current 往後 "挪" 了一下
      previous.next = node 
      node.next = current
    }
    // 新增了都要更新連結串列長度
    this.count++
    return true
  }

任意位置刪除元素

也是要用到上面定義的 getElementAt(index) 方法, 整體邏輯和插入邏輯是差不多的.

首先都是要根據位置, 獲取到該元素節點 target, 然後根據位置進行分別斷鏈處理即可.

  • 刪除的是頭元素, 直接讓 head -> target.next
  • 刪除的是非頭元素, 讓 target 前一個節點 -> target 的後一個節點就搞定
// 任意位置刪除元素
removeAt(index) {
    // 索引越界檢查
    if (index < 0 || index > this.count) return false
    let current = this.head
    
    // 判斷 index 是否為 0, 對應不同的處理方式
    if (index == 0) {
        // 若移除的是頭元素, 則將連結串列頭指標指向下一個元素
    	this.head = current.next
    } else {
        // 找到目標位置的前一個元素, 後一個元素, 進行相連即可
        const previous = this.getElementAt(index -1)
        current = previous.next 
        previous.next = current.next
    }
    // 最後記得長度減1, 並返回被刪除的元素
    this.count -= 1
    return current.element
}

返回元素的位置

即從頭到尾遍歷連結串列, 當找到時就返回位置, 沒有找到則返回 -1.

indexOf(element) {
    let current = this.head
    for (let i = 0; i < this.count && current != null) {
        if (current.element = element) return i
        // 往後移動指標
        current = current.next
    }
    // 移動完都沒找到就返回 -1
    return -1
}

刪除元素

直接用上面的 indexOf(element) 找到元素的位置, 然後用 removeAt(index) 就搞定啦.

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

完整實現

// 節點類, 輔助
class Node {
  constructor(element) {
    this.element = element
    this.next = null
  }
}


// 連結串列類
class LinkedList {
  constructor() {
    this.count = 0        // 連結串列元素數量
    this.head = null      // 頭結點元素的引用
  }
  // 連結串列大小, 是否為空, 返回頭結點, toString 等
  size() {
    return this.count
  }

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

  getHead() {
    return this.head.element
  }

  toString() {
    if (this.head == null) return undefined

    let objString = `${this.head.element}`
    let current = this.head.next
    // 遍歷節點將元素連結起來
    for (let i = 1; i < this.size() && current != null; i++) {
      objString = `${objString}, ${current.element}`
      current = current.next
    }
    return objString
  }

  // 根據元素位置索引, 查詢並返回該元素
  getElementAt(index) {
    // 越界檢查
    if (index < 0 || index > this.count) return undefined
    // 從連結串列頭部開始迭代
    let node = this.head
    for (let i = 0; i < index && node != null; i++) {
      node = node.next
    }
    return node
  }

  // 鏈尾新增元素
  push(element) {
    // 元素新增進節點, 和宣告 current 指標, 預設指向 head
    const node = new Node(element)
    let current = this.head

    // 當尾空連結串列時, 此時的元素是第一個, 則讓連結串列的 head 指向該 node
    if (this.head == null) {
      this.head = node
    } else {
      // 連結串列中已有元素, 移動 current 指標到最後
      while (current.next != undefined) {
        current = current.next
      }

      current.next = node
    }
    this.count++
  }

  // 任意位置插入元素
  insert(index, element) {
    // 越界檢查
    if (index < 0 || index > this.count) return false 

    const node = new Node(element)
    if (index == 0) {
      node.next = this.head 
      this.head = node
    } else {
      // 獲取目標位置的前一個元素, 當前元素, 下一個元素
      const previous = this.getElementAt(index - 1)
      const current = previous.next

      previous.next = node 
      node.next = current
    }
    this.count++
    return true
  }

  // 移除指定位置的元素 
  removeAt(index) {
    // 索引越界檢查
    if (index < 0 || index > this.count) return undefined
    // 用一個指標預設指向連結串列頭元素
    let current = this.head
    if (index == 0) {
      // 如果移除的是第一項, 則將連結串列的頭元素指向下一個元素即可
      this.head = current.next
    } else {
      const previous = this.getElementAt(index - 1)
      current = previous.next
      previous.next = current.next
    }
    this.count--
    return current.element
  }

  // 返回元素的位置 
  indexOf(element) {
    let current = this.head
    for (let i = 0; i < this.count && current != null; i++) {
      if (current.element === element) return i 
      // 移動指標
      current = current.next 
    }
    return -1
  }

  // 移除元素
  remove(element) {
    const index = this.indexOf(element)
    return this.removeAt(index)
  }
}

// test 
const list = new LinkedList()
console.log('連結串列元素是:',  list.toString());
list.push(10)
console.log('連結串列元素是:',  list.toString());

list.push(20)
list.push(30)
list.push(40)
console.log('連結串列元素是:',  list.toString());
console.log('連結串列的長度是: ', list.count);
console.log('刪除的元素是: ', list.removeAt(0))
console.log('連結串列元素是:',  list.toString());
console.log('連結串列的長度是: ', list.count);

console.log('在位置 2 的地方新增 666:', list.insert(2, 666))
console.log('在位置 0 的地方新增 999:', list.insert(0, 999))
console.log('此時的頭結點值是: ', list.getHead());
console.log('連結串列元素是:',  list.toString());
console.log('連結串列的長度是: ', list.count);

console.log('查詢 666 的索引位置是: ', list.indexOf(666))
console.log('查詢 888 的索引位置是: ', list.indexOf(888))

console.log('刪除掉元素 666: ', list.remove(666))
console.log('連結串列元素是:',  list.toString());
console.log('連結串列的長度是: ', list.count);
console.log('此時的頭結點值是: ', list.getHead());

console.log('從頭節點插入 nb: ',  list.insert(0, 'nb'))
console.log('此時連結串列元素是:',  list.toString());
console.log('此時的頭結點值是: ', list.getHead());

console.log('從位置 2 出插入 222: ', list.insert(2, 222))
console.log('此時連結串列元素是:',  list.toString());
console.log('此時的頭結點值是: ', list.getHead());

輸出:

PS F:\algorithms> node .\linkedList.js
連結串列元素是: undefined
連結串列元素是: 10
連結串列元素是: 10, 20, 30, 40
連結串列的長度是:  4
刪除的元素是:  10
連結串列元素是: 20, 30, 40    
連結串列的長度是:  3
在位置 2 的地方新增 666: true
在位置 0 的地方新增 999: true
此時的頭結點值是:  999
連結串列元素是: 999, 20, 30, 666, 40
連結串列的長度是:  5
查詢 666 的索引位置是:  3
查詢 888 的索引位置是:  -1
刪除掉元素 666:  666
連結串列元素是: 999, 20, 30, 40
連結串列的長度是:  4
此時的頭結點值是:  999
從頭節點插入 nb:  true
此時連結串列元素是: nb, 999, 20, 30, 40
此時的頭結點值是:  nb
從位置 2 出插入 222:  true
此時連結串列元素是: nb, 999, 222, 20, 30, 40
此時的頭結點值是:  nb

至此, 單連結串列相關的基本實現就搞定了, 實際應用中基本不用, 但關鍵在於理解這個過程和訓練程式設計思維.

相關文章