链表这个词对很多前端开发者来说都是比较陌生的,多出现在c语言个java里面。但是链表并非这些语言独有的。他代表的是一种存储结构,在任何语言都有自己的实现方式,js也不例外。
链表与数组
很多时候链表看起来和数组有些相似,数组其实也是一种存储结构,但是它是一种线性表的顺序
存储结构,它的特点是用一组地址连续的存储单元依次存储数据元素
。这也是数据可以使用有序的下标获取元素的原因。但是却也正是如此,带来了一些缺陷。每次数次添加或者删除元素,都会对该元素位置后面的所有元素做出位置调整,可能会是一个O(n)的操作。而链表则不会有这种缺陷。链表不要求逻辑上相邻的元素在物理位置上也相邻
,它是一种物理存储单元上非连续、非顺序
的存储结构,但是也缺失了数组随机读取的优势。
链表有单向链表、双向链表和循环链表
单向链表(指向一个方向)
链表是由一组节点组成的集合。每个节点都使用一个对象的引用指向它的后继。指向另一
个节点的引用叫做链。
一般的链表都会额外添加一个头节点(作为辅助)和尾节点,例如下面这种样子
数组元素靠它们的位置进行引用,链表元素则是靠相互之间的关系进行引用。在上图中,我们说 bread 跟在 milk 后面,而不说 bread 是链表中的第二个元素。遍历链表,就是跟着链接,从链表的首元素一直走到尾元(但这不包含链表的头节点,头节点常常用来作为链表的接入点)。上图中另外一个值得注意的地方是,链表的尾元素指向一个 null 节点。
单向链表的特点:
- 用一组任意的内存空间去存储数据元素(这里的内存空间可以是连续的,也可以是不连续的)
- 每个节点(node)都由数据本身和一个指向后续节点的指针组成
- 整个链表的存取必须从头指针开始,头指针指向第一个节点
- 最后一个节点的指针指向空(NULL)
实现
// 创建节点的构造器(类)
class Node {
constructor(element) {
this.element = element;
this.next = null;
}
}
// 创建链表
class LinkedList {
constructor() {
// 链表一般默认有一个辅助头叫head,尾指向null
this.head = null;
// 存储链表长度
// 这里不同于数组的下标,仅仅代表存储长度,遍历长度可以拿到链表的各个元素
this.size = 0;
}
// 往链表里添加节点(添加到末尾)
append(element) {
// 创建一个节点
let node = new Node(element);
// 如果head指向null表示这个链表是空的,则将head指向新节点
if (this.head === null) {
this.head = node;
} else {
// 如果head指向不为空,则需要拿到最后的节点,将next指向新节点
let curNode = this.getNode(this.size - 1);
curNode.next = node;
}
this.size++;
}
// 在指定位置追加
appendAt(index, element) {
if (index < 0 || index > this.size) {
throw new Error("节点添加失败");
}
let node = new Node(element);
if (index === 0) {
this.head = node;
} else {
let preNode = this.getNode(index - 1);
node.next = preNode.next;
preNode.next = node;
}
this.size++;
}
// 删除指定节点
removeAt(index) {
if (index < 0 || index > this.size) {
throw new Error("节点删除失败");
}
if (index === 0) {
this.head = this.head.next;
} else {
let preNode = this.getNode(index - 1);
let curNode = preNode.next;
preNode.next = curNode.next;
}
this.size--;
}
// 拿到最后节点的方法。
getNode(index) {
// 节点下标不能小于0或者大于链表长度(该下标不同于数组下标,仅仅作为一个标识)
if ((index < 0) | (index >= this.size)) {
throw new Error("获取当前节点失败");
}
// 获取头部,因为需要遍历节点下标,下标可能为0,所以使用head当作第一个遍历出的值
let curNode = this.head;
for (let i = 0; i < index; i++) {
// 每次遍历将当前节点指向下一个节点
curNode = curNode.next;
}
return curNode;
}
indexOf(element) {
let curNode = this.head;
for (let i = 0; i < this.size; i++) {
// 每次遍历将当前节点指向下一个节点
if (curNode.element === element) {
return i;
}
curNode = curNode.next;
}
}
}
测试通过
let l1 = new LinkedList();
l1.append(1);
l1.append(2);
l1.append(3);
l1.append(4);
l1.removeAt(2);
l1.appendAt(2, "fufu");
console.log(l1, l1.indexOf("fufu"));