前言
【從蛋殼到滿天飛】JS 資料結構解析和演算法實現,全部文章大概的內容如下: Arrays(陣列)、Stacks(棧)、Queues(佇列)、LinkedList(連結串列)、Recursion(遞迴思想)、BinarySearchTree(二分搜尋樹)、Set(集合)、Map(對映)、Heap(堆)、PriorityQueue(優先佇列)、SegmentTree(線段樹)、Trie(字典樹)、UnionFind(並查集)、AVLTree(AVL 平衡樹)、RedBlackTree(紅黑平衡樹)、HashTable(雜湊表)
原始碼有三個:ES6(單個單個的 class 型別的 js 檔案) | JS + HTML(一個 js 配合一個 html)| JAVA (一個一個的工程)
全部原始碼已上傳 github,點選我吧,光看文章能夠掌握兩成,動手敲程式碼、動腦思考、畫圖才可以掌握八成。
本文章適合 對資料結構想了解並且感興趣的人群,文章風格一如既往如此,就覺得手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加原始碼,時間跨度也算將近半年時間了,希望對想學習資料結構的人或者正在學習資料結構的人群有幫助。
集合和對映 Set And Map
- 集合和對映是高層的資料結構,
- 高層的資料結構還有棧和佇列,
- 這種資料結構更像是定義好了這種資料結構的相應的使用介面,
- 有了這些使用的介面包括這些資料結構本身所維持的一些性質,
- 就可以非常容易的把它們放入一些具體的應用中,
- 但是底層實現可以是多種多樣的,
- 比如棧和佇列的底層實現即可以是動態陣列也可以是連結串列,
- 集合 Set 和對映 Map 也是類似這樣的資料結構。
集合-基於二分搜尋樹的實現
- 集合就是承載元素的一個容器
- 在集合中有一個非常重要的特點,
- 也就是每個元素只能存在一次,
- 在具體應用的時候需要這樣的資料結構,
- 它能夠幫助你非常快速的進行去重這個工作,
- 去重指的是去除所有重複的元素,讓所有的元素只保留一份,
- 例如你想統計一個飯館有多少位會員,
- 這時候你就需要進行一個去重的操作,會員不能夠重複,
- 無論是新客戶還是老客戶都只能手持一張會員卡。
- 在二分搜尋樹的新增操作的時候
- 最開始實現的時候是不能盛放重複元素的,
- 所以這個二分搜尋樹本身
- 就是一個非常好的實現“集合”的底層資料結構,
集合介面
MySet
void add (e)
: 不能新增重複元素void remove (e)
boolean conatains (e)
int getSize ()
boolean isEmpty ()
- 使用 MyBSTSet 來實現這個集合的介面
集合的應用
- 典型的應用:用於客戶的統計
- 如你做一個網站,對訪問的 ip 進行一個統計,
- 不僅要關注總訪問量,還要關注有多少不同的 ip 訪問,
- 或者今天跟昨天相比又有多少個新的 ip 來訪問,
- 在這種時候就應該使用集合這種資料結構來做統計。
- 典型的應用:詞彙量的統計
- 在進行英文閱讀的時候你會去參考,這本書的詞彙量究竟有多少,
- 對於一本書的詞彙量來說,相同的單詞是隻記一次的,
- 在這種時候就應該使用集合這種資料結構來做統計。
程式碼示例
-
(class: MyBinarySearchTree, class: MyBSTSet, class: Main)
-
MyBinarySearchTree
// 自定義二分搜尋樹節點 class MyBinarySearchTreeNode { constructor(element, left = null, right = null) { // 實際儲存的元素 this.element = element; // 當前節點的左子樹 this.left = left; // 當前節點的右子樹 this.right = right; } } // 自定義二分搜尋樹 class MyBinarySearchTree { constructor() { this.root = null; this.size = 0; } // 新增元素到二分搜尋樹中 + add(element) { if (element === null) throw new Error("element is null. can't store."); this.root = this.recursiveAdd(this.root, element); } // 新增元素到二分搜尋樹中 遞迴演算法 - recursiveAdd(node, newElement) { // 解決最基本的問題 也就是遞迴函式呼叫的終止條件 if (node === null) { this.size++; return new MyBinarySearchTreeNode(newElement); } // 1. 當前節點的元素比新元素大 // 那麼新元素就會被新增到當前節點的左子樹去 // 2. 當前節點的元素比新元素小 // 那麼新元素就會被新增到當前節點的右子樹去 // 3. 當前節點的元素比新元素相等 // 什麼都不做了,因為目前不新增重複的元素 if (this.compare(node.element, newElement) > 0) node.left = this.recursiveAdd(node.left, newElement); else if (this.compare(node.element, newElement) < 0) node.right = this.recursiveAdd(node.right, newElement); else { } // 將複雜問題分解成多個性質相同的小問題, // 然後求出小問題的答案, // 最終構建出原問題的答案 return node; } // 判斷二分搜尋樹中是否包含某個元素 + contains(element) { if (this.root === null) throw new Error("root is null. can't query."); return this.recursiveContains(this.root, element); } // 判斷二分搜尋樹種是否包含某個元素 遞迴演算法 - recursiveContains(node, element) { if (node === null) return false; // 當前節點元素比 要搜尋的元素 大 if (this.compare(node.element, element) > 0) return this.recursiveContains(node.left, element); else if (this.compare(node.element, element) < 0) // 當前元素比 要搜尋的元素 小 return this.recursiveContains(node.right, element); // 兩個元素相等 else return true; } // 找到二分搜尋樹中的最大值的元素 + maximum() { if (this.size === 0) throw new Error('binary search tree is empty.'); return this.recursiveMaximum(this.root).element; } // 找到二分搜尋樹中的最大值的元素的節點 遞迴演算法 - recursiveMaximum(node) { // 解決最基本的問題 向右走再也走不動了,說明當前節點就是最大值節點。 if (node.right === null) return node; return this.recursiveMaximum(node.right); } // 刪除二分搜尋樹中最大值的元素的節點,並返回這個節點的元素 + removeMax() { let maxElement = this.maximum(); this.root = this.recursiveRemoveMax(this.root); return maxElement; } // 刪除二分搜尋樹中最大值的元素的節點,並返回這個節點 遞迴演算法 - recursiveRemoveMax(node) { if (node.right === null) { // 先存 當前這個節點的左子樹, // 因為可能當前這個節點僅僅沒有右子樹,只有左子樹, // 那麼左子樹可以替代當前這個節點。 let leftNode = node.left; node.left = null; this.size--; return leftNode; } node.right = this.recursiveRemoveMax(node.right); return node; } // 找到二分搜尋樹中的最小值 + minimum() { if (this.size === 0) throw new Error('binary search tree is empty.'); return this.recursiveMinimum(this.root).element; } // 找到二分搜尋樹中的最小值的元素的節點 遞迴演算法 - recursiveMinimum(node) { if (node.left === null) return node; return this.recursiveMinimum(node.left); } // 刪除二分搜尋樹中最小值的元素的節點,並返回這個節點的元素 + removeMin() { let leftNode = this.minimum(); this.root = this.recursiveRemoveMin(this.root); return leftNode; } // 刪除二分搜尋樹中最小值的元素的節點,並返回這個節點 遞迴演算法 - recursiveRemoveMin(node) { // 解決最簡單的問題 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } // 將複雜的問題拆分為性質相同的小問題, // 然後求出這些小問題的解後構建出原問題的答案 node.left = this.recursiveRemoveMin(node.left); return node; } // 刪除二分搜尋樹上的任意節點 remove(element) { this.root = this.recursiveRemove(this.root, element); } // 刪除二分搜尋樹上的任意節點 遞迴演算法 // 返回刪除對應元素節點後新的二分搜尋樹的根 recursiveRemove(node, element) { if (node === null) return null; // 當前節點的元素值比待刪除的元素小 那麼就向當前節點的右子樹中去找 if (this.compare(node.element, element) < 0) { node.right = this.recursiveRemove(node.right, element); return node; } else if (this.compare(node.element, element) > 0) { // 向當前節點的左子樹中去找 node.left = this.recursiveRemove(node.left, element); return node; } else { // 如果找到了相同值的節點了,開始進行相應的處理 // 如果這個節點左子樹為空,那麼就讓這個節點的右子樹覆蓋當前節點 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } // 如果當前節點的右子樹為空,那麼就讓這個節點的左子樹覆蓋當前節點 if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } // 如果當前節點的左右子樹都不為空,那麼就開始特殊操作 // 1. 先找到當前節點右子樹上最小的那個節點,儲存起來 // 2. 然後刪除掉當前節點右子樹上最小的那個節點, // 3. 讓儲存起來的那個節點覆蓋掉當前節點 // 1. 也就是儲存起來的那個節點的right = 刪除掉當前節點右子樹上最小的節點後返回的那個節點 // 2. 再讓儲存起來的那個節點的left = 當前節點的left // 4. 解除當前節點及其left和right,全都賦值為null,這樣就相當於把當前節點從二分搜尋樹中剔除了 // 5. 返回儲存的這個節點 let successtor = this.recursiveMinimum(node.right); successtor.right = this.recursiveRemoveMin(node.right); // 恢復removeMin 操作的this.size -- 帶來的影響 this.size++; successtor.left = node.left; // 開始正式的刪除當前節點的操作 node = node.left = node.right = null; this.size--; // 返回當前儲存的節點 return successtor; } } // 前序遍歷 + preOrder(operator) { this.recursivePreOrder(this.root, operator); } // 前序遍歷 遞迴演算法 - recursivePreOrder(node, operator) { if (node === null) return; // 呼叫一下操作方法 operator(node.element); console.log(node, node.element); // 繼續遞迴遍歷左右子樹 this.recursivePreOrder(node.left, operator); this.recursivePreOrder(node.right, operator); } // 前序遍歷 非遞迴演算法 + nonRecursivePreOrder(operator) { let stack = new MyLinkedListStack(); stack.push(this.root); let node = null; while (!stack.isEmpty()) { // 出棧操作 node = stack.pop(); operator(node.element); // 訪問當前的節點 console.log(node.element); // 棧是先入後出的,把需要後訪問的節點 先放進去,先訪問的節點後放進去 // 前序遍歷是訪問當前節點,然後再遍歷左子樹,最後遍歷右子樹 if (node.right !== null) stack.push(node.right); if (node.left !== null) stack.push(node.left); } } // 中序遍歷 + inOrder(operator) { this.recursiveInOrder(this.root, operator); } // 中序遍歷 遞迴演算法 - recursiveInOrder(node, operator) { if (node == null) return; this.recursiveInOrder(node.left, operator); operator(node.element); console.log(node.element); this.recursiveInOrder(node.right, operator); } // 後序遍歷 + postOrder(operator) { this.recursivePostOrder(this.root, operator); } // 後序遍歷 遞迴演算法 - recursivePostOrder(node, operator) { if (node == null) return; this.recursivePostOrder(node.left, operator); this.recursivePostOrder(node.right, operator); operator(node.element); console.log(node.element); } // 層序遍歷 levelOrder(operator) { let queue = new MyLinkedListQueue(); queue.enqueue(this.root); let node = null; while (!queue.isEmpty()) { node = queue.dequeue(); operator(node.element); console.log(node.element); // 佇列 是先進先出的,所以從左往右入隊 // 棧 是後進先出的, 所以從右往左入棧 if (node.left !== null) queue.enqueue(node.left); if (node.right !== null) queue.enqueue(node.right); } } // 獲取二分搜尋樹中節點個數 + getSize() { return this.size; } // 返回二分搜尋樹是否為空的bool值 + isEmpty() { return this.size === 0; } // 新增一個比較的方法,專門用來比較新增的元素大小 - // 第一個元素比第二個元素大 就返回 1 // 第一個元素比第二個元素小 就返回 -1 // 第一個元素比第二個元素相等 就返回 0 compare(elementA, elementB) { if (elementA === null || elementB === null) throw new Error("element is null. can't compare."); // 先直接寫死 if (elementA > elementB) return 1; else if (elementA < elementB) return -1; else return 0; } // 輸出二分搜尋樹中的資訊 // @Override toString 2018-11-03-jwl toString() { let treeInfo = ''; treeInfo += this.getBinarySearchTreeString(this.root, 0, treeInfo); return treeInfo; } // 寫一個輔助函式,用來生成二分搜尋樹資訊的字串 getBinarySearchTreeString(node, depth, treeInfo, pageContent = '') { //以前序遍歷的方式 if (node === null) { treeInfo += this.getDepthString(depth) + 'null \r\n'; pageContent = this.getDepthString(depth) + 'null<br /><br />'; document.body.innerHTML += `${pageContent}`; return treeInfo; } treeInfo += this.getDepthString(depth) + node.element + '\r\n'; pageContent = this.getDepthString(depth) + node.element + '<br /><br />'; document.body.innerHTML += `${pageContent}`; treeInfo = this.getBinarySearchTreeString( node.left, depth + 1, treeInfo ); treeInfo = this.getBinarySearchTreeString( node.right, depth + 1, treeInfo ); return treeInfo; } // 寫一個輔助函式,用來生成遞迴深度字串 getDepthString(depth) { let depthString = ''; for (var i = 0; i < depth; i++) { depthString += '-- '; } return depthString; } } 複製程式碼
-
MyBSTSet
// 自定義二分搜尋樹集合Set class MyBinarySearchTreeSet { constructor() { // 借用二分搜尋樹來實現這個介面 this.myBinarySearchTree = new MyBinarySearchTree(); } // 新增元素 add(element) { this.myBinarySearchTree.add(element); } // 移除元素 remove(element) { this.myBinarySearchTree.remove(element); } // 是否包含這個元素 contains(element) { return this.myBinarySearchTree.contains(element); } // 遍歷操作 // 第一個引數 是回掉函式, // 第二個引數 是遍歷的方式 深度優先遍歷(前pre、中in、後post),廣度優先遍歷(層序level) each(operator, method) { // 遍歷方式預設是非遞迴的前序遍歷, // 其它的遍歷方式就是遞迴的前、中、後、層序遍歷。 switch (method) { case 'pre': this.myBinarySearchTree.preOrder(operator); break; case 'in': this.myBinarySearchTree.inOrder(operator); break; case 'post': this.myBinarySearchTree.postOrder(operator); break; case 'level': this.myBinarySearchTree.levelOrder(operator); break; default: this.myBinarySearchTree.nonRecursivePreOrder(operator); break; } } // 獲取集合中實際的元素個數 getSize() { return this.myBinarySearchTree.getSize(); } // 返回集合是否為空的bool值 isEmpty() { return this.myBinarySearchTree.isEmpty(); } } 複製程式碼
-
Main
// main 函式 class Main { constructor() { this.alterLine('MyBinarySearchTreeSet Area'); { let n = 5; let set = new MyBinarySearchTreeSet(); let random = Math.random; let temp = null; for (var i = 0; i < n; i++) { temp = random(); set.add(n * n * n * temp); set.add(n * n * n * temp); set.add(n * n * n * temp); set.add(n * n * n * temp); set.add(n * n * n * temp); set.add(n * n * n * temp); set.add(n * n * n * temp); } console.log(set.getSize()); this.show(set.getSize()); let array = new MyArray(n); set.each(element => { console.log(element); this.show(element); array.add(element); }); for (var i = 0; i < array.getSize(); i++) { set.remove(array.get(i)); } console.log(set.getSize()); this.show(set.getSize()); } } // 將內容顯示在頁面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割線 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } // 頁面載入完畢 window.onload = function() { // 執行主函式 new Main(); }; 複製程式碼
集合-基於連結串列的實現
-
集合設計的是一個介面,
- 所以可以採用不同的底層資料結構來進行實現它,
- 和棧和佇列一樣,
- 可以使用底層的資料結構動態陣列和連結串列來實現它,
- 那麼也可以通過連結串列來實現集合。
-
二分搜尋樹和連結串列都屬於動態資料結構
- 對於二分搜尋樹來說資料都是儲存在一個一個 node 中,
- 連結串列也是把資料儲存到一個一個的 node 中,
- 只不過這兩個 node 的定義是不同的,
- 對於二分搜尋樹來說有左右兩個指標來指向左子樹和右子樹,
- 而對於連結串列來說每一個 node 都指向了下一個 node,
- 由於它們同樣是動態資料結構,
- 所以可以基於這兩種資料結構為底層實現這個集合,
- 還可以相應的進行比較這兩種資料結構實現後的效能,
- 通過它們的效能比較可以看出二分搜尋樹這種資料結構的優勢所在。
// 二分搜尋樹的Node class Node { e; // Element left; // Node right; // Node } // 連結串列的Node class Node { e; // Element next; // Node } 複製程式碼
集合介面
MySet
void add (e)
: 不能新增重複元素void remove (e)
boolean conatains (e)
int getSize ()
boolean isEmpty ()
- 使用 MyLinkedListSet 來實現這個集合的介面
程式碼示例
-
( class: MyLinkedList, class: MyLinkedListSet)
-
MyLinkedList
// 自定義連結串列節點 class MyLinkedListNode { constructor(element = null, next = null) { this.element = element; this.next = next; } // 將一個陣列物件 轉換為一個連結串列 並且追加到當前節點上 appendToLinkedListNode(array) { let head = null; if (this.element === null) { // 頭部新增 head = this; head.element = array[0]; head.next = null; } else { // 插入式 head = new MyLinkedListNode(array[0], null); head.next = this.next; this.next = head; } // 新增節點的方式 頭部新增、尾部新增、中間插入 // 尾部新增節點的方式 for (var i = 1; i < array.length; i++) { head.next = new MyLinkedListNode(array[i], null); head = head.next; } } //@override // toString 2018-10-20-jwl toString() { return this.element.toString(); } } // 自定義連結串列 class MyLinkedList { constructor() { this.dummyHead = new MyLinkedListNode(null, null); this.size = 0; } // 獲取連結串列中實際的節點個數 getSize() { return this.size; } // 判斷連結串列是否為空 isEmpty() { return this.size === 0; } // 在連結串列頭新增節點 addFirst(element) { // let node = new MyLinkedListNode(element, null); // node.next = this.head; // this.head = node; // this.size ++; // 改用虛擬頭節點 this.insert(0, element); } // 在連結串列指定索引處插入節點 insert(index, element) { if (index < 0 || index > this.size) { throw new Error('add error. index < 0 or index > size'); } // 第一個prev就是dummyHead let prev = this.dummyHead; // 之前變數i(索引)之所以要從 1 開始,因為索引為0的那個節點就是head,迴圈就不需要從0開始了, // 現在索引之所以要從 0 開始, 因為初始化時 多增加了一個虛擬的頭節點 // (因為這個索引為0的節點並不是dummyHead,dummyHead這個節點並不記錄為連結串列中的實際節點), // 小於index是因為要找到指定索引位置的前一個節點 // 迴圈是因為 要繼續找到指定索引處的節點的前一個節點 for (var i = 0; i < index; i++) { // 不停的切換引用,直到找到對應索引處節點的下一個節點 prev = prev.next; } let node = new MyLinkedListNode(element, null); node.next = prev.next; prev.next = node; this.size++; } // 擴充套件:在連結串列最後一個節點的位置新增節點 addLast(element) { this.insert(this.size, element); } // 獲取指定索引位置的元素 get(index) { // 判斷索引合法性 if (index < 0 || index >= this.size) { throw new Error('get error. index < 0 or index >= size'); } // 如果你要找指定索引節點的前一個節點 就使用dummyHead // 如果你要找到指定索引節點 就使用dummyHead.next // 因為duumyHead並不是第一個節點,因為它是一個虛擬節點, // dummyHead.next才是真正被記錄的第一個節點。 let node = this.dummyHead.next; for (var i = 0; i < index; i++) { node = node.next; } return node.element; } // 獲取頭節點的元素 getFirst() { return this.get(0); } // 獲取尾節點的元素 getLast() { return this.get(this.size - 1); } // 設定指定索引位置的元素值 set(index, element) { // 判斷索引合法性 if (index < 0 || index >= this.size) { throw new Error('get error. index < 0 or index >= size'); } // 從第一個真正被記錄的節點開始,從0開始 let node = this.dummyHead.next; // 索引為 0 時,實際上切換到的節點 它的索引為 1 // i < index ,當索引為 index-1 時, 實際上切換到的節點 它的索引為index for (let i = 0; i < index; i++) { // 每一次切換 都只是改變引用 // 不的在連結串列中找下一個節點 node = node.next; } node.element = element; } // 所有節點中是否有包含該元素 contains(element) { let node = this.dummyHead; while (node.next !== null) { if (node.next.element === element) return true; // 不停的向下切換 node = node.next; } return false; } // 刪除指定索引位置的節點 remove(index) { // 驗證索引的合法性 if (index < 0 || index >= this.size) { throw new Error('remove error. index < 0 or index > this.size'); } let node = this.dummyHead; for (let i = 0; i < index; i++) { node = node.next; } // 待刪除的節點 let delNode = node.next; // 給待刪除那個節點的前一個的節點的next引用替換為 // 但刪除的這個節點的next node.next = delNode.next; // 或者這樣也行 // node.next = node.next.next; // 臨時儲存待刪除的那個節點裡的元素 let element = delNode.element; // 清空 待刪除的節點 delNode = null; this.size--; return element; } // 擴充套件:移除連結串列頭的元素 removeFirst() { return this.remove(0); } // 擴充套件:移除連結串列尾部的元素 removeLast() { return this.remove(this.size - 1); } // 輸出連結串列中的資訊 // @Override toString 2018-10-21-jwl toString() { let arrInfo = `LinkedList: size = ${this.size},\n`; arrInfo += `data = front [`; let node = this.dummyHead.next; while (node.next !== null) { arrInfo += `${node.element}->`; node = node.next; } arrInfo += 'NULL] tail'; // 在頁面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 複製程式碼
-
MyLinkedListSet
// 自定義連結串列集合Set class MyLinkedListSet { // constructor() { this.myLinkedList = new MyLinkedList(); } add(element) { if (!this.myLinkedList.contains(element)) this.myLinkedList.addFirst(element); } remove(element) { this.myLinkedList.removeElement(element); } contains(element) { return this.myLinkedList.contains(element); } each(operator) { let size = this.myLinkedList.getSize(); for (var i = 0; i < size; i++) { operator(this.myLinkedList.get(i)); } } getSize() { return this.myLinkedList.getSize(); } isEmpty() { return this.myLinkedList.isEmpty(); } } 複製程式碼
集合類的複雜度分析
- 實現了兩個基於動態資料結構的集合類
- 基於二分搜尋樹的集合類 MyBSTSet,
- 基於連結串列的集合類 MyLinkedListSet,
- 基於連結串列的集合類效能要差一些。
- 集合的時間複雜度分析
- 增加 add
- 查詢 contains
- 刪除 remove
- MyLinkedListSet 與 MyBSTSet 時間複雜度對比
- MyLinkedListSet 的時間複雜度為
O(n)
, - MyBSTSet 的時間複雜度為
O(h) or O(log n)
, h = log2 (n+1) = O(log2 n)
,- h 和 n 之間成一個 log 關係,log 以 2 為底的(n+1),
- 通常稱它們之間的關係為
O(log2 n)
- 也就是大 O log 以 2 為底 n 這樣的一個關係,
- 在大 O 這樣的一個定義下,這個底的大小可以忽略不計,
- 因為認為常數不重要,
- 以 2 為底、以 10 為底、以 100 為底,它都是一個 log 級別的函式,
- 就像看線性關係,前面的係數也是不關注的,
- 它是
1*n、2*n、100*n、10000*n
,它們都是線性的一個關係, - 所以這個關係可以寫成
O(log n)
。
- MyLinkedListSet 的時間複雜度為
- 二分搜尋樹的侷限性
- 雖然二分搜尋樹實現的集合時間複雜度為
O(log n)
, - 但是計算出這個時間複雜度是在滿二叉樹的情況下,
- 所以這個
O(log n)
其實是一個最優的情況, - 如果二叉樹稍微有一些傾斜,也能達到
O(log n)
這個級別, - 但是自己實現的二分搜尋樹有一個致命的問題,
- 它有最壞的情況,對於同樣的資料,可以建立出不同的二分搜尋樹。
- 例如每一個節點的左孩子都為空只有右孩子,
- 在這種情況下二分搜尋樹和一個連結串列是一樣的,
- 也就是說這棵二分搜尋樹的高度等於節點數,
- 這就是二分搜尋樹最壞的情況,例如按照順序新增
[1,2,3,4,5,6]
, - 就可以建立出一顆退化成連結串列的二分搜尋樹,
- 在大多數實際情況下 MyBSTSet 不會那麼奇怪的按照順序新增資料,
- 因為那樣會讓二分搜尋樹退化成連結串列,
- 但是也有可能你的二分搜尋樹就會遇到最差的情況或者接近最差的情況,
- 那麼此時你二分搜尋樹的效能近乎接近連結串列的效能,
- 這樣的效能其實也是
O(n)
這樣的效能, - 這也是你之前實現的二分搜尋樹的侷限性。
- 但是平均來講,大多數情況下它都能達到
O(log n)
的時間複雜度, - 但是依然會有特殊情況,最差的情況他會退化成和連結串列一樣的
O(n)
, - 所以就需要解決這個問題,
- 解決這個問題的方式就是建立所謂的平衡二叉樹。
- 所以非常準確的說二分搜尋樹的時間複雜度為
O(h)
, - 在大多數情況下這個 h 等於
log n
,如果非常的不巧,那麼這個 h 等於 n。
- 雖然二分搜尋樹實現的集合時間複雜度為
- logn 和 n 的差距
- 當
n=16
的時候,logn 讓它取 2 為底, - 相應的
log2 n
的值為 4,n 的值就是 16。 - 也就是說它們相差四倍,隨著 n 的增大,
- 它們之間的差距就會越來越大。
- 比如說
n=1024
的時候,logn 還是讓它取 2 為底, - 因為 2 的十次方為 1024,那麼相應的
log2 n
的值為 10, - n 的值就是 1024,在這種情況下,二者之間相差一百倍。
- 如果
n=100萬
這個級別的資料的話,logn 還是讓它取 2 為底, - 因為 2 的 20 次方大概就是 100 萬,那麼相應的
log2 n
的值為 20, - n 的值就是 100 萬,在這種情況下,二者之間相差 5 萬倍。
- 當
- 它們之間的差距非常非常大
- 比如你使用
O(log n)
這個演算法和O(n)
這個演算法, - 輸入的資料為 100 萬個,
- 如果你
O(log n)
這個演算法花一秒時間就跑完的話, - 那麼
O(n)
這個演算法就需要花 5 萬秒,大概 14 個小時, - 再比如你
O(log n)
這個演算法要花一天的時間跑完這個程式, - 那麼
O(n)
這個演算法就需要花 5 萬天,大概 137 年的事件才能跑出結果, - 這概念就像你睡一覺 24 小時後
O(log n)
演算法的程式就跑出結果了, - 而
O(n)
演算法的程式你這一輩子都跑不出結果來。
- 比如你使用
- logn 這個複雜度是非常快非常快的一個時間複雜度
- 很多高階的排序演算法最終它是 nlogn 這個時間複雜度,
- 這個時間複雜度比 n 方的時間複雜度快了非常多倍,
- 快的倍數與
O(log n)
和O(n)
差不多, - 雖然它們之間還有常數的差異,
- 但是在複雜度分析的時不去管這個常數的差異。
MyLinkedListSet
- 增加 add
O(n)
- 為了防止元素重複,所以必須先查詢一遍,
- 然後再決定新增不新增,雖然新增的複雜度為
O(1)
, - 但是查詢的操作是遍歷整個連結串列,所以整體時間複雜度為
O(n)
。
- 查詢 contains
O(n)
- 查詢的操作是遍歷整個連結串列,
- 所以時間複雜度為
O(n)
- 刪除 remove
O(n)
- 刪操作也需要遍歷整個連結串列,
- 所以時間複雜度為
O(n)
MyBSTSet
- 增加 add
O(h) or O(log n)
- 新增一個元素,
- 待新增的這個元素和根節點的這個元素進行比較,
- 如果小於的話直接去左子樹,如果大於的話直接去右子樹,
- 每一次近乎都能把一半兒的元素給扔掉,
- 新增這個元素這個過程其實就像是在走一個連結串列,
- 一層一層的從這個樹的根節點向葉子節點出發,
- 最終一共經歷的節點個數就是這棵樹的
高度
, - 也就是整棵書最大的深度,查詢元素也是如此,
- 刪除元素還是如此,所以對於二分搜尋樹來說,
- 這三個時間複雜度都是
O(h)
這個級別的, - 這個 h 就是二分搜尋樹的高度。
- 查詢 contains
O(h) or O(log n)
- 刪除 remove
O(h) or O(log n)
二分搜尋樹
- 滿的二叉樹每一層有多少個節點
- 第 0 層:1 個節點
- 第 1 層:2 個節點
- 第 2 層:4 個節點
- 第 3 層:8 個節點
- 第 4 層:16 個節點
- 第 h-1 層:2^(h-1)個節點
- 滿的二叉樹一共有多少個節點
- 可以通過等比數列來進行計算,
2^(1-1) + 2^(2-1) + ... + 2^(h-1)
= 1 x (1-2^(h)) / (1-2) = 2^(h) - 1 = n
。
- 滿的二叉樹的高度與節點個數之間的關係
h = log2 (n+1) = O(log2 n)
,- h 和 n 之間成一個 log 關係,log 以 2 為底的(n+1),
- 通常稱它們之間的關係為
O(log2 n)
- 也就是大 O log 以 2 為底 n 這樣的一個關係,
- 在大 O 這樣的一個定義下,這個底的大小可以忽略不計,
- 因為認為常數不重要,
- 以 2 為底、以 10 為底、以 100 為底,它都是一個 log 級別的函式,
- 就像看線性關係,前面的係數也是不關注的,
- 它是
1*n、2*n、100*n、10000*n
,它們都是線性的一個關係, - 所以這個關係可以寫成
O(log n)
。
- 二分搜尋樹這種底層的資料結構實現的集合
- 它效能是大大的快於連結串列實現的集合。
程式碼示例
-
(class: MyLinkedList, class: MyBinarySearchTree, class: PerformanceTest,
class: MyLinkedListSet, class:MyBSTSet , class: Main)
-
MyLinkedList
// 自定義連結串列節點 class MyLinkedListNode { constructor(element = null, next = null) { this.element = element; this.next = next; } // 將一個陣列物件 轉換為一個連結串列 並且追加到當前節點上 appendToLinkedListNode(array) { let head = null; if (this.element === null) { // 頭部新增 head = this; head.element = array[0]; head.next = null; } else { // 插入式 head = new MyLinkedListNode(array[0], null); head.next = this.next; this.next = head; } // 新增節點的方式 頭部新增、尾部新增、中間插入 // 尾部新增節點的方式 for (var i = 1; i < array.length; i++) { head.next = new MyLinkedListNode(array[i], null); head = head.next; } } //@override // toString 2018-10-20-jwl toString() { return this.element.toString(); } } // 自定義連結串列 class MyLinkedList { constructor() { this.dummyHead = new MyLinkedListNode(null, null); this.size = 0; } // 獲取連結串列中實際的節點個數 getSize() { return this.size; } // 判斷連結串列是否為空 isEmpty() { return this.size === 0; } // 在連結串列頭新增節點 addFirst(element) { // let node = new MyLinkedListNode(element, null); // node.next = this.head; // this.head = node; // this.size ++; // 改用虛擬頭節點 this.insert(0, element); } // 在連結串列指定索引處插入節點 insert(index, element) { if (index < 0 || index > this.size) { throw new Error('add error. index < 0 or index > size'); } // 第一個prev就是dummyHead let prev = this.dummyHead; // 之前變數i(索引)之所以要從 1 開始,因為索引為0的那個節點就是head,迴圈就不需要從0開始了, // 現在索引之所以要從 0 開始, 因為初始化時 多增加了一個虛擬的頭節點 // (因為這個索引為0的節點並不是dummyHead,dummyHead這個節點並不記錄為連結串列中的實際節點), // 小於index是因為要找到指定索引位置的前一個節點 // 迴圈是因為 要繼續找到指定索引處的節點的前一個節點 for (var i = 0; i < index; i++) { // 不停的切換引用,直到找到對應索引處節點的下一個節點 prev = prev.next; } let node = new MyLinkedListNode(element, null); node.next = prev.next; prev.next = node; this.size++; } // 擴充套件:在連結串列最後一個節點的位置新增節點 addLast(element) { this.insert(this.size, element); } // 獲取指定索引位置的元素 get(index) { // 判斷索引合法性 if (index < 0 || index >= this.size) { throw new Error('get error. index < 0 or index >= size'); } // 如果你要找指定索引節點的前一個節點 就使用dummyHead // 如果你要找到指定索引節點 就使用dummyHead.next // 因為duumyHead並不是第一個節點,因為它是一個虛擬節點, // dummyHead.next才是真正被記錄的第一個節點。 let node = this.dummyHead.next; for (var i = 0; i < index; i++) { node = node.next; } return node.element; } // 獲取頭節點的元素 getFirst() { return this.get(0); } // 獲取尾節點的元素 getLast() { return this.get(this.size - 1); } // 設定指定索引位置的元素值 set(index, element) { // 判斷索引合法性 if (index < 0 || index >= this.size) { throw new Error('get error. index < 0 or index >= size'); } // 從第一個真正被記錄的節點開始,從0開始 let node = this.dummyHead.next; // 索引為 0 時,實際上切換到的節點 它的索引為 1 // i < index ,當索引為 index-1 時, 實際上切換到的節點 它的索引為index for (let i = 0; i < index; i++) { // 每一次切換 都只是改變引用 // 不的在連結串列中找下一個節點 node = node.next; } node.element = element; } // 所有節點中是否有包含該元素 contains(element) { let node = this.dummyHead; while (node.next !== null) { if (node.next.element === element) return true; // 不停的向下切換 node = node.next; } return false; } // 刪除指定索引位置的節點 remove(index) { // 驗證索引的合法性 if (index < 0 || index >= this.size) { throw new Error('remove error. index < 0 or index > this.size'); } let node = this.dummyHead; for (let i = 0; i < index; i++) { node = node.next; } // 待刪除的節點 let delNode = node.next; // 給待刪除那個節點的前一個的節點的next引用替換為 // 但刪除的這個節點的next node.next = delNode.next; // 或者這樣也行 // node.next = node.next.next; // 臨時儲存待刪除的那個節點裡的元素 let element = delNode.element; // 清空 待刪除的節點 delNode = null; this.size--; return element; } // 擴充套件:移除連結串列頭的元素 removeFirst() { return this.remove(0); } // 擴充套件:移除連結串列尾部的元素 removeLast() { return this.remove(this.size - 1); } // 新增:根據元素來刪除連結串列中的元素 2018-11-05 removeElement(element) { let prev = this.dummyHead; while (prev.next !== null) { if (prev.next.element === element) break; prev = prev.next; } if (prev.next !== null) { let delNode = prev.next; prev.next = delNode.next; delNode = null; this.size--; } } // 輸出連結串列中的資訊 // @Override toString 2018-10-21-jwl toString() { let arrInfo = `LinkedList: size = ${this.size},\n`; arrInfo += `data = front [`; let node = this.dummyHead.next; while (node.next !== null) { arrInfo += `${node.element}->`; node = node.next; } arrInfo += 'NULL] tail'; // 在頁面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 複製程式碼
-
MyBinarySearchTree
// 自定義二分搜尋樹節點 class MyBinarySearchTreeNode { constructor(element, left = null, right = null) { // 實際儲存的元素 this.element = element; // 當前節點的左子樹 this.left = left; // 當前節點的右子樹 this.right = right; } } // 自定義二分搜尋樹 class MyBinarySearchTree { constructor() { this.root = null; this.size = 0; } // 新增元素到二分搜尋樹中 + add(element) { if (element === null) throw new Error("element is null. can't store."); this.root = this.recursiveAdd(this.root, element); } // 新增元素到二分搜尋樹中 遞迴演算法 - recursiveAdd(node, newElement) { // 解決最基本的問題 也就是遞迴函式呼叫的終止條件 if (node === null) { this.size++; return new MyBinarySearchTreeNode(newElement); } // 1. 當前節點的元素比新元素大 // 那麼新元素就會被新增到當前節點的左子樹去 // 2. 當前節點的元素比新元素小 // 那麼新元素就會被新增到當前節點的右子樹去 // 3. 當前節點的元素比新元素相等 // 什麼都不做了,因為目前不新增重複的元素 if (this.compare(node.element, newElement) > 0) node.left = this.recursiveAdd(node.left, newElement); else if (this.compare(node.element, newElement) < 0) node.right = this.recursiveAdd(node.right, newElement); else { } // 將複雜問題分解成多個性質相同的小問題, // 然後求出小問題的答案, // 最終構建出原問題的答案 return node; } // 判斷二分搜尋樹中是否包含某個元素 + contains(element) { if (this.root === null) throw new Error("root is null. can't query."); return this.recursiveContains(this.root, element); } // 判斷二分搜尋樹種是否包含某個元素 遞迴演算法 - recursiveContains(node, element) { if (node === null) return false; // 當前節點元素比 要搜尋的元素 大 if (this.compare(node.element, element) > 0) return this.recursiveContains(node.left, element); else if (this.compare(node.element, element) < 0) // 當前元素比 要搜尋的元素 小 return this.recursiveContains(node.right, element); // 兩個元素相等 else return true; } // 找到二分搜尋樹中的最大值的元素 + maximum() { if (this.size === 0) throw new Error('binary search tree is empty.'); return this.recursiveMaximum(this.root).element; } // 找到二分搜尋樹中的最大值的元素的節點 遞迴演算法 - recursiveMaximum(node) { // 解決最基本的問題 向右走再也走不動了,說明當前節點就是最大值節點。 if (node.right === null) return node; return this.recursiveMaximum(node.right); } // 刪除二分搜尋樹中最大值的元素的節點,並返回這個節點的元素 + removeMax() { let maxElement = this.maximum(); this.root = this.recursiveRemoveMax(this.root); return maxElement; } // 刪除二分搜尋樹中最大值的元素的節點,並返回這個節點 遞迴演算法 - recursiveRemoveMax(node) { if (node.right === null) { // 先存 當前這個節點的左子樹, // 因為可能當前這個節點僅僅沒有右子樹,只有左子樹, // 那麼左子樹可以替代當前這個節點。 let leftNode = node.left; node.left = null; this.size--; return leftNode; } node.right = this.recursiveRemoveMax(node.right); return node; } // 找到二分搜尋樹中的最小值 + minimum() { if (this.size === 0) throw new Error('binary search tree is empty.'); return this.recursiveMinimum(this.root).element; } // 找到二分搜尋樹中的最小值的元素的節點 遞迴演算法 - recursiveMinimum(node) { if (node.left === null) return node; return this.recursiveMinimum(node.left); } // 刪除二分搜尋樹中最小值的元素的節點,並返回這個節點的元素 + removeMin() { let leftNode = this.minimum(); this.root = this.recursiveRemoveMin(this.root); return leftNode; } // 刪除二分搜尋樹中最小值的元素的節點,並返回這個節點 遞迴演算法 - recursiveRemoveMin(node) { // 解決最簡單的問題 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } // 將複雜的問題拆分為性質相同的小問題, // 然後求出這些小問題的解後構建出原問題的答案 node.left = this.recursiveRemoveMin(node.left); return node; } // 刪除二分搜尋樹上的任意節點 remove(element) { this.root = this.recursiveRemove(this.root, element); } // 刪除二分搜尋樹上的任意節點 遞迴演算法 // 返回刪除對應元素節點後新的二分搜尋樹的根 recursiveRemove(node, element) { if (node === null) return null; // 當前節點的元素值比待刪除的元素小 那麼就向當前節點的右子樹中去找 if (this.compare(node.element, element) < 0) { node.right = this.recursiveRemove(node.right, element); return node; } else if (this.compare(node.element, element) > 0) { // 向當前節點的左子樹中去找 node.left = this.recursiveRemove(node.left, element); return node; } else { // 如果找到了相同值的節點了,開始進行相應的處理 // 如果這個節點左子樹為空,那麼就讓這個節點的右子樹覆蓋當前節點 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } // 如果當前節點的右子樹為空,那麼就讓這個節點的左子樹覆蓋當前節點 if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } // 如果當前節點的左右子樹都不為空,那麼就開始特殊操作 // 1. 先找到當前節點右子樹上最小的那個節點,儲存起來 // 2. 然後刪除掉當前節點右子樹上最小的那個節點, // 3. 讓儲存起來的那個節點覆蓋掉當前節點 // 1. 也就是儲存起來的那個節點的right = 刪除掉當前節點右子樹上最小的節點後返回的那個節點 // 2. 再讓儲存起來的那個節點的left = 當前節點的left // 4. 解除當前節點及其left和right,全都賦值為null,這樣就相當於把當前節點從二分搜尋樹中剔除了 // 5. 返回儲存的這個節點 let successtor = this.recursiveMinimum(node.right); successtor.right = this.recursiveRemoveMin(node.right); // 恢復removeMin 操作的this.size -- 帶來的影響 this.size++; successtor.left = node.left; // 開始正式的刪除當前節點的操作 node = node.left = node.right = null; this.size--; // 返回當前儲存的節點 return successtor; } } // 前序遍歷 + preOrder(operator) { this.recursivePreOrder(this.root, operator); } // 前序遍歷 遞迴演算法 - recursivePreOrder(node, operator) { if (node === null) return; // 呼叫一下操作方法 operator(node.element); console.log(node, node.element); // 繼續遞迴遍歷左右子樹 this.recursivePreOrder(node.left, operator); this.recursivePreOrder(node.right, operator); } // 前序遍歷 非遞迴演算法 + nonRecursivePreOrder(operator) { let stack = new MyLinkedListStack(); stack.push(this.root); let node = null; while (!stack.isEmpty()) { // 出棧操作 node = stack.pop(); operator(node.element); // 訪問當前的節點 console.log(node.element); // 棧是先入後出的,把需要後訪問的節點 先放進去,先訪問的節點後放進去 // 前序遍歷是訪問當前節點,然後再遍歷左子樹,最後遍歷右子樹 if (node.right !== null) stack.push(node.right); if (node.left !== null) stack.push(node.left); } } // 中序遍歷 + inOrder(operator) { this.recursiveInOrder(this.root, operator); } // 中序遍歷 遞迴演算法 - recursiveInOrder(node, operator) { if (node == null) return; this.recursiveInOrder(node.left, operator); operator(node.element); console.log(node.element); this.recursiveInOrder(node.right, operator); } // 後序遍歷 + postOrder(operator) { this.recursivePostOrder(this.root, operator); } // 後序遍歷 遞迴演算法 - recursivePostOrder(node, operator) { if (node == null) return; this.recursivePostOrder(node.left, operator); this.recursivePostOrder(node.right, operator); operator(node.element); console.log(node.element); } // 層序遍歷 levelOrder(operator) { let queue = new MyLinkedListQueue(); queue.enqueue(this.root); let node = null; while (!queue.isEmpty()) { node = queue.dequeue(); operator(node.element); console.log(node.element); // 佇列 是先進先出的,所以從左往右入隊 // 棧 是後進先出的, 所以從右往左入棧 if (node.left !== null) queue.enqueue(node.left); if (node.right !== null) queue.enqueue(node.right); } } // 獲取二分搜尋樹中節點個數 + getSize() { return this.size; } // 返回二分搜尋樹是否為空的bool值 + isEmpty() { return this.size === 0; } // 新增一個比較的方法,專門用來比較新增的元素大小 - // 第一個元素比第二個元素大 就返回 1 // 第一個元素比第二個元素小 就返回 -1 // 第一個元素比第二個元素相等 就返回 0 compare(elementA, elementB) { if (elementA === null || elementB === null) throw new Error("element is null. can't compare."); // 先直接寫死 if (elementA > elementB) return 1; else if (elementA < elementB) return -1; else return 0; } // 輸出二分搜尋樹中的資訊 // @Override toString 2018-11-03-jwl toString() { let treeInfo = ''; treeInfo += this.getBinarySearchTreeString(this.root, 0, treeInfo); return treeInfo; } // 寫一個輔助函式,用來生成二分搜尋樹資訊的字串 getBinarySearchTreeString(node, depth, treeInfo, pageContent = '') { //以前序遍歷的方式 if (node === null) { treeInfo += this.getDepthString(depth) + 'null \r\n'; pageContent = this.getDepthString(depth) + 'null<br /><br />'; document.body.innerHTML += `${pageContent}`; return treeInfo; } treeInfo += this.getDepthString(depth) + node.element + '\r\n'; pageContent = this.getDepthString(depth) + node.element + '<br /><br />'; document.body.innerHTML += `${pageContent}`; treeInfo = this.getBinarySearchTreeString( node.left, depth + 1, treeInfo ); treeInfo = this.getBinarySearchTreeString( node.right, depth + 1, treeInfo ); return treeInfo; } // 寫一個輔助函式,用來生成遞迴深度字串 getDepthString(depth) { let depthString = ''; for (var i = 0; i < depth; i++) { depthString += '-- '; } return depthString; } } 複製程式碼
-
PerformanceTest
// 效能測試 class PerformanceTest { constructor() {} // 對比都列 testQueue(queue, openCount) { let startTime = Date.now(); let random = Math.random; for (var i = 0; i < openCount; i++) { queue.enqueue(random() * openCount); } while (!queue.isEmpty()) { queue.dequeue(); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 對比棧 testStack(stack, openCount) { let startTime = Date.now(); let random = Math.random; for (var i = 0; i < openCount; i++) { stack.push(random() * openCount); } while (!stack.isEmpty()) { stack.pop(); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 對比集合 testSet(set, openCount) { let startTime = Date.now(); let random = Math.random; let arr = []; let temp = null; // 第一遍測試 for (var i = 0; i < openCount; i++) { temp = random(); // 新增重複元素,從而測試集合去重的能力 set.add(temp * openCount); set.add(temp * openCount); arr.push(temp * openCount); } for (var i = 0; i < openCount; i++) { set.remove(arr[i]); } // 第二遍測試 for (var i = 0; i < openCount; i++) { set.add(arr[i]); set.add(arr[i]); } while (!set.isEmpty()) { set.remove(arr[set.getSize() - 1]); } let endTime = Date.now(); // 求出兩次測試的平均時間 let avgTime = Math.ceil((endTime - startTime) / 2); return this.calcTime(avgTime); } // 計算執行的時間,轉換為 天-小時-分鐘-秒-毫秒 calcTime(result) { //獲取距離的天數 var day = Math.floor(result / (24 * 60 * 60 * 1000)); //獲取距離的小時數 var hours = Math.floor((result / (60 * 60 * 1000)) % 24); //獲取距離的分鐘數 var minutes = Math.floor((result / (60 * 1000)) % 60); //獲取距離的秒數 var seconds = Math.floor((result / 1000) % 60); //獲取距離的毫秒數 var milliSeconds = Math.floor(result % 1000); // 計算時間 day = day < 10 ? '0' + day : day; hours = hours < 10 ? '0' + hours : hours; minutes = minutes < 10 ? '0' + minutes : minutes; seconds = seconds < 10 ? '0' + seconds : seconds; milliSeconds = milliSeconds < 100 ? milliSeconds < 10 ? '00' + milliSeconds : '0' + milliSeconds : milliSeconds; // 輸出耗時字串 result = day + '天' + hours + '小時' + minutes + '分' + seconds + '秒' + milliSeconds + '毫秒' + ' <<<<============>>>> 總毫秒數:' + result; return result; } } 複製程式碼
-
MyLinkedListSet
// 自定義連結串列集合Set class MyLinkedListSet { // constructor() { this.myLinkedList = new MyLinkedList(); } add(element) { if (!this.myLinkedList.contains(element)) this.myLinkedList.addFirst(element); } remove(element) { this.myLinkedList.removeElement(element); } contains(element) { return this.myLinkedList.contains(element); } each(operator) { let size = this.myLinkedList.getSize(); for (var i = 0; i < size; i++) { operator(this.myLinkedList.get(i)); } } getSize() { return this.myLinkedList.getSize(); } isEmpty() { return this.myLinkedList.isEmpty(); } } 複製程式碼
-
MyBSTSet
// 自定義二分搜尋樹集合Set class MyBinarySearchTreeSet { constructor() { // 借用二分搜尋樹來實現這個介面 this.myBinarySearchTree = new MyBinarySearchTree(); } // 新增元素 add(element) { this.myBinarySearchTree.add(element); } // 移除元素 remove(element) { this.myBinarySearchTree.remove(element); } // 是否包含這個元素 contains(element) { return this.myBinarySearchTree.contains(element); } // 遍歷操作 // 第一個引數 是回掉函式, // 第二個引數 是遍歷的方式 深度優先遍歷(前pre、中in、後post),廣度優先遍歷(層序level) each(operator, method) { // 遍歷方式預設是非遞迴的前序遍歷, // 其它的遍歷方式就是遞迴的前、中、後、層序遍歷。 switch (method) { case 'pre': this.myBinarySearchTree.preOrder(operator); break; case 'in': this.myBinarySearchTree.inOrder(operator); break; case 'post': this.myBinarySearchTree.postOrder(operator); break; case 'level': this.myBinarySearchTree.levelOrder(operator); break; default: this.myBinarySearchTree.nonRecursivePreOrder(operator); break; } } // 獲取集合中實際的元素個數 getSize() { return this.myBinarySearchTree.getSize(); } // 返回集合是否為空的bool值 isEmpty() { return this.myBinarySearchTree.isEmpty(); } } 複製程式碼
-
Main
// main 函式 class Main { constructor() { this.alterLine('Set Comparison Area'); let myLinkedListSet = new MyLinkedListSet(); let myBinarySearchTreeSet = new MyBinarySearchTreeSet(); let performanceTest = new PerformanceTest(); let myLinkedListSetInfo = performanceTest.testSet( myLinkedListSet, 5000 ); let myBinarySearchTreeSetInfo = performanceTest.testSet( myBinarySearchTreeSet, 5000 ); this.alterLine('MyLinkedListSet Area'); console.log(myLinkedListSetInfo); this.show(myLinkedListSetInfo); this.alterLine('MyBinarySearchTreeSet Area'); console.log(myBinarySearchTreeSetInfo); this.show(myBinarySearchTreeSetInfo); } // 將內容顯示在頁面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割線 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } // 頁面載入完畢 window.onload = function() { // 執行主函式 new Main(); }; 複製程式碼
更多集合相關的問題
- 在計算機的世界中,集合是一種非常有用的一種資料結構
- 集合在生活中可以做客戶統計、詞彙量統計。
- 在很多演算法面試題來說,集合也是有很大的作用的。
- 使用 內建的集合
- 內建的 Set 比自己實現的 MyBSTSet 要強大很多,
- 因為 底層實現 Set 的樹結構是一個平衡二叉樹,
- 更準確的說是基於紅黑樹來進行實現的,
- 所以這個 Set 不會出現最差的時間複雜度
O(n)
的情況, - 在最差的情況下在 Set 中進行增刪查也是
O(logn)
這種級別, - 並且這個 Set 還定義了更多的操作,
- 這些操作都是和二分搜尋樹具有順序性相關的操作。
解決 leetcode 上的集合問題
-
804.唯一摩爾斯密碼詞
-
網址:
https://leetcode-cn.com/problems/unique-morse-code-words/
-
解答
// 答題 class Solution { // leetcode 804. 唯一摩爾斯密碼詞 uniqueMorseRepresentations(words) { /** * @param {string[]} words * @return {number} * 使用自己的二分搜尋樹來實現 */ var uniqueMorseRepresentations = function(words) { // 摩斯碼 const codes = [ '.-', '-...', '-.-.', '-..', '.', '..-.', '--.', '....', '..', '.---', '-.-', '.-..', '--', '-.', '---', '.--.', '--.-', '.-.', '...', '-', '..-', '...-', '.--', '-..-', '-.--', '--..' ]; const myBinarySearchTreeSet = new MyBinarySearchTreeSet(); let content = ''; // 獲取起始字元的aceii碼, // 從而可以求出某個單詞的每一個字元在字母表中佔的位置索引, // 根據這些位置索引就可以在摩斯表中找到相應的摩斯碼, // 一個單詞就是一組摩斯碼,然後使用set新增,就可以直接實現去重的操作了 const start = 'a'.charCodeAt(0); for (const word of words) { for (const w of word) content += codes[w.charCodeAt(0) - start]; myBinarySearchTreeSet.add(content); content = ''; } return myBinarySearchTreeSet.getSize(); }; /** * @param {string[]} words * @return {number} * 使用系統內建的Set集合類 */ var uniqueMorseRepresentations = function(words) { // 摩斯碼 const codes = [ '.-', '-...', '-.-.', '-..', '.', '..-.', '--.', '....', '..', '.---', '-.-', '.-..', '--', '-.', '---', '.--.', '--.-', '.-.', '...', '-', '..-', '...-', '.--', '-..-', '-.--', '--..' ]; const set = new Set(); let content = ''; // 獲取起始字元的aceii碼, // 從而可以求出某個單詞的每一個字元在字母表中佔的位置索引, // 根據這些位置索引就可以在摩斯表中找到相應的摩斯碼, // 一個單詞就是一組摩斯碼,然後使用set新增,就可以直接實現去重的操作了 const start = 'a'.charCodeAt(0); for (const word of words) { for (const w of word) content += codes[w.charCodeAt(0) - start]; set.add(content); content = ''; } return set.size; }; return uniqueMorseRepresentations(words); } } // main 函式 class Main { constructor() { this.alterLine('leetcode 804.唯一摩爾斯密碼詞'); let s = new Solution(); let words = ['gin', 'zen', 'gig', 'msg']; this.show(s.uniqueMorseRepresentations(words)); } // 將內容顯示在頁面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割線 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } // 頁面載入完畢 window.onload = function() { // 執行主函式 new Main(); }; 複製程式碼
有序集合和無須集合
- 之前實現的集合是基於二分搜尋樹實現的集合,
- 還有 系統內建的基於紅黑樹實現的集合,
- 它們本質都是有序的集合。
- 有序的集合是指元素在集合中是具有順序性的
- 例如在二分搜尋樹中儲存的元素,
- 可以很輕易地從小到大遍歷出來或者
- 去看這個元素的是上或下一個元素是誰等等,
- 這個準確的來說就是有序集合(OrderSet)。
- 無序的集合是指元素在集合中是沒有順序的
- 例如在連結串列中儲存的元素,
- 只是根據元素插入的順序來決定這些元素在集合中的順序,
- 不能夠輕易地從小到大來遍歷在這個集合中所有的元素,
- 也無法非常容易的去找到這個集合中最小最大的元素是誰、
- 上或下一個元素誰等等這些操作。
- 通常有序的集合都是通過搜尋樹來實現的,
- 無論是二分搜尋樹還是平衡二叉樹它們都是搜尋樹,
- 因為搜尋樹就有這樣的優勢,它可以實現有序的集合,
- 對於有些問題集合的有序性是非常重要的,
- 在另一些問題中完全沒有必要使用有序集合,
- 比如僅僅是處理放重複元素的問題上,根本利用不到集合的有序性,
- 完全可以使用無序的集合來解決這個問題。
- 對於無序的集合其實還是有更好的解決方案的,
- 那就是基於雜湊表的實現,對於雜湊表來說,
- 相應的增刪查這樣的操作其實比搜尋樹還要快,
- 其實對於搜尋樹的實現來說如果它保持了有序性,
- 那麼它的能力其實也會更大,這個能力就表現在很輕易的查詢到最大最小元素,
- 或者某一個元素的前一個元素和後一個元素等等,
- 輕易完成這些操作是有代價的,
- 這個代價其實就在時間複雜性上,它是稍微差於雜湊表的。
多重集合
- 對於集合來說在大多數情況下是不希望有重複元素的,
- 但是在有些情況下也希望有集合可以容納重複的元素,
- 在這種情況下就稱之為多重集合(MultipleSet),
- 多重集合具體的實現也非常簡單,
- 只需要在允許重複的二分搜尋樹上進行包裝一下一下即可,
- 你所解決的問題是否需要使用多重集合是根據業務場景所決定的,
- 通常使用集合的大多數情況下還是選擇不包含重複元素的集合。
對映(Map)
-
高中數學裡的函式就可以理解成是一種對映
- 如
f(x)=2*x+1
,在映域中每取出一個值, - 相應的在值域中都有有一個值與它對應,
- 如 x 為 1,f(x)就為 3,x 為 2,f(x)就為 5,
- 從一個值向另外一個值的對應關係其實就是對映。
- 如
-
對映關係你也可以把它稱之為字典
- 如 單詞 -----> 釋意,
- 字典就是這樣一個從單詞對應到釋意這種資料的一個集合,
- 字典的英文是 dictionary,
- 在很多語言中把對映這樣的一種資料結構稱之為 dictionary 的簡寫 dict,
- 最典型的就是 python 裡面基礎資料結構 dict,
- 但是在 java、c++、js 語言中把這種關係稱之為 Map,
- 其實它描述的就是類似字典這樣的資料結構。
-
生活中的對映的應用
dict:key ----> value
字典:單詞 ----> 釋意
名冊:身份證號 ----> 人
車輛管理:車牌號 ----> 車
資料庫:id ----> 資訊
詞頻統計:單詞 ----> 頻率
-
儲存(鍵,值)資料對的資料結構(key,value)
- 資料是一對一對出現的,這樣的資料結構就叫對映,
- 很多時候都是要根據鍵(Key)來尋找值(Value),
- 例如生活中的對映的應用例子。
-
可以非常容易的使用連結串列或者二分搜尋樹來實現對映。
// 連結串列實現時的Node class Node { key; // Key value; //Value Node next;// Node } // 二分搜尋樹實現時的Node class Node { key; // Key value; //Value left;// Node right;// Node } 複製程式碼
對映介面
MyMap
void add(k, v)
V remove(k)
boolean contains(k)
V get(k)
void set(k, v)
int getSize()
boolean isEmpty()
使用連結串列來實現對映 Map
程式碼示例
-
(class: MyLinkedListMap)
-
MyLinkedListMap
// 自定義連結串列對映節點 LinkedListMapNode class MyLinkedListMapNode { constructor(key = null, value = null, next = null) { this.key = key; this.value = value; this.next = next; } // @Override toString 2018-11-5-jwl toString() { return this.key.toString() + '---------->' + this.value.toString(); } } // 自定義連結串列對映 Map class MyLinkedListMap { constructor() { this.dummyHead = new MyLinkedListMapNode(); this.size = 0; } // 根據key獲取節點 - getNode(key) { let cur = this.dummyHead.next; while (cur !== null) { if (cur.key === key) return cur; cur = cur.next; } return null; } // 新增操作 + add(key, value) { let node = this.getNode(key); // 這個節點如果存在就 覆蓋值即可 if (node !== null) node.value = value; else { // 如果不存在,那麼就在頭部新增以下 let newNode = new MyLinkedListMapNode(key, value); newNode.next = this.dummyHead.next; this.dummyHead.next = newNode; this.size++; } } // 刪除操作 返回被刪除的元素 + remove(key) { let prev = this.dummyHead; // 迴圈查詢 while (prev.next !== null) { if (prev.next.key === key) break; prev = prev.next; } // 如果觸碰了break, 那就滿足條件 if (prev.next !== null) { let delNode = prev.next; prev.next = delNode.next; let value = delNode.value; devNode = delNode.next = null; this.size--; return value; } // 如果沒有觸屏break 那就返回空值回去 return null; } // 查詢操作 返回查詢到的元素 + get(key) { let node = this.getNode(key); if (node === null) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(key); if (node === null) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含該key的元素的判斷值 + contains(key) { return this.getNode(key) !== null; } // 返回對映中實際的元素個數 + getSize() { return this.size; } // 返回對映中是否為空的判斷值 + isEmpty() { return this.size === 0; } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyLinkedListMap: size = ${this.size}, data = [ `; document.body.innerHTML += `MyLinkedListMap: size = ${ this.size }, data = [ <br/><br/>`; let cur = this.dummyHead.next; for (var i = 0; i < this.size - 1; i++) { mapInfo += ` ${cur.toString()}, \r\n`; document.body.innerHTML += ` ${cur.toString()}, <br/><br/>`; cur = cur.next; } if (cur !== null) { mapInfo += ` ${cur.toString()} \r\n`; document.body.innerHTML += ` ${cur.toString()} <br/><br/>`; } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ] <br/><br/>`; return mapInfo; } } 複製程式碼
使用二分搜尋樹來實現對映 Map
程式碼示例
-
(class: MyBSTMap)
-
MyBSTMap
// 自定義二分搜尋樹樹對映節點 TreeMapNode class MyBinarySearchTreeMapNode { constructor(key = null, value = null, left = null, right = null) { this.key = key; this.value = value; this.left = left; this.right = right; } // @Override toString 2018-11-5-jwl toString() { return this.key.toString() + '---------->' + this.value.toString(); } } // 自定義二分搜尋樹對映 Map class MyBinarySearchTreeMap { constructor() { this.root = null; this.size = 0; } // 比較的功能 compare(keyA, keyB) { if (keyA === null || keyB === null) throw new Error("key is error. key can't compare."); if (keyA > keyB) return 1; else if (keyA < keyB) return -1; else return 0; } // 根據key獲取節點 - getNode(node, key) { // 先解決最基本的問題 if (node === null) return null; // 開始將複雜的問題 逐漸縮小規模 // 從而求出小問題的解,最後構建出原問題的解 switch (this.compare(node.key, key)) { case 1: // 向左找 return this.getNode(node.left, key); break; case -1: // 向右找 return this.getNode(node.right, key); break; case 0: // 找到了 return node; break; default: throw new Error( 'compare result is error. compare result : 0、 1、 -1 .' ); break; } } // 新增操作 + add(key, value) { this.root = this.recursiveAdd(this.root, key, value); } // 新增操作 遞迴演算法 - recursiveAdd(node, key, value) { // 解決最簡單的問題 if (node === null) { this.size++; return new MyBinarySearchTreeMapNode(key, value); } // 將複雜的問題規模逐漸變小, // 從而求出小問題的解,從而構建出原問題的答案 if (this.compare(node.key, key) > 0) node.left = this.recursiveAdd(node.left, key, value); else if (this.compare(node.key, key) < 0) node.right = this.recursiveAdd(node.right, key, value); else node.value = value; return node; } // 刪除操作 返回被刪除的元素 + remove(key) { let node = this.getNode(this.root, key); if (node === null) return null; this.root = this.recursiveRemove(this.root, key); return node.value; } // 刪除操作 遞迴演算法 + recursiveRemove(node, key) { // 解決最基本的問題 if (node === null) return null; if (this.compare(node.key, key) > 0) { node.left = this.recursiveRemove(node.left, key); return node; } else if (this.compare(node.key, key) < 0) { node.right = this.recursiveRemove(node.right, key); return node; } else { // 當前節點的key 與 待刪除的key的那個節點相同 // 有三種情況 // 1. 當前節點沒有左子樹,那麼只有讓當前節點的右子樹直接覆蓋當前節點,就表示當前節點被刪除了 // 2. 當前節點沒有右子樹,那麼只有讓當前節點的左子樹直接覆蓋當前節點,就表示當前節點被刪除了 // 3. 當前節點左右子樹都有, 那麼又分兩種情況,使用前驅刪除法或者後繼刪除法 // 1. 前驅刪除法:使用當前節點的左子樹上最大的那個節點覆蓋當前節點 // 2. 後繼刪除法:使用當前節點的右子樹上最小的那個節點覆蓋當前節點 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } else if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } else { let predecessor = this.maximum(node.left); node.left = this.removeMax(node.left); this.size++; // 開始嫁接 當前節點的左右子樹 predecessor.left = node.left; predecessor.right = node.right; // 將當前節點從根節點剔除 node = node.left = node.right = null; this.size--; // 返回嫁接後的新節點 return predecessor; } } } // 刪除操作的兩個輔助函式 // 獲取最大值、刪除最大值 // 以前驅的方式 來輔助刪除操作的函式 // 獲取最大值 maximum(node) { // 再也不能往右了,說明當前節點已經是最大的了 if (node.right === null) return node; // 將複雜的問題漸漸減小規模,從而求出小問題的解,最後用小問題的解構建出原問題的答案 return this.maximum(node.right); } // 刪除最大值 removeMax(node) { // 解決最基本的問題 if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } // 開始化歸 node.right = this.removeMax(node.right); return node; } // 查詢操作 返回查詢到的元素 + get(key) { let node = this.getNode(this.root, key); if (node === null) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(this.root, key); if (node === null) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含該key的元素的判斷值 + contains(key) { return this.getNode(this.root, key) !== null; } // 返回對映中實際的元素個數 + getSize() { return this.size; } // 返回對映中是否為空的判斷值 + isEmpty() { return this.size === 0; } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `; document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [ <br/><br/>`; // 以非遞迴的前序遍歷 輸出字串 let stack = new MyLinkedListStack(); stack.push(this.root); if (this.root === null) stack.pop(); while (!stack.isEmpty()) { let node = stack.pop(); if (node.left !== null) stack.push(node.left); if (node.right !== null) stack.push(node.right); if (node.left === null && node.right === null) { mapInfo += ` ${node.toString()} \r\n`; document.body.innerHTML += ` ${node.toString()} <br/><br/>`; } else { mapInfo += ` ${node.toString()}, \r\n`; document.body.innerHTML += ` ${node.toString()}, <br/><br/>`; } } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ] <br/><br/>`; return mapInfo; } } 複製程式碼
兩種對映 Map 的時間複雜度分析
MyLinkedListMap O(n)
- 增加 add
O(n)
- 為了防止指定 key 的節點不存在,所以必須先查詢一遍,
- 然後再決定是直接賦值還是建立新節點,雖然新增的複雜度為
O(1)
, - 但是查詢的操作是遍歷整個連結串列,所以整體時間複雜度為
O(n)
。
- 查詢 contains、get
O(n)
- 查詢的操作是遍歷整個連結串列,
- 所以時間複雜度為
O(n)
- 修改 set
O(n)
- 為了防止指定 key 的節點不存在,所以必須先查詢一遍,
- 所以時間複雜度為
O(n)
- 刪除 remove
O(n)
- 刪操作也需要遍歷整個連結串列,
- 所以時間複雜度為
O(n)
MyBSTMap O(h) or O(log n)
- 增加 add
O(h) or O(log n)
- 新增一個元素(key/value),
- 待新增的這個元素 key 和根節點的這個元素 key 進行比較,
- 如果小於的話直接去左子樹,如果大於的話直接去右子樹,
- 每一次近乎都能把一半兒的元素(key/value)給扔掉,
- 新增這個元素這個過程其實就像是在走一個連結串列,
- 一層一層的從這個樹的根節點向葉子節點出發,
- 最終一共經歷的節點個數就是這棵樹的
高度
, - 也就是整棵書最大的深度,查詢元素也是如此,
- 刪除元素還是如此,所以對於二分搜尋樹來說,
- 這三個時間複雜度都是
O(h)
這個級別的, - 這個 h 就是二分搜尋樹的高度。
- 查詢 contains、get
O(h) or O(log n)
- 修改 set
O(h) or O(log n)
- 刪除 remove
O(h) or O(log n)
程式碼示例
-
class: MyLinkedListMap, class: MyBSTMap , class: PerformanceTest, class: Main)
-
MyLinkedListMap
// 自定義連結串列對映節點 LinkedListMapNode class MyLinkedListMapNode { constructor(key = null, value = null, next = null) { this.key = key; this.value = value; this.next = next; } // @Override toString 2018-11-5-jwl toString() { return this.key.toString() + '---------->' + this.value.toString(); } } // 自定義連結串列對映 Map class MyLinkedListMap { constructor() { this.dummyHead = new MyLinkedListMapNode(); this.size = 0; } // 根據key獲取節點 - getNode(key) { let cur = this.dummyHead.next; while (cur !== null) { if (cur.key === key) return cur; cur = cur.next; } return null; } // 新增操作 + add(key, value) { let node = this.getNode(key); // 這個節點如果存在就 覆蓋值即可 if (node !== null) node.value = value; else { // 如果不存在,那麼就在頭部新增以下 let newNode = new MyLinkedListMapNode(key, value); newNode.next = this.dummyHead.next; this.dummyHead.next = newNode; this.size++; } } // 刪除操作 返回被刪除的元素 + remove(key) { let prev = this.dummyHead; // 迴圈查詢 while (prev.next !== null) { if (prev.next.key === key) break; prev = prev.next; } // 如果觸碰了break, 那就滿足條件 if (prev.next !== null) { let delNode = prev.next; prev.next = delNode.next; let value = delNode.value; delNode = delNode.next = null; this.size--; return value; } // 如果沒有觸屏break 那就返回空值回去 return null; } // 查詢操作 返回查詢到的元素 + get(key) { let node = this.getNode(key); if (node === null) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(key); if (node === null) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含該key的元素的判斷值 + contains(key) { return this.getNode(key) !== null; } // 返回對映中實際的元素個數 + getSize() { return this.size; } // 返回對映中是否為空的判斷值 + isEmpty() { return this.size === 0; } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyLinkedListMap: size = ${this.size}, data = [ `; document.body.innerHTML += `MyLinkedListMap: size = ${ this.size }, data = [ <br/><br/>`; let cur = this.dummyHead.next; for (var i = 0; i < this.size - 1; i++) { mapInfo += ` ${cur.toString()}, \r\n`; document.body.innerHTML += ` ${cur.toString()}, <br/><br/>`; cur = cur.next; } if (cur !== null) { mapInfo += ` ${cur.toString()} \r\n`; document.body.innerHTML += ` ${cur.toString()} <br/><br/>`; } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ] <br/><br/>`; return mapInfo; } } 複製程式碼
-
MyBSTMap
// 自定義二分搜尋樹樹對映節點 TreeMapNode class MyBinarySearchTreeMapNode { constructor(key = null, value = null, left = null, right = null) { this.key = key; this.value = value; this.left = left; this.right = right; } // @Override toString 2018-11-5-jwl toString() { return this.key.toString() + '---------->' + this.value.toString(); } } // 自定義二分搜尋樹對映 Map class MyBinarySearchTreeMap { constructor() { this.root = null; this.size = 0; } // 比較的功能 compare(keyA, keyB) { if (keyA === null || keyB === null) throw new Error("key is error. key can't compare."); if (keyA > keyB) return 1; else if (keyA < keyB) return -1; else return 0; } // 根據key獲取節點 - getNode(node, key) { // 先解決最基本的問題 if (node === null) return null; // 開始將複雜的問題 逐漸縮小規模 // 從而求出小問題的解,最後構建出原問題的解 switch (this.compare(node.key, key)) { case 1: // 向左找 return this.getNode(node.left, key); break; case -1: // 向右找 return this.getNode(node.right, key); break; case 0: // 找到了 return node; break; default: throw new Error( 'compare result is error. compare result : 0、 1、 -1 .' ); break; } } // 新增操作 + add(key, value) { this.root = this.recursiveAdd(this.root, key, value); } // 新增操作 遞迴演算法 - recursiveAdd(node, key, value) { // 解決最簡單的問題 if (node === null) { this.size++; return new MyBinarySearchTreeMapNode(key, value); } // 將複雜的問題規模逐漸變小, // 從而求出小問題的解,從而構建出原問題的答案 if (this.compare(node.key, key) > 0) node.left = this.recursiveAdd(node.left, key, value); else if (this.compare(node.key, key) < 0) node.right = this.recursiveAdd(node.right, key, value); else node.value = value; return node; } // 刪除操作 返回被刪除的元素 + remove(key) { let node = this.getNode(this.root, key); if (node === null) return null; this.root = this.recursiveRemove(this.root, key); return node.value; } // 刪除操作 遞迴演算法 + recursiveRemove(node, key) { // 解決最基本的問題 if (node === null) return null; if (this.compare(node.key, key) > 0) { node.left = this.recursiveRemove(node.left, key); return node; } else if (this.compare(node.key, key) < 0) { node.right = this.recursiveRemove(node.right, key); return node; } else { // 當前節點的key 與 待刪除的key的那個節點相同 // 有三種情況 // 1. 當前節點沒有左子樹,那麼只有讓當前節點的右子樹直接覆蓋當前節點,就表示當前節點被刪除了 // 2. 當前節點沒有右子樹,那麼只有讓當前節點的左子樹直接覆蓋當前節點,就表示當前節點被刪除了 // 3. 當前節點左右子樹都有, 那麼又分兩種情況,使用前驅刪除法或者後繼刪除法 // 1. 前驅刪除法:使用當前節點的左子樹上最大的那個節點覆蓋當前節點 // 2. 後繼刪除法:使用當前節點的右子樹上最小的那個節點覆蓋當前節點 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } else if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } else { let predecessor = this.maximum(node.left); node.left = this.removeMax(node.left); this.size++; // 開始嫁接 當前節點的左右子樹 predecessor.left = node.left; predecessor.right = node.right; // 將當前節點從根節點剔除 node = node.left = node.right = null; this.size--; // 返回嫁接後的新節點 return predecessor; } } } // 刪除操作的兩個輔助函式 // 獲取最大值、刪除最大值 // 以前驅的方式 來輔助刪除操作的函式 // 獲取最大值 maximum(node) { // 再也不能往右了,說明當前節點已經是最大的了 if (node.right === null) return node; // 將複雜的問題漸漸減小規模,從而求出小問題的解,最後用小問題的解構建出原問題的答案 return this.maximum(node.right); } // 刪除最大值 removeMax(node) { // 解決最基本的問題 if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } // 開始化歸 node.right = this.removeMax(node.right); return node; } // 查詢操作 返回查詢到的元素 + get(key) { let node = this.getNode(this.root, key); if (node === null) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(this.root, key); if (node === null) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含該key的元素的判斷值 + contains(key) { return this.getNode(this.root, key) !== null; } // 返回對映中實際的元素個數 + getSize() { return this.size; } // 返回對映中是否為空的判斷值 + isEmpty() { return this.size === 0; } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `; document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [ <br/><br/>`; // 以非遞迴的前序遍歷 輸出字串 let stack = new MyLinkedListStack(); stack.push(this.root); if (this.root === null) stack.pop(); while (!stack.isEmpty()) { let node = stack.pop(); if (node.left !== null) stack.push(node.left); if (node.right !== null) stack.push(node.right); if (node.left === null && node.right === null) { mapInfo += ` ${node.toString()} \r\n`; document.body.innerHTML += ` ${node.toString()} <br/><br/>`; } else { mapInfo += ` ${node.toString()}, \r\n`; document.body.innerHTML += ` ${node.toString()}, <br/><br/>`; } } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ] <br/><br/>`; return mapInfo; } } 複製程式碼
-
PerformanceTest
// 效能測試 class PerformanceTest { constructor() {} // 對比都列 testQueue(queue, openCount) { let startTime = Date.now(); let random = Math.random; for (var i = 0; i < openCount; i++) { queue.enqueue(random() * openCount); } while (!queue.isEmpty()) { queue.dequeue(); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 對比棧 testStack(stack, openCount) { let startTime = Date.now(); let random = Math.random; for (var i = 0; i < openCount; i++) { stack.push(random() * openCount); } while (!stack.isEmpty()) { stack.pop(); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 對比集合 testSet(set, openCount) { let startTime = Date.now(); let random = Math.random; let arr = []; let temp = null; // 第一遍測試 for (var i = 0; i < openCount; i++) { temp = random(); // 新增重複元素,從而測試集合去重的能力 set.add(temp * openCount); set.add(temp * openCount); arr.push(temp * openCount); } for (var i = 0; i < openCount; i++) { set.remove(arr[i]); } // 第二遍測試 for (var i = 0; i < openCount; i++) { set.add(arr[i]); set.add(arr[i]); } while (!set.isEmpty()) { set.remove(arr[set.getSize() - 1]); } let endTime = Date.now(); // 求出兩次測試的平均時間 let avgTime = Math.ceil((endTime - startTime) / 2); return this.calcTime(avgTime); } // 對比對映 testMap(map, openCount) { let startTime = Date.now(); let array = new MyArray(); let random = Math.random; let temp = null; let result = null; for (var i = 0; i < openCount; i++) { temp = random(); result = openCount * temp; array.add(result); array.add(result); array.add(result); array.add(result); } for (var i = 0; i < array.getSize(); i++) { result = array.get(i); if (map.contains(result)) map.add(result, map.get(result) + 1); else map.add(result, 1); } for (var i = 0; i < array.getSize(); i++) { result = array.get(i); map.remove(result); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 計算執行的時間,轉換為 天-小時-分鐘-秒-毫秒 calcTime(result) { //獲取距離的天數 var day = Math.floor(result / (24 * 60 * 60 * 1000)); //獲取距離的小時數 var hours = Math.floor((result / (60 * 60 * 1000)) % 24); //獲取距離的分鐘數 var minutes = Math.floor((result / (60 * 1000)) % 60); //獲取距離的秒數 var seconds = Math.floor((result / 1000) % 60); //獲取距離的毫秒數 var milliSeconds = Math.floor(result % 1000); // 計算時間 day = day < 10 ? '0' + day : day; hours = hours < 10 ? '0' + hours : hours; minutes = minutes < 10 ? '0' + minutes : minutes; seconds = seconds < 10 ? '0' + seconds : seconds; milliSeconds = milliSeconds < 100 ? milliSeconds < 10 ? '00' + milliSeconds : '0' + milliSeconds : milliSeconds; // 輸出耗時字串 result = day + '天' + hours + '小時' + minutes + '分' + seconds + '秒' + milliSeconds + '毫秒' + ' <<<<============>>>> 總毫秒數:' + result; return result; } } 複製程式碼
-
Main
// main 函式 class Main { constructor() { this.alterLine('Map Comparison Area'); let myLinkedListMap = new MyLinkedListMap(); let myBinarySearchTreeMap = new MyBinarySearchTreeMap(); let systemMap = new Map(); let performanceTest = new PerformanceTest(); systemMap.remove = systemMap.delete; systemMap.contains = systemMap.has; systemMap.add = systemMap.set; systemMap.isEmpty = () => systemMap.size === 0; systemMap.getSize = () => systemMap.size; let myLinkedListMapInfo = performanceTest.testMap( myLinkedListMap, 50000 ); let myBinarySearchTreeMapInfo = performanceTest.testMap( myBinarySearchTreeMap, 50000 ); let systemMapInfo = performanceTest.testMap(systemMap, 50000); this.alterLine('MyLinkedListMap Area'); console.log(myLinkedListMapInfo); this.show(myLinkedListMapInfo); this.alterLine('MyBinarySearchTreeMap Area'); console.log(myBinarySearchTreeMapInfo); this.show(myBinarySearchTreeMapInfo); this.alterLine('SystemMap Area'); console.log(systemMapInfo); this.show(systemMapInfo); } // 將內容顯示在頁面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割線 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } // 頁面載入完畢 window.onload = function() { // 執行主函式 new Main(); }; 複製程式碼
更多 Map 相關的問題
有序對映和無序對映
- 有序對映是指在 map 中的鍵是具有順序性的
- 對映中這些 key 就充當了集合中相應的元素 e,
- 只不過在對映中每一個 key 都有一個 value 的值而已,
- 有序對映通常都是基於搜尋樹來實現的,
- 因為搜尋樹具有這樣額外的能力,
- 可以維持資料的有序性。
- 無序對映是指在 map 中鍵不具有順序性的
- 連結串列實現的對映也是無序對映,
- 而且它非常的慢,
- 無序對映通常基於雜湊表來實現的。
多重對映
- 普通對映的鍵是不能夠重複的
- 但是在極個別的情況下,
- 有些應用場景可能希望對映 map 中
- 可以儲存具有重複鍵的相應的資料對,
- 在這種情況下就需要使用多重對映了。
- 多重對映中的鍵可以重複
集合和對映的關係
MySet
void add (e)
: 不能新增重複元素void remove (e)
boolean conatains (e)
int getSize ()
boolean isEmpty ()
MyMap
void add(k, v)
V remove(k)
boolean contains(k)
V get(k)
void set(k, v)
int getSize()
boolean isEmpty()
- 實現這兩種資料結構的時候既可以使用連結串列也可以使用二分搜尋樹
- 在實現的過程中,這兩種資料結構有很多相同之處,
- 對於對映來說它本身也是一個集合,
- 只不過是一個鍵 key 這樣的集合,
- 而且每一個 key 還帶著一個 value 而已,
- 它的本質和集合並沒有太大的區別,
- 只不過最開始實現的二分搜尋樹只能夠儲存一個元素,
- 所以在用二分搜尋樹實現 map 的時候很多方法需要重新寫一遍,
- 但是它的實質和集合中的邏輯沒有什麼大的區別,
- 所以集合和對映之間是存在這樣的聯絡的。
- 在很多系統類庫中完全可以基於集合 set 的實現去實現對映 map
- 或者基於對映 map 的實現來實現集合 set,
- 其實這個方法非常的簡單,
- 例如你有了一個集合的底層實現,
- 在這種情況下再完成一個對映的只需要重定義集合中的元素是什麼,
- 這個時候你只需要定義集合中的元素是鍵值對(key/value),
- 並且一定要特別的強調對於這種新的鍵值的資料對比較的時候,
- 是以鍵 key 的值來進行比較的而不是去比較 value 的值,
- 在這樣的定義下,對於集合的定義所有操作都會適用於對映,
- 不過對於對映還需要新增新的操作,
- 所以更加常見的的方式是基於對映 map 的底層實現,
- 直接包裝出集合 set 來,
- 當你有了一個對映的底層實現的時候,
- 直接將相應的對映的鍵值對(key/value)中的 value 賦值為空即可,
- 也就是隻使用 key 而不使用 value,只考慮鍵 key 不考慮值 value,
- 這樣一來整個 map 就是一個鍵 key 的集合,
- 只考慮鍵的時候,get 方法和 set 方法就沒有意義了,
- 這樣就相當於實現了一個對映之後在對這個對映進行包裝,
- 就可以包裝出集合這個資料結構了。
- 集合對映的核心邏輯其實是一致的。
- 其實你可以直接對連結串列和二分搜尋樹直接設定 key 和 value
- 這種很常見的設計思路,
- 平衡二叉樹、紅黑樹這樣的樹結構直接帶有 key 和 value。
解決 leetcode 上的更多集合和對映問題
- leetcode 上
349.兩個陣列的交集
https://leetcode-cn.com/problems/intersection-of-two-arrays/
- 這個交集不保留重複元素,
- 使用 系統內建 Set 即可
- leetcode 上
350.兩個陣列的交集 II
https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/
- 這個交集保留重複元素
- 使用 系統內建 Map 即可。
- 其實和雜湊表相關的大多數問題,
- 可以使用 Set 和 Map 來解決
- 其實系統內建的 Set 和 Map 都是通過雜湊表來實現的,再底層才會是紅黑樹,
- 使用基於雜湊表實現的集合或者對映來解決和雜湊表相關的大多數問題。
- 系統內建的 Set 和 Map 是先基於 hash 表的底層實現,
- 然後 hash 表是再基於平衡二叉樹的底層實現,
- set 和 map 的結構是相同的,所以從使用者使用的角度來看,
- 可以完全不管它們的底層是怎麼回事兒,
- 只需要知道它們可以實現這樣的功能就好了,
- 相應的也應該知道它們背後不同的底層實現的時間複雜度是怎樣的,
- 在多大數情況下使用平衡二叉樹實現的 Set 和 Map,
- 在時間上是完全沒有問題的,logn 這個複雜度也是非常非常快的。
- 可以嘗試去使用 Set 和 Map 去實現 leetcode 上的雜湊表標籤的問題
https://leetcode-cn.com/tag/hash-table/
程式碼示例
-
(class: Solution, class: Solution)
-
兩道題目
- Solution:leetcode 上
349.兩個陣列的交集
- Solution:leetcode 上
350.兩個陣列的交集 II
// 答題 class Solution { // leetcode 349. 兩個陣列的交集 intersection(nums1, nums2) { /** * @param {number[]} nums1 * @param {number[]} nums2 * @return {number[]} */ var intersection = function(nums1, nums2) { let set = new Set(); let arr = []; for (const num of nums1) set.add(num); for (const num of nums2) { if (set.has(num)) { arr.push(num); set.delete(num); } } return arr; }; return intersection(nums1, nums2); } // leetcode 350.兩個陣列的交集 II intersect(nums1, nums2) { /** * @param {number[]} nums1 * @param {number[]} nums2 * @return {number[]} */ var intersect = function(nums1, nums2) { let map = new Map(); let arr = []; for (const num of nums1) { if (map.has(num)) map.set(num, map.get(num) + 1); else map.set(num, 1); } for (const num of nums2) { if (map.has(num)) { arr.push(num); let result = map.get(num) - 1; map.set(num, result); if (result === 0) map.delete(num); } } return arr; }; return intersect(nums1, nums2); } } // main 函式 class Main { constructor() { this.alterLine('leetcode 349. 兩個陣列的交集'); let s = new Solution(); var nums1 = [1, 2, 2, 1], nums2 = [2, 2]; var nums3 = [4, 9, 5], nums4 = [9, 4, 9, 8, 4]; console.log('[' + s.intersection(nums1, nums2) + ']'); console.log('[' + s.intersection(nums3, nums4) + ']'); this.show('[' + s.intersection(nums1, nums2) + ']'); this.show('[' + s.intersection(nums3, nums4) + ']'); this.alterLine('leetcode 350. 兩個陣列的交集 II'); console.log('[' + s.intersect(nums1, nums2) + ']'); console.log('[' + s.intersect(nums3, nums4) + ']'); this.show('[' + s.intersect(nums1, nums2) + ']'); this.show('[' + s.intersect(nums3, nums4) + ']'); } // 將內容顯示在頁面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割線 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } // 頁面載入完畢 window.onload = function() { // 執行主函式 new Main(); }; 複製程式碼
- Solution:leetcode 上