數據結構是在計算機中組織和存儲數據的一種特殊方式,使得數據能夠高效地被訪問和修改。更確切地說,數據結構是數據值的集合,表示數據之間的關係,也包括了做用在數據上的函數或操做。javascript
Array
Stack
Queue
Linked Lists
Trees
Graphs
Trie
Hash Tables
在較高的層次上,基本上有三種類型的數據結構:前端
在複雜性方面:java
就效率而已:node
數組是最簡單的數據結構,這裏就不講過多了。
貼一張每一個函數都運行10,000次迭代:python
[ { id: "key0", content: "I ate pizza 0 times" }, { id: "key1", content: "I ate pizza 1 times" }, { id: "key2", content: "I ate pizza 2 times" }, ... ] ["key284", "key958", "key23", "key625", "key83", "key9", ... ]
for... in
爲什麼這麼慢?for... in
語法使人難以置信的緩慢。在測試中就已經比正常狀況下慢近9倍的循環。面試
這是由於for ... in
語法是第一個可以迭代對象鍵的JavaScript語句。算法
循環對象鍵({}
)與在數組([]
)上進行循環不一樣,編程
由於引擎會執行一些額外的工做來跟蹤已經迭代的屬性。segmentfault
Stack
堆棧是元素的集合,能夠在頂部添加項目,咱們有幾個實際的堆棧示例:後端
三句話解釋堆棧:
push
和pop
。Push
將元素添加到數組的頂部,而Pop
將它們從同一位置刪除。Last In,First Out
",即:LIFO
,後進先出。請注意,下方例子中,咱們能夠顛倒堆棧的順序:底部變爲頂部,頂部變爲底部。
所以,咱們能夠分別使用數組unshift
和shift
方法代替push
和pop
。
class Stack { constructor(...items) { this.reverse = false; this.stack = [...items]; } push(...items) { return this.reverse ? this.stack.unshift(...items) : this.stack.push(...items); } pop() { return this.reverse ? this.stack.shift() : this.stack.pop(); } } const stack = new Stack(4, 5); stack.reverse = true; console.log(stack.push(1, 2, 3) === 5) // true console.log(stack.stack ===[1, 2, 3, 4, 5]) // true
Queue
在計算機科學中,一個隊列(queue)是一種特殊類型的抽象數據類型或集合。集合中的實體按順序保存。
而在前端開發中,最著名的隊列使用當屬瀏覽器/NodeJs中 關於宏任務與微任務,任務隊列的知識。這裏就再也不贅述了。
在後端領域,用得最普遍的就是消息隊列:Message queue
:如RabbitMQ
、ActiveMQ
等。
以編程思想而言,Queue
能夠用兩句話描述:
unshift
和pop
。"Fist In,first out"
即:FIFO
,先進先出。
請注意,下方例子中,咱們能夠顛倒堆隊列的順序。
所以,咱們能夠分別使用數組unshift
和shift
方法代替push
和pop
。
class Queue { constructor(...items) { this.reverse = false; this.queue = [...items]; } enqueue(...items) { return this.reverse ? this.queue.push(...items) : this.queue.unshift(...items); } dequeue() { return this.reverse ? this.queue.shift() : this.queue.pop(); } }
Linked Lists
與數組同樣,鏈表是按順序存儲數據元素。
鏈表不是保留索引,而是指向其餘元素。
第一個節點稱爲頭部(head
),而最後一個節點稱爲尾部(tail
)。
單鏈表與雙向鏈表:
鏈表的優勢:
鏈表的應用場景:
連接列表在客戶端和服務器上都頗有用。
Redux
就以鏈表方式構建其中的邏輯。React
核心算法 React Fiber
的實現就是鏈表。
React Fiber
以前的Stack Reconciler
,是自頂向下的遞歸mount/update
,沒法中斷(持續佔用主線程),這樣主線程上的佈局、動畫等週期性任務以及交互響應就沒法當即獲得處理,影響體驗。React Fiber
解決過去Reconciler
存在的問題的思路是把渲染/更新過程(遞歸diff)拆分紅一系列小任務,每次檢查樹上的一小部分,作完看是否還有時間繼續下一個任務,有的話繼續,沒有的話把本身掛起,主線程不忙的時候再繼續。Express
這樣的Web
框架也以相似的方式構建其中間件邏輯。當請求被接收時,它從一箇中間件管道輸送到下一個,直到響應被髮出。單鏈表的操做核心有:
push(value)
- 在鏈表的末尾/頭部添加一個節點pop()
- 從鏈表的末尾/頭部刪除一個節點get(index)
- 返回指定索引處的節點delete(index)
- 刪除指定索引處的節點isEmpty()
- 根據列表長度返回true或falseprint()
- 返回鏈表的可見表示class Node { constructor(data) { this.data = data this.next = null } } class LinkedList { constructor() { this.head = null this.tail = null // 長度非必要 this.length = 0 } push(data) { // 建立一個新節點 const node = new Node(data) // 檢查頭部是否爲空 if (this.head === null) { this.head = node this.tail = node } this.tail.next = node this.tail = node this.length++ } pop(){ // 先檢查鏈表是否爲空 if(this.isEmpty()) { return null } // 若是長度爲1 if (this.head === this.tail) { this.head = null this.tail = null this.length-- return this.tail } let node = this.tail let currentNode = this.head let penultimate while (currentNode) { if (currentNode.next === this.tail) { penultimate = currentNode break } currentNode = currentNode.next } penultimate.next = null this.tail = penultimate this.length -- return node } get(index){ // 處理邊界條件 if (index === 0) { return this.head } if (index < 0 || index > this.length) { return null } let currentNode = this.head let i = 0 while(i < index) { i++ currentNode = currentNode.next } return currentNode } delete(index){ let currentNode = this.head if (index === 0) { let deletedNode currentNode.next = this.head deletedNode = currentNode this.length-- return deletedNode } if (index < 0 || index > this.length) { return null } let i = 0 let previous while(i < index) { i++ previous = currentNode currentNode = currentNode.next } previous.next = currentNode.next this.length-- return currentNode } isEmpty() { return this.length === 0 } print() { const list = [] let currentNode = this.head while(currentNode){ list.push(currentNode.data) currentNode = currentNode.next } return list.join(' => ') } }
測試一下:
const l = new LinkedList() // 添加節點 const values = ['A', 'B', 'C'] values.forEach(value => l.push(value)) console.log(l) console.log(l.pop()) console.log(l.get(1)) console.log(l.isEmpty()) console.log(l.print())
相似於單鏈表,雙向鏈表由一系列節點組成。每一個節點包含一些數據以及指向列表中下一個節點的指針和指向前一個節點的指針。這是JavaScript
中的簡單表示:
class Node { constructor(data) { // data 包含鏈表項應存儲的值 this.data = data; // next 是指向列表中下一項的指針 this.next = null; // prev 是指向列表中上一項的指針 this.prev = null; } }
仍是敲一遍吧:
class DoublyLinkedList { constructor() { this.head = null; this.tail = null; } // 各類操做方法 // ... }
Append & AppendAt
: 在鏈表的尾部/ 指定位置添加節點append( item ) { let node = new Node( item ); if(!this.head) { this.head = node; this.tail = node; } else { node.prev = this.tail; this.tail.next = node; this.tail = node } }
appendAt( pos, item ) { let current = this.head; let counter = 1; let node = new Node( item ); if( pos == 0 ) { this.head.prev = node node.next = this.head this.head = node } else { while(current) { current = current.next; if( counter == pos ) { node.prev = current.prev current.prev.next = node node.next = current current.prev = node } counter++ } } }
Remove & RemoveAt
: 在鏈表的尾部/ 指定位置刪除節點remove( item ) { let current = this.head; while( current ) { if( current.data === item ) { if( current == this.head && current == this.tail ) { this.head = null; this.tail = null; } else if ( current == this.head ) { this.head = this.head.next this.head.prev = null } else if ( current == this.tail ) { this.tail = this.tail.prev; this.tail.next = null; } else { current.prev.next = current.next; current.next.prev = current.prev; } } current = current.next } }
removeAt( pos ) { let current = this.head; let counter = 1; if( pos == 0 ) { this.head = this.head.next; this.head.prev = null; } else { while( current ) { current = current.next if ( current == this.tail ) { this.tail = this.tail.prev; this.tail.next = null; } else if( counter == pos ) { current.prev.next = current.next; current.next.prev = current.prev; break; } counter++; } } }
Reverse
: 翻轉雙向鏈表reverse(){ let current = this.head; let prev = null; while( current ){ let next = current.next current.next = prev current.prev = next prev = current current = next } this.tail = this.head this.head = prev }
Swap
:兩節點間交換。swap( nodeOne, nodeTwo ) { let current = this.head; let counter = 0; let firstNode; while( current !== null ) { if( counter == nodeOne ){ firstNode = current; } else if( counter == nodeTwo ) { let temp = current.data; current.data = firstNode.data; firstNode.data = temp; } current = current.next; counter++; } return true }
IsEmpty & Length
:查詢是否爲空或長度。length() { let current = this.head; let counter = 0; while( current !== null ) { counter++ current = current.next } return counter; } isEmpty() { return this.length() < 1 }
Traverse
: 遍歷鏈表traverse( fn ) { let current = this.head; while( current !== null ) { fn(current) current = current.next; } return true; }
<center>每一項都加10</center>
Search
:查找節點的索引。search( item ) { let current = this.head; let counter = 0; while( current ) { if( current.data == item ) { return counter } current = current.next counter++ } return false; }
Tree
計算機中常常用到的一種非線性的數據結構——樹(Tree),因爲其存儲的全部元素之間具備明顯的層次特性,所以常被用來存儲具備層級關係的數據,好比文件系統中的文件;也會被用來存儲有序列表等。
常見的樹分類以下,其中咱們掌握二叉搜索樹便可。
Binary Search Tree
AVL Tree
Red-Black Tree
Segment Tree
- with min/max/sum range queries examplesFenwick Tree
(Binary Indexed Tree
)DOM
樹。每一個網頁都有一個樹數據結構。
Vue
和React
的Virtual DOM
也是樹。
Binary Search Tree
按必定的規則和順序走遍二叉樹的全部結點,使每個結點都被訪問一次,並且只被訪問一次,這個操做被稱爲樹的遍歷,是對樹的一種最基本的運算。
因爲二叉樹是非線性結構,所以,樹的遍歷實質上是將二叉樹的各個結點轉換成爲一個線性序列來表示。
按照根節點訪問的順序不一樣,二叉樹的遍歷分爲如下三種:前序遍歷,中序遍歷,後序遍歷;
前序遍歷:Pre-Order
根節點->左子樹->右子樹
中序遍歷:In-Order
左子樹->根節點->右子樹
後序遍歷:Post-Order
左子樹->右子樹->根節點
所以咱們能夠得之上面二叉樹的遍歷結果以下:
class Node { constructor(data) { this.left = null this.right = null this.value = data } } class BST { constructor() { this.root = null } // 二叉樹的各類操做 // insert(value) {...} // insertNode(root, newNode) {...} // ...
insertNode
& insert
:插入新子節點/節點insertNode(root, newNode) { if (newNode.value < root.value) { // 先執行無左節點操做 (!root.left) ? root.left = newNode : this.insertNode(root.left, newNode) } else { (!root.right) ? root.right = newNode : this.insertNode(root.right, newNode) } } insert(value) { let newNode = new Node(value) // 若是沒有根節點 if (!this.root) { this.root = newNode } else { this.insertNode(this.root, newNode) } }
removeNode
& remove
:移除子節點/節點removeNode(root, value) { if (!root) { return null } // 從該值小於根節點開始判斷 if (value < root.value) { root.left = this.removeNode(root.left, value) return root } else if (value > root.value) { root.right = tis.removeNode(root.right, value) return root } else { // 若是沒有左右節點 if (!root.left && !root.right) { root = null return root } // 存在左節點 if (root.left) { root = root.left return root // 存在右節點 } else if (root.right) { root = root.right return root } // 獲取正確子節點的最小值以確保咱們有有效的替換 let minRight = this.findMinNode(root.right) root.value = minRight.value // 確保刪除已替換的節點 root.right = this.removeNode(root.right, minRight.value) return root } } remove(value) { if (!this.root) { return 'Tree is empty!' } else { this.removeNode(this.root, value) } }
findMinNode
:獲取子節點的最小值findMinNode(root) { if (!root.left) { return root } else { return this.findMinNode(root.left) } }
searchNode
& search
:查找子節點/節點searchNode(root, value) { if (!root) { return null } if (value < root.value) { return this.searchNode(root.left, value) } else if (value > root.value) { return this.searchNode(root.right, value) } return root } search(value) { if (!this.root) { return 'Tree is empty' } else { return Boolean(this.searchNode(this.root, value)) } }
Pre-Order
:前序遍歷preOrder(root) { if (!root) { return 'Tree is empty' } else { console.log(root.value) this.preOrder(root.left) this.preOrder(root.right) } }
In-Order
:中序遍歷inOrder(root) { if (!root) { return 'Tree is empty' } else { this.inOrder(root.left) console.log(root.value) this.inOrder(root.right) } }
Post-Order
:後序遍歷postOrder(root) { if (!root) { return 'Tree is empty' } else { this.postOrder(root.left) this.postOrder(root.right) console.log(root.value) } }
Graph
圖是由具備邊的節點集合組成的數據結構。圖能夠是定向的或不定向的。
圖的介紹普及,找了一圈文章,仍是這篇最佳:
Graphs—-A Visual Introduction for Beginners
在如下場景中,你都使用到了圖:
Google
,百度。LBS
地圖服務,如高德,谷歌地圖。Facebook
。
圖用於不一樣的行業和領域:
GPS
系統和谷歌地圖使用圖表來查找從一個目的地到另外一個目的地的最短路徑。Google
搜索算法使用圖 來肯定搜索結果的相關性。圖,能夠說是應用最普遍的數據結構之一,真實場景中到處有圖。
圖表用於表示,查找,分析和優化元素(房屋,機場,位置,用戶,文章等)之間的鏈接。
Node
,好比地鐵站中某個站/多個村莊中的某個村莊/互聯網中的某臺主機/人際關係中的人.Edge
,好比地鐵站中兩個站點之間的直接連線, 就是一個邊。
|V|
=圖中頂點(節點)的總數。|E|
=圖中的鏈接總數(邊)。在下面的示例中
|V| = 6 |E| = 7
圖根據其邊(鏈接)的特徵進行分類。
在有向圖中,邊具備方向。它們從一個節點轉到另外一個節點,而且沒法經過該邊返回到初始節點。
以下圖所示,邊(鏈接)如今具備指向特定方向的箭頭。 將這些邊視爲單行道。您能夠向一個方向前進併到達目的地,可是你沒法經過同一條街道返回,所以您須要找到另外一條路徑。
<center>有向圖</center>
在這種類型的圖中,邊是無向的(它們沒有特定的方向)。將無向邊視爲雙向街道。您能夠從一個節點轉到另外一個節點並返回相同的「路徑」。
在加權圖中,每條邊都有一個與之相關的值(稱爲權重)。該值用於表示它們鏈接的節點之間的某種可量化關係。例如:
著名的Dijkstra
算法,就是使用這些權重經過查找網絡中節點之間的最短或最優的路徑來優化路由。
當圖中的邊數接近最大邊數時,圖是密集的。
<center>密集圖</center>
當圖中的邊數明顯少於最大邊數時,圖是稀疏的。
<center>稀疏圖</center>
若是你按照圖中的一系列鏈接,可能會找到一條路徑,將你帶回到同一節點。這就像「走在圈子裏」,就像你在城市周圍開車同樣,你走的路能夠帶你回到你的初始位置。🚗
在圖中,這些「圓形」路徑稱爲「循環」。它們是在同一節點上開始和結束的有效路徑。例如,在下圖中,您能夠看到,若是從任何節點開始,您能夠經過跟隨邊緣返回到同一節點。
循環並不老是「孤立的」,由於它們能夠是較大圖的一部分。能夠經過在特定節點上開始搜索並找到將你帶回同一節點的路徑來檢測它們。
<center>循環圖</center>
咱們將實現具備鄰接列表的有向圖。
class Graph { constructor() { this.AdjList = new Map(); } // 基礎操做方法 // addVertex(vertex) {} // addEdge(vertex, node) {} // print() {} }
addVertex
:添加頂點addVertex(vertex) { if (!this.AdjList.has(vertex)) { this.AdjList.set(vertex, []); } else { throw 'Already Exist!!!'; } }
嘗試建立頂點:
let graph = new Graph(); graph.addVertex('A'); graph.addVertex('B'); graph.addVertex('C'); graph.addVertex('D');
打印後將會發現:
Map { 'A' => [], 'B' => [], 'C' => [], 'D' => [] }
之因此都爲空數組'[]'
,是由於數組中須要儲存邊(Edge
)的關係。
例以下圖:
該圖的Map
將爲:
Map { 'A' => ['B', 'C', 'D'], // B沒有任何指向 'B' => [], 'C' => ['B'], 'D' => ['C'] }
addEdge
:添加邊(Edge
)addEdge(vertex, node) { // 向頂點添加邊以前,必須驗證該頂點是否存在。 if (this.AdjList.has(vertex)) { // 確保添加的邊尚不存在。 if (this.AdjList.has(node)){ let arr = this.AdjList.get(vertex); // 若是都經過,那麼能夠將邊添加到頂點。 if(!arr.includes(node)){ arr.push(node); } }else { throw `Can't add non-existing vertex ->'${node}'`; } } else { throw `You should add '${vertex}' first`; } }
print
:打印圖(Graph
)print() { for (let [key, value] of this.AdjList) { console.log(key, value); } }
let g = new Graph(); let arr = ['A', 'B', 'C', 'D', 'E', 'F']; for (let i = 0; i < arr.length; i++) { g.addVertex(arr[i]); } g.addEdge('A', 'B'); g.addEdge('A', 'D'); g.addEdge('A', 'E'); g.addEdge('B', 'C'); g.addEdge('D', 'E'); g.addEdge('E', 'F'); g.addEdge('E', 'C'); g.addEdge('C', 'F'); g.print(); /* PRINTED */ // A [ 'B', 'D', 'E' ] // B [ 'C' ] // C [ 'F' ] // D [ 'E' ] // E [ 'F', 'C' ] // F []
到目前爲止,這就是建立圖所需的。可是,99%的狀況下,會要求你實現另外兩種方法:
BFS
。DFS
BFS
的重點在於隊列,而 DFS
的重點在於遞歸。這是它們的本質區別。廣度優先算法(Breadth-First Search),同廣度優先搜索。
是一種利用隊列實現的搜索算法。簡單來講,其搜索過程和 「湖面丟進一塊石頭激起層層漣漪」 相似。
如上圖所示,從起點出發,對於每次出隊列的點,都要遍歷其四周的點。因此說 BFS 的搜索過程和 「湖面丟進一塊石頭激起層層漣漪」 很類似,此即 「廣度優先搜索算法」 中「廣度」的由來。
該算法的具體步驟爲:
'A'
)visited
。q
,該數組將用做隊列。(visited = {'A': true})
(q = ['A'])
循環內部:
q
並將其存儲在變量中。(let current = q.pop())
current
current
的邊。(let arr = this.AdjList.get(current))
。visited = { 'A': true, 'B': true, 'D': true, 'E': true } q = ['B', 'D', 'E']
具體實現:
createVisitedObject(){ let arr = {}; for(let key of this.AdjList.keys()){ arr[key] = false; } return arr; } bfs(startingNode){ let visited = this.createVisitedObject(); let q = []; visited[startingNode] = true; q.push(startingNode); while(q.length){ let current = q.pop() console.log(current); let arr = this.AdjList.get(current); for(let elem of arr){ if(!visited[elem]){ visited[elem] = true; q.unshift(elem) } } } }
深度優先搜索算法(Depth-First-Search,縮寫爲 DFS),是一種利用遞歸實現的搜索算法。簡單來講,其搜索過程和 「不撞南牆不回頭」 相似。
如上圖所示,從起點出發,先把一個方向的點都遍歷完纔會改變方向...... 因此說,DFS 的搜索過程和 「不撞南牆不回頭」 很類似,此即 「深度優先搜索算法」 中「深度」的由來。
該算法的前期步驟和BFS類似,接受起始節點並跟蹤受訪節點,最後執行遞歸的輔助函數。
具體步驟:
dfs(startingNode)
。let visited = this.createVisitedObject()
。this.dfsHelper(startingNode, visited)
。dfsHelper
將其標記爲已訪問並打印出來。createVisitedObject(){ let arr = {}; for(let key of this.AdjList.keys()){ arr[key] = false; } return arr; } dfs(startingNode){ console.log('\nDFS') let visited = this.createVisitedObject(); this.dfsHelper(startingNode, visited); } dfsHelper(startingNode, visited){ visited[startingNode] = true; console.log(startingNode); let arr = this.AdjList.get(startingNode); for(let elem of arr){ if(!visited[elem]){ this.dfsHelper(elem, visited); } } } doesPathExist(firstNode, secondNode){ let path = []; let visited = this.createVisitedObject(); let q = []; visited[firstNode] = true; q.push(firstNode); while(q.length){ let node = q.pop(); path.push(node); let elements = this.AdjList.get(node); if(elements.includes(secondNode)){ console.log(path.join('->')) return true; }else{ for(let elem of elements){ if(!visited[elem]){ visited[elem] = true; q.unshift(elem); } } } } return false; } }
Vans
,下一個。
Trie
Trie
(一般發音爲「try」)是針對特定類型的搜索而優化的樹數據結構。當你想要獲取部分值並返回一組可能的完整值時,可使用Trie
。典型的例子是自動完成。
Trie
,是一種搜索樹,也稱字典樹或單詞查找樹,此外也稱前綴樹,由於某節點的後代存在共同的前綴。
它的特色:
O(k)
,k爲字符串長度例如:
搜索前綴「b」的匹配將返回6個值:be
,bear
,bell
,bid
,bull
,buy
。
搜索前綴「be
」的匹配將返回2個值:bear,bell
只要你想要將前綴與可能的完整值匹配,就可使用Trie
。
現實中多運用在:
也能夠運用在:
class PrefixTreeNode { constructor(value) { this.children = {}; this.endWord = null; this.value = value; } } class PrefixTree extends PrefixTreeNode { constructor() { super(null); } // 基礎操做方法 // addWord(string) {} // predictWord(string) {} // logAllWords() {} }
addWord
: 建立一個節點addWord(string) { const addWordHelper = (node, str) => { if (!node.children[str[0]]) { node.children[str[0]] = new PrefixTreeNode(str[0]); if (str.length === 1) { node.children[str[0]].endWord = 1; } else if (str.length > 1) { addWordHelper(node.children[str[0]], str.slice(1)); } }; addWordHelper(this, string); }
predictWord
:預測單詞即:給定一個字符串,返回樹中以該字符串開頭的全部單詞。
predictWord(string) { let getRemainingTree = function(string, tree) { let node = tree; while (string) { node = node.children[string[0]]; string = string.substr(1); } return node; }; let allWords = []; let allWordsHelper = function(stringSoFar, tree) { for (let k in tree.children) { const child = tree.children[k] let newString = stringSoFar + child.value; if (child.endWord) { allWords.push(newString); } allWordsHelper(newString, child); } }; let remainingTree = getRemainingTree(string, this); if (remainingTree) { allWordsHelper(string, remainingTree); } return allWords; }
logAllWords
:打印全部的節點logAllWords() { console.log('------ 全部在字典樹中的節點 -----------') console.log(this.predictWord('')); }
logAllWords
,經過在空字符串上調用predictWord
來打印Trie
中的全部節點。
Hash Tables
使用哈希表能夠進行很是快速的查找操做。可是,哈希表到底是什麼玩意兒?
不少語言的內置數據結構像python
中的字典,java
中的HashMap
,都是基於哈希表實現。但哈希表到底是啥?
散列(hashing)是電腦科學中一種對資料的處理方法,經過某種特定的函數/算法(稱爲散列函數/算法)將要檢索的項與用來檢索的索引(稱爲散列,或者散列值)關聯起來,生成一種便於搜索的數據結構(稱爲散列表)。也譯爲散列。舊譯哈希(誤覺得是人名而採用了音譯)。它也經常使用做一種資訊安全的實做方法,由一串資料中通過散列算法(
Hashing algorithms
)計算出來的資料指紋(data fingerprint
),常常用來識別檔案與資料是否有被竄改,以保證檔案與資料確實是由原創者所提供。 —-Wikipedia
Hash Tables
優化了鍵值對的存儲。在最佳狀況下,哈希表的插入,檢索和刪除是恆定時間。哈希表用於存儲大量快速訪問的信息,如密碼。
哈希表能夠概念化爲一個數組,其中包含一系列存儲在對象內部子數組中的元組:
{[[['a',9],['b',88]],[['e',7],['q',8]],[['j',7],['l ',8]]]};
這裏我就嘗試以大白話形式講清楚基礎的哈希表知識:
散列是一種用於從一組類似對象中惟一標識特定對象的技術。咱們生活中如何使用散列的一些例子包括:
在這兩個例子中,學生和書籍都被分紅了一個惟一的數字。
假設有一個對象,你想爲其分配一個鍵以便於搜索。要存儲鍵/值對,您可使用一個簡單的數組,如數據結構,其中鍵(整數)能夠直接用做存儲值的索引。
可是,若是密鑰很大而且沒法直接用做索引,此時就應該使用散列。
具體步驟以下:
O(1)
時間內訪問該元素。具體執行分兩步:
hash = hashfunc(key) index = hash % array_size
在此方法中,散列與數組大小無關,而後經過使用運算符(%)將其縮減爲索引(介於0
和array_size之間的數字 - 1
)。
要實現良好的散列機制,須要具備如下基本要求:
注意:不管散列函數有多健壯,都必然會發生衝突。所以,爲了保持哈希表的性能,經過各類衝突解決技術來管理衝突是很重要的。
假設您必須使用散列技術{「abcdef」,「bcdefa」,「cdefab」,「defabc」}
等字符串存儲在散列表中。
首先是創建索引:
a,b,c,d,e
和f
的ASCII
值分別爲97,98,99,100,101
和102
,總和爲:597
597
不是素數,取其附近的素數599
,來減小索引不一樣字符串(衝突)的可能性。哈希函數將爲全部字符串計算相同的索引,而且字符串將如下格式存儲在哈希表中。
因爲全部字符串的索引都相同,此時全部字符串都在同一個「桶」中。
O(n)
時間(其中n是字符串數)。如何優化這個哈希函數?
注意觀察這些字符串的異同
{「abcdef」,「bcdefa」,「cdefab」,「defabc」}
a,b,c,d,e
和f
組成來嘗試不一樣的哈希函數。
2069
(素數)取餘。字符串哈希函數索引
字符串 | 索引生成 | 計算值 |
---|---|---|
abcdef | (97 1 + 98 2 + 99 3 + 100 4 + 101 5 + 102 6)%2069 | 38 |
bcdefa | (98 1 + 99 2 + 100 3 + 101 4 + 102 5 + 97 6)%2069 | 23 |
cdefab | (99 1 + 100 2 + 101 3 + 102 4 + 97 5 + 98 6)%2069 | 14 |
defabc | (100 1 + 101 2 + 102 3 + 97 4 + 98 5 + 99 6)%2069 | 11 |
在合理的假設下,在哈希表中搜索元素所需的平均時間應是O(1)。
class Node { constructor( data ){ this.data = data; this.next = null; } } class HashTableWithChaining { constructor( size = 10 ) { this.table = new Array( size ); } // 操做方法 // computeHash( string ) {...} // ... }
isPrime
:素數判斷isPrime( num ) { for(let i = 2, s = Math.sqrt(num); i <= s; i++) if(num % i === 0) return false; return num !== 1; }
computeHash|findPrime
:哈希函數生成computeHash( string ) { let H = this.findPrime( this.table.length ); let total = 0; for (let i = 0; i < string.length; ++i) { total += H * total + string.charCodeAt(i); } return total % this.table.length; } // 取模 findPrime( num ) { while(true) { if( this.isPrime(num) ){ break; } num += 1 } return num; }
Put
:插入值put( item ) { let key = this.computeHash( item ); let node = new Node(item) if ( this.table[key] ) { node.next = this.table[key] } this.table[key] = node }
Remove
:刪除值remove( item ) { let key = this.computeHash( item ); if( this.table[key] ) { if( this.table[key].data === item ) { this.table[key] = this.table[key].next } else { let current = this.table[key].next; let prev = this.table[key]; while( current ) { if( current.data === item ) { prev.next = current.next } prev = current current = current.next; } } } }
contains
:判斷包含contains(item) { for (let i = 0; i < this.table.length; i++) { if (this.table[i]) { let current = this.table[i]; while (current) { if (current.data === item) { return true; } current = current.next; } } } return false; }
Size & IsEmpty
:判斷長度或空size( item ) { let counter = 0 for(let i=0; i<this.table.length; i++){ if( this.table[i] ) { let current = this.table[i] while( current ) { counter++ current = current.next } } } return counter } isEmpty() { return this.size() < 1 }
Traverse
:遍歷traverse( fn ) { for(let i=0; i<this.table.length; i++){ if( this.table[i] ) { let current = this.table[i]; while( current ) { fn( current ); current = current.next; } } } }
仍是和麪試有關。雖然leetcode
上的題刷過一些,但由於缺少對數據結構的總體認知。不少時候被問到或考到,會無所下手。
網上的帖子大多深淺不一,寫這篇的過程當中翻閱了大量的資料和示例。在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出。
參考:
- DS with JS - Hash Tables— I
- Joseph Crick - Practical Data Structures for Frontend Applications: When to use Tries
- [Thon Ly - Data Structures in JavaScript