棧是一個線性結構,在計算機中是一個至關常見的數據結構。javascript
棧的特色是隻能在某一端添加或刪除數據,遵循先進後出的原則java
每種數據結構均可以用不少種方式來實現,其實能夠把棧當作是數組的一個子集,因此這裏使用數組來實現node
class Stack {
constructor() {
this.stack = []
}
push(item) {
this.stack.push(item)
}
pop() {
this.stack.pop()
}
peek() {
return this.stack[this.getCount() - 1]
}
getCount() {
return this.stack.length
}
isEmpty() {
return this.getCount() === 0
}
}
複製代碼
選取了 LeetCode 上序號爲 20 的題目數組
題意是匹配括號,能夠經過棧的特性來完成這道題目數據結構
var isValid = function (s) {
let map = {
'(': -1,
')': 1,
'[': -2,
']': 2,
'{': -3,
'}': 3
}
let stack = []
for (let i = 0; i < s.length; i++) {
if (map[s[i]] < 0) {
stack.push(s[i])
} else {
let last = stack.pop()
if (map[last] + map[s[i]] != 0) return false
}
}
if (stack.length > 0) return false
return true
};
複製代碼
隊列一個線性結構,特色是在某一端添加數據,在另外一端刪除數據,遵循先進先出的原則。學習
這裏會講解兩種實現隊列的方式,分別是單鏈隊列和循環隊列。優化
class Queue {
constructor() {
this.queue = []
}
enQueue(item) {
this.queue.push(item)
}
deQueue() {
return this.queue.shift()
}
getHeader() {
return this.queue[0]
}
getLength() {
return this.queue.length
}
isEmpty() {
return this.getLength() === 0
}
}
複製代碼
由於單鏈隊列在出隊操做的時候須要 O(n) 的時間複雜度,因此引入了循環隊列。循環隊列的出隊操做平均是 O(1) 的時間複雜度。ui
class SqQueue {
constructor(length) {
this.queue = new Array(length + 1)
// 隊頭
this.first = 0
// 隊尾
this.last = 0
// 當前隊列大小
this.size = 0
}
enQueue(item) {
// 判斷隊尾 + 1 是否爲隊頭
// 若是是就表明須要擴容數組
// % this.queue.length 是爲了防止數組越界
if (this.first === (this.last + 1) % this.queue.length) {
this.resize(this.getLength() * 2 + 1)
}
this.queue[this.last] = item
this.size++
this.last = (this.last + 1) % this.queue.length
}
deQueue() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
let r = this.queue[this.first]
this.queue[this.first] = null
this.first = (this.first + 1) % this.queue.length
this.size--
// 判斷當前隊列大小是否太小
// 爲了保證不浪費空間,在隊列空間等於總長度四分之一時
// 且不爲 2 時縮小總長度爲當前的一半
if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
this.resize(this.getLength() / 2)
}
return r
}
getHeader() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
return this.queue[this.first]
}
getLength() {
return this.queue.length - 1
}
isEmpty() {
return this.first === this.last
}
resize(length) {
let q = new Array(length)
for (let i = 0; i < length; i++) {
q[i] = this.queue[(i + this.first) % this.queue.length]
}
this.queue = q
this.first = 0
this.last = this.size
}
}
複製代碼
鏈表是一個線性結構,同時也是一個自然的遞歸結構。鏈表結構能夠充分利用計算機內存空間,實現靈活的內存動態管理。可是鏈表失去了數組隨機讀取的優勢,同時鏈表因爲增長告終點的指針域,空間開銷比較大。this
單向鏈表spa
class Node {
constructor(v, next) {
this.value = v
this.next = next
}
}
class LinkList {
constructor() {
// 鏈表長度
this.size = 0
// 虛擬頭部
this.dummyNode = new Node(null, null)
}
find(header, index, currentIndex) {
if (index === currentIndex) return header
return this.find(header.next, index, currentIndex + 1)
}
addNode(v, index) {
this.checkIndex(index)
// 當往鏈表末尾插入時,prev.next 爲空
// 其餘狀況時,由於要插入節點,因此插入的節點
// 的 next 應該是 prev.next
// 而後設置 prev.next 爲插入的節點
let prev = this.find(this.dummyNode, index, 0)
prev.next = new Node(v, prev.next)
this.size++
return prev.next
}
insertNode(v, index) {
return this.addNode(v, index)
}
addToFirst(v) {
return this.addNode(v, 0)
}
addToLast(v) {
return this.addNode(v, this.size)
}
removeNode(index, isLast) {
this.checkIndex(index)
index = isLast ? index - 1 : index
let prev = this.find(this.dummyNode, index, 0)
let node = prev.next
prev.next = node.next
node.next = null
this.size--
return node
}
removeFirstNode() {
return this.removeNode(0)
}
removeLastNode() {
return this.removeNode(this.size, true)
}
checkIndex(index) {
if (index < 0 || index > this.size) throw Error('Index error')
}
getNode(index) {
this.checkIndex(index)
if (this.isEmpty()) return
return this.find(this.dummyNode, index, 0).next
}
isEmpty() {
return this.size === 0
}
getSize() {
return this.size
}
}
複製代碼
樹擁有不少種結構,二叉樹是樹中最經常使用的結構,同時也是一個自然的遞歸結構。
二叉樹擁有一個根節點,每一個節點至多擁有兩個子節點,分別爲:左節點和右節點。樹的最底部節點稱之爲葉節點,當一顆樹的葉數量數量爲滿時,該樹能夠稱之爲滿二叉樹。
二分搜索樹也是二叉樹,擁有二叉樹的特性。可是區別在於二分搜索樹每一個節點的值都比他的左子樹的值大,比右子樹的值小。
這種存儲方式很適合於數據搜索。以下圖所示,當須要查找 6 的時候,由於須要查找的值比根節點的值大,因此只須要在根節點的右子樹上尋找,大大提升了搜索效率。
class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}
class BST {
constructor() {
this.root = null
this.size = 0
}
getSize() {
return this.size
}
isEmpty() {
return this.size === 0
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
// 添加節點時,須要比較添加的節點值和當前
// 節點值的大小
_addChild(node, v) {
if (!node) {
this.size++
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
}
return node
}
}
複製代碼
以上是最基本的二分搜索樹實現,接下來實現樹的遍歷。
對於樹的遍從來說,有三種遍歷方法,分別是先序遍歷、中序遍歷、後序遍歷。三種遍歷的區別在於什麼時候訪問節點。在遍歷樹的過程當中,每一個節點都會遍歷三次,分別是遍歷到本身,遍歷左子樹和遍歷右子樹。若是須要實現先序遍歷,那麼只須要第一次遍歷到節點時進行操做便可。
如下都是遞歸實現,若是你想學習非遞歸實現,能夠 點擊這裏閱讀
// 先序遍歷可用於打印樹的結構
// 先序遍歷先訪問根節點,而後訪問左節點,最後訪問右節點。
preTraversal() {
this._pre(this.root)
}
_pre(node) {
if (node) {
console.log(node.value)
this._pre(node.left)
this._pre(node.right)
}
}
// 中序遍歷可用於排序
// 對於 BST 來講,中序遍歷能夠實現一次遍歷就
// 獲得有序的值
// 中序遍歷表示先訪問左節點,而後訪問根節點,最後訪問右節點。
midTraversal() {
this._mid(this.root)
}
_mid(node) {
if (node) {
this._mid(node.left)
console.log(node.value)
this._mid(node.right)
}
}
// 後序遍歷可用於先操做子節點
// 再操做父節點的場景
// 後序遍歷表示先訪問左節點,而後訪問右節點,最後訪問根節點。
backTraversal() {
this._back(this.root)
}
_back(node) {
if (node) {
this._back(node.left)
this._back(node.right)
console.log(node.value)
}
}
複製代碼
以上的這幾種遍歷均可以稱之爲深度遍歷,對應的還有種遍歷叫作廣度遍歷,也就是一層層地遍歷樹。對於廣度遍從來說,咱們須要利用以前講過的隊列結構來完成。
breadthTraversal() {
if (!this.root) return null
let q = new Queue()
// 將根節點入隊
q.enQueue(this.root)
// 循環判斷隊列是否爲空,爲空
// 表明樹遍歷完畢
while (!q.isEmpty()) {
// 將隊首出隊,判斷是否有左右子樹
// 有的話,就先左後右入隊
let n = q.deQueue()
console.log(n.value)
if (n.left) q.enQueue(n.left)
if (n.right) q.enQueue(n.right)
}
}
複製代碼
接下來先介紹如何在樹中尋找最小值或最大數。由於二分搜索樹的特性,因此最小值必定在根節點的最左邊,最大值相反
getMin() {
return this._getMin(this.root).value
}
_getMin(node) {
if (!node.left) return node
return this._getMin(node.left)
}
getMax() {
return this._getMax(this.root).value
}
_getMax(node) {
if (!node.right) return node
return this._getMin(node.right)
}
複製代碼
向上取整和向下取整,這兩個操做是相反的,因此代碼也是相似的,這裏只介紹如何向下取整。既然是向下取整,那麼根據二分搜索樹的特性,值必定在根節點的左側。只須要一直遍歷左子樹直到當前節點的值再也不大於等於須要的值,而後判斷節點是否還擁有右子樹。若是有的話,繼續上面的遞歸判斷。
floor(v) {
let node = this._floor(this.root, v)
return node ? node.value : null
}
_floor(node, v) {
if (!node) return null
if (node.value === v) return v
// 若是當前節點值還比須要的值大,就繼續遞歸
if (node.value > v) {
return this._floor(node.left, v)
}
// 判斷當前節點是否擁有右子樹
let right = this._floor(node.right, v)
if (right) return right
return node
}
複製代碼
排名,這是用於獲取給定值的排名或者排名第幾的節點的值,這兩個操做也是相反的,因此這個只介紹如何獲取排名第幾的節點的值。對於這個操做而言,咱們須要略微的改造點代碼,讓每一個節點擁有一個 size
屬性。該屬性表示該節點下有多少子節點(包含自身)。
class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
// 修改代碼
this.size = 1
}
}
// 新增代碼
_getSize(node) {
return node ? node.size : 0
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
// 修改代碼
node.size++
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
// 修改代碼
node.size++
node.right = this._addChild(node.right, v)
}
return node
}
select(k) {
let node = this._select(this.root, k)
return node ? node.value : null
}
_select(node, k) {
if (!node) return null
// 先獲取左子樹下有幾個節點
let size = node.left ? node.left.size : 0
// 判斷 size 是否大於 k
// 若是大於 k,表明所須要的節點在左節點
if (size > k) return this._select(node.left, k)
// 若是小於 k,表明所須要的節點在右節點
// 注意這裏須要從新計算 k,減去根節點除了右子樹的節點數量
if (size < k) return this._select(node.right, k - size - 1)
return node
}
複製代碼
接下來說解的是二分搜索樹中最難實現的部分:刪除節點。由於對於刪除節點來講,會存在如下幾種狀況
對於前兩種狀況很好解決,可是第三種狀況就有難度了,因此先來實現相對簡單的操做:刪除最小節點,對於刪除最小節點來講,是不存在第三種狀況的,刪除最大節點操做是和刪除最小節點相反的,因此這裏也就再也不贅述。
delectMin() {
this.root = this._delectMin(this.root)
console.log(this.root)
}
_delectMin(node) {
// 一直遞歸左子樹
// 若是左子樹爲空,就判斷節點是否擁有右子樹
// 有右子樹的話就把須要刪除的節點替換爲右子樹
if ((node != null) & !node.left) return node.right
node.left = this._delectMin(node.left)
// 最後須要從新維護下節點的 `size`
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
複製代碼
最後講解的就是如何刪除任意節點了。對於這個操做,T.Hibbard 在 1962 年提出瞭解決這個難題的辦法,也就是如何解決第三種狀況。
當遇到這種狀況時,須要取出當前節點的後繼節點(也就是當前節點右子樹的最小節點)來替換須要刪除的節點。而後將須要刪除節點的左子樹賦值給後繼結點,右子樹刪除後繼結點後賦值給他。
你若是對於這個解決辦法有疑問的話,能夠這樣考慮。由於二分搜索樹的特性,父節點必定比全部左子節點大,比全部右子節點小。那麼當須要刪除父節點時,勢必須要拿出一個比父節點大的節點來替換父節點。這個節點確定不存在於左子樹,必然存在於右子樹。而後又須要保持父節點都是比右子節點小的,那麼就能夠取出右子樹中最小的那個節點來替換父節點。
delect(v) {
this.root = this._delect(this.root, v)
}
_delect(node, v) {
if (!node) return null
// 尋找的節點比當前節點小,去左子樹找
if (node.value < v) {
node.right = this._delect(node.right, v)
} else if (node.value > v) {
// 尋找的節點比當前節點大,去右子樹找
node.left = this._delect(node.left, v)
} else {
// 進入這個條件說明已經找到節點
// 先判斷節點是否擁有擁有左右子樹中的一個
// 是的話,將子樹返回出去,這裏和 `_delectMin` 的操做同樣
if (!node.left) return node.right
if (!node.right) return node.left
// 進入這裏,表明節點擁有左右子樹
// 先取出當前節點的後繼結點,也就是取當前節點右子樹的最小值
let min = this._getMin(node.right)
// 取出最小值後,刪除最小值
// 而後把刪除節點後的子樹賦值給最小值節點
min.right = this._delectMin(node.right)
// 左子樹不動
min.left = node.left
node = min
}
// 維護 size
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
複製代碼
二分搜索樹實際在業務中是受到限制的,由於並非嚴格的 O(logN),在極端狀況下會退化成鏈表,好比加入一組升序的數字就會形成這種狀況。
AVL 樹改進了二分搜索樹,在 AVL 樹中任意節點的左右子樹的高度差都不大於 1,這樣保證了時間複雜度是嚴格的 O(logN)。基於此,對 AVL 樹增長或刪除節點時可能須要旋轉樹來達到高度的平衡。
由於 AVL 樹是改進了二分搜索樹,因此部分代碼是於二分搜索樹重複的,對於重複內容不做再次解析。
對於 AVL 樹來講,添加節點會有四種狀況
對於左左狀況來講,新增長的節點位於節點 2 的左側,這時樹已經不平衡,須要旋轉。由於搜索樹的特性,節點比左節點大,比右節點小,因此旋轉之後也要實現這個特性。
旋轉以前:new < 2 < C < 3 < B < 5 < A,右旋以後節點 3 爲根節點,這時候須要將節點 3 的右節點加到節點 5 的左邊,最後還須要更新節點的高度。
對於右右狀況來講,相反於左左狀況,因此再也不贅述。
對於左右狀況來講,新增長的節點位於節點 4 的右側。對於這種狀況,須要經過兩次旋轉來達到目的。
首先對節點的左節點左旋,這時樹知足左左的狀況,再對節點進行一次右旋就能夠達到目的。
class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
this.height = 1
}
}
class AVL {
constructor() {
this.root = null
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
} else {
node.value = v
}
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
let factor = this._getBalanceFactor(node)
// 當須要右旋時,根節點的左樹必定比右樹高度高
if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
return this._rightRotate(node)
}
// 當須要左旋時,根節點的左樹必定比右樹高度矮
if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
return this._leftRotate(node)
}
// 左右狀況
// 節點的左樹比右樹高,且節點的左樹的右樹比節點的左樹的左樹高
if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
node.left = this._leftRotate(node.left)
return this._rightRotate(node)
}
// 右左狀況
// 節點的左樹比右樹矮,且節點的右樹的右樹比節點的右樹的左樹矮
if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
node.right = this._rightRotate(node.right)
return this._leftRotate(node)
}
return node
}
_getHeight(node) {
if (!node) return 0
return node.height
}
_getBalanceFactor(node) {
return this._getHeight(node.left) - this._getHeight(node.right)
}
// 節點右旋
// 5 2
// / \ / \
// 2 6 ==> 1 5
// / \ / / \
// 1 3 new 3 6
// /
// new
_rightRotate(node) {
// 旋轉後新根節點
let newRoot = node.left
// 須要移動的節點
let moveNode = newRoot.right
// 節點 2 的右節點改成節點 5
newRoot.right = node
// 節點 5 左節點改成節點 3
node.left = moveNode
// 更新樹的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))
return newRoot
}
// 節點左旋
// 4 6
// / \ / \
// 2 6 ==> 4 7
// / \ / \ \
// 5 7 2 5 new
// \
// new
_leftRotate(node) {
// 旋轉後新根節點
let newRoot = node.right
// 須要移動的節點
let moveNode = newRoot.left
// 節點 6 的左節點改成節點 4
newRoot.left = node
// 節點 4 右節點改成節點 5
node.right = moveNode
// 更新樹的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))
return newRoot
}
}
複製代碼
在計算機科學,trie,又稱前綴樹或字典樹,是一種有序樹,用於保存關聯數組,其中的鍵一般是字符串。
簡單點來講,這個結構的做用大可能是爲了方便搜索字符串,該樹有如下幾個特色
總得來講 Trie 的實現相比別的樹結構來講簡單的不少,實現就以搜索英文字符爲例。
class TrieNode {
constructor() {
// 表明每一個字符通過節點的次數
this.path = 0
// 表明到該節點的字符串有幾個
this.end = 0
// 連接
this.next = new Array(26).fill(null)
}
}
class Trie {
constructor() {
// 根節點,表明空字符
this.root = new TrieNode()
}
// 插入字符串
insert(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
// 得到字符先對應的索引
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 若是索引對應沒有值,就建立
if (!node.next[index]) {
node.next[index] = new TrieNode()
}
node.path += 1
node = node.next[index]
}
node.end += 1
}
// 搜索字符串出現的次數
search(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 若是索引對應沒有值,表明沒有須要搜素的字符串
if (!node.next[index]) {
return 0
}
node = node.next[index]
}
return node.end
}
// 刪除字符串
delete(str) {
if (!this.search(str)) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 若是索引對應的節點的 Path 爲 0,表明通過該節點的字符串
// 已經一個,直接刪除便可
if (--node.next[index].path == 0) {
node.next[index] = null
return
}
node = node.next[index]
}
node.end -= 1
}
}
複製代碼
並查集是一種特殊的樹結構,用於處理一些不交集的合併及查詢問題。該結構中每一個節點都有一個父節點,若是隻有當前一個節點,那麼該節點的父節點指向本身。
這個結構中有兩個重要的操做,分別是:
class DisjointSet {
// 初始化樣本
constructor(count) {
// 初始化時,每一個節點的父節點都是本身
this.parent = new Array(count)
// 用於記錄樹的深度,優化搜索複雜度
this.rank = new Array(count)
for (let i = 0; i < count; i++) {
this.parent[i] = i
this.rank[i] = 1
}
}
find(p) {
// 尋找當前節點的父節點是否爲本身,不是的話表示還沒找到
// 開始進行路徑壓縮優化
// 假設當前節點父節點爲 A
// 將當前節點掛載到 A 節點的父節點上,達到壓縮深度的目的
while (p != this.parent[p]) {
this.parent[p] = this.parent[this.parent[p]]
p = this.parent[p]
}
return p
}
isConnected(p, q) {
return this.find(p) === this.find(q)
}
// 合併
union(p, q) {
// 找到兩個數字的父節點
let i = this.find(p)
let j = this.find(q)
if (i === j) return
// 判斷兩棵樹的深度,深度小的加到深度大的樹下面
// 若是兩棵樹深度相等,那就無所謂怎麼加
if (this.rank[i] < this.rank[j]) {
this.parent[i] = j
} else if (this.rank[i] > this.rank[j]) {
this.parent[j] = i
} else {
this.parent[i] = j
this.rank[j] += 1
}
}
}
複製代碼
堆一般是一個能夠被看作一棵樹的數組對象。
堆的實現經過構造二叉堆,實爲二叉樹的一種。這種數據結構具備如下性質。
將根節點最大的堆叫作最大堆或大根堆,根節點最小的堆叫作最小堆或小根堆。
優先隊列也徹底能夠用堆來實現,操做是如出一轍的。
堆的每一個節點的左邊子節點索引是 i * 2 + 1
,右邊是 i * 2 + 2
,父節點是 (i - 1) /2
。
堆有兩個核心的操做,分別是 shiftUp
和 shiftDown
。前者用於添加元素,後者用於刪除根節點。
shiftUp
的核心思路是一路將節點與父節點對比大小,若是比父節點大,就和父節點交換位置。
shiftDown
的核心思路是先將根節點和末尾交換位置,而後移除末尾元素。接下來循環判斷父節點和兩個子節點的大小,若是子節點大,就把最大的子節點和父節點交換。
class MaxHeap {
constructor() {
this.heap = []
}
size() {
return this.heap.length
}
empty() {
return this.size() == 0
}
add(item) {
this.heap.push(item)
this._shiftUp(this.size() - 1)
}
removeMax() {
this._shiftDown(0)
}
getParentIndex(k) {
return parseInt((k - 1) / 2)
}
getLeftIndex(k) {
return k * 2 + 1
}
_shiftUp(k) {
// 若是當前節點比父節點大,就交換
while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
this._swap(k, this.getParentIndex(k))
// 將索引變成父節點
k = this.getParentIndex(k)
}
}
_shiftDown(k) {
// 交換首位並刪除末尾
this._swap(k, this.size() - 1)
this.heap.splice(this.size() - 1, 1)
// 判斷節點是否有左孩子,由於二叉堆的特性,有右必有左
while (this.getLeftIndex(k) < this.size()) {
let j = this.getLeftIndex(k)
// 判斷是否有右孩子,而且右孩子是否大於左孩子
if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
// 判斷父節點是否已經比子節點都大
if (this.heap[k] >= this.heap[j]) break
this._swap(k, j)
k = j
}
}
_swap(left, right) {
let rightValue = this.heap[right]
this.heap[right] = this.heap[left]
this.heap[left] = rightValue
}
}
複製代碼