一、链表介绍
维基百科介绍:
在计算机科学中,链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
二、优缺点
优点:
- 存储和删除效率非常高。数组的删除和插入对后面元素有很大影响,需要重新排列。
- 与数组相比,可以动态的扩容
缺点:
- 不能像数组那样根据下标值取数据,必须要重头遍历
- 存储空间比数组大
三、实现单向链表
首先,我们定义一个节点类
// 节点类
class Node {
constructor(val, next) {
this.val = val;
this.next = next;
}
}
接着定义一个单向链表类
class LinkNode {
constructor() {
this.length = 0;
this.head = null;
}
}
接着我们只需要往链表类里添加链表常用的方法就可以了。
1. 添加节点
append(data) {
// 创建节点
const node = new Node(data);
// 如果链表为空
if (this.length === 0) {
this.head = node;
} else {
// 链表不为空
let current = this.head;
// 取到最后的节点
while (current.next) {
current = current.next;
}
current.next = node;
}
this.length += 1;
}
2. 字符串化输出
遍历输出链表
toString() {
let string = "";
let current = this.head;
while (current) {
string += current.val + " ";
current = current.next;
}
return string;
}
3. 插入节点
插入节点相对来说会复杂一点。我们从以下这几个方面考虑
- 插入的位置是头节点
- 插入的节点是尾结点
- 插入的节点是中间节点
insert(position, val) {
// 校验要插入的位置
if (position < 0 || position > this.length) {
return false;
}
const node = new Node(val);
// 链表为空
if (!this.head) {
this.head = node;
} else {
// 链表不为空
// 1.position = 0
if (position === 0) {
node.next = this.head;
this.head = node;
} else {
// 2. 后面的任意位置
let current = this.head;
let previous = null;
let index = 0;
// 找到目标位置和目标位置的前一个元素
while (index < position) {
previous = current;
current = current.next;
index++;
}
// 插入
node.next = current;
previous.next = node;
this.length += 1;
}
}
}
4. 获取指定位置节点
get(position) {
if (position < 0 || position >= this.length) {
return false;
}
let current = this.head;
let index = 0;
while (index < position) {
current = current.next;
index += 1;
}
return current.val;
}
5. 更新节点
update(position, val) {
if (position < 0 || position >= this.length) {
return false;
}
let current = this.head;
let index = 0;
while (index < position) {
current = current.next;
index += 1;
}
current.val = val;
}
6. 根据索引删除节点
我们只需要思考一下这些问题
- 删除的是头结点
- 删除的是后面的节点
- 只需要把要删除的节点的next节点指向要删除节点的next节点
removeAt(position) {
if (position < 0 || position >= this.length) {
return false;
}
let current = this.head;
let previous = null;
let index = 0;
if (position === 0) {
this.head = this.head.next;
} else {
while (index < position) {
previous = current;
current = current.next;
index += 1;
}
// 移除
previous.next = current.next;
this.length -= 1;
}
return current.val;
}
7. 根据值删除节点
remove(val) {
let current = this.head;
let previous = null;
while (current) {
if (current.val === val) {
this.length -= 1;
// 如果删除的不是第一个元素
if (previous) {
previous.next = current.next;
} else {
// 删除的是第一个元素
this.head = current.next;
}
return true;
}
previous = current;
current = current.next;
}
return false;
}
四、实现双向链表
对于双向链表,我们需要改造一下我们的类
- 新增前指向
- 新增尾指针
class Node {
constructor(val) {
this.val = val;
this.next = null;
this.prev = null;
}
}
class LinksNode {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
}
至于方法来说,相对于单向链表更加灵活了,但是思维逻辑要复杂一点。我们来看一下功能方法的实现过程。
1. 添加节点
多了尾指针和前一个节点的指向
append(val) {
const node = new Node(val);
// 链表为空
if (this.length === 0) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
// 链表长度+1
this.length += 1;
}
2. 字符串化输出
没有改变,就不展示代码了
3. 插入节点
多了尾指针和前一个节点的指向
append(val) {
const node = new Node(val);
// 链表为空
if (this.length === 0) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
// 链表长度+1
this.length += 1;
}
4. 获取指定位置节点
没有改变,就不展示代码了
5. 更新节点
没有改变,就不展示代码了
6. 根据索引删除节点
我们还是需要思考一下这些问题
- 删除的是头结点
- 删除的是中间的节点
- 删除的是尾结点
removeAt(position) {
if (position < 0 || position >= this.length) {
return false;
}
let current = this.head;
let index = 0;
// 当删除的是第一个元素时
if (position === 0) {
this.head = current.next;
current.next.prev = null;
} else if (position === this.length - 1) {
// 删除的是尾结点
while (index < position) {
current = current.next;
index++;
}
this.tail = current.prev;
current.prev.next = null;
this.tail.next = null;
} else {
// 删除的中间节点
while (index < position) {
console.log(current);
current = current.next;
index++;
}
// 存储上一个元素
const previous = current.prev;
// 删除
previous.next = current.next;
current.next.prev = previous;
}
this.length -= 1;
return true;
}
7. 根据值删除节点
和上面删除要考虑的方面一致
remove(val) {
let current = this.head;
let index = 0;
while (current) {
if (current.val === val) {
// 头结点
if (current === this.head) {
this.head = current.next;
current.next.prev = null;
return true;
} else if (current === this.tail) {
// 尾节点
this.tail = current.prev;
current.prev.next = null;
this.tail.next = null;
return true;
} else {
// 存储上一个元素
const previous = current.prev;
// 删除
previous.next = current.next;
current.next.prev = previous;
return true;
}
}
current = current.next;
}
return false;
}
8. 前序字符串输出
从头节点开始遍历
forwardString() {
let str = "";
let current = this.head;
while (current) {
str += current.val + " ";
current = current.next;
}
return str;
}
9. 后续序字符串输出
从尾结点开始遍历
backwardString() {
let str = "";
let current = this.tail;
while (current) {
str += current.val + " ";
current = current.prev;
}
return str;
}