「这是我参与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.next 为 null,不满足 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。
- 新增结点位置在除去首结点的其余地方
比如想让新增结点作为链表的第 4 个结点插入,那么 position 就为 3。我们新定义一个变量 current,让它一开始指向 head,然后进行 for 循环,让 current 等于 current.next,直至 position 前的一个结点处停止遍历。图示如下:
此时,
current 指向的是position - 1 的那个结点,那么它的 next 指向的结点就是原本 position 处的结点,也就是新结点的 next 应该指向的结点。之后, position - 1 结点的 next 就应该指向新结点。如下图:
最后不要忘记 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 的,都需要做一下判断。
现在,先来做个测试,验证下 toString 和 insert 方法:
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 的不同分为两种情况:
position为0,也就是删掉第 1 个结点,那么直接让head指向head.next即可。原本的第 1 个结点此时虽然还有条引用指向原本的第 2 个结点,但是其本身没有被引用了,所以会被浏览器 GC 机制进行垃圾回收;position为除去第 1 个结点之外的结点,那么就先找到该结点,再让前一个指向后一个,这里面需要新定义一个变量pre来实现。示意图如下:
代码如下:
// 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)
}