【從蛋殼到滿天飛】JS 數據結構解析和算法實現-Arrays(數組)

思惟導圖

前言

【從蛋殼到滿天飛】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 (一個一個的工程)git

所有源代碼已上傳 github,點擊我吧,光看文章可以掌握兩成,動手敲代碼、動腦思考、畫圖才能夠掌握八成。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 中的數組封裝到一個類中,ide

    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();
    };
    複製代碼
相關文章
相關標籤/搜索