JavaScript數據結構04 - 鏈表

一、定義

1.1 概念

前面我們學習了數組這種數據結構。數組(或者也可以稱爲列表)是一種非常簡單的存儲數據序列的數據結構。在這一節,我們要學習如何實現和使用鏈表這種動態的數據結構,這意味着我們可以從中任意添加或移除項,它會按需進行擴容。

要存儲多個元素,數組(或列表)可能是最常用的數據結構,它提供了一個便利的**[]**語法來訪問它的元素。然而,這種數據結構有一個缺點:(在大多數強類型語言中)數組的大小是固定的,需要預先分配,從數組的起點或中間插入或移除項的成本很高,因爲需要移動元素。 (注意:在JavaScript中數組的大小隨時可變,不需要預先定義長度)

鏈表存儲有序的元素集合,但不同於數組,鏈表中的元素在內存中並不是連續放置的。每個元素由一個存儲元素本身的節點和一個指向下一個元素的引用(也稱指針或鏈接)組成。

相對於傳統的數組,鏈表的一個好處在於,添加或刪除元素的時候不需要移動其他元素。然而,鏈表需要使用指針,因此實現鏈表時需要額外注意。數組的另一個細節是可以直接訪問任何位置的元素,而想要訪問鏈表中間的一個元素,需要從起點(表頭)開始迭代列表直到找到所需的元素。

火車可以當做生活中一個典型的鏈表的例子。一列火車是由一系列車廂組成的。每節車廂都相互連接。你很容易分離一節車廂,改變它的位置,添加或移除它。

1.2 分類

鏈表最常用的有三類

  1. 單向鏈表
  2. 雙向鏈表
  3. 循環鏈表

二、鏈表的實現

2.1 單向鏈表

創建單向鏈表類:

// SinglyLinkedList
function SinglyLinkedList () {
  function Node (element) {
    this.element = element;
    this.next = null;
  }

  var length = 0;
  var head = null;
  
  this.append = function (element) {};
  this.insert = function (position, element) {};
  this.removeAt = function (position) {};
  this.remove = function (element) {};
  this.indexOf = function (element) {};
  this.isEmpty = function () {};
  this.size = function () {};
  this.getHead = function () {};
  this.toString = function () {};
  this.print = function () {};
}
複製代碼

SinglyLinkedList需要一個輔助類Node。Node類表示要加入鏈表的項。它包含一個element屬性,即要添加到鏈表的值,以及一個next屬性,即指向鏈表中下一個節點項的指針。

鏈表裏面有一些聲明的輔助方法:

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

下面我們來一一實現這些輔助方法:

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

    // 判斷是否爲空鏈表
    if (currentNode === null) {
      // 空鏈表
      head = node;
    } else {
      // 從head開始一直找到最後一個node
      while (currentNode.next) {
        // 後面還有node
        currentNode = currentNode.next;
      }
      currentNode.next = 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) {
        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;
        }
        previousNode.next = currentNode.next;
      }

      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;
        }
        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());
  };
複製代碼

創建單向鏈表實例進行測試:

// 創建單向鏈表實例
var singlyLinked = new SinglyLinkedList();
console.log(singlyLinked.removeAt(0));              // true
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"
複製代碼

2.2 雙向鏈表

雙向鏈表和普通鏈表的區別在於,在普通鏈表中,一個節點只有鏈向下一個節點的鏈接,而在雙向鏈表中,鏈接是雙向的:一個鏈向下一個元素,另一個鏈向前一個元素。

創建雙向鏈表類:

// 創建雙向鏈表DoublyLinkedList類
function DoublyLinkedList () {
  function Node (element) {
    this.element = element;
    this.next = null;
    this.prev = null;        // 新增
  }

  var length = 0;
  var head = null;
  var tail = null;          // 新增
}
複製代碼

可以看到,雙向鏈表在Node類裏有prev屬性(一個新指針),在DoublyLinkedList類裏也有用來保存對列表最後一項的引用的tail屬性。

雙向鏈表提供了兩種迭代列表的方法:從頭到尾,或者從尾到頭。我們可以訪問一個特定節點的下一個或前一個元素。

在單向鏈表中,如果迭代鏈表時錯過了要找的元素,就需要回到鏈表起點,重新開始迭代。在雙向鏈表中,可以從任一節點,向前或向後迭代,這是雙向鏈表的一個優點。

實現雙向鏈表的輔助方法:

  // 向鏈表尾部添加一個新的項
  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());
  };
複製代碼

創建雙向鏈表實例進行測試:

// 創建雙向鏈表
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"
複製代碼

2.3 循環鏈表

循環鏈表可以像單向鏈表一樣只有單向引用,也可以像雙向鏈表一樣有雙向引用。循環鏈表和普通鏈表之間唯一的區別在於,最後一個元素指向下一個元素的指針(next)不是引用null,而是指向第一個元素(head)。這裏就不進行代碼實現了,大家可以結合上面的單向鏈表和雙向鏈表自己實現一個循環鏈表。

三、結束

完整代碼可以到我的github倉庫查看,如果對你有幫助的話歡迎點一個Star~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章