链表和数组

大家都用过js中的数组,数组其实是一种线性表的顺序存储结构,它的特点是用一组地址连续的存储单元依次存储数据元素。而它的缺点也正是其特点而造成,比如对数组做删除或者插入的时候,可能需要移动大量的元素。

这里大致模拟一下数组的插入操作:


function insert(arr, index, data) {
for (let i = arr.length; i >index; i--) {
arr[i] = arr[i - 1];
}
arr[index] = data;
}

从上面的代码可以看出数组的插入以及删除都有可能会是一个O(n)的操作。从而就引出了链表这种数据结构,链表不要求逻辑上相邻的元素在物理位置上也相邻,因此它没有顺序存储结构所具有的缺点,当然它也失去了数组在一块连续空间内随机存取的优点。

单向链表

单向链表的特点:

  • 用一组任意的内存空间去存储数据元素(这里的内存空间可以是连续的,也可以是不连续的)
  • 每个节点(node)都由数据本身和一个指向后续节点的指针组成
  • 整个链表的存取必须从头指针开始,头指针指向第一个节点
  • 最后一个节点的指针指向空(NULL)

链表中的几个主要操作

  • 创建节点
  • 插入节点
  • 搜索/遍历节点
  • 删除节点
  • 合并

初始化节点

  • 指针指向空
  • 存储数据

class Node {
constructor(key) {
this.next = null;
this.key = key;
}
}

初始化单向链表

  • 每个链表都有一个头指针,指向第一个节点,没节点则指向NULL

class List {
constructor() {
this.head = null;
}
}

创建节点


static createNode(key) {
return new createNode(key);
}

这里说明一下,这一块我是向外暴露了一个静态方法来创建节点,而并非直接把它封装进插入操作里去,因为我感觉这样的逻辑会更加正确一些。 从创建一个链表 -> 创建一个节点 -> 将节点插入进链表中。可能你会遇到一些文章介绍的方式是直接将一个数据作为参数去调用insert操作,在insert内部做了一个创建节点。

插入节点(插入到头节点之后)

插入操作只需要去调整节点的指针即可,两种情况:

  • head没有指向任何节点,说明当前插入的节点是第一个

    • head指向新节点
    • 新节点的指针指向NULL
  • head有指向的节点

    • head指向新的节点
    • 新节点的指针指向原本head所指向的节点

insert(node) {
// 如果head有指向的节点
if(this.head){
node.next = this.head;
}else {
node.next = null;
}
this.head = node;
}

搜索节点

  • 从head开始查找
  • 找到节点中的key等于想要查找的key的时候,返回该节点

find(key) {
let node = this.head;
while(node !== null && node.key !== key){
node = node.next;
}
return node;
}

删除节点

这里分三种情况:

  • 所要删除的节点刚好是第一个,也就是head指向的节点

    • 将head指向所要删除节点的下一个节点(node.next)
  • 要删除的节点为最后一个节点

    • 寻找到所要删除节点的上一个节点(prevNode)
    • 将prevNode中的指针指向NULL
  • 在列表中间删除某个节点

    • 寻找到所要删除节点的上一个节点(prevNode)
    • 将prevNode中的指针指向当前要删除的这个节点的下一个节点

delete(node) {
// 第一种情况
if(node === this.head){
this.head = node.next;
return;
} // 查找所要删除节点的上一个节点
let prevNode = this.head;
while (prevNode.next !== node) {
prevNode = prevNode.next;
} // 第二种情况
if(node.next === null) {
prevNode.next = null;
} // 第三种情况
if(node.next) {
prevNode.next = node.next;
}
}

单向链表整体的代码


class ListNode {
constructor(key) {
this.next = null;
this.key = key;
}
} class List {
constructor() {
this.head = null;
this.length = 0;
} static createNode(key) {
return new ListNode(key);
} // 往头部插入数据
insert(node) {
// 如果head后面有指向的节点
if (this.head) {
node.next = this.head;
} else {
node.next = null;
}
this.head = node;
this.length++;
} find(key) {
let node = this.head;
while (node !== null && node.key !== key) {
node = node.next;
}
return node;
} delete(node) {
if (this.length === 0) {
throw 'node is undefined';
} if (node === this.head) {
this.head = node.next;
this.length--;
return;
} let prevNode = this.head; while (prevNode.next !== node) {
prevNode = prevNode.next;
} if (node.next === null) {
prevNode.next = null;
}
if (node.next) {
prevNode.next = node.next;
}
this.length--;
}
}

双向链表

如果你把上面介绍的单向列表都看明白了,那么这里介绍的双向列表其实差不多。

从上面的图可以很清楚的看到双向链表和单向链表的区别。双向链表多了一个指向上一个节点的指针。

初始化节点

  • 指向前一个节点的指针
  • 指向后一个节点的指针
  • 节点数据

class ListNode {
this.prev = null;
this.next = null;
this.key = key;
}

初始化双向链表

  • 头指针指向NULL

class List {
constructor(){
this.head = null;
}
}

创建节点


static createNode(key){
return new ListNode(key);
}

插入节点((插入到头节点之后)

  • 看上图中head后面的第一个节点可以知道,该节点的prev指向NULL
  • 节点的next指针指向后一个节点, 也就是当前头指针所指向的那个节点
  • 如果head后有节点,那么原本head后的节点的prev指向新插入的这个节点(因为是双向的嘛)
  • 最后将head指向新的节点

insert(node) {
node.prev = null;
node.next = this.head;
if(this.head){
this.head.prev = node;
}
this.head = node;
}

搜索节点

这里和单向节点一样,就直接贴代码了


search(key) {
let node = this.head;
while (node !== null && node.key !== key) {
node = node.next;
}
return node;
}

删除节点

和之前单向链表一样,分三种情况去看:

  • 删除的是第一个节点

    • head指向所要删除节点的下一个节点
    • 下一个节点的prev指针指向所要删除节点的上一个节点
  • 删除的是中间的某个节点

    • 所要删除的前一个节点的next指向所要删除的下一个节点
    • 所要删除的下一个节点的prev指向所要删除的前一个节点
  • 删除的是最后一个节点

    • 要删除的节点的上一个节点的next指向null(也就是指向删除节点的next所指的地址)


delete(node) {
const {prev,next} = node;
delete node.prev;
delete node.next;
if(node === this.head){
this.head = next;
}
if(next){
next.prev = prev;
}
if(prev){
prev.next = next;
}
}

双向链表整体代码


class ListNode {
constructor(key) {
// 指向前一个节点
this.prev = null;
// 指向后一个节点
this.next = null;
// 节点的数据(或者用于查找的键)
this.key = key;
}
} /**
* 双向链表
*/
class List {
constructor() {
this.head = null;
} static createNode(key) {
return new ListNode(key);
} insert(node) {
node.prev = null;
node.next = this.head;
if (this.head) {
this.head.prev = node;
}
this.head = node;
} search(key) {
let node = this.head;
while (node !== null && node.key !== key) {
node = node.next;
}
return node;
} delete(node) {
const { prev, next } = node;
delete node.prev;
delete node.next; if (node === this.head) {
this.head = next;
} if (prev) {
prev.next = next;
}
if (next) {
next.prev = prev;
}
}
}

总结

这里做一个小总结吧,可能有一部分人读到这里还不是特别的明白,我的建议是先好好看懂上面的单向链表。 其实只要你明白了链表的基础概念,就是有一个head,然后在有好多的节点(Node),然后用一个指针把他们串起来就好了,至于里面的插入操作也好,删除也好,其实都是在调整节点中指针的指向。

后续

后续可能还会是数据结构,可能是讲二叉堆,也可能回过头来讲一些队列和栈的思想在程序中的应用。欢迎大家指出文章的错误,如果有什么写作建议也可以提出。我会持续的去写关于前端的一些技术文章,如果大家喜欢的话可以关注一下哈

来源:https://segmentfault.com/a/1190000017569816

js数据结构-链表的更多相关文章

  1. (js描述的)数据结构[链表](4)

    (js描述的)数据结构 [链表](4) 一.基本结构 二.想比于数组,链表的一些优点 1.内存空间不是必须连续的,可以充分利用计算机的内存,事项灵活的内存动态管理. 2.链表不必再创建时就确定大小,并 ...

  2. JS数据结构第三篇---双向链表和循环链表之约瑟夫问题

    一.双向链表 在上文<JS数据结构第二篇---链表>中描述的是单向链表.单向链表是指每个节点都存有指向下一个节点的地址,双向链表则是在单向链表的基础上,给每个节点增加一个指向上一个节点的地 ...

  3. JS数据结构与算法-概述

    JS数据结构与算法概述 数据结构: 计算机存储, 组织数据的方式, 就像锅碗瓢盆 算法: 一系列解决问题的清晰指令, 就像食谱 两者关系: 程序 = 数据结构 + 算法 邂逅数据结构与算法 什么是数据 ...

  4. Python—数据结构——链表

    数据结构——链表 一.简介 链表是一种物理存储上非连续,数据元素的逻辑顺序通过链表中的指针链接次序,实现的一种线性存储结构.由一系列节点组成的元素集合.每个节点包含两部分,数据域item和指向下一个节 ...

  5. JS数据结构与算法——栈

    JS数据结构与算法--栈 1.栈结构概念 栈(Stack)是一种先进后出(LIFO Last in First out)的线性表,先进栈的将会比后进栈的先出栈. 栈的限制是仅允许在一端进行插入和删除运 ...

  6. 数据结构和算法(Golang实现)(12)常见数据结构-链表

    链表 讲数据结构就离不开讲链表.因为数据结构是用来组织数据的,如何将一个数据关联到另外一个数据呢?链表可以将数据和数据之间关联起来,从一个数据指向另外一个数据. 一.链表 定义: 链表由一个个数据节点 ...

  7. Redis数据结构—链表与字典的结构

    目录 Redis数据结构-链表与字典的结构 链表 Redis链表节点的结构 Redis链表的表示 Redis链表用在哪 字典 Redis字典结构总览 Redis字典结构分解 Redis字典的使用 Re ...

  8. Redis数据结构—链表与字典

    目录 Redis数据结构-链表与字典 链表 Redis链表节点的结构 Redis链表的表示 Redis链表用在哪 字典 Redis字典结构总览 Redis字典结构分解 哈希算法 解决键冲突 rehas ...

  9. JS数据结构之 Map

    JS数据结构之 Map Map介绍 Map(映射)是ES6引入的一种数据结构.这是一种存储键值对列表很方便的方法,类似于其他编程语言的哈希表. HashMap(哈希表),也叫做散列表.是根据关键码值 ...

随机推荐

  1. Java集合框架之LinkedHashSet

    简述 LinkedHashSet底层使用 LinkedHashMap 来保存所有元素,它继承自 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上 ...

  2. 大体知道java语法2----------理解面向对象

    我参加过大大小小n场面试,被好几位面试官问到过:能不能谈谈面向对象的几大特征?什么是面向对象?对于这两个问题,我始终觉得一定要理解,其实不只是这种概念题(姑且算它是概念题吧),包括各种语法都应该去理解 ...

  3. 服务器上class文件是新的,但就是执行的老代码

    故事是这样的. 上周末回老家,n个测试和开发找我,说我写的代码哪儿哪儿不行,吓得我赶紧打开电脑,连上阿里云数据库,修改了代码,测试们拉包重新测试后,还是不行,通过看打出的日志,还是执行的修改之前的代码 ...

  4. Jmeter设置成中文

    首次启动Jmeter为中文 选择后即变为中文

  5. [题解] [CF451E] Devu and Flowers

    题面 题解 就是一个求\(\sum_{i= 1}^{n}x _ i = m\)的不重复多重集的个数, 我们可以由容斥原理得到: \[ ans = C_{n + m - 1}^{n - 1} - \su ...

  6. 关于MongoDB在windows下安装【解压版】

    一.首先创建数据库存储文件夹和日志文件夹,在mongodb下创建即可,找起来也方便,日志文件:mongo.log,安装出错方便查阅问题: 二.启动MongoDB服务:注意:一定要用管理员身份运行DOS ...

  7. Java 注解指导手册(上)

      编者的话:注解是java的一个主要特性且每个java开发者都应该知道如何使用它.   我们已经在Java Code Geeks提供了丰富的教程, 如Creating Your Own Java A ...

  8. 邻居子系统 之 更新neigh_update

    概述 neigh_update函数用来更新指定的邻居项,更新内容是硬件地址和状态,更新之后,会根据新状态设置其输出函数,CONNECTED状态则使用快速输出,否则使用慢速输出:如果是由原来的无效状态变 ...

  9. TCP主动打开 之 第一次握手-发送SYN

    tcp客户端与服务器端建立连接需要经过三次握手过程,本文主要分析客户端主动打开中的第一次握手部分,即客户端发送syn段到服务器端: tcp_v4_connect为发起连接主流程,首先对必要参数进行检查 ...

  10. VMware NAT模式设置静态IP(可上网)

    在搞电商架构的高并发高可用时,需要在VMware新建几个linux虚拟机,如果使用VMware的默认网络是自动获取的,但有时候启动虚拟机IP地址会改变,使用很不方便,所以就整理一份静态IP地址设置的方 ...