首頁 >web前端 >js教程 >深入了解JS中的資料結構之鍊錶(Linked-list)

深入了解JS中的資料結構之鍊錶(Linked-list)

青灯夜游
青灯夜游轉載
2021-02-12 09:04:572981瀏覽

深入了解JS中的資料結構之鍊錶(Linked-list)

相關推薦:《javascript影片教學

#對於JS 初學者,理解鍊錶可能是一項比較困難的任務,因為JS沒有提供內建的鍊錶。在像 JS 這樣的高階語言中,我們需要從頭開始實現此資料結構,如果你不熟悉此資料結構的工作方式,則實現部分會變得更加困難 ?。

在本文中,我們將討論如何將鍊錶儲存在資料庫中,實現鍊錶的新增和刪除,尋找以及反轉鍊錶等操作。在實現鍊錶之前,需要知道相比數組和對象,鍊錶的優點是什麼。

我們知道,陣列中的元素以索引編號和順序儲存在資料庫中:

深入了解JS中的資料結構之鍊錶(Linked-list)

#在使用陣列時,在開始或特定索引處新增/刪除元素這樣的操作可能是一項效能較低的任務,因為我們必須移動所有其他元素的索引,造成這種原因是由數組的編號索引特性導致的。

使用物件可以解決上述問題。由於在物件中,元素儲存位置是隨機的,因此,在執行諸如在開始處或特定索引處添加/刪除元素之類的操作時,無需移動元素的索引:

深入了解JS中的資料結構之鍊錶(Linked-list)

儘管在物件中新增和刪除元素速度很快,但是從上圖可以看出,在進行迭代操作時,物件並不是最佳選擇,因為物件的元素儲存在隨機位置。因此,迭代操作可能需要很長時間。這是鍊錶引出的原因。

那麼什麼是鍊錶呢 ?

從名字本身可以看出它是一個以某種方式鍊錶。那麼它是如何連結的,清單包含什麼呢?

鍊錶由具有兩個屬性的節點組成:資料和指標

節點內的指標指向清單中的下一個節點。鍊錶中的第一個節點稱為head。為了更好地理解,讓我們來看看描述鍊錶圖示:

深入了解JS中的資料結構之鍊錶(Linked-list)

#從上圖可以看出,每個節點都有兩個屬性,datapointer。指標指向列表中的下一個節點,最後一個節點的指標指向null,上圖是一個單鍊錶 ?。

鍊錶和物件時有很大的不同。在鍊錶中,每個節點都透過指標(pointer)連接到下一個節點。因此,我們在鍊錶的每個節點之間都有連接,而在物件中,鍵值對是隨機儲存的,彼此之間沒有連接。

接著,我們實作一個儲存整數的鍊錶。由於JS 不提供內建的鍊錶支持,因此我們將使用物件和類別來實現鍊錶?

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

class LinkedList {
  constructor () {
    this.head = null
    this.tail = this.head
    this.length = 0
  }
  append (value) {
  }

  prepend (value) {

  }

  insert (value, index) {

  }

  lookup (index) {

  }

  remove (index) {

  }

  reverse () {
    
  }
}

在上面的程式碼中,我們創建了兩個類,一個用於來鍊錶本身,一個是節點本身。如我們所討論的,每個節點將具有兩個屬性,一個和一個指標(對應 next 欄位)。

LinkedList類別包含三個屬性,head(初始值為null),用於儲存鍊錶的最後一個節點的tail(也指向null)和用來保存鍊錶長度的length屬性。接著,我們來實現裡面的方法 ?。

append (依序新增值)

這個函數將一個節點加入到鍊錶的末端。為了實現這個函數,我們需要理解它需要執行的一些操作:

深入了解JS中的資料結構之鍊錶(Linked-list)

#從上圖中,我們可以透過以下方式實作 append函數:

  append (value) {
    const newNode = new Node(value)
    if (!this.head) {
      this.head = newNode
      this.tail = newNode
    } else {
      this.tail.next = newNode
      this.tail = newNode
    }
    this.length++
  }

簡單的對append 方法解釋一下?:

const linkedList1 = new LinkedList()
linkedList1.append(2)

檢查head是否指向null,此時的head指向null,因此我們建立一個新對象,並將新物件指派給headtail# :

let node = new Node(2)
this.head = newNode
this.tail = newNode

現在,headtail 都指向同一個對象,這一點很重要,要記住。

接著,我們再向鍊錶加兩個值:

linkedList1.append(3)
linkedList1.append(4)

現在,head 不指向null,所以我們進入append 函數的else分支:

this.tail.next = node

由于headtail 都指向同一个对象,tail的变化都会导致head对象的变化,这是JS 中对象的工作方式。在JavaScript中,对象是通过引用传递的,因此 headtail都指向存储对象的相同地址空间。上面这行代码相当于

this.head.next = node;

下一行:

this.tail = node

现在,在执行完上面的代码行之后,this.head.nextthis.tail指向同一对象,因此,每当我们添加新节点时,head对象都会自动更新。

执行三次append之后,linkedList1 的结构应该是这样的:

head: {value: 2 , next: {value: 3, next: {value: 4,next: null}}}
tail : {value: 4, next: null}
length:3

从上面的代码中我们可以看到,链表的append函数的复杂度是O(1),因为我们既不需要移动索引,也不需要遍历链表。

我们来看下一个函数 ?

prepend (将值添加到链表的开头)

为了实现此函数,我们使用Node类创建一个新节点,并将该新节点的下一个对象指向链表的head 。 接下来,我们将新节点分配给链表的head

与append函数一样,这个函数的复杂度也是O(1)。

  prepend (value) {
  const node = new Node(value)

  node.next = this.head
  this.head = node
  this.length++
}

就像append函数一样,此函数的复杂度也为O(1)

insert (在特定索引处添加值)

在实现此函数之前,我们先看看它的一个转化过程。因此,出于理解目的,我们先创建一个值很少的链表,然后可视化insert函数。 insert  函数接受两个参数,值和索引:

let linkedList2 = new LinkedList()
linkedList2.append(23)
linkedList2.append(89)
linkedList2.append(12)
linkedList2.append(3)
linkedList2.insert(45,2)

第1步:

遍历链表,直到到达index-1位置:

深入了解JS中的資料結構之鍊錶(Linked-list)

第2步:

将索引为1的节点的指针(在本例中为89)分配给新节点(在本例中为45):

深入了解JS中的資料結構之鍊錶(Linked-list)

第3步:

将新节点(45)的 next 指向给下一个节点(12)

深入了解JS中的資料結構之鍊錶(Linked-list)

这就是执行插入操作的方式。 通过以上可视化,我们观察到需要在index-1位置和index位置找到节点,以便可以在它们之间插入新节点。 在代码中实现:

insert (value, index) {
  if (index >= this.length) {
  this.append(value)
}

  const node = new Node(value)

  const { prevNode, nextNode } = thisg.getPrevNextNodes(index)
  prevNode.next = node
  node.next = nextNode

  this.length++
}

简单分析一下上面的函数:

如果index的值大于或等于length属性,则将操作移交给append函数。 对于 else 分支,我们使用 Node 类创建一个新节点,接下来观察一个新函数getPrevNextNodes() ,通过该函数我们可以接收prevNodenextNode的值。 getPrevNextNodes函数的实现如下:

 getPrevNextNodes(index){
    let count = 0;
    let prevNode = this.head;
    let nextNode = prevNode.next;

    while(count < index - 1){
      prevNode = prevNode.next;
      nextNode = prevNode.next;
      count++;
    }

    return {
      prevNode,
      nextNode
    }
  }

通过遍历链表返回在index-1位置和index位置的节点,并将prevNodenext属性指向新节点,并将新节点的next属性指向nextNode

链表的插入操作的复杂度为 O(n),因为我们必须遍历链表并在index-1index 位置搜索节点。 尽管复杂度为O(n),但我们发现此插入操作比对数组的插入操作快得多,在数组中,我们必须将所有元素的索引移到特定索引之后,但是在链接中,我们仅操纵 index-1index 位置的节点的下一个属性。

remove (删除特定索引处的元素)

实现了插入操作之后,删除操作就比较容易理解,因为它几乎与插入操作相同,当我们从getPrevNextNodes函数获取prevNodenextNode值时,我们必须在remove中执行以下操作:

remove(index){
  let {previousNode,currentNode} = this.getNodes(index)
  previousNode.next = currentNode.next
  this.length--
}

删除操作的复杂度也为 O(n),类似于插入操作,链表中的删除操作比数组中的删除操作要快。

reverse (反转链表)

虽然看起来很简单,但反转链表常常是实现起来最令人困惑的操作,因此,在面试中会经常询问这个操作。在实现这个函数之前,让我们先把反转链表的策略可视化一下。

为了反转链表,我们需要跟踪三个节点,previousNodecurrentNodenextNode

考虑下面的链表:

let linkedList2 = new LinkedList()
linkedList2.append(67)
linkedList2.append(32)
linkedList2.append(44)

第一步:

开始,previousNode的值为null,而currentNode的值为head

深入了解JS中的資料結構之鍊錶(Linked-list)

第二步:

接下来,我们将nextNode分配给currentNode.next

深入了解JS中的資料結構之鍊錶(Linked-list)

第三步:

接下来,我们将currentNode.next属性指向previousNode

深入了解JS中的資料結構之鍊錶(Linked-list)

第三步:

现在,我们将previousNode移至currentNode,将currentNode移至nextNode

1深入了解JS中的資料結構之鍊錶(Linked-list)

这个过程从步骤2重复操作,一直到currentNode 等于 null

reverse (){
  let previousNode = null
  let currentNode = this.head

  while(currentNode !== null) {
    let nextNode = currentNode.next
    currentNode.next = previousNode
    previousNode = currentNode
    currentNode = nextNode
  }

  this.head = previousNode
}

就像我们看到的一样,直到currentNode === null,我们一直在遍历和移动这些值。 最后,我们将previousNode值分配给head

反向运算的复杂度为O(n)

查找 (查找特定索引的值)

这个操作很简单,我们只是遍历链表并返回特定索引处的节点。这个操作的复杂度也是O(n)

lookup(index){
    let counter = 0;
    let currentNode = this.head;
    while(counter < index){
      currentNode = currentNode.next;
      counter++;
    }
    return currentNode;
  }

好了,我们已经完成了用javascript实现单个链表的基本操作。单链表和双链表的区别在于,双链表的节点具有指向前一个节点和下一个节点的指针。

总结

链表为我们提供了快速的append(末尾添加元素)和prepend(开头添加元素)操作。 尽管链表中的插入操作的复杂度为O(n),但比数组的插入操作要快得多。 使用数组时我们面临的另一个问题是大小复杂性,当使用动态数组时,在添加元素时,我们必须将整个数组复制到另一个地址空间,然后添加元素,而在链表中,我们不需要 面对这样的问题。

在使用对象时,我们面临的问题是元素在内存中的随机位置,而在链表中,节点是通过指针相互连接的,指针提供了一定的顺序。

原文地址:https://blog.soshace.com/understanding-data-structures-in-javascript-linked-lists/

作者:Vivek Bisht 

译文地址:https://segmentfault.com/a/1190000024565634

更多编程相关知识,请访问:编程教学!!

以上是深入了解JS中的資料結構之鍊錶(Linked-list)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除