關於連結串列
要儲存多個元素的時候, 陣列/列表 是最為常用的資料結構, 幾乎每個程式語言都實現了陣列或者列表.
但這種結構的缺點是, 通常陣列大小是固定的, 即便類似 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
至此, 單連結串列相關的基本實現就搞定了, 實際應用中基本不用, 但關鍵在於理解這個過程和訓練程式設計思維.