【前端數據結構基礎】鏈表

前言

數組是咱們很是熟悉且經常使用的一種數據結構。但咱們發現,數組不老是組織數據的最佳數據結構。由於在不少編程語言中,數組的長度是固定的,因此當數組已經被數據填滿時,再加入新的元素就會很是困難。同時,在數組中添加或刪除元素也很麻煩,由於須要將數組中的其餘元素向前或向後平移,以反映數組進行了添加或刪除的操做。
雖說在JavaScript中的數組不存在上述問題,咱們使用splice()方法不須要再訪問數組中的其餘元素。可是在JavaScript中,數組被實現成了對象,所以與其餘語言中的數組相比,效率很低。
在不少狀況下,當咱們發現數組在實際使用時很慢,就能夠考慮使用鏈表來代替它。除了對數據的隨機訪問,鏈表幾乎能夠用在任何可使用一維數組的狀況中。若是須要隨機訪問,數組仍然是最好的選擇。算法

1、什麼是鏈表

概念

鏈表是由一組節點組成的集合。每一個節點都使用一個對象的引用指向它的後繼。指向另外一個節點的引用叫作鏈。以下圖所示
鏈表圖示
數組元素依靠它們的位置進行引用,而鏈表元素依靠相互關係進行引用。如咱們能夠說Item2在Item1後面,而不能說Item2是鏈表中的第二個元素。
咱們所說的遍歷鏈表,就是跟着連接,從鏈表的首元素一直走到尾元素(不包括鏈表的頭節點)。
咱們能夠發現,鏈表的尾元素指向一個null節點。編程

有頭節點的鏈表

要標識出鏈表的起始節點有些麻煩,所以咱們常常會在鏈表最前面有一個特殊節點,叫作頭節點。數組

插入節點

鏈表中插入一個節點的效率很高,咱們只須要修改其前面的節點,使其指向新加入的節點,同時將新加入的節點指向原來前驅指向的節點便可。數據結構

刪除節點

鏈表中刪除一個節點也很是容易。將待刪元素的前驅節點指向待刪元素的後繼節點,再講待刪元素指向null便可。編程語言

2、構造基於對象的鏈表

咱們將用JavaScript構造一個基於對象的鏈表結構,各部分功能使用註釋說明。函數

/**
 * Node類 表示節點,咱們使用構造函數來建立節點
 * element 用來保存節點上的數據
 * next 用來保存指向下一個節點的連接
 * @param {*} element
 */
function Node (element) {
  this.element = element
  this.next = null
}

/**
 * LList類 提供對鏈表操做的方法
 * find 用於查找元素
 * insert 用於插入新節點
 * display 用於遍歷顯示鏈表結構
 * findPrev 用於遍歷查找待刪除數據的前一個節點
 * remove 用於刪除節點
 */
function LList () {
  this.head = new Node('head')
  this.find = find
  this.insert = insert
  this.display = display
  this.findPrev = findPrev
  this.remove = remove
}

/**
 * find() 方法用於經過遍歷鏈表,查找給定數據
 * 返回保存該數據的節點
 * @param {*} item
 */
function find (item) {
  // 初始化當前位置爲鏈表頭部
  let currNode = this.head
  // 循環遍歷尋找當前位置並返回
  while ((currNode != null) && (currNode.element != null) && (currNode.next != null)) {
    currNode = currNode.next
  }
  return currNode
}

/**
 * insert() 方法用於插入新節點
 * @param {*} newEle
 * @param {*} item
 */
function insert (newEle, item) {
  // 建立新節點
  let newNode = new Node(newEle)
  // 查找要插入的節點位置
  let current = this.find(item)
  // 將新節點的後繼指向要插入位置的後繼
  if (current != null) {
    newNode.next = current.next
    // 將要插入位置的後繼指向新節點
    current.next = newNode
  } else {
    // current 爲null時
    newNode.next = null
    this.head.next = newNode
  }
}

/**
 * findPrev() 方法用於遍歷查找待刪除數據的前一個節點
 * @param {*} item
 */
function findPrev (item) {
  // 初始化當前節點爲頭節點
  let currNode = this.head
  // 當前節點的後繼爲item時中止遍歷並返回,即返回待查找節點的前驅節點
  while (!(currNode.next == null) && (currNode.next.element != item)) {
    currNode = currNode.next
  }
  return currNode
}

/**
 * remove() 方法用於刪除一個節點
 * @param {*} item
 */
function remove (item) {
  // 找到item數據節點的前驅節點
  let prevNode = this.findPrev(item)
  if (!(prevNode.next == null)) {
    // 將前驅節點的後繼節點賦值爲其後繼節點的後繼節點,即跳過了待刪節點
    prevNode.next = prevNode.next.next
  }
}

/**
 * display() 方法用於遍歷鏈表
 */
function display () {
  // 初始化當前節點爲頭節點
  let currNode = this.head
  while (!(currNode.next == null)) {
    // 遍歷輸出節點,並指向下一節點
    console.log(currNode.next.element)
    currNode = currNode.next
  }
}
// 測試代碼
let students = new LList()
students.insert('Miyang', 'head')
students.insert('Tom', 'Miyang')
students.insert('Jerry', 'Tom')
students.remove('Tom')
students.display()

// 輸出結果
Miyang
Tom
Jerry

3、雙向鏈表

關於雙向鏈表的實現,咱們只須要在單向鏈表的基礎上,增長一個指向前驅節點的連接。
實現代碼以下:測試

/**
 * Node類 表示節點,咱們使用構造函數來建立節點
 * element 用來保存節點上的數據
 * next 用來保存指向下一個節點的連接
 * @param {*} element
 */
function Node (element) {
  this.element = element
  this.next = null
  this.previous = null
}

/**
 * LList類 提供對鏈表操做的方法
 * find 用於查找元素
 * insert 用於插入新節點
 * display 用於遍歷顯示鏈表結構
 * findPrev 用於遍歷查找待刪除數據的前一個節點
 * remove 用於刪除節點
 */
function LList () {
  this.head = new Node('head')
  this.find = find
  this.insert = insert
  this.display = display
  this.findPrev = findPrev
  this.remove = remove
  this.findLast = findLast
  this.dispReverse = dispReverse
}

/**
 * find() 方法用於經過遍歷鏈表,查找給定數據
 * 返回保存該數據的節點
 * @param {*} item
 */
function find (item) {
  // 初始化當前位置爲鏈表頭部
  let currNode = this.head
  // 循環遍歷尋找當前位置並返回
  while ((currNode != null) && (currNode.element != null) && (currNode.next != null)) {
    currNode = currNode.next
  }
  return currNode
}

/**
 * insert() 方法用於插入新節點
 * @param {*} newEle
 * @param {*} item
 */
function insert (newEle, item) {
  // 建立新節點
  let newNode = new Node(newEle)
  // 查找要插入的節點位置
  let current = this.find(item)
  // 將新節點的後繼指向要插入位置的後繼
  if (current != null) {
    newNode.next = current.next
    newNode.previous = current
    // 將要插入位置的後繼指向新節點
    current.next = newNode
  } else {
    // current 爲null時
    newNode.next = null
    newNode.previous = null
    this.head.next = newNode
  }
}

/**
 * findPrev() 方法用於遍歷查找待刪除數據的前一個節點
 * @param {*} item
 */
function findPrev (item) {
  // 初始化當前節點爲頭節點
  let currNode = this.head
  // 當前節點的後繼爲item時中止遍歷並返回,即返回待查找節點的前驅節點
  while (!(currNode.next == null) && (currNode.next.element != item)) {
    currNode = currNode.next
  }
  return currNode
}

/**
 * remove() 方法用於刪除一個節點
 * @param {*} item
 */
function remove (item) {
  // 找到item數據節點的前驅節點
  let currNode = this.find(item)
  if (!(currNode.next == null)) {
    currNode.previous.next = currNode.next
    currNode.next.previous = currNode.previous
    currNode.next = null
    currNode.previous = null
  }
}

/**
 * display() 方法用於遍歷鏈表
 */
function display () {
  // 初始化當前節點爲頭節點
  let currNode = this.head
  while (!(currNode.next == null)) {
    // 遍歷輸出節點,並指向下一節點
    console.log(currNode.next.element)
    currNode = currNode.next
  }
}

/**
 * findLast() 方法用於找到鏈表中最後一個節點
 */
function findLast () {
  let currNode = this.head
  while (!(currNode.next == null)) {
    currNode = currNode.next
  }
  return currNode
}

/**
 * dispReverse() 方法用於反向遍歷鏈表
 */
function dispReverse () {
  let currNode = this.head
  currNode = this.findLast()
  while (!(currNode.previous == null)) {
    console.log(currNode.element)
    currNode = currNode.previous
  }
}
// 測試代碼
let students = new LList()
students.insert('Miyang', 'head')
students.insert('Tom', 'Miyang')
students.insert('Jerry', 'Tom')
students.remove('Tom')
students.display()
console.log()
students.dispReverse()

// 輸出結果
Miyang
Tom
Jerry

Jerry
Tom
Miyang

4、循環鏈表

循環鏈表和單向鏈表類似,惟一的區別是,在建立循環鏈表時,讓其頭節點的next屬性指向它自己,即:this

head.next = head

修改LList類的構造函數:spa

function LList () {
  this.head = new Node('head')
  this.head.next = this.head
  this.find = find
  this.insert = insert
  this.display = display
  this.findPrev = findPrev
  this.remove = remove
  this.findLast = findLast
  this.dispReverse = dispReverse
}

同時,其餘地方也須要修改,如display()方法,不然會形成死循環code

function display () {
  // 初始化當前節點爲頭節點
  let currNode = this.head
  while (!(currNode.next == null) && !(currNode.next.element == 'head')) {
    // 遍歷輸出節點,並指向下一節點
    console.log(currNode.next.element)
    currNode = currNode.next
  }
}

一樣的,其餘方法也須要作相似修改,在此就不一一舉例了。

結束語

上面對JavaScript實現鏈表作了基本介紹,你們也能夠嘗試去定義一些其餘方法,如在鏈表中向前移動n個節點advance(n)、在雙向鏈表中向後移動n個節點back(n)等。

參考資料:數據結構與算法JavaScript描述 第6章 鏈表 因爲書上的源代碼出現了錯誤,所以代碼根據實際運行結果作了相應修改。
相關文章
相關標籤/搜索