Skip to main content

链表的定义及实现

1. 链表的定义

1.1 什么是链表

链表是由一组元素组成的集合,每个元素都使用一个对象的引用指向它的后继,其中指向另一个元素的引用叫做链。

1.2 链表的组成

链表每个元素(即节点)包含:

  • 存储的数据(任何有效数据类型)
  • 指向下一个节点的链

链表最前面有一个头节点,链表的尾元素指向一个 null 节点。

1.3 链表与数组的区别

JS 中数组被实现成了对象,且元素存储在特定的位置中,效率较低;而链表中的元素在内存中不是连续放置的,每个与元素由一个存储自身的节点和一个指向下一个元素的引用组成。因此,与数组相比,链表的一个好处在于添加或移除元素时不需要移动其他元素;

然而链表需要使用指针,不像数组可以直接访问任何位置的元素,如果想访问链表中间的一个元素,需要从头开始访问,无法跳过第一个元素去访问任何一个元素。

2. 单向链表

2.1 什么是单向链表

单向链表是从头遍历到尾或者从尾遍历到头的链表:

2.2 单向链表的实现

下面是创建一个单向链表类 LinkedList:

class LinkedList {
// 初始链表长度为 0
length = 0;

// 初始 head 为 null,head 指向链表的第一个节点
head = null;

// 内部类(链表里的节点 Node)
Node = class {
data;
next = null;
constructor(data) {
this.data = data;
}
};
}

2.2.1 实现 append() 方法

向链表尾部添加一个新的项。

首先让 currentNode 指向第一个节点:

通过 while 循环使 currentNode 指向最后一个节点,最后通过 currentNode.next = newNode,让最后一个节点指向新节点 newNode

代码实现如下:

// append() 往链表尾部追加数据
append(data) {
// 1、创建新节点
const newNode = new this.Node(data);

// 2、追加新节点
if (this.length === 0) {

// 链表长度为 0 时,即只有 head 的时候
this.head = newNode;

} else {
// 链表长度大于 0 时,在最后面添加新节点
let currentNode = this.head;

// 当 currentNode.next 不为空时,
// 循序依次找最后一个节点,即节点的 next 为 null 时
while (currentNode.next !== null) {
currentNode = currentNode.next;
}

// 最后一个节点的 next 指向新节点
currentNode.next = newNode;
}

// 3、追加完新节点后,链表长度 + 1
this.length++;
}

代码测试:

const linkedList = new LinkedList();
// 测试 append 方法
linkedList.append("A");
linkedList.append("B");
linkedList.append("C");
console.log(linkedList);

2.2.2 实现 toString() 方法

由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。

代码实现:

toString() {
let currentNode = this.head;
let result = '';

// 遍历所有的节点,拼接为字符串,直到节点为 null
while (currentNode) {
result += currentNode.data + ' ';
currentNode = currentNode.next;
}

return result;
}

代码测试:

// 测试 toString 方法
console.log(linkedList.toString()); //--> AA BB CC

2.2.3 实现 insert() 方法

向链表的特定位置插入一个新的项。

代码实现:

// insert() 在指定位置(position)插入节点
insert(position, data) {
// position 新插入节点的位置
// position = 0 表示新插入后是第一个节点
// position = 1 表示新插入后是第二个节点,以此类推

// 1、对 position 进行越界判断,不能小于 0 或大于链表长度
if (position < 0 || position > this.length) return false;

// 2、创建新节点
const newNode = new this.Node(data);

// 3、插入节点
if (position === 0) { // position = 0 的情况
// 让新节点的 next 指向 原来的第一个节点,即 head
newNode.next = this.head;

// head 赋值为 newNode
this.head = newNode;
} else { // 0 < position <= length 的情况

// 初始化一些变量
let currentNode = this.head; // 当前节点初始化为 head
let previousNode = null; // head 的 上一节点为 null
let index = 0; // head 的 index 为 0

// 在 0 ~ position 之间遍历,不断地更新 currentNode 和 previousNode
// 直到找到要插入的位置
while (index++ < position) {
previousNode = currentNode;
currentNode = currentNode.next;
}

// 在当前节点和当前节点的上一节点之间插入新节点,即它们的改变指向
newNode.next = currentNode;
previousNode.next = newNode;
}

// 更新链表长度
this.length++;
return newNode;
}

代码测试:

// 测试 insert 方法
linkedList.insert(0, "123");
linkedList.insert(2, "456");
console.log(linkedList.toString()); //--> 123 AA 456 BB CC

2.2.4 实现 getData() 方法

获取指定位置(position)的 data。

代码实现:

getData(position) {
// 1、position 越界判断
if (position < 0 || position >= this.length) return null;

// 2、获取指定 position 节点的 data
let currentNode = this.head;
let index = 0;

while (index++ < position) {
currentNode = currentNode.next;
}
// 3、返回 data
return currentNode.data;
}

代码测试:

// 测试 getData 方法
console.log(linkedList.getData(0)); //--> 123
console.log(linkedList.getData(1)); //--> AA

2.2.5 实现 indexOf() 方法

indexOf(data) 返回指定 data 的 index,如果没有,返回 -1。

代码实现:

indexOf(data) {

let currentNode = this.head;
let index = 0;

while (currentNode) {
if (currentNode.data === data) {
return index;
}
currentNode = currentNode.next;
index++;
}

return -1;
}

代码测试:

// 测试 indexOf 方法
console.log(linkedList.indexOf("AA")); //--> 1
console.log(linkedList.indexOf("ABC")); //--> -1

2.2.6 实现 update() 方法

update(position, data) 修改指定位置节点的 data。

代码实现:

update(position, data) {
// 涉及到 position 都要进行越界判断
// 1、position 越界判断
if (position < 0 || position >= this.length) return false;

// 2、痛过循环遍历,找到指定 position 的节点
let currentNode = this.head;
let index = 0;
while (index++ < position) {
currentNode = currentNode.next;
}

// 3、修改节点 data
currentNode.data = data;

return currentNode;
}

代码测试:

// 测试 update 方法
linkedList.update(0, "12345");
console.log(linkedList.toString()); //--> 12345 AA 456 BB CC
linkedList.update(1, "54321");
console.log(linkedList.toString()); //--> 12345 54321 456 BB CC

2.2.7 实现 removeAt() 方法

removeAt(position) 删除指定位置的节点。

代码实现:

removeAt(position) {
// 1、position 越界判断
if (position < 0 || position >= this.length) return null;

// 2、删除指定 position 节点
let currentNode = this.head;
if (position === 0) {
// position = 0 的情况
this.head = this.head.next;

} else {
// position > 0 的情况
// 通过循环遍历,找到指定 position 的节点,赋值到 currentNode

let previousNode = null;
let index = 0;

while (index++ < position) {
previousNode = currentNode;
currentNode = currentNode.next;
}

// 巧妙之处,让上一节点的 next 指向到当前的节点的 next,相当于删除了当前节点。
previousNode.next = currentNode.next;
}

// 3、更新链表长度 -1
this.length--;

return currentNode;
}

代码测试:

// 测试 removeAt 方法
linkedList.removeAt(3);
console.log(linkedList.toString()); //--> 12345 54321 456 CC

2.2.8 实现 remove() 方法

remove(data) 删除指定 data 所在的节点。

代码实现:

remove(data) {
this.removeAt(this.indexOf(data));
}

代码测试:

// 测试 remove 方法
linkedList.remove("CC");
console.log(linkedList.toString()); //--> 12345 54321 456

2.2.9 实现 isEmpty() 方法

isEmpty() 判断链表是否为空。

代码实现:

isEmpty() {
return this.length === 0;
}

代码测试:

// 测试 isEmpty 方法
console.log(linkedList.isEmpty()); //--> false

2.2.10 实现 size() 方法

size() 获取链表的长度。

代码实现:

size() {
return this.length;
}

代码测试:

// 测试 size 方法
console.log(linkedList.size()); //--> 3

2.2.11 完整实现

class LinkedList {
// 初始链表长度为 0
length = 0;

// 初始 head 为 null,head 指向链表的第一个节点
head = null;

// 内部类(链表里的节点 Node)
Node = class {
data;
next = null;

constructor(data) {
this.data = data;
}
};

// ------------ 链表的常见操作 ------------ //

// append() 往链表尾部追加数据
append(data) {
// 1、创建新节点
const newNode = new this.Node(data);

// 2、追加新节点
if (this.length === 0) {
// 链表长度为 0 时,即只有 head 的时候
this.head = newNode;
} else {
// 链表长度大于 0 时,在最后面添加新节点
let currentNode = this.head;

// 当 currentNode.next 不为空时,
// 循序依次找最后一个节点,即节点的 next 为 null 时
while (currentNode.next !== null) {
currentNode = currentNode.next;
}

// 最后一个节点的 next 指向新节点
currentNode.next = newNode;
}

// 3、追加完新节点后,链表长度 + 1
this.length++;
}

// insert() 在指定位置(position)插入节点
insert(position, data) {
// position 新插入节点的位置
// position = 0 表示新插入后是第一个节点
// position = 1 表示新插入后是第二个节点,以此类推

// 1、对 position 进行越界判断,不能小于 0 或大于链表长度
if (position < 0 || position > this.length) return false;

// 2、创建新节点
const newNode = new this.Node(data);

// 3、插入节点
if (position === 0) {
// position = 0 的情况
// 让新节点的 next 指向 原来的第一个节点,即 head
newNode.next = this.head;

// head 赋值为 newNode
this.head = newNode;
} else {
// 0 < position <= length 的情况

// 初始化一些变量
let currentNode = this.head; // 当前节点初始化为 head
let previousNode = null; // head 的 上一节点为 null
let index = 0; // head 的 index 为 0

// 在 0 ~ position 之间遍历,不断地更新 currentNode 和 previousNode
// 直到找到要插入的位置
while (index++ < position) {
previousNode = currentNode;
currentNode = currentNode.next;
}

// 在当前节点和当前节点的上一节点之间插入新节点,即它们的改变指向
newNode.next = currentNode;
previousNode.next = newNode;
}

// 更新链表长度
this.length++;
return newNode;
}

// getData() 获取指定位置的 data
getData(position) {
// 1、position 越界判断
if (position < 0 || position >= this.length) return null;

// 2、获取指定 position 节点的 data
let currentNode = this.head;
let index = 0;

while (index++ < position) {
currentNode = currentNode.next;
}

// 3、返回 data
return currentNode.data;
}

// indexOf() 返回指定 data 的 index,如果没有,返回 -1。
indexOf(data) {
let currentNode = this.head;
let index = 0;

while (currentNode) {
if (currentNode.data === data) {
return index;
}
currentNode = currentNode.next;
index++;
}

return -1;
}

// update() 修改指定位置节点的 data
update(position, data) {
// 涉及到 position 都要进行越界判断
// 1、position 越界判断
if (position < 0 || position >= this.length) return false;

// 2、痛过循环遍历,找到指定 position 的节点
let currentNode = this.head;
let index = 0;
while (index++ < position) {
currentNode = currentNode.next;
}

// 3、修改节点 data
currentNode.data = data;

return currentNode;
}

// removeAt() 删除指定位置的节点
removeAt(position) {
// 1、position 越界判断
if (position < 0 || position >= this.length) return null;

// 2、删除指定 position 节点
let currentNode = this.head;
if (position === 0) {
// position = 0 的情况
this.head = this.head.next;
} else {
// position > 0 的情况
// 通过循环遍历,找到指定 position 的节点,赋值到 currentNode

let previousNode = null;
let index = 0;

while (index++ < position) {
previousNode = currentNode;
currentNode = currentNode.next;
}

// 巧妙之处,让上一节点的 next 指向到当前的节点的 next,相当于删除了当前节点。
previousNode.next = currentNode.next;
}

// 3、更新链表长度 -1
this.length--;

return currentNode;
}

// remove() 删除指定 data 的节点
remove(data) {
this.removeAt(this.indexOf(data));
}

// isEmpty() 判断链表是否为空
isEmpty() {
return this.length === 0;
}

// size() 获取链表的长度
size() {
return this.length;
}

// toString() 链表数据以字符串形式返回
toString() {
let currentNode = this.head;
let result = "";

// 遍历所有的节点,拼接为字符串,直到节点为 null
while (currentNode) {
result += currentNode.data + " ";
currentNode = currentNode.next;
}

return result;
}
}

3. 双向链表

3.1 什么是双向链表

双向链表是可以从头遍历到尾,也可以从尾遍历到头的链表:

3.2 双向链表的实现

创建双向链表类 DoublyLinkedList:

  • DoublyNode 类继承单向链表的 Node 类,新添加 this.prev 属性,该属性用于指向上一个节点。
  • DoublyLinkedList 类继承 LinkedList 类,新添加 this.tail 属性,该属性指向末尾的节点。
// 双向链表的节点类(继承单向链表的节点类)
class DoublyNode extends Node {
constructor(element) {
super(element);
this.prev = null;
}
}

// 双向链表类继承单向链表类
class DoublyLinkedList extends LinkedList {
constructor() {
super();
this.tail = null;
}
}

3.2.1 实现 append(element)

向链表尾部追加一个新元素。

// append(element) 往双向链表尾部追加一个新的元素
// 重写 append()
append(element) {

// 1、创建双向链表节点
const newNode = new DoublyNode(element);

// 2、追加元素
if (this.head === null) {
this.head = newNode;
this.tail = newNode;
} else {
// !!跟单向链表不同,不用通过循环找到最后一个节点
// 巧妙之处
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}

this.length++;
}

3.2.2 实现 insert(position, element)

向链表的指定位置插入一个新元素。

// insert(position, data) 插入元素
// 重写 insert()
insert(position, element) {
// 1、position 越界判断
if (position < 0 || position > this.length) return false;

// 2、创建新的双向链表节点
const newNode = new DoublyNode(element);

// 3、判断多种插入情况
if (position === 0) { // 在第 0 个位置插入

if (this.head === null) {
this.head = newNode;
this.tail = newNode;
} else {
//== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==//
newNode.next = this.head;
this.head.perv = newNode;
this.head = newNode;
}

} else if (position === this.length) { // 在最后一个位置插入

this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
} else { // 在 0 ~ this.length 位置中间插入

let targetIndex = 0;
let currentNode = this.head;
let previousNode = null;

// 找到要插入位置的节点
while (targetIndex++ < position) {
previousNode = currentNode;
currentNode = currentNode.next;
}

// 交换节点信息
previousNode.next = newNode;
newNode.prev = previousNode;

newNode.next = currentNode;
currentNode.prev = newNode;
}

this.length++;

return true;
}

3.2.3 实现 removeAt(position)

从链表删除指定的元素。

  // removeAt() 删除指定位置的节点
// 重写 removeAt()
removeAt(position) {
// 1、position 越界判断
if (position < 0 || position > this.length - 1) return null;

// 2、根据不同情况删除元素
let currentNode = this.head;
if (position === 0) { // 删除第一个节点的情况

if (this.length === 1) { // 链表内只有一个节点的情况
this.head = null;
this.tail = null;
} else { // 链表内有多个节点的情况
this.head = this.head.next;
this.head.prev = null;
}

} else if (position === this.length - 1) { // 删除最后一个节点的情况

currentNode = this.tail;
this.tail.prev.next = null;
this.tail = this.tail.prev;

} else { // 删除 0 ~ this.length - 1 里面节点的情况

let targetIndex = 0;
let previousNode = null;
while (targetIndex++ < position) {
previousNode = currentNode;
currentNode = currentNode.next;
}

previousNode.next = currentNode.next;
currentNode.next.perv = previousNode;

}

this.length--;
return currentNode.data;
}

3.2.4 实现 update(position, data)

修改指定位置上的元素。

  // update(position, data) 修改指定位置的节点
// 重写 update()
update(position, data) {
// 1、删除 position 位置的节点
const result = this.removeAt(position);

// 2、在 position 位置插入元素
this.insert(position, data);
return result;
}

3.2.5 实现 forwardToString()

返回正向遍历节点字符串形式。

// forwardToString() 链表数据从前往后以字符串形式返回
forwardToString() {
let currentNode = this.head;
let result = '';

// 遍历所有的节点,拼接为字符串,直到节点为 null
while (currentNode) {
result += currentNode.data + '--';
currentNode = currentNode.next;
}

return result;
}

3.2.6 实现 backwardString()

返回反向遍历的节点的字符串形式。

// backwardString() 链表数据从后往前以字符串形式返回
backwardString() {
let currentNode = this.tail;
let result = '';

// 遍历所有的节点,拼接为字符串,直到节点为 null
while (currentNode) {
result += currentNode.data + '--';
currentNode = currentNode.prev;
}

return result;
}

双向链表的其他方法通过继承单向链表来实现。

3.2.7 完整实现

class DoublyLinkedList extends LinkedList {
constructor() {
super();
this.tail = null;
}

// ------------ 链表的常见操作 ------------ //
// append(element) 往双向链表尾部追加一个新的元素
// 重写 append()
append(element) {
// 1、创建双向链表节点
const newNode = new DoublyNode(element);

// 2、追加元素
if (this.head === null) {
this.head = newNode;
this.tail = newNode;
} else {
// !!跟单向链表不同,不用通过循环找到最后一个节点
// 巧妙之处
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}

this.length++;
}

// insert(position, data) 插入元素
// 重写 insert()
insert(position, element) {
// 1、position 越界判断
if (position < 0 || position > this.length) return false;

// 2、创建新的双向链表节点
const newNode = new DoublyNode(element);

// 3、判断多种插入情况
if (position === 0) {
// 在第 0 个位置插入

if (this.head === null) {
this.head = newNode;
this.tail = newNode;
} else {
//== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==//
newNode.next = this.head;
this.head.perv = newNode;
this.head = newNode;
}
} else if (position === this.length) {
// 在最后一个位置插入

this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
} else {
// 在 0 ~ this.length 位置中间插入

let targetIndex = 0;
let currentNode = this.head;
let previousNode = null;

// 找到要插入位置的节点
while (targetIndex++ < position) {
previousNode = currentNode;
currentNode = currentNode.next;
}

// 交换节点信息
previousNode.next = newNode;
newNode.prev = previousNode;

newNode.next = currentNode;
currentNode.prev = newNode;
}

this.length++;

return true;
}

// getData() 继承单向链表
getData(position) {
return super.getData(position);
}

// indexOf() 继承单向链表
indexOf(data) {
return super.indexOf(data);
}

// removeAt() 删除指定位置的节点
// 重写 removeAt()
removeAt(position) {
// 1、position 越界判断
if (position < 0 || position > this.length - 1) return null;

// 2、根据不同情况删除元素
let currentNode = this.head;
if (position === 0) {
// 删除第一个节点的情况

if (this.length === 1) {
// 链表内只有一个节点的情况
this.head = null;
this.tail = null;
} else {
// 链表内有多个节点的情况
this.head = this.head.next;
this.head.prev = null;
}
} else if (position === this.length - 1) {
// 删除最后一个节点的情况

currentNode = this.tail;
this.tail.prev.next = null;
this.tail = this.tail.prev;
} else {
// 删除 0 ~ this.length - 1 里面节点的情况

let targetIndex = 0;
let previousNode = null;
while (targetIndex++ < position) {
previousNode = currentNode;
currentNode = currentNode.next;
}

previousNode.next = currentNode.next;
currentNode.next.perv = previousNode;
}

this.length--;
return currentNode.data;
}

// update(position, data) 修改指定位置的节点
// 重写 update()
update(position, data) {
// 1、删除 position 位置的节点
const result = this.removeAt(position);

// 2、在 position 位置插入元素
this.insert(position, data);
return result;
}

// remove(data) 删除指定 data 所在的节点(继承单向链表)
remove(data) {
return super.remove(data);
}

// isEmpty() 判断链表是否为空
isEmpty() {
return super.isEmpty();
}

// size() 获取链表的长度
size() {
return super.size();
}

// forwardToString() 链表数据从前往后以字符串形式返回
forwardToString() {
let currentNode = this.head;
let result = "";

// 遍历所有的节点,拼接为字符串,直到节点为 null
while (currentNode) {
result += currentNode.data + "--";
currentNode = currentNode.next;
}

return result;
}

// backwardString() 链表数据从后往前以字符串形式返回
backwardString() {
let currentNode = this.tail;
let result = "";

// 遍历所有的节点,拼接为字符串,直到节点为 null
while (currentNode) {
result += currentNode.data + "--";
currentNode = currentNode.prev;
}

return result;
}
}

代码测试:

const doublyLinkedList = new DoublyLinkedList();

// append() 测试
doublyLinkedList.append("ZZ");
doublyLinkedList.append("XX");
doublyLinkedList.append("CC");
console.log(doublyLinkedList);

// insert() 测试
doublyLinkedList.insert(0, "00");
doublyLinkedList.insert(2, "22");
console.log(doublyLinkedList);

// getData() 测试
console.log(doublyLinkedList.getData(1)); //--> ZZ

// indexOf() 测试
console.log(doublyLinkedList.indexOf("XX")); //--> 3
console.log(doublyLinkedList);

// removeAt() 测试
doublyLinkedList.removeAt(0);
doublyLinkedList.removeAt(1);
console.log(doublyLinkedList);

// update() 测试
doublyLinkedList.update(0, "111111");
console.log(doublyLinkedList);

// remove() 测试
console.log(doublyLinkedList.remove("111111"));
console.log(doublyLinkedList.remove("22222"));
console.log(doublyLinkedList);

// forwardToString() 测试
console.log(doublyLinkedList.forwardToString());

// backwardString() 测试
console.log(doublyLinkedList.backwardString());