前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【JS】206-数据结构之链表,这一篇就够了

【JS】206-数据结构之链表,这一篇就够了

作者头像
pingan8787
发布于 2019-07-23 10:19:20
发布于 2019-07-23 10:19:20
69600
代码可运行
举报
文章被收录于专栏:前端自习课前端自习课
运行总次数:0
代码可运行
阅读本文大约需要 17 分钟

写在前面

上一篇博客发布以后,仅几天的时间竟然成为了我写博客以来点赞数最多的一篇博客。欢喜之余,不由得思考背后的原因,前端er离数据结构与算法太遥远了,论坛里也少有人去专门为数据结构与算法撰文,才使得这看似平平的文章收获如此。不过,这样也更加坚定了我继续学习数据结构与算法的决心(虽然只是入门级的)

一、链表数据结构

相较于之前学习的 栈/队列 只关心 栈顶/首尾 的模式,链表更加像是数组。链表和数组都是用于存储有序元素的集合,但有几点大不相同

  1. 链表不同于数组,链表中的元素在内存中并不是连续放置的
  2. 链表添加或移除元素不需要移动其他元素
  3. 数组可以直接访问任何一个位置的元素,链表必须从表头开始迭代到指定位置访问

下面是单链表的基本结构

  • 长度为3的单链表
  • 每个元素由一个存储元素本身data的节点和一个指向下一个元素的引用next(也称指针或链接)组成
  • 尾节点的引用next指向为null

类比:寻宝游戏,你有一条线索,这条线索是指向寻找下一条线索的地点的指针。你顺着这条链接去下一个地点,得到另一条指向再下一处的线索。得到列表中间的线索的唯一办法,就是从起点(第一条线索)顺着列表寻找

二、链表的实现

链表的实现不像之前介绍的栈和队列一般依赖于数组(至少我们目前是这样实现的),它必须自己构建类并组织逻辑实现。我们先创建一个Node

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 节点基类
class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

一般单链表有以下几种方法:

  • append 在链表尾部添加一个元素
  • insert 在指定位置插入元素
  • removeAt 在指定位置删除元素
  • getNode 获取指定位置的元素
  • print 打印整个链表
  • indexOf 查找链表中是否有某个元素,有则返回索引,没有则返回-1
  • getHead 获取链表头部
  • getTail 获取链表尾部(有些并未实现尾部)
  • size 返回链表包含的元素个数
  • clear 清空链表
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 初始化链表
class LinkedList {
  constructor() {
    this._head = null;
    this._tail = null;
    this._length = 0;
  }
  // 方法...
}

下面我们来实现几个重要的方法

2.1 append 方法

在链表尾部添加一个新的元素可分为两种情况:

  1. 原链表中无元素,添加元素后,headtail均指向新元素
  2. 原链表中有元素,更新tail元素(如下)

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 在链表尾端添加元素
append(data) {
  const newNode = new Node(data);
  if (this._length === 0) {
    this._head = newNode;
    this._tail = newNode;
  } else {
    this._tail.next = newNode;
    this._tail = newNode;
  }

  this._length += 1;
  return true;
}

2.2 print 方法

为方便验证,我们先实现print方法。方法虽简单,这里却涉及到链表遍历精髓

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 打印链表
print() {
  let ret = [];
  // 遍历需从链表头部开始
  let currNode = this._head;
  // 单链表最终指向null,作为结束标志
  while (currNode) {
    ret.push(currNode.data);
    // 轮询至下一节点
    currNode = currNode.next;
  }
  console.log(ret.join(' --> '));
}

// 验证
const link = new LinkedList();
link.append(1);
link.append(2);
link.append(3);

link.print(); // 1 --> 2 --> 3

2.3 getNode方法

获取指定索引位置的节点,依次遍历链表,直到指定位置返回

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 获取指定位置元素
getNode(index) {
  let currNode = this._head;
  let currIndex = 0;
  while (currIndex < index) {
    currIndex += 1;
    currNode = currNode.next;
  }
  return currNode;
}

// 验证【衔接上面的链表实例】
console.log(link.getNode(0));
// Node { data: 1, next: Node { data: 2, next: Node { data: 3, next: null } } }
console.log(link.getNode(3)); // null

2.4 insert 方法

插入元素,需要考虑三种情况

  1. 插入尾部,相当于append
  2. 插入首部,替代_head并指向原有头部元素
  3. 中间,需要断开原有链接并重新组合(如下)

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 在链表指定位置插入元素
insert(index, data) {
  // 不满足条件的索引值
  if (index < 0 || index > this._length) return false;
  // 插入尾部
  if (index === this._length) return this.append(data);

  const insertNode = new Node(data);
  if (index === 0) {
    // 插入首部
    insertNode.next = this._head;
    this._head = insertNode;
  } else {
    // 找到原有位置节点
    const prevTargetNode = this.getNode(index - 1);
    const targetNode = prevTargetNode.next;
    // 重塑节点连接
    prevTargetNode.next = insertNode;
    insertNode.next = targetNode;
  }

  this._length += 1;
  return true;
}

// 验证
link.insert(0, 0);
link.insert(4, 4);
link.insert(5, 5);
link.print(); // 0 --> 1 --> 2 --> 3 --> 4 --> 5

2.5 removeAt 方法

在指定位置删除元素同添加元素类似

  1. 首部:重新定义_head
  2. 其他:找到目标位置的前后元素,重塑连接,如果目标位置为尾部,则重新定义_tail

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 在链表指定位置移除元素
removeAt(index) {
  if (index < 0 || index >= this._length) return false;

  if (index === 0) {
    this._head = this._head.next;
  } else {
    const prevNode = this.getNode(index - 1);
    const delNode = prevNode.next;
    const nextNode = delNode.next;
    // 若移除为最后一个元素
    if (!nextNode) this._tail = prevNode;
    prevNode.next = nextNode;
  }

  this._length -= 1;
  return true;
}

// 验证
link.removeAt(3);
link.print(); // 0 --> 1 --> 2 --> 4 --> 5

2.6 其它方法

完整的链表代码,可点此获取点击预览

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 判断数据是否存在于链表内,存在返回index,否则返回-1
indexOf(data) {
  let currNode = this._head;
  let index = 0;
  while (currNode) {
    if (currNode.data === data) return index;
    index += 1;
    currNode = currNode.next;
  }

  return -1;
}

getHead() {
  return this._head;
}

getTail() {
  return this._tail;
}

size() {
  return this._length;
}

isEmpty() {
  return !this._length;
}

clear() {
  this._head = null;
  this._tail = null;
  this._length = 0;
}

三、链表的应用

3.1 基于链表实现的`Stack`和`Queue`

基于链表实现栈

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Stack {
  constructor() {
    this._link = new LinkedList();
  }
  push(item) {
    this._link.append(item);
  }
  pop() {
    const tailIndex = this._link - 1;
    return this._link.removeAt(tailIndex);
  }
  peek() {
    if (this._link.size() === 0) return undefined;
    return this._link.getTail().data;
  }
  size() {
    return this._link.size();
  }
  isEmpty() {
    return this._link.isEmpty();
  }
  clear() {
    this._link.clear()
  }
}

基于链表实现队列

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Queue {
  constructor() {
    this._link = new LinkedList();
  }
  enqueue(item) {
    this._link.append(item);
  }
  dequeue() {
    return this._link.removeAt(0);
  }
  head() {
    if (this._link.size() === 0) return undefined;
    return this._link.getHead().data;
  }
  tail() {
    if (this._link.size() === 0) return undefined;
    return this._link.getTail().data;
  }
  size() {
    return this._link.size();
  }
  isEmpty() {
    return this._link.isEmpty();
  }
  clear() {
    this._link.clear()
  }
}

3.2 链表翻转【面试常考】

(1)迭代法

迭代法的核心就是currNode.next = prevNode,然后从头部一次向后轮询

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
reverse() {
  if (!this._head) return false;

  let prevNode = null;
  let currNode = this._head;
  while (currNode) {
    // 记录下一节点并重塑连接
    const nextNode = currNode.next;
    currNode.next = prevNode;
    // 轮询至下一节点
    prevNode = currNode;
    currNode = nextNode;
  }

  // 交换首尾
  let temp = this._tail;
  this._tail = this._head;
  this._head = temp;

  return true;
}

(2)递归法

递归的本质就是执行到当前位置时,自己并不去解决,而是等下一阶段执行。直到递归终止条件,然后再依次向上执行

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function _reverseByRecusive(node) {
  if (!node) return null;
  if (!node.next) return node; // 递归终止条件

  var reversedHead = _reverseByRecusive(node.next);
  node.next.next = node;
  node.next = null;

  return reversedHead;
};

_reverseByRecusive(this._head);

3.3 链表逆向输出

利用递归,反向输出

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function _reversePrint(node){
  if(!node) return;// 递归终止条件
  _reversePrint(node.next);
  console.log(node.data);
};

四、双向链表和循环链表

4.1 双向链表

双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素,如下图

正是因为这种变化,使得链表相邻节点之间不仅只有单向关系,可以通过prev来访问当前节点的上一节点。相应的,双向循环链表的基类也有变化

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
    this.prev = null;
  }
}

继承单向链表后,最终的双向循环链表DoublyLinkedList如下【prev对应的更改为NEW

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class DoublyLinkedList extends LinkedList {
  constructor() {
    super();
  }

  append(data) {
    const newNode = new DoublyNode(data);

    if (this._length === 0) {
      this._head = newNode;
      this._tail = newNode;
    } else {
      newNode.prev = this._tail; // NEW
      this._tail.next = newNode;
      this._tail = newNode;
    }

    this._length += 1;
    return true;
  }

  insert(index, data) {
    if (index < 0 || index > this._length) return false;
    if (index === this._length) return this.append(data);

    const insertNode = new DoublyNode(data);
    if (index === 0) {
      insertNode.prev = null; // NEW
      this._head.prev = insertNode; // NEW
      insertNode.next = this._head;
      this._head = insertNode;
    } else {
      // 找到原有位置节点
      const prevTargetNode = this.getNode(index - 1);
      const targetNode = prevTargetNode.next;
      // 重塑节点连接
      prevTargetNode.next = insertNode;
      insertNode.next = targetNode;
      insertNode.prev = prevTargetNode; // NEW
      targetNode.prev = insertNode; // NEW
    }

    this._length += 1;
    return true;
  }

  removeAt(index) {
    if (index < 0 || index > this._length) return false;

    let delNode;

    if (index === 0) {
      delNode = this._head;
      this._head = this._head.next;
      this._head.prev = null; // NEW
    } else {
      const prevNode = this.getNode(index - 1);
      delNode = prevNode.next;
      const nextNode = delNode.next;
      // 若移除为最后一个元素
      if (!nextNode) {
        this._tail = prevNode;
        this._tail.next = null; // NEW
      } else {
        prevNode.next = nextNode; // NEW
        nextNode.prev = prevNode; // NEW
      }
    }

    this._length -= 1;
    return delNode.data;
  }
}

4.2 循环链表

循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链 表之间唯一的区别在于,单向循环链表最后一个节点指向下一个节点的指针tail.next不是引用null, 而是指向第一个节点head,双向循环链表的第一个节点指向上一节点的指针head.prev不是引用null,而是指向最后一个节点tail

总结

链表的实现较于栈和队列的实现复杂许多,同样的,链表的功能更加强大

我们可以通过链表实现栈和队列,同样也可以通过链表来实现栈和队列的问题

链表更像是数组一样的基础数据结构,同时也避免了数组操作中删除或插入元素对其他元素的影响

原文链接:https://segmentfault.com/a/1190000017970029

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-04-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端自习课 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
数据结构于JS也可以成为CP(五)链表
Hello大家好~兔妞今天给大家带来的是链表哦!为什么有链表呢,因为数组并不总是组织数据的最佳数据结构。由于在JavaScript中数组是一个对象,所以js的数组相比其他语言的数组效率较低。那么我们就可以考虑使用链表啦。
萌兔IT
2019/07/26
6520
数据结构于JS也可以成为CP(五)链表
一文带你拿下前端必备数据结构 -- 链表 !!
链表是一组由节点组成的集合,每个节点都有一个指针指向它的下一个节点。举个栗子来说,就像上图的小火车一样,每一节车厢之间都通过绳索相连接,每节车厢都是一个节点,车厢间的连接就是指针❤️
小丞同学
2021/08/16
7910
理解JavaScript中的数据结构(链表)
最近开源了一个 Vue 组件,还不够完善,欢迎大家来一起完善它,也希望大家能给个 star 支持一下,谢谢各位了。
前端小智@大迁世界
2021/01/07
1.3K0
链表的实现
链表分为单向链表、双向链表和循环链表。链表这种数据结构就像是火车车厢一样,每个车厢可以插入到任意的的位置。与数组不同的是,数组的数据存储是连续的存储单元,就好比坐在一排座位的人,这些人必须坐的没有空位置(挨着挨坐),当有人离开座位(删除操作)或者来到某个座位(增加或插入元素)时,如果要保持挨着挨坐,那就可能会移动比较多的位置,我们可以使用下标来获取数组不同位置的数据。而链表的数据存储单元却不一定是连续的,它由指针来标记下一个存储数据的位置。
多云转晴
2019/10/23
5470
链表的实现
数据结构——双向链表(C语言版)
通过上述代码示例,我们实现了双向链表的基本操作,包括初始化、插入和删除节点,以及遍历链表。双向链表是一种灵活且高效的数据结构,适用于需要频繁插入和删除操作的场景。通过深入理解双向链表的实现原理,我们可以更好地应用它解决实际问题。
GG Bond1
2024/06/14
1120
「算法与数据结构」JavaScript中的链表
此文会先探讨下什么是链表以及在 JavaScript 中的链表,接着我们会使用 JavaScript 这门语言动手实现下各类链表的设计,最后我们会抛出一些常规疑问,并从各个方面一一解答,总之,目的就是完全搞定链表
isboyjc
2022/03/28
9260
「算法与数据结构」JavaScript中的链表
数据结构基础温故-1.线性表(中)
在上一篇中,我们学习了线性表最基础的表现形式-顺序表,但是其存在一定缺点:必须占用一整块事先分配好的存储空间,在插入和删除操作上需要移动大量元素(即操作不方便),于是不受固定存储空间限制并且可以进行比较快捷地插入和删除操作的链表横空出世,所以我们就来复习一下链表。
Edison Zhou
2018/08/20
5140
数据结构基础温故-1.线性表(中)
【数据结构】线性表(三)循环链表的各种操作(创建、插入、查找、删除、修改、遍历打印、释放内存空间)
按照线性表结点间的逻辑顺序依次将它们存储于一组地址连续的存储单元中的存储方式被称为线性表的顺序存储方式。按顺序存储方式存储的线性表具有顺序存储结构,一般称之为顺序表。换言之,在程序中采用定长的一维数组,按照顺序存储方式存储的线性表,被称为顺序表。
Qomolangma
2024/07/30
1570
【数据结构】线性表(三)循环链表的各种操作(创建、插入、查找、删除、修改、遍历打印、释放内存空间)
从 0 开始学习 JavaScript 数据结构与算法(七)双向链表
本专辑所有文章&源码&测试环境均托管在 GitHub 仓库[1] 欢迎同学们 Star 和 Fork。
XPoet
2021/04/26
5610
重学数据结构(一、线性表)
线性表是最常见也是最简单的一种数据结构。简言之, 线性表是n个数据元素的有限序列。 其一般描述为:
三分恶
2020/08/21
7540
重学数据结构(一、线性表)
JS数据结构第三篇---双向链表和循环链表之约瑟夫问题
在上文《JS数据结构第二篇---链表》中描述的是单向链表。单向链表是指每个节点都存有指向下一个节点的地址,双向链表则是在单向链表的基础上,给每个节点增加一个指向上一个节点的地址。然后头结点的上一个节点,和尾结点的下一个节点都指向null。同时LinkedList类中再增加一个last内部属性,一直指向链表中最后一个节点。结构模拟如图:
tandaxia
2019/07/01
7420
JS数据结构第三篇---双向链表和循环链表之约瑟夫问题
JS高级-数据结构的封装
最近在看了《数据结构与算法JavaScript描述》这本书,对大学里学的数据结构做了一次复习(其实差不多忘干净了,哈哈)。如果能将这些知识捡起来,融入到实际工作当中,估计编码水平将是一次质的飞跃。带着这个美好的愿望,开始学习吧O(∩_∩)O~~ 我们知道在JS中,常常用来组织数据的无非是数组和对象(这些基础就不介绍了)。但在数据结构中,还有一些抽象的数据类型:列表、栈、队列、链表、字典、散列、集合、二叉树、图等,可以用来更好的对实际场景建模。当然这些数据类型,原生JS不支持,那么就需要通过封装来模拟,其底层
小古哥
2018/03/08
8.1K0
数据结构图文解析之:数组、单链表、双链表介绍及C++模板实现
0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组、单链表、双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 数据结构图文解析之:树的简介及二叉排序树C++模板实现. 数据结构图文解析之:AVL树详解及C++模板实现 数据结构图文解析之:二叉堆详解及C++模板实现 1. 线性表简介 线性表是一种线性结构,它是由零个或多个数据元素构成的有限序列。线性表的特征是在一个序列中,除了头尾元素,每个元素都有且只有一个直接前驱,
Tencent JCoder
2018/07/02
1.2K0
JavaScript数据结构04 - 链表
前面我们学习了数组这种数据结构。数组(或者也可以称为列表)是一种非常简单的存储数据序列的数据结构。在这一节,我们要学习如何实现和使用链表这种动态的数据结构,这意味着我们可以从中任意添加或移除项,它会按需进行扩容。
leocoder
2018/10/31
5840
【学点数据结构和算法】02-链表
上一篇博客博主为大家带来了数组的内容分享,本篇博客我们来学习另外一个重要的数据结构——链表!
大数据梦想家
2021/01/27
5660
【学点数据结构和算法】02-链表
前端学习 数据结构与算法 快速入门 系列 —— 链表(转载非原创)
转载来源:https://www.cnblogs.com/pengjiali/p/15320535.html
xlj
2021/09/23
8621
【数据结构】我的学习笔记
常言说,打蛇打七寸,学习数据结构,关键要理解数据结构特点以及每种结构的增、删、查方法
萌萌哒草头将军
2023/05/24
4450
【数据结构】我的学习笔记
双向链表的增删改查
双向链表,我们曾经拿了一幅非常形象的图片来形容他,就像几个人手拉手围成一个圈一样。在我们代码中的呈现就是每个节点都有一个指向下一个节点的指针,同时也有一个指向上一个节点的指针。就因为新增了这个指向上一个节点指针的特性,它解决了单向循环链表的诸多问题,如下:
我与梦想有个约会
2023/10/20
1490
双向链表的增删改查
窥探数据结构的世界
数据结构是在计算机中组织和存储数据的一种特殊方式,使得数据可以高效地被访问和修改。更确切地说,数据结构是数据值的集合,表示数据之间的关系,也包括了作用在数据上的函数或操作。
用户1462769
2019/08/29
8220
窥探数据结构的世界
大厂面试:JavaScript各种源码解析
func 是要调用的函数,是由 context 调用,func 的指向 context,所以关注点在 context 指向
Javanx
2019/09/04
7400
推荐阅读
相关推荐
数据结构于JS也可以成为CP(五)链表
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验