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

哎喲迪奧發表於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,點選我吧,光看文章能夠掌握兩成,動手敲程式碼、動腦思考、畫圖才可以掌握八成。

本文章適合 對資料結構想了解並且感興趣的人群,文章風格一如既往如此,就覺得手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加原始碼,時間跨度也算將近半年時間了,希望對想學習資料結構的人或者正在學習資料結構的人群有幫助。

JS 中的陣列

陣列基礎

  1. 把資料碼成一排進行存放

    1. 強語言中資料的型別是確認的,
    2. 弱語言中資料的型別是不確認的,
    3. 但是也有方法可以進行確認。
    4. 陣列就是把一個一個的資料近挨著排成一排。
    5. 可以給一個陣列起一個名字,起名字要有語義化。
    6. 陣列中有一個很重要的概念叫做索引,
    7. 也就是陣列元素的編號,編號從 0 開始的,
    8. 所以最後一個元素的索引為陣列的長度-1 即 n-1,
    9. 可以通過陣列名[索引]來訪問陣列中的元素。
  2. js 中的陣列是有侷限性的。

    'use strict';
    
    // 輸出
    console.log('Array');
    
    // 定義陣列
    let arr = new Array(10);
    
    for (var i = 0; i < arr.length; i++) {
       arr[i] = i;
    }
    
    // 定義陣列
    let scores = new Array(100, 99, 98);
    
    // 遍歷輸出
    for (var i = 0; i < scores.length; i++) {
       console.log(scores[i]);
    }
    
    // 修改陣列中某個元素
    scores[1] = 60;
    
    // foreach 遍歷陣列
    for (let index in scores) {
       console.log(scores[index]);
    }
    複製程式碼

二次封裝陣列

  1. 陣列的索引可以有語意也可以沒有語意。
    1. scores[2] 代表一個班級中第三個學生。
  2. 陣列的最大優點
    1. 快速查詢,如 scores[2]
  3. 陣列最好應用於“索引有語意”的情況。
    1. 如果索引沒有語意的話,
    2. 那麼使用別的資料結構那會是一個更好的選擇。
  4. 計算機處理的問題有千千萬萬個
    1. 有很多場景即使能給索引定義出來語意,
    2. 但是它有可能不適用於陣列。
    3. 比如身份證號可以設計為一個陣列,
    4. 用來儲存相應的工資情況,
    5. 如果想索引到不同的人,
    6. 那麼使用身份證號就是一個很好的方式,
    7. 但是身份證號不能作為一個陣列的索引,
    8. 因為這個身份證號太大了,
    9. 如果想要使用身份證號作為一個陣列的索引,
    10. 那麼開闢的空間會非常的大,
    11. 例如arr[110103198512112312]
    12. 對於一般的計算機來說開闢這樣的一塊兒空間,
    13. 是非常不值當的,甚至是不可能的,
    14. 而且大部分空間都是浪費的,
    15. 比如你就想考察 100 個人的工資情況,
    16. 你卻開闢了 1000 兆倍的空間。
  5. 陣列也可以處理“索引沒有語意”的情況。
    1. 在索引有語意的情況下使用陣列非常簡單,
    2. 直接就可以查到相應的資料。
    3. 在索引沒有語義的情況下使用陣列,
    4. 那麼就會產生很多新的問題。
    5. 因為這個時候陣列只是一個待存
    6. 放那些要考察的資料的空間,
    7. 例如你開闢了 8 個元素的空間,
    8. 但是你只考察 2 個元素,
    9. 此時就有問題了,剩下的空間都沒有元素,
    10. 可能訪問剩下的空間就是非法的,
    11. 因為從使用者的角度上來看是沒有那麼多元素的,
    12. 只有兩個元素。

將陣列封裝成自己的陣列

  1. 將原本 js 中的陣列封裝到一個類中,

    1. 從而封裝一個屬於自己的陣列,這個類就叫做 MyArray,
    2. 在這個類中封裝一個 js 的陣列,這個陣列叫做 data,
    3. 對這個陣列進行增刪改插等等的功能。
  2. 資料結構的本質也是儲存資料,

    1. 之後再進行高效的對這些資料進行操作,
    2. 只不過你設計的資料結構會把這些資料儲存在記憶體中,
    3. 所以針對這些資料結構的所新增的操作在大的類別的劃分上,
    4. 也是增刪改查。
  3. 針對不同的資料結構,

    1. 對應的增刪改查的方式是截然不同的,
    2. 甚至某些資料結構會忽略掉增刪改查中的某一個操作,
    3. 但是增刪改查可以作為研究某一個資料結構的相應的脈絡,
  4. 陣列本身是靜態的,必須在建立的時候指定他的大小,

    1. 可以把這個容量叫做 capacity,
    2. 也就是陣列空間最多可以裝多少個元素,
    3. 陣列空間最多可以裝多少個元素與
    4. 陣列中實際裝多少個元素是沒有關係的,
    5. 因為這是兩回事兒,
    6. 陣列中實際能夠裝多少個元素可以叫做 size,
    7. 通過它來控制,在初始化的時候,
    8. 陣列中一個元素都沒有,所以 size 為 0,
    9. 這個 size 相當於陣列中第一個沒有盛放元素的相應索引,
    10. 增加陣列元素和刪除陣列元素的時候就要維護這個 size。
  5. 程式碼示例(class: MyArray)

    class MyArray {
       // 建構函式,傳入陣列的容量capacity構造Array 預設陣列的容量capacity=10
       constructor(capacity = 10) {
          this.data = new Array(10);
          this.size = 0;
       }
    
       // 獲取陣列中的元素實際個數
       getSize() {
          return this.size;
       }
    
       // 獲取陣列的容量
       getCapacity() {
          return this.data.length;
       }
    
       // 判斷陣列是否為空
       isEmpty() {
          return this.size === 0;
       }
    }
    複製程式碼

對自己的陣列進行新增操作

  1. 向陣列新增元素最簡單的形式
    1. 就是在陣列的末尾新增一個元素,
    2. size 這個變數其實就是指向陣列中的末尾,
    3. 新增完元素之後其實也需要維護這個 size,
    4. 因為陣列中的元素多了一個,所以要讓它加加。
  2. 如果是給元素進行插入的操作
    1. 那麼要先判陣列的容量是否已經裝滿了,
    2. 然後再判斷索引是否小於 0 或者大於 size,
    3. 都沒有問題了,就可以根據索引來進行插入了,
    4. 插入的原理就是那個索引位置及其後的元素,
    5. 全都都往後移動一位,所以迴圈是從後往前的,
    6. 最後讓該索引處的舊元素被新元素覆蓋,
    7. 但舊元素並沒消失,而是位置往後移動了一位,
    8. 最後要記得維護 size。
  3. 向陣列中新增元素可以複用向陣列中插入元素的方法,
    1. 因為插入元素的方法也是在給陣列新增元素,
    2. 並且插入元素的方法可以在任何位置插入新元素,
    3. 那麼就可以擴充套件兩個方法,
    4. 一個插入到陣列最前面(插入到索引為 0 的位置),
    5. 一個是插入到陣列最後面
    6. (插入到索引為 陣列最後一個元素的索引+1 的位置)。
  4. 給陣列新增元素的時候如果元素為數字(新增時可排序可不排序)
    1. 那麼每一次新增操作時可以給陣列中的元素進行排序,
    2. 排序方式是按照從小到大來進行排序。
    3. 先判斷新增的這個元素大於陣列中哪一個元素,
    4. 然後將那個元素及其後面的所有元素往後移一位,
    5. 最後將新增的這個元素插入到那個元素後面。
    6. 先要對陣列中的容量進行判斷,
    7. 如果超過了就不新增,並且報錯,
    8. 每次新增之前要判斷一下插入的位置,
    9. 它後面還有沒有元素或者這個陣列是否為空。
    10. 記住每次新增操作都要維護 size 這個變數。

程式碼示例(class: MyArray)

class MyArray {
   // 建構函式,傳入陣列的容量capacity構造Array 預設陣列的容量capacity=10
   constructor(capacity = 10) {
      this.data = new Array(10);
      this.size = 0;
   }

   // 獲取陣列中的元素實際個數
   getSize() {
      return this.size;
   }

   // 獲取陣列的容量
   getCapacity() {
      return this.data.length;
   }

   // 判斷陣列是否為空
   isEmpty() {
      return this.size === 0;
   }

   // 在指定索引處插入元素
   insert(index, element) {
      // 先判斷陣列是否已滿
      if (this.size == this.getCapacity()) {
         throw new Error('add error. Array is full.');
      }

      // 然後判斷索引是否符合要求
      if (index < 0 || index > size) {
         throw new Error('insert error. require  index < 0 or index > size');
      }

      // 最後 將指定索引處騰出來
      // 從指定索引處開始,所有陣列元素全部往後移動一位
      // 從後往前移動
      for (let i = size - 1; i >= index; i--) {
         this.data[i + 1] = this.data[i];
      }

      // 在指定索引處插入元素
      this.data[index] = element;
      // 維護一下size
      size++;
   }

   // 擴充套件 在陣列最前面插入一個元素
   unshift(element) {
      insert(0, element);
   }

   // 擴充套件 在陣列最後面插入一個元素
   push(element) {
      insert(size, element);
   }

   // 其實在陣列中新增元素 就相當於在陣列最後面插入一個元素
   add(element) {
      if (this.size == getCapacity()) {
         throw new Error('add error. Array is full.');
      }

      // size其實指向的是 當前陣列最後一個元素的 後一個位置的索引。
      this.data[size] = element;
      // 維護size
      size++;
   }
}
複製程式碼

對自己的陣列進行查詢和修改操作

  1. 如果你要覆蓋父類中的方法,記得要加備註
    1. // @Override: 方法名 日期-開發人員
  2. 獲取自定義陣列中指定索引位置的元素
    1. 首先要判斷索引是否小於 0 或者
    2. 大於等於 實際元素的個數,都沒有問題時,
    3. 就可以返回索引位置的元素了。
    4. 使用者沒有辦法去訪問那些沒有使用的陣列空間。
  3. 修改自動陣列中指定索引位置的元素
    1. 和獲取是一樣的,要先判斷,
    2. 只能設定已有存在的元素索引位置的元素,
    3. 使用者沒有辦法去修改那些沒有使用的陣列空間。

程式碼示例(class: MyArray, class: Main)

  1. 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;
       }
    
       // 在指定索引處插入元素
       insert(index, element) {
          // 先判斷陣列是否已滿
          if (this.size == this.getCapacity()) {
             throw new Error('add error. Array is full.');
          }
    
          // 然後判斷索引是否符合要求
          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.');
          }
    
          // 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];
       }
    
       // set
       set(index, newElement) {
          // 不能修改沒有存放元素的位置
          if (index < 0 || index >= this.size) {
             throw new Error('set error. index < 0 or index >= size');
          }
          this.data[index] = newElement;
       }
    
       // @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]}, `;
          }
          arrInfo += `${this.data[this.size - 1]}`;
          arrInfo += `]`;
    
          // 在頁面上展示
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製程式碼
  2. Main

    class Main {
       constructor() {
          let ma = new MyArray(20);
          for (let i = 1; i <= 10; i++) {
             ma.add(i);
          }
    
          console.log(ma.toString());
    
          ma.insert(1, 200);
          console.log(ma.toString());
    
          ma.unshift(-1);
          console.log(ma.toString());
    
          ma.push(9999);
          console.log(ma.toString());
    
          ma.set(5, 8888);
          console.log(ma.get(5));
          this.show(ma.get(5));
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    }
    
    window.onload = function() {
       // 執行主函式
       new Main();
    };
    複製程式碼

對自己的陣列進行包含、查詢、和刪除操作

  1. 繼續對自定義的陣列增加新功能
    1. 包含、搜尋、刪除這三個功能。
  2. 包含:
    1. 判斷陣列中 是否存在這個元素,
    2. 不存在返回 false。
  3. 搜尋:
    1. 根據這個元素來進行 索引的獲取,
    2. 找不到返回 非法索引 -1。
  4. 刪除:
    1. 首先要判斷索引的合法性,
    2. 其次這個操作與插入的操作其實原理類似,
    3. 只不過是一個反向的過程,
    4. 指定索引位置的元素其後面的元素的位置
    5. 全部往前一位,迴圈時 初始索引為 指定的這個索引,
    6. 從前往後的不斷向前移動,這樣被刪除的元素就被覆蓋了,
    7. 覆蓋之前要儲存一下指定索引的元素的值,
    8. 這個值要作為返回值來進行返回,
    9. 然後讓 size 減減,因為覆蓋掉這個元素,
  5. 由於陣列訪問會有索引合法性的判斷
    1. 一定要小於 size,於是使用者是訪問不到 size 位置的元素了,
    2. 所以 size 位置的元素可以不用去處理它,
    3. 但你也可以手動的將這個元素值設定為預設值。
  6. 有了指定索引刪除某個元素並返回被刪除的這個元素的操作
    1. 那麼就可以擴充套件出兩個方法,
    2. 和使用插入方法來進行擴充套件的那兩個方法類似,
    3. 分別是 刪除第一個元素和刪除最後一個元素,
    4. 並且返回被刪除的元素,
    5. 刪除陣列元素時會判斷陣列索引的合法性,
    6. 如果陣列為空,那麼合法性驗證就無法通過。
  7. 根據元素來刪除陣列中的某個元素
    1. 首先通過 包含 的那個方法來判斷這個元素是否存在,
    2. 如果元素不存在那就不進行刪除操作,也可以報一個異常,
    3. 如果元素存在,那就根據 搜尋 的那個方法來獲取這個元素的索引,
    4. 最後根據 獲取到合法索引 來進行元素的刪除。
    5. 其實你可以使用通過 搜尋 的那個方法直接返回元素的索引,
    6. 如果索引合法你就直接刪除,
    7. 如果索引不合法那就不刪除然後也可以報一個異常。
  8. 可以對那些方法進行擴充套件
    1. 如 刪除陣列中所有的指定元素
    2. 如 找到陣列中所有的指定元素的索引
  9. 關於自定義的陣列已經實現了很多功能,
    1. 但是這個自定義陣列還有很多的侷限性,
    2. 在後面會慢慢解決這些侷限性,
    3. 如 這個陣列能存放的資料型別不能是任意的資料型別,
    4. 如果 這個陣列的容量是一開始就固定好的,超出就報異常。

程式碼示例(class: MyArray, class: Main)

  1. 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;
       }
    
       // 在指定索引處插入元素
       insert(index, element) {
          // 先判斷陣列是否已滿
          if (this.size == this.getCapacity()) {
             throw new Error('add error. Array is full.');
          }
    
          // 然後判斷索引是否符合要求
          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.');
          }
    
          // 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];
       }
    
       // 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.data[this.size - 1] = undefined;
          this.size--;
    
          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]}, `;
          }
          arrInfo += `${this.data[this.size - 1]}`;
          arrInfo += `]`;
    
          // 在頁面上展示
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製程式碼
  2. Main

    class Main {
       constructor() {
          let ma = new MyArray(20);
          for (let i = 1; i <= 10; i++) {
             ma.add(i);
          }
    
          /*
           * Array: size = 10,capacity = 20
           * [1,2,3,4,5,6,7,8,9,10]
           */
          console.log(ma.toString());
    
          /*
           * Array: size = 11,capacity = 20
           * [1,200,2,3,4,5,6,7,8,9,10]
           */
          ma.insert(1, 200);
          console.log(ma.toString());
    
          /*
           * Array: size = 12,capacity = 20
           * [-1,1,200,2,3,4,5,6,7,8,9,10]
           */
          ma.unshift(-1);
          console.log(ma.toString());
    
          /*
           * Array: size = 13,capacity = 20
           * [-1,1,200,2,3,4,5,6,7,8,9,10,9999]
           */
          ma.push(9999);
          console.log(ma.toString());
    
          /*
           * 8888
           */
          ma.set(5, 8888);
          console.log(ma.get(5));
          this.show(ma.get(5));
    
          /*
           * Array: size = 13,capacity = 20
           * [-1,1,200,2,3,8888,5,6,7,8,9,10,9999]
           * true
           * 6
           */
          console.log(ma.toString());
          this.show(ma.contain(5));
          this.show(ma.find(5));
    
          /*
           * Array: size = 12,capacity = 20
           * [-1,1,200,2,3,8888,6,7,8,9,10,9999]
           */
          ma.remove(ma.find(5));
          console.log(ma.toString());
    
          /*
           * -1
           * 9999
           * Array: size = 10,capacity = 20
           * [1,200,2,3,8888,6,7,8,9,10]
           */
    
          this.show(ma.shift());
          this.show(ma.pop());
          console.log(ma.toString());
    
          /*
           * Array: size = 9,capacity = 20
           * [1,200,2,3,6,7,8,9,10]
           */
          ma.removeElement(8888);
          console.log(ma.toString());
    
          /*
           * Array: size = 3,capacity = 20
           * [9,10,11]
           * Array: size = 12,capacity = 20
           * [1,200,2,3,6,7,8,9,10,123456,123456,123456]
           */
          ma.add(123456);
          ma.add(123456);
          ma.add(123456);
          this.show(ma.findAll(123456));
          console.log(ma.toString());
    
          /*
           * Array: size = 9,capacity = 20
           * [1,200,2,3,6,7,8,9,10]
           */
          ma.removeAllElement(123456);
          console.log(ma.toString());
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    }
    
    window.onload = function() {
       // 執行主函式
       new Main();
    };
    複製程式碼

讓自己的陣列成為動態陣列

  1. 自定義陣列的侷限性還有容量為固定的大小,
    1. 因為內部還是使用的 js 的靜態陣列,
    2. 靜態陣列的容量從定義開始就是固定的,
    3. 如果一開始就把容量開的太大了,
    4. 那麼就會浪費很多的空間,
    5. 如果容量開的太小了,
    6. 那就可能存放的空間不夠用。
  2. 使用一種解決方案,讓自定義資料的容量可伸縮
    1. 讓自定義陣列變成一個動態的陣列,
    2. 當自定義陣列中的空間已經滿了,
    3. 那就建立一個新的陣列,
    4. 這個陣列的容量定義為原來的容量的兩倍,
    5. 然後將舊陣列中的元素全部放到新陣列中,
    6. 以迴圈的方式放入新陣列中。
  3. 讓新陣列替代掉舊陣列,
    1. size == capcity時建立新陣列,容量翻倍,
    2. size == capcity / 2時建立新陣列,容量縮小一倍,
    3. 最終都會讓新陣列替代掉舊陣列。
    4. 使用這種方式會讓整體效能上很有優勢。
    5. 在 js 的動態陣列中選擇是擴容倍數是 1.5,
    6. 然後無論是 1.5 還是 2 或者 3 都是可以的,
    7. 只不過是一個引數的選擇,
    8. 你可以根據使用場景來進行擴容。
  4. 自定義陣列的這些操作及效能需要分析。
    1. 也就是要進行一個時間複雜度的分析。

程式碼示例(class: MyArray, class: Main)

  1. 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];
       }
    
       // 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 為容量的一半時 就可以縮容了
          // 防止 size 為 0 時 data.length 為1  那麼縮容時也為 0
          if (
             Math.floor(this.getCapacity() / 2) === this.size &&
             Math.floor(this.getCapacity() / 2) !== 0
          ) {
             // 縮容一半
             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;
       }
    }
    複製程式碼
  2. Main

    class Main {
       constructor() {
          this.alterLine('MyArray Area');
    
          let ma = new MyArray();
          for (let i = 1; i <= 10; i++) {
             ma.add(i);
          }
    
          /*
           * Array: size = 10,capacity = 20
           * [1,2,3,4,5,6,7,8,9,10]
           */
          console.log(ma.toString());
    
          /*
           * Array: size = 11,capacity = 20
           * [1,2,3,4,5,6,7,8,99999,9,10]
           */
          ma.insert(8, 9999);
          console.log(ma.toString());
    
          /*
           * Array: size = 10,capacity = 20
           * [1,2,3,4,5,6,7,8,9,10]
           */
          ma.remove(8);
          console.log(ma.toString());
    
          /*
           * Array: size = 11,capacity = 20
           * [1,2,3,4,5,6,7,8,9,10,9999]
           */
          ma.push(9999);
          console.log(ma.toString());
       }
    
       // 將內容顯示在頁面上
       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. 在演算法和資料結構領域有一個非常重要的內容
    1. 使用複雜度分析的方式來檢視程式碼相應的效能好不好,
    2. 時間複雜度分析是一個理論化的領域,
    3. 如果非要非常嚴謹的去研究它,
    4. 那就會涉及到很多數學方面的內容以及很多新概念,
    5. 所以只需要對時間複雜度有一個簡單的認識即可。
  2. 常見的演算法的時間複雜度
    1. O(1)、O(n)、O(lgn)、O(nlogn)、O(n^2)等等
  3. 這個大 O 簡單的來說描述的是
    1. 演算法的執行時間和輸入資料之間的關係。
    2. 如最簡單的求和,使用 for 迴圈來進行求和
    3. 他的時間複雜度就是 O(n)
    4. 這個 n 表示的是求和 for 迴圈遍歷的次數,
    5. 這個演算法執行的時間和 for 迴圈遍歷的次數成線性關係,
    6. 演算法和 n 呈線性關係就是O(n)
  4. 為什麼要用大 O,為什麼要叫做O(n)
    1. 因為忽略掉了很多的常數,
    2. 實際時間用線性方程來表示:T = c1*n + c2
    3. 其中的 c1 表示迴圈遍歷的每一次的時間,
    4. 遍歷的次數就為 n,
    5. c2 表示遍歷之前和之後的程式碼執行時間,
    6. 也就是其它地方的程式碼執行消耗的時間
    7. 如 你要初始化一個變數 sum,
    8. 如果你寫的是一個方法,你要返回最終結果 sum
    function calcSum(nums) {
       let sum = 0;
       for (let num of nums) {
          sum += num;
       }
       return sum;
    }
    複製程式碼
  5. 如果在具體分析演算法的時候把 c1 和 c2 也都具體的分析出來,
    1. 其實那樣沒有什麼必要,並且在有些情況下也不可能做到,
    2. 例如不同的語言實現,執行的時間是不等的,
    3. 因為轉換為機器碼後的指令數也是不一樣的,
    4. 就算指令都一樣,還有不同系統 cpu 執行的操作也是不一樣的,
    5. 很難判斷出來 c1 是幾條指令、c2 又是幾條指令,
    6. 正因為如此所以在分析時間複雜度的時候,
    7. 是一定要忽略掉這些常數的,
    8. 忽略掉這些常數之後,
    9. 演算法一:T = 2*n + 2、演算法二:T = 2000*n + 10000
    10. 他們的時候複雜度都是 O(n)
    11. 換句話來說他們都是線性時間的演算法,
    12. 這些演算法消耗的時間和輸入資料的規模是成一個線性關係。
  6. 如果有一個演算法三:T = 1*n*n + 0
    1. 不要認為這個 1 很小、0 也很小,
    2. 但是它依然是一個O(n^2)級別的一個演算法,
    3. 也就是說這個演算法消耗的時間和這個資料的規模成一個平方關係的,
    4. O(n^2)要比O(n)效能差很多,因為前者是 N 的平方級別的,
    5. 雖然第二個演算法的常數 2000 和 10000 特別的大,
    6. 而第三個演算法的常數 1 和 0 特別的小,
    7. 的確如此,假設這個 n 為 10,
    8. 那麼第三個演算法消耗的時間會比第二個演算法消耗的時間要長,
    9. 但是那並不能證明第三個演算法比第二個演算法效能就要差,
    10. 因為這個本來就要分具體情況,常數會影響到執行的時間,
    11. 但是計算時間複雜度就是要忽略掉常數的,
    12. 因為你實際使用沒有辦法控制所有常數。
  7. 這個 O 其實表示的一個漸進的時間複雜度
    1. 這個漸進 描述的是當 n 趨近於無窮大的時候,
    2. 例如第二個演算法與第三個演算法中的 n 為 3000,
    3. 那麼很明顯第三個演算法肯定要比第二個演算法執行時間長,
    4. 當這個 n 比 3000 還要大的時候,
    5. 那麼O(n^2)要比O(n)的執行時間差的越來越大,
    6. 所以當 n 越大,一個低階演算法的效能就越能體現出來,
    7. 也就是 n 越大就越能看出來O(n^2)要比O(n)快。
  8. 在實際使用時可能會用到高階演算法
    1. 當 n 比較小的時候有可能因為他的常數比較低,
    2. 反而可能會快於一個低階演算法,
    3. 例如 高階的排序演算法 歸併排序、快速排序,
    4. 這些高階排序法都可以對於比較小的陣列轉而使用插入排序這種方式,
    5. 可以很好的進行優化,通常這種優化能獲得百分之 10 到百分之 15 效能提升,
    6. 它的眼裡實際上是插入排序演算法的常數要比歸併排序、快速排序演算法的常數小一些,
    7. 這樣低階的演算法執行時間要比高階的演算法執行時間要快一些。
  9. 大 O 描述的是一個演算法或者操作的一個漸進式的時間複雜度,
    1. 也就是在說這個演算法操作所消耗的時間和輸入資料的規模之間的關係
    2. 由於大 O 描述的是 n 趨近於無窮大的情況下這個演算法的時間複雜度,
    3. 所以當出現這種演算法時T = 2*n*n + 300n + 10
    4. 他的時間複雜度還是O(n^2)
    5. 如果這個演算法的時間和 n 成 2 次方關係的話,
    6. 相應這個演算法的時間和 n 成 1 次方的關係會被忽略掉,
    7. 因為在這種情況下 低階項會被忽略掉,
    8. 因為當 n 趨近於無窮的時候 低階項起到的作用太小了,
    9. 所以當 n 無窮大的時候低階項的大小是可以忽略不計的,
    10. 所以T = 2*n*n + 300n + 10的時間複雜度還是O(n^2)

分析動態陣列的時間複雜度

  1. 增:O(n)
  2. 刪:O(n)
  3. 改:已知索引 O(1),未知索引 O(n)
  4. 查詢:已知索引 O(1),未知索引 O(n)
  5. 其它
    1. 未知索引的刪除,需要先查後刪:O(n^2)
    2. 未知索引的刪除全部,需要先遍歷再查再刪:O(n^3)
    3. 未置索引的查詢全部,需要先遍歷:O(n)
  6. 所以在使用陣列的時候
    1. 索引要具有一定語意,
    2. 這樣就可以直接通過索引來進行操作,
    3. 如果索引沒有語意,
    4. 那麼修改和查詢會讓效能大幅度降低。
  7. 增和刪如果只對最後一個元素進行操作
    1. 那麼時間複雜度就為O(1)
    2. 但是動態陣列要有 resize 伸縮容量的功能,
    3. 所以增和刪的時間複雜度依然是O(n)
  8. 一旦要 resize 了,就需要把整個元素全都複製一遍
    1. 複製給一片新的空間,
    2. 雖然說 resize 好像是一個效能很差的操作,
    3. 但是實際上並不是這樣的,
    4. 完全使用最壞情況的時間複雜度來分析 resize 是不合理的,
    5. 應該使用均攤時間複雜度分析來分析 resize,
    6. 其實 resize 所消耗的效能在整體上沒有那麼的糟糕。

新增操作:時間複雜度為 O(n)

  1. push(e):向陣列末尾新增一個元素
    1. 非常簡單,只是簡單的給data[size]賦值,
    2. 所以它的時間複雜度為 O(1)
    3. O(1)的時間複雜度表示這個操作所消耗的時間
    4. 與 資料的規模是沒有關係的,
    5. 在分析陣列的時間複雜度的時候,
    6. 那個時間複雜度與這個陣列有多少個元素有關係,
    7. 由於你要遍歷陣列中每一個元素,
    8. 那麼這個時間複雜度就為O(n)(操作 n 個元素),
    9. push 都能在常數時間內完成,
    10. 所以他的時間複雜度就為O(1)(操作 1 個元素)。
  2. unshift(e):向陣列頭部新增一個元素
    1. 需要把陣列中的元素都往後移動一個位置,
    2. 所以這涉及到遍歷陣列中每一個元素,
    3. 那麼這個時間複雜度就為O(n)(操作 n 個元素),
    4. 雖然最後也有O(1)(操作 1 個元素)的操作 ,
    5. 但是在有O(n)情況時,
    6. 更低階項O(1)會被忽略掉。
  3. insert(index, e):在 index 索引這個位置插入一個元素
    1. 當 index 為 0 的時候就和unshift(e)一樣要向後移動 n 個元素,
    2. 當 index 為 size(陣列中實際元素個數)的時候就和push(e)一樣
    3. 只是簡單的給data[size]賦值,
    4. 由於這個 index 可以取 0 到 size 中任何一個值,有那麼多種可能性,
    5. 那麼就可以進行假設在具體操作的時候取到每一個值的概率都是一樣的,
    6. 在這種情況下進行操作時它所消耗的時間的期望,
    7. 有些情況 index 會小一些,那麼向後移動位置的元素會多一些,
    8. 有些情況 index 會大一些,那麼向後移動位置的元素會少一些,
    9. 平均而言這個演算法的時間複雜度為O(n/2)
    10. 但是這個 2 是一個常數,需要忽略常數,
    11. 所以忽略常數後這個演算法的時間複雜度為O(n)
    12. 所以最好的情況下時間複雜就為O(1)
    13. 最壞的情況下時間複雜度就為O(n)
    14. 中等的情況下時間複雜度就為O(n/2)
  4. 新增操作綜合來看是一個O(n)級別的演算法
    1. push(e)O(1)
    2. unshift(e)O(n)
    3. insert(index, e)O(n/2)=O(n)
    4. 嚴格計算就需要一些概率論上的知識,
    5. 所以在演算法複雜度分析上,
    6. 通常關注的是某個演算法時間複雜度的最壞情況、最糟糕的情況,
    7. 也會有一些特例,但是在現實生活中你不能按照最好的情況去解決問題。
    8. 例如 你去上班,公司距離你家的位置最快只需要 5 分鐘,
    9. 然後你每次去上班只留五分鐘的時間從家裡出來到公司去,
    10. 你這樣做就是很高概率的讓每次上班都會遲到。
    11. 例如 在考試時,考試最好的情況是考滿分,
    12. 然後你每次都考試都以為自己能考滿分的蒙題而不去準備,
    13. 你這樣做的就是很高概率的讓每次考試都會不及格。
    14. 在大多數情況下去考慮最好的情況是沒有多大意義的,
    15. 在演算法分析的領域通常會比較嚴格一些去考察最壞的情況。
  5. 在新增操作時,自定義的動態陣列容量已滿
    1. 就會進行 resize 操作,這個 resize 操作顯然是O(n)
    2. 以為因為要給新陣列重新賦值一遍。

刪除操作:時間複雜度為 O(n)

  1. removeLast():在陣列末尾刪除一個元素
    1. 給末尾的陣列元素設定預設值,然後size--
    2. 所以它的時間複雜度為 O(1)
    3. O(1)的時間複雜度表示這個操作所消耗的時間
    4. 與 資料的規模是沒有關係的,
    5. 他每次只是操作一個陣列元素。
  2. removeFirst():在陣列頭部刪除一個元素
    1. 需要把陣列中的元素都往前移動一個位置,
    2. 所以這涉及到遍歷陣列中每一個元素,
    3. 那麼這個時間複雜度就為O(n)(操作 n 個元素),
    4. 雖然最後也有O(1)(操作 1 個元素)的操作 ,
    5. 給末尾的陣列元素設定預設值,然後size--
    6. 但是在有O(n)情況時,
    7. 更低階項O(1)會被忽略掉。
  3. remove(index):刪除指定索引位置處的元素並返回
    1. 所以最好的情況下時間複雜就為O(1)
    2. 最壞的情況下時間複雜度就為O(n)
    3. 中等的情況下時間複雜度就為O(n/2)
    4. 忽略常數後這個演算法的時間複雜度為O(n)
  4. 刪除操作綜合來看是一個O(n)級別的演算法
    1. removeLast()O(1)
    2. removeFirst()O(n)
    3. remove(index)O(n/2)=O(n)
  5. 在刪除操作時,自定義的動態陣列中實際元素個數為其容量的一半時,
    1. 就會進行 resize 操作,這個 resize 操作顯然是O(n)
    2. 以為因為要給新陣列重新賦值一遍。

修改操作:時間複雜度為 O(1)

  1. set(index, e):指定索引修改一個元素的值
    1. 簡單的賦值操作,時間複雜度為O(1)
    2. 陣列最大的優勢就是支援隨機訪問,
    3. 訪問到對應索引的值後就可以修改對應索引的值了,
    4. 效能超級好。

查詢操作:時間複雜度為 O(n)

  1. get(index):指定索引查詢一個元素的值
    1. 簡單的獲取操作,時間複雜度為O(1)
    2. 陣列最大的優勢就是支援隨機訪問,
    3. 只要知道我要訪問的索引是那個數字,
    4. 就能夠一下子訪問到對應索引的值,
    5. 效能超級好。
  2. contains(e):指定元素來查詢,判斷元素是否存在
    1. 複雜的獲取操作,時間複雜度為O(n)
    2. 需要遍歷整個陣列從而找到相同的元素,
    3. 這個元素在陣列中可能找的到也可能找不到,
    4. 所以最好的情況下時間複雜就為O(1),第一個,
    5. 最壞的情況下時間複雜度就為O(n),最後一個或者沒找到,
    6. 中等的情況下時間複雜度就為O(n/2),在中間,
    7. 忽略常數後這個演算法的時間複雜度為O(n)
    8. 分析演算法要關注最壞的情況。
  3. find(e):指定元素來查詢,返回該元素對應的索引
    1. 複雜的獲取操作,時間複雜度為O(n)
    2. 需要遍歷整個陣列從而找到相同的元素,
    3. 這個元素在陣列中可能找的到也可能找不到,
    4. 所以最好的情況下時間複雜就為O(1),第一個,
    5. 最壞的情況下時間複雜度就為O(n),最後一個或者沒找到,
    6. 中等的情況下時間複雜度就為O(n/2),在中間,
    7. 忽略常數後這個演算法的時間複雜度為O(n)
    8. 分析演算法要關注最壞的情況。

其它擴充套件操作

  1. removeElement(e):根據指定元素來進行刪除第一相同的元素
    1. 首先要進行遍歷操作,然後找到指定元素的索引,
    2. 最後根據索引來進行刪除操作,刪除操作中又會進行元素位置移動
    3. 於是就有兩輪迴圈了,所以時間複雜度為O(n^2)
  2. removeAll(e)::根據指定元素來進行刪除所有相同的元素
    1. 首先要進行遍歷操作,找到一個元素後就刪除這個元素,
    2. 會複用到removeElement(e),於是有三輪迴圈了,
    3. 所以這個操作是O(n^3)
  3. findAll(e):根據指定元素來進行查詢,找到所有的元素
    1. 首先要進行遍歷操作,找到一個元素後就將元素的索引存起來,
    2. 所以這個操作是一輪迴圈,時間複雜度為O(n)

均攤複雜度和防止複雜度的震盪

resize 的複雜度分析

  1. 不可能每次執行 push 操作的時候都會觸發 resize
    1. 假如陣列有十個空間,你執行 push 操作操作之後,
    2. 只有第十次才會觸發 resize,並且陣列的容量會翻一倍,
    3. 隨著你新增的越多,陣列的容量會呈現倍數增長,
    4. 那麼觸碰 resize 的概率就越小了,
    5. 根本不可能每次新增元素就觸發 resize,
    6. 所以使用最壞的情況去分析 resize 是非常不合理的。
  2. 假設當前的 capacity = 10,並且每次新增操作都使用 push
    1. 那麼在觸發 resize 的時候,一共進行了 11 次 push 操作
    2. 其實一共進行了 21 次基本賦值的操作(10+10+1),
    3. 11 新增操作和十次轉移陣列元素的操作,
    4. 因為 resize 裡面會將原陣列中的元素賦值給新陣列,
    5. 所以平均每次 push 操作,就約等於進行了 2 次的基本賦值操作。
  3. 那可以繼續假設 capacity = n,n+1 次 push 觸發 resize,
    1. 總共進行了 2n+1 次基本賦值操作,
    2. 這樣一來平均來講 每次 push 操作,
    3. 都在進行 2 次的基本賦值操作。
  4. 相當於就是將 resize 的時間平攤給了 n+1 次 push 操作
    1. 從而得到一個結論,平均每次 push 操作,都會進行 2 次基本操作,
    2. 那麼 push 的時間複雜度不會因為 resize 而變為O(n)級別的,
    3. 這就意味著 這樣的均攤計算,addLast 時間複雜度其實是O(1)級別的,
    4. 而且他和當前陣列中有多少個元素完全沒有關係的,
    5. 所以在這個例子裡,這樣的均攤計算比計算最壞情況要更有意義,
    6. 這樣計算複雜度就叫均攤複雜度,
    7. 也就是說 push 的均攤複雜度為O(1)

均攤複雜度(amortized time complexity)

  1. 均攤複雜度在很多教材中並不會進行介紹,
    1. 但是在實際工程中這樣的一個思想是很有意義的,
    2. 一個相對比較耗時的操作,如果能夠保證它不會每次都觸發,
    3. 那麼這個相對比較耗時的操作相應的時間
    4. 是可以分攤到其它的操作中來,
    5. 其實這樣一來,removeLast 操作,
    6. 它的均攤複雜度是為O(1)的,
    7. 雖然它的最壞複雜度是O(n)級別的,
    8. 但是它的均攤複雜度也是為O(1)級別的。

複雜度震盪

  1. 同時去看 push 和 removeLast 操作時:
    1. 如 capacity = n,然後陣列容量已經滿了,
    2. 這時候使用 push 操作,
    3. 這時候陣列就要進行擴容,
    4. 那麼就會耗費O(n)的時間,
    5. 這時候又去使用 removeLast 操作,
    6. 這時候陣列又要進行縮容,
    7. 那麼又會耗費O(n)的時間,
    8. 就這樣一直的 addLast、removeLast,
    9. 那麼操作都是在耗費O(n)的時間,
  2. 這種情況每次都會耗費O(n)的複雜度
    1. 這就是複雜度的震盪,
    2. 明明在均攤的時候是一個O(1)的級別,
    3. 但是在一些特殊的情況下猛的一下竄到了O(n)的級別,
    4. 從而產生了這個震盪。
  3. 這個震盪發生的原因是:
    1. removeLast 時 resize 過於激進(Eager),
    2. 當元素的個數變為容量的二分之一的時候,
    3. 立馬就讓陣列進行縮容,
    4. 此時整個陣列中的元素是滿的,
    5. 元素的個數和容量是相等的,
    6. 然後使用一下 push 操作時就又需要擴容了。
  4. 解決方案:Lazy
    1. 當 removeLast 時進行 resize 不急著進行縮容,
    2. 而是等 size 為當前容量的四分之一時再進行縮容,
    3. 縮容的大小為原來容量的一半,
    4. 這樣一來就算立馬進行 push 操作也不會立馬進行擴容操作,
    5. 也就是將原來的策略改成了
    6. 只有當size == capcity / 4時,才將 capacity 減半,
    7. 原來是size == capcity / 2時,才將 capacity 減半,
    8. 通過這樣的策略就防止了複雜度的震盪。
    9. 要防止容量為 4 時,size 又為 1 時,
    10. data.length / 2 為 0,那樣縮容的容量就為 0 了,
    11. 這樣一來你任何操作都可能會報異常了。
  5. 這種方式其實是非常有意思的方式,
    1. 在演算法的領域有的時候懶一些
    2. 反而讓演算法最終的整體效能更加好,
    3. 所以有時候是在更懶的時候其實是在改善演算法效能,
    4. 雖然說演算法更懶,但是不代表程式碼更容易編寫,
    5. 也不代表程式碼量更少,
    6. 有時候讓演算法更懶,其實程式碼量會更加的大。
  6. 陣列背後其實陣列這種資料結構背後還有很多東西值得探究,
    1. 不要小看陣列,
    2. 設計一個資料結構整體上就要看它怎麼具體的儲存資料,
    3. 在這個具體的儲存的基礎上怎樣進行資料的增刪改查。

程式碼示例(class: MyArray, class: Main)

  1. 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];
       }
    
       // 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 為容量的一半時 就可以縮容了
          // 防止 size 為 0 時 data.length 為1  那麼縮容時也為 0
          if (
             Math.floor(this.getCapacity() / 2) === this.size &&
             Math.floor(this.getCapacity() / 2) !== 0
          ) {
             // 縮容一半
             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;
       }
    }
    複製程式碼
  2. Main

    class Main {
       constructor() {
          this.alterLine('MyArray Area');
    
          let ma = new MyArray();
          for (let i = 1; i <= 10; i++) {
             ma.add(i);
          }
    
          /*
           * Array: size = 10,capacity = 20
           * [1,2,3,4,5,6,7,8,9,10]
           */
          console.log(ma.toString());
    
          /*
           * Array: size = 11,capacity = 20
           * [1,2,3,4,5,6,7,8,99999,9,10]
           */
          ma.insert(8, 9999);
          console.log(ma.toString());
    
          /*
           * Array: size = 10,capacity = 20
           * [1,2,3,4,5,6,7,8,9,10]
           */
          ma.remove(8);
          console.log(ma.toString());
    
          /*
           * Array: size = 11,capacity = 20
           * [1,2,3,4,5,6,7,8,9,10,9999]
           */
          ma.push(9999);
          console.log(ma.toString());
    
          for (let i = 1; i <= 11; i++) {
             ma.remove(0);
          }
          /*
           * Array: size = 6,capacity = 10
           * [1,7,8,9,10,9999]
           */
          console.log(ma.toString());
       }
    
       // 將內容顯示在頁面上
       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();
    };
    複製程式碼

相關文章