链表

什么是链表

链表是一种基础的数据结构,是一种线性表,在链表中,数据存储在内存中分散的位置,可以存储多个值链表的的每个元素由一个存储元素本身的节点和下一个元素的引用组成。由于数据存储在不同的位置,每个数据只能够通过其链表项提供的指针访问,数据的添加只需通过替换添加任一侧的指针即可。

现实中链表:火车、链条、寻宝游戏、许多人手牵着手

链表与数组的比较

常见的链表结构

  1. 单向链表
  2. 双向链表
  3. 循环链表
  4. 有序链表

优势

  1. 相对于数组,添加或移除元素不需要移动其他元素,在进行添加或移除元素时的效率高。
  2. 内存利用率高,只有在需要的时候才会创建内存空间。

劣势

查找链表的元素效率低,必须从链表的首节点或尾节点开始向后遍历查找

单向链表

概念

单向链表的链接方向是单向的,对单向链表的访问要通过顺序读取从头部开始,即单向链表能从头节点正向遍历访问下一个节点,直到单向链表的尾节点,单向链表的尾节点的指向永远为null,单向链表也不能逆向遍历前面的单向链表节点。

劣势

  1. 查找链表的元素效率低,必须从链表的第一个节点开始向后遍历查找

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/**
* Node 辅助类,表示想要添加的项
* @param {*} element 需要加入链表元素的值
*/
export class Node{
constructor(element) {
this.element = element; //要添加到列表的项
this.next = null; //指向列表中下一个节点项的指针
}
}
// 链表
export class LinkedList{
constructor() {
this.head = null; //链表的第一个节点
this.length = 0; //链表的长度
}
/**
* 向链表尾部添加一个新的项
* @param {*} element 需要添加的项
*/
push(element) {
const node = new Node(element); //把需要添加的项(element)作为值传入,创建Node项
let current;
// 如果head为null,则添加的是链表的第一个节点(为空的列表添加一个元素)
if (this.head === null) {
this.head = node; //把要添加的项指向head
} else {
// 如果head不为null,则列表不为空(向一个不为空的列表尾部添加元素)
/**
* 第一步:需要找到链表最后一个元素
* 由于我们只有第一个元素的引用,我们需要一个current指向,还需要把第一个元素的引用赋值给current,然后循环访问列表,直到找到最后一项。
*/

//把链表的第一个元素指向current
current = this.head;

// 循环列表,直到current.next元素为null,此时已经到达链表的尾部,也就是找到最后一项
while (current.next) {
current = current.next;
}

// 第二步:找到最后一项,将它的next赋为node(当前想要添加到列表的节点),建立链接
current.next = node;
}

// 递增链表的长度

this.length++; // 更新链表的长度
}
/**
* 从链表的特定位置移除一项
* @param {Number} index 需要移除链表项的位置
*/
removeAt(index) {
// 检查越界值(检查输入的移除元素位置是否有效,参考数组)
if (index >= 0 && index < this.length) {
let current = this.head; // 对链表当前元素的引用,默认为链表第一个元素
let index = 0; //用于循环索引链表项的变量,默认为第一个元素的位置

/**
* 从链表中移除第一个元素
* 实现方法:将head指向链表的第二个元素
*/
if (index === 0) {
this.head = current.next;
} else {
/**
* 从链表中移除某一项或者最后一项
* 第一步:将循环索引的下标不断递增,寻找到最后一项,current此时保存着最后一项的引用,previous保存着当前元素前一个元素的引用
*/
const previous = this.getElementAt(index - 1);// 对链表当前元素的前一个元素的引用
current = previous.next;
// console.log(previous, current,index);
/**
* 如果是移除最后一项,此时current.next的指向值是null,把previous.next设置为null,则等于将最后一项移除
* 如果是中间元素的引用,此时current.next的指向值是下一个链表元素的引用,把previous.next设置为current.next,则等于移除了当前元素
*/
// 将previous与current的下一项链接起来:跳过current,从而移除它
previous.next = current.next;
}
// 减少链表的长度
this.length--; // 更新链表的长度

return current.element; // 移除成功,返回被移除的元素
} else {
// 不是有效的位置,返回undefined(即没有从链表中移除元素)
return undefined;
}
}
/**
* 返回链表中特定位置的元素。如果链表中不存在这样的元素,则返回 undefined。
* @param {*} index 链表中元素的位置
*/
getElementAt(index) {
// 检查越界值(检查需要插入元素的位置是否有效,参考数组)
if (index >= 0 && index <= this.length) {
let current = this.head; // 对链表当前元素的引用,默认为链表第一个元素
// 迭代整个链表直到找到目标index
for (let i = 0; i < index && current; i++) {
// 找到时,把current设置为index位置元素的引用
current = current.next;
}
// 返回该元素
return current;
}
// 不是有效的位置,返回undefined(即这个位置在链表中并不存在)
return undefined;
}
/**
* 向链表的特定位置插入一个新的项
* @param {*} element 需要插入列表的项
* @param {Number} index 需要插入列表的位置
*/
insert(index, element) {
// 检查越界值(检查需要插入元素的位置是否有效,参考数组)
if (index >= 0 && index <= this.length) {
const node = new Node(element);// 针对要插入的项,创建node项
// 在链表的起始位置添加一个元素
if (index === 0) { //在第一个位置添加
const current = this.head; // 对链表想要插入新元素的位置之后一个元素的引用,默认为链表第一个元素
//将node.next设置为列表的第一个元素:current
node.next = current;
// 将头部的引用指向插入项(node)
this.head = node;
} else {
/**
* 从链表中间或尾部添加一个元素
* 第一步:将循环索引的下标不断递增,寻找到最后一项,current此时保存着对链表想要插入新元素的位置之后一个元素的引用,previous保存着对链表想要插入新元素的位置之前一个元素的引用
*/
const previous = this.getElementAt(index - 1);// 对链表想要插入新元素的位置之前一个元素的引用
const current = previous.next; // 对链表想要插入新元素的位置之后一个元素的引用
// console.log(previous, current,position);
/**
* 第二步:
* 要在previous和current之间添加新项,把新项node和当前项current链接起来,然后改变previous.next的指向为node
* 如果是在最后一个位置添加一个新元素,previous是链表最后一项的引用,current则是null,此时将node.next指向current,previous.next指向node,就有了一个新的项
* 如果在链表中间添加一个新元素,此时需要将node插入previous和current元素之间,把node.next指向current,把previous.next设为node,此时列表中就有了一个新的项。
*/
node.next = current;
previous.next = node;
}
// 递增链表的长度
this.length++; // 更新链表的长度

return true;

} else {
// 不是有效的位置,返回false(即没有从链表中插入新元素)
return false;
}
}
/**
* 以字符串的方式输出链表的值
* @returns {String}
*/
toString() {
// 如果head为空,则链表是一个空链表,直接返回一个空字符串
if (!this.head) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next; // 对链表当前元素的引用,默认为链表第二个元素
for (let i = 1; i < this.size() && current; i++) {
objString = `${objString},${current.element}`
// console.log(objString);
current = current.next;
}
// 把最终生成的链表元素通过字符串方式返回
return objString;
}
/**
* 以数组的方式输出链表的值
* @returns {Array}
*/
toArray() {
let current = this.head;// 对链表当前元素的引用,默认为链表第一个元素
let array = []; //接收元素值变量
// 检查current是否存在
while (current) {
// 存在则添加到数组中
array.push(current.element);
// 把current引用改为下一个链表元素
current = current.next;
}
// 把最终生成的链表元素通过数组方式返回
return array;
}
/**
* 返回元素在链表中的索引。如果链表中没有该元素则返回-1。
* @param {*} element 需要在链表中查询的元素
* @returns {Number}
*/
indexOf(element) {
let current = this.head;// 对链表当前元素的引用,默认为链表第一个元素
// 迭代并在迭代时检查current是否存在
for (let i = 0; i < this.length && current; i++) {
// 检查输入的元素值与current.element的元素值是否相等,如果相等,则当前元素是我们要找到
if (element === current.element) {
//找到了,则返回他的位置
return i;
}
// 把current引用改为下一个链表元素
current = current.next;
}
//如果仍然还是找不到,则该元素在链表中不存在,则返回-1
return -1;
}
/**
* 丛链表中移除一项
* @param {*} element 需要从链表中移除的项
*/
remove(element) {
// 通过indexOf方法找到元素的位置
let index = this.indexOf(element);
// 然后调用removeAt方法传入找到的位置进行删除
return this.removeAt(index);
}
/**
* 返回链表包含的元素个数。
* @returns {Number}
*/
size() {
return this.length;
}
/**
* 如果链表中不包含任何元素,则返回true,如果链表长度大于0,则返回false
* @returns {Boolean}
*/
isEmpty() {
return this.size() === 0;
}
/**
* 返回链表包含的头节点(Head)
*/
getHead() {
return this.head;
}
/**
* 清空链表
*/
clear() {
this.head = undefined; // 链表头节点设置为空
this.length = 0; // 链表长度重置为0
}

}
let list = new LinkedList();
list.push(15);
// list.push(10);
// list.push(17);
// console.log(list);
// list.insert(3, 5);
// list.removeAt(2);
// console.log(list);
list.toString();
// console.log(list.toString());

双向链表

概念

双向链表和普通单向链表的区别在于,在单向链表中,一个节点只有链向下一个节点的链接;而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素。

在单向链表中,如果迭代时错过了要找的元素,就需要回到起点,重新开始迭,而双向链表,可以从头到尾,或者从尾到头。我们也可以访问一个特定节点的下一个或前一个元素。

优势

  1. 双向链表中提供了两种迭代的方法:从头到尾,或者从尾到头。
  2. 可以访问一个特定节点 的下一个或前一个元素。

劣势

  1. 每次添加或删除节点操作更复杂,需要操作的引用较多(4个)
  2. 内存空间占用比单向链表大

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { LinkedList, Node } from '/LinkedList/app.js';
// DoublyNode 辅助类 继承自 Node 辅助类
export class DoublyNode extends Node {
constructor(element, next, prev) {
super(element, next);
this.prev = prev; // 当前节点的前一个节点(新增)
}
}

/**
* 双向链表 继承自 LinkedList 类
* 双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接;而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素。
* 在单向链表中,如果迭代时错过了要找的元素,就需要回到起点,重新开始迭代,而双向链表,可以从头到尾,或者从尾到头。我们也可以访问一个特定节点的下一个或前一个元素。
*/
export class DoublyLinkedList extends LinkedList {
constructor() {
this.tail = undefined; // 对链表最后一个元素的引用(新增)
}
/**
* 向链表尾部添加一个新的项
* @param {*} element 需要添加的项
*/
push(element) {
const node = new DoublyNode(element); //把需要添加的项(element)作为值传入,创建DoublyNode项
let current;
// 如果head为null,则添加的是链表的第一个节点(为空的列表添加一个元素)
if (this.head === null) {
this.head = node; //把要添加的项指向head
this.tail = node; //把链表尾项指向node
} else {
// 如果head不为null,则列表不为空(向一个不为空的列表尾部添加元素),只需要附加到尾节点

//把链表尾节点的后一项引用设为node
this.tail.next = node;

// node的前一项的引用改为链表尾节点(this.tail)
node.prev = this.tail;;

// 最后将node赋值给链表尾节点,完成插入
this.tail = node;
}

// 递增链表的长度

this.length++; // 更新链表的长度
}
/**
* 向链表的特定位置插入一个新的项(重写insert方法)
* @param {*} element 需要插入列表的项
* @param {Number} index 需要插入列表的位置
**/
insert(index, element) {
// 检查越界值(检查需要插入元素的位置是否有效,参考数组)
if (index >= 0 && index <= this.length) {
const node = new DoublyNode(element);// 针对要插入的项,创建DoublyNode项
let current = this.head; //存储双向链表第一个元素的引用
// 在链表的起始位置插入一个元素
if (index === 0) { //在第一个位置添加
/**
* 如果双向链表为空,则只需要把head(链表的第一个节点)和tail(链表的最后一个节点)都指向node
*/
if (this.head === null) {
this.head = node; //链表第一个节点的引用指向node
this.tail = node; // 链表最后一个节点的引用指向弄得
} else {
/**
* 如果双向链表不为空,node.next(node下一个节点的指针)设置为当前链表第一个元素的引用-current(即this.head)
* 再将当前链表首节点的引用(current)的前一个节点的指针指向node
* 最后将当前的头节点(this.head)设置为node(由于插入的index位置为0,node的prev指针本身便是undefined,所以不需要对它进行操作)
*/
node.next = this.head; // node的下一个节点的指针指向当前链表的首节点
current.prev = node; // 将当前链表第一个元素的引用的前一个节点指向node
this.head = node; // 最后将当前的头节点(this.head)设置为node
}
} else if (index === this.length) { // 在链表最后一项插入一个元素(新增)
/**
* 在链表的尾节点添加一个元素
* 1.current保存链表尾节点的引用
* 2.current的下一个元素指针指向node
* 3.node的前一个节点的引用设置为current(链表尾节点的引用)
* 4.更新tail,将链表尾节点的引用设为node
*/
current = this.tail; // current保存链表尾节点的引用
current.next = node; // current的下一个元素指针指向node
node.prev = current; // node的前一个节点的引用设置为current(链表尾节点的引用)
this.tail = node; // 更新tail,将链表尾节点的引用设为node
} else {
/**
* 从链表中间(除首节点和尾节点)插入一个元素
* 第一步:将循环索引的下标不断递增,寻找到最后一项,current此时保存着对链表想要插入新元素的位置之后一个元素的引用,previous保存着对链表想要插入新元素的位置之前一个元素的引用
*/
const previous = this.getElementAt(index - 1);// 对链表想要插入新元素的位置之前一个元素的引用
const current = previous.next; // 对链表想要插入新元素的位置之后一个元素的引用
// console.log(previous, current,position);
/**
* 第二步:
* 要在previous和current之间添加新项,把node的下一个节点的引用(next)改为current(current存着对链表想要插入新元素的位置之后一个元素的引用)
* previous(对链表想要插入新元素的位置之前一个元素的引用)的next(previous的后一个节点的引用)设为node
* current(current存着对链表想要插入新元素的位置之后一个元素的引用)的prev(current的前一个节点的引用)设为node
* node前一个节点的引用(prev)设置为previous(对链表想要插入新元素的位置之前一个元素的引用)
*/
node.next = current;
previous.next = node;
current.prev = node;
node.prev = previous;
}
// 递增链表的长度
this.length++; // 更新链表的长度

return true;

} else {
// 不是有效的位置,返回false(即没有从链表中插入新元素)
return false;
}
}
/**
* 从链表的特定位置移除一项
* @param {Number} index 需要移除链表项的位置
*/
removeAt(index) {
// 检查越界值(检查输入的移除元素位置是否有效,参考数组)
if (index >= 0 && index < this.length) {
let current = this.head; // 对链表当前元素的引用,默认为链表第一个元素
let index = 0; //用于循环索引链表项的变量,默认为第一个元素的位置

/**
* 从链表中移除第一个元素
* 实现方法:1.将head指向链表的第二个元素
*/
if (index === 0) {
this.head = current.next;
// 2.如果只有一项,此时current.next返回的值为undefined,还需要更新tail为undefined(新增)
if (this.length === 1) {
this.tail = undefined; // 将链表尾节点置空。
} else {
// 2.如果不止一项,则将头节点的前一个节点的引用置空(新增)
this.head.prev = undefined;
}
} else if (index === this.length - 1) {
/**
* 从链表移除最后一项(新增)
* 实现方法:
* 1.将链表尾节点赋值给current
* 2.把tail的引用更新为双向链表中倒数第二个元素
* 3.把 tail的next 指针更新为undefined(尾节点的下一个节点清空)
*/
current = this.tail; //将链表尾节点赋值给current
this.tail = current.prev; // 把尾节点的引用改为尾节点的前一个节点
this.tail.next = undefined; // 把尾节点的下一个节点引用清空
} else {
/**
* 从链表中移除某一项(除首节点和尾节点)
* 第一步:将循环索引的下标不断递增,寻找到最后一项,current此时保存着要移除的元素,previous保存着当前元素前一个元素的引用
*/
current = this.getElementAt(index);// 链表中要移除的元素
const previous = current.prev; // 链表中要移除元素前一个元素的引用
// console.log(previous, current,index);
/**
* 移除中间元素的引用,此时current.next的指向值是下一个链表元素的引用,把previous.next设置为current.next,则等于移除了当前元素,再将current.next的前一个元素的引用(prev)指向为previous
*/
// 将previous与current的下一项链接起来:跳过current,从而移除它
previous.next = current.next;
// 修改current.next的前一个元素指向为previous
current.next.prev = previous;
}
// 减少链表的长度
this.length--; // 更新链表的长度

return current.element; // 移除成功,返回被移除的元素
} else {
// 不是有效的位置,返回undefined(即没有从链表中移除元素)
return undefined;
}
}
/**
* 返回链表包含的Tail
*/
getTail() {
super.clear(); // 执行父类的clear方法
this.tail = undefined; // 链表尾节点设置为undefined
}
/**
* 以字符串的方式输出反向链表的值
* @returns {String}
*/
inverseToString() {
// 如果head为空,则链表是一个空链表,直接返回一个空字符串
if (!this.tail) {
return '';
}
let objString = `${this.tail.element}`; // 链表尾元素的element
let previous = this.tail.prev; // 对链表尾元素前一个元素的引用
while (previous !== null) {
objString = `${objString},${previous.element}`;
previous = previous.prev;
}
// 把最终生成的链表元素通过字符串方式返回
return objString;
}

}

/**性能优化相关:
* 1.在结果为否的情况下,可以把元素插入双向链表的尾部。
* 2.如果position 大于 length/2,就最好从尾部开始迭代,而不是从头开始(这样就能迭代双向链表中更少的元素)。
*/

let list = new DoublyLinkedList();
list.push(13);
console.log(list);

循环链表

概念

循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针,不是引用undefined,而是指向第一个元素。

优势

  1. 遍历的时候可以从任意的节点开始,增加了遍历的灵活性

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { LinkedList, Node } from '/LinkedList/app.js';
/**
* 循环链表 可继承自 LinkedList 类 或 DoublyLinkedList 类
* 循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链
*表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用
*undefined,而是指向第一个元素(head)。
*/
export class CircularLinkedList extends LinkedList {
constructor() {
}
/**
* 向链表尾部添加一个新的项
* @param {*} element 需要添加的项
*/
push(element) {
const node = new Node(element); //把需要添加的项(element)作为值传入,创建Node项
let current;
// 如果head为null,则添加的是链表的第一个节点(为空的列表添加一个元素)
if (this.head == null) {
this.head = node; //把要添加的项指向head
} else {
// 如果head不为null,则列表不为空(向一个不为空的列表尾部添加元素)
/**
* 第一步:找到链表最后一个元素
*/
current = this.getElementAt(this.size() - 1);
//把链表的第一个元素指向current的下一个项的指针
current.next = node;
}
// 设置node的下一个项为链表头,完成循环链表的循环(新增)
node.next = this.head;
this.length++; // 更新链表的长度
}
/**
* 向链表的特定位置插入一个新的项
* @param {*} element 需要插入列表的项
* @param {Number} index 需要插入列表的位置
*/
insert(element, index) {
// 检查越界值(检查需要插入元素的位置是否有效,参考数组)
if (index >= 0 && index <= this.length) {
const node = new Node(element); // 针对要插入的项,创建node项
let current = this.head; //对链表当前元素的引用,默认为链表第一个元素
// 在链表的起始位置添加一个元素
if (index === 0) { //在第一个位置添加
if (this.head === null) {
// 如果没有任何链表项在这个链表
this.head = node; // 链表头设置为node
node.next = this.head; // node的下一个元素的指针指向链表头(新增)
} else {
// 如果链表不是空的
node.next = current; // 将node.next 指向现在的 head 引用的节点(current 变量)
current = this.getElementAt(this.size()); // 取得最后一个链表项的引用
this.head = node; // 更新头部元素为新插入的元素node
current.next = this.head; // 将最后一个节点(current)的next指向新的头部节点,完成循环链表的循环(新增)
}
} else {
/**
* 从链表中间或尾部添加一个元素
* 第一步:将循环索引的下标不断递增,寻找到最后一项,current此时保存着对链表想要插入新元素的位置之后一个元素的引用,previous保存着对链表想要插入新元素的位置之前一个元素的引用
*/
const previous = this.getElementAt(index - 1);// 对链表想要插入新元素的位置之前一个元素的引用
/**
* 第二步:
* 要在previous和current之间添加新项,把新项node和当前项current链接起来,然后改变previous.next的指向为node
* 如果是在最后一个位置添加一个新元素,previous是链表最后一项的引用,current则是null,此时将node.next指向current,previous.next指向node,就有了一个新的项
* 如果在链表中间添加一个新元素,此时需要将node插入previous和current元素之间,把node.next指向current,把previous.next设为node,此时列表中就有了一个新的项。
*/
node.next = previous.next;
previous.next = node;
}
this.length++; // 更新链表的长度
return true;
}
return false;
}
/**
* 从链表的特定位置移除一项
* @param {Number} index 需要移除链表项的位置
*/
removeAt(index) {
// 检查越界值(检查输入的移除元素位置是否有效,参考数组)
if (index >= 0 && index < this.length) {
let current = this.head; // 对链表当前元素的引用,默认为链表第一个元素
/**
* 从链表中移除第一个元素
*/
if (index === 0) { //如果移除的链表元素是第一个
// 如果链表长度为1
if (this.size() === 1) {
this.head = undefined; // 直接清空链表头(和 LinkedList 类中的实现一样)
} else {
// 从链表中移除某一项或者最后一项(新增)
const removed = this.head; // 首先保存现在的 head 元素的引用
current = this.getElementAt(this.size() - 1); // 要获得循环链表最后一个元素的引用
this.head = this.head.next; // 更新 head element,将其指向第二个元素(head.next)
current.next = this.head; // 将最后一个 element(current.next)指向新的 head
current = removed; // 更新current变量的引用为removed(用于return currentl.element)
}
} else {
/**
* 从链表中移除某一项或者最后一项
*/
// no need to update last element for circular list
const previous = this.getElementAt(index - 1); // 对链表当前元素的前一个元素的引用
/**
* 如果是移除最后一项,此时current.next的指向值是null,把previous.next设置为null,则等于将最后一项移除
* 如果是中间元素的引用,此时current.next的指向值是下一个链表元素的引用,把previous.next设置为current.next,则等于移除了当前元素
*/
// 将previous与current的下一项链接起来:跳过current,从而移除它
current = previous.next;
previous.next = current.next;
}
// 减少链表的长度
this.length--; // 更新链表的长度
return current.element; // 移除成功,返回被移除的元素
}
// 不是有效的位置,返回undefined(即没有从链表中移除元素)
return undefined;
}

}

let list = new CircularLinkedList();
list.push(13);
console.log(list);

有序链表

概念

有序链表是指保持元素有序的链表结构。除了使用排序算法之外,我们还可以将元素插入到正确的位置来保证链表的有序性。

在有序链表中,数据是按照关键值有序排列的。一般在大多数需要使用有序数组的场合也可以使用有序链表。有序链表优于有序数组的地方是插入的速度(因为元素不需要移动),另外链表可以扩展到全部有效的使用内存,而数组只能局限于一个固定的大小中。

(下面的代码例子为一个从头节点到尾节点有序递减的有序链表)

优势

  1. 相比数组,可以扩展到全部有效的使用内存,而数组在大多数编程语言中只能局限于一个固定的大小中(PS:JavaScript的数组不属于固定大小的数组)
  2. 有序链表优于有序数组的插入速度

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { LinkedList } from '/LinkedList/app.js';
/**
* 有序链表 继承自 LinkedList 类
* 有序链表是指保持元素有序的链表结构。除了使用排序算法之外,我们还可以将元素插入到正确的位置来保证链表的有序性。
* 这里假设是一个从头节点到尾节点有序递减的有序链表
*/
// 比较用的常量(保证代码优雅)
export const Compare = {
LESS_THAN: -1, // 如果第一个元素小于第二个元素,它就返回-1
BIGGER_THAN: 1, // 如果第一个元素大于第二个元素,它就返回1
EQUALS: 0 // 如果元素有相同的引用,它就返回 0
};
// 比较方法
export function defaultCompare(a, b) {
// 如果元素有相同的引用,它就返回 0
if (a === b) {
return Compare.EQUALS;
}
// 如果第一个元素小于第二个元素,它就返回-1,否之返回1
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}

export class SortedLinkedList extends LinkedList {
constructor() {
this.compareFn = compareFn;
}
/**
* 向有序链表尾部添加一个新的项
* @param {*} element 需要添加的项
*/
push(element) {
// 如果如果有序链表为空
if (this.isEmpty()) {
// 直接调用父类的push方法上传新的项
super.push(element);
} else {
// 如果有序链表不为空
const index = this.getIndexNextSortedElement(element); //获取插入元素的正确位置
super.insert(element, index); // 调用 LinkedList的 insert 方法,传入该位置来保证链表有序
}
}

insert(element, index = 0) {
if (this.isEmpty()) {
return super.insert(element, index === 0 ? index : 0);
}
const pos = this.getIndexNextSortedElement(element);
return super.insert(element, pos);
}
/**
* 获得插入元素的正确位置
* @param {*} element 需要查询插入元素位置的项
*/
getIndexNextSortedElement(element) {
let current = this.head; //用current保存链表头的引用
// 循环整个有序链表
let i = 0;
for (; i < this.size() && current; i++) {
/**
* 比较传入的元素
* @param {*} element 需要查询插入元素位置的项
* @param {*} current.element 当前循环项的元素内容
*/
const comp = this.compareFn(element, current.element);
// 当要插入有序链表的元素小于 current 的元素时,就找到了插入元素的位置
if (comp === Compare.LESS_THAN) {
// 返回找到的下标
return i;
}
// 否组继续循环迭代
current = current.next;
}
// 如果还是没有,则返回0
return i;
}

}

let list = new SortedLinkedList();
list.push(13);
console.log(list);

链表
https://sothx.com/2021/03/24/LinkedList/
作者
Sothx
发布于
2021年3月24日
许可协议