链表和数组一样,可以用于存储一系列的元素,但是列表和数组的实现机制完全不同。
在学习链表之前我们先了解一下数组的不足(缺点):
- 要存储多个元素,数组可能是最常用的数据结构。几乎每一种编程语言都有默认实现的数组结构,数组的创建通常需要申请一段连续的内存空间,并且大小是固定的(大多数语言是固定的),所以当数组不能满足容量需求时,需要扩容,一般情况下是申请一个更大的数组,将原来数组中元素复制过去。而且数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移。尽管JavaScript中的Array类可以帮助我们做这些事,但背后的原理依然事这样的。
相对于数组链表的优势:
-
链表的内存空间不是必须连续的,可以充分利用计算机的内存,实习灵活的内存动态管理,链表不必在创建的时候确定大小,并且大小可以无限制的延申下去,链表在插入和删除数据时,时间复杂度可达到O(1),相对于数组效率会高很多。
-
但是相对于数组,列表也有一些缺点:链表访问任何一个位置的元素时,都需要从头开始访问,无法跳过第一个元素访问任何一个元素,无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素。
什么是链表
在上面已经简单提及链表的结构,我们用图的方式在清晰的了解一下什么是链表结构。
如上图所示,在链表中,每一个元素都包含两个属性,即 该元素的值item 和 下一个元素next,next指向下一个节点。同时链表中有一个指针 head 指向链表第一个元素,最后一个元素则指向null。
链表中的常见操作
- 向链表尾部添加一个新的项。append(element)
- 向链表的特定位置插入一个新的项。insert(position,element)
- 获取对应位置的元素。get(position)
- 返回元素在链表中的索引。如果链表中没有该元素则返回-1。indexOf(element)
- 修改某个位置的元素。update(position ,element)
- 从列表的特定位置移除一项。removeAt(position)
- 从链表中移除一项。remove(element)
- 如果链表中不包含任何元素,返回true,如果链表的长度大于0则返回false。isEmpty()
- 返回链表包含的元素个数。与数组length属性类似。size()
封装单向链表结构
class Node {
constructor(element) {
// 保存元素
this.element = element;
// 指向下一个元素
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.length = 0;
}
// - 向链表尾部添加一个新的项。append(element)
append(element) {
// 根据element创建Node对象
const newNode = new Node(element);
// 追加到最后 判断链表结构head是否为空(为空就表示链表中没有任何节点),将新创建的节点赋值给head,此时表示该链表中已有节点
if (!this.head) {
this.head = newNode;
}
// 如果head不为空(表示链表中已有节点),最开始定义一个变量将头节点(head)赋值给current(也就是指向第一个节点),
//利用while循环依次判断是否存在下一个节点,如果不存在,就将这个新创建的额节点赋值给最后一个节点的next属性
else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.length++;
}
// - 向链表的特定位置插入一个新的项。insert(position,element) position表示插入元素的位置
insert(position, element) {
// 判断输入的值是否越界
if (position < 0 || position > this.length) return false;
// 创建新的节点
const newNode = new Node(element);
// 插入元素
// 判断插入的元素的位置是否在头节点(head)
if (position === 0) {
// 插入的节点在第一个就将此时指向头节点的节点赋值给插入节点的next属性
newNode.next = this.head;
//之后再将新插入的节点赋值给头节点
this.head = newNode;
}
// 在其他位置插入节点 ,需要将插入位置的新节点前一个节点指向该新节点,再将新节点的next属性指向后一个节点
else {
let index = 0;
let current = this.head;
// 插入节点的前一个位置
let previous = null;
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = newNode;
newNode.next = current;
}
this.length++;
// 表示插入成功
return true;
}
// - 获取对应位置的元素。get(position)
get(position) {
if (position < 0 || position > this.length - 1) return null;
// 查找指定位置的元素时,需要一个一个的往后找,直到找到指定位置将此位置的节点next属性赋值给变量current
let index = 0;
let current = this.head;
while (index++ < position) {
current = current.next;
}
return current.element;
}
// - 返回元素在链表中的索引。如果链表中没有该元素则返回-1。indexOf(element)
indexOf(element) {
let current = this.head;
let index = 0;
while (current) {
if (current.element === element) {
return index;
}
index++;
current = current.next;
}
return -1;
}
// - 修改某个位置的元素。update(position ,element)
update(position, element) {
let result = this.removeAt(position);
this.insert(position, element);
return result;
}
// - 从列表的特定位置移除一项。removeAt(position)
removeAt(position) {
if (position < 0 || position > this.length - 1) return null;
let current = this.head;
let previous = null;
let index = 0;
if (position === 0) {
this.head = current.next;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = current.next;
}
this.length--;
return current.element;
}
// - 从链表中移除一项。remove(element)
remove(element) {
const index = this.indexOf(element);
if (index === -1) return;
this.removeAt(index);
}
// - 如果链表中不包含任何元素,返回true,如果链表的长度大于0则返回false。isEmpty()
isEmpty() {
return this.length === 0;
}
// - 返回链表包含的元素个数。与数组length属性类似。size()
size() {
return this.length;
}
}
module.exports = new LinkedList();
测试
const linkedList = require("./linkdeList");
// 测试向链表添加节点
linkedList.append("a");
linkedList.append("b");
linkedList.append("c");
linkedList.append("d");
linkedList.append("e");
console.log(linkedList);
// 测试向指定位置插入节点
const res = linkedList.insert("1", "f");
console.log(linkedList, res);
// 测试获取指定位置的元素
console.log(linkedList.get(1));
// 测试通过元素获取下标值{}
console.log(linkedList.indexOf("b"));
// 测试从列表的特定位置移除一项
console.log(linkedList.removeAt(4));
console.log(linkedList);
// 测试修改某个位置的元素
console.log(linkedList.update(1, "www"));
console.log(linkedList);
// 测试从链表中移除一项
console.log(linkedList.remove("a"));
console.log(linkedList);
认识双向列表
- 为什么要使用双向链表
单向链表只能从头遍历到尾或者从尾遍历到头,也就是链表相连过程是 单向的,实现的原理是上一个节点中有一个指向下一个的引用。
单向链表有一个明显的缺点,我们可以轻松的拿到下一个节点,但是返回前一个节点是很难的(需要从头在开始遍历),但是在实际开发中,经常会遇到回到上一个节点的情况。所以双向链表可以解决单向链表很难拿到上一个节点的情况。
- 双向链表实现的原理
双向链表基于单向链表实现,其实就是在每个节点中添加一个指向上一个节点的引用(prev),第一个节点的prev指向的是null,最后一个节点的next指向的也是null(如下图),这样双向链表就可以有效的解决上面提出的问题。
但是双向链表也有一些缺点,每次在插入或者删除某个节点时,需要处理四个引用,而不是两个,实现起来也比较麻烦。并且相当于单向链表,必然占用的内存空间更大一些。
封装双向链表
根据双向链表的要求,我们只需要继承单向链表来重写添加,插入,根据下标删除三个方法
const { LinkedList, Node } = require("./linkdeList");
class DoublyNode extends Node {
constructor(element) {
super(element);
this.prev = null;
}
}
class DoublyLinkedList extends LinkedList {
constructor() {
super();
this.tail = null;
}
// - 向链表尾部添加一个新的项。append(element)
append(element) {
const newNode = new DoublyNode(element);
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,element)
insert(position, element) {
if (position < 0 || position > this.length) return false;
const newNode = new DoublyNode(element);
if (position === 0) {
if (this.head == null) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head.prev = newNode;
this.head = newNode;
}
} else if (position === this.length) {
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
} else {
let index = 0;
let current = this.head;
let previous = null;
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = newNode;
newNode.prev = previous;
newNode.next = current;
current.prev = newNode;
}
}
// - 获取对应位置的元素。get(position)
// - 返回元素在链表中的索引。如果链表中没有该元素则返回-1。indexOf(element)
// - 修改某个位置的元素。update(position ,element)
// - 从列表的特定位置移除一项。removeAt(position)
removeAt(position) {
if (position < 0 || position > this.length - 1) return null;
let current = 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) {
current = this.tail;
this.tail.prev.next = null;
this.tail = this.tail.prev;
} else {
let index = 0;
let previous = null;
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = current.next;
current.next.prev = previous;
}
return current.element;
}
// - 从链表中移除一项。remove(element)
// - 如果链表中不包含任何元素,返回true,如果链表的长度大于0则返回false。isEmpty()
// - 返回链表包含的元素个数。与数组length属性类似。size()
}
module.exports = DoublyLinkedList;
测试
const DoublyLinkedList = require("./doubly_linkedList");
const doublyLinkedList = new DoublyLinkedList();
doublyLinkedList.append("a");
doublyLinkedList.append("b");
doublyLinkedList.append("c");
doublyLinkedList.append("d");
doublyLinkedList.append("e");
doublyLinkedList.append("f");
console.log(doublyLinkedList);
doublyLinkedList.insert(1, "g");
console.log(doublyLinkedList);
console.log(doublyLinkedList.get(1));
console.log(doublyLinkedList.indexOf("g"));
console.log(doublyLinkedList.removeAt(1));
console.log(doublyLinkedList.update(0, "nnn"));
console.log(doublyLinkedList.remove("f"));