一、什么是链表
链表(Linked list)是一种常见的基础数据结构。是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针。 链表中的元素在内存中并不是连续放置的,每个元素由一个存储元素本身的节点和一个指向下个一个元素的引用组成。
结构:
相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然 而,链表需要使用指针,因此实现链表时需要额外注意
二、实现链表基本结构
2.1 创建链表
//LinkedList.js
import {defaultEquals} from './utils/index.js'
export default class LinkedList{
constructor(){
this.count = 0; //来存储链表中的元素数量
this.head = undefined; // 保存引用
this.equalsFn = defaultEquals;
}
}
上述要引用的模块
- defaultEquals默认相等的比较函数
export function defaultEquals(a, b) {
return a === b;
}
- 列表中元素的表示方法Node.Node表示我们要添加到列表中的项
// linked-list-models.js
export class Node{
constructor(element){
this.element = element;
this.next = null;
}
}
接下来我们来实现常见的链表的一些方法:
- 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 向链表尾部添加元素
- 向空列表添加一个元素。当我们创建一个 LinkedList 对象时,head 会指向 undefined(或者是 null)。
- 如果 head 元素为 undefined 或 null,就意味着在向链表添加第一 个元素。因此要做的就是让 head 元素指向 node 元素。下一个 node 元素会自动成为 undefined
- 向一个不为空的链表尾部 添加元素: 首先要找到最后一个元素,循环访问列表,找到最后一项。直到当前next元素为 undefined 或 null 时,我们到达链表尾部,找到了最后一项
push(element) {
const node = new Node(element)
// 向空链表中插入数据
if (this.head == null) {
this.head = node
}
// 向不为空的链表尾部添加元素
else{
// 循环遍历,找到最后一个元素,再元素next位置添加新元素
let current = this.head
while(current.next != null){
current = current.next
}
current.next = node
}
this.count++;// 链表长度+1
}
测试代码:
import LinkedList from './LinkedList.js'
const linkedList = new LinkedList()
linkedList.push(15)
linkedList.push(10)
console.log(linkedList)
输出结果
LinkedList {
count: 2,
head: Node { element: 15, next: Node { element: 10, next: null } },
equalsFn: [Function: defaultEquals]
}
2.3 从链表特定位置移除一个元素(removeAt)
与插入元素类似,移除元素也存在两种场景:
- 是移除第一个元素
2. 移除第一个元素之外的其他元素
removeAt(index) {
// 检查传入的index是否合法,是否越界
if (index >= 0 && index < this.count) {
let current = this.head;
if (index == 0) {//删除第一项元素
this.head = current.next
} else {
// 记录当前元素对前一个元素的引用
let previous;
for (let i = 0; i < index; i++) {
previous = current;
current = current.next;
}
// 删除项为current项,将 previous 与 current 的下一项链接起来:跳过 current,从而移除它
previous.next = current.next
}
this.count--
return current.element;
}
return undefined
}
2.4 返回列表中特定位置的元素
getElementAt(index) {
// 判断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
}
由此可以对上面removeAt方法做出优化
removeAt(index) {
// 检查传入的index是否合法,是否越界
if (index >= 0 && index < this.count) {
let current = this.head;
if (index == 0) {//删除第一项元素
this.head = current.next
} else {
// // 记录当前元素对前一个元素的引用
// let previous;
// for (let i = 0; i < index; i++) {
// previous = current;
// current = current.next;
// }
// // 删除项为current项,将 previous 与 current 的下一项链接起来:跳过 current,从而移除它
// previous.next = current.next
let previous = this.getElementAt(index -1 )
current = previous.next
previous.next = current.next
}
this.count--
return current.element;
}
return undefined
}
2.5 链表中任意位置插入元素
返回值:true,false 两种场景:
- 在链表的起点添加一个元素, 也就是第一个位置
- 在链表中间或尾部添加一个元素
insert(element, index) {
// 判断index的合法性
if (index >= 0 && index <= this.count) {
const node = new Node(element);
if (index == 0) { // 起点位置插入第一个元素
let current = this.head;
node.next = current
this.head = node;
} else {
//中间或者尾部插入元素,首先要找到要插入的位置
let previous = this.getElementAt(index - 1)
let current = previous.next
node.next = current
previous.next = node
}
this.count++
return true
}
return false
}
2.6 返回一个元素的位置 indexOf
indexOf 方法接收一个元素的值,如果在链表中找 到了它,就返回元素的位置,否则返回-1
indexOf(element) {
let current = this.head
// 循环访问链表,判断元素是否相等
for (let i = 0; i < this.count && current != null; i++) {
if (this.equalsFn(element, current.element)) {
return i
} else {
current = current.next
}
}
return -1
}
2.7 从链表中移除元素
我们可以从indexOf方法中找到对应的index,则可以直接调用removeAt方法进行删除
remove(element) {
const index = this.indexOf(element)
return this.removeAt(index)
}
2.8 size 、isEmpty getHead、clear方法
size() {
return this.count
}
isEmpty() {
return this.size() === 0
}
getHead() {
return this.head
}
clear() {
this.head = undefined;
this.count = 0;
}
2.9 toString方法
返回表示整个链表的字符串。由于列表项使用了 Node 类,就需要重写继 承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值 循环输出
toString() {
if (this.head == null) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next;
for (let i = 1; i < this.size() && current != null; i++) {
objString = `${objString},${current.element}`;
current = current.next;
}
return objString;
}
三、双向链表
与普通链表的区别:
- 在链表中, 一个节点只有链向下一个节点的链接
- 而在双向链表中,链接是双向的:一个链向下一个元素, 另一个链向前一个元素
3.1 创建双向链表
DoublyLinkedList类继承LinkedList中的属性和方法。我们也会保存对链表最后一个元素的引用:tail
import LinkedList from './LinkedList.js'
import { defaultEquals } from './utils/index.js'
export default class DoublyLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn);
this.tail = undefined;
}
}
DoublyNode:扩展了Node类,因此我们继承 element 和 next 属性,由于使用了继承,我们需要在 DoublyNode 类的 构造函数中调用 Node 的构造函数
export class DoublyNode extends Node {
constructor(element, next, prev) {
super(element, next);
this.prev = prev;
}
}
3.2 在任意位置插入元素
考虑到两种情况:
- 在双向链表的第一个位置(起点)插入一个新元素
- 我们要在双向链表最后添加一个新元素
- 在双向链表中间插入一个新元素
从上面分析,我们分别写出对应的代码 注意的点:
- index是否越界
- 链表第一个位置时:this.head为空,即无元素时
- 最后一项时(index == this.count),注意tail的取值
- 在任意为位置插入元素时,要先获取当前元素的前一个元素,因为要改变指针的引用
insert(element, index) {
if (index >= 0 && index <= this.count) {
const node = new DoublyNode(element)
let current = this.head
// 第一种情况,在链表的第一个位置插入元素
if (index === 0) {
if (this.head == null) {//元素新增
this.head = node;
this.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
}
3.3 从任意位置移除元素
和插入一样,处理三种情况
- 从头部移除一个元素
2. 从尾部移除一个元素
- 从任意位置移除元素
removeAt(index) {
if (index >= 0 && index < this.count) {
let current = this.head
if (index === 0) { // 从头部移除元素
this.head = current.next
if (this.count == 1) { // 证明只有一项,要操作tail
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.next = current.next
current.next.prev = previous
}
this.count--
return current.element
}
return undefined
}
3.4 其余方法
我们上述仅写了insert和removeAt方法,接下来我们实现剩余的其他方法
3.4.1 push
push(element) {
const node = new DoublyNode(element)
// 向空链表中插入数据
if (this.head == null) {
this.head = node
this.tail = node
}
// 向不为空的链表尾部添加元素
else {
this.tail.next = node
node.prev = this.tail
this.tail = node
}
this.count++;// 链表长度+1
}
3.4.2 size isEmpty getHead,getTail,clear
getHead() {
return this.head;
}
getTail() {
return this.tail;
}
clear() {
super.clear();
this.tail = undefined;
}
3.4.3 toString
toString() {
if (this.head == null) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next;
while (current != null) {
objString = `${objString},${current.element}`;
current = current.next;
}
return objString;
}
inverseToString() {
if (this.tail == null) {
return '';
}
let objString = `${this.tail.element}`;
let previous = this.tail.prev;
while (previous != null) {
objString = `${objString},${previous.element}`;
previous = previous.prev;
}
return objString;
}
四、 循环链表
循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。 。循环链表和链 表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用 undefined,而是指向第一个元素(head)
由上述循环链表,可延伸出双向循环链表。 双向循环链表有指向 head 元素的 tail.next 和指向 tail 元素的 head.prev
4.1 创建循环链表
// 循环链表
import { defaultEquals } from './utils/index.js'
import LinkedList from './LinkedList.js'
export default class CircularLinkedList extends LinkedList{
constructor(equalsFn = defaultEquals){
super(equalsFn);
}
}
4.2 在任意位置插入元素
- 在循环链表第一个位置插入元素
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) {// 循环链表为空,直接插入元素
this.head = node
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 在任意位置移除元素
两种特殊情况
- 只有一个元素的循环列表中移除元素
- 一个非空循环表中移除第一个元素
- 移除其余位置元素,和链表移除代码相同
removeAt(index) {
if (index >= 0 && index < this.count) {
let current = this.head
console.log('current', current, this.count)
if (index == 0) {
if (this.size() === 1) {
this.head = undefined
} else {
const removed = this.head
current = this.getElementAt(this.size() - 1)
this.head = this.head.next
console.log('current', current, this.head, this.size())
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
}
4.4添加元素
push(element) {
const node = new Node(element);
let current;
if (this.head == null) {
this.head = node
} else {
current = this.getElementAt(this.size() - 1);
current.next = node
}
node.next = this.head
this.count++
}
五、 有序链表
有序链表是指保持元素有序的链表结构。在实现有序链表的过程中,我们需要使用到排序算法来确保数据的有序性
5.1 创建有序链表
import { defaultCompare } from './utils/index.js'
import { Node } from './models/linked-list-models.js'
import LinkedList from './LinkedList.js'
export default class SortedLinkedList extends LinkedList{
constructor(equalsFn = defaultEquals, compareFn = defaultCompare){
super(equalsFn)
this.compareFn = compareFn // 声明compareFn函数,用来比较元素
}
}
其中defaultCompare是排序方法:
export const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1
};
export function defaultCompare(a, b) {
if (a === b) { // {1}
return 0;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN; // {2}
}
5.2 有序的插入元素
考虑两种情况:
- 链表为空,直接插入
- 链表不为空,比较元素的大小,循环找到对应的位置插入
if (this.isEmpty()) {
return super.insert(element, 0)
} else {
const pos = this.getIndexNextSortedElement(element)
return super.insert(element, pos)
}
}
getIndexNextSortedElement(element) {
let current = this.head
let i = 0;
for (; i < this.count; i++) {
const comp = this.compareFn(element, current.element)
if (comp === Compare.LESS_THAN) {
return i
} else {
current = current.next
}
}
return i
}
六、 链表实现栈结构
我们可以使用 LinkedList 类及其变种作为内部的数据结构来创建其他数据结构,例如 栈、队列和双向队列。我们接下来以栈数据结构为例,演示下链表结构如何生成栈。
import DoublyLinkedList from './doubly-linked-list';
export default class StackLinkedList {
constructor() {
this.items = new DoublyLinkedList();
}
push(element) {
this.items.push(element);
}
pop() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items.removeAt(this.size() - 1);
return result;
}
peek() {
if (this.isEmpty()) {
return undefined;
}
return this.items.getElementAt(this.size() - 1).element;
}
isEmpty() {
return this.items.isEmpty();
}
size() {
return this.items.size();
}
clear() {
this.items.clear();
}
toString() {
return this.items.toString();
}
}