链表是一种存储结构,一个链表包含若干个节点,每个节点至少包含一个数据域和指针域,指针域指向下一个节点。
链表的元素存储并不连续,用next指针连在一起。
链表结构有很多种,常见的有单向链表、循环链表、双向链表等。
-
单向链表
-
循环链表
-
双向链表
不同与Array、set、map等数据结构,目前js中没有链表的数据结构,但是我们可以通过对象来实现。
实现单向链表
先定义一个基本骨架
class Node {
constructor(data) {
this.data = data;
this.next = null;
this.prev = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.length = 0;
}
// 获取节点
get() {}
// 添加节点
add() {}
// 插入节点
insert() {}
// 删除指定位置的节点
removeAt() {}
// 更新节点
update() {}
// 获取节点所在位置
indexOf() {}
// 删除节点
remove() {}
// 判断链表是否为空
isEmpty() {
return this.length === 0;
}
// 返回链表长度
size() {
return this.length;
}
}
接下来,一步一步实现链表的基本操作
获取节点
先判断传入的参数是否在链表的长度范围之内,如果是在范围内则依次找到链表的对应位置的节点
get(position) {
if (
typeof position !== "number" ||
position < 0 ||
position > this.length - 1
){
return;
}
let index = 0;
let target = this.head;
while (index++ < position) {
target = target.next;
}
return target;
}
添加节点
先获取链表的最后一个节点,如果没有则将头部节点赋值为新节点,如果有则将新节点添加到最后节点的next指向,最后将长度加一。
// 添加节点
add(data) {
const newNode = new Node(data);
let node = this.get(this.length - 1);
if (node) {
node.next = newNode;
} else {
this.head = newNode;
}
this.length++;
}
插入节点
如果是起点插入,则新的节点的next指向原先的头部节点,然后将头部节点改为新的节点,如果不是起点插入,则获取上一个节点,将新节点的next指向上一个的节点的next指向的节点,将上一个节点的next指向修改为新节点,最后将长度加一。
insert(position, data) {
if (position < 0 || position > this.length)
return;
const newNode = new Node(data);
if (position === 0) {
newNode.next = this.head;
this.head = newNode;
} else {
let preNode = this.get(position - 1);
newNode.next = preNode.next;
preNode.next = newNode;
}
this.length++;
}
删除指定位置的节点
如果是删除头部节点,则将头部节点设置为原先的头部节点的next指向,如果不是头部节点,则获取删除节点位置的上一个节点,然后将上一个节点的next指向删除节点的下一个节点,最后将长度减一。
removeAt(position) {
if (position < 0 || position > this.length - 1) return;
if (position === 0) {
this.head = this.head.next;
} else {
let preNode = this.get(position - 1);
preNode.next = preNode.next.next;
}
this.length--;
}
更新节点
先删除对应的节点,再插入新的节点
update(position, data) {
if (position < 0 || position > this.length - 1) return;
this.removeAt(position);
this.insert(position, data);
}
获取节点所在位置
遍历链表找出节点位置
indexOf(data) {
let index = 0;
let current = this.head;
while (current) {
if (current.data === data) {
return index;
}
index++;
current = current.next;
}
return -1;
}
删除节点
先找出节点的位置,再删除
remove(data) {
let index = this.indexOf(data);
if(index === -1) return;
this.removeAt(index);
}
至此,便大致实现了一个单向链表,代码总览如下
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.length = 0;
}
// 获取节点
get(position) {
if (
typeof position !== "number" ||
position < 0 ||
position > this.length - 1
)
return;
let index = 0;
let target = this.head;
while (index++ < position) {
target = target.next;
}
return target;
}
// 添加节点
add(data) {
const newNode = new Node(data);
let node = this.get(this.length - 1);
if (node) {
node.next = newNode;
} else {
this.head = newNode;
}
this.length++;
}
// 插入节点
insert(position, data) {
if (position < 0 || position > this.length) return;
const newNode = new Node(data);
if (position === 0) {
newNode.next = this.head;
this.head = newNode;
} else {
let preNode = this.get(position - 1);
newNode.next = preNode.next;
preNode.next = newNode;
}
this.length++;
}
// 删除指定位置的节点
removeAt(position) {
if (position < 0 || position > this.length - 1) return;
if (position === 0) {
this.head = this.head.next;
} else {
let preNode = this.get(position - 1);
preNode.next = preNode.next.next;
}
this.length--;
}
// 删除节点
remove(data) {
let index = this.indexOf(data);
if (index === -1) return;
this.removeAt(index);
}
// 更新节点
update(position, data) {
if (position < 0 || position > this.length - 1) return;
this.remove(position);
this.insert(position, data);
}
// 获取节点所在位置
indexOf(data) {
let index = 0;
let current = this.head;
while (current) {
if (current.data === data) {
return index;
}
index++;
current = current.next;
}
return -1;
}
// 判断链表是否为空
isEmpty() {
return this.length === 0;
}
// 返回链表长度
size() {
return this.length;
}
}
实现循环链表
循环链表和单向链表相似,唯一区别在于,循环链表的最后一个节点的next指针指向第一个节点
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
class CircleLinkedList {
constructor() {
this.length = 0
this.head = null
}
// 获取节点
get() {}
// 添加节点
add() {}
// 插入节点
insert() {}
// 删除指定位置的节点
removeAt() {}
// 更新节点
update() {}
// 获取节点所在位置
indexOf() {}
// 删除节点
remove() {}
// 判断链表是否为空
isEmpty() {
return this.length === 0;
}
// 返回链表长度
size() {
return this.length;
}
}
添加节点
添加节点的方法在单向链表的添加节点方法的基础上增加新节点的next指向头部节点
add(data) {
const newNode = new Node(data);
let node = this.get(this.length - 1);
if (node) {
node.next = newNode;
} else {
this.head = newNode;
}
newNode.next = this.head;
this.length++;
}
插入节点
在单向链表的插入节点方法的基础上,如果是插入头部节点,则将尾部节点的next指向插入的头部节点
insert(position, data) {
if (position < 0 || position > this.length) return;
const newNode = new Node(data);
if (position === 0) {
newNode.next = this.head;
this.head = newNode;
this.length++; // 头部节点插入后,将length+1再获取正确的尾部节点
let lastNode = this.get(this.length - 1);
lastNode.next = this.head;
} else {
let preNode = this.get(position - 1);
newNode.next = preNode.next;
preNode.next = newNode;
this.length++;
}
}
删除指定位置的节点
在单向链表的删除节点方法的基础上,如果是删除头部节点,则将尾部节点的next指向新的头部节点
removeAt(position) {
if (position < 0 || position > this.length - 1) return;
if (position === 0) {
this.head = this.head.next;
this.length--; // 头部节点删除后,将length-1以获取正确的尾部节点
let lastNode = this.get(this.length - 1);
lastNode.next = this.head;
} else {
let preNode = this.get(position - 1);
preNode.next = preNode.next.next;
this.length--;
}
}
获取节点、更新节点、获取节点所在位置、删除节点的方法与单向链表一致,代码如下
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
class CircleLinkedList {
constructor() {
this.length = 0;
this.head = null;
}
// 获取节点
get(position) {
if (
typeof position !== "number" ||
position < 0 ||
position > this.length - 1
) {
return;
}
let index = 0;
let target = this.head;
while (index++ < position) {
target = target.next;
}
return target;
}
// 添加节点
add(data) {
const newNode = new Node(data);
let node = this.get(this.length - 1);
if (node) {
node.next = newNode;
} else {
this.head = newNode;
}
newNode.next = this.head;
this.length++;
}
// 插入节点
insert(position, data) {
if (position < 0 || position > this.length) return false;
const newNode = new Node(data);
if (position === 0) {
newNode.next = this.head;
this.head = newNode;
this.length++;
let lastNode = this.get(this.length - 1);
lastNode.next = this.head;
} else {
let preNode = this.get(position - 1);
newNode.next = preNode.next;
preNode.next = newNode;
this.length++;
}
}
// 删除指定位置的节点
removeAt(position) {
if (position < 0 || position > this.length - 1) return;
if (position === 0) {
this.head = this.head.next;
this.length--;
let lastNode = this.get(this.length - 1);
lastNode.next = this.head;
} else {
let preNode = this.get(position - 1);
preNode.next = preNode.next.next;
this.length--;
}
}
// 更新节点
update(position, data) {
if (position < 0 || position > this.length - 1) return;
this.remove(position);
this.insert(position, data);
}
// 获取节点所在位置
indexOf(data) {
let index = 0;
let current = this.head;
while (current) {
if (current.data === data) {
return index;
}
index++;
current = current.next;
}
return -1;
}
// 删除节点
remove(data) {
let index = this.indexOf(data);
if (index === -1) return;
this.removeAt(index);
}
// 判断链表是否为空
isEmpty() {
return this.length === 0;
}
// 返回链表长度
size() {
return this.length;
}
}
实现双向链表
先定义一个基本骨架
class DoublyNode {
constructor(data) {
this.data = data;
this.next = null;
this.prev = null;
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null; // 指向最后一个节点
this.length = 0;
}
// 获取节点
get() {}
// 添加节点
add() {}
// 插入节点
insert() {}
// 删除指定位置的节点
removeAt() {}
// 更新节点
update() {}
// 获取节点所在位置
indexOf() {}
// 删除节点
remove() {}
// 判断链表是否为空
isEmpty() {
return this.length === 0;
}
// 返回链表长度
size() {
return this.length;
}
}
获取节点
双向链表获取节点,由于双向链表具备从尾部节点往回查找节点的能力,所以可以先做一层二分判断,增加检索速度
get(position) {
if (
typeof position !== "number" ||
position < 0 ||
position > this.length - 1
) {
return;
}
if (position > (this.length - 1) / 2) {
let index = this.length - 1;
let target = this.tail;
while (index-- > position) {
target = target.prev;
}
return target;
} else {
let index = 0;
let target = this.head;
while (index++ < position) {
target = target.next;
}
return target;
}
}
添加节点
判断是否存在头部节点,如果不存在则把头部和尾部节点都设置为新节点;如果存在则将新节点的prev指向当前的尾部节点,并把当前尾部节点的next指向新节点,最后把尾部节点重新设置为新节点。
add(data) {
const newNode = new DoublyNode(data);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode
}
this.length++;
}
插入节点
分头部、尾部、中间插入三种情况
insert(position, data) {
if (position < 0 || position > this.length) return;
const newNode = new DoublyNode(data);
const currentNode = this.get(position);
if (position === 0) {
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.head = newNode;
newNode.next = currentNode;
currentNode.prev = newNode;
}
} else if (position === this.length) {
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
} else {
newNode.next = currentNode;
newNode.prev = currentNode.prev;
currentNode.prev.next = newNode;
currentNode.prev = newNode;
}
this.length++;
}
删除指定位置的节点
分删除头部、尾部、中间三种情况
removeAt(position) {
if (position < 0 || position > this.length - 1) return;
if (position === 0) {
this.head = this.head.next;
if (this.length === 1) {
this.tail = null;
} else {
this.head.prev = null;
}
} else if (position === this.length - 1) {
this.tail = this.tail.prev;
this.tail.next = null;
} else {
let currentNode = this.get(position);
currentNode.prev.next = currentNode.next;
currentNode.next.prev = currentNode.prev;
}
this.length--;
}
获取节点、更新节点、获取节点所在位置、删除节点的方法与单向链表类似,代码如下
class DoublyNode {
constructor(data) {
this.data = data;
this.next = null;
this.prev = null;
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
// 获取节点
get(position) {
if (
typeof position !== "number" ||
position < 0 ||
position > this.length - 1
) {
return;
}
if (position > (this.length - 1) / 2) {
let index = this.length - 1;
let target = this.tail;
while (index-- > position) {
target = target.prev;
}
return target;
} else {
let index = 0;
let target = this.head;
while (index++ < position) {
target = target.next;
}
return target;
}
}
// 添加节点
add(data) {
const newNode = new DoublyNode(data);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
}
// 插入节点
insert(position, data) {
if (position < 0 || position > this.length) return;
const newNode = new DoublyNode(data);
const currentNode = this.get(position);
if (position === 0) {
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.head = newNode;
newNode.next = currentNode;
currentNode.prev = newNode;
}
} else if (position === this.length) {
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
} else {
newNode.next = currentNode;
newNode.prev = currentNode.prev;
currentNode.prev.next = newNode;
currentNode.prev = newNode;
}
this.length++;
}
// 删除指定位置的节点
removeAt(position) {
if (position < 0 || position > this.length - 1) return;
if (position === 0) {
this.head = this.head.next;
if (this.length === 1) {
this.tail = null;
} else {
this.head.prev = null;
}
} else if (position === this.length - 1) {
this.tail = this.tail.prev;
this.tail.next = null;
} else {
let currentNode = this.get(position);
currentNode.prev.next = currentNode.next;
currentNode.next.prev = currentNode.prev;
}
this.length--;
}
// 更新节点
update(position, data) {
if (position < 0 || position > this.length - 1) return;
this.remove(position);
this.insert(position, data);
}
// 获取节点所在位置
indexOf(data) {
let index = 0;
let current = this.head;
while (current) {
if (current.data === data) {
return index;
}
index++;
current = current.next;
}
return -1;
}
// 删除节点
remove(data) {
let index = this.indexOf(data);
if (index === -1) return;
this.removeAt(index);
}
// 判断链表是否为空
isEmpty() {
return this.length === 0;
}
// 返回链表长度
size() {
return this.length;
}
}
数组和链表
结构
数组和链表都是线性数据结构
- 数组为静态结构,静态分配内存。
- 链表可以动态分配内存。
内存
- 数组在数据储存时需要占用整块、连续的内存空间。
- 链表是由一组零散(非连续)的内存块透过指针串连而成。
查询
-
数组可以通过索引值查询,效率较高。
-
链表需要一步步遍历到目标节点,时间复杂度在
O(1)-O(n)
之间,效率较低。
插入、删除
-
数组在增删非首尾元素时往往需要移动元素。
-
链表在插入/删除时只需要考虑相邻节点的指针变化,无需移动其他节点,时间复杂度就为
O(1)
,处理效率较高。
在实际开发过程中,我们可以根据不同的功能需要采用不同的数据结构。