【從蛋殼到滿天飛】JS 資料結構解析和演算法實現-棧和佇列

哎喲迪奧發表於2019-03-20

思維導圖

前言

【從蛋殼到滿天飛】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. 相比陣列來說相應的操作更少,
    1. 棧對應的操作是陣列的子集,
    2. 因為它的本質就是一個陣列,
    3. 並且它有比陣列更多的限制。
  3. 棧的本質就是一個陣列
    1. 它將資料排開來放的,
    2. 新增元素的時候只能從棧的一端新增元素,
    3. 取出元素的時候也只能棧的一端取出元素,
    4. 這一端叫做棧頂,當這樣的限定了陣列,
    5. 從而形成了棧這種資料結構之後,
    6. 它可以在計算機世界中對於
    7. 組建邏輯產生非常非常重要的作用。
  4. 棧的操作
    1. 從棧頂新增元素,把元素一個一個的放入到棧中,
    2. 如新增值的時候為 1、2、3,
    3. 你取值的時候順序則為 3、2、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. 也是有重要的應用的,最典型的應用是廣度優先遍歷。

相關文章