学习数据结构的 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());

JavaScript 数据结构与算法3(链表)的更多相关文章

  1. JavaScript数据结构与算法(六) 链表的实现

    // 链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的.每个 // 元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成.下图展 // 示了一个链表的 ...

  2. 为什么我要放弃javaScript数据结构与算法(第五章)—— 链表

    这一章你将会学会如何实现和使用链表这种动态的数据结构,这意味着我们可以从中任意添加或移除项,它会按需进行扩张. 本章内容 链表数据结构 向链表添加元素 从链表移除元素 使用 LinkedList 类 ...

  3. JavaScript数据结构与算法-链表练习

    链表的实现 一. 单向链表 // Node类 function Node (element) { this.element = element; this.next = null; } // Link ...

  4. 重读《学习JavaScript数据结构与算法-第三版》- 第6章 链表(一)

    定场诗 伤情最是晚凉天,憔悴厮人不堪言: 邀酒摧肠三杯醉.寻香惊梦五更寒. 钗头凤斜卿有泪,荼蘼花了我无缘: 小楼寂寞新雨月.也难如钩也难圆. 前言 本章为重读<学习JavaScript数据结构 ...

  5. JavaScript 数据结构与算法之美 - 线性表(数组、栈、队列、链表)

    前言 基础知识就像是一座大楼的地基,它决定了我们的技术高度. 我们应该多掌握一些可移值的技术或者再过十几年应该都不会过时的技术,数据结构与算法就是其中之一. 栈.队列.链表.堆 是数据结构与算法中的基 ...

  6. 为什么我要放弃javaScript数据结构与算法(第九章)—— 图

    本章中,将学习另外一种非线性数据结构--图.这是学习的最后一种数据结构,后面将学习排序和搜索算法. 第九章 图 图的相关术语 图是网络结构的抽象模型.图是一组由边连接的节点(或顶点).学习图是重要的, ...

  7. 为什么我要放弃javaScript数据结构与算法(第八章)—— 树

    之前介绍了一些顺序数据结构,介绍的第一个非顺序数据结构是散列表.本章才会学习另一种非顺序数据结构--树,它对于存储需要快速寻找的数据非常有用. 本章内容 树的相关术语 创建树数据结构 树的遍历 添加和 ...

  8. 为什么我要放弃javaScript数据结构与算法(第七章)—— 字典和散列表

    本章学习使用字典和散列表来存储唯一值(不重复的值)的数据结构. 集合.字典和散列表可以存储不重复的值.在集合中,我们感兴趣的是每个值本身,并把它作为主要元素.而字典和散列表中都是用 [键,值]的形式来 ...

  9. 为什么我要放弃javaScript数据结构与算法(第六章)—— 集合

    前面已经学习了数组(列表).栈.队列和链表等顺序数据结构.这一章,我们要学习集合,这是一种不允许值重复的顺序数据结构. 本章可以学习到,如何添加和移除值,如何搜索值是否存在,也可以学习如何进行并集.交 ...

随机推荐

  1. JDBC 中如何进行事务处理?

    Connection 提供了事务处理的方法,通过调用 setAutoCommit(false)可以设置 手动提交事务:当事务完成后用 commit()显式提交事务:如果在事务处理过程中 发生异常则通过 ...

  2. 为什么要用Spring

    1.方便解耦,简化开发 通过Spring提供的IoC容器,我们可以将对象之间的依赖关系交由Spring进行控制,避免硬编码所造成的过度程序耦合.有了Spring,用户不必再为单实例模式类.属性文件解析 ...

  3. 什么是编织(Weaving)?

    为了创建一个 advice 对象而链接一个 aspect 和其它应用类型或对象,称为编 织(Weaving).在 Spring AOP 中,编织在运行时执行.

  4. k-medoids算法

    k-medoids算法 对上面图形的解释 第一幅图:原来p属于Oj的(实线),当Orandom代替了Oj,p属于Oi了(虚线):第二幅图:原来p属于Oj的(实线),当Orandom代替了Oj,p属于O ...

  5. 论Hello World 有多少种输出方法:

    论Hello World 有多少种输出方法: C: printf("Hello Word!"); C++: cout<<"Hello Word!"; ...

  6. webpack系列——webpack3导入jQuery的新方案

    本文的目的 拒绝全局导入jQuery!! 拒绝script导入jQuery!! 找到一种只在当前js组件中引入jQuery,并且使用webpack切割打包的方案! 测试环境 以下测试在webpack3 ...

  7. C#设计编写一个控制台应用程序

    设计编写一个控制台应用程序,练习类的继承. (1) 编写一个抽象类 People,具有"姓名","年龄"字段,"姓名"属性,Work 方法. ...

  8. java中抛出throw关键字是怎么用的? 举例?

    5.抛出throw关键字 马克-to-win:我们先说5/0的原理,当程序运行到5/0的时候,java系统JVM会在后台new出一个除0异常实例,之后把这个实例传入catch块儿供开发者使用.马克-t ...

  9. 【Android开发】LogcatView,手机中查看logcat神器

    先上图 集成: 1, allprojects { repositories { ... maven { url 'https://www.jitpack.io' } } } 2, dependenci ...

  10. jsp笔记---标签

    <meta>标签 <meta> 标签提供了 HTML 文档的元数据.元数据不会显示在客户端,但是会被浏览器解析. META元素通常用于指定网页的描述,关键词,文件的最后修改时间 ...