【從蛋殼到滿天飛】JS 數據結構解析和算法實現-棧和隊列

思惟導圖

前言

【從蛋殼到滿天飛】JS 數據結構解析和算法實現,所有文章大概的內容以下: Arrays(數組)、Stacks(棧)、Queues(隊列)、LinkedList(鏈表)、Recursion(遞歸思想)、BinarySearchTree(二分搜索樹)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(優先隊列)、SegmentTree(線段樹)、Trie(字典樹)、UnionFind(並查集)、AVLTree(AVL 平衡樹)、RedBlackTree(紅黑平衡樹)、HashTable(哈希表)html

源代碼有三個:ES6(單個單個的 class 類型的 js 文件) | JS + HTML(一個 js 配合一個 html)| JAVA (一個一個的工程)前端

所有源代碼已上傳 github,點擊我吧,光看文章可以掌握兩成,動手敲代碼、動腦思考、畫圖才能夠掌握八成。git

本文章適合 對數據結構想了解而且感興趣的人羣,文章風格一如既往如此,就以爲手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加源碼,時間跨度也算將近半年時間了,但願對想學習數據結構的人或者正在學習數據結構的人羣有幫助。github

棧 Statck

  1. 棧也是一種線性結構
  2. 相比數組來講相應的操做更少,
    1. 棧對應的操做是數組的子集,
    2. 由於它的本質就是一個數組,
    3. 而且它有比數組更多的限制。
  3. 棧的本質就是一個數組
    1. 它將數據排開來放的,
    2. 添加元素的時候只能從棧的一端添加元素,
    3. 取出元素的時候也只能棧的一端取出元素,
    4. 這一端叫作棧頂,當這樣的限定了數組,
    5. 從而造成了棧這種數據結構以後,
    6. 它能夠在計算機世界中對於
    7. 組建邏輯產生很是很是重要的做用。
  4. 棧的操做
    1. 從棧頂添加元素,把元素一個一個的放入到棧中,
    2. 如添加值的時候爲 一、二、3,
    3. 你取值的時候順序則爲 三、二、1,
    4. 由於你添加元素是隻能從一端放入,
    5. 取出元素時也只能從一端取出,
    6. 而這一段就是棧頂,
    7. 棧的出口和入口都是同一個位置,
    8. 因此你只能按照先進後出、後進先出的順序
    9. 添加數據或者取出數據,不存在插入和索引。
  5. 棧是一種後進先出的數據結構
    1. 也就是 Last In First Out(LIFO),
    2. 這樣的一種數據結構,在計算機的世界裏,
    3. 它擁有着難以想象的做用,
    4. 不管是經典的算法仍是算法設計都接觸到
    5. 棧這種看似很簡單但其實應用很是普遍的數據結構,

棧的簡單應用

  1. 無處不在的 Undo 操做(撤銷)面試

    1. 編輯器的撤銷操做的原理就是靠一個棧來進行維護的,
    2. 如 將 每次輸入的內容依次放入棧中 我 喜歡 你,
    3. 若是 你 字寫錯,你撤銷一下,變成 我 喜歡,
    4. 再撤銷一下 變成 我。
  2. 程序調用的系統棧算法

    1. 程序調用時常常會出如今一個邏輯中間
    2. 先終止而後跳到另一個邏輯去執行,
    3. 所謂的子函數的調用就是這個過程,
    4. 在這個過程當中計算機就須要使用一個
    5. 稱爲系統棧的一個數據結構來記錄程序的調用過程。
    6. 例若有三個函數 A、B、C,
    7. 當 A 執行到一半的時候調用 B,
    8. 當 B 執行到一半的時候調用 C,
    9. C 函數能夠執行運行完,
    10. C 函數運行完了以後繼續運行未完成的 B 函數,
    11. B 函數運行完了就運行未完成 A 函數,
    12. A 函數運行完了就結束了。
    function A () {
          1 ...;
          2 B();
          3 ...;
       }
    
       function B () {
        1 ...;
        2 C();
        3 ...;
       }
    
       function C () {
        1 ...;
        2 ...;
        3 ...;
       }
    複製代碼
  3. 系統棧記錄的過程是:編程

    1. A 函數執行,在第二行中斷了,由於要去執行函數 B 了,
    2. 這時候函數信息A2會被放入系統棧中,系統棧中顯示:[A2]
    3. 而後 B 函數執行,在第二行也中斷了,由於要去執行函數 C 了,
    4. 這時候函數信息 B2 會被放入系統棧中,系統棧中顯示:[A2, B2]
    5. 而後 C 函數執行,C 函數沒有子函數可執行,那麼執行到底,函數 C 執行完畢,
    6. 從系統棧中取出函數 B 的信息,系統棧中顯示:[A2]
    7. 根據從系統棧中取出的函數 B 的信息,從函數 B 原來中斷的位置繼續開始執行,
    8. B 函數執行完畢了,這時候會再從系統棧中取出函數 A 的,系統棧中顯示:[]
    9. 根據從系統棧中取出的函數 A 的信息,從函數 A 原來中斷的位置繼續開始執行,
    10. A 函數執行完了,系統棧中已經沒有函數信息了,好的,程序結束。
    11. 存入系統棧中的是函數執行時的一些信息,
    12. 因此取出來後,能夠根據這些信息來繼續完成
    13. 原來函數未執行完畢的那部分代碼。
  4. 2 和 3 中解釋的原理 就是系統棧最神奇的地方數組

    1. 在編程的時候進行子過程調用的時候,
    2. 當一個子過程執行完成以後,
    3. 能夠自動的回到上層調用中斷的位置,
    4. 而且繼續執行下去。
    5. 都是靠一個系統棧來記錄每一次調用過程當中
    6. 中斷的那個調用的點來實現的。
  5. 棧雖然是一個很是簡單的數據結構性能優化

    1. 可是它可以解決計算機領域很是複雜的一個問題,
    2. 這個問題就是這種子過程子邏輯的調用,
    3. 在編譯器內部它運行實現的原理是什麼,
    4. 深刻理解這個過程,
    5. 甚至可以幫助你理解一些更復雜的邏輯過程,
    6. 好比遞歸這樣的一個過程,你會有更加深入的理解。

棧的實現

  1. 棧這種數據結構很是有用
    1. 但實際上是很是簡單的。
  2. MyStack
    1. void push(e):入棧
    2. E pop():出棧
    3. E peek():查看位於棧頂位置的元素
    4. int getSize():獲取棧中實際元素的個數
    5. boolean isEmpty():棧是否爲空
  3. 從用戶的角度看
    1. 只要支持這些操做就行了,
    2. 用戶無論你要怎樣 resize,
    3. 他只要知道你這個數組是一個動態的,
    4. 他能夠不停的往裏面添加元素,
    5. 而且不會出現問題就 ok,
    6. 其實對於棧也是這樣的,
    7. 對於具體的底層實現,用戶不關心,
    8. 實際底層也有多種實現方式,
    9. 因此用戶就更加不關心了。
  4. 爲了讓代碼更加的清晰,
    1. 同時也是爲了支持面向對象的一些特性,
    2. 好比說支持多態性,
    3. 那麼就會這樣的去設計,
    4. 定義一個接口叫作 IMyStack,
    5. 接口中有棧默認的全部方法,
    6. 而後再定義一個類叫作 MyStack,
    7. 讓它去實現 IMyStack,
    8. 這樣就能夠在 MyStack 中完成對應的邏輯,
    9. 這個 MyStack 就是自定義的棧。
  5. 會複用到以前自定義數組對象。

棧的複雜度分析

  1. MyStack
    1. void push(e):O(1) 均攤
    2. E pop():O(1) 均攤
    3. E peek():O(1)
    4. int getSize():O(1)
    5. boolean isEmpty():O(1)

代碼示例

  1. (class: MyArray, class: MyStack, class: Main)網絡

  2. 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;
       }
    }
    複製代碼
  3. 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;
       }
    }
    複製代碼
  4. 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();
    };
    複製代碼

棧的應用

  1. undo 操做-編輯器
  2. 系統調用棧-操做系統
  3. 括號匹配-編譯器

以編程的方式體現棧的應用

  1. 括號匹配-編譯器

    1. 不管是寫表達式,這個表達式中有小括號、中括號、大括號,
    2. 天然會出現括號套括號的狀況發生,
    3. 在這種狀況下就必定會產生一個括號匹配的問題,
    4. 若是括號匹配是不成功的,那麼編譯器會進行報錯。
  2. 編譯器是如何檢查括號匹配的問題?

    1. 原理是使用了一個棧。
  3. 能夠經過解答 Leetcode 中的一個問題,

    1. 同時來看棧在括號匹配這個問題中的應用。
    2. Leetcode 是總部在美國硅谷一家
    3. 很是有年頭又同時有信譽度的面向 IT 公司
    4. 面試這樣一個在線的平臺,
    5. 只須要註冊一個 Leetcode 用戶後,
    6. 就能夠看到 Leetcode 上有很是多的問題,
    7. 對於每個問題會規定輸入和輸出以後,
    8. 而後就能夠編寫屬於本身的邏輯,
    9. 更重要的是能夠直接把你編寫的這個程序
    10. 提交給這個網站,
    11. 這個網站會自動的判斷你的邏輯書寫的是否正確,
    12. 英文網址:leetcode.com
    13. 2017 中文網址:leetcode-cn.com
  4. leetcode.comleetcode-cn.com的區別

    1. leetcode-cn.com支持中文,
    2. leetcode-cn.com的題目數量沒有英文版的多。
    3. leetcode-cn.com的探索欄目的內容沒有英文版的多。
    4. leetcode-cn.com中的題目沒有社區討論功能,但英文版的有。
  5. leetcode 中第二十號題目:有效的括號

    1. 如:{ [ ( ) ] }
    2. 從左往右,先將左側的括號入棧,
    3. 而後遇到右側的括號時就查看棧頂的左側括號進行匹配,
    4. 若是能夠匹配表示括號有效,不然括號無效,
    5. 括號有效那麼就將棧頂的左側括號取出,
    6. 而後繼續從左往右,左側括號就入棧,右側括號就匹配,
    7. 匹配成功就讓左側括號出棧,匹配失敗就是無效括號。
    8. 其實棧頂元素反映了在嵌套的層級關係中,
    9. 最新的須要匹配的元素。
    10. 這個算法很是的簡單,可是也很是的實用。
    11. 不少工具中都有這樣的邏輯來檢查括號的匹配。
    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();
    };
    複製代碼
  6. leetcode 是一個很是好的準備面試的一個平臺

    1. 同時它也是算法競賽的一個入門的地方。
    2. 你能夠經過題庫來進行訓練,
    3. 題庫的右邊有關於這些題目的標籤,
    4. 你能夠選擇性的去練習,
    5. 並且能夠根據難度來進行排序這些題目,
    6. 你不必定要所有答對,
    7. 由於這些題目不只僅只有一個標籤。
  7. 若是你想使用你本身寫的類,

    1. 那麼你能夠你本身寫的自定義棧做爲內部類來進行使用,
    2. 例如 把自定義棧的代碼放到 Solution 類中,
    3. 那樣也是可使用,
    4. 還樣就順便測試了你本身數據結構實現的邏輯是否正確。

學習方法討論

  1. 不要完美主義。掌握好「度」。
    1. 太過於追求完美會把本身逼的太緊,
    2. 會產生各類焦慮的心態,. 最後甚至會懷疑本身,
    3. 溫故而知新,不要中止不前,
    4. 掌握好這個度,不存在你把那些你認爲徹底掌握了,
    5. 而後就成了某一個領域的專家,
    6. 相反一旦你產生很濃厚的厭惡感,
    7. 那麼就意味着你即將會放棄或者已經選擇了放棄,
    8. 雖然你以前想把它作到 100 分,
    9. 可是因爲你的放棄讓它變爲 0 分。
  2. 學習本着本身的目標去。
    1. 不要在學的過程當中偏離了本身的目標。
    2. 要分清主次。
  3. 難的東西,你能夠慢慢的回頭看一看。
    1. 那樣纔會更加的柳暗花明,
    2. 更能提高本身的收穫。

隊列 Queue

  1. 隊列也是一種線性的數據結構
    1. 依然就是將數據排成一排。
  2. 相比數組,隊列對應的操做是數組的子集。
    1. 與棧只能在同一端添加元素和取出元素有所不一樣,
    2. 在隊列中只能從一端(隊尾)添加元素,
    3. 只能從另外一端(隊首)取出元素。
  3. 例如你去銀行取錢
    1. 你須要排隊,入隊的人不容許插隊,
    2. 因此他要從隊尾開始排隊,
    3. 而前面取完錢的會從隊首離開,
    4. 而後後面的人再往前移動一位,
    5. 最後重複這個過程,
    6. 直到沒人再排隊取錢了。
  4. 隊列是一種先進先出的數據結構(先到先得)
    1. First In First Out(FIFO) 先進先出

隊列的實現

  1. Queue
    1. void enqueue(E):入隊
    2. E dequeue():出隊
    3. E getFront():查看隊首的元素
    4. int getSize():獲取隊列中的實際元素大小
    5. boolean isEmpty():獲取隊列是否爲空的 bool 值
  2. 寫一個接口叫作 IMyQueue
    1. 讓 MyQueue 實現這個接口
    2. 這樣就符合了面向對象的特性。

代碼示例

  1. class: MyArray, class: MyQueue, class: Main)

  2. 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;
       }
    }
    複製代碼
  3. 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;
       }
    }
    複製代碼
  4. 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 />`;
       }
    }
    複製代碼

隊列的複雜度分析

  1. MyQueue
    1. void enqueue(E)O(1) 均攤
    2. E dequeue()O(n) 出隊的性能消耗太大了
    3. E getFront()O(1)
    4. int getSize()O(1)
    5. boolean isEmpty()O(1)
  2. 出隊的性能消耗太大了
    1. 若是有一百萬條數據,每次都要操做一百萬次,
    2. 那麼須要優化它,要讓他出隊的時候時間複雜度爲O(1)
    3. 而且還要讓他入隊的時候時間複雜度依然是O(1)
    4. 可使用循環隊列的方式來解決這個問題。

循環隊列

  1. 自定義隊列的性能是有侷限性的
    1. 出隊操做時的時間複雜度爲O(n)
    2. 要把他變爲O(1)
  2. 當取出隊列的第一個元素後,
    1. 第一個元素後面全部的元素位置不動,
    2. 這樣一來時間複雜度就爲O(1)了,
    3. 下一次再取元素的時候從第二個開始,
    4. 取完第二個元素以後,
    5. 第二個元素後面全部的元素位置也不動,
    6. 入隊的話直接往隊尾添加元素便可。
  3. 循環隊列的使用
    1. 你能夠先用一個數字變量 front 指向隊首,
    2. 而後再用一個數字變量 tail 指向隊尾,
    3. front 指向的是隊列中的第一個元素,
    4. tail 指向的是隊列中最後一個元素的後一個位置,
    5. 當隊列總體爲空的時候,它們纔會指向同一個位置,
    6. 因此front == tail時隊列就爲空,
    7. 若是有一個元素入隊了,
    8. front 會指向這個元素,
    9. 而 tail 會指向這個元素後一個位置(也就是 tail++),
    10. 而後再有一個元素入隊了,
    11. front 仍是指向第一個元素的位置,
    12. 而 tail 會指向第二個元素的後一個位置(仍是 tail++),
    13. 而後再來四個元素入隊了,
    14. front 仍是指向第一個元素的位置,
    15. 而 tail 會指向第六個元素的後一個位置(tail++四次),
    16. 以後 要出隊兩個元素,
    17. front 會指向第三個元素的位置(也就是 front++兩次),
    18. front 從指向第一個元素變成指向第三個元素的位置,
    19. 由於前兩個已經出隊了,
    20. 這時候再入隊一個元素,
    21. tail 會指向第七個元素的後一個位置(仍是 tail++),
    22. 這時隊列的容量已經滿了,可能須要擴容,
    23. 可是因爲隊列中有兩個元素已經出隊了,
    24. 那這兩個位置空出來了,這時就須要利用這兩個位置的空間了,
    25. 這就是循環隊列了,以循環的方式重複利用空間,
    26. 自定義隊列使用自定義數組實現的,
    27. 其實就是把數組當作一個環,數組中一共能夠容納 8 個元素,
    28. 索引是 0-7,那麼 7 以後的索引應該是 0,tail 應該指向 0,
    29. 而不是認爲整個數組的空間已經滿了,
    30. 應該使用 tail 對數組的容量進行求餘計算,
    31. tail 爲 8,容量也爲 8,求餘以後爲 0,因此 tail 應該指向 0,
    32. 這時再入隊一個元素,tail 指向這個元素的後一個位置,即 1,
    33. 這時候若是再入隊一個元素,那麼此時 tail 和 front 相等,
    34. 可是那並不能證實隊列爲空,反而是隊列滿了,
    35. 因此須要在隊列滿以前進行判斷,tail+1==front
    36. 就表示隊列已滿,當數組中只剩最後一個空間了,
    37. 隊列就算是滿的,由於再入隊就會讓 tail 與 front 相等,
    38. 而那個條件是隊列已空才成立的,雖然對於整個數組空間來講,
    39. 是有意識地浪費了一個空間,可是減小了很大的時間消耗,
    40. 因此當(tail+1)%c==front時就能夠擴容了,
    41. tail+1==front變成(tail+1)%c==front是由於
    42. tail 從數組的末端跑到前端是有一個求餘的過程,
    43. 例如 front 指向的是第一個元素,而 tail 指向的第六個元素以後的位置,
    44. 那麼此時 front 爲 0,tail 爲 7,容量爲 8,還有一個浪費掉的空間,
    45. 這時候(tail+1)%c==front,因此隊列滿了,
    46. 這就是循環隊列全部的具體實現必須遵照的規則,
    47. 全部的 front 和 tail 向後移動的過程都要是這種循環的移動,
    48. 例如鐘錶,11 點鐘的下一個鐘頭爲 12 點鐘,也能夠管它叫作 0 點,
    49. 以後又會變成 1 點、2 點、3 點、4 點依次類推,
    50. 因此整個循環隊列的索引也是像鐘錶同樣造成了一個環,
    51. 只不過不必定有 12 刻度,而刻度的數量是由數組的容量(空間總數)決定的,
    52. 這就是循環隊列的原理。
  4. 使用循環隊列以後,
    1. 出隊操做再也不是總體往前移動一位了
    2. 而是經過改變 front 的指向,
    3. 入隊操做則是改變 tail 的指向,
    4. 整個操做循環往復,
    5. 這樣一來出隊入隊的時間複雜度都爲O(1)了。

循環隊列的簡單實現解析

  1. 循環隊列 MyLoopQueue
    1. 他的實現與 MyQueue 有很大的不一樣,
    2. 因此就不使用 MyArray 自定義動態數組了。
  2. 循環隊列要從底層從新開始寫起
    1. data:一個數組。
    2. front: 指向隊頭有效元素的索引。
    3. tail: 指向隊尾有效元素的後一個位置的索引。
    4. size: 經過 front 和 tail 也能夠作到循環。
    5. 可是使用 size 可以讓邏輯更加的清晰明瞭。
  3. 循環隊列實現完畢以後,
    1. 你能夠不使用 size 來進行循環隊列的維護,
    2. 而完徹底全的使用 front 和 tail,
    3. 這樣難度會稍微的難一點,
    4. 由於具體邏輯須要特別的當心,
    5. 會有一些小陷阱。
    6. 能夠試着添加 resize 數組擴容縮容功能到極致,
    7. 能夠鍛鍊邏輯能力、程序編寫調試能力等等。

循環隊列的實現

  1. 入隊前先判斷隊列是否已經滿了
    1. 判斷方式 (tail + 1) % data.length == front
    2. 判斷分析 (隊尾指向的索引 + 1)餘以數組的容量是否爲隊首指向的索引,
  2. 從用戶的角度上來看
    1. 隊列裏就是有這麼多元素,
    2. 一側是隊首一側是隊尾,
    3. 其它的內容包括實際的數組的大小是用戶指定的容量大小+1,
    4. 這些實現細節,用戶是所有不知道的,給用戶屏蔽掉了,
    5. 這就是封裝自定義數據結構的目的所在,
    6. 用戶在具體使用這些自定義數據結構的時候,
    7. 只須要了解接口中所涉及到的這些方法便可,
    8. 至於它的內部細節用戶徹底能夠不用關心。

代碼示例 (class: MyLoopQueue, class: Main)

  1. 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;
       }
    }
    複製代碼
  2. 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 />`;
       }
    }
    複製代碼

自定義隊列兩種方式的對比

  1. 原來自定隊列的出隊時,時間複雜度爲O(n)
    1. 使用循環隊列的方式後,
    2. 出隊時時間複雜度爲O(1)
    3. 複雜度的分析只是一個抽象上的理論結果,
    4. 具體這個變化在性能上意味着會有一個質的飛躍,
    5. 隊列中元素越多,性能就更可以體現出來。

自定義隊列的時間複雜度對比

  1. MyQueue:數組隊列,使用了自定義數組
    1. void enqueue(E)O(1) 均攤
    2. E dequeue()O(n) 出隊的性能消耗太大了
    3. E getFront()O(1)
    4. int getSize()O(1)
    5. boolean isEmpty()O(1)
  2. MyLoopQueue:循環隊列,沒有使用自定義數組
    1. void enqueue(E)O(1) 均攤
    2. E dequeue()O(1) 均攤
    3. E getFront()O(1)
    4. int getSize()O(1)
    5. boolean isEmpty()O(1)

循環隊列的複雜度分析

  1. 經過設置循環隊列底層的機制
    1. 雖然稍微比數組隊列要複雜一些,
    2. 可是這些複雜的工做是值得的,
    3. 由於他使得在數組隊列中,
    4. 出隊本該有O(n)的複雜度變爲了O(1)的複雜度,
    5. 可是這個O(1)爲均攤的時間複雜度,
    6. 由於出隊仍是會涉及到縮容的操做,
    7. 在縮容的過程當中仍是免不了對隊列中全部的元素進行一次遍歷,
    8. 可是因爲不可能每一次操做都會觸發縮容操做來遍歷全部的元素,
    9. 因此應該使用均攤複雜度的分析方式,那樣才更加合理。
  2. 循環隊列中全部的操做都是O(1)的時間複雜度。
  3. O(n)的複雜度要比O(1)要慢,
    1. 可是具體會慢多少能夠經過程序來進行測試,
    2. 這樣就可以知道在算法領域和數據結構領域
    3. 要費這麼大的勁去研究更加優化的操做
    4. 這背後實際的意義到底在哪裏。
  4. 讓這兩個隊列進行入隊和出隊操做,
    1. 操做的次數爲 100000 次,
    2. 經過在同一臺機器上的耗時狀況,
    3. 就可以知道性能有什麼不一樣。
  5. 數據隊列與循環隊列十萬次入隊出隊操做後的結果是:
    1. MyQueue,time:15.463472711s
    2. MyLoopQueue,time:0.009602136s
    3. 循環隊列就算操做一億次,
    4. 時間也才MyLoopQueue,time:2.663835877s
    5. 這個差距主要是在出隊的操做中體現出來的,
    6. 這個性能差距是上千倍,因此這也是性能優化的意義。
  6. 測試性能時,不要只測試一次,你能夠測試 100 次
    1. 取平均值便可,由於這不光和你的程序相關,
    2. 還會和你當前計算機的狀態有關,
    3. 特別是在兩個算法的時間複雜度一致時,
    4. 測試性能時可能出入會特別大,
    5. 由於這有多方面緣由、如語法、語言、編譯器、解釋器等等,
    6. 這些都會致使你代碼真正運行的邏輯機制
    7. 和你理論分析的是不同的,
    8. 可是當兩個算法的時間複雜度不一致時,
    9. 這時候測試性能的結果確定會有巨大的差別,
    10. O(1)O(n)O(n)O(n^2)O(n)O(logn)

代碼示例

  1. 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;
       }
    }
    複製代碼
  2. 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 />`;
       }
    }
    複製代碼

隊列的應用

  1. 隊列的概念在生活中隨處可見
    1. 因此使用計算機來模擬生活中隊列,
    2. 如在業務方面你須要排隊,
    3. 或者更加專業的一些領域,
    4. 好比 網絡數據包的排隊、
    5. 操做系統中執行任務的排隊等,
    6. 均可以使用隊列。
  2. 隊列自己是一個很複雜的問題
    1. 對於排隊來講,隊首到底怎麼定義,
    2. 是有多樣的定義方式的,也正由於如此,
    3. 因此存在廣義隊列這個概念,
    4. 這兩種自定義隊列
    5. 在組建計算機世界的其它算法邏輯的時候
    6. 也是有重要的應用的,最典型的應用是廣度優先遍歷。
相關文章
相關標籤/搜索