这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战
链表
链表存储有序的元素集合,但是不同于数组,链表中的元素在内存中并不是连续放置的,每个元素由一个存储元素本身的节点和一个指向下一个元素的引用组成。他相比于数组的好处在于添加或删除元素的时候不需要移动其他元素;坏处在于要查找一个元素需要从头遍历查找,而不是像数组一样直接访问元素。在本文我们会依次介绍链表、双向链表、循环链表和有序链表等,比依次进行简单的分析和实现。
创建链表
理解什么是链表后,我们来想想如何来实现我们想要的数据结构,定义一个类LinkedList,他应该有哪些属性和方法呢?由上可知,这个数据结构是动态的,首先我们需要head属性来保存第一个元素的引用,每个元素存储的是元素本身和下一个元素的引用,因此我们需要一个类Node来存储这两个信息,分别命名为element、next,链表中存的就是Node 节点;除此之外,我们还需要count属性来存储链表的长度(元素个数),以及在查找链表元素时,需要传入一个enqualsFn函数来比较两个元素是否相等,可以用户自定义。那么一个链表还需要哪些方法呢?
- push(element): 像链表尾部添加一个新元素
- insert(element, position): 像链表特定位置添加一个新元素
- getElementAt(index): 返回链表特定位置的元素,不存在则返回undefined
- remove(element): 从链表中移出一个元素
- indexOf(element): 返回元素在链表中的索引
- removeAt(position): 从链表的特定位置中移出一个元素
- isEmpty(): 链表是否为空
- size(): 返回链表包含元素个数
- toString(): 返回表示整个链表的字符串
看起来有点多,我们可以一个一个来实现,可以先分析一下怎么下手?从最开始的分析来看,我们需要实现一个Node类,来存储每个节点的元素信息,以及我们要实现的LinkedList类,如下:
const defaultEnquals = (a, b) => a === b;
class Node {
constructor(element) {
this.element = element;
this.next = null;
}
}
class LinkedList {
constructor(enqualsFn = defaultEnquals) {
this._count = 0; // 存储链表元素个数
this.head = null; // 头指针,只想第一个元素(Node节点)
this.enqualsFn = enqualsFn; // 用于比较两个元素是否相等
}
}
接下来我们该实现上面列着的方法了。
- 实现
push(element)方法,像链表尾部添加元素。在添加元素进链表时有两种情况,一是链表为空时,我们可以直接将head元素指向node元素;二是链表不为空时,迭代至最后一个元素让最后一个元素指向想要添加进来的节点。
push(element) {
// 1. this.head 是否为空? 为空则直接添加,因为Node.next === null
// 2. 不为空?迭代至最后一个元素,将其next指向新元素
const node = new Node(element);
if (this.head == null) {
this.head = node;
} else {
let current = this.head;
while (current.next != null) {
current = current.next;
}
current.next = node;
}
this._count++;
}
removeAt(position)函数从链表的特定位置中移出一个元素。在删除元素时也分为两种情况,以链表1 => 2 => 3 => 4为例,第一种情况:删除第零个位置(第一项)怎么办?直接将head指向head.next,即将head指向 2;第二种情况:想要移除第二个位置的项(3)怎么办?迭代到第二个位置(current),将current的前一项previous的next指向current的next,即2 => 4,达到了删除3的目的
removeAt(position) {
// 不合法的索引,直接返回 undefin
if (position < 0 || position >= this._count) return undefined;
// 合法的,根据上面两种情况进行处理
let current = this.head;
// 第一种情况
if (position === 0) {
this.head = current.next;
} else {
// 第二种情况
let previous; // 上一个元素索引
for (let i = 0; i < position; i++) {
previous = current
current = current.next
}
previous.next = current.next;
}
this._count--;
return current.element;
}
getElementAt(index)函数返回链表特定位置的元素,不存在则返回undefined。
getElementAt(index) {
// 不合法的索引,直接返回 undefin
if (index < 0 || index >= this._count) return undefined;
let current = this.head;
for (let i = 0; i < index && current != null; i++) {
current = current.next
}
return current;
}
实现了这个方法,我们可以把removeAt(position)方法进行重构了,大家可以思考一下。
insert(element, position)函数
// 像链表特定位置添加一个新元素
insert(element, position) {
// 不合法的索引,直接返回 undefined
if (position < 0 || position >= this._count) return undefined;
const node = new Node(element);
// 合法的
if (position === 0) {
const current = this.head;
node.next = current;
this.head = node;
} else {
const previous = this.getElementAt(position - 1);
const current = previous.next;
node.next = current;
previous.next = node;
}
this._count++;
return true
}
indexOf(element)函数
// 返回元素在链表中的索引
indexOf(element) {
let current = this.head;
for (let i = 0; i < this._count && current != null; i++) {
if (this.enqualsFn(element, current.element)) {
return i
}
current = current.next;
}
return -1;
}
remove(element)函数从链表中移出一个元素
// 从链表中移出一个元素
remove(element) {
const index = this.indexOf(element);
return this.removeAt(index);
}
isEmpty()、size()
isEmpty() {
return this.size() === 0
}
size() {
return this._count;
}
toString()函数返回表示整个链表的字符串
// 返回表示整个链表的字符串
toString() {
if (this.head == null) return ''
let str = `${this.head.element}`;
let current = this.head.next;
for (let i = 1; i < this._count && current != null; i++) {
str = `${str},${current.element}`;
current = current.next;
}
return str;
}
以上就实现了我们一个基本的链表的功能,在接下来我们来了解其他类型的链表。
双向链表
双向链表和普通链表的区别在于,在双向链表中,链接是双向的,每一个节点有一个属性next指向下一个元素,prev属性指向上一个元素,并且链表不仅只有头指针head,还有尾指针tail,有上面得知我们的类应该新增几个属性,并且重写几个方法,具体如下,可以见代码。
class DoublyNode extends Node {
constructor(element, next, prev) {
super(element, next);
this.prev = prev;
}
}
class DoublyLinkedList extends LinkedList {
constructor(enqualsFn = defaultEnquals) {
super(enqualsFn);
this.tail = null;
}
// 在任意位置插入元素
insert(element, index) {
if (index < 0 || index >= this._count) return false;
const node = new DoublyNode(element);
// 在头部添加元素
if (index === 0) {
// 链表为空时,直接指向第一个节点
if (this.head == null) {
this.head = node;
this.tail = node;
} else {
const current = this.head;
node.next = current;
current.prev = node;
this.head = node;
}
} else if (index === this._count) {
// 在尾部添加元素
const current = this.tail;
current.next = node;
node.prev = current;
this.tail = node;
} else {
const previous = this.getElementAt(position - 1);
const current = previous.next;
node.next = current;
current.prev = node;
previous.next = node;
node.prev = previous;
}
this._count++;
return true;
}
removeAt(index) {
if (index < 0 || index >= this._count) return undefined;
// 合法的,根据上面两种情况进行处理
let current = this.head;
if (index === 0) {
this.head = current.next;
if (this._count === 1) {
this.tail = null
} else {
this.head.prev = null;
}
} else if (index === this._count - 1) {
current = this.tail;
const prev = current.prev;
prev.next = null;
this.tail = prev;
} else {
const previous = this.getElementAt(position - 1);
const current = previous.next;
previous.next = current.next;
current.next.prev = previous;
}
this._count--;
return current.element;
}
}
循环链表
循环链表可以像链表一样单向引用,也可以像双向链表一样双向引用,循环链表区别在于最后一个元素的next不是null,而是指向第一个元素。双向循环链表就是最后一个的next指向第一个,第一个的prev指向最后一个元素。具体实现如下。
class CirlLinkedList extends LinkedList {
constructor(enqualsFn = defaultEnquals) {
super(enqualsFn);
}
// 在任意位置插入元素
insert(element, index) {
if (index < 0 || index >= this._count) return false;
const node = new Node(element);
if (index === 0) {
if (this.head == null) {
this.head = node;
node.next = this.head;
} else {
const current = this.head;
node.next = current;
this.head = node;
const tail = this.getElementAt(this.size());
tail.next = this.head;
}
} else {
const previous = this.getElementAt(position - 1);
const current = previous.next;
node.next = current;
previous.next = node;
}
this._count++;
return true;
}
removeAt(index) {
if (index < 0 || index >= this._count) return undefined;
// 合法的,根据上面两种情况进行处理
let current = this.head;
if (index === 0) {
if (this._count === 1) {
this.head = null
} else {
const tail = this.getElementAt(this.size());
this.head = current.next;
tail.next = this.head;
}
} else {
const previous = this.getElementAt(index - 1);
const current = previous.next;
previous.next = current.next;
}
this._count--;
return current.element;
}
}
有序链表
有序链表是指保持元素有序的链表结构,将元素插入正确的位置来使链表保持有序,所以我们需要一个比较函数compareFn用来进行比较。他将继承LinkedList类,并重写insert方法,其他是一样的。