連結串列
連結串列資料結構
前面我們已經學習了陣列資料結構,但是從陣列頭部或中間插入元素,或者移除元素的成本比較高,因為需要移動元素。
就像這樣:
// 從頭部插入元素
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:理解了什麼是連結串列,比較容易想到的方法有
- 插入和刪除:
push
、insert
、remove
、removeAt
- 其他:
size
、isEmpty
、toString
向連結串列尾部新增一個新元素
第一種實現,需要依賴於另外兩個方法:
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
}
剩餘的方法直接呼叫父類:indexOf
、toString
、remove
、size
、isEmpty
、getElementAt
使用 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:其中 defaultEquals
在 LinkedList
類中已經實現過:
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