一、链表
要存储多个元素,数组(或列表)可能是最常用的数据结构。这种数据结构非常方便,提供了一个便利的[]语法来访问其元素。然而,这种数据结构有一个缺点:(在大多数语言中)数组的大小是固定的,从数组的起点或中间插入或移除项的成本很高,因为需要移动元素。(尽管我们已经学过,JavaScript 有来自 Array 类的方法可以帮我们做这些事,但背后的情况同样如此。)
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。在数组中,我们可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,则需要从起点(表头)开始迭代链表直到找到所需的元素。
现实中链表的例子:一列火车是由一系列车厢(也称车皮)组成的。每节车厢或车皮都相互连接。你很容易分离一节车皮,改变它的位置、添加或移除它。每节车皮都是链表的元素,车皮间的连接就是指针。
下面来实现一个链表:
/*
*为了复用,用Node类表示我们想要添加到链表中的项。
*它包含一个 element 属性,该属性表示要加入链表元素的值;以及一个 next 属性,该属性是指向链表中下一个元素的指针。
*/
class Node {
constructor(element) {
this.element = element;
this.next = undefined;
}
}
//默认比较函数
function defaultEquals(a, b) {
return a === b;
}
//链表类需要实现的一些方法
/*
*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 方法,让其只输出元素的值。
*/
class LinkedList {
constructor(equalsFn = defaultEquals) {
this.count = 0; // {2}
this.head = undefined; // {3}
this.equalsFn = equalsFn; // {4}
}
/*向 LinkedList 对象尾部添加一个元素时,可能有两种场景:链表为空,添加的是第一个
元素;链表不为空,向其追加元素。*/
push(element) {
let node = new Node(element);
if (this.count === 0) {
this.head = node
} else {
let current = this.head;
while(current.next != null) {
current = current.next
}
current.next = node
}
this.count++
}
/*和 push 方法一样,对于从链表中移除元素也存在两种场景:第一
种是移除第一个元素,第二种是移除第一个元素之外的其他元素。*/
removeAt(index) {
//移除index的边界限制
if (index >= 0 && index < this.count) {
let current = this.head;
//移除第一项的
if (index === 0) {
this.head = current.next;
} else {
let previous = this.getElementAt(index-1);
current = previous.next
// 将 previous 与 current 的下一项链接起来:跳过 current,从而移除它
previous.next = current.next;
}
this.count--
return current.element
}
return undefined
}
getElementAt(index) {
if (index >= 0 && index <= this.count) {
let node = this.head;
for (let i = 0; i < index && node != null; i++) {
node = node.next;
}
return node;
}
return undefined;
}
insert(element, index) {
//插入index边界限制
if(index>=0 && index <= this.count) {
let node = new Node(element);
let current;
//插入到第一位
if (index === 0) {
current = this.head;
node.next = current;
this.head = node;
} else {
let previous = this.getElementAt(index-1);
current = previous.next;
node.next = current;
previous.next = node
}
this.count++
return true
}
return false
}
indexOf(element) {
let current = this.head;
for(let i=0; i<this.count && current != null; i++) {
if (this.equalsFn(element, current.element)) {
return i
}
current = current.next
}
return -1
}
remove(element) {
let index = this.indexOf(element);
return this.removeAt(index)
}
toString() {
if (this.head == null) {
return ''
}
let current = this.head;
let str = current.element;
for(let i=1; i<this.count && current != null; i++) {
current = current.next;
str += `,${current.element}`
}
return str
}
}
二、双向链表:双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接;而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素。双向链表提供了两种迭代的方法:从头到尾,或者从尾到头。我们也可以访问一个特定节点的下一个或前一个元素。
//通过扩展前面的Node类和LinkedList 来实现双向链表
class DoublyNode extends Node {
constructor(element, next, prev) {
super(element, next);
this.prev = prev;
}
}
class DoublyLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn);
this.tail = undefined;
}
insert(element, index) {
//插入index边界限制
if(index>=0 && index <= this.count) {
let node = new DoublyNode(element);
let current = this.head;
//插入到第一位
if (index === 0) {
if (this.count === 0) {
//没有元素的时候插入头部
this.head = node
this.tail = node
} else {
current.prev = node;
node.next = current;
this.head = node;
}
} else if (index === this.count){
//插入到最后位置
current = this.tail;
current.next = node;
node.prev = current;
this.tail = node;
} else {
//插入到中间位置
let previous = this.getElementAt(index-1);
current = previous.next;
node.next = current;
node.pre = previous;
current.prev = node;
previous.next = node;
}
this.count++
return true
}
return false
}
removeAt(index) {
//移除index边界限制
if (index >= 0 && index < this.count) {
let current = this.head;
//移除第一项的
if (index === 0) {
this.head = current.next;
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 {
//移除中间项时
let previous = this.getElementAt(index-1);
current = previous.next;
previous.next = current.next;
current.next.prev = previous;
}
this.count--
return current.element
}
return undefined
}
}
三、循环链表:1)循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用undefined,而是指向第一个元素(head)。
2)双向循环链表有指向 head 元素的 tail.next 和指向 tail 元素的 head.prev。
由于和链表和双向链表没有·多大改动在此不作举例。
四、有序链表:有序链表是指保持元素有序的链表结构。除了使用排序算法之外,我们还可以将元素插入到正确的位置来保证链表的有序性。
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1
};
function defaultCompare(a, b) {
if (a === b) {
return 0;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
/*如果用于比较的元素更复杂一些,我们可以创建自定义的
比较函数并将它传入 SortedLinkedList 类的构造函数中。*/
class SortedLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
super(equalsFn);
this.compareFn = compareFn;
}
insert(element) {
if (this.isEmpty()) {
return super.insert(element, 0);
}
const pos = this.getIndexNextSortedElement(element);
return super.insert(element, pos);
}
getIndexNextSortedElement(element) {
let current = this.head;
let i = 0;
for (; i < this.size() && current; i++) {
const comp = this.compareFn(element, current.element);
if (comp === Compare.LESS_THAN) {
return i;
}
current = current.next;
}
return i;
}
}
本内容整理自《学习JavaScript数据结构与算法》