前言
【從蛋殼到滿天飛】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,點選我吧,光看文章能夠掌握兩成,動手敲程式碼、動腦思考、畫圖才可以掌握八成。
本文章適合 對資料結構想了解並且感興趣的人群,文章風格一如既往如此,就覺得手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加原始碼,時間跨度也算將近半年時間了,希望對想學習資料結構的人或者正在學習資料結構的人群有幫助。
棧 Statck
- 棧也是一種線性結構
- 相比陣列來說相應的操作更少,
- 棧對應的操作是陣列的子集,
- 因為它的本質就是一個陣列,
- 並且它有比陣列更多的限制。
- 棧的本質就是一個陣列
- 它將資料排開來放的,
- 新增元素的時候只能從棧的一端新增元素,
- 取出元素的時候也只能棧的一端取出元素,
- 這一端叫做棧頂,當這樣的限定了陣列,
- 從而形成了棧這種資料結構之後,
- 它可以在計算機世界中對於
- 組建邏輯產生非常非常重要的作用。
- 棧的操作
- 從棧頂新增元素,把元素一個一個的放入到棧中,
- 如新增值的時候為 1、2、3,
- 你取值的時候順序則為 3、2、1,
- 因為你新增元素是隻能從一端放入,
- 取出元素時也只能從一端取出,
- 而這一段就是棧頂,
- 棧的出口和入口都是同一個位置,
- 所以你只能按照先進後出、後進先出的順序
- 新增資料或者取出資料,不存在插入和索引。
- 棧是一種後進先出的資料結構
- 也就是 Last In First Out(LIFO),
- 這樣的一種資料結構,在計算機的世界裡,
- 它擁有著不可思議的作用,
- 無論是經典的演算法還是演算法設計都接觸到
- 棧這種看似很簡單但其實應用非常廣泛的資料結構,
棧的簡單應用
-
無處不在的 Undo 操作(撤銷)
- 編輯器的撤銷操作的原理就是靠一個棧來進行維護的,
- 如 將 每次輸入的內容依次放入棧中 我 喜歡 你,
- 如果 你 字寫錯,你撤銷一下,變成 我 喜歡,
- 再撤銷一下 變成 我。
-
程式呼叫的系統棧
- 程式呼叫時經常會出現在一個邏輯中間
- 先終止然後跳到另外一個邏輯去執行,
- 所謂的子函式的呼叫就是這個過程,
- 在這個過程中計算機就需要使用一個
- 稱為系統棧的一個資料結構來記錄程式的呼叫過程。
- 例如有三個函式 A、B、C,
- 當 A 執行到一半的時候呼叫 B,
- 當 B 執行到一半的時候呼叫 C,
- C 函式可以執行執行完,
- C 函式執行完了之後繼續執行未完成的 B 函式,
- B 函式執行完了就執行未完成 A 函式,
- A 函式執行完了就結束了。
function A () { 1 ...; 2 B(); 3 ...; } function B () { 1 ...; 2 C(); 3 ...; } function C () { 1 ...; 2 ...; 3 ...; } 複製程式碼
-
系統棧記錄的過程是:
- A 函式執行,在第二行中斷了,因為要去執行函式 B 了,
- 這時候函式資訊
A2
會被放入系統棧中,系統棧中顯示:[A2]
, - 然後 B 函式執行,在第二行也中斷了,因為要去執行函式 C 了,
- 這時候函式資訊 B2 會被放入系統棧中,系統棧中顯示:
[A2, B2]
, - 然後 C 函式執行,C 函式沒有子函式可執行,那麼執行到底,函式 C 執行完畢,
- 從系統棧中取出函式 B 的資訊,系統棧中顯示:
[A2]
, - 根據從系統棧中取出的函式 B 的資訊,從函式 B 原來中斷的位置繼續開始執行,
- B 函式執行完畢了,這時候會再從系統棧中取出函式 A 的,系統棧中顯示:
[]
, - 根據從系統棧中取出的函式 A 的資訊,從函式 A 原來中斷的位置繼續開始執行,
- A 函式執行完了,系統棧中已經沒有函式資訊了,好的,程式結束。
- 存入系統棧中的是函式執行時的一些資訊,
- 所以取出來後,可以根據這些資訊來繼續完成
- 原來函式未執行完畢的那部分程式碼。
-
2 和 3 中解釋的原理 就是系統棧最神奇的地方
- 在程式設計的時候進行子過程呼叫的時候,
- 當一個子過程執行完成之後,
- 可以自動的回到上層呼叫中斷的位置,
- 並且繼續執行下去。
- 都是靠一個系統棧來記錄每一次呼叫過程中
- 中斷的那個呼叫的點來實現的。
-
棧雖然是一個非常簡單的資料結構
- 但是它能夠解決計算機領域非常複雜的一個問題,
- 這個問題就是這種子過程子邏輯的呼叫,
- 在編譯器內部它執行實現的原理是什麼,
- 深入理解這個過程,
- 甚至能夠幫助你理解一些更復雜的邏輯過程,
- 比如遞迴這樣的一個過程,你會有更加深刻的理解。
棧的實現
- 棧這種資料結構非常有用
- 但其實是非常簡單的。
MyStack
void push(e)
:入棧E pop()
:出棧E peek()
:檢視位於棧頂位置的元素int getSize()
:獲取棧中實際元素的個數boolean isEmpty()
:棧是否為空
- 從使用者的角度看
- 只要支援這些操作就好了,
- 使用者不管你要怎樣 resize,
- 他只要知道你這個陣列是一個動態的,
- 他可以不停的往裡面新增元素,
- 並且不會出現問題就 ok,
- 其實對於棧也是這樣的,
- 對於具體的底層實現,使用者不關心,
- 實際底層也有多種實現方式,
- 所以使用者就更加不關心了。
- 為了讓程式碼更加的清晰,
- 同時也是為了支援物件導向的一些特性,
- 比如說支援多型性,
- 那麼就會這樣的去設計,
- 定義一個介面叫做 IMyStack,
- 介面中有棧預設的所有方法,
- 然後再定義一個類叫做 MyStack,
- 讓它去實現 IMyStack,
- 這樣就可以在 MyStack 中完成對應的邏輯,
- 這個 MyStack 就是自定義的棧。
- 會複用到之前自定義陣列物件。
棧的複雜度分析
MyStack
void push(e)
:O(1) 均攤E pop()
:O(1) 均攤E peek()
:O(1)int getSize()
:O(1)boolean isEmpty()
:O(1)
程式碼示例
-
(class: MyArray, class: MyStack, class: Main)
-
MyArray
class MyArray { // 建構函式,傳入陣列的容量capacity構造Array 預設陣列的容量capacity=10 constructor(capacity = 10) { this.data = new Array(capacity); this.size = 0; } // 獲取陣列中的元素實際個數 getSize() { return this.size; } // 獲取陣列的容量 getCapacity() { return this.data.length; } // 判斷陣列是否為空 isEmpty() { return this.size === 0; } // 給陣列擴容 resize(capacity) { let newArray = new Array(capacity); for (var i = 0; i < this.size; i++) { newArray[i] = this.data[i]; } // let index = this.size - 1; // while (index > -1) { // newArray[index] = this.data[index]; // index --; // } this.data = newArray; } // 在指定索引處插入元素 insert(index, element) { // 先判斷陣列是否已滿 if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * 2); } // 然後判斷索引是否符合要求 if (index < 0 || index > this.size) { throw new Error( 'insert error. require index < 0 or index > size.' ); } // 最後 將指定索引處騰出來 // 從指定索引處開始,所有陣列元素全部往後移動一位 // 從後往前移動 for (let i = this.size - 1; i >= index; i--) { this.data[i + 1] = this.data[i]; } // 在指定索引處插入元素 this.data[index] = element; // 維護一下size this.size++; } // 擴充套件 在陣列最前面插入一個元素 unshift(element) { this.insert(0, element); } // 擴充套件 在陣列最後面插入一個元素 push(element) { this.insert(this.size, element); } // 其實在陣列中新增元素 就相當於在陣列最後面插入一個元素 add(element) { if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * 2); } // size其實指向的是 當前陣列最後一個元素的 後一個位置的索引。 this.data[this.size] = element; // 維護size this.size++; } // get get(index) { // 不能訪問沒有存放元素的位置 if (index < 0 || index >= this.size) { throw new Error('get error. index < 0 or index >= size.'); } return this.data[index]; } // 擴充套件: 獲取陣列中第一個元素 getFirst() { return this.get(0); } // 擴充套件: 獲取陣列中最後一個元素 getLast() { return this.get(this.size - 1); } // set set(index, newElement) { // 不能修改沒有存放元素的位置 if (index < 0 || index >= this.size) { throw new Error('set error. index < 0 or index >= size.'); } this.data[index] = newElement; } // contain contain(element) { for (var i = 0; i < this.size; i++) { if (this.data[i] === element) { return true; } } return false; } // find find(element) { for (var i = 0; i < this.size; i++) { if (this.data[i] === element) { return i; } } return -1; } // findAll findAll(element) { // 建立一個自定義陣列來存取這些 元素的索引 let myarray = new MyArray(this.size); for (var i = 0; i < this.size; i++) { if (this.data[i] === element) { myarray.push(i); } } // 返回這個自定義陣列 return myarray; } // 刪除指定索引處的元素 remove(index) { // 索引合法性驗證 if (index < 0 || index >= this.size) { throw new Error('remove error. index < 0 or index >= size.'); } // 暫存即將要被刪除的元素 let element = this.data[index]; // 後面的元素覆蓋前面的元素 for (let i = index; i < this.size - 1; i++) { this.data[i] = this.data[i + 1]; } this.size--; this.data[this.size] = null; // 如果size 為容量的四分之一時 就可以縮容了 // 防止複雜度震盪 if (Math.floor(this.getCapacity() / 4) === this.size) { // 縮容一半 this.resize(Math.floor(this.getCapacity() / 2)); } return element; } // 擴充套件:刪除陣列中第一個元素 shift() { return this.remove(0); } // 擴充套件: 刪除陣列中最後一個元素 pop() { return this.remove(this.size - 1); } // 擴充套件: 根據元素來進行刪除 removeElement(element) { let index = this.find(element); if (index !== -1) { this.remove(index); } } // 擴充套件: 根據元素來刪除所有元素 removeAllElement(element) { let index = this.find(element); while (index != -1) { this.remove(index); index = this.find(element); } // let indexArray = this.findAll(element); // let cur, index = 0; // for (var i = 0; i < indexArray.getSize(); i++) { // // 每刪除一個元素 原陣列中就少一個元素, // // 索引陣列中的索引值是按照大小順序排列的, // // 所以 這個cur記錄的是 原陣列元素索引的偏移量 // // 只有這樣才能夠正確的刪除元素。 // index = indexArray.get(i) - cur++; // this.remove(index); // } } // @Override toString 2018-10-17-jwl toString() { let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`; arrInfo += `data = [`; for (var i = 0; i < this.size - 1; i++) { arrInfo += `${this.data[i]}, `; } if (!this.isEmpty()) { arrInfo += `${this.data[this.size - 1]}`; } arrInfo += `]`; // 在頁面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 複製程式碼
-
MyStack
class MyStack { constructor(capacity = 10) { this.myArray = new MyArray(capacity); } // 入棧 push(element) { this.myArray.push(element); } // 出棧 pop() { return this.myArray.pop(); } // 檢視棧頂的元素 peek() { return this.myArray.getLast(); } // 棧中實際元素的個數 getSize() { return this.myArray.getSize(); } // 棧是否為空 isEmpty() { return this.myArray.isEmpty(); } // 檢視棧的容量 getCapacity() { return this.myArray.getCapacity(); } // @Override toString 2018-10-20-jwl toString() { let arrInfo = `Stack: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`; arrInfo += `data = [`; for (var i = 0; i < this.myArray.size - 1; i++) { arrInfo += `${this.myArray.data[i]}, `; } if (!this.isEmpty()) { arrInfo += `${this.myArray.data[this.myArray.size - 1]}`; } arrInfo += `] stack top is right!`; // 在頁面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 複製程式碼
-
Main
class Main { constructor() { this.alterLine('MyStack Area'); let ms = new MyStack(10); for (let i = 1; i <= 10; i++) { ms.push(i); console.log(ms.toString()); } console.log(ms.peek()); this.show(ms.peek()); while (!ms.isEmpty()) { console.log(ms.toString()); ms.pop(); } } // 將內容顯示在頁面上 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(); }; 複製程式碼
棧的應用
- undo 操作-編輯器
- 系統呼叫棧-作業系統
- 括號匹配-編譯器
以程式設計的方式體現棧的應用
-
括號匹配-編譯器
- 無論是寫表示式,這個表示式中有小括號、中括號、大括號,
- 自然會出現括號套括號的情況發生,
- 在這種情況下就一定會產生一個括號匹配的問題,
- 如果括號匹配是不成功的,那麼編譯器會進行報錯。
-
編譯器是如何檢查括號匹配的問題?
- 原理是使用了一個棧。
-
可以通過解答 Leetcode 中的一個問題,
- 同時來看棧在括號匹配這個問題中的應用。
- Leetcode 是總部在美國矽谷一家
- 非常有年頭又同時有信譽度的面向 IT 公司
- 面試這樣一個線上的平臺,
- 只需要註冊一個 Leetcode 使用者後,
- 就可以看到 Leetcode 上有非常多的問題,
- 對於每一個問題會規定輸入和輸出之後,
- 然後就可以編寫屬於自己的邏輯,
- 更重要的是可以直接把你編寫的這個程式
- 提交給這個網站,
- 這個網站會自動的判斷你的邏輯書寫的是否正確,
- 英文網址:
leetcode.com
, - 2017 中文網址:
leetcode-cn.com
-
leetcode.com
與leetcode-cn.com
的區別leetcode-cn.com
支援中文,leetcode-cn.com
的題目數量沒有英文版的多。leetcode-cn.com
的探索欄目的內容沒有英文版的多。leetcode-cn.com
中的題目沒有社群討論功能,但英文版的有。
-
leetcode 中第二十號題目:有效的括號
- 如:
{ [ ( ) ] }
, - 從左往右,先將左側的括號入棧,
- 然後遇到右側的括號時就檢視棧頂的左側括號進行匹配,
- 如果可以匹配表示括號有效,否則括號無效,
- 括號有效那麼就將棧頂的左側括號取出,
- 然後繼續從左往右,左側括號就入棧,右側括號就匹配,
- 匹配成功就讓左側括號出棧,匹配失敗就是無效括號。
- 其實棧頂元素反映了在巢狀的層級關係中,
- 最新的需要匹配的元素。
- 這個演算法非常的簡單,但是也非常的實用。
- 很多工具中都有這樣的邏輯來檢查括號的匹配。
class Solution { isValid(s) { // leetcode 20. 有效的括號 /** * @param {string} s * @return {boolean} */ var isValid = function(s) { let stack = []; // 以遍歷的方式進行匹配操作 for (let i = 0; i < s.length; i++) { // 是否是正括號 switch (s[i]) { case '{': case '[': case '(': stack.push(s[i]); break; default: break; } // 是否是反括號 switch (s[i]) { case '}': if (stack.length === 0 || stack.pop() !== '{') { console.log('valid error. not parentheses. in'); return false; } break; case ']': if (stack.length === 0 || stack.pop() !== '[') { console.log('valid error. not parentheses. in'); return false; } break; case ')': if (stack.length === 0 || stack.pop() !== '(') { console.log('valid error. not parentheses. in'); return false; } break; default: break; } } // 是否全部匹配成功 if (stack.length === 0) { return true; } else { console.log('valid error. not parentheses. out'); return false; } }; return isValid(s); } } 複製程式碼
class Main { constructor() { // this.alterLine("MyStack Area"); // let ms = new MyStack(10); // for (let i = 1; i <= 10 ; i++) { // ms.push(i); // console.log(ms.toString()); // } // console.log(ms.peek()); // this.show(ms.peek()); // while (!ms.isEmpty()) { // console.log(ms.toString()); // ms.pop(); // } this.alterLine('leetcode 20. 有效的括號'); let s = new Solution(); this.show(s.isValid('{ [ ( ) ] }')); this.show(s.isValid(' [ ( ] ) ')); } // 將內容顯示在頁面上 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(); }; 複製程式碼
- 如:
-
leetcode 是一個非常好的準備面試的一個平臺
- 同時它也是演算法競賽的一個入門的地方。
- 你可以通過題庫來進行訓練,
- 題庫的右邊有關於這些題目的標籤,
- 你可以選擇性的去練習,
- 而且可以根據難度來進行排序這些題目,
- 你不一定要全部答對,
- 因為這些題目不僅僅只有一個標籤。
-
如果你想使用你自己寫的類,
- 那麼你可以你自己寫的自定義棧作為內部類來進行使用,
- 例如 把自定義棧的程式碼放到 Solution 類中,
- 那樣也是可以使用,
- 還樣就順便測試了你自己資料結構實現的邏輯是否正確。
學習方法討論
- 不要完美主義。掌握好“度”。
- 太過於追求完美會把自己逼的太緊,
- 會產生各種焦慮的心態,. 最後甚至會懷疑自己,
- 溫故而知新,不要停止不前,
- 掌握好這個度,不存在你把那些你認為完全掌握了,
- 然後就成了某一個領域的專家,
- 相反一旦你產生很濃厚的厭惡感,
- 那麼就意味著你即將會放棄或者已經選擇了放棄,
- 雖然你之前想把它做到 100 分,
- 但是由於你的放棄讓它變為 0 分。
- 學習本著自己的目標去。
- 不要在學的過程中偏離了自己的目標。
- 要分清主次。
- 難的東西,你可以慢慢的回頭看一看。
- 那樣才會更加的柳暗花明,
- 更能提升自己的收穫。
佇列 Queue
- 佇列也是一種線性的資料結構
- 依然就是將資料排成一排。
- 相比陣列,佇列對應的操作是陣列的子集。
- 與棧只能在同一端新增元素和取出元素有所不同,
- 在佇列中只能從一端(隊尾)新增元素,
- 只能從另一端(隊首)取出元素。
- 例如你去銀行取錢
- 你需要排隊,入隊的人不允許插隊,
- 所以他要從隊尾開始排隊,
- 而前面取完錢的會從隊首離開,
- 然後後面的人再往前移動一位,
- 最後重複這個過程,
- 直到沒人再排隊取錢了。
- 佇列是一種先進先出的資料結構(先到先得)
- First In First Out(FIFO) 先進先出
佇列的實現
Queue
void enqueue(E)
:入隊E dequeue()
:出隊E getFront()
:檢視隊首的元素int getSize()
:獲取佇列中的實際元素大小boolean isEmpty()
:獲取佇列是否為空的 bool 值
- 寫一個介面叫做 IMyQueue
- 讓 MyQueue 實現這個介面
- 這樣就符合了物件導向的特性。
程式碼示例
-
class: MyArray, class: MyQueue, class: Main)
-
MyArray
class MyArray { // 建構函式,傳入陣列的容量capacity構造Array 預設陣列的容量capacity=10 constructor(capacity = 10) { this.data = new Array(capacity); this.size = 0; } // 獲取陣列中的元素實際個數 getSize() { return this.size; } // 獲取陣列的容量 getCapacity() { return this.data.length; } // 判斷陣列是否為空 isEmpty() { return this.size === 0; } // 給陣列擴容 resize(capacity) { let newArray = new Array(capacity); for (var i = 0; i < this.size; i++) { newArray[i] = this.data[i]; } // let index = this.size - 1; // while (index > -1) { // newArray[index] = this.data[index]; // index --; // } this.data = newArray; } // 在指定索引處插入元素 insert(index, element) { // 先判斷陣列是否已滿 if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * 2); } // 然後判斷索引是否符合要求 if (index < 0 || index > this.size) { throw new Error( 'insert error. require index < 0 or index > size.' ); } // 最後 將指定索引處騰出來 // 從指定索引處開始,所有陣列元素全部往後移動一位 // 從後往前移動 for (let i = this.size - 1; i >= index; i--) { this.data[i + 1] = this.data[i]; } // 在指定索引處插入元素 this.data[index] = element; // 維護一下size this.size++; } // 擴充套件 在陣列最前面插入一個元素 unshift(element) { this.insert(0, element); } // 擴充套件 在陣列最後面插入一個元素 push(element) { this.insert(this.size, element); } // 其實在陣列中新增元素 就相當於在陣列最後面插入一個元素 add(element) { if (this.size == this.getCapacity()) { // throw new Error("add error. Array is full."); this.resize(this.size * 2); } // size其實指向的是 當前陣列最後一個元素的 後一個位置的索引。 this.data[this.size] = element; // 維護size this.size++; } // get get(index) { // 不能訪問沒有存放元素的位置 if (index < 0 || index >= this.size) { throw new Error('get error. index < 0 or index >= size.'); } return this.data[index]; } // 擴充套件: 獲取陣列中第一個元素 getFirst() { return this.get(0); } // 擴充套件: 獲取陣列中最後一個元素 getLast() { return this.get(this.size - 1); } // set set(index, newElement) { // 不能修改沒有存放元素的位置 if (index < 0 || index >= this.size) { throw new Error('set error. index < 0 or index >= size.'); } this.data[index] = newElement; } // contain contain(element) { for (var i = 0; i < this.size; i++) { if (this.data[i] === element) { return true; } } return false; } // find find(element) { for (var i = 0; i < this.size; i++) { if (this.data[i] === element) { return i; } } return -1; } // findAll findAll(element) { // 建立一個自定義陣列來存取這些 元素的索引 let myarray = new MyArray(this.size); for (var i = 0; i < this.size; i++) { if (this.data[i] === element) { myarray.push(i); } } // 返回這個自定義陣列 return myarray; } // 刪除指定索引處的元素 remove(index) { // 索引合法性驗證 if (index < 0 || index >= this.size) { throw new Error('remove error. index < 0 or index >= size.'); } // 暫存即將要被刪除的元素 let element = this.data[index]; // 後面的元素覆蓋前面的元素 for (let i = index; i < this.size - 1; i++) { this.data[i] = this.data[i + 1]; } this.size--; this.data[this.size] = null; // 如果size 為容量的四分之一時 就可以縮容了 // 防止複雜度震盪 if (Math.floor(this.getCapacity() / 4) === this.size) { // 縮容一半 this.resize(Math.floor(this.getCapacity() / 2)); } return element; } // 擴充套件:刪除陣列中第一個元素 shift() { return this.remove(0); } // 擴充套件: 刪除陣列中最後一個元素 pop() { return this.remove(this.size - 1); } // 擴充套件: 根據元素來進行刪除 removeElement(element) { let index = this.find(element); if (index !== -1) { this.remove(index); } } // 擴充套件: 根據元素來刪除所有元素 removeAllElement(element) { let index = this.find(element); while (index != -1) { this.remove(index); index = this.find(element); } // let indexArray = this.findAll(element); // let cur, index = 0; // for (var i = 0; i < indexArray.getSize(); i++) { // // 每刪除一個元素 原陣列中就少一個元素, // // 索引陣列中的索引值是按照大小順序排列的, // // 所以 這個cur記錄的是 原陣列元素索引的偏移量 // // 只有這樣才能夠正確的刪除元素。 // index = indexArray.get(i) - cur++; // this.remove(index); // } } // @Override toString 2018-10-17-jwl toString() { let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`; arrInfo += `data = [`; for (var i = 0; i < this.size - 1; i++) { arrInfo += `${this.data[i]}, `; } if (!this.isEmpty()) { arrInfo += `${this.data[this.size - 1]}`; } arrInfo += `]`; // 在頁面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 複製程式碼
-
MyQueue
class MyQueue { constructor(capacity = 10) { this.myArray = new MyArray(capacity); } // 入隊 enqueue(element) { this.myArray.push(element); } // 出隊 dequeue() { return this.myArray.shift(); } // 檢視隊首的元素 getFront() { return this.myArray.getFirst(); } // 檢視佇列中實際元素的個數 getSize() { return this.myArray.getSize(); } // 檢視 佇列當前的容量 getCapacity() { return this.myArray.getCapacity(); } // 檢視佇列是否為空 isEmpty() { return this.myArray.isEmpty(); } // 輸出佇列中的資訊 // @Override toString 2018-10-20-jwl toString() { let arrInfo = `Queue: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`; arrInfo += `data = front [`; for (var i = 0; i < this.myArray.size - 1; i++) { arrInfo += `${this.myArray.data[i]}, `; } if (!this.isEmpty()) { arrInfo += `${this.myArray.data[this.myArray.size - 1]}`; } arrInfo += `] tail`; // 在頁面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 複製程式碼
-
Main
class Main { constructor() { this.alterLine('MyQueue Area'); let mq = new MyQueue(10); for (let i = 1; i <= 10; i++) { mq.enqueue(i); console.log(mq.toString()); } console.log(mq.getFront()); this.show(mq.getFront()); while (!mq.isEmpty()) { console.log(mq.toString()); mq.dequeue(); } } // 將內容顯示在頁面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割線 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } 複製程式碼
佇列的複雜度分析
MyQueue
void enqueue(E)
:O(1)
均攤E dequeue()
:O(n)
出隊的效能消耗太大了E getFront()
:O(1)
int getSize()
:O(1)
boolean isEmpty()
:O(1)
- 出隊的效能消耗太大了
- 如果有一百萬條資料,每次都要操作一百萬次,
- 那麼需要優化它,要讓他出隊的時候時間複雜度為
O(1)
, - 並且還要讓他入隊的時候時間複雜度依然是
O(1)
。 - 可以使用迴圈佇列的方式來解決這個問題。
迴圈佇列
- 自定義佇列的效能是有侷限性的
- 出隊操作時的時間複雜度為
O(n)
, - 要把他變為
O(1)
。
- 出隊操作時的時間複雜度為
- 當取出佇列的第一個元素後,
- 第一個元素後面所有的元素位置不動,
- 這樣一來時間複雜度就為
O(1)
了, - 下一次再取元素的時候從第二個開始,
- 取完第二個元素之後,
- 第二個元素後面所有的元素位置也不動,
- 入隊的話直接往隊尾新增元素即可。
- 迴圈佇列的使用
- 你可以先用一個數字變數 front 指向隊首,
- 然後再用一個數字變數 tail 指向隊尾,
- front 指向的是佇列中的第一個元素,
- tail 指向的是佇列中最後一個元素的後一個位置,
- 當佇列整體為空的時候,它們才會指向同一個位置,
- 所以
front == tail
時佇列就為空, - 如果有一個元素入隊了,
- front 會指向這個元素,
- 而 tail 會指向這個元素後一個位置(也就是 tail++),
- 然後再有一個元素入隊了,
- front 還是指向第一個元素的位置,
- 而 tail 會指向第二個元素的後一個位置(還是 tail++),
- 然後再來四個元素入隊了,
- front 還是指向第一個元素的位置,
- 而 tail 會指向第六個元素的後一個位置(tail++四次),
- 之後 要出隊兩個元素,
- front 會指向第三個元素的位置(也就是 front++兩次),
- front 從指向第一個元素變成指向第三個元素的位置,
- 因為前兩個已經出隊了,
- 這時候再入隊一個元素,
- tail 會指向第七個元素的後一個位置(還是 tail++),
- 這時佇列的容量已經滿了,可能需要擴容,
- 但是由於佇列中有兩個元素已經出隊了,
- 那這兩個位置空出來了,這時就需要利用這兩個位置的空間了,
- 這就是迴圈佇列了,以迴圈的方式重複利用空間,
- 自定義佇列使用自定義陣列實現的,
- 其實就是把陣列看成一個環,陣列中一共可以容納 8 個元素,
- 索引是 0-7,那麼 7 之後的索引應該是 0,tail 應該指向 0,
- 而不是認為整個陣列的空間已經滿了,
- 應該使用 tail 對陣列的容量進行求餘計算,
- tail 為 8,容量也為 8,求餘之後為 0,所以 tail 應該指向 0,
- 這時再入隊一個元素,tail 指向這個元素的後一個位置,即 1,
- 這時候如果再入隊一個元素,那麼此時 tail 和 front 相等,
- 但是那並不能證明佇列為空,反而是佇列滿了,
- 所以需要在佇列滿之前進行判斷,
tail+1==front
, - 就表示佇列已滿,當陣列中只剩最後一個空間了,
- 佇列就算是滿的,因為再入隊就會讓 tail 與 front 相等,
- 而那個條件是佇列已空才成立的,雖然對於整個陣列空間來說,
- 是有意識地浪費了一個空間,但是減少了很大的時間消耗,
- 所以當
(tail+1)%c==front
時就可以擴容了, - 將
tail+1==front
變成(tail+1)%c==front
是因為 - tail 從陣列的末端跑到前端是有一個求餘的過程,
- 例如 front 指向的是第一個元素,而 tail 指向的第六個元素之後的位置,
- 那麼此時 front 為 0,tail 為 7,容量為 8,還有一個浪費掉的空間,
- 這時候
(tail+1)%c==front
,所以佇列滿了, - 這就是迴圈佇列所有的具體實現必須遵守的規則,
- 所有的 front 和 tail 向後移動的過程都要是這種迴圈的移動,
- 例如鐘錶,11 點鐘的下一個鐘頭為 12 點鐘,也可以管它叫做 0 點,
- 之後又會變成 1 點、2 點、3 點、4 點依次類推,
- 所以整個迴圈佇列的索引也是像鐘錶一樣形成了一個環,
- 只不過不一定有 12 刻度,而刻度的數量是由陣列的容量(空間總數)決定的,
- 這就是迴圈佇列的原理。
- 使用迴圈佇列之後,
- 出隊操作不再是整體往前移動一位了
- 而是通過改變 front 的指向,
- 入隊操作則是改變 tail 的指向,
- 整個操作迴圈往復,
- 這樣一來出隊入隊的時間複雜度都為
O(1)
了。
迴圈佇列的簡單實現解析
- 迴圈佇列 MyLoopQueue
- 他的實現與 MyQueue 有很大的不同,
- 所以就不使用 MyArray 自定義動態陣列了。
- 迴圈佇列要從底層重新開始寫起
- data:一個陣列。
- front: 指向隊頭有效元素的索引。
- tail: 指向隊尾有效元素的後一個位置的索引。
- size: 通過 front 和 tail 也可以做到迴圈。
- 但是使用 size 能夠讓邏輯更加的清晰明瞭。
- 迴圈佇列實現完畢之後,
- 你可以不使用 size 來進行迴圈佇列的維護,
- 而完完全全的使用 front 和 tail,
- 這樣難度會稍微的難一點,
- 因為具體邏輯需要特別的小心,
- 會有一些小陷阱。
- 可以試著新增 resize 陣列擴容縮容功能到極致,
- 可以鍛鍊邏輯能力、程式編寫除錯能力等等。
迴圈佇列的實現
- 入隊前先判斷佇列是否已經滿了
- 判斷方式
(tail + 1) % data.length == front
- 判斷分析 (隊尾指向的索引 + 1)餘以陣列的容量是否為隊首指向的索引,
- 判斷方式
- 從使用者的角度上來看
- 佇列裡就是有這麼多元素,
- 一側是隊首一側是隊尾,
- 其它的內容包括實際的陣列的大小是使用者指定的容量大小+1,
- 這些實現細節,使用者是全部不知道的,給使用者遮蔽掉了,
- 這就是封裝自定義資料結構的目的所在,
- 使用者在具體使用這些自定義資料結構的時候,
- 只需要瞭解介面中所涉及到的這些方法即可,
- 至於它的內部細節使用者完全可以不用關心。
程式碼示例 (class: MyLoopQueue, class: Main)
-
MyLoopQueue
class MyLoopQueue { constructor(capacity = 10) { // 初始化新陣列 this.data = new Array(capacity); // 初始化 隊首、隊尾的值 (索引) this.front = this.tail = 0; // 佇列中實際元素個數 this.size = 0; } // 擴容 resize(capacity) { let newArray = new Array(capacity); let index = 0; for (let i = 0; i < this.size; i++) { // 索引可能會越界,於是就要取餘一下, // 如果越界了,就從隊首開始 index = (this.front + i) % this.getCapacity(); newArray[i] = this.data[index]; } this.data = newArray; this.front = 0; this.tail = this.size; } // 入隊 enqueue(element) { // 判斷佇列中是否已滿 if ((this.tail + 1) % this.getCapacity() === this.front) { this.resize(Math.floor(this.getCapacity() * 2)); } this.data[this.tail] = element; this.tail = (this.tail + 1) % this.getCapacity(); this.size++; } // 出隊 dequeue() { // 判斷佇列是否為空 if (this.isEmpty()) { throw new Error("can't dequeue from an empty queue."); } let element = this.data[this.front]; this.data[this.front] = null; this.front = (this.front + 1) % this.getCapacity(); this.size--; // 當size 為容量的四分之一時就縮容一倍 if (this.size === Math.floor(this.getCapacity() / 4)) { this.resize(this.getCapacity() / 2); } return element; } // 檢視隊首的元素 getFront() { if (this.isEmpty()) { throw new Error('queue is empty.'); } return this.data[front]; } // 檢視實際的元素個數 getSize() { return this.size; } // 檢視容量 getCapacity() { return this.data.length; } // 佇列是否為空 isEmpty() { // return this.size === 0; return this.front == this.tail; } // 輸出迴圈佇列中的資訊 // @Override toString 2018-10-20-jwl toString() { let arrInfo = `LoopQueue: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`; arrInfo += `data = front [`; for (var i = 0; i < this.myArray.size - 1; i++) { arrInfo += `${this.myArray.data[i]}, `; } if (!this.isEmpty()) { arrInfo += `${this.myArray.data[this.myArray.size - 1]}`; } arrInfo += `] tail`; // 在頁面上展示 document.body.innerHTML += `${arrInfo}<br /><br /> `; return arrInfo; } } 複製程式碼
-
Main
class Main { constructor() { this.alterLine('MyLoopQueue Area'); let mlq = new MyQueue(10); for (let i = 1; i <= 10; i++) { mlq.enqueue(i); console.log(mlq.toString()); } console.log(mlq.getFront()); this.show(mlq.getFront()); while (!mlq.isEmpty()) { console.log(mlq.toString()); mlq.dequeue(); } } // 將內容顯示在頁面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割線 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } 複製程式碼
自定義佇列兩種方式的對比
- 原來自定佇列的出隊時,時間複雜度為
O(n)
,- 使用迴圈佇列的方式後,
- 出隊時時間複雜度為
O(1)
, - 複雜度的分析只是一個抽象上的理論結果,
- 具體這個變化在效能上意味著會有一個質的飛躍,
- 佇列中元素越多,效能就更能夠體現出來。
自定義佇列的時間複雜度對比
MyQueue
:陣列佇列,使用了自定義陣列void enqueue(E)
:O(1)
均攤E dequeue()
:O(n)
出隊的效能消耗太大了E getFront()
:O(1)
int getSize()
:O(1)
boolean isEmpty()
:O(1)
MyLoopQueue
:迴圈佇列,沒有使用自定義陣列void enqueue(E)
:O(1)
均攤E dequeue()
:O(1)
均攤E getFront()
:O(1)
int getSize()
:O(1)
boolean isEmpty()
:O(1)
迴圈佇列的複雜度分析
- 通過設定迴圈佇列底層的機制
- 雖然稍微比陣列佇列要複雜一些,
- 但是這些複雜的工作是值得的,
- 因為他使得在陣列佇列中,
- 出隊本該有
O(n)
的複雜度變為了O(1)
的複雜度, - 但是這個
O(1)
為均攤的時間複雜度, - 因為出隊還是會涉及到縮容的操作,
- 在縮容的過程中還是免不了對佇列中所有的元素進行一次遍歷,
- 但是由於不可能每一次操作都會觸發縮容操作來遍歷所有的元素,
- 所以應該使用均攤複雜度的分析方式,那樣才更加合理。
- 迴圈佇列中所有的操作都是
O(1)
的時間複雜度。 O(n)
的複雜度要比O(1)
要慢,- 但是具體會慢多少可以通過程式來進行測試,
- 這樣就能夠知道在演算法領域和資料結構領域
- 要費這麼大的勁去研究更加優化的操作
- 這背後實際的意義到底在哪裡。
- 讓這兩個佇列進行入隊和出隊操作,
- 操作的次數為 100000 次,
- 通過在同一臺機器上的耗時情況,
- 就能夠知道效能有什麼不同。
- 資料佇列與迴圈佇列十萬次入隊出隊操作後的結果是:
MyQueue,time:15.463472711s
,MyLoopQueue,time:0.009602136s
,- 迴圈佇列就算操作一億次,
- 時間也才
MyLoopQueue,time:2.663835877s
, - 這個差距主要是在出隊的操作中體現出來的,
- 這個效能差距是上千倍,所以這也是效能優化的意義。
- 測試效能時,不要只測試一次,你可以測試 100 次
- 取平均值即可,因為這不光和你的程式相關,
- 還會和你當前計算機的狀態有關,
- 特別是在兩個演算法的時間複雜度一致時,
- 測試效能時可能出入會特別大,
- 因為這有多方面原因、如語法、語言、編譯器、直譯器等等,
- 這些都會導致你程式碼真正執行的邏輯機制
- 和你理論分析的是不一樣的,
- 但是當兩個演算法的時間複雜度不一致時,
- 這時候測試效能的結果肯定會有巨大的差異,
- 如
O(1)
對O(n)
、O(n)
對O(n^2)
、O(n)
對O(logn)
。
程式碼示例
-
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); } // 計算執行的時間,轉換為 天-小時-分鐘-秒-毫秒 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
class Main { constructor() { this.alterLine('Queues Comparison Area'); let mq = new MyQueue(); let mlq = new MyLoopQueue(); let performanceTest = new PerformanceTest(); let mqInfo = performanceTest.testQueue(mq, 10000); let mlqInfo = performanceTest.testQueue(mlq, 10000); this.alterLine('MyQueue Area'); console.log(mqInfo); this.show(mqInfo); this.alterLine('MyLoopQueue Area'); console.log(mlqInfo); this.show(mlqInfo); } // 將內容顯示在頁面上 show(content) { document.body.innerHTML += `${content}<br /><br />`; } // 展示分割線 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}<br /><br />`; } } 複製程式碼
佇列的應用
- 佇列的概念在生活中隨處可見
- 所以使用計算機來模擬生活中佇列,
- 如在業務方面你需要排隊,
- 或者更加專業的一些領域,
- 比如 網路資料包的排隊、
- 作業系統中執行任務的排隊等,
- 都可以使用佇列。
- 佇列本身是一個很複雜的問題
- 對於排隊來說,隊首到底怎麼定義,
- 是有多樣的定義方式的,也正因為如此,
- 所以存在廣義佇列這個概念,
- 這兩種自定義佇列
- 在組建計算機世界的其它演算法邏輯的時候
- 也是有重要的應用的,最典型的應用是廣度優先遍歷。