JavaScript 數據結構與算法之美 - 線性表(數組、棧、隊列、鏈表)

clipboard.png

前言

  1. 基礎知識就像是一座大樓的地基,它決定了咱們的技術高度。
  2. 咱們應該多掌握一些可移值的技術或者再過十幾年應該都不會過期的技術,數據結構與算法就是其中之一。

棧、隊列、鏈表、堆 是數據結構與算法中的基礎知識,是程序員的地基。前端

筆者寫的 JavaScript 數據結構與算法之美 系列用的語言是 JavaScript ,旨在入門數據結構與算法和方便之後複習。node

1. 線性表與非線性表

線性表(Linear List):就是數據排成像一條線同樣的結構。每一個線性表上的數據最多隻有前和後兩個方向。數組、鏈表、隊列、棧 等就是線性表結構。git

clipboard.png

非線性表:數據之間並非簡單的先後關係。二叉樹、堆、圖 就是非線性表。程序員

clipboard.png

本文主要講線性表,非線性表會在後面章節講。github

2. 數組

clipboard.png

定義

  • 數組 (Array) 是一個有序的數據集合,咱們能夠經過數組名稱 (name) 和索引 (index) 進行訪問。
  • 數組的索引是從 0 開始的。

特色

  • 數組是用一組連續的內存空間來存儲的

因此數組支持 隨機訪問,根據下標隨機訪問的時間複雜度爲 O(1)。面試

  • 低效的插入和刪除

數組爲了保持內存數據的連續性,會致使插入、刪除這兩個操做比較低效,由於底層一般是要進行大量的數據搬移來保持數據的連續性。
插入與刪除的時間複雜度以下:
插入:從最好 O(1) ,最壞 O(n) ,平均 O(n)
刪除:從最好 O(1) ,最壞 O(n) ,平均 O(n)算法

注意

可是由於 JavaScript 是弱類型的語言,弱類型則容許隱式類型轉換。編程

隱式:是指源碼中沒有明顯的類型轉換代碼。也就是說,一個變量,能夠賦值字符串,也能夠賦值數值。segmentfault

let str = "string"
str = 123 
console.log(str)  //   123

你還能夠直接讓字符串類型的變量和數值類型的變量相加,雖然得出的最終結果未必是你想象的那樣,但必定不會報錯。數組

let a = 123
let b = "456"
let c = a + b
// 數值加字符串,結果是字符串
console.log(c)  //   "123456"

數組的每一項能夠是不一樣的類型,好比:

// 數組的類型有 數值、字符串,還能夠隨意變動類型
const arr = [ 12, 34, "abc" ]
arr[2] = { "key": "value" }  // 把數組的第二項變成對象
console.log(arr) //  [ 12, 34,  { "key": "value"} ]

定義的數組的大小是可變的,不像強類型語言,定義某個數組變量的時候就要定義該變量的大小。

const arr = [ 12, 34, "abc"] 
arr.push({ "key": "value" }) // 添加一項 對象
consolelog(arr) //  [ 12, 34, "abc", { "key": "value" } ]

實現

JavaScript 原生支持數組,並且提供了不少操做方法,這裏不展開講。

3. 棧

clipboard.png

定義

  1. 後進者先出,先進者後出,簡稱 後進先出(LIFO),這就是典型的結構。
  2. 新添加的或待刪除的元素都保存在棧的末尾,稱做棧頂,另外一端就叫棧底
  3. 在棧裏,新元素都靠近棧頂,舊元素都接近棧底。
  4. 從棧的操做特性來看,是一種 操做受限的線性表,只容許在一端插入和刪除數據。
  5. 不包含任何元素的棧稱爲空棧

棧也被用在編程語言的編譯器和內存中保存變量、方法調用等,好比函數的調用棧。

實現

棧的方法:

  • push(element):添加一個(或幾個)新元素到棧頂。
  • pop():移除棧頂的元素,同時返回被移除的元素。
  • peek():返回棧頂的元素,不對棧作任何修改。
  • isEmpty():若是棧裏沒有任何元素就返回 true,不然返回 false。
  • clear():移除棧裏的全部元素。
  • size():返回棧裏的元素個數。
// Stack類
function Stack() {
  this.items = [];

  // 添加新元素到棧頂
  this.push = function(element) {
    this.items.push(element);
  };
  // 移除棧頂元素,同時返回被移除的元素
  this.pop = function() {
    return this.items.pop();
  };
  // 查看棧頂元素
  this.peek = function() {
    return this.items[this.items.length - 1];
  };
  // 判斷是否爲空棧
  this.isEmpty = function() {
    return this.items.length === 0;
  };
  // 清空棧
  this.clear = function() {
    this.items = [];
  };
  // 查詢棧的長度
  this.size = function() {
    return this.items.length;
  };
  // 打印棧裏的元素
  this.print = function() {
    console.log(this.items.toString());
  };
}

測試:

// 建立Stack實例
var stack = new Stack();
console.log(stack.isEmpty()); // true
stack.push(5); // undefined
stack.push(8); // undefined
console.log(stack.peek()); // 8
stack.push(11); // undefined
console.log(stack.size()); // 3
console.log(stack.isEmpty()); // false
stack.push(15); // undefined
stack.pop(); // 15
console.log(stack.size()); // 3
stack.print(); // 5,8,11
stack.clear(); // undefined
console.log(stack.size()); // 0

棧的應用實例:JavaScript 數據結構與算法之美 - 實現一個前端路由,如何實現瀏覽器的前進與後退 ?

4. 隊列

clipboard.png

普通隊列

定義

  • 隊列是遵循 FIFO(First In First Out,先進先出)原則的一組有序的項。
  • 隊列在尾部添加新元素,並從頂部移除元素。
  • 最新添加的元素必須排在隊列的末尾。
  • 隊列只有 入隊 push() 和出隊 pop()。

實現

隊列裏面有一些聲明的輔助方法:

  • enqueue(element):向隊列尾部添加新項。
  • dequeue():移除隊列的第一項,並返回被移除的元素。
  • front():返回隊列中第一個元素,隊列不作任何變更。
  • isEmpty():若是隊列中不包含任何元素,返回 true,不然返回 false。
  • size():返回隊列包含的元素個數,與數組的 length 屬性相似。
  • print():打印隊列中的元素。
  • clear():清空整個隊列。

代碼:

// Queue類
function Queue() {
    this.items = [];

    // 向隊列尾部添加元素
    this.enqueue = function(element) {
        this.items.push(element);
    };

    // 移除隊列的第一個元素,並返回被移除的元素
    this.dequeue = function() {
        return this.items.shift();
    };

    // 返回隊列的第一個元素
    this.front = function() {
        return this.items[0];
    };

    // 判斷是否爲空隊列
    this.isEmpty = function() {
        return this.items.length === 0;
    };

    // 獲取隊列的長度
    this.size = function() {
        return this.items.length;
    };

    // 清空隊列
    this.clear = function() {
        this.items = [];
    };

    // 打印隊列裏的元素
    this.print = function() {
        console.log(this.items.toString());
    };
}

測試:

// 建立Queue實例
var queue = new Queue();
console.log(queue.isEmpty()); // true
queue.enqueue('John'); // undefined
queue.enqueue('Jack'); // undefined
queue.enqueue('Camila'); // undefined
queue.print(); // "John,Jack,Camila"
console.log(queue.size()); // 3
console.log(queue.isEmpty()); // false
queue.dequeue(); // "John"
queue.dequeue(); // "Jack"
queue.print(); // "Camila"
queue.clear(); // undefined
console.log(queue.size()); // 0

優先隊列

定義

優先隊列中元素的添加和移除是依賴優先級的。

應用

  • 一個現實的例子就是機場登機的順序。頭等艙和商務艙乘客的優先級要高於經濟艙乘客。
  • 再好比:火車,老年人、孕婦和帶小孩的乘客是享有優先檢票權的。

優先隊列分爲兩類

  • 最小優先隊列
  • 最大優先隊列

最小優先隊列是把優先級的值最小的元素被放置到隊列的最前面(表明最高的優先級)。
好比:有四個元素:"John", "Jack", "Camila", "Tom",他們的優先級值分別爲 4,3,2,1。
那麼最小優先隊列排序應該爲:"Tom","Camila","Jack","John"。

最大優先隊列正好相反,把優先級值最大的元素放置在隊列的最前面。
以上面的爲例,最大優先隊列排序應該爲:"John", "Jack", "Camila", "Tom"。

實現

實現一個優先隊列,有兩種選項:

    1. 設置優先級,根據優先級正確添加元素,而後和普通隊列同樣正常移除
    1. 設置優先級,和普通隊列同樣正常按順序添加,而後根據優先級移除

這裏最小優先隊列和最大優先隊列我都採用第一種方式實現,你們能夠嘗試一下第二種。

下面只重寫 enqueue() 方法和 print() 方法,其餘方法和上面的普通隊列徹底相同。

實現最小優先隊列

// 定義最小優先隊列
function MinPriorityQueue () {
  this.items = [];

  this.enqueue = enqueue;
  this.dequeue = dequeue;
  this.front = front;
  this.isEmpty = isEmpty;
  this.size = size;
  this.clear = clear;
  this.print = print;
}

實現最小優先隊列 enqueue() 方法和 print() 方法:

// 優先隊列添加元素,要根據優先級判斷在隊列中的插入順序
function enqueue (element, priority) {
  var queueElement = {
    element: element,
    priority: priority
  };

  if (this.isEmpty()) {
    this.items.push(queueElement);
  } else {
    var added = false;
    for (var i = 0; i < this.size(); i++) {
      if (queueElement.priority < this.items[i].priority) {
        this.items.splice(i, 0, queueElement);
        added = true;
        break ;
      }
    }

    if (!added) {
      this.items.push(queueElement);
    }
  }
}

// 打印隊列裏的元素
function print () {
  var strArr = [];

  strArr = this.items.map(function (item) {
    return `${item.element}->${item.priority}`;
  });

  console.log(strArr.toString());
}

最小優先隊列測試:

// 建立最小優先隊列minPriorityQueue實例
var minPriorityQueue = new MinPriorityQueue();

console.log(minPriorityQueue.isEmpty());     // true
minPriorityQueue.enqueue("John", 1);         // undefined
minPriorityQueue.enqueue("Jack", 3);         // undefined
minPriorityQueue.enqueue("Camila", 2);       // undefined
minPriorityQueue.enqueue("Tom", 3);          // undefined
minPriorityQueue.print();                    // "John->1,Camila->2,Jack->3,Tom->3"
console.log(minPriorityQueue.size());        // 4
console.log(minPriorityQueue.isEmpty());     // false
minPriorityQueue.dequeue();                  // {element: "John", priority: 1}
minPriorityQueue.dequeue();                  // {element: "Camila", priority: 2}
minPriorityQueue.print();                    // "Jack->3,Tom->3"
minPriorityQueue.clear();                    // undefined
console.log(minPriorityQueue.size());        // 0

實現最大優先隊列

// 最大優先隊列 MaxPriorityQueue 類
function MaxPriorityQueue () {
  this.items = [];

  this.enqueue = enqueue;
  this.dequeue = dequeue;
  this.front = front;
  this.isEmpty = isEmpty;
  this.size = size;
  this.clear = clear;
  this.print = print;
}

// 優先隊列添加元素,要根據優先級判斷在隊列中的插入順序
function enqueue (element, priority) {
  var queueElement = {
    element: element,
    priority: priority
  };

  if (this.isEmpty()) {
    this.items.push(queueElement);
  } else {
    var added = false;

    for (var i = 0; i < this.items.length; i++) {
      // 注意,只須要將這裏改成大於號就能夠了
      if (queueElement.priority > this.items[i].priority) {
        this.items.splice(i, 0, queueElement);
        added = true;
        break ;
      }
    }

    if (!added) {
      this.items.push(queueElement);
    }
  }
}

最大優先隊列測試:

// 建立最大優先隊列maxPriorityQueue實例
var maxPriorityQueue = new MaxPriorityQueue();

console.log(maxPriorityQueue.isEmpty());     // true
maxPriorityQueue.enqueue("John", 1);         // undefined
maxPriorityQueue.enqueue("Jack", 3);         // undefined
maxPriorityQueue.enqueue("Camila", 2);       // undefined
maxPriorityQueue.enqueue("Tom", 3);          // undefined
maxPriorityQueue.print();                    // "Jack->3,Tom->3,Camila->2,John->1"
console.log(maxPriorityQueue.size());        // 4
console.log(maxPriorityQueue.isEmpty());     // false
maxPriorityQueue.dequeue();                  // {element: "Jack", priority: 3}
maxPriorityQueue.dequeue();                  // {element: "Tom", priority: 3}
maxPriorityQueue.print();                    // "Camila->2,John->1"
maxPriorityQueue.clear();                    // undefined
console.log(maxPriorityQueue.size());        // 0

循環隊列

定義

循環隊列,顧名思義,它長得像一個環。把它想像成一個圓的鐘就對了。

關鍵是:肯定好隊空和隊滿的斷定條件。

循環隊列的一個例子就是擊鼓傳花遊戲(Hot Potato)。在這個遊戲中,孩子們圍城一個圓圈,擊鼓的時候把花盡快的傳遞給旁邊的人。某一時刻擊鼓中止,這時花在誰的手裏,誰就退出圓圈直到遊戲結束。重複這個過程,直到只剩一個孩子(勝者)。

下面咱們在普通隊列的基礎上,實現一個模擬的擊鼓傳花遊戲,下面只寫擊鼓傳花的代碼片斷:

// 實現擊鼓傳花
function hotPotato (nameList, num) {
  var queue = new Queue();

  for (var i = 0; i < nameList.length; i++) {
    queue.enqueue(nameList[i]);
  }

  var eliminated = '';

  while (queue.size() > 1) {
    // 循環 num 次,隊首出來去到隊尾
    for (var i = 0; i < num; i++) {
      queue.enqueue(queue.dequeue());
    }
    // 循環 num 次事後,移除當前隊首的元素
    eliminated = queue.dequeue();
    console.log(`${eliminated} 在擊鼓傳花中被淘汰!`);
  }

  // 最後只剩一個元素
  return queue.dequeue();
}

// 測試
var nameList = ["John", "Jack", "Camila", "Ingrid", "Carl"];
var winner = hotPotato(nameList, 10);
console.log(`最後的勝利者是:${winner}`);

執行結果爲:

// John 在擊鼓傳花中被淘汰!
// Ingrid 在擊鼓傳花中被淘汰! 
// Jack 在擊鼓傳花中被淘汰!
// Camila 在擊鼓傳花中被淘汰!
// 最後的勝利者是:Carl

隊列小結

一些具備某些額外特性的隊列,好比:循環隊列、阻塞隊列、併發隊列。它們在不少偏底層系統、框架、中間件的開發中,起着關鍵性的做用。

以上隊列的代碼要感謝 leocoder351

5. 鏈表

定義

  • 鏈表存儲有序的元素集合,但不一樣於數組,鏈表中的元素在內存中並非連續放置的,它是經過 指針零散的內存塊 串連起來的。
  • 每一個元素由一個存儲元素自己的 節點 和一個指向下一個元素的 引用(也稱指針或連接)組成。

簡單的連接結構圖:

clipboard.png

其中,data 中保存着數據,next 保存着下一個鏈表的引用。
上圖中,咱們說 data2 跟在 data1 後面,而不是說 data2 是鏈表中的第二個元素。值得注意的是,咱們將鏈表的尾元素指向了 null 節點,表示連接結束的位置。

特色

  • 鏈表是經過指針將零散的內存塊串連起來的

因此鏈表不支持 隨機訪問,若是要找特定的項,只能從頭開始遍歷,直到找到某個項。
因此訪問的時間複雜度爲 O(n)。

  • 高效的插入和刪除

鏈表中插入或者刪除一個數據,咱們並不須要爲了保持內存的連續性而搬移結點,由於鏈表的存儲空間自己就不是連續的,只須要考慮相鄰結點的指針改變。
因此,在鏈表中插入和刪除一個數據是很是快速的,時間複雜度爲 O(1)。

三種最多見的鏈表結構,它們分別是:

  • 單鏈表
  • 雙向鏈表
  • 循環鏈表

單鏈表

定義

clipboard.png

因爲鏈表的起始點的肯定比較麻煩,所以不少鏈表的實現都會在鏈表的最前面添加一個特殊的節點,稱爲 頭節點,表示鏈表的頭部。

通過改造,鏈表就成了以下的樣子:

clipboard.png

針對鏈表的插入和刪除操做,咱們只須要考慮相鄰結點的指針改變,因此插入與刪除的時間複雜度爲 O(1)。

在 d2 節點後面插入 d4 節點:

clipboard.png

刪除 d4 節點:

clipboard.png

實現

  • Node 類用來表示節點。
  • LinkedList 類提供插入節點、刪除節點等一些操做。

單向鏈表的八種經常使用操做:

  • append(element):尾部添加元素。
  • insert(position, element):特定位置插入一個新的項。
  • removeAt(position):特定位置移除一項。
  • remove(element):移除一項。
  • indexOf(element):返回元素在鏈表中的索引。若是鏈表中沒有該元素則返回 -1。
  • isEmpty():若是鏈表中不包含任何元素,返回 true,若是鏈表長度大於 0,返回 false。
  • size():返回鏈表包含的元素個數,與數組的 length 屬性相似。
  • getHead():返回鏈表的第一個元素。
  • toString():因爲鏈表使用了 Node 類,就須要重寫繼承自 JavaScript 對象默認的 toString() 方法,讓其只輸出元素的值。
  • print():打印鏈表的全部元素。

具體代碼:

// 單鏈表
function SinglyLinkedList() {
    // 節點
    function Node(element) {
        this.element = element; // 當前節點的元素
        this.next = null; // 下一個節點指針
    }

    var length = 0; // 鏈表的長度
    var head = null; // 鏈表的頭部節點

    // 向鏈表尾部添加一個新的節點
    this.append = function(element) {
        var node = new Node(element);
        var currentNode = head;

        // 判斷是否爲空鏈表
        if (head === null) {
            // 是空鏈表,就把當前節點做爲頭部節點
            head = node;
        } else {
            // 從 head 開始一直找到最後一個 node
            while (currentNode.next) {
                // 後面還有 node
                currentNode = currentNode.next;
            }
            // 把當前節點的 next 指針 指向 新的節點
            currentNode.next = node;
        }
        // 鏈表的長度加 1
        length++;
    };

    // 向鏈表特定位置插入一個新節點
    this.insert = function(position, element) {
        if (position < 0 && position > length) {
            // 越界
            return false;
        } else {
            var node = new Node(element);
            var index = 0;
            var currentNode = head;
            var previousNode;

            // 在最前插入節點
            if (position === 0) {
                node.next = currentNode;
                head = node;
            } else {
                // 循環找到位置
                while (index < position) {
                    index++;
                    previousNode = currentNode;
                    currentNode = currentNode.next;
                }
                // 把前一個節點的指針指向新節點,新節點的指針指向當前節點,保持鏈接性
                previousNode.next = node;
                node.next = currentNode;
            }

            length++;

            return true;
        }
    };

    // 從鏈表的特定位置移除一項
    this.removeAt = function(position) {
        if ((position < 0 && position >= length) || length === 0) {
            // 越界
            return false;
        } else {
            var currentNode = head;
            var index = 0;
            var previousNode;

            if (position === 0) {
                head = currentNode.next;
            } else {
                // 循環找到位置
                while (index < position) {
                    index++;
                    previousNode = currentNode;
                    currentNode = currentNode.next;
                }
                // 把當前節點的 next 指針 指向 當前節點的 next 指針,便是 刪除了當前節點
                previousNode.next = currentNode.next;
            }

            length--;

            return true;
        }
    };

    // 從鏈表中移除指定項
    this.remove = function(element) {
        var index = this.indexOf(element);
        return this.removeAt(index);
    };

    // 返回元素在鏈表的索引,若是鏈表中沒有該元素則返回 -1
    this.indexOf = function(element) {
        var currentNode = head;
        var index = 0;

        while (currentNode) {
            if (currentNode.element === element) {
                return index;
            }

            index++;
            currentNode = currentNode.next;
        }

        return -1;
    };

    // 若是鏈表中不包含任何元素,返回 true,若是鏈表長度大於 0,返回 false
    this.isEmpty = function() {
        return length === 0;
    };

    // 返回鏈表包含的元素個數,與數組的 length 屬性相似
    this.size = function() {
        return length;
    };

    // 獲取鏈表頭部元素
    this.getHead = function() {
        return head.element;
    };

    // 因爲鏈表使用了 Node 類,就須要重寫繼承自 JavaScript 對象默認的 toString() 方法,讓其只輸出元素的值
    this.toString = function() {
        var currentNode = head;
        var string = '';

        while (currentNode) {
            string += ',' + currentNode.element;
            currentNode = currentNode.next;
        }

        return string.slice(1);
    };

    // 打印鏈表數據
    this.print = function() {
        console.log(this.toString());
    };

    // 獲取整個鏈表
    this.list = function() {
        console.log('head: ', head);
        return head;
    };
}

測試:

// 建立單向鏈表實例
var singlyLinked = new SinglyLinkedList();
console.log(singlyLinked.removeAt(0)); // false
console.log(singlyLinked.isEmpty()); // true
singlyLinked.append('Tom');
singlyLinked.append('Peter');
singlyLinked.append('Paul');
singlyLinked.print(); // "Tom,Peter,Paul"
singlyLinked.insert(0, 'Susan');
singlyLinked.print(); // "Susan,Tom,Peter,Paul"
singlyLinked.insert(1, 'Jack');
singlyLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(singlyLinked.getHead()); // "Susan"
console.log(singlyLinked.isEmpty()); // false
console.log(singlyLinked.indexOf('Peter')); // 3
console.log(singlyLinked.indexOf('Cris')); // -1
singlyLinked.remove('Tom');
singlyLinked.removeAt(2);
singlyLinked.print(); // "Susan,Jack,Paul"
singlyLinked.list(); // 具體控制檯

整個鏈表數據在 JavaScript 裏是怎樣的呢 ?

爲了看這個數據,特地寫了個 list 函數:

// 獲取整個鏈表
    this.list = function() {
        console.log('head: ', head);
        return head;
    };

重點上上面的最後一行代碼: singlyLinked.list() ,打印的數據以下:

clipboard.png

因此,在 JavaScript 中,單鏈表的真實數據有點相似於對象,其實是 Node 類生成的實例。

雙向鏈表

單向鏈表只有一個方向,結點只有一個後繼指針 next 指向後面的結點。
而雙向鏈表,它支持兩個方向,每一個結點不止有一個後繼指針 next 指向後面的結點,還有一個前驅指針 prev 指向前面的結點。

clipboard.png

clipboard.png

clipboard.png

單向鏈表與又向鏈表比較

  • 雙向鏈表須要額外的兩個空間來存儲後繼結點和前驅結點的地址。

因此,若是存儲一樣多的數據,雙向鏈表要比單鏈表佔用更多的內存空間。
雖然兩個指針比較浪費存儲空間,但能夠支持雙向遍歷,這樣也帶來了雙向鏈表操做的靈活性。

  • 雙向鏈表提供了兩種迭代列表的方法:從頭至尾,或者從尾到頭

咱們能夠訪問一個特定節點的下一個或前一個元素。

  • 在單向鏈表中,若是迭代鏈表時錯過了要找的元素,就須要回到鏈表起點,從新開始迭代。
  • 在雙向鏈表中,能夠從任一節點,向前或向後迭代,這是雙向鏈表的一個優勢。
  • 因此,雙向鏈表能夠支持 O(1) 時間複雜度的狀況下找到前驅結點,正是這樣的特色,也使雙向鏈表在某些狀況下的插入、刪除等操做都要比單鏈表簡單、高效。

實現

具體代碼:

// 建立雙向鏈表 DoublyLinkedList 類
function DoublyLinkedList() {
  function Node(element) {
    this.element = element; //當前節點的元素
    this.next = null; //下一個節點指針
    this.previous = null; //上一個節點指針
  }

  var length = 0; // 鏈表長度
  var head = null; // 鏈表頭部
  var tail = null; // 鏈表尾部

  // 向鏈表尾部添加一個新的項
  this.append = function(element) {
    var node = new Node(element);
    var currentNode = tail;

    // 判斷是否爲空鏈表
    if (currentNode === null) {
      // 空鏈表
      head = node;
      tail = node;
    } else {
      currentNode.next = node;
      node.prev = currentNode;
      tail = node;
    }

    length++;
  };

  // 向鏈表特定位置插入一個新的項
  this.insert = function(position, element) {
    if (position < 0 && position > length) {
      // 越界
      return false;
    } else {
      var node = new Node(element);
      var index = 0;
      var currentNode = head;
      var previousNode;

      if (position === 0) {
        if (!head) {
          head = node;
          tail = node;
        } else {
          node.next = currentNode;
          currentNode.prev = node;
          head = node;
        }
      } else if (position === length) {
        this.append(element);
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }

        previousNode.next = node;
        node.next = currentNode;

        node.prev = previousNode;
        currentNode.prev = node;
      }

      length++;

      return true;
    }
  };

  // 從鏈表的特定位置移除一項
  this.removeAt = function(position) {
    if ((position < 0 && position >= length) || length === 0) {
      // 越界
      return false;
    } else {
      var currentNode = head;
      var index = 0;
      var previousNode;

      if (position === 0) {
        // 移除第一項
        if (length === 1) {
          head = null;
          tail = null;
        } else {
          head = currentNode.next;
          head.prev = null;
        }
      } else if (position === length - 1) {
        // 移除最後一項
        if (length === 1) {
          head = null;
          tail = null;
        } else {
          currentNode = tail;
          tail = currentNode.prev;
          tail.next = null;
        }
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }
        previousNode.next = currentNode.next;
        previousNode = currentNode.next.prev;
      }

      length--;

      return true;
    }
  };

  // 從鏈表中移除指定項
  this.remove = function(element) {
    var index = this.indexOf(element);
    return this.removeAt(index);
  };

  // 返回元素在鏈表的索引,若是鏈表中沒有該元素則返回 -1
  this.indexOf = function(element) {
    var currentNode = head;
    var index = 0;

    while (currentNode) {
      if (currentNode.element === element) {
        return index;
      }

      index++;
      currentNode = currentNode.next;
    }

    return -1;
  };

  // 若是鏈表中不包含任何元素,返回 true ,若是鏈表長度大於 0 ,返回 false
  this.isEmpty = function() {
    return length == 0;
  };

  // 返回鏈表包含的元素個數,與數組的 length 屬性相似
  this.size = function() {
    return length;
  };

  // 獲取鏈表頭部元素
  this.getHead = function() {
    return head.element;
  };

  // 因爲鏈表使用了 Node 類,就須要重寫繼承自 JavaScript 對象默認的 toString() 方法,讓其只輸出元素的值
  this.toString = function() {
    var currentNode = head;
    var string = '';

    while (currentNode) {
      string += ',' + currentNode.element;
      currentNode = currentNode.next;
    }

    return string.slice(1);
  };

  this.print = function() {
    console.log(this.toString());
  };

  // 獲取整個鏈表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };
}

測試:

// 建立雙向鏈表
var doublyLinked = new DoublyLinkedList();
console.log(doublyLinked.isEmpty()); // true
doublyLinked.append('Tom');
doublyLinked.append('Peter');
doublyLinked.append('Paul');
doublyLinked.print(); // "Tom,Peter,Paul"
doublyLinked.insert(0, 'Susan');
doublyLinked.print(); // "Susan,Tom,Peter,Paul"
doublyLinked.insert(1, 'Jack');
doublyLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(doublyLinked.getHead()); // "Susan"
console.log(doublyLinked.isEmpty()); // false
console.log(doublyLinked.indexOf('Peter')); // 3
console.log(doublyLinked.indexOf('Cris')); // -1
doublyLinked.remove('Tom');
doublyLinked.removeAt(2);
doublyLinked.print(); // "Susan,Jack,Paul"
doublyLinked.list(); // 請看控制檯輸出

整個鏈表數據在 JavaScript 裏是怎樣的呢 ?

// 獲取整個鏈表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };

調用 doublyLinked.list(); .

控制檯輸出以下:

clipboard.png

鏈表代碼實現的關鍵是弄清楚:前節點與後節點與邊界。

循環鏈表

循環鏈表是一種特殊的單鏈表。
循環鏈表和單鏈表類似,節點類型都是同樣。
惟一的區別是,在建立循環鏈表的時候,讓其頭節點的 next 屬性指向它自己
即:

head.next = head;

這種行爲會致使鏈表中每一個節點的 next 屬性都指向鏈表的頭節點,換句話說,也就是鏈表的尾節點指向了頭節點,造成了一個循環鏈表。以下圖所示:

clipboard.png

循環鏈表:在單鏈表的基礎上,將尾節點的指針指向頭結點,就構成了一個循環鏈表。環形鏈表從任意一個節點開始,均可以遍歷整個鏈表。

代碼:

// 循環鏈表
function CircularLinkedList() {
    // 節點
    function Node(element) {
        this.element = element; // 當前節點的元素
        this.next = null; // 下一個節點指針
    }

    var length = 0,
        head = null;

    this.append = function(element) {
        var node = new Node(element),
            current;

        if (!head) {
            head = node;
            // 頭的指針指向本身
            node.next = head;
        } else {
            current = head;

            while (current.next !== head) {
                current = current.next;
            }

            current.next = node;
            // 最後一個節點指向頭節點
            node.next = head;
        }

        length++;
        return true;
    };

    this.insert = function(position, element) {
        if (position > -1 && position < length) {
            var node = new Node(element),
                index = 0,
                current = head,
                previous;

            if (position === 0) {
                // 頭節點指向本身
                node.next = head;
                head = node;
            } else {
                while (index++ < position) {
                    previous = current;
                    current = current.next;
                }
                previous.next = node;
                node.next = current;
            }
            length++;
            return true;
        } else {
            return false;
        }
    };
    this.removeAt = function(position) {
        if (position > -1 && position < length) {
            var current = head,
                previous,
                index = 0;
            if (position === 0) {
                head = current.next;
            } else {
                while (index++ < position) {
                    previous = current;
                    current = current.next;
                }
                previous.next = current.next;
            }
            length--;
            return current.element;
        } else {
            return false;
        }
    };
    this.remove = function(element) {
        var current = head,
            previous,
            indexCheck = 0;
        while (current && indexCheck < length) {
            if (current.element === element) {
                if (indexCheck == 0) {
                    head = current.next;
                    length--;
                    return true;
                } else {
                    previous.next = current.next;
                    length--;
                    return true;
                }
            } else {
                previous = current;
                current = current.next;
                indexCheck++;
            }
        }
        return false;
    };
    this.remove = function() {
        if (length === 0) {
            return false;
        }
        var current = head,
            previous,
            indexCheck = 0;
        if (length === 1) {
            head = null;
            length--;
            return current.element;
        }
        while (indexCheck++ < length) {
            previous = current;
            current = current.next;
        }
        previous.next = head;
        length--;
        return current.element;
    };
    this.indexOf = function(element) {
        var current = head,
            index = 0;
        while (current && index < length) {
            if (current.element === element) {
                return index;
            } else {
                index++;
                current = current.next;
            }
        }
        return -1;
    };
    this.isEmpty = function() {
        return length === 0;
    };
    this.size = function() {
        return length;
    };

    // 因爲鏈表使用了 Node 類,就須要重寫繼承自 JavaScript 對象默認的 toString() 方法,讓其只輸出元素的值
    this.toString = function() {
        var current = head,
            string = '',
            indexCheck = 0;
        while (current && indexCheck < length) {
            string += ',' + current.element;
            current = current.next;
            indexCheck++;
        }
        return string.slice(1);
    };

    // 獲取鏈表頭部元素
    this.getHead = function() {
        return head.element;
    };

    // 打印鏈表數據
    this.print = function() {
        console.log(this.toString());
    };

    // 獲取整個鏈表
    this.list = function() {
        console.log('head: ', head);
        return head;
    };
}

測試:

// 建立單向鏈表實例
var circularLinked = new CircularLinkedList();
console.log(circularLinked.removeAt(0)); // false
console.log(circularLinked.isEmpty()); // true
circularLinked.append('Tom');
circularLinked.append('Peter');
circularLinked.append('Paul');
circularLinked.print(); // "Tom,Peter,Paul"
circularLinked.insert(0, 'Susan');
circularLinked.print(); // "Susan,Tom,Peter,Paul"
circularLinked.insert(1, 'Jack');
circularLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(circularLinked.getHead()); // "Susan"
console.log(circularLinked.isEmpty()); // false
console.log(circularLinked.indexOf('Peter')); // 3
console.log(circularLinked.indexOf('Cris')); // -1
circularLinked.remove('Tom');
circularLinked.removeAt(2);
circularLinked.print(); // "Susan,Jack,Paul"
circularLinked.list(); // 具體控制檯

整個鏈表數據在 JavaScript 裏是怎樣的呢 ?

// 獲取整個鏈表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };

調用 circularLinked.list() 。

控制檯輸出以下:

clipboard.png

你知道你們發現沒有,爲何從 1 - 4 - 1 了,還有 next 節點,並且是還能夠一直點 next ,重複的展開下去,這正是 循環 的緣由。

鏈表總結

  • 寫鏈表代碼是最考驗邏輯思惟能力的,要熟練鏈表,只有 多寫多練,沒有捷徑
  • 由於,鏈表代碼處處都是指針的操做、邊界條件的處理,稍有不慎就容易產生 Bug。
  • 鏈表代碼寫得好壞,能夠看出一我的寫代碼是否夠細心,考慮問題是否全面,思惟是否縝密。
  • 因此,這也是不少面試官喜歡讓人手寫鏈表代碼的緣由。
  • 必定要本身寫代碼實現一下,纔有效果。

6. 文章輸出計劃

JavaScript 數據結構與算法之美 的系列文章,堅持 3 - 7 天左右更新一篇,暫定計劃以下表。

| 標題 | 連接 |
| :------ | :------ |
| 時間和空間複雜度 | https://github.com/biaochenxu... |
| 線性表(數組、鏈表、棧、隊列) | https://github.com/biaochenxu... |
| 實現一個前端路由,如何實現瀏覽器的前進與後退 ?| https://github.com/biaochenxu... |
| 棧內存與堆內存 、淺拷貝與深拷貝 | 精彩待續 |
| 非線性表(樹、堆) | 精彩待續 |
| 遞歸 | 精彩待續 |
| 冒泡排序 | 精彩待續 |
| 插入排序 | 精彩待續 |
| 選擇排序 | 精彩待續 |
| 歸併排序 | 精彩待續 |
| 快速排序 | 精彩待續 |
| 計數排序 | 精彩待續 |
| 基數排序 | 精彩待續 |
| 桶排序 | 精彩待續 |
| 希爾排序 | 精彩待續 |
| 堆排序 | 精彩待續 |
| 十大經典排序彙總 | 精彩待續 |

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。

7. 最後

文章中的代碼已經所有放在了個人 github 上,若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

關注個人公衆號,第一時間接收最新的精彩博文。

文章能夠轉載,但須註明做者及出處,須要轉載到公衆號的,喊我加下白名單就好了。

參考文章:

數組:爲何不少編程語言中數組都從 0 開始編號?
JS中的算法與數據結構——鏈表(Linked-list)
JavaScript數據結構 03 - 隊列
鏈表(上):如何實現 LRU 緩存淘汰算法?
JavaScript數據結構——隊列

clipboard.png

clipboard.png

相關文章
相關標籤/搜索