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

    常用命令 git add 添加 git status 查看状态 git status -s 状态概览 git diff 对比 git diff --staged 对比暂存区 git commit 提交 ...

  2. [Errno 14] curl#6 - "Could not resolve host: mirrors.cloud.aliyuncs.com; Name or service not known"

    修改/etc/resolv.conf文件 [root@lihui ~]# vi /etc/resolv.conf nameserver 8.8.8.8 nameserver 114.114.114.1 ...

  3. 【leetcode 29】 两数相除(中等)

    题目描述 给定两个整数,被除数 dividend 和除数 divisor.将两数相除,要求不使用乘法.除法和 mod 运算符. 返回被除数 dividend 除以除数 divisor 得到的商. 整数 ...

  4. 002.MEMS应用在开关电源上,实现大功率超小型化

    设计任务书 1.有关MEMS还有待具体了解 2.有关开关电源的目前难题也需要了解

  5. 【uniapp 开发】文字缩略css

    文字超出两行后显示省略号 display: -webkit-box; overflow: hidden; text-overflow: ellipsis; word-wrap: break-word; ...

  6. 大数据学习之路之ambari配置(四)

    试了很多遍,内存还是不够,电脑不太行的,不建议用ambari!!! 放弃了

  7. 搭建 SpringBoot 项目(前端页面 + 数据库 + 实现源码)

    SpringBoot 项目整合源码 SpringBoot 项目整合 一.项目准备 1.1 快速创建 SpringBoot 项目 1.2 项目结构图如下 1.3 数据表结构图如下 1.4 运行结果 二. ...

  8. 嵌入式Servlet容器

    配置嵌入式Servlet容器 ##Spring Boot里面内置了嵌入式的Servlet容器(tomcat) 点击pom.xml->右键->Diagrams->show Depend ...

  9. 如何使用Android可视化埋点

    Android可视化埋点是Android全埋点的增强.开发者可以将App界面同步至DTM界面,并在DTM界面通过可视化点击的方式添加埋点事件.目前Android可视化埋点包含两种埋点方式:普通可视化埋 ...

  10. 两数之和II_LeetCode_167_1099

    LeetCode_167原题链接:https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/ LeetCode_1099原题链 ...