链表结构对于前端来说可能比较陌生,毕竟平时用JS写代码的时候很少会用到,使用最多的数据结构应该是数组。JS中的数组主要的问题是被实现成了对象,和其他语言的数组相比效率很低。链表除了不能随机访问之外,其他的使用与一维数组类似。
链表是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个结点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域。
常见的链表结构有单链表、双向链表、循环链表等。这边文章主要是以单链表为例,介绍一下JS中链表的实现方式。
链表结构
链表实际上就是一个一个节点Node连接形成一条链的结构,每个节点中存储着当前的值和下一个节点的地址,如下图所示。
要实现链表数据结构,关键在于保存head元素(链表的头元素)以及每一个元素的next指针。我们从链表的头部,依次根据每个节点的next指针往下遍历,就可以得到整条链表了。
具体的实现方式:
- 首先可以构建一个Node类,用来描述链表中的节点。这个类有两个属性,一个用来保存节点的值,一个用来保存指向下一个节点的指针。
let Node = function (element) {
this.element = element;
this.next = null;
};
- 接着构建一下链表的基本骨架,其实就是一个链表类和相关的操作函数。
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
//在链表中查找给定节点的索引
indexOf (element) {}
//返回链表中索引所对应的节点
find (position) {}
//向链表中添加节点
append (element) {}
//在链表的指定位置插入节点
insert (position, element) {}
//删除链表中指定位置的节点,并返回这个节点的值
removeAt (position) {}
//删除链表中对应的节点
remove (element) {}
//判断链表是否为空
isEmpty () {}
//返回链表的长度
size () {}
//返回链表的头节点
getHead () {}
//清空链表
clear () {}
//辅助方法,遍历整个链表,按指定格式输出链表中的所有节点,方便测试验证结果
toString () {}
}
然后,再来实现一些相关的链表操作方法。
查找链表元素
实现indexOf方法,该方法返回给定元素在链表中的索引位置。
indexOf (element) {
//从链表的头部开始遍历,直到找到和给定元素相同的元素,然后返回对应的索引号。如果没有找到对应的元素,则返回-1。
let current = this.head;
for (let i = 0; i < this.length; i++) {
if (current.element === element) return i;
current = current.next;
}
return -1;
}
实现find方法,用于查找链表中指定位置的节点。
find(position) {
//首先判断参数position的边界值,如果值超出了索引的范围(小于0或者大于length - 1),则返回null
if (position < 0 || position >= this.length) return null;
//从链表的head开始,遍历整个链表直到找到对应索引位置的节点,然后返回这个节点。
let current = this.head;
for (let i = 0; i < position; i++) {
current = current.next;
}
return current;
}
新增节点
有了find方法,就可以很方便地实现append方法,用来在链表的尾部添加新节点。
append (element) {
let node = new Node(element);
// 如果当前链表为空,则将head指向node
if (this.head === null) {
this.head = node;
}else {
// 否则,找到链表尾部的元素,然后添加新元素
let current = this.find(this.length - 1);
current.next = node;
}
//将节点挂到链表上之后,需要将链表的长度加1,我们需要通过length属性来记录链表的长度
this.length++;
}
插入节点
insert方法比append更灵活一些,可以在链表的任意位置添加节点。
- 首先需要判断位置不能超出边界,即索引不能小于零,也不能大于链表的长度,否则返回
false - 接下来需要判断索引是否为0,若索引为0,则表示添加在头部,将新节点的next指针指向现在的head,然后更新head的值为新插入的节点。具体如下图所示:
- 如果要新加节点在链表的中间或者尾部,假设链表长度为3,要在位置2插入新节点,我们首先找到位置2的前一个节点previous node,将新节点new node的next指针指向previous node的next所对应的节点,然后再将previous node的next指针指向new node,这样就把新节点挂到链表中了。考虑一下,当插入的节点在链表的尾部,这种情况也是适用的。对应的操作如下图。
具体代码如下:
insert (position, element) {
// position不能超出边界值
if (position < 0 || position > this.length) return false;
let node = new Node(element);
if (position === 0) {
node.next = this.head;
this.head = node;
}else {
let previous = this.find(position - 1);
node.next = previous.next;
previous.next = node;
}
//将节点挂到链表上之后,需要将链表的长度加1
this.length++;
return true;
}
删除节点
与insert相似,删除操作removeAt,也需要判断索引边界和具体添加的位置。
如果要删除的节点为链表的头部,只需要将head移到下一个节点即可。如果当前链表只有一个节点,那么下一个节点为null,此时将head指向下一个节点等同于将head设置成null,删除之后链表为空。如果要删除的节点在链表的中间部分,需要找出position所在位置的前一个节点,将它的next指针指向position所在位置的下一个节点。
具体如下图所示:
具体代码如下:
removeAt (position) {
// position不能超出边界值
if (position < 0 || position >= this.length) return null;
let current = this.head;
if (position === 0) {
this.head = current.next;
}else {
let previous = this.find(position - 1);
current = previous.next;
previous.next = current.next;
}
//删除之后将链表长度减1
this.length--;
return current.element;
}
完整代码
比较关键的几个方法已经介绍过了,还有一些比较简单的方法就不一一讲解了,直接放上完整代码吧。
let Node = function (element) {
this.element = element;
this.next = null;
};
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
//在链表中查找给定节点的索引
indexOf (element) {
let current = this.head;
for (let i = 0; i < this.length; i++) {
if (current.element === element) return i;
current = current.next;
}
return -1;
}
//返回链表中索引所对应的节点
find (position) {
if (position < 0 || position >= this.length) return null;
let current = this.head;
for (let i = 0; i < position; i++) {
current = current.next;
}
return current;
}
//向链表中添加节点
append (element) {
let node = new Node(element);
if (this.head === null) this.head = node;
else {
let current = this.find(this.length - 1);
current.next = node;
}
this.length++;
}
//在链表的指定位置插入节点
insert (position, element) {
if (position < 0 || position > this.length) return false;
let node = new Node(element);
if (position === 0) {
node.next = this.head;
this.head = node;
}
else {
let previous = this.find(position - 1);
node.next = previous.next;
previous.next = node;
}
this.length++;
return true;
}
//删除链表中指定位置的节点,并返回这个节点的值
removeAt (position) {
if (position < 0 || position >= this.length) return null;
let current = this.head;
if (position === 0) this.head = current.next;
else {
let previous = this.find(position - 1);
current = previous.next;
previous.next = current.next;
}
this.length--;
return current.element;
}
//删除链表中对应的节点
remove (element) {
let index = this.indexOf(element);
return this.removeAt(index);
}
//判断链表是否为空
isEmpty () {
// return this.head === null;
return this.length === 0;
}
//返回链表的长度
size () {
return this.length;
}
//返回链表的头节点
getHead () {
return this.head;
}
//清空链表
clear () {
this.head = null;
this.length = 0;
}
//辅助方法,遍历整个链表,按指定格式输出链表中的所有节点,方便测试验证结果
toString () {
let current = this.head;
let s = '';
while (current) {
let next = current.next;
next = next ? next.element : 'null';
s += `[element: ${current.element}, next: ${next}] `;
current = current.next;
}
return s;
}
}
//测试用例
let linkedList = new LinkedList();
linkedList.append('KDJ');
linkedList.append('BOLL');
linkedList.append('MACD');
console.log(linkedList.toString());
//[element: KDJ, next: BOLL] [element: BOLL, next: MACD] [element: MACD, next: null]
linkedList.insert(0, 'MA');
linkedList.insert(3, 'PE');
linkedList.insert(5, 'WR');
console.log(linkedList.toString());
//[element: MA, next: KDJ] [element: KDJ, next: BOLL] [element: BOLL, next: PE] [element: PE, next: MACD] [element: MACD, next: WR] [element: WR, next: null]
console.log(linkedList.find(1));
//Node {element: 'KDJ', next: Node}
console.log(linkedList.removeAt(0)); //MA
console.log(linkedList.removeAt(1)); //BOLL
console.log(linkedList.removeAt(3)); //WR
console.log(linkedList.toString()); //[element: KDJ, next: PE] [element: PE, next: MACD] [element: MACD, next: null]
console.log(linkedList.indexOf('BOLL'));//-1
linkedList.remove(20);
console.log(linkedList.toString()); //[element: KDJ, next: PE] [element: PE, next: MACD] [element: MACD, next: null]
linkedList.clear();
console.log(linkedList.size()); //0
总结
从上面的例子我们可以看出,链表有以下优点:
- 不需要初始化容量,可以任意加减元素
- 添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快
有以下缺点:
- 因为含有大量的指针域,占用空间较大
- 查找元素需要遍历链表来查找,非常耗时
文章主要用JavaScript对单链表做了简单介绍和实现。双向链表和循环列表的实现,可以参考《JavaScript数据结构——链表的实现与应用》。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞。