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

思惟導圖

前言

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

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

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

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

鏈表

  1. 鏈表是最基礎的動態數據結構
  2. 鏈表是很是重要的線性數據結構
    1. 如下三種,底層都是依託靜態數組,靠 resize 解決固定容量問題。
    2. 動態數組:所謂動態,是從用戶的角度上來看的。
    3. 隊列
  3. 鏈表是真正的動態數據結構
    1. 它是數據結構中的一個重點,
    2. 也有多是一個難點,
    3. 它是最簡單的一種動態數據結構,
    4. 其它更高級的動態數據結構有 二分搜索樹、Trie、
    5. 平衡二叉樹、AVL、紅黑樹等等,
    6. 熟悉了最簡單的動態數據結構,
    7. 那麼對於更高級的也會比較容易掌握了。
  4. 對於鏈表來講它涉及到了計算機領域一個很是重要的概念
    1. 更深刻的理解引用(或者指針),
    2. 這個概念和內存相關,
    3. 在 JS 裏面不須要手動的管理內存,
    4. 可是對鏈表這種數據結構更加深刻的理解,
    5. 可讓你對 引用、指針甚至計算機系統中
    6. 和內存管理相關不少話題有更加深刻的認識。
  5. 鏈表自己也是有它很是清晰的遞歸結構的,
    1. 只不過因爲鏈表這種數據結構它自己是一種線性的數據結構,
    2. 因此依然能夠很是容易的使用循環的方式來對鏈表進行操做,
    3. 可是鏈表自己因爲它天生也是具備這種遞歸結構的性質,
    4. 因此它也能讓你更加深刻的理解遞歸機制相應的這種數據結構,
    5. 在學習更加複雜的數據結構的時候,
    6. 對遞歸這種邏輯機制深刻的理解是必不可缺的。
  6. 鏈表這種數據結構自己就具備功能性
    1. 它能夠用來組成更加複雜的數據結構,
    2. 好比 圖結構、hash 表、棧(鏈表版)、隊列(鏈表版),
    3. 因此他能夠輔助組成其它數據結構。

什麼是鏈表

  1. 數據存儲在「節點」(Node)中
    1. 把數據存在一種單獨的結構中,
    2. 這個結構一般管它叫作節點,
    3. 對於鏈表節點來講他一般只有兩部分,
    4. 一部分就是存儲真正的數據,
    5. 另外一部分存儲的是當前節點的下一個節點,
    6. 鏈表其實就像一個火車,每個節點都是一節車箱,
    7. 在車箱中存儲數據,而車箱和車箱之間還會進行鏈接,
    8. 以使得這些數據是整合在一塊兒的,
    9. 這樣用戶能夠方便的在全部的這些數據上進行查詢等等其它的操做,
    10. 數據與數據之間鏈接就是用下面的這個 next 來完成的
    class Node {
       e; //Element
       next; //Node
    }
    複製代碼
  2. 鏈表的優勢
    1. 真正的動態,不須要處理固定容量的問題,
    2. 它不像靜態數組那樣一會兒 new 出來一片空間,
    3. 也根本不須要考慮這個空間是否是不夠用,
    4. 也根本不用去考慮這個空間是否是開多了。
  3. 對於鏈表來講,你須要多少個數據。
    1. 你就能夠生成多少個節點,
    2. 把它們掛接起來了,這就是所謂的動態。
  4. 鏈表的缺點
    1. 和數組相比,它喪失了隨機訪問的能力。
    2. 不能像數組那樣給一個索引
    3. 就能直接訪問對應索引位置的元素,
    4. 這是由於從底層機制上數組所開闢的空間
    5. 在內存裏是連續分佈的,
    6. 因此纔可以直接去向索引對應的偏移
    7. 計算出相應的數據所存儲的內存地址,
    8. 而後就可以直接用O(1)的複雜度取出這個元素,
    9. 可是鏈表不一樣,鏈表因爲是靠 next 一層一層鏈接的,
    10. 因此在計算機的底層,每個節點他所在的內存的位置是不一樣的,
    11. 只可以靠這個 next 來一層一層的找到這個元素,
    12. 這就是鏈表最大的缺點。

數組和鏈表的對比

  1. 數組面試

    1. 數組最好永遠索引有語意的狀況,如 scores[2]
    2. 最大的優勢:支持快速查詢
  2. 鏈表算法

    1. 鏈表不適合用於索引有語意的狀況。
    2. 最大的優勢:動態存儲。
  3. 對比數組

    1. 數組也能夠沒有語意,並非全部的時候索引是有語意的,
    2. 也並非全部有語意的這樣的一個標誌就適合作索引,如身份證號,
    3. 將一個靜態數組改變爲一個動態數組,
    4. 就是在應付不方便使用索引的時候有關數據存儲的問題,
    5. 對於這樣的存儲數據的需求使用鏈表是更合適的,
    6. 由於鏈表最大的優勢是動態存儲。
  4. 要清楚何時使用數組這樣的靜態數據結構,數據結構

    1. 何時使用鏈表這類的動態數據結構。
  5. 簡單的代碼示例MyLinkedListide

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    class MyLinkedList {
       constructor() {}
    }
    複製代碼

在鏈表中進行添加、插入操做

  1. 鏈表是經過節點來裝載元素
    1. 而且節點和節點之間會進行鏈接。
    2. 若是想訪問這條鏈表中全部的節點,
    3. 那麼就必須把鏈表的頭給存儲起來,
    4. 一般這個鏈表的頭叫作的 head,
    5. 應該說在MyLinkedList
    6. 應該有一個變量指向鏈表中的第一個節點。
  2. 給自定義數組添加元素是從數組尾部開始添加,
    1. 給鏈表添加元素是從數組頭部開始添加,
    2. 由於自定義數組有維護一個 size,
    3. 這個 size 指向的是下一個待添加元素的位置,
    4. 因此你直接將 size 作索引直接賦值便可,
    5. 有 size 這個變量在跟蹤數組的尾巴,
    6. 而對於鏈表來講 有設置一個鏈表的頭這樣的一個變量,
    7. 也就是說有一個變量在跟蹤鏈表的頭,
    8. 並無一個變量去跟蹤鏈表的尾巴,
    9. 因此在鏈表頭部添加元素是很是方便。
  3. 添加操做原理
    1. 將一個新的元素掛接到鏈表頭部,
    2. 同時不破壞如今鏈表的這個結構,
    3. 添加一個新元素 666,它的名字叫 node,
    4. 就只須要使用 node.next = head
    5. 也就是將新添加的元素的 next 指向鏈表的頭,
    6. 這個時候新添加的元素也成爲新的鏈表頭,
    7. 也就是head = node
    8. 這樣 head 就會指向新的元素 666,也就是 node 這個節點,
    9. 如此一來就完成了將 node 這個節點插入了整個鏈表的頭部,
    10. node 這個變量是在一個函數中聲明的,
    11. 因此函數結束以後這個變量的做用域也就結束了,
    12. 鏈表的 head 也等於 node,
    13. 鏈表中就新增了一個新元素。
  4. 在鏈表頭部添加元素很是簡單,
    1. 只須要建立節點,讓節點的 next 指向 head,
    2. 而後讓 head 從新指向這個節點,也就是讓 head 變成新的元素,
    3. 由於鏈表是從頭部添加元素的。
  5. 在鏈表中間添加元素,
    1. 定義一個 prev 的變量指向中間元素的前一個元素,
    2. 而後讓新增長的元素這樣,node.next = prev.next
    3. 以後這樣,prev.next = node
    4. 這樣一來就在 prev 這個元素和本來 prev 的下一個元素之間
    5. 插入了一個新元素,叫作 node,
    6. 整個過程就是將 prev 的 next(下一個元素)的引用
    7. 傳遞給新元素的 next(下一個元素),
    8. 而後將 prev 的 next(下一個元素)變動爲這個新元素,
    9. 最後就將新元素插入到中間了。
    10. 這個過程的關鍵是找到待添加的節點的前一個節點,
    11. 這個就是 prev 變量要作的事情。
  6. 在鏈表的操做中不少時候順序很是重要,
    1. 如在鏈表中插入一個元素,在指定的元素後面插入一個元素,
    2. 而且不影響整個鏈表結構,和在鏈表中間添加元素同樣,
    3. 把本來的node.next = prev.nextprev.next = node
    4. 的順序變一下,prev.next = node在前,
    5. node.next = prev.next 在後,這樣一來邏輯就不成立了,
    6. 鏈表不只斷開了,並且新節點的 next 指向的是本身,死循環了,
    7. 因此同樣的代碼,順序顛倒了,獲得的結果就是錯誤的。
  7. 在鏈表的 index(0-based)位置添加元素 e
    1. 經過索引來操做鏈表,在鏈表中不是一個經常使用的操做,
    2. 由於在選擇使用鏈表的時候一般就選擇不使用索引了,
    3. 可是這個需求在面試等一些考題中出現的機率很大,
    4. 因此他的主要做用更多的是一個練習。

學習方式

  1. 若是剛接觸鏈表,對鏈表不熟悉,
    1. 而後不少這種操做在腦子裏不能很是快的反應出來,
    2. 不清楚他的意義是什麼,那你可使用紙筆來稍微畫一下,
    3. 每一步程序邏輯的執行意味着當前這個鏈表變成了什麼樣子,
    4. 這個過程可以幫助你更加深刻的理解程序,
    5. 包括在調試的時候來看你的程序到底爲何出了錯誤時,
    6. 很是很是有幫助的,不要犯懶,爲了更加深刻的理解你的程序,
    7. 不能只盯着你的代碼去想,必定要寫寫畫畫,
    8. 必要的時候甚至根據你的程序進行具體的調試,
    9. 這些都是很是重要很是有幫助的。

代碼示例 (class: MyLinkedList)

  1. MyLinkedList函數

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.head = null;
          this.size = 0;
       }
    
       // 獲取鏈表中實際的節點個數
       getSise() {
          return this.size;
       }
    
       // 判斷鏈表是否爲空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在鏈表頭添加節點
       addFirst(element) {
          let node = new MyLinkedListNode(element, null);
          node.next = this.head;
          this.head = node;
          this.size++;
       }
    
       // 在鏈表指定索引處插入節點
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          if (index === 0) {
             this.addFirst(element);
          } else {
             // 第一個prev就是head
             let prev = this.head;
    
             // 變量i(索引)之因此要從 1 開始,由於索引爲0的那個節點就是head,循環就不須要從0開始了,
             // 小於index是由於要找到指定索引位置的前一個節點
             // 循環是由於 要繼續找到指定索引處的節點的前一個節點
             for (var i = 1; i < index; i++) {
                // 不停的切換引用,直到找到對應索引處節點的下一個節點
                prev = prev.next;
             }
    
             let node = new MyLinkedListNode(element, null);
             node.next = prev.next;
             prev.next = node;
             this.size++;
          }
       }
    
       // 擴展:在鏈表最後一個節點的位置添加節點
       addLast(element) {
          this.insert(this.size, element);
       }
    }
    複製代碼

給鏈表設立虛擬頭節點

  1. 在鏈表中進行指定索引處插入元素時
    1. 對鏈表頭插入元素與對鏈表其它位置插入元素的邏輯有區別,
    2. 因此就致使每次操做都須要對索引進行判斷,
    3. 若是你是在索引 0 的位置插入元素,那麼就沒有 prev(前一個元素),
    4. 天然就不可使用node.next = prev.nextprev.next = node了,
    5. 那時候你能夠直接使用node.next = headhead = node
    6. 不過已經有了向鏈表頭添加元素的方法了,
    7. 因此向頭部插入元素時根本就不須要這麼作,
    8. 這時候能夠經過給鏈表設立虛擬頭節點來解決這個問題。
  2. 爲何對鏈表頭插入元素那麼特殊?
    1. 由於插入元素時,必須要找到該索引位置的節點以前的一個節點(prev),
    2. 而鏈表頭(head)以前並無任何節點,因此邏輯上就會特殊一些。
    3. 不過在鏈表的具體實現中有一個很是經常使用的技巧,
    4. 能夠把鏈表頭的這種特殊的操做與其它的操做一塊兒統一塊兒來,
    5. 其實這個方法的想法很簡單,既然它沒有鏈表頭以前的節點,
    6. 那就本身造一個鏈表頭以前的節點,
    7. 這個鏈表頭以前的節點不會用來存儲任何元素,存儲的數據爲空,
    8. 這個空節點就稱之爲整個鏈表頭的 head,也能夠叫它 dummyHead,
    9. 由於它就是一個假的 head,它也是爲鏈表設立的虛擬頭節點,
    10. 這樣一來 鏈表的第一個元素就是 dummyHead 的 next 所對應的那個節點,
    11. 由於 dummyHead 這個位置的元素是根本不存在的,
    12. 對用戶來說也是根本沒有意義的,
    13. 這只是爲了編寫邏輯方便而出現的一個虛擬的頭節點,
    14. 因此一個這樣的內部機制對用戶來講也是不可見的,
    15. 用戶不知道鏈表中有沒有虛擬的頭節點,
    16. 這和自定義的循環隊列有點像,自定義循環隊列中有一個不可用的空間,
    17. 有意識地去浪費一個空間,爲了編寫邏輯更加的方便,
    18. 從而也讓性能在總體上獲得了提高。
  3. 有了 dummyHead 以後就不須要處理頭節點這個特殊的操做
    1. 由於當你在頭部插入元素時,能夠這樣
    2. node.next = dummyHead.nextdummyHead.next = node
    3. 這樣你就解決了每次插入時都要進行一次邏輯判斷了,
    4. 這樣一來就說明了鏈表中全部的節點都有前一個節點了,
    5. 在初始的時候 dummyHead 指向的是索引爲 0 的元素前一個位置的節點。
  4. 鏈表操做的實際原理
    1. 在沒有使用虛擬頭節點以前,一直都是在鏈表頭 head 這個地方不停的添加節點,
    2. 而後將 head 這個變量不停的指向新添加的節點 node.next = head.next;head = node;
    3. 在使用了虛擬頭節點以後,
    4. 一直是在鏈表虛擬頭 dummyHead 這個地方後面一個位置不停的插入新節點,
    5. dummyHead 從未變更過,永遠在鏈表的第一個位置,
    6. node.next = dummyHead.next; dummyHead.next = node;
    7. 可是dummyHead.next纔是鏈表中第一個實際記錄的節點,
    8. 用戶會不知道虛擬節點的存在,由於 dummyHead 並不算在鏈表的實際節點中,
    9. 也就是 size 中不會維護 dummyHead,dummyHead 的存在是爲了維護一致的插入操做。

代碼示例 (class: MyLinkedList)

  1. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 獲取鏈表中實際的節點個數
       getSise() {
          return this.size;
       }
    
       // 判斷鏈表是否爲空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在鏈表頭添加節點
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虛擬頭節點
          insert(0, element);
       }
    
       // 在鏈表指定索引處插入節點
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一個prev就是dummyHead
          let prev = this.dummyHead;
          // 以前變量i(索引)之因此要從 1 開始,由於索引爲0的那個節點就是head,循環就不須要從0開始了,
          // 如今索引之因此要從 0 開始, 由於初始化時 多增長了一個虛擬的頭節點
          // (由於這個索引爲0的節點並非dummyHead,dummyHead這個節點並不記錄爲鏈表中的實際節點),
          // 小於index是由於要找到指定索引位置的前一個節點
          // 循環是由於 要繼續找到指定索引處的節點的前一個節點
          for (var i = 0; i < index; i++) {
             // 不停的切換引用,直到找到對應索引處節點的下一個節點
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 擴展:在鏈表最後一個節點的位置添加節點
       addLast(element) {
          this.insert(this.size, element);
       }
    }
    複製代碼

在鏈表中進行遍歷、查詢、修改操做

  1. 若是要找指定索引元素的前一個節點
    1. 那麼就要從dummyHead開始遍歷,
    2. 若是要找指定索引元素的那個節點,
    3. 那麼就要從dummyHead.next開始遍歷。

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

  1. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 獲取鏈表中實際的節點個數
       getSize() {
          return this.size;
       }
    
       // 判斷鏈表是否爲空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在鏈表頭添加節點
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虛擬頭節點
          this.insert(0, element);
       }
    
       // 在鏈表指定索引處插入節點
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一個prev就是dummyHead
          let prev = this.dummyHead;
          // 以前變量i(索引)之因此要從 1 開始,由於索引爲0的那個節點就是head,循環就不須要從0開始了,
          // 如今索引之因此要從 0 開始, 由於初始化時 多增長了一個虛擬的頭節點
          // (由於這個索引爲0的節點並非dummyHead,dummyHead這個節點並不記錄爲鏈表中的實際節點),
          // 小於index是由於要找到指定索引位置的前一個節點
          // 循環是由於 要繼續找到指定索引處的節點的前一個節點
          for (var i = 0; i < index; i++) {
             // 不停的切換引用,直到找到對應索引處節點的下一個節點
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 擴展:在鏈表最後一個節點的位置添加節點
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 獲取指定索引位置的元素
       get(index) {
          // 判斷索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引節點的前一個節點 就使用dummyHead
          // 若是你要找到指定索引節點 就使用dummyHead.next
          // 由於duumyHead並非第一個節點,由於它是一個虛擬節點,
          // dummyHead.next纔是真正被記錄的第一個節點。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 獲取頭節點的元素
       getFirst() {
          return this.get(0);
       }
    
       // 獲取尾節點的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 設置指定索引位置的元素值
       set(index, element) {
          // 判斷索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 從第一個真正被記錄的節點開始,從0開始
          let node = this.dummyHead.next;
    
          // 索引爲 0 時,實際上切換到的節點 它的索引爲 1
          // i < index ,當索引爲 index-1 時, 實際上切換到的節點 它的索引爲index
          for (let i = 0; i < index; i++) {
             // 每一次切換 都只是改變引用
             // 不的在鏈表中找下一個節點
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部節點中是否有包含該元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切換
             node = node.next;
          }
    
          return false;
       }
    
       // 輸出鏈表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在頁面上展現
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製代碼
  2. Main

    class Main {
       constructor () {
          /this.alterLine("MyLinkedList Area");
          let mylinkedList = new MyLinkedList();
    
          for (let i = 1; i <= 5 ; i++) {
             mylinkedList.addFirst(i);
             console.log(mylinkedList.toString());
          }
          mylinkedList.insert(2, 88888);
          console.log(mylinkedList.toString());
       }
    
       // 將內容顯示在頁面上
       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 位置的元素
    2. 就是要索引爲 2 位置的元素的前一個元素,
    3. 還要索引爲 2 位置的元素的後一個元素,
    4. 讓前一個元素的 next 等於後一個元素,
    5. 也就是前一個元素的 next 等於索引爲 2 的元素的 next。
    6. 也就是讓 prev 指向待刪除的那個節點的前一個節點,
    7. prev 這個節點的 next 就是要刪除的那個節點 delNode,
    8. 而後讓prev.next = delNode.next
    9. 這樣就讓待刪除的那個節點的前一個節點的 next,
    10. 也就是直接指向了待刪除的那個節點的後一個節點了,
    11. 而後讓delNode.next = null 就完成了刪除,
    12. 千萬不要這樣delNode = delNode.next
    13. 這個並非刪除,而是將待刪除節點的引用指向了下一個節點,
    14. 經過設置爲 null 纔是真正的與當前鏈表脫離關係。

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

  1. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 獲取鏈表中實際的節點個數
       getSize() {
          return this.size;
       }
    
       // 判斷鏈表是否爲空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在鏈表頭添加節點
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虛擬頭節點
          this.insert(0, element);
       }
    
       // 在鏈表指定索引處插入節點
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一個prev就是dummyHead
          let prev = this.dummyHead;
          // 以前變量i(索引)之因此要從 1 開始,由於索引爲0的那個節點就是head,循環就不須要從0開始了,
          // 如今索引之因此要從 0 開始, 由於初始化時 多增長了一個虛擬的頭節點
          // (由於這個索引爲0的節點並非dummyHead,dummyHead這個節點並不記錄爲鏈表中的實際節點),
          // 小於index是由於要找到指定索引位置的前一個節點
          // 循環是由於 要繼續找到指定索引處的節點的前一個節點
          for (var i = 0; i < index; i++) {
             // 不停的切換引用,直到找到對應索引處節點的下一個節點
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 擴展:在鏈表最後一個節點的位置添加節點
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 獲取指定索引位置的元素
       get(index) {
          // 判斷索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引節點的前一個節點 就使用dummyHead
          // 若是你要找到指定索引節點 就使用dummyHead.next
          // 由於duumyHead並非第一個節點,由於它是一個虛擬節點,
          // dummyHead.next纔是真正被記錄的第一個節點。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 獲取頭節點的元素
       getFirst() {
          return this.get(0);
       }
    
       // 獲取尾節點的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 設置指定索引位置的元素值
       set(index, element) {
          // 判斷索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 從第一個真正被記錄的節點開始,從0開始
          let node = this.dummyHead.next;
    
          // 索引爲 0 時,實際上切換到的節點 它的索引爲 1
          // i < index ,當索引爲 index-1 時, 實際上切換到的節點 它的索引爲index
          for (let i = 0; i < index; i++) {
             // 每一次切換 都只是改變引用
             // 不的在鏈表中找下一個節點
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部節點中是否有包含該元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切換
             node = node.next;
          }
    
          return false;
       }
    
       // 刪除指定索引位置的節點
       remove(index) {
          // 驗證索引的合法性
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index > this.size');
          }
    
          let node = this.dummyHead;
          for (let i = 0; i < index; i++) {
             node = node.next;
          }
    
          // 待刪除的節點
          let delNode = node.next;
          // 給待刪除那個節點的前一個的節點的next引用替換爲
          // 但刪除的這個節點的next
          node.next = delNode.next;
          // 或者這樣也行
          // node.next = node.next.next;
    
          // 臨時存儲待刪除的那個節點裏的元素
          let element = delNode.element;
          // 清空 待刪除的節點
          delNode = null;
          this.size--;
    
          return element;
       }
    
       // 擴展:移除鏈表頭的元素
       removeFirst() {
          return this.remove(0);
       }
    
       // 擴展:移除鏈表尾部的元素
       removeLast() {
          return this.remove(this.size - 1);
       }
    
       // 輸出鏈表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在頁面上展現
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製代碼
  2. Main

    class Main {
       constructor() {
          this.alterLine('MyLinkedList Area');
          let mylinkedList = new MyLinkedList();
    
          for (let i = 1; i <= 5; i++) {
             mylinkedList.addFirst(i);
             console.log(mylinkedList.toString());
          }
          mylinkedList.insert(2, 88888);
          console.log(mylinkedList.toString());
    
          mylinkedList.remove(2);
          console.log(mylinkedList.toString());
    
          mylinkedList.removeFirst();
          console.log(mylinkedList.toString());
    
          mylinkedList.removeLast();
          console.log(mylinkedList.toString());
       }
    
       // 將內容顯示在頁面上
       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):在只對鏈表頭進行操做時爲O(1)
  2. 刪:O(n):在只對鏈表頭進行操做時爲O(1)
  3. 改:O(n)
  4. 查:O(n):只查鏈表頭的元素時爲O(1)
  5. 鏈表增刪改查的時間複雜度
    1. 總體上要比數組增刪改查的時間複雜度要差,
    2. 由於數組有一個優點,
    3. 你有索引的話你就能夠快速訪問,
    4. 而鏈表沒有這樣的優點
    5. 可是鏈表的優點是,
    6. 若是你只對鏈表頭進行增刪的操做,
    7. 那麼它的時間複雜度是 O(1)級別的,
    8. 並且它總體是動態的,因此不會大量的浪費內存空間。 6.鏈表適合作的事情
    9. 不去修改、也不去查任意的元素,
    10. 就算查,也只能查鏈表頭的元素,
    11. 查的時候,只有那樣纔是 O(1)級別的時間複雜度,
    12. 而且增長刪除的時候只對鏈表頭進行操做,
    13. 只有在這種時候纔會和數組總體的時間複雜度是同樣的,
    14. 不過鏈表總體是動態的,不會去大量的浪費內存空間,
    15. 因此它具備必定的優點。
  6. 鏈表還有諸多的改進的方式
    1. 因此應用鏈表在一些狀況下仍然是有它的優點的,
    2. 最最重要的是 鏈表自己是一種最最基礎的動態數據結構,
    3. 對這種動態數據結構要深入的理解和掌握,
    4. 對於學習更重要的動態數據結構是有巨大的幫助,
    5. 好比說二叉樹、平衡二叉樹、AVL、紅黑樹等等。

### 添加操做 O(n)

  1. addLast(e)O(n)
    1. 會從頭遍歷到尾,
    2. 找到最後一個節點以後才能添加元素
  2. addFirst(e)O(1)
    1. 直接從頭部添加元素
  3. insert(index, e)O(n/2) = O(n)
    1. 會去遍歷鏈表中每個節點,
    2. 檢索到合適的位置纔會添加這個元素。

刪除操做 O(n)

  1. removeLast()O(n)
    1. 會去遍歷鏈表中每個節點,
    2. 找到最後一個節點後纔會刪除
  2. removeFirst()O(1)
    1. 直接從頭部刪除這個節點
  3. remove(index)O(n/2) = O(n)
    1. 會去遍歷鏈表中每個節點,
    2. 檢索到合適的位置纔會移除這個元素

修改操做 O(n)

  1. set(index, e)O(n)
    1. 會去遍歷鏈表中每個節點,
    2. 找到了合適位置的節點後,
    3. 纔會修改元素

查找操做 O(n)

  1. get(index)O(n)
    1. 會去遍歷鏈表中每個節點,
    2. 找到了合適位置的節點後,
    3. 返回這個元素。
  2. contains(e)O(n)
    1. 會去遍歷鏈表中每個節點,
    2. 返回 是否有相同元素的 bool 值。
  3. find(e)O(n)
    1. 這個方法對鏈表來講是沒有用的,
    2. 就算你拿到了那個索引你也不能快速訪問。

使用鏈表來實現棧

  1. 對鏈表進行添加操做時
    1. 時間複雜度爲O(n)
    2. 可是在只對鏈表頭進行操做時爲O(1)
  2. 對鏈表進行刪除操做時
    1. 時間複雜度爲O(n)
    2. 可是在只對鏈表頭進行操做時爲O(1)
  3. 對鏈表進行查詢操做時
    1. 時間複雜度爲O(n)
    2. 可是在只查鏈表頭的元素時爲O(1)
  4. 這些特性很符合棧的需求
    1. 棧是後進先出的,而且棧只查棧頂的元素,
    2. 因此可使用鏈表來實現棧這樣的數據結構。

鏈表實現的棧

  1. MyLinkedListStack的接口。
    1. void push(E e):添加一個元素
    2. E pop():移除一個元素
    3. E peek():查看棧頂的元素
    4. int getSize():獲取棧中實際的元素個數
    5. boolean isEmpty():判斷棧是否爲空

代碼示例

  1. (class: MyLinkedList,class: MyLinkedListStack, class: Main)

  2. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 獲取鏈表中實際的節點個數
       getSize() {
          return this.size;
       }
    
       // 判斷鏈表是否爲空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在鏈表頭添加節點
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虛擬頭節點
          this.insert(0, element);
       }
    
       // 在鏈表指定索引處插入節點
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一個prev就是dummyHead
          let prev = this.dummyHead;
          // 以前變量i(索引)之因此要從 1 開始,由於索引爲0的那個節點就是head,循環就不須要從0開始了,
          // 如今索引之因此要從 0 開始, 由於初始化時 多增長了一個虛擬的頭節點
          // (由於這個索引爲0的節點並非dummyHead,dummyHead這個節點並不記錄爲鏈表中的實際節點),
          // 小於index是由於要找到指定索引位置的前一個節點
          // 循環是由於 要繼續找到指定索引處的節點的前一個節點
          for (var i = 0; i < index; i++) {
             // 不停的切換引用,直到找到對應索引處節點的下一個節點
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 擴展:在鏈表最後一個節點的位置添加節點
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 獲取指定索引位置的元素
       get(index) {
          // 判斷索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引節點的前一個節點 就使用dummyHead
          // 若是你要找到指定索引節點 就使用dummyHead.next
          // 由於duumyHead並非第一個節點,由於它是一個虛擬節點,
          // dummyHead.next纔是真正被記錄的第一個節點。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 獲取頭節點的元素
       getFirst() {
          return this.get(0);
       }
    
       // 獲取尾節點的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 設置指定索引位置的元素值
       set(index, element) {
          // 判斷索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 從第一個真正被記錄的節點開始,從0開始
          let node = this.dummyHead.next;
    
          // 索引爲 0 時,實際上切換到的節點 它的索引爲 1
          // i < index ,當索引爲 index-1 時, 實際上切換到的節點 它的索引爲index
          for (let i = 0; i < index; i++) {
             // 每一次切換 都只是改變引用
             // 不的在鏈表中找下一個節點
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部節點中是否有包含該元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切換
             node = node.next;
          }
    
          return false;
       }
    
       // 刪除指定索引位置的節點
       remove(index) {
          // 驗證索引的合法性
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index > this.size');
          }
    
          let node = this.dummyHead;
          for (let i = 0; i < index; i++) {
             node = node.next;
          }
    
          // 待刪除的節點
          let delNode = node.next;
          // 給待刪除那個節點的前一個的節點的next引用替換爲
          // 但刪除的這個節點的next
          node.next = delNode.next;
          // 或者這樣也行
          // node.next = node.next.next;
    
          // 臨時存儲待刪除的那個節點裏的元素
          let element = delNode.element;
          // 清空 待刪除的節點
          delNode = null;
          this.size--;
    
          return element;
       }
    
       // 擴展:移除鏈表頭的元素
       removeFirst() {
          return this.remove(0);
       }
    
       // 擴展:移除鏈表尾部的元素
       removeLast() {
          return this.remove(this.size - 1);
       }
    
       // 輸出鏈表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在頁面上展現
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製代碼
  3. MyLinkedListStack

    class MyLinkedListStack {
       constructor() {
          this.myLinkedList = new MyLinkedList();
       }
    
       // 入棧
       push(element) {
          this.myLinkedList.addFirst(element);
       }
    
       // 出棧
       pop() {
          return this.myLinkedList.removeFirst();
       }
    
       // 查看棧頂元素
       peek() {
          return this.myLinkedList.getFirst();
       }
    
       // 查看棧中實際元素的個數
       getSize() {
          return this.myLinkedList.getSize();
       }
    
       // 判斷棧是否爲空
       isEmpty() {
          return this.myLinkedList.isEmpty();
       }
    
       // 輸出棧中信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedListStack: size = ${this.getSize()},\n`;
          arrInfo += `data = stack top [`;
          let node = this.myLinkedList.dummyHead.next;
          for (var i = 1; i < this.getSize(); i++) {
             arrInfo += `${node.element},`;
             node = node.next;
          }
          if (!this.isEmpty()) {
             arrInfo += `${node.element}`;
          }
          arrInfo += ']';
    
          // 在頁面上展現
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製代碼
  4. Main

    class Main {
       constructor() {
          this.alterLine('MyLinkedListStack Area');
          let myLinkedListStack = new MyLinkedListStack();
          for (let i = 1; i <= 5; i++) {
             myLinkedListStack.push(i);
             console.log(myLinkedListStack.toString());
          }
    
          console.log(myLinkedListStack.peek());
          this.show(myLinkedListStack.peek());
    
          for (let i = 0; i < 5; i++) {
             console.log(myLinkedListStack.toString());
             myLinkedListStack.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. 自定義數組棧與自定義鏈表棧的性能相差不多
    1. 可是隨着操做的次數增加,數組棧會慢慢強過鏈表棧,
    2. 自定義鏈表棧中有太多的 new 操做,
    3. new 操做在有一些系統上比較耗費性能的,
    4. 由於它在不停的在內存中尋找能夠開闢空間的地方來進行開闢空間,
    5. 自定義數組棧中有比較多的擴容操做,
    6. 因此這個比較是相對比較複雜的,
    7. 和你的語法、操做系統、編譯器、解釋器都有關係,
    8. 不過他們的時間複雜度都是O(1)級別的,
    9. 因此他們之間的性能差別無非就 1-2 倍這樣,
    10. 在最極端的狀況下 3-5 倍就已經很難了,
    11. 不會有幾百倍的巨大的差別,由於畢竟他們的時間複雜度同樣。

代碼示例

  1. (class: MyLinkedList, class: MyLinkedListStack, class: MyArray, class: MyStack, class: Main)

  2. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 獲取鏈表中實際的節點個數
       getSize() {
          return this.size;
       }
    
       // 判斷鏈表是否爲空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在鏈表頭添加節點
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虛擬頭節點
          this.insert(0, element);
       }
    
       // 在鏈表指定索引處插入節點
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一個prev就是dummyHead
          let prev = this.dummyHead;
          // 以前變量i(索引)之因此要從 1 開始,由於索引爲0的那個節點就是head,循環就不須要從0開始了,
          // 如今索引之因此要從 0 開始, 由於初始化時 多增長了一個虛擬的頭節點
          // (由於這個索引爲0的節點並非dummyHead,dummyHead這個節點並不記錄爲鏈表中的實際節點),
          // 小於index是由於要找到指定索引位置的前一個節點
          // 循環是由於 要繼續找到指定索引處的節點的前一個節點
          for (var i = 0; i < index; i++) {
             // 不停的切換引用,直到找到對應索引處節點的下一個節點
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 擴展:在鏈表最後一個節點的位置添加節點
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 獲取指定索引位置的元素
       get(index) {
          // 判斷索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引節點的前一個節點 就使用dummyHead
          // 若是你要找到指定索引節點 就使用dummyHead.next
          // 由於duumyHead並非第一個節點,由於它是一個虛擬節點,
          // dummyHead.next纔是真正被記錄的第一個節點。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 獲取頭節點的元素
       getFirst() {
          return this.get(0);
       }
    
       // 獲取尾節點的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 設置指定索引位置的元素值
       set(index, element) {
          // 判斷索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 從第一個真正被記錄的節點開始,從0開始
          let node = this.dummyHead.next;
    
          // 索引爲 0 時,實際上切換到的節點 它的索引爲 1
          // i < index ,當索引爲 index-1 時, 實際上切換到的節點 它的索引爲index
          for (let i = 0; i < index; i++) {
             // 每一次切換 都只是改變引用
             // 不的在鏈表中找下一個節點
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部節點中是否有包含該元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切換
             node = node.next;
          }
    
          return false;
       }
    
       // 刪除指定索引位置的節點
       remove(index) {
          // 驗證索引的合法性
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index > this.size');
          }
    
          let node = this.dummyHead;
          for (let i = 0; i < index; i++) {
             node = node.next;
          }
    
          // 待刪除的節點
          let delNode = node.next;
          // 給待刪除那個節點的前一個的節點的next引用替換爲
          // 但刪除的這個節點的next
          node.next = delNode.next;
          // 或者這樣也行
          // node.next = node.next.next;
    
          // 臨時存儲待刪除的那個節點裏的元素
          let element = delNode.element;
          // 清空 待刪除的節點
          delNode = null;
          this.size--;
    
          return element;
       }
    
       // 擴展:移除鏈表頭的元素
       removeFirst() {
          return this.remove(0);
       }
    
       // 擴展:移除鏈表尾部的元素
       removeLast() {
          return this.remove(this.size - 1);
       }
    
       // 輸出鏈表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在頁面上展現
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製代碼
  3. MyLinkedListStack

    class MyLinkedListStack {
       constructor() {
          this.myLinkedList = new MyLinkedList();
       }
    
       // 入棧
       push(element) {
          this.myLinkedList.addFirst(element);
       }
    
       // 出棧
       pop() {
          return this.myLinkedList.removeFirst();
       }
    
       // 查看棧頂元素
       peek() {
          return this.myLinkedList.getFirst();
       }
    
       // 查看棧中實際元素的個數
       getSize() {
          return this.myLinkedList.getSize();
       }
    
       // 判斷棧是否爲空
       isEmpty() {
          return this.myLinkedList.isEmpty();
       }
    
       // 輸出棧中信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedListStack: size = ${this.getSize()},\n`;
          arrInfo += `data = stack top [`;
          let node = this.myLinkedList.dummyHead.next;
          for (var i = 1; i < this.getSize(); i++) {
             arrInfo += `${node.element},`;
             node = node.next;
          }
          if (!this.isEmpty()) {
             arrInfo += `${node.element}`;
          }
          arrInfo += ']';
    
          // 在頁面上展現
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製代碼
  4. 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;
       }
    }
    複製代碼
  5. 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;
       }
    }
    複製代碼
  6. Main

    class Main {
       constructor() {
          this.alterLine('Stacks Comparison Area');
          let myStack = new MyStack();
          let myLinkedListStack = new MyLinkedListStack();
          let performanceTest = new PerformanceTest();
    
          let myStackInfo = performanceTest.testStack(myStack, 100000);
          let myLinkedListStackInfo = performanceTest.testStack(
             myLinkedListStack,
             100000
          );
    
          this.alterLine('MyStack Area');
          console.log(myStackInfo);
          this.show(myStackInfo);
    
          this.alterLine('MyLinkedListStack Area');
          console.log(myLinkedListStackInfo);
          this.show(myLinkedListStackInfo);
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    class Student {
       constructor(studentName, studentScore) {
          this.name = studentName;
          this.score = studentScore;
       }
    
       toString() {
          let studentInfo = `Student(name: ${this.name}, score: ${this.score})`;
          return studentInfo;
       }
    }
    
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

使用鏈表來實現隊列

  1. 對鏈表進行添加操做時
    1. 時間複雜度爲O(n)
    2. 只對鏈表頭進行操做時爲O(1)
    3. 對鏈表尾部進行操做時爲O(n)
  2. 對鏈表進行刪除操做時
    1. 時間複雜度爲O(n)
    2. 只對鏈表頭進行操做時爲O(1)
    3. 對鏈表尾部進行操做時爲O(n)
  3. 對鏈表進行查詢操做時
    1. 時間複雜度爲O(n)
    2. 只查鏈表頭的元素時爲O(1)
    3. 查鏈表尾部的元素時爲O(n)
  4. 隊列中的操做
    1. 在線性結構的一端插入元素,
    2. 在另一端刪除元素,
    3. 因此必然會在線性結構的兩端同時操做,
    4. 此時就會有一端的操做的複雜度是O(n)級別,
    5. 這個問題在用自定義數組實現時也遇到了,
    6. 也正由於如此產生了循環隊列,
    7. 經過改進使用數組來實現隊列的方式,
    8. 因此鏈表也能夠進行改進。
  5. 改進鏈表
    1. 不能使用以前的鏈表來進行隊列的實現,
    2. 設置一個 head 變量指向鏈表的頭部第一個節點,
    3. 設置一個 tail 變量指向鏈表的尾部第一個節點,
    4. 有了 tail 這個變量,那麼在鏈表的尾部添加一個元素很是容易,
    5. 有了 head 和 tail 以後在兩端添加節點是很是容易的,
    6. 可是沒法使用O(1)去刪除尾部的節點,
    7. 從 tail 這一端刪除元素並不容易,
    8. 可是若是將 head 做爲隊首,將 tail 做爲隊尾,
    9. 隊列操做是從隊尾進隊首出的,
    10. 只須要從隊尾 tail 插入元素,從隊首 head 刪除元素,
    11. 這樣一來就能夠實現隊列的功能。
  6. 鏈表中再也不有插入的功能因此不須要 dummyHead
    1. 可是因爲沒有 dummyHead,因此須要注意鏈表爲空的狀況。
  7. 讓自定義動態數組隊列與自定義鏈表隊列進行對比
    1. 自定義動態數組隊列的性能最差
    2. 自定義動態數組循環隊列與自定義鏈表隊列的性能相近。
  8. 與鏈表相關的有一個很是重要的內容
    1. 就是遞歸,由於鏈表自己具備自然的遞歸性質,
    2. 同時它又是一種很是簡單的數據結構,
    3. 因此鏈表是一種很是好的
    4. 研究學習遞歸的邏輯機制的的數據結構。

代碼示例

  1. ( class: MyArray, class: MyQueue, class: MyLoopQueue, class: MyLinkedListQueue, 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. 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(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(Math.floor(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;
       }
    }
    複製代碼
  5. MyLinkedListQueue

    class MyLinkedListQueue {
       constructor() {
          this.front = this.tail = null;
          this.size = 0;
       }
    
       // 入隊
       enqueue(element) {
          // 判斷隊尾是否爲空
          if (this.tail === null) {
             // 第一個節點 便是尾也是頭
             this.tail = new MyLinkedListNode(element, null);
             this.front = this.tail;
          } else {
             let node = new MyLinkedListNode(element, null);
             this.tail.next = node;
             this.tail = node;
          }
          this.size++;
       }
    
       // 出隊
       dequeue() {
          // 判斷隊首是否爲空
          if (this.front === null) {
             throw new Error('front is empty.');
          }
    
          let delNode = this.front;
          let element = delNode.element;
          this.front = this.front.next;
          delNode = null;
          if (this.front === null)
             // 若是頭爲空了,那麼尾部也爲空
             this.tail = null;
          this.size--;
    
          return element;
       }
    
       // 查看隊首的元素
       getFront() {
          // 判斷隊首是否爲空
          if (this.front === null) {
             throw new Error('front is empty.');
          }
    
          return this.front.element;
       }
    
       // 查看隊列中實際元素的個數
       getSize() {
          return this.size;
       }
    
       // 判斷隊列是否爲空
       isEmpty() {
          return this.size === 0;
       }
    
       // 輸出隊列中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedListQueue: size = ${this.getSize()},\n`;
          arrInfo += `data = front [`;
          let node = this.front;
          for (var i = 1; i < this.getSize(); i++) {
             arrInfo += `${node.element},`;
             node = node.next;
          }
          if (!this.isEmpty()) {
             arrInfo += `${node.element}`;
          }
          arrInfo += '] tail';
    
          // 在頁面上展現
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    複製代碼
  6. Main

    class Main {
       constructor() {
          this.alterLine('MyLinkedListQueue Area');
          let myLinkedListQueue = new MyLinkedListQueue();
          for (let i = 1; i <= 5; i++) {
             myLinkedListQueue.enqueue(i);
             console.log(myLinkedListQueue.toString());
          }
    
          console.log(myLinkedListQueue.getFront());
          this.show(myLinkedListQueue.getFront());
    
          for (let i = 0; i < 5; i++) {
             console.log(myLinkedListQueue.toString());
             myLinkedListQueue.dequeue();
          }
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    class Student {
       constructor(studentName, studentScore) {
          this.name = studentName;
          this.score = studentScore;
       }
    
       toString() {
          let studentInfo = `Student(name: ${this.name}, score: ${this.score})`;
          return studentInfo;
       }
    }
    
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼
相關文章
相關標籤/搜索