学习数据结构的 git 代码地址: https://gitee.com/zhangning187/js-data-structure-study
1、链表
本章学习如何实现和使用链表这种动态的数据结构。在这种结构里面可以从中随意添加或移除项,可以按需进行扩容。
该章节内容包括一下内容:
- 链表数据结构
- 向链表添加元素
- 从链表移除元素
- 使用 LinkedList 类
- 双向链表
- 循环链表
- 排序链表
- 通过链表实现栈
1.1 认识链表结构
链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。
数组的特点:
要存储多个元素,数组(列表)是最常用的数据结构。
几乎每一种编程语言都实现了数组结构。
缺点:
数组的创建需要申请一段连续的内存空间,并且大小是固定的,当当前数组不能满足需求时需要扩容(扩容很耗性能)。
而且在数组的开头或中间位置插入数据的成本很高,需要进行大量元素的位移。
要存储多个元素,另外一个选择就是链表。
不同于数组,链表中的元素在内存中不必是连续的空间。
链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也叫指针或连接)组成。
(最后一个节点的next指向 null,如果一个节点都没有,head 直接指向null就可以了,head 是指向链表里面所有节点的第一个节点)
相对于数组,链表的一些优点:
-
- 内存空间不是必须连续的,可以充分利用计算机的内存,实现灵活的内存动态管理。
- 链表不必在创建时就确定大小,并且大小可以无限的延伸下去。
- 链表在插入和删除数据时,时间复杂度可以达到O(1).(大欧表示法)相对数组高效很多。
相对于数组,链表的一些缺点:
-
- 链表访问任何一个位置的元素时,都需要从头开始访问。(无法跳过第一个元素访问任何一个元素)
- 无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素。(在访问的元素时候相对于数组性能较低)
频繁的删除或添加元素选择链表。通过下标查询元素选择数组合适。
链表相对于传统数组的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。在数组中,可以直接访问任何位置的任何元素,而要想访问链表中的一个元素,则需要从起点(表头)开始迭代链表直到找到所需元素。
链表类似于火车:有一个火车头,火车头会连接一个节点,节点上有乘客(类似于数据),并且这个节点会连接下一个节点,以此类推。
1.2封装链表结构
创建一个链表类:
首先链表里面有个head属性,指向链表里所有节点的第一个节点,每个节点有两部分组成,一个是data,一个是next指向下一个节点的引用。
// 封装链表节点类,方便复用 export class Node { constructor(element) { // element: 当前数据 this.element = element; // next: 下一个节点的引用 this.next = undefined; } }
// 比较两个值是否相等的默认方法 export function defaultEquals(a, b) { return a === b; }
// 链表类 import {Node} from '../models/index.js'; import {defaultEquals} from '../util.js'; export default class LinkedList { constructor(equalsFn = defaultEquals) { // length 记录链表长度 this.length = 0; // head 默认执行undefined,即没有一个元素 this.head = undefined; // 用于判断 元素 是否相等,(可以自定义) // 在要实现 indexOf 方法的时候,要比较链表中的元素是否相等,需要使用一个内部调用的函数,equalsFn。 // 可以传入一个自定义函数用于比较两个 js 对象或值是否相等。 this.equalsFn = equalsFn; } }
1.3链表的常见操作(增删改查)
- append(element): 向列表尾部添加一个新的项
- insert(position, element): 向列表特定位置插入一个新的项
- get(position): 获取对应位置的元素
- indexOf(element): 返回元素在列表中的索引。如果列表中没有该元素返回 -1
- update(position, data): 修改某个位置的元素
- removeAt(position): 删除列表的特定位置项
- remove(element): 从列表中移除一项
- isEmpty():链表中没有任何元素返回true,否则返回 false
- size(): 返回链表包含的元素个数。与数组的length 属性类似
- toString(): 由于列表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值
以上的操作和数组非常相似,因为链表本身就是可以代替数组的结构。
1.3.1 push(element) 向列表尾部添加一个新的项
// 追加方法 push(element) { // 创建新的数据节点 const newNode = new Node(element); // 链表为空,添加第一个元素,链表不为空,追加元素 if (this.length === 0) { this.head = newNode; } else { // 找到链表中的最后一个节点,让最后一个节点的 next 等于 newNode let current = this.head; while (current.next) {// 如果next为空表示current是链表的最后一个元素 current = current.next; } // 这时 current 为链表最后一个节点,next 指向新的节点 current.next = newNode; } this.length++; };
1.3.2 insert(element, index): 向列表特定位置插入一个新的项
// 指定位置插入 insert(element, index) { // 检查是否越界 if (index >= 0 && index <= this.length) { const newNode = new Node(element); let current = this.head; // 插入元素分为两种情况,移除第一个元素,移除其他元素 if (index == 0) { newNode.next = current; this.head = newNode; } else { let lastNode; // 得到当前需要移除的节点 current,上一个节点 lastNode for (let i = 0; i < index; i++) { lastNode = current; current = current.next; } newNode.next = current; lastNode.next = newNode; } this.length++; // 返回删除的节点 return true; } return undefined; };
1.3.3 get(index): 获取对应位置的元素
// 获取对应位置的元素 get(index) { // 检查是否越界 if (index >= 0 && index <= this.length) { let current = this.head; for (let i = 0; i < index && current != null; i++) { current = current.next; } return current; } return undefined; };
1.3.4 indexOf(element): 返回元素在列表中的索引。如果列表中没有该元素返回 -1
// indexOf(element): 返回元素在列表中的索引。如果列表中没有该元素返回 -1 indexOf(element) { let current = this.head; for (let i = 0; i < this.length; i++) { if (this.equalsFn(current.element, element)) { return i; } else { current = current.next; } } return -1; };
1.3.5 update(index, element): 修改某个位置的元素
// update(index, element): 修改某个位置的元素 update(index, element) { if (index >= 0 && index <= this.length) { let current = this.head; for (let i = 0; i < index; i++) { current = current.next; } current.element = element; return true; } return false; };
1.3.6 toString() 转换为字符串
// 转换字符串 tostring() { let current = this.head; let listString = ''; // 循环每一个节点 while (current) { listString += current.element + ' '; // 每次指向下一个 current = current.next; } return listString; };
1.3.7 removeAt(index) 移除指定位置项
// 移除元素removeAt() removeAt(index) { // 检查是否越界 if (index >= 0 && index <= this.length) { let current = this.head; // 移除元素分为两种情况,移除第一个元素,移除其他元素 if (index == 0) { this.head = undefined; } else { let lastNode; // 得到当前需要移除的节点 current,上一个节点 lastNode for (let i = 0; i < index; i++) { lastNode = current; current = current.next; } lastNode.next = current.next; this.length--; // 返回删除的节点 return current.element; } } return undefined; };
1.3.8 remove(element): 从列表中移除一项
// 移除元素remove() remove(element) { let current = this.head; let lastNode; for (let i = 0; i < this.length; i++) { lastNode = current; current = current.next; if (current && current.element == element) { lastNode.next = current.next; this.length--; return true; } } return false; };
1.3.9 isEmpty():链表中没有任何元素返回true,否则返回 false
// isEmpty():链表中没有任何元素返回true,否则返回 false isEmpty() { return this.head ? false : true; };
1.4 双向链表
双向链表与普通链表的区别:
在链表中一个节点只有链向下一个节点的链接;
在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素
创建双向链表类
import {Node} from '../models/index.js'; import LinkedList from '../1.封装链表/LinkedList'; import {defaultEquals} from '../util'; class DoublyNode extends Node { constructor(element, next, prev) { super(element, next); // prev: 指向前一个节点 this.prev = prev; } } class DoublyLinkedList extends LinkedList { constructor(equalsFn = defaultEquals) { // 调用 LinkedList 的构造函数,它会初始化 equalsFn、length、head 属性 super(equalsFn); // 保存对链表最后一个元素的引用 this.tail = undefined; } }
双向链表提供了两种迭代的方法:从头到尾,或者从尾到头。还可以访问一个特定节点的下一个或前一个元素。
1.4.1 实现双向链表的添加方法 push(element)
// 追加方法 push(element) { const newNode = new DoublyNode(element); let current = this.tail; // 在头部插入 if (this.length == 0) { this.head = newNode; this.tail = newNode; } else { // 当前节点 current.next = newNode; newNode.prev = current; this.tail = newNode; } this.length++; return true; };
1.4.2 实现指定位置插入方法 insert(index, element)
// 在指定位置插入新元素,单向链表只要控制一个 next 指针,而双向链表则要同时控制 next 和 prev 这两个指针 // 重写 insert 方法,表示我们会使用一个和 LinkedList 类中的方法行为不同的方法 insert(element, index) { if (index >= 0 && index <= this.length) { const newNode = new DoublyNode(element); let current = this.head; // 在头部插入 if (index == 0) { // 当没有一条数据的时候 if (!this.head) { this.head = newNode; this.tail = newNode; } else { newNode.next = this.head; current.prev = newNode; this.head = newNode; } // 在尾部插入 } else if (index == this.length) { current = this.tail; current.next = newNode; newNode.prev = current; this.tail = newNode; } else { // 获取 index 位置的前一个元素 const previous = this.get(index - 1); // 当前节点 current = previous.next; // 当前节点的 prev 为要插入的节点 current.prev = newNode; // 要插入的节点的 next 为 current 节点 newNode.next = current; newNode.prev = previous; previous.next = newNode; } this.length++; return true; } return false; };
1.4.3 实现移除指定位置元素
// 从任意位置移除元素 removeAt(index) { // 检查是否越界 if (index >= 0 && index < this.length) { // 移除元素分为两种情况,移除第一个元素,移除其他元素 let current = this.head; if (index == 0) { if (this.length == 1) { this.tail = undefined; } else { current.prev = undefined; } this.head = current.next; } else if (index === this.length - 1) {// 判断是否是最后一个元素 current = this.tail; this.tail = current.prev; this.tail.next = undefined; } else { current = this.get(index); const previous = current.prev; previous.next = current.next; current.next.prev = previous; } this.length--; return current.element; } return undefined; };
1.5 循环链表
循环链表可以像链表一样只有单项引用,也可以像双向链表一样有双向引用。循环链表与链表之间唯一的区别在于,最后一个元素指向下一个元素的指针不是 undefined,而是第一个元素(head)。
双向循环链表指向head元素的 tail.next 和 指向 tail 元素的 head.prev。
1.5.1 封装循环链表结构
import LinkedList from '../1.封装链表/LinkedList.js'; import {Node} from '../models/index.js'; import {defaultEquals} from '../util.js'; // CircularLinkedList 类不需要任何额外的属性,直接扩展 LinkedList 类并覆盖需要改写的方法即可。 class CircularLinkedList extends LinkedList { constructor(equalsFn = defaultEquals) { super(equalsFn); } }
1.5.2 添加节点方法 push(element)
// 添加节点 push(element) { const newNode = new Node(element); // 先判断是否存在节点 if (!this.head) { this.head = newNode; newNode.next = newNode; } else { let current = this.head; // 找到最后一个元素 while (current.next != this.head) { current = current.next; } current.next = newNode; newNode.next = this.head; } this.length++; return true; }
1.5.3 指定位置插入新元素 insert(index, element)
// 在指定位置插入新元素 // 这里插入逻辑和普通链表插入逻辑是一样的,不同之处在于我们需要将循环链表尾部节点的 next 引用指向头部节点。 insert(element, index) { // 检查是否越界 if (index >= 0 && index <= this.length) { const newNode = new Node(element); if (index == 0) {// 判断头部插入 if (this.length == 0) {// 没有数据的时候插入 this.head = newNode; newNode.next = newNode; } else { newNode.next = this.head; this.head = newNode; } } else if (index == this.length) {// 判断尾部插入 this.push(element); } else { // 得到插入的上一个节点 const lastNode = this.get(index - 1); newNode.next = lastNode.next; lastNode.next = newNode; } this.length++; return true; } return false; }
1.5.4 从指定位置移除元素 removeAt(index)
// 从指定位置移除元素 removeAt(index) { if (index >= 0 && index < this.length) { let current = this.head; if (index == 0) { if (this.length == 1) { this.head = undefined; } else { // 得到最后一个元素 const endNode = this.get(this.length - 1); this.head = current.next; endNode.next = current.next; } } else { // 得到需要删除的元素的前一个元素 const previous = this.get(index - 1); previous.next = previous.next.next; } this.length--; } return undefined; }
1.6 有序链表
保持元素有序的链表结构。除了使用链表算法之外,还可以将元素插入到正确的位置来保证链表的有序性。
1.6.1 有序链表结构封装
/* * @author: zhangning * @date: 2022/2/11 17:55 * @Description: 封装有序链表 **/ import LinkedList from '../1.封装链表/LinkedList.js'; import {defaultEquals} from '../util.js'; // 比较状态的返回值,为了代码好看通过声明常量表示两个值 const Compare = { LESS_THAN: -1, BIGGER_THEN: 1 }; // 比较数据大小 function defaultCompare(a, b) { if (a === b) { return 0; } return a < b ? Compare.LESS_THAN : Compare.BIGGER_THEN; } // 声明有序列表类,继承 LinkedList 类中所有的属性和方法, // 这个类比较特别,需要一个用来比较 元素的函数 compareFn 默认使用 defaultCompare,支持自定义 class SortedLinkedList extends LinkedList { constructor(equalsFn = defaultEquals, compareFn = defaultCompare) { super(equalsFn); this.compareFn = compareFn; } // 覆盖 insert 方法 insert(element, index = 0) { debugger if (this.isEmpty()) { return super.push(element); } // 得到要插入的位置 const pos = this.getIndexNextSortedElement(element); return super.insert(element, pos); } // 获取插入的位置 getIndexNextSortedElement(element) { debugger let current = this.head; let i = 0; for (; i < this.length; i++) { const comp = this.compareFn(element, current.element); if (comp === Compare.LESS_THAN) { return i; } current = current.next; } return i; } } const sList = new SortedLinkedList(); sList.insert(8); sList.insert(2); sList.insert(1); sList.insert(6); sList.insert(3); console.log(sList);
1.7 创建 StackLinkedList 栈数据结构
除了上面的数据结构,还可以使用 LinkedList 类及其变种作为内部的数据结构来创建其他数据结构,如:栈、队列、双向队列…等
1.7.1 创建栈数据结构(先进后出,后进先出)
/* * @author: zhangning * @date: 2022/2/11 19:42 * @Description: 创建栈数据结构(先进后出,后进先出) **/ import DoublyLinkedList from '../2.双向链表/DoublyLinkedList.js'; class StackLinkedList { constructor() { // 使用双向链表进行存储数据,对于栈来说,会像链表尾部添加元素,也会从链表尾部移除元素,双向链表类中有最后一个元素 tail 的引用,不需要迭代整个链表就能够获取到它。 // 双向链表可以直接获取头尾的元素,减少过程消耗,它的时间复杂度和原始的 Stack 实现相同为O(1). // 当然也可以对 LinkedList 类进行优化,保存一个指向尾部元素的引用 this.item = new DoublyLinkedList(); } // 添加元素 push(element) { this.item.push(element); } // 移除元素 pop() { if (this.item.isEmpty()) { return undefined; } return this.item.removeAt(this.item.length - 1); } // 获取最顶层元素的值 peek() { if (this.item.isEmpty()) { return undefined; } return this.item.get(this.item.length - 1).element; } // 获取栈的长度 size() { return this.item.length; } } const stackList = new StackLinkedList(); stackList.push(100); stackList.push(111); stackList.pop(); stackList.push(222); console.log(stackList.peek()); stackList.push(333); console.log(stackList); console.log(stackList.size());