JavaScript数据结构——链表

前言

嘿,掘友们!今天我们来了解并实现数据结构 —— 链表。

本文主要内容

  • 单向链表
  • 双向链表
  • 循环链表

链表

要存储多个元素,数组可能是最常用的数据结构。但是这种数据结构有一个缺点,数组的大小是固定的,从数组的起点或中间插入或移除的成本很高,因为需要移动元素。

链表存储有序的元素集合,不同于数组的是,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针或链接)组成。下图是一个链表的结构。

JavaScript数据结构——链表_第1张图片

相比数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针。在数组中,我们可以直接访问任何位置的任何元素,而想要访问链表中间的一个元素,则需要从起点开始迭代链表知道找到所需要的元素。

在实现链表之前,我们先声明和定义属性和方法。

  • 属性
    • count 存储链表中的元素数量
    • head 头指针
    • equalsFn 比较元素是否相等
  • 方法
    • push(element) 向链表尾部添加一个新元素
    • insert(element, index) 向链表的特定位置插入一个新元素
    • getElementAt(index) 返回链表中的特定位置的元素,如果不存在返回undefined
    • remove(element) 从链表中移除一个元素
    • removeAt(index) 从链表的特定位置移除一个元素
    • indexOf(index) 返回元素在链表中的索引,如果链表中没有该元素,返回-1
    • isEmpty() 如果链表中不包含任何元素,返回true,否则返回false
    • size() 返回链表包含的元素个数
    • print() 返回表示整个链表的字符串

定义defaultEquals函数,作为默认的相等性比较函数。

function defaultEquals(a, b) {
  return a === b
} 

单向链表

要表示链表中的元素,我们需要一个助手类,叫做 Node。Node类表示我们想要添加到链表中的项。

class Node {
  constructor(element) {
    this.element = element	// 元素的值
    this.next = undefined	// 下一个元素的指针
  }
} 

创建 LinkedList 类的“骨架”

class LinkedList {
  constructor() {
    this.count = 0
    this.head = undefined
  }
} 

1. 向链表尾部添加元素

在尾部添加元素可能有两种场景:链表为空,添加的是第一个元素;链表不为空,向其尾部添加元素

class LinkedList {
  constructor() { ... }
  push(element) {
    const node = new Node(element)	// 创建Node项
    if(this.head == null) {
      this.head = node
    } else {
      let current = this.head		// 指向链表的current变量
      while (current.next != null) {
        current = current.next
      }
      current.next = node
    }
    this.count++
  }
} 

首先,把 element作为值传入,创建Node项

先实现第一个场景,向空链表添加一个元素,当创建一个 LinkedList 对象时,head会指向 undefined

如果 head 元素为 undefinednull,就意味着在向链表添加第一个元素。因此要做的就是让 head 指向 node 元素。

再来看第二个场景,向一个不为空的链表尾部添加元素。

要在链表的尾部添加元素,就要找到最后一个元素。但我们只有第一个元素的引用,需要循环访问链表,直到最后一项。当 current.next 元素为 undefinednull 时,我们就知道到达链表尾部了。然后让当前元素的 next 指向 node 元素。

this.head == null 相当于 this.head === undefined || this.head === null

current.next !=null 相当于 current.next !== undefined || current.next !== null

2. 循环迭代链表找到目标

循环到目标 index 的代码片段在 LinkedList 类的方法中很常见。将这部分逻辑独立为单独的方法,这样就能在不同的地方复用它。

class LinkedList {
  constructor() { ... }
  push(element) { ... }
  getElementAt(index) {
    if(index >= 0 && index < this.count) {
      let node = this.head
      for(let i = 0; i < index && node != null; i++) {
        node = node.next
      }
      return node
    }
    return undefined
  }
} 

为了确保我们能迭代链表知道找到一个合法的位置,需要对传入的 index 参数进行合法性验证。如果传入的位置不合法,返回 undefined,因为这个位置在链表中不存在。

然后,初始化 node 变量,该变量会从链表的第一个元素 head 开始,迭代整个链表知道目标 index,结束循环时,node 元素将是 index 位置元素的引用。

3. 从链表中移除元素

我们要实现两种移除元素的方法。第一种是从特定位置移除一个元素(removeAt),第二种是根据元素值移除元素(remove)。

我们先实现第一种移除元素的方法,要移除元素存在两种场景:第一种,移除第一个元素,第二种,移除第一个元素以外的元素。

removeAt(index) {
  if(index >= 0 && index < this.count) {
    if(index === 0) {
      this.head = this.head.next
    } else {
      const previous = this.getElement(index - 1)
      const current = previous.next
      previous.next = current.next
    }
    this.count-- 
    return current.element
  }
  return undefined
} 

先看第一种场景:我们从链表中移除第一个元素。想移除第一个元素,让 head 指向链表的第二个元素就实现了。

再看第二种场景:移除除第一个元素以外的元素。我们获取要删除元素的前一个元素。current 引用要删除的元素。将前一个元素的 next 指向要删除元素的 next,就可以实现了。

移除最后一个元素也通用,previous 引用最后元素的前一个元素,最后一个元素的 next 指向 undefined,那么将 previous.next = undefined,就完成了最后一个元素的移除。

我们再来实现移除元素的第二种方法:根据元素值移除元素(remove)。

remove(element) {
  const index = this.getElement(element)
  return this.removeAt(index)
} 

我们复用前面的两种方法 getElementremoveAt ,就可以实现。

先获取要删除元素的索引࿰

你可能感兴趣的:(链表,数据结构,javascript)