1. 链表介绍
- 链表存储有序的元素集合
- 链表中的元素在内存中并不连续放置
- 每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成
与数组相比:
- 链表在添加或移除时不需要移动其他元素
- 链表使用需要指针
- 在数组中可以直接访问任何位置的任何元素,而链表中想访问链表中间的任一元素,需要从表头开始迭代链表直到找到所需的元素
2. 单链表
2.1 创建链表
// 作为默认的相等性比较函数
function defaultEquals(a, b) {
return a === b;
}
// 助手类,表示链表中的第一个及其他元素
class Node {
constructor(element, next) {
// 要添加到链表中的项
this.element = element;
// 指向链表中下一个元素的指针
this.next = next;
}
}
class LinkedList {
// 可以自行传入用于比较两个 JavaScript 对象或值是否相等的自定义函数。
// 如果没有传入这个自定义函数,该数据结构将使用defaultEquals函数作为默认的相等性比较函数
constructor(equalsFn = defaultEquals) {
// count属性,用来存储链表中的元素数量
this.count = 0;
// 保存第一个元素的引用
this.head = undefined;
// 要比较链表中元素是否相等,需要使用一个内部调用的函数 equalsFn
this.equalsFn = equalsFn;
}
}
LinkedList类的方法:
- push(element):向链表尾部添加一个新元素
- insert(element, position):向链表的特定位置插入一个新元素
- getElementAt(index):返回链表中特定位置的元素。如果链表中不存在这样一个元素,返回undefined
- remove(element):从链表种移除一个元素
- indexOf(element):返回元素在链表中的索引。如果链表种没有该元素就返回-1
- removeAt(position):从链表的特定位置移除一个元素
- isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0,返回false
- size():返回链表包含的元素个数,与数组的length属性类似
- toString():返回整个链表的字符串。由于列表项使用了Node类,需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值
2.2 向链表尾部添加元素
两种场景:
- 链表为空,添加第一个元素
- 链表不为空,向其追加元素
push(element) {
// 把element作为值传入,创建Node项
const node = new Node(element);
let current;
// 向空数组添加一个元素
if (this.head == null) {
// 让head指针指向node元素
this.head = node;
} else { // 追加元素
// 只有第一个元素的引用
current = this.head;
// 获取最后一项
while (current.next != null) {
current = current.next;
}
// 将其next赋为新元素,建立链接
current.next = node;
}
// 递增链表长度
this.count++;
}
链表最后一个节点的下一个元素始终是undefined或null。
2.3 从链表中移除元素
从特定位置移除一个元素(removeAt)。存在两个场景:
- 移除第一个元素
- 移除除第一个元素之外的其他元素
removeAt(index) {
// 检查越界值
if (index >= 0 && index < this.count) { // 需要得到移除的index的位置,先验证该index是有效的
// 用current创建一个对链表中的第一个元素的引用
let current = this.head;
// 移除第一项
if (index === 0) {
// 移除第一项,就是让head指向列表的第二个元素
this.head = current.next;
} else { // 移除链表中最后一个或中间某个元素
// 对当前元素的前一个元素的引用
let previous;
// 迭代链表的节点,直到达到目标位置
for (let i = 0; i < index; i++) {
previous = current;
// 对循环列表当前元素的引用
current = current.next;
}
// 将previous与current的下一项链接起来:跳过current,从而移除它。当前节点会被丢弃在计算机内存内,等着被垃圾回收器清除
previous.next = current.next;
}
this.count--;
return current.element;
}
return undefined;
}
2.4 循环迭代链表到目标位置
getElementAt(index) {
if (index >= 0 && index <= this.count) {
// 初始化node变量,从链表的第一个元素head开始
let node = this.head;
for (let i = 0; i < index && node != null; i++) {
node = node.next;
}
return node;
}
return undefined;
}
- 重构removeAt方法
使用getElementAt方法重构remove方法来移除元素。
remove(index) {
if (index >= 0 && index < this.count) {
let current = this.head;
if (index === 0) {
this.head = current.next;
} else {
// 用getElementAt重构remove方法
const previous = this.getElementAt(index - 1);
current = previous.next;
previous.next = current.next;
}
this.count--;
return current.element;
}
return undefined;
}
2.5 在任意位置插入元素
- 在链表第一个位置添加元素:
current元素是对链表中第一个元素的引用。将node.next的值设为current(链表中第一个元素),此时head和node.next都指向current,接下去将head的引用改为node,添加成功。 - 在链表最后一个位置添加元素:
previous是对最后一个元素的引用,current将为undefined。此时node.next指向current,previous.next指向node,添加成功。 - 在链表中间添加元素:
将新元素node插入previous和current之间,先将node.next的值指向current,再把previous.next的值设为node,添加成功。
insert(element, index) {
if (index >= 0 && index <= this.count) {
const node = new Node(element);
// 在第一个位置添加
if (index === 0) {
// 对第一个元素的引用
const current = this.head;
node.next = current;
this.head = node;
} else {
// 迭代数组找到目标位置
const previous = this.getElementAt(index - 1);
const current = previous.next;
node.next = current;
previous.next = node;
}
this.count++;
return true;
}
// 越界了就返回false
return false;
}
2.6 返回一个元素的位置
indexOf()方法接收一个元素的值,日过在链表中找到了它,就返回元素的位置,否则返回-1。
// 返回一个元素的位置
indexOf(element) {
let current = this.head;
for (let i = 0; i < this.count && current != null; i++) {
// 验证current节点的元素和目标元素是否相等
if (this.equalsFn(element, current.element)) {
// 如果当前元素是要寻找的值,返回它的位置;否则迭代下一个节点
return i;
}
current = current.next;
}
return -1;
}
2.7 从链表中移除元素
// 移除元素
remove(element) {
const index = this.indexOf(element);
return this.removeAt(index);
}
2.8 判空、链表元素个数、获取第一个元素
// 返回链表的元素个数
size() {
return this.count;
}
// 链表中没有元素返回true,否则返回false
isEmpty() {
return this.size() === 0;
}
// 获取类的第一个元素
getHead() {
return this.head;
}
2.9 将LinkedList对象转换成一个字符串
toString() {
// 链表为空,返回一个空字符串
if (this.head == null) {
return '';
}
// 用链表第一个元素的值来初始化方法最后返回的字符串
let objString = `${this.head.element}`;
let current = this.head.next;
// 迭代其他元素,将元素值添加到字符串上
// 如果链表只有一个元素,current != null不会执行
for (let i = 1; i < this.size() && current != null; i++) {
objString = `${objString}, ${current.element}`;
current = current.next;
}
return objString;
}
3. 双向链表
3.1 创建链表
优势:单向链表中,如果迭代中错过了要找的元素,要回到起点重新开始迭代。
在单链表的基础上:
import LinkedList from '单链表文件路径';
export default class DoublyLinkedList extends LinkedList {
...
}
实现 DoublyLinkedList类:
// DoublyNode扩展了Node类,可以继承element和next属性
class DoublyNode extends Node {
constructor(element, next, prev) {
// 使用了继承所以需要在DoublyNode类的构造函数中调用Node的构造函数
super(element, next);
this.prev = prev; // 新增
}
}
// 扩展LinkedList类
class DoublyLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
// 调用LinkedList的构造函数,他会初始化equalsFn、const和head属性
super(equalsFn);
// 保存对链表最后一个元素的引用
this.tail = undefined; // 新增
}
}
3.2 在任意位置插入新元素
区别:单向链表只要控制一个next指针,双向链表要同时控制next和prev两个指针。
insert(element, index) {
if (index >= 0 && index <= this.count) {
const node = new DoublyNode(element);
let current = this.head;
// 场景一:在双向链表的第一个位置插入一个新元素
if (index === 0) {
// 如果双向链表为空,将head和tail指向新节点
if (this.head == null) { // 新增
this.head = node;
thia.tail = node;
} else { // 不为空
node.next = this.head;
current.prev = node; // 新增
this.head = node;
}
}
// 场景二:在双向链表最后添加新元素
else if (index === this.count) { // 最后一项,新增
current = this.tail;
current.next = node;
node.prev = current;
this.tail = node;
}
// 场景三:在双向链表中间插入新元素
else {
// 迭代链表,找到位置
const previous = this.getElementAt(index - 1);
current = previous.next;
node.next = current;
previous.next = node;
current.prev = node; // 新增
node.prev = previous; // 新增
}
this.count++;
return true;
}
return false;
}
对insert和remove两个方法的改进:
在在结果为否的情况下,可以把元素插入双向链表的尾部。
如position大于length/2的时候,可以从尾部开始迭代。
3.3 从任意位置移除元素
removeAt(index) {
if (index >= 0 && index < this.count) {
let current = this.head;
// 移除第一个元素
if (index === 0) {
this.head = current.next;
// 如果只有一项,更新tail 新增
if (this.count === 1) {
this.tail = undefined;
} else {
this.head.prev = undefined;
}
}
// 移除最后一个元素
else if (index === this.count - 1) {
current = this.tail;
this.tail = current.prev;
this.tail.next = undefined;
}
// 移除中间的某个元素
else {
// 迭代得到元素位置
current = this.getElementAt(index);
const previous = current.prev;
// 将previous和current的下一项连接起来,跳过current
previous.next = current.next;
current.next.prev = previous; // 新增
}
this.count--;
return current.element;
}
return undefined;
}
4. 循环链表
循环链表可以只有单向引用,也可以有双向引用。
最后一个元素指向下一个元素的指针不是undefined,是第一个元素head。
双向循环链表有指向head元素的tail.next和指向tail的head.prev。
4.1 创建CircularLinkedList类
// 不需要额外属性,直接扩展LinkedList类并覆盖需要改写的方法即可
class CircularLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn);
}
}
4.2 在任意位置插入元素
insert(element, index) {
if (index >= 0 && index <= this.count) {
const node = new Node(element);
let current = this.head;
// 场景一:在循环链表第一个位置插入新元素
if (index === 0) {
// 链表为空
if (this.head == null) {
// 将head赋值为新创建的元素
this.head = node;
// 将最后一个节点链接到head
node.next = this.head; // 新增
}
// 链表不为空
else {
node.next = current;
current = this.getElementAt(this.size());
// 更新最后一个元素
this.head = node;
current.next = this.head; // 新增
}
}
// 场景二:在链表中间插入元素
else { // 这种场景没有变化
const previous = this.getElementAt(index - 1);
node.next = previous.next;
previous.next = node;
}
this.count--;
return true;
}
return false;
}
4.3 从任意位置移除元素
从循环链表中移除元素,只需要考虑修改循环链表的head元素的情况。
removeAt(index) {
if (index >= 0 && index < this.count) {
let current = this.head;
if (index === 0) {
if (this.size() === 1) {
this.head = undefined;
} else {
const removed = this.head;
current = this.getElementAt(this.size()); // 新增
this.head = this.head.next;
current.next = this.head;
current = removed;
}
} else {
// 不需要修改循环链表最后一个元素
const previous = this.getElementAt(index - 1);
current = previous.next;
previous.next = current.next;
}
this.count--;
return current.element;
}
return undefined;
}
5. 有序链表
有序链表是保持元素有序的链表结构。
将元素插入到正确的位置来保证链表的有序性。
5.1 创建SortedLinkedList类
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1
};
function defaultCompare(a, b) {
// 如果元素有相同的引用,就返回0
if (a === b) {
return 0;
}
// 如果第一个元素小于第二个元素,返回-1,否则返回1
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
class SortedLinkedList extends LinkedList {
constructor(enqualsFn = defaultEquals, compareFn = defaultCompare) {
super(enqualsFn);
// 该类需要一个用来比较元素的函数,所以需要声明compareFn
this.compareFn = compareFn;
}
}
如果用于比较的元素更加复杂,可以创建自定义的比较函数并将它传入SortedLinkedList类的构造函数中。
5.2 有序插入元素
覆盖insert方法:
// 不允许在任意位置插入元素,所以要给index参数设置一个默认值,以便直接调用list.insert(element)而无需传入参数
insert(element, index = 0) {
if (this.isEmpty()) {
return super.insert(element, 0);
}
// 链表不为空,会知道插入元素的正确位置,并调用LinkedList的insert方法,传入该位置保证链表的有序
const pos = this.getIndexNextSortedElement(element);
return super.insert(element, pos);
}
getIndexNextSortedElement(element) {
let current = this.head;
let i = 0;
for (; i < this.size(); i++) {
// 传入比较的数组
const comp = this.compareFn(element, current.element);
// 插入的元素小于current的元素时,就找到了插入的位置
if (comp === Compare.LESS_THAN) {
return i;
}
current = current.next;
}
// 返回有序链表的长度
return i;
}
6. 创建StackLinkedList类
使用LinkedList类及其变种作为内部的数据结构来创建其他数据结构,如栈、队列、双向队列。
创建栈数据结构:
class StackLinkedList {
constructor() {
// 使用DoublyLinkedList来存储数据
this.items = new DoublyLinkedList();
}
push(element) {
this.items.push(element);
}
pop() {
if (this.isEmpty()) {
return undefined;
}
return this.items.removeAt(this.size() - 1);
}
}