在js中存储多个数据的通常方式是使用数组
,其实还可以使用链表
这种数据格式来保存数据,但是原生js并没有提供链表这种特殊的数据格式
在一些场景下,利用链表的的特性来解决一些算法类的问题是具有一定优势的。
链表是什么
先说说什么是链表
,通常链表是由多个节点
构成,每个节点都保存一个值(value),另外还保存了指向下一个节点的引用(指针next)
上边的链表就由5个节点构成,其中第一个节点叫做链表的头部(head
),最后一个节点叫做链表的尾部(tail
)。
我们猜想链表应该还具有一个length
属性来记录链表长度(节点的个数)
动手开始写代码
首先由两个类分别用来创建节点和链表:
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = this.tail = null;
this.length = 0;
}
}
Node
初始化的时候,用value记录了自身的值。next表示下一个节点,默认指向nullLinkedList
初始化的时候,链表头尾都指向null,自身长度默认为0。其中head
表示第一个节点,tail
表示最后一个节点
末尾追加节点
class LinkedList {
constructor() {
this.head = this.tail = null;
this.length = 0;
}
// 末尾增加节点
append(value) {
const node = new Node(value);
if (!this.head) {
this.head = this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.length++;
}
}
- 首先新建一个节点,
- 判断是否是空链表:
- 如果是空链表,直接把节点赋值给head和tail,
- 如果不是空链表,就让最后一个节点tail指向新节点node ,其实这时候新节点node变成了最后一个节点,所以node才是tail,于是改写tail为node,
- 由于末尾新增了一个节点,于是length加1
头部增加节点
// 头部增加节点
prepend(value) {
const node = new Node(value);
node.next = this.head;
this.head = node;
this.length++;
}
- 这里和上面类似,但是不用判断是否是空链表,因为在append的时候,tail有可能是null,所以不存在next属性,强行获取tail身上的next属性就会报错。
- 而prepend的时候node本身就是一个对象,可以放心地设置next属性。
查找指定索引节点
在链表中获取指定索引的元素不像数组那么方便,需要一个遍历
操作才能找到指定节点(这也是链表的缺点之一)
find(index) {
const { length } = this;
if (index < 0 || index > length - 1 || length === 0) return null; // 范围溢出或者空链表
let currentNode = this.head;
let i = 0;
while (i < index) {
currentNode = currentNode.next;
i++;
}
return currentNode;
}
- 首先排除索引超出范围和链表为空的情况
- 记录第一个节点,开始遍历,只要没到目标索引,就不断获取下一个节点,最后返回目标节点。
- 这样看来,访问越后面的节点需要循环查询的次数越多。时间复杂度是O(n)
在指定索引插入节点
插入节点的操作分为3个步骤
- 找到目标索引的节点和前一个节点
- 把前一个节点指向新节点
- 把新节点指向目标索引节点
insert(value, index) {
if (index <= 0) {
this.prepend(value);
} else if (index >= this.length) {
this.append(value);
} else {
const node = new Node(value);
const prevNode = this.find(index - 1);
const nextNode = prevNode.next;
prevNode.next = node;
node.next = nextNode;
this.length++;
}
}
先处理两种特殊情况,
- 如果index等于0说明要在头部插入节点,可以直接使用prepend方法。
- 如果index大于链表长度,说明是要在末尾插入节点,于是可以直接使用append方法。
- 默认情况下新建一个节点,获取前一个节点prevNode 与目标节点nextNode ,改变前一个节点与新节点的指向,再把length加1。
移除节点
移除节点的思想和插入节点类似,找到相应节点,改变节点指向
remove(index) {
const { length } = this;
if (index < 0 || index > length - 1 || length === 0) return; // 范围溢出或者空链表
// 只有一个节点
if (length === 1) {
this.head = this.tail = null;
}
// 移除头节点
else if (index === 0) {
this.head = this.head.next;
}
// 移除尾节点
else if (index === length - 1) {
const prevNode = this.find(index - 1);
this.tail = prevNode;
prevNode.next = null;
} else {
const prevNode = this.find(index - 1);
const nextNode = prevNode.next.next;
prevNode.next = nextNode;
}
this.length--;
}
这里边考虑了几种特殊情况:
- 范围溢出或者链表为空就啥也不用做
- 只有一个节点的话,直接重写head和tail为null就相当于移除节点了,因为此时将没有任何一个指针指向该节点,节点会被js的垃圾回收机制清除
- 当index为0的时候,表示要移除第一个节点,直接重写head就好了,原来的head由于没有人指向他,会被自动清除
- 当index指向最后一个节点的时候,先找到倒数第二个节点,把他指向null,这就意味着原来的最后一个节点没有人指向他了,会被自动清除
- 默认情况下,直接找到前一个节点与下一个节点,直接把前一个节点指向下一个节点,原来index位置的节点被跳过了,于是被自动清除了。
反转链表
反转链表稍微复杂一些,
- 这里用2个指针分别记录前一个节点,当前节点,
- 每次循环改变当前节点的指向,然后把2个指针往右边移动一格,
- 直到当前节点为空的时候,整个链表指针就被翻转过来了
reverse() {
let prevNode = null;
let currentNode = (this.tail = this.head);
while (currentNode) {
const nextNode = currentNode.next;
currentNode.next = prevNode; // 反转
prevNode = currentNode; // 指针右移
currentNode = nextNode; // 指针右移
}
this.head = prevNode;
}
- 翻转开始之前,记得重写一下tail。
- 在每次循环的时候。临时用nextNode 保存一下下一个节点,不然改变currentNode.next 的时候,下一个节点会丢失,这样循环就没办法连续进行下去了。
- 循环结束后prevNode其实表示最后一个节点,也就是翻转以后的第一个节点,所以赋值给head。
完整代码贴上
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = this.tail = null;
this.length = 0;
}
// 末尾增加节点
append(value) {
const node = new Node(value);
if (!this.head) {
this.head = this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.length++;
}
// 头部增加节点
prepend(value) {
const node = new Node(value);
node.next = this.head;
this.head = node;
this.length++;
}
// 指定索引插入节点
insert(value, index) {
if (index <= 0) {
this.prepend(value);
} else if (index >= this.length) {
this.append(value);
} else {
const node = new Node(value);
const prevNode = this.find(index - 1);
const nextNode = prevNode.next;
prevNode.next = node;
node.next = nextNode;
this.length++;
}
}
// 移除节点
remove(index) {
const { length } = this;
if (index < 0 || index > length - 1 || length === 0) return; // 范围溢出或者空链表
// 只有一个节点
if (length === 1) {
this.head = this.tail = null;
}
// 移除头节点
else if (index === 0) {
this.head = this.head.next;
}
// 移除尾节点
else if (index === length - 1) {
const prevNode = this.find(index - 1);
this.tail = prevNode;
prevNode.next = null;
} else {
const prevNode = this.find(index - 1);
const nextNode = prevNode.next.next;
prevNode.next = nextNode;
}
this.length--;
}
// 反转链表
reverse() {
let prevNode = null;
let currentNode = (this.tail = this.head);
while (currentNode) {
const nextNode = currentNode.next;
currentNode.next = prevNode; // 反转
prevNode = currentNode; // 指针右移
currentNode = nextNode; // 指针右移
}
this.head = prevNode;
}
// 查找指定索引节点
find(index) {
const { length } = this;
if (index < 0 || index > length - 1 || length === 0) return null; // 范围溢出或者空链表
let currentNode = this.head;
let i = 0;
while (i++ < index) {
currentNode = currentNode.next;
}
return currentNode;
}
}
测试:
const list = new LinkedList();
list.append("111");
list.append("222");
list.append("333");
list.append("444");
list.prepend("000");
console.log(list, list.find(2));