JavaScript - 數據結構詳解(一)

數據結構的概念

數據結構,直白的理解,就是研究數據的存儲方式。算法

數據結構是一門學科,它教會咱們「如何存儲具備複雜關係的數據更有助於後期對數據的再利用」。編程

存儲結構

數據結構大體包含如下幾種存儲結構:數組

  1. 線性表數據結構

    • 順序表
    • 鏈表
    • 棧和隊列
  2. 樹結構編程語言

    • 普通樹
    • 二叉樹
    • 線索二叉樹等
  3. 圖存儲結構

下面對各類數據結構作詳細講解。測試

線性表

將具備 「一對一」 關係的數據 「線性」 地存儲到物理空間中,這種存儲結構就稱爲線性存儲結構(簡稱線性表)。this

使用線性表存儲數據的方式能夠這樣理解,即「把全部數據用一根線兒串起來,再存儲到物理空間中」。

線性表

使用線性表存儲的數據,要求數據類型必須一致。

線性表並非一種具體的存儲結構,它包含順序存儲結構鏈式存儲結構,是順序表和鏈表的統稱。spa

上圖中咱們能夠看出,線性表存儲數據可細分爲如下 2 種:設計

  • 如圖 3a) 所示,將數據依次存儲在連續的整塊物理空間中,這種存儲結構稱爲順序存儲結構(簡稱順序表);
  • 如圖 3b) 所示,數據分散的存儲在物理空間中,經過一根線保存着它們之間的邏輯關係,這種存儲結構稱爲鏈式存儲結構(簡稱鏈表);

前驅和後繼

數據結構中,一組數據中的每一個個體被稱爲數據元素,簡稱元素指針

另外,對於具備 「一對一」 邏輯關係的數據,咱們一直在用「某一元素的左側(前邊)或右側(後邊)」這樣不專業的詞,其實線性表中有更準確的術語:

  • 某一元素的左側相鄰元素稱爲 直接前驅,位於此元素左側的全部元素都統稱爲 前驅元素
  • 某一元素的右側相鄰元素稱爲 直接後繼,位於此元素右側的全部元素都統稱爲 後繼元素

以下圖,元素 3 它的直接前驅是 2 ,此元素的前驅元素有 2 個,分別是 12;同理,此元素的直接後繼是 4 ,後繼元素也有 2 個,分別是 45

前驅和後繼

順序表

順序表,全名順序存儲結構,是線性表的一種。

順序表存儲數據時,會提早申請一整塊足夠大小的物理空間,而後將數據依次存儲起來,存儲時作到數據元素之間不留一絲縫隙。

順序表,簡單的理解就是經常使用的數組,例如使用順序表存儲 [1, 2, 3, 4, 5],如圖:

順序表

因爲順序表結構的底層實現藉助的就是數組,所以對於初學者來講,能夠把順序表徹底等價爲數組,但實則不是這樣。數據結構是研究數據存儲方式的一門學科,它囊括的都是各類存儲結構,而數組只是各類編程語言中的基本數據類型,並不屬於數據結構的範疇。

順序表的初始化

使用順序表存儲數據以前,除了要申請足夠大小的物理空間以外,爲了方便後期使用表中的數據,順序表還須要實時記錄如下 2 項數據:

  • 順序表申請的存儲容量;
  • 順序表的長度,也就是表中存儲數據元素的個數;

因爲順序表能夠簡單的理解爲數組,因此在這裏就定義一個數組,將數組看成一個順序表來處理。

和大多數其餘語言不一樣, JavaScript 數組的 length 是沒有上界的。 因此存儲容量也是動態變化的。
// 定義一個順序表
function List(list) {
  this.data = list || [];
  this.getLength = getLength;
  this.clear = clear; // 清空順序表
  this.insertEl = insertEl; // 插入元素
  this.removeEl = removeEl; // 刪除元素
  this.changeEl = changeEl; // 更改元素
  this.findEl = findEl;     // 查找元素
}

順序表插入元素

向已有順序表中插入數據元素,根據插入位置的不一樣,可分爲如下 3 種狀況:

  • 插入到順序表的表頭;
  • 在表的中間位置插入元素;
  • 尾隨順序表中已有元素,做爲順序表中的最後一個元素;

雖然數據元素插入順序表中的位置有所不一樣,可是都使用的是同一種方式去解決,即:經過遍歷,找到數據元素要插入的位置,而後作以下兩步工做:

  • 將要插入位置元素以及後續的元素總體向後移動一個位置;
  • 將元素放到騰出來的位置上;

例如,在 [1, 2, 3, 4, 5] 的第 3 個位置上插入元素 6,實現過程以下:

  • 遍歷至順序表存儲第 3 個數據元素的位置。

    找到目標元素位置

  • 將元素 3 以及後續元素 45 總體向後移動一個位置。

    將插入位置騰出

  • 將新元素 6 放入騰出的位置。

    插入目標元素

function getLength() {
  return this.data.length;
}

/**
 * @param {any} el 要插入的元素
 * @param {Number} index 元素下標
 */
function insertEl(el, index) {
  if (index > this.getLength() || index < 0) {
      throw new Error('插入位置有錯');
  } else {
    // 插入操做,須要將從插入位置開始的後續元素,逐個後移
    for (var len = this.getLength(); len >= index; len--) {
        this.data[len] = this.data[len - 1];
    }
    // 後移完成後,直接將所需插入元素,添加到順序表的相應位置
    this.data[index] = el;
    return this.data;
  }
}
爲數組添加元素,能夠經過 push()unshift()splice(index, 0, element) 方法實現。
// 向數組頭部添加元素:
arr.unshift();
// 向數組末尾添加元素:
arr.push();
// 向數組下標 index 處插入元素:
arr.splice(index, 0, el)

順序表刪除元素

從順序表中刪除指定元素,只需找到目標元素,並將其後續全部元素總體前移 1 個位置便可。

後續元素總體前移一個位置,會直接將目標元素刪除,可間接實現刪除元素的目的。

例如,從 [1, 2, 3, 4, 5] 中刪除元素 3 的過程,以下圖所示:

順序表刪除元素

js 代碼實現:

/**
 * @param {Number} index 被刪除元素的下標
 */
function removeEl(index) {
  if (index > this.getLength() || index < 0) {
      throw new Error('刪除位置有誤');
  } else {
    // 刪除操做
    for (var i = index; i < this.getLength() - 1; i++) {
       this.data[i] = this.data[i + 1];
    }
    this.data.length--;
    return this.data;
  }
}
刪除數組的元素,能夠經過 pop()shift()splice(index, 1) 方法實現。
// 從數組頭部刪除元素:
arr.shift();
// 從數組末尾刪除元素:
arr.pop();
// 從數組下標 index 處刪除一個元素:
arr.splice(index, 1);

順序表查找元素

順序表中查找目標元素,可使用多種查找算法實現,好比說二分查找算法、插值查找算法等。

這裏,咱們選擇順序查找算法,具體實現代碼爲:

/**
 * @param {any} el 須要查找的元素
 */
function findEl(el) {
  for (let i = 0; i < this.getLength(); i++) {
    if (this.data[i] == el) {
        return i; // 第一次匹配到的元素的下標
    }
  }
  return -1; //若是查找失敗,返回 -1
}
查找數組元素的下標,可使用 indexOf()lastIndexOf() 方法實現。

順序表更改元素

順序表更改元素的實現過程是:

  • 找到目標元素;
  • 直接修改該元素的值;
/**
 * @param {any} el 須要更改的元素
 * @param {any} newEl 爲新的數據元素
 */
function changeEl( el, newEl) {
  const index = this.findEl(el);
  this.data[index] = newEl;
  return this.data;
}

以上是順序表使用過程當中最經常使用的基本操做,這裏給出完整的實現代碼:

class List {
  constructor(list) {
    this.data = list || [];
  }

  clear() {
    delete this.data;
    this.data = [];
    return this.data;
  }

  getLength() {
    return this.data.length;
  }

  insertEl(index, el) {
    if (index > this.getLength() || index < 0) {
        throw new Error('插入位置有錯');
    } else {
      // 插入操做,須要將從插入位置開始的後續元素,逐個後移
      for (var len = this.getLength(); len >= index; len--) {
          this.data[len] = this.data[len - 1];
      }
      // 後移完成後,直接將所需插入元素,添加到順序表的相應位置
      this.data[index] = el;
      return this.data;
    }
  }

  // 經過下標刪除元素
  removeEl(index) {
    if (index > this.getLength() || index < 0) {
        throw new Error('刪除位置有誤');
    } else {
      // 刪除操做
      for (var i = index; i < this.getLength() - 1; i++) {
         this.data[i] = this.data[i + 1];
      }
      this.data.length--;
      return this.data;
    }
  }

  changeEl(el, newEl) {
    const index = this.findEl(el);
    this.data[index] = newEl;
    return this.data;
  }

  findEl(el) {
    for (let i = 0; i < this.getLength(); i++) {
      if (this.data[i] == el) {
          return i; // 第一次匹配到的元素的下標
      }
    }
    return -1; //若是查找失敗,返回 -1
  }
}

const list = new List([1, 2, 3, 4, 5])

console.log('初始化順序表:')
console.log(list.data.join(','));

console.log('刪除下標爲 0 的元素:')
console.log(list.removeEl(0).join(','));

console.log('在下標爲 3 的位置插入元素 6:')
console.log(list.insertEl(3, 6).join(','));

console.log('查找元素 3 的下標:')
console.log(list.findEl(3));

console.log('將元素 3 改成 6:')
console.log(list.changeEl(3, 6).join(','))

程序運行結果:

初始化順序表:
1,2,3,4,5

刪除下標爲 0 的元素:
2,3,4,5

在下標爲 3 的位置插入元素 6:
2,3,4,6,5

查找元素 3 的下標:
1

將元素 3 改成 6:
2,6,4,6,5

順序表(數組)的缺點

數組不老是組織數據的最佳數據結構,緣由以下。在不少編程語言中,數組的長度是固定的,因此當數組已被數據填滿時,再要加入新的元素就會很是困難。在數組中,添加和刪除元素也很麻煩,由於須要將數組中的其餘元素向前或向後平移,以反映數組剛剛進行了添加或刪除操做。

然而,JavaScript 的數組並不存在上述問題,由於使用 split() 方法不須要再訪問數組中的其餘元素了。

JavaScript 中數組的主要問題是,它們被實現成了對象,與其餘語言(好比 C++ 和 Java) 的數組相比,效率很低(請參考 Crockford 那本書的第 6 章)。

若是你發現數組在實際使用時很慢,就能夠考慮使用鏈表來替代它。除了對數據的隨機訪問,鏈表幾乎能夠用在任何可使用一維數組的狀況中。若是須要隨機訪問,數組仍然是更好的選擇。

鏈表

鏈表,別名鏈式存儲結構單鏈表,用於存儲邏輯關係爲 「一對一」 的數據。

咱們知道,使用順序表(底層實現靠數組)時,須要提早申請必定大小的存儲空間,這塊存儲空間的物理地址是連續的,以下圖所示。

順序表

鏈表則徹底不一樣,使用鏈表存儲數據時,是隨用隨申請,所以數據的存儲位置是相互分離的,換句話說,數據的存儲位置是隨機的。

單鏈表

咱們看到,上圖根本沒法體現出各數據之間的邏輯關係。對此,鏈表的解決方案是,每一個數據元素在存儲時都配備一個指針,用於指向本身的直接後繼元素,以下圖所示。

鏈表

數據元素隨機存儲,並經過指針表示數據之間邏輯關係的存儲結構就是鏈式存儲結構。

鏈表的節點

鏈表中每一個數據的存儲都由如下兩部分組成:

  • 數據元素自己,其所在的區域稱爲數據域
  • 指向直接後繼元素的指針,所在的區域稱爲指針域

鏈表中存儲各數據元素的結構以下圖所示:

鏈表

上圖所示的結構在鏈表中稱爲節點。也就是說,鏈表實際存儲的是一個一個的節點,真正的數據元素包含在這些節點中

節點

頭節點、頭指針和首元節點

其實,一個完整的鏈表須要由如下幾部分構成:

  1. 頭指針:一個普通的指針,它的特色是永遠指向鏈表第一個節點的位置。很明顯,頭指針用於指明鏈表的位置,便於後期找到鏈表並使用表中的數據;
  2. 節點:鏈表中的節點又細分爲頭節點首元節點其餘節點

    • 頭節點:其實就是一個不存任何數據的空節點,一般做爲鏈表的第一個節點。對於鏈表來講,頭節點不是必須的,它的做用只是爲了方便解決某些實際問題;
    • 首元節點:因爲頭節點(也就是空節點)的緣故,鏈表中稱第一個存有數據的節點爲首元節點。首元節點只是對鏈表中第一個存有數據節點的一個稱謂,沒有實際意義;
    • 其餘節點:鏈表中其餘的節點;

一個存儲 [1, 2, 3] 的完整鏈表結構如圖所示:

鏈表

鏈表中有頭節點時,頭指針指向頭節點;反之,若鏈表中沒有頭節點,則頭指針指向首元節點。

鏈表的初始化

咱們設計的鏈表包含兩個類:

  • Node 類用來表示節點;
  • LinkedList 類提供了插入的節點、刪除節點、顯示列表元素的方法,以及其餘輔助方法。

Node 類包含兩個屬性,elementnext

// Node 類
class Node {
  constructor(element) {
    this.element = element; // element 用來保存節點上的數據
    this.next = null; // next 用來保存指向下一節點的指針
  }
}

LinkedList 類提供了對鏈表進行操做的方法。鏈表只有一個屬性,那就是使用一個 Node 對象來保存該鏈表的頭節點。

function LinkedList() { 
  // head 節點的 next 屬性被初始化爲 null,當有新元素插入時,next 會指向新的元素
  this.head = new Node('head');
  this.find = find;
  this.insert = insert;
  this.findPrevious = findPrevious;
  this.remove = remove;
  this.display = display;
}

鏈表插入元素

同順序表同樣,向鏈表中增添元素,根據添加位置不一樣,可分爲如下 3 種狀況:

  • 插入到鏈表的頭部(頭節點以後),做爲首元節點;
  • 插入到鏈表中間的某個位置;
  • 插入到鏈表的最末端,做爲鏈表中最後一個數據元素;

雖然新元素的插入位置不固定,可是鏈表插入元素的思想是固定的,只需作如下兩步操做,便可將新元素插入到指定的位置:

  • 將新節點的 next 指針指向插入位置後的節點;
  • 將插入位置前節點的 next 指針指向插入節點;

例如,咱們在鏈表 [1, 2, 3, 4] 的基礎上分別實如今頭部、中間部位、尾部插入新元素 5,其實現過程以下圖所示:

鏈表插入節點

注意:鏈表插入元素的操做必須是先步驟 1,再步驟 2;反之,若先執行步驟 2,除非再添加一個指針,做爲插入位置後續鏈表的頭指針,不然會致使插入位置後的這部分鏈表丟失,沒法再實現步驟 1

建立一個輔助方法 find(),該方法遍歷鏈表,查找給定數據。

find() 方法實現:

function find(item) {
  var currNode = this.head; // 建立一個新節點
  while (currNode.element != item) { // 若是當前節點數據不符合咱們要找的節點
      currNode = currNode.next; // 當前節點移動到一下節點
  }
  return currNode;
}

一旦找到 給定 的節點,就能夠將新節點插入鏈表了。首先,將新節點的 next 指針指向 給定 節點 next 指向的節點。而後設置 給定 節點的 next 屬性指向新節點。

insert() 方法的定義以下:

function insert(newElement, item) {
  var newNode = new Node(newElement);
  var current = this.find(item);
  // 向鏈表插入節點 newNode
  newNode.next = current.next; // 將新節點的 next 指針指向插入位置後的節點 (步驟 1)
  current.next = newNode; // 設置插入位置前的 next 指針指向新節點(步驟2)
}

如今已經能夠開始測試咱們的鏈表實現了。然而在測試以前,先來定義一個 display() 方法,該方法用來顯示鏈表中的元素:

function display() {
  var currNode = this.head;
  while(!(currNode.next == null)) { // 噹噹前節點的 next 指針爲 null 時 循環結束
    //爲了避免顯示頭節點,程序只訪問當前節點的下一個節點中保存的數據
    console.log(chrrNode.next.element);
    currNode = currNode.next;
  }
}

測試程序:

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

function LinkedList() {
  this.head = new Node('head');
  this.find = find;
  this.insert = insert;
  this.findPrevious = findPrevious;
  this.remove = remove;
  this.display = display;
}

function find(item) {
  var currNode= this.head;
  while(currNode.element != item) {
    currNode = currNode.next;
  }
  return currNode;
}

function insert(newElement, item) {
  var newNode = new Node(newElement);
  var current = this.find(item);
  newNode.next = current.next;
  current.next = newNode;
}
function findPrevious() {}
function remove() {}
function display() {
  var currNode = this.head;
  while(!(currNode.next == null)) {
    console.log(currNode.next.element)
    currNode = currNode.next;
  }
}
var list = new LinkedList();

list.insert(1, 'head')
list.insert(2, 1)
list.insert(3, 2)
list.insert(4, 3)
list.display()

程序運行結果:

1
2
3
4

鏈表刪除元素

從鏈表中刪除指定數據元素時,須要進行如下 2 步操做:

  • 須要先找到待刪除節點前面的節點 直接前驅節點
  • 找到這個節點後,修改它的 next 指針,使其再也不指向待刪除節點;

刪除節點

從鏈表中刪除節點時,須要先找到待刪除節點的直接前驅節點。找到這個節點後,修改它的 next 指針,咱們能夠定義一個方法 findPrevious(),來作這件事。

findPrevious() 方法遍歷鏈表中的元素,檢查每個節點的 直接後繼節點 中是否存儲着待刪除數據。若是找到,返回該節點(待刪除節點的直接前驅節點)。

function findPrevious(item) {
  var currNode = this.head;
  while(!(currNode.next == null) && currNode.next.element != item) {
    currNode = currNode.next;
  }
  return currNode;
}

刪除節點的原理很簡單,找到該節點的直接前驅節點 prevNode 後,修改它的 next 指針:

prevNode.next = prevNode.next.next;

remove() 方法的 js 實現:

function remove(item) {
  var prevNode = this.findPrevious(item);
  if(!(prevNode.next == null)) {
    prevNode.next = prevNode.next.next;
  }
}

測試程序:

...
var list = new LinkedList();
list.insert(1, 'head')
list.insert(2, 1)
list.insert(3, 2)
list.insert(4, 3)
list.display() 
list.remove(2)
list.display()

程序運行結果:

// 沒調用 list.remove(2) 以前
1
2
3
4
// 調用list.remove(2)
1
3
4

鏈表更改節點

更新鏈表中的元素,只需經過遍歷找到存儲此元素的節點,對節點中的數據域作更改操做便可。

function change(index, newElement) {
  var currNode = this.head;
  for(var i = 0; i <= index; i++) {
    currNode = currNode.next;
    if (currNode == null) {
      throw new Error('更新位置無效');
      return;
    }
  }
  currNode.element = newElement;
  return currNode;
}

最後給出 js 實現的擁有基本操做 增刪改查 的鏈表完整代碼:

class Node {
  constructor(element) {
    this.element = element;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = new Node('head');
  }
  // 鏈表查找元素
  find(item) { 
    var currNode= this.head;
    while(currNode.element != item) {
      currNode = currNode.next;
    }
    return currNode;
  }
  // 鏈表添加元素
  insert(newElement, item) { 
    var newNode = new Node(newElement);
    var current = this.find(item);
    newNode.next = current.next;
    current.next = newNode;
  }
  // 鏈表查找直接前驅節點(輔助方法)
  findPrevious(item) {
    var currNode = this.head;
    while(!(currNode.next == null) && currNode.next.element != item) {
      currNode = currNode.next;
    }
    return currNode;
  }
  // 鏈表刪除節點
  remove(item) {
    var prevNode = this.findPrevious(item);
    if(!(prevNode.next == null)) {
      prevNode.next = prevNode.next.next;
    }
  }
  // 鏈表更改節點
  change(index, newElement) {
    var currNode = this.head;
    for(var i = 0; i <= index; i++) {
      currNode = currNode.next;
      if (currNode == null) {
        throw new Error('更新位置無效');
        return;
      }
    }
    currNode.element = newElement;
    return currNode;
  }
  display() {
    var currNode = this.head;
    while(!(currNode.next == null)) {
      console.log(currNode.next.element)
      currNode = currNode.next;
    }
  }
}

var list = new LinkedList();
list.insert(1, 'head')
list.insert(2, 1)
list.insert(3, 2)
list.insert(4, 3)
console.log('初始化鏈表:')
list.display() // 1, 2, 3, 4
list.remove(2)
console.log('刪除數據爲 2 的節點: ')
list.display() // 1, 3, 4
list.change(2, 6) 
console.log('將下標爲 2 的 節點數據更改成 6:')
list.display() // 1, 3, 6

總體程序運行結果:

初始化鏈表:
1
2
3
4
刪除數據爲 2 的節點: 
1
3
4
將下標爲 2 的 節點數據更改成 6:
1
3
6
相關文章
相關標籤/搜索