前言:相对于结构比较简单的栈和队列,链表的结构就复杂一些。链表中的节点元素在存储元素值的同时,还包括了前后元素的的指引,又可以称之为指针。本篇博客主要记录了个人学习链表数据结构过程中的一些笔记,包含了基本的单向链表、双向链表、循环链表,以及补充了基于双向链表封装的栈结构方法。链表作为存储有序元素的集合,内部的元素在内存中并不是连续放置的。在增加和删除元素操作比较频繁的时候,链表由于不需要移动移动其他元素的位置相对于数组而言更加高效。

一、链表的相关概念以及和数组的区别

很多人都玩过寻宝游戏,就是最初得到一条线索,此条线索就是指向寻找下一条线索的地点的指针,每个地点都有下一条线索的地址,直到找到最终宝物。这和链表的结构及其相似。下图是最简单的链表结构示意图,也称之为单向链表结构。

数组:

1、创建的时候需要申请一段连续的内存空间,大部分语言中数组在初始化的时候长度就是固定的,当数组的容量不满足新的要求时,往往需要进行扩容操作,数组扩容很消耗资源。

2、在数组中插入和删除元素需要对其他的元素进行移动,操作成本很高。

链表:

1、链表中的元素在内存空间的存储不是连续的,内存空间的利用率很高。

2、链表中的每一个节点都是由一个存储元素值本身和指向下一个元素的引用所组成的。

作为一种数据结构,我们通常需要进行增删改查的操作。链表的方法也无非就是增删改查四个方面,接下来我们就先使用构造函数的方式封装一个单向链表结构,包含其基本属性和操作方法。

二、利用构造函数封装一个链表结构

利用构造函数封装一个链表结构如下,内部包含了一个创建子节点的构造函数。

​
function LinkList(){//1 内部节点类function Node(data){this.data=data;this.next=null;}//属性this.head=null;this.length=0;//方法//1、向列表的尾部添加一个新的项。this.append=(data)=>{let newNode=new Node(data);//链表里面如果为空,则直接将head指向插入的节点元素。if(this.length===0){this.head=newNode;    //将newNode赋值给this.head表示其指向。}else{//循环找到最后一个节点,while循环体可以实现查找到满足条件的元素let current=this.head;  //声明一个变量表示链表中的第一个节点。while(current.next!==null){current=current.next;}//并将其指向赋值给newNode完成在链表尾部添加元素current.next=newNode;}//插入元素后别忘了将链表的长度加1,也可以写成this.length++this.length+=1;}//2、insert方法,将元素插入到链表指定的位置this.insert=(position,data)=>{let newNode=new Node(data);//检查越界值,如果不在范围内,则返回falseif(position<0||position>this.length) return false;//当链表内部为空时,直接插入到第一个位置,默认的索引位置从1开始// this.append(data);// this.length+=1;if(position==0){//断开head和当前第一个元素的关系,将插入的元素的next指向当前第一个元素newNode.next=this.head;//再将head的指向调整为newNode。this.head=newNode;}else{//插入的位置不是第一个//声明一个变量存储第一个元素,以及一个变量接收当前元素的前一个元素//声明一个变量index作为查找指定位置的循环条件let index=0;let current=this.head;let previous=null;while(index++!=position){previous=current;   //存储当前位置元素current=current.next; //将当前元素指向下一个循环}//查找到指定位置后,将需要插入的元素位置的next指向current表示其向后移动一位newNode.next=current;//前一个元素的next指向需要插入的元素。previous.next=newNode}//插入元素完成后,链表长度增加1this.length+=1;return true;}//3、返回指定位置的元素值this.getPosition=(position)=>{if(position<0||position>=this.length) return null; let current=this.head; //存储第一个元素let index=0; //查找到满足条件的元素while(index++!==position){current=current.next;}return current.data;}//4、返回元素在列表中的索引。如果没有该元素,则返回-1//链表中查找元素要从第一个元素开始this.indexOf=(data)=>{let current=this.head;let index=0;//依旧使用当current为空作为循环退出条件while(current){if(current.data==data){return index;}current=current.next;index+=1;}//循环体结束还没有找到,表示元素不存在。return -1;}//5、修改某个元素的位置。即根据给定的位置信息将其替换成最新的this.update=(position,data)=>{//先做是否越界的判断if(position<0||position>=this.length) return false;let current=this.head;let index=0;while(index!==position){current=current.next;index+=1;}//找到了指定位置的元素,将其新传入的值重新赋值current.data=data;return true;}//6、将指定位置的移出一项this.removeAt=(position)=>{if(position<0||position>=this.length) return false;if(position==0){this.head=this.head.next;}let current=this.head;let previous=null;let index=0;while(index!==position){previous=current;current=current.next;index+=1;}previous.next=current.next; //断开指定元素的连接就是将指定元素的下一个元素和当前元素的前一个元素建立索引关系this.length-=1;return true;}//7、删除链表中的指定元素的一项。this.remove=(data)=>{//直接调用前面封装好的查找和删除方法let position=this.indexOf(data);//根据查找到的位置信息删除元素return this.removeAt(position);}//判断链表内容是否为空this.isEmpty=()=>{return this.length===0;}// 返回链表中元素的个数多少this.size=()=>{return this.length;}//toString方法this.toString=()=>{//1、获取链表中的第一个元素,实际上根据this.head表示的就是第一个元素。let current=this.head;//2、定义一个空的字符串来接收结果let resultString="";while(current){resultString+=current.data+" ";  //循环迭代字符串化。current=current.next;   //自身字符串化之后指向下一个节点,直到指向空退出循环}return resultString;}
}​

链表封装完成后,我们应该对链表中的方法进行测试,通过测试结果查看其是否达到了想要的效果。测试代码如下:

let links=new LinkList();
//1、测试向链表尾部添加元素的方法
links.append('lsx');
links.append('lls');
links.append('zdy');
console.log(links.toString())
//2、测试向链表的指定位置添加元素的方法
links.insert(3,'hyw');
//3、测试将链表中的数据字符串输出的方法
console.log(links.toString());
console.log(links.length);
//4、测试链表的长度是否正确
console.log(links.isEmpty());
//5、测试获取指定位置元素的方法
console.log(links.getPosition(3));
//6、查找指定元素在链表中索引值的方法
console.log(links.indexOf('lls'));
console.log(links.indexOf('zzz'));
//7、将链表中指定位置的元素进行更新的方法
console.log(links.update(1,'sjj'));
console.log(links.toString());
//8、移出指定位置元素的方法
console.log(links.removeAt(3));
console.log(links.toString());
//9、移出指定元素的方法
console.log(links.remove("zdy"));  //测试remove其返回值结果为true,证明查找到了此元素进行删除。
console.log(links.toString());

测试结果如下,证明封装的链表属性和方法均有效。

三、使用类封装一个双向链表结构

链表相对于数组而言,在删除和修改元素的时候效率更高,但其也存在着很多的局限性。比如我们可以依次往下查找到元素,但是有时候想回到上一个元素时,我们需要从链表首部重新开始往下查找。双向链表中的元素节点不仅包含了当前元素值和下一个元素的引用地址,还包括了对前一个元素的引用地址,对于我们提高链表的使用效率大有帮助。但是在处理其元素的插入、删除时,我们需要处理好的引用关系也相对较多,需要仔细理清楚引用之间的关系。双向链表的尾部也多了一个引用tail,可以通过其获取到链表的最后一个元素。

class DoubleNode{constructor(element){this.element=element;this.next=null;this.prev=null;}
}
class DoubleLinkList{constructor(){this.head=null;this.tail=null;this.length=0;}//1、向链表的尾部添加一个元素。append(element){//如果链表为空,则将元素插入到第一个位置let newNode=new DoubleNode(element)if(this.length===0){//head指向第一个节点,this.head实际上就是第一个元素。this.head=newNode;//tail指向最后一个节点,this.tail实际上就是最后一个元素this.tail=newNode;}else{//链表中存在一个tail表示尾部的节点元素,可以实现从尾部开始查找//需要先获取到当前最后一个节点,将其next指向新插入的节点,再改变其tail的指向this.tail.next=newNode;newNode.prev=this.tail;  //newNode的prev指向当前最后一个节点,this.tail=newNode;  //再改变this.tail的指向。}//链表长度增加1this.length+=1;}//2、向链表的指定位置插入一个元素,这也是相对情况最多的一种方法,要考虑清楚。insert(position,element){let newNode=new DoubleNode(element);//先做越界判断if(position<0||position>this.length) return false;//其实和单向链表类似,首先是需要找到指定位置的元素,然会对指引进行多一步的处理即可//但是需要充分考虑在首部和尾部插入元素时两个tail和head的指向都需要移动变化if(position==0){if(this.length==0){this.head=newNode;this.tail=newNode;}else{//链表中的首部存在了一个元素newNode.next=this.head;this.head.prev=newNode;this.head=newNode;}}else if(position==this.length){//在链表的末尾插入元素this.tail.next=newNode;newNode.prev=this.tail;this.tail=newNode;}else{//插入到链表的中间部分位置,先要求找到对应的位置,再进行插入操作。let current=this.head;let previous=null;let index=0;//通过while循环查找到对应的元素while(index<position){//保存当前元素previous=current;//继续赋值下一个元素current=current.next;//索引值加1index++;}//通过循环查找到了对应的元素值。//先断开建立右侧元素的关系newNode.next=current;current.prev=newNode;//再断开建立左侧的联系previous.next=newNode;newNode.prev=previous;} //完成插入后链表的长度加1this.length+=1;return true;
}//3、删除指定位置的元素,这个操作方法也要仔细考虑清楚。removeAt(position){if(position<0||position>=this.length) return null;//1 删除链表首部的元素?let current=this.head;if(position===0){//如果链表中仅有一个元素if(this.length===1){this.head=null;this.tail=null;}else{//链表中不只有一个元素this.head=this.head.next;this.head.prev=null;}  }else if(position===this.length-1){current=this.tail;current.prev.next=null;this.tail=this.tail.prev;}else{let previous=null;let index=0;while(index!==position){previous=current;current=current.next;index++;}//找到目标元素后,断开指向关系并建立新的指引关系//右侧写需要指向的值,左侧写相关节点元素的指引current.next.prev=previous;previous.next=current.next;//将链表的长度相应的减1this.length--;}return current.element;}//双向链表中的update方法其实和普通链表中的一样,找到元素后,进行替换掉update(position,newElement){if(position<0||position>=this.length) return false;//let newNode=new DoubleNode(position,newElement);let current=this.head;let index=0;while(index!==position){if(current.element==newElement){return current;}current=current.next;index++;} current.element=newElement;return true;}//返回指定元素值的下标索引值indexOf(newElement){let current=this.head;let index=0;  //作为定义的索引值数据返回//以current不为空作为循环判断条件while(current){if(current.element==newElement){return index;}current=current.next;index++;}return -1; //没有查找到,则返回空值。}isEmpty(){return this.length===0;}size(){return this.length;}//将链表中的所有数据以字符串的形式输出toString(){let resultString='';let current=this.head;//以当前元素current不为空作为循环遍历的条件while(current){resultString+=current.element+'  ';current=current.next;}return resultString;}
}​

测试双向链表的代码及其结果如下:

const dbl=new DoubleLinkList();
//1、测试向链表尾部添加元素的方法
dbl.append('lsx');
dbl.append('zdy');
dbl.append('hyw');
dbl.append('sx');
dbl.append('tz');
console.log(dbl.length);
console.log(dbl.toString())
//2、测试Insert方法
dbl.insert(3,'wsy');
dbl.insert(2,'sjj');
console.log(dbl.toString());
//3、测试指定位置删除元素,并返回所删除的元素的方法
console.log(dbl.removeAt(2));
//console.log(dbl.length);
console.log(dbl.toString());
//4、测试更新指定位置元素的方法
dbl.update(1,'dsd');
//5、测试返回指定元素下标索引的方法。
console.log(dbl.indexOf('dsd'));
console.log(dbl.toString());
console.log(dbl.length);

结果证明,上述使用类封装的双向链表属性以及方法封装完全符合条件。

双向链表和普通链表的区别主要在于增加和删除节点元素时,需要考虑的分类情况比较多,需要处理的指引值数量也较多,除了这两种常用到的链表,还有一些其他的链表结构。

四、循环链表和基于链表封装的栈

循环链表并不是什么新的数据结构,其可以如链表一样只有单向的引用,也可以向双向链表一样存在双向引用关系。循环链表的唯一特点就是,最后一个元素的指向下一个元素的指针(tail.next)不是undefined,而是应该指向第一个元素head。双向的循环链表有指向元素head的tail.next,也有指向tail元素的head.prev。循环链表结构示意图如下。

之前我们所封装的栈是基于数组的,在我们学习完链表结构之后,可以基于链表实现栈的的一些方法的封装,比较典型的方法就是出栈pop和入栈的方法push。

class StackLinkList{constructor(){//使用双向链表来存储数据this.items=new DoubleLinkList();}//1、将元素压入栈push(element){this.items.append(element)}//2、将元素出栈pop(){//先判断栈内是否为空,为空则返回Undefined;if(this.items.isEmpty()) return undefined;return this.items.removeAt(this.items.length-1);  //将栈顶的元素定向删除,也就是出栈}
}
//测试代码
let stack=new StackLinkList();
stack.push('111');
stack.push('222');
console.log(stack);
console.log(stack.pop('222'));

测试代码输出的结果如下:

五、总结归纳

链表是目前为止学到的相对复杂一点的数据结构,相对于之前的栈和队列,我们需要在增删的同时处理其节点元素中的签后元素的索引值。学习链表结构我们要抓住以下几点,学起来会相对轻松一点。

1、紧扣链表中的属性,如this.length,存储指向头部元素的head,指向尾部元素的tail。在进行增删操作时,我们通常需要先通过this.head以及this.tail来获取链表中的首部节点元素和尾部节点元素,再根据具体情况修改其索引值的指向,右侧写元素节点,左侧写赋予元素节点的指引,结构清晰易理解。

2、链表的操作比较抽象,在写的时候可以配合着画图加强理解,根据图解理清楚指引的关系。

3、充分考虑所有的情况,以及一些特殊情况,比如在插入删除时需要考虑在首部,尾部,还是中间位置插入元素各种情况。

学习完链表之后,下面就是进入到集合Set和字典Map的学习了,预计花费2到3天时间,届时将会整理出相应的学习笔记和一些思考。未完待续..........

学习JavaScript数据结构与算法进度117/293  0531

再远的路只要不断出发总能走完!

JavaScript数据结构与算法基础学习笔记03----链表与双向链表相关推荐

  1. Python 基础学习笔记 03

    Python基础系列 Python 基础学习笔记 01 Python 基础学习笔记 02 Python 基础学习笔记 03 Python 基础学习笔记 04 Python 基础学习笔记 05 文章目录 ...

  2. 数据结构与算法基础-学习-19-哈夫曼解码

    一.个人理解 哈夫曼树和哈夫曼编码相关概念.代码.实现思路分享,请看之前的文章链接<数据结构与算法基础-学习-17-二叉树之哈夫曼树>.<数据结构与算法基础-学习-18-哈夫曼编码& ...

  3. JavaScript数据结构与算法 基础

    - 栈 1.栈的应用场景 场景一:十进制转二进制 后出来的余数反而要排到前面 把余数依次入栈,就可以实现倒序输出 场景二:有效的括号 越靠前的左括号,对应的左括号越靠前. 左括号入栈,右括号出栈,最后 ...

  4. 数据结构与算法基础学习(一)

    http://www.cnblogs.com/yangwujun/archive/2012/12/29/2839038.html 基本概念和术语 1.数据(Data) 数据是外部世界信息的载体,它能够 ...

  5. Spring Boot基础学习笔记03:Spring Boot两种全局配置和两种注解

    文章目录 零.学习目标 1.掌握application.properties配置文件 2.掌握application.yaml配置文件 3.掌握使用@ConfigurationProperties注入 ...

  6. 【数据结构与算法】学习笔记-《算法笔记》-7

    查找元素 找x #include <cstdio> #include <cstring> #include <cmath> using namespace std; ...

  7. 【数据结构与算法】学习笔记——第一章 绪论1

    ✔前言: 新的专栏开启啦. 持续更新~ 关注我,我们一起学习

  8. 学习JavaScript数据结构与算法(一):栈与队列

    本系列的第一篇文章: 学习JavaScript数据结构与算法(一),栈与队列 第二篇文章:学习JavaScript数据结构与算法(二):链表 第三篇文章:学习JavaScript数据结构与算法(三): ...

  9. 2022年Spark基础学习笔记目录

    一.Spark学习笔记 在私有云上创建与配置虚拟机 Spark基础学习笔记01:初步了解Spark Spark基础学习笔记02:Spark运行时架构 Spark基础学习笔记03:搭建Spark单机版环 ...

  10. 2022年Spark基础学习笔记

    一.Spark学习笔记 在OpenStack私有云上创建与配置虚拟机 Spark基础学习笔记01:初步了解Spark Spark基础学习笔记02:Spark运行时架构 Spark基础学习笔记03:搭建 ...

最新文章

  1. 年度盛宴——2012年最精彩的15个 CSS3 教程
  2. c#_序列化与反序列化的应用
  3. acm pc^2的配置与使用
  4. spring mvc DispatcherServlet详解前传---HttpServletBean类
  5. 在项目中让Ajax面向对象 (二)
  6. C语言试题八十之统计单词个数
  7. Spring Security:基于内存的角色授权
  8. python 获取数据库字段类型_python中如何读取数据库数据类型
  9. EXCEL使用vlookup函数合并多个工作表
  10. [生存志] 第99节 白起奋威屠百万
  11. 接口测试主要做以下3个方面:
  12. Unity Shader - Specular mode: Specular parameter 高光模式中的高光参数
  13. 北京工作居住证续签流程条件及材料
  14. 负折射率波导matlab,宁波大学教授—董建峰
  15. pyQt5 学习笔记(18)QLineEdit 单行文本输入
  16. Qt笔记 之 QListWidget控件的使用
  17. Docker镜像原理及容器数据卷
  18. Java常见面试题—”static”关键字有什么用?
  19. 怎么让计算机唱歌视频教程,【唱歌视频教学】如何才能把一首歌曲唱好?!
  20. Python——format格式化函数

热门文章

  1. 卸载#流氓软件#快压的方法
  2. python调用按键精灵插件_按键精灵WQM软件使用说明书,资深老师讲解就是详细
  3. 计算机网络 MOOC 哈尔滨工程大学 pdf课件
  4. linux设备驱动程序第10章,linux中秒字符设备驱动(宋宝华设备驱动开发详解第10章)...
  5. springboot test
  6. 计算机考研复试之计算机网络
  7. win7升级Internet Explorer 11 先决条件更新
  8. DNF装备强化的算法分析与实现
  9. python 前端素材提供
  10. 2019年1月份整理的Unity3D游戏完整源码