JS 数据结构 —— 单向链表(下篇)✍

264 阅读6分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

本文接上文《单向链表(上篇)》 继续手写 LinkList 函数来实现简单的单向链表结构。有不足之处或是任何意见建议,欢迎各位大佬不吝斧正~

构造函数 LinkList 整体来看可以分为一个内置的构造函数 Node、属性和方法 3 部分,其中前两者在上篇文章的分析中已经介绍了,不再赘述,下面主要是对链表的增删改查的方法部分:

toString

toString() 方法主要是方便我们调试时打印查看链表中的结点数据。链表中每个结点都是一个构造函数 Node 的实例对象,而在 js 中,每个对象默认就有 toString() 方法,比如我们直接往链表里加入一个数据然后执行 toString()

linkList.insert(1)
console.log(linkList.toString())

默认的结果是 [object Object],其中第 2 个 Object 代表 linkList 的类型。所以我们需要自己重新定义此方法,让其返回每个结点的数据的值,覆盖默认定义。我们新定义一个变量 current,让它指向了链表的 head。而在链表有结点的情况下,head 指向的是第一个结点,于是 current 也就指向了第一个结点。那么当 current 有值时,进行 while 循环,将每个结点的 data 转为字符串拼接到 tempString 最终返回,直至最后一个结点,即 current.nextnull,不满足 while 循环条件,循环终止。

// toString 方法
LinkList.prototype.toString = function () {
  let current = this.head
  let tempString = ''
  while (current) {
    tempString += current.item + ' '
    current = current.next
  }
  return tempString
}

insert(增)

insert 方法用于新增结点,接收两个参数:data,要存储的数据,必传项;position 位置,从 0 开始,如果不传值则默认为当前链表的长度 length,即往最后添加 。可以依据新增结点的位置分为两种情况考虑:

  • 新增结点位置在一开始

图示如下,此时只需要将新增结点指向原本 head 指向的结点即可,当然也可能原本一个结点也没有,那么 head 指向的就是 null。然后让 head 指向新增结点 new。

yuque_diagram (2).jpg

  • 新增结点位置在除去首结点的其余地方

比如想让新增结点作为链表的第 4 个结点插入,那么 position 就为 3。我们新定义一个变量 current,让它一开始指向 head,然后进行 for 循环,让 current 等于 current.next,直至 position 前的一个结点处停止遍历。图示如下:
yuque_diagram (3).jpg 此时, current 指向的是position - 1 的那个结点,那么它的 next 指向的结点就是原本 position 处的结点,也就是新结点的 next 应该指向的结点。之后, position - 1 结点的 next 就应该指向新结点。如下图:

2024-03-17_152234.png

最后不要忘记 length 需要加 1,代码如下:

// insert, 添加结点(增)
LinkList.prototype.insert = function (data, position = this.length) {
  // 对 position 进行边界判断
  if (position < 0 || position > this.length)
    throw Error('请传入正确的 position')
  // 创建新结点
  const node = new Node(data)
  // 判断添加的位置是否是第一个
  if (position === 0) {
    node.next = this.head
    this.head = node
  } else {
    let current = this.head
    for (let i = 0; i < position - 1; i++) {
      current = current.next
    }
    /**
     * 循环结束,此时 current 指向的是 position 的前一个结点
     * 可以把 current 就看成 position - 1 处的那个结点
     * 那么 current.next 指向的原本 position 处的结点,此时就是新结点的 next 应该指向的结点
     * 然后再让 current.next 指向新结点
     */
    node.next = current.next
    current.next = node
  }
  this.length++
}

注意:在方法的一开始,需要对传入的 position 做个边界判断,看看传入的值是否越界。之后的方法里,凡是需要传入 position 的,都需要做一下判断。 现在,先来做个测试,验证下 toStringinsert 方法:

const linkList = new LinkList()
try {
  linkList.insert(5)
  linkList.insert(3, 0)
  linkList.insert(2)
  linkList.insert(8)
  linkList.insert(10, 3)
  console.log(linkList.toString()) // 3 5 2 10 8
} catch (error) {
  console.log(error.message)
}

get(查)

get 方法用于查询链表某个 position 的值,思路其实和上面 insert 方法差不多,只是上面用的是 for 循环,这里用 while 循环:最开始让 current 指向 head,然后通过 next 属性一个个地改变 current 的指向,当 index 的值等于 position 时,循环结束,此时 current 指向的就是 position 处的结点。

// get,获取结点数据(查)
LinkList.prototype.get = function (position) {
  if (position < 0 || position > this.length - 1)
    throw Error('请传入正确的 position')
  let index = 0
  let current = this.head
  while (index++ < position) {
    current = current.next
  }
  return current.data
}

indexOf(查)

indexOf 方法用于查询传入的 data 在链表中的位置,如果链表中不包含 data,则返回 -1

// indexOf,获取结点位置(查)
LinkList.prototype.indexOf = function (data) {
  let current = this.head
  let index = 0
  while (current) {
    if (current.data === data) return index
    // 如果当前项的 data 不等于传入的 data
    current = current.next
    index++
  }
  // 如果 while 循环没有 return,说明链表中没有 data,返回 -1
  return -1
}

思路和之前的方法差不多,只是多了一个 index 变量用于记录当前的位置。

updata(改)

updata 用于更改某一 position 处结点的数据(data)。其实和前面的 get 方法差不多,只不过 get 是返回 position 处的 data。既然 get 方法用的是 while 循环,那么这里就用for 循环写一写:

// updata,更新结点(改)
LinkList.prototype.updata = function (newData, position) {
  if (position < 0 || position > this.length - 1)
    throw Error('请传入正确的 position')
  let current = this.head
  for (let i = 0; i < position; i++) {
    current = current.next
  }
  current.data = newData
}

removeAt(删)

removeAt 方法用于删除指定 position 处的结点。按 position 的不同分为两种情况:

  1. position0,也就是删掉第 1 个结点,那么直接让 head 指向 head.next 即可。原本的第 1 个结点此时虽然还有条引用指向原本的第 2 个结点,但是其本身没有被引用了,所以会被浏览器 GC 机制进行垃圾回收;
  2. position 为除去第 1 个结点之外的结点,那么就先找到该结点,再让前一个指向后一个,这里面需要新定义一个变量 pre 来实现。示意图如下:

yuque_diagram (4).jpg 代码如下:

// removeAt,删除结点(删)
LinkList.prototype.removeAt = function (position) {
  if (position < 0 || position > this.length - 1)
    throw Error('请传入正确的 position')
  // current 定义在 if else 外面是为了方便最终返回删除的数据
  let current = this.head
  if (position === 0) {
    this.head = this.head.next
  } else {
    let pre = null // 用于记录被删除结点的前一个结点
    let index = 0
    while (index++ < position) {
      pre = current
      current = current.next
    }
    // 找到 position 处的结点后,让前一个结点指向后一个结点
    pre.next = current.next
  }
  this.length--
  return current.data
}

注意最后不要忘了让 length 减 1。

remove(删)

remove 方法传入一个 data,从链表中删除存储该 data 的结点。有了前面的准备,我们只需要先通过 indexOf 找到该结点的位置,然后通过 removeAt 删除这个位置的结点即可:

// remove,删除结点(删)
LinkList.prototype.remove = function (data) {
  // 找到该结点位置
  const position = this.indexOf(data)
  // 通过位置删除结点
  this.removeAt(position)
}

感谢.gif 点赞.png