JS中的算法与数据结构之链表(Linked-list)实例详解


Posted in Javascript onAugust 20, 2019

本文实例讲述了JS中的算法与数据结构之链表(Linked-list)。分享给大家供大家参考,具体如下:

链表(Linked-list)

前面我们讨论了如何使用栈、队列进行存数数据,他们其实都是列表的一种,底层存储的数据的数据结构都是数组。

但是数组不总是最佳的数据结构,因为,在很多编程语言中,数组的长度都是固定的,如果数组已被数据填满,再要加入新的元素是非常困难的。而且,对于数组的删除和添加操作,通常需要将数组中的其他元素向前或者向后平移,这些操作也是十分繁琐的。

然而,JS中数组却不存在上述问题,主要是因为他们被实现了成了对象,但是与其他语言相比(比如C或Java),那么它的效率会低很多。

这时候,我们可以考虑使用链表(Linked-list) 来替代它,除了对数据的随机访问,链表几乎可以在任何可以使用一维数组的情况中。如果你正巧在使用C或者Java等高级语言,你会发现链表的表现要优于数组很多。

链表其实有许多的种类:单向链表、双向链表、单向循环链表和双向循环链表,接下来,我们基于对象来实现一个单向链表,因为它的使用最为广泛。

链表的定义

首先,要实现链表,我们先搞懂一些链表的基本东西,因为这很重要!

链表是一组节点组成的集合,每个节点都使用一个对象的引用来指向它的后一个节点。指向另一节点的引用讲做链。下面我画了一个简单的链接结构图,方便大家理解。

JS中的算法与数据结构之链表(Linked-list)实例详解 
链表结构图

其中,data中保存着数据,next保存着下一个链表的引用。上图中,我们说 data2 跟在 data1 后面,而不是说 data2 是链表中的第二个元素。上图,值得注意的是,我们将链表的尾元素指向了 null 节点,表示链接结束的位置。

由于链表的起始点的确定比较麻烦,因此很多链表的实现都会在链表的最前面添加一个特殊的节点,称为 头节点,表示链表的头部。进过改造,链表就成了如下的样子:

JS中的算法与数据结构之链表(Linked-list)实例详解 
有头节点的链表

向链表中插入一个节点的效率很高,需要修改它前面的节点(前驱),使其指向新加入的节点,而将新节点指向原来前驱节点指向的节点即可。下面我将用图片演示如何在 data2 节点 后面插入 data4 节点。

JS中的算法与数据结构之链表(Linked-list)实例详解 
插入节点

同样,从链表中删除一个节点,也很简单。只需将待删节点的前驱节点指向待删节点的,同时将待删节点指向null,那么节点就删除成功了。下面我们用图片演示如何从链表中删除 data4 节点。

JS中的算法与数据结构之链表(Linked-list)实例详解 
删除节点

链表的设计

我们设计链表包含两个类,一个是 Node 类用来表示节点,另一个事 LinkedList 类提供插入节点、删除节点等一些操作。

Node类

Node类包含连个属性: element 用来保存节点上的数据,next 用来保存指向下一个节点的链接,具体实现如下:

//节点
function Node(element) {
  this.element = element;  //当前节点的元素
  this.next = null;     //下一个节点链接
}

LinkedList类

LinkedList类提供了对链表进行操作的方法,包括插入删除节点,查找给定的值等。值得注意的是,它只有一个属性,那就是使用一个 Node 对象来保存该链表的头节点。

它的构造函数的实现如下:

//链表类
function LList () {
  this.head = new Node( 'head' );   //头节点
  this.find = find;          //查找节点
  this.insert = insert;        //插入节点
  this.remove = remove;        //删除节点
  this.findPrev = findPrev;      //查找前一个节点
  this.display = display;       //显示链表
}

head节点的next属性初始化为 null ,当有新元素插入时,next会指向新的元素。

接下来,我们来看看具体方法的实现。

insert:向链表插入一个节点

我们先分析分析insert方法,想要插入一个节点,我们必须明确要在哪个节点的前面或后面插入。我们先来看看,如何在一个已知节点的后面插入一个节点。

在一个已知节点后插入新节点,我们首先得找到该节点,为此,我们需要一个 find 方法用来遍历链表,查找给定的数据。如果找到,该方法就返回保存该数据的节点。那么,我们先实现 find 方法。

find:查找给定节点

//查找给定节点

function find ( item ) {
  var currNode = this.head;
  while ( currNode.element != item ){
    currNode = currNode.next;
  }
  return currNode;
}

find 方法同时展示了如何在链表上移动。首先,创建一个新节点,将链表的头节点赋给这个新创建的节点,然后在链表上循环,如果当前节点的 element 属性和我们要找的信息不符,就将当前节点移动到下一个节点,如果查找成功,该方法返回包含该数据的节点;否则,就会返回null。

一旦找到了节点,我们就可以将新的节点插入到链表中了,将新节点的 next 属性设置为后面节点的 next 属性对应的值,然后设置后面节点的 next 属性指向新的节点,具体实现如下:

//插入节点

function insert ( newElement , item ) {
  var newNode = new Node( newElement );
  var currNode = this.find( item );
  newNode.next = currNode.next;
  currNode.next = newNode;
}

现在我们可以测试我们的链表了。等等,我们先来定义一个 display 方法显示链表的元素,不然我们怎么知道对不对呢?

display:显示链表

//显示链表元素

function display () {
  var currNode = this.head;
  while ( !(currNode.next == null) ){
    console.log( currNode.next.element );
    currNode = currNode.next;
  }
}

实现原理同上,将头节点赋给一个新的变量,然后循环链表,直到当前节点的 next 属性为 null 时停止循环,我们循环过程中将每个节点的数据打印出来就好了。

var fruits = new LList();

fruits.insert('Apple' , 'head');
fruits.insert('Banana' , 'Apple');
fruits.insert('Pear' , 'Banana');

console.log(fruits.display());    // Apple
                   // Banana
                   // Pear

remove:从链表中删除一个节点

从链表中删除节点时,我们先要找个待删除节点的前一个节点,找到后,我们修改它的 next 属性,使其不在指向待删除的节点,而是待删除节点的下一个节点。那么,我们就得需要定义一个 findPrevious 方法遍历链表,检查每一个节点的下一个节点是否存储待删除的数据。如果找到,返回该节点,这样就可以修改它的 next 属性了。 findPrevious 的实现如下:

//查找带删除节点的前一个节点

function findPrev( item ) {
  var currNode = this.head;
  while ( !( currNode.next == null) && ( currNode.next.element != item )){
    currNode = currNode.next;
  }
  return currNode;
}

这样,remove 方法的实现也就迎刃而解了

//删除节点

function remove ( item ) {
  var prevNode = this.findPrev( item );
  if( !( prevNode.next == null ) ){
    prevNode.next = prevNode.next.next;
  }
}

我们接着写一段测试程序,测试一下 remove 方法:

// 接着上面的代码,我们再添加一个水果

fruits.insert('Grape' , 'Pear');
console.log(fruits.display());   // Apple
                  // Banana
                  // Pear
                  // Grape

// 我们把香蕉吃掉

fruits.remove('Banana');
console.log(fruits.display());   // Apple
                  // Pear
                  // Grape

Great!成功了,现在你已经可以实现一个基本的单向链表了。

双向链表

尽管从链表的头节点遍历链表很简单,但是反过来,从后向前遍历却不容易。我们可以通过给Node类增加一个previous属性,让其指向前驱节点的链接,这样就形成了双向链表,如下图:

JS中的算法与数据结构之链表(Linked-list)实例详解 
双向链表

此时,向链表插入一个节点就要更改节点的前驱和后继了,但是删除节点的效率提高了,不再需要寻找待删除节点的前驱节点了。

双向链表的实现

要实现双向链表,首先需要给 Node 类增加一个 previous 属性:

//节点类

function Node(element) {
  this.element = element;  //当前节点的元素
  this.next = null;     //下一个节点链接
  this.previous = null;   //上一个节点链接
}

双向链表的 insert 方法与单链表相似,但需要设置新节点的 previous 属性,使其指向该节点的前驱,定义如下:

//插入节点
function insert ( newElement , item ) {
  var newNode = new Node( newElement );
  var currNode = this.find( item );
  newNode.next = currNode.next;
  newNode.previous = currNode;
  currNode.next = newNode;
}

双向链表的删除 remove 方法比单链表效率高,不需要查找前驱节点,只要找出待删除节点,然后将该节点的前驱 next 属性指向待删除节点的后继,设置该节点后继 previous 属性,指向待删除节点的前驱即可。定义如下:

//删除节点

function remove ( item ) {
  var currNode = this.find ( item );
  if( !( currNode.next == null ) ){
    currNode.previous.next = currNode.next;
    currNode.next.previous = currNode.previous;
    currNode.next = null;
    currNode.previous = null;
  }
}

还有一些反向显示链表 dispReverse,查找链表最后一个元素 findLast 等方法,相信你已经有了思路,这里我给出一个基本双向链表的完成代码,供大家参考。

//节点
 
function Node(element) {
  this.element = element;  //当前节点的元素
  this.next = null;     //下一个节点链接
  this.previous = null;     //上一个节点链接
}

//链表类

function LList () {
  this.head = new Node( 'head' );
  this.find = find;
  this.findLast = findLast;
  this.insert = insert;
  this.remove = remove;
  this.display = display;
  this.dispReverse = dispReverse;
}

//查找元素

function find ( item ) {
  var currNode = this.head;
  while ( currNode.element != item ){
    currNode = currNode.next;
  }
  return currNode;
}

//查找链表中的最后一个元素

function findLast () {
  var currNode = this.head;
  while ( !( currNode.next == null )){
    currNode = currNode.next;
  }
  return currNode;
}


//插入节点

function insert ( newElement , item ) {
  var newNode = new Node( newElement );
  var currNode = this.find( item );
  newNode.next = currNode.next;
  newNode.previous = currNode;
  currNode.next = newNode;
}

//显示链表元素

function display () {
  var currNode = this.head;
  while ( !(currNode.next == null) ){
    console.debug( currNode.next.element );
    currNode = currNode.next;
  }
}

//反向显示链表元素

function dispReverse () {
  var currNode = this.findLast();
  while ( !( currNode.previous == null )){
    console.log( currNode.element );
    currNode = currNode.previous;
  }
}

//删除节点

function remove ( item ) {
  var currNode = this.find ( item );
  if( !( currNode.next == null ) ){
    currNode.previous.next = currNode.next;
    currNode.next.previous = currNode.previous;
    currNode.next = null;
    currNode.previous = null;
  }
}

var fruits = new LList();

fruits.insert('Apple' , 'head');
fruits.insert('Banana' , 'Apple');
fruits.insert('Pear' , 'Banana');
fruits.insert('Grape' , 'Pear');

console.log( fruits.display() );    // Apple
                    // Banana
                    // Pear
                    // Grape
                    
console.log( fruits.dispReverse() );  // Grape
                    // Pear
                    // Banana
                    // Apple

循环链表

循环链表和单链表相似,节点类型都是一样,唯一的区别是,在创建循环链表的时候,让其头节点的 next 属性执行它本身,即

head.next = head;

这种行为会导致链表中每个节点的 next 属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,形成了一个循环链表,如下图所示:

JS中的算法与数据结构之链表(Linked-list)实例详解 
循环链表

原理相信你已经懂了,循环链表这里就不贴代码了,相信你自己能独立完成!

至此,我们对链表有了比较深刻的认识,如果想让我们的链表更加健全,我们还可发挥自己的思维,给链表添加比如向前移动几个节点,向后移动几个节点,显示当前节点等方法,大家一起加油!

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行效果。

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
JavaScript定义类的几种方式总结
Jan 06 Javascript
微信分享的标题、缩略图、连接及描述设置方法
Oct 14 Javascript
node.js中的fs.lchownSync方法使用说明
Dec 16 Javascript
跟我学习javascript的异步脚本加载
Nov 20 Javascript
深入理解js promise chain
May 05 Javascript
如何利用Promises编写更优雅的JavaScript代码
May 17 Javascript
jQuery使用经验小技巧(推荐)
May 31 Javascript
Webpack如何引入bootstrap的方法
Jun 17 Javascript
vue-router 导航钩子的具体使用方法
Aug 31 Javascript
Angular中的ng-template及angular 使用ngTemplateOutlet 指令的方法
Aug 08 Javascript
详解vue2.0模拟后台json数据
May 16 Javascript
Vue中关闭弹窗组件时销毁并隐藏操作
Sep 01 Javascript
使用 webpack 插件自动生成 vue 路由文件的方法
Aug 20 #Javascript
微信小程序webview与h5通过postMessage实现实时通讯的实现
Aug 20 #Javascript
JS中的算法与数据结构之队列(Queue)实例详解
Aug 20 #Javascript
JS中的算法与数据结构之栈(Stack)实例详解
Aug 20 #Javascript
小程序如何在不同设备上自适应生成海报的实现方法
Aug 20 #Javascript
使用 Vue 实现一个虚拟列表的方法
Aug 20 #Javascript
基于vue手写tree插件的那点事儿
Aug 20 #Javascript
You might like
php计算数组不为空元素个数的方法
2014/01/27 PHP
php英文单词统计器
2016/06/23 PHP
Yii支持多域名cors原理的实现
2018/12/05 PHP
JavaScript 验证浏览器是否支持javascript的方法小结
2009/05/17 Javascript
基于jQuery的消息提示插件之旅 DivAlert(三)
2010/04/01 Javascript
jquery中文乱码的多种解决方法
2013/06/21 Javascript
Jquery 点击按钮自动高亮实现原理及代码
2014/04/25 Javascript
JQuery ztree带筛选、异步加载实例讲解
2016/02/25 Javascript
全面解析DOM操作和jQuery实现选项移动操作代码分享
2016/06/07 Javascript
Node.js如何自动审核团队的代码
2016/07/20 Javascript
微信小程序 后台https域名绑定和免费的https证书申请详解
2016/11/10 Javascript
JavaScript实现form表单的多文件上传
2020/03/27 Javascript
jQuery实现上传图片前预览效果功能
2017/08/03 jQuery
js获取css的各种样式并且设置他们的方法
2017/08/22 Javascript
Puppeteer 爬取动态生成的网页实战
2018/11/14 Javascript
layui table复选框禁止某几条勾选的实例
2019/09/20 Javascript
解决vue自定义指令导致的内存泄漏问题
2020/08/04 Javascript
[06:14]《辉夜杯》外卡赛附加赛 4支战队巡礼
2015/10/23 DOTA
[52:37]完美世界DOTA2联赛循环赛 Forest vs DM BO2第一场 10.29
2020/10/29 DOTA
python转换摩斯密码示例
2014/02/16 Python
Python 正则表达式入门(初级篇)
2016/12/07 Python
python中logging库的使用总结
2017/10/18 Python
Python Json模块中dumps、loads、dump、load函数介绍
2018/05/15 Python
从numpy数组中取出满足条件的元素示例
2019/11/26 Python
使用IPython或Spyder将省略号表示的内容完整输出
2020/04/20 Python
HTML5 canvas画矩形时出现边框样式不一致的解决方法
2013/10/14 HTML / CSS
德国知名健康零食网上商店:Seeberger
2017/07/27 全球购物
将"引用"作为函数返回值类型的格式、好处和需要遵守的规则
2016/02/09 面试题
几个SQL的面试题
2014/03/08 面试题
学生自我评语大全
2014/04/18 职场文书
中学生民族团结演讲稿
2014/08/27 职场文书
分居协议书范本(律师见证版)
2014/11/26 职场文书
2015年档案管理工作总结
2015/04/08 职场文书
美甲店的创业计划书模板
2019/08/23 职场文书
如何制作自己的原生JavaScript路由
2021/05/05 Javascript
为自由献出你的心脏!「进击的巨人展 FINAL」2022年6月在台开展
2022/04/13 日漫