javascript數據結構
棧
一種遵從先進後出原則的有序集合
隊列
遵從先進先出原則的有序項
優先隊列
修改版的隊列,設置優先級
循環隊列
基於隊列,克服‘假溢出’想象的隊列。例如隊列長度爲5,取第6個數據時,會取第一個數據
鏈表
要存儲多個元素,數組(或列表)可能是最常用的數據結構
每種語言都實現了數組。這種數據結構非常方便,提供了一個便利的[]語法來訪問它的元素。
然後,這種數據結構有一個缺點:在大多數語言中,數組的大小是固定的,從數組的起點或中間插入或移除項的成本很高,因需要移動元素。
儘管javascript中的Array類方法可以幫我們做這些事,但背後的處理機制同樣如此。
鏈表儲存有序的元素集合,但不同於數組,鏈表中的元素在內存中不是連續放置的。每個元素由一個儲存元素本身的節點和一個指向下一個元素的引用(也稱指針或鏈接)組成
相對於傳統的數組,鏈表的一個好處在於,添加或移除元素的時候不需要移動其他元素。然而,鏈表需要使用指針,因此實現鏈表時需要額外注意。
數組的另一個細節是可以直接訪問任意位置的任何元素,而要想訪問鏈表中間的一個元素,需要從起點(表頭)開始迭代列表直到找到所需的元素
// 鏈表節點
class Node {
constructor(element) {
this.element = element
this.next = null
}
}
// 鏈表
class LinkedList {
constructor() {
this.head = null
this.length = 0
}
// 追加元素
append(element) {
const node = new Node(element)
let current = null
if (this.head === null) {
this.head = node
} else {
current = this.head
while(current.next) {
current = current.next
}
current.next = node
}
this.length++
}
// 任意位置插入元素
insert(position, element) {
if (position >= 0 && position <= this.length) {
const node = new Node(element)
let current = this.head
let previous = null
let index = 0
if (position === 0) {
this.head = node
} else {
while (index++ < position) {
previous = current
current = current.next
}
node.next = current
previous.next = node
}
this.length++
return true
}
return false
}
// 移除指定位置元素
removeAt(position) {
// 檢查越界值
if (position > -1 && position < length) {
let current = this.head
let previous = null
let index = 0
if (position === 0) {
this.head = current.next
} else {
while (index++ < position) {
previous = current
current = current.next
}
previous.next = current.next
}
this.length--
return current.element
}
return null
}
// 尋找元素下標
findIndex(element) {
let current = this.head
let index = -1
while (current) {
if (element === current.element) {
return index + 1
}
index++
current = current.next
}
return -1
}
// 刪除指定文檔
remove(element) {
const index = this.indexOf(element)
return this.removeAt(index)
}
isEmpty() {
return !this.length
}
size() {
return this.length
}
// 轉爲字符串
toString() {
let current = this.head
let string = ''
while (current) {
string += ` ${current.element}`
current = current.next
}
return string
}
}
雙向鏈表
與普通鏈表的區別在於,雙向鏈表的鏈接是雙向的,一個鏈向下一個元素,一個鏈向上一個元素。
雙向鏈表提供了兩種迭代列表的方法,從頭到尾或反過來。在單向鏈表中,如果迭代列表時錯過了要找的元素,就需要回到列表起點,重新開始迭代。
循環鏈表
循環鏈表可以是單向鏈表一樣只有單向引用,也可以向雙向鏈表有雙向引用。循環鏈表和鏈表之間唯一的區別在於,最後元素指向下一個元素的指針(tail.next)不是引用null,而是指向第一個元素(head)
鏈表相比數組最重要的優點,就是無需移動鏈表中的元素,就能輕鬆地添加移除元素。因此,當你需要添加移除很多元素時,最好的選擇就是鏈表,而不是數組
集合
集合是由一組無序且唯一(不能重複)的項組成的。這個數據結構使用了與優先集合相同的數學概念,但應用在計算機科學的數據結構中
class Set {
constructor() {
this.items = {}
}
has(value) {
return this.items.hasOwnProperty(value)
}
add(value) {
if (!this.has(value)) {
this.items[value] = value
return true
}
return false
}
remove(value) {
if (this.has(value)) {
delete this.items[value]
return true
}
return false
}
get size() {
return Object.keys(this.items).length
}
get values() {
return Object.keys(this.items)
}
}
字典
集合,字典,散列表都可以存儲不重複的數據。字典和集合很像,集合是以{ value: value }的形式存儲數據,而字典是以{ key: value}的形式存儲數據,字典也稱爲映射。
object對象便是字典在javascript中的實現
樹
一個樹結構包含一系列存在父子關係的節點。每個節點都有一個父節點(除頂部的第一個節點)以及零個或者多個子節點
二叉樹和二叉搜索樹
二叉樹中的節點最多只能有兩個子節點:一個是左側子節點,另一個是右側子節點。二叉樹在計算機科學中應用非常廣泛
二叉搜索樹(BST)是二叉樹的一種,但是它只允許你在左側節點存儲比父節點小的值,右側節點存儲比父節點大的值
下圖展示二叉搜索樹的組織結構方式
代碼實現二叉搜索樹
class Node {
constructor(key) {
this.key = key
this.left = null
this.right = null
}
}
class BinarySearchTree {
constructor() {
this.root = null
}
insert(key) {
const newNode = new Node(key)
const insertNode = (node, newNode) => {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode
} else {
insertNode(node.left, newNode)
}
} else {
if (node.right === null) {
node.right = newNode
} else {
insertNode(node.right, newNode)
}
}
}
if (!this.root) {
this.root = newNode
} else {
insertNode(this.root, newNode)
}
}
}
使用二叉搜索樹
const tree = new BinarySearchTree()
tree.insert(11)
tree.insert(7)
tree.insert(5)
tree.insert(3)
tree.insert(9)
tree.insert(8)
tree.insert(10)
tree.insert(13)
tree.insert(12)
tree.insert(14)
tree.insert(20)
tree.insert(18)
tree.insert(25)
構建的樹如下圖:
樹的遍歷
遍歷一顆樹是指訪問樹的每一個節點並對它們進行某種操作的過程。
訪問樹的所有節點有三種方式:中序、先序、後序
中序遍歷
中序遍歷是一種以上行順序訪問BST所有節點的遍歷方式,也就是以從小到最大的順序訪問所有的節點。中序遍歷的一種應用就是對樹進行排序操作。實現如下:
inOrderTraverse(callback) {
const inOrderTraverseNode = (node, callback) => {
if (node !== null) {
inOrderTraverseNode(node.left, callback)
callback(node.key)
inOrderTraverseNode(node.right, callback)
}
}
inOrderTraverseNode(this.root, callback)
}
inOrderTraverse方法接受一個回調函數作爲參數,回掉函數用來定義我們對便利到的每個節點進行的操作,這也叫做訪問者模式
在之前展示的樹上執行下面的方法:
tree.inOrderTraverse(value => { console.log(value) })
控制檯將會按照順序輸出:3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
下圖描述了inOrderTraverse方法的訪問路徑
先序遍歷
先序遍歷是以優先於後代節點的順序訪問每個節點的,先序遍歷的一種應用是打印一個結構化的文檔。
preOrderTraverse(callback) {
const preOrderTraverseNode = (node, callback) => {
if (node !== null) {
callback(node.key)
preOrderTraverseNode(node.left, callback)
preOrderTraverseNode(node.right, callback)
}
}
preOrderTraverseNode(this.root, callback)
}
下面的圖描繪了 preOrderTraverse 方法的訪問路徑:
後序遍歷
後序遍歷是先訪問節點的後代節點,再訪問節點本身。後續遍歷的一種應用是計算一個目錄和它的子目錄所有文件所佔空間的大小
postOrderTraverse(callback) {
const postOrderTraverseNode = (node, callback) => {
if (node !== null) {
postOrderTraverseNode(node.left, callback)
postOrderTraverseNode(node.right, callback)
callback(node.key)
}
}
postOrderTraverseNode(this.root, callback)
}
這個例子中,後續遍歷會先訪問左側節點,然後是右側節點,最後是父節點本身。
中序遍歷、先序遍歷、後續遍歷的實現方式相似的,唯一不同是三行代碼的執行順序。
下圖描繪postOrderTraverse方法的訪問路徑
三種遍歷訪問順序的不同
- 先序遍歷:節點本身 => 左側子節點 => 右側子節點
- 中序遍歷:左側子節點 => 節點本身 => 右側子節點
- 後序遍歷:左側子節點 => 節點本身 => 右側子節點
搜索樹中的值
在樹中,有三種經常執行的搜索順序:
- 最大值
- 最小值
- 搜索特定值
搜索最大值和最小值
用下圖的樹作爲示例
實現方法:
min(node) {
const minNode = node => {
return node ? (node.left ? minNode(node.left) : node) : null
}
return minNode(node || this.root)
}
max(node) {
const maxNode = node => {
return node ? (node.right ? maxNode(node.right) : node) : null
}
return maxNode(node || this.root)
}
搜索一個特定的值
search(key) {
const searchNode = (node, key) => {
if (node === null) return false
if (node.key === key) return node
return searchNode((key < node.key) ? node.left : node.right, key)
}
return searchNode(root, key)
}
移除一個節點
remove(key) {
const removeNode = (node, key) => {
if (node === null) return false
if (node.key === key) {
console.log(node)
if (node.left === null && node.right === null) {
let _node = node
node = null
return _node
} else {
console.log('key', key)
}
} else if (node.left !== null && node.key > key) {
if (node.left.key === key) {
node.left.key = this.min(node.left.right).key
removeNode(node.left.right, node.left.key)
return node.left
} else {
return removeNode(node.left, key)
}
} else if (node.right !== null && node.key < key) {
if (node.right.key === key) {
node.right.key = this.min(node.right.right).key
removeNode(node.right.right, node.right.key)
return node.right
} else {
return removeNode(node.right, key)
}
} else {
return false
}
return removeNode((key < node.key) ? node.left : node.right, key)
}
return removeNode(this.root, key)
}
完整寫法:
var removeNode = function(node, key){
if (node === null){ //{2}
return null;
}
if (key < node.key){ //{3}
node.left = removeNode(node.left, key); //{4}
return node; //{5}
} else if (key > node.key){ //{6}
node.right = removeNode(node.right, key); //{7}
return node; //{8}
} else { //鍵等於node.key
//第一種情況——一個葉節點
if (node.left === null && node.right === null){ //{9}
node = null; //{10}
return node; //{11}
}
//第二種情況——一個只有一個子節點的節點
if (node.left === null){ //{12}
node = node.right; //{13}
return node; //{14}
} else if (node.right === null){ //{15}
node = node.left; //{16}
return node; //{17}
}
//第三種情況——一個有兩個子節點的節點
var aux = findMinNode(node.right); //{18}
node.key = aux.key; //{19}
node.right = removeNode(node.right, aux.key); //{20}
return node; //{21}
}
};
var findMinNode = function(node){
while (node && node.left !== null) {
node = node.left;
}
return node;
};
自平衡二叉搜索樹和紅黑樹
TODO