前面讲解了数组,栈和队列。其实大家回想一下。它们有很多相似的地方。甚至栈和队列这两种数据结构在js中的实现方式也都是基于数组。无论增删的方式、遵循的原则如何,它们都是有序集合的列表。在js中,我们新建一个数组并不需要限定他的大小也就是长度,但是实际上,数组的底层仍旧为初始化的数组设置了一个长度限制。我们想要在数组中任意的插入和删除元素的成本很高,虽然在js中我们有便捷的方法可以操作数组,但是其底层原理仍旧是这样的。只是我们对它并没有感觉,比如在java中,声明一个数组是必须要限制它的长度的。并且在扩容的情况下,操作起来也不是十分方便。这就需要用到其它的数据结构来应对我们不同的需要,比如链表。

  链表存储有序的元素的集合,但是和数组不同的是,链表中的元素在内存中的存储并不是连续的。每一个链表元素都包含了一个存储元素本身的节点一个指向下一个元素的引用。看起来就像这样:

  相对于传统的数组,链表的一个好处就是增删的时候无需移动其它元素,只要更改指针的指向就可以了。但是缺点就是如果想要访问链表中的元素,需要从头开始循环迭代到你想要的元素。

  那么简单介绍了什么是链表之后,我们看看如何用js来实现链表,同样的链表有其自身的几种方法。

  1、append(element),向列表尾部添加一个新的元素,注意这里所指的列表并不是我们想象中的有序列表,链表是无序的。

  2、insert(position,element),在链表的指定位置插入一个新的元素。

  3、remove(element),从列表中移除一项。

  4、indexOf(element),返回该元素在列表中的索引,如果列表中没有该元素就返回-1。

  5、removeAt(position),从列表的指定位置移除元素。

  6、isEmpty(),判断该链表是否为空

  7、size(),返回该链表包含的元素个数。

  8、toString(),返回链表元素的字符串值。

  以上描述了链表包含的各种方法,其实说到底也就是增删改查,任何的数据结构的方法种类也就几乎如此。下面我们来看下具体的实现。

 function LinkedList(){
let Node = function (element) {
this.element = element;
this.next = null;
} let length = 0;
let head = null; this.append = function (element) {};
this.insert = function (position,element){};
this.removeAt = function (position) {};
this.remove = function (element) {};
this.indexOf = function (element) {};
this.isEmpty = function () {};
this.size = function () {};
this.getHead = function () {};
this.toString = function () {};
this.print = function () {};
}

  这是整个LinkedList类的基本架子,其中Node类就是我们链表中的每一个节点元素,每一个节点元素都包含一个自身的值(element)和指向下一个节点的指针(next),length自然就是我们记录链表长度的变量,而head是指向第一个元素的指针,初始值跟next是一样的,都是null。

  既然架子搭完了,我们接下来看看如何实现每一个具体的方法。链表的方法要比栈或队列的实现稍微复杂些,希望大家仔细阅读。

  代码着实有点长,注释是重点,如果你认真读下来,链表的基本构成和原理想必你也就理解了。

// 下面的所有的注释所解释的语句都是注释下面的语句。以下所有的“节点元素”都代表node
function LinkedList(){
//node才是链表中的单独元素,但是这个元素中又包含自身的值和指向下一个node的指针
let Node = function (element) {
//node的自身元素
this.element = element;
/*
这个next要特别注意,它在理论上是指向链表下一个节点元素的指针,但是在js的实现中,其实这个指针不过是一个对象的索引,而这个索引所包含的就是下一个node
就像是这样{element:1,next:{element,next:{element:3,next...}}},这种对象的一层层嵌套,这样也可以解释了为什么在中间插入链表元素时,
需要一层一层的迭代到需要插入的位置。
*/
/*
换句话说,这里的next指针,指向的是下一个node节点元素的整体,不单单只是node中的element元素。
*/
this.next = null;
} let length = 0;//链表长度初始化
let head = null;//在链表中,我们需要存储第一个节点元素的引用,也就是head,在没有节点元素的时候初始化为null。
// append方法类似于js数组的push,向链表的尾部添加节点元素。
// 在append方法中有两种情况,一种是没有节点元素,链表的长度是0,另一种是已经存在了至少一个节点元素,应对这两种不同的情况会有不同的操作。
this.append = function (element) {
//声明变量,append添加的element应该是node,所以通过Node类进行包装
let node = new Node(element);
//这里就存在了一个问题,那么就是我们在给链表添加节点元素的时候只有head的引用,也就是我们只知道head是什么,但是其他的我们一概不知。
//所以这里声明一个current变量,用来存储我们当前的节点是什么。
let current;
//这里,如果head是null,说明该链表是没有节点元素的,因为有节点元素的话head不可能为null(head会指向第一个节点元素),那么既然如此,我们的head=node就可以了。
//还有,这里的“=”,实在是让人很迷茫,既然是指针,为什么要“赋值”?
//因为无论是head、node.next(链表节点元素的指针)还是current还是下面会声明的previous。都是存储当前位置信息的一个存储器。
//也就是说,这些变量所代表的是一个值信息的存储,他们存储的值代表他们所指向的节点元素。
//嗯,,,,希望我说明白了。。。。
console.log(head);//你可以看到head以及链表在js中展现大概是什么样的。
if(head === null) {
head = node;
} else {
//这里,如果head!=null,说明该链表至少有一个节点元素,那么当前的current自然就是head,因为我们要从head开始迭代到结尾。
current = head;
//这里容易让人疑惑的地方是current.next是啥?
//上面current已经是head了,那么无论是只有一个节点元素还是多个节点元素,最后一个节点元素的next必为null,别问我为啥了。
//所以这里只要current.next不为null(也就是有实际意义的值),那么就循环到current.next是null为止。
//因为只有这样才说明当前的current是链表中的最后一个节点元素
while(current.next) {
current = current.next;
}
//既然我们找到了链表中的最后一个节点元素,那么把该节点元素的next=node就好了。
//那么这里还要说的是,每一个新node的next必然是null,嗯,就是这么定义的,没有为啥。
//所以在我们将current.next指向node的时候,链表最后一个节点元素的指向自然就是null了。
current.next = node;
}
// 嗯...别忘了增加一个单位长度
length++;
};
// 在链表的任意“合法”位置插入节点元素,position代表要插入的位置,element不多说,代表要插入的元素。
this.insert = function (position,element){
// 这个判断比较有趣,如果position小于0并且大于该链表的长度,说明这个position不合法。直接返回false
//如果在大于等于0并且小于等于length,OK,插入位置合法,继续...
if(position >= 0 && position <= length) {
//同样的,要建个node
let node = new Node(element),
// 同样的,当前的current是head
current = head,
// 新增了一个previous,这个previous是为了衔接需要插入的节点元素的。
previous,
// 这个index不是length,它是为了记录限定循环的计数器,作用类似于current和previous。
index = 0;
// 这里,如果position是0,意味着我要在头部插入元素。
if(position === 0) {
// 那么自然,新建的节点元素的指针(next)就指向了当前元素。而head自然就是新建的节点元素(node)了。
node.next = current;
head = node;
} else {
// 那么如果想要在除了第一个元素的其他位置插入元素。
// 在没有到达想要插入的位置的时候,我们需要迭代替换previous和current,使其依次的往后移动。
while (index++ < position) {
//这里就是每一次的移动,前一个等于当前,当前的又变成了下一个(就这样依次移动到指定的position位置)
previous = current;
current = current.next;
}
//那么在到达了这个位置后,我们需要把新建的node节点元素插入近previous和current。
//也就是改变node节点元素和previous的指针。使node节点元素指向当前的current。而previous的指针指向node。
//这样也就完成了节点元素在指定位置的插入
node.next = current;
previous.next = node;
}
//插入成功,长度增加一个单位并返回true
length ++ ;
return true;
} else {
return false;
}
};
// 这个方式是移除制定位置的节点元素。
this.removeAt = function (position) {
//同样的合法值范围限制。
if(position > -1 && position < length) {
//同样的变量声明。
let current = head,previous,index = 0;
//这里比较有趣,如果是要移除第一个节点元素,那么直接把head的指针指向当前节点元素(current)的下一个(.next)就可以了。
//因为我们中断了head和current的链接,直接使current不存在于链表中了,这样我们无论如何迭代都获取不到此时的current。
//这样操作之后,我们只要等待js垃圾回收器回收它就好了。
if(position === 0) {
head = current.next;
} else {
//同样的迭代移动
while (index++ < position) {
previous = current;
current = current.next;
}
//这里我们迭代到了我们想要移除的元素的位置,同样中断了current的在链表中的链接。也就删除了该节点元素
previous.next = current.next;
}
//长度减少一个单位。
length --;
//返回删除的元素值
return current.element;
} else {
return null;
}
};
//获取该元素在链表中的位置
this.indexOf = function (element) {
let current = head,index = 0;//不解释了,这里index为0,因为要从链表的第一个0位开始遍历
//那么这里又比较有趣了,这里的current无非两种情况,null或者一个具体的值。
//如果是null,说明该链表是空的,不然current=head不可能为null
//如果不为null,继续判断
while(current) {
//那么如果current不为null,并且如果element和current的element相等。说明找到了,直接返回index
if(element === current.element) {
return index;
}
//这里其实可以是为上面if判断的else分支,如果不相等,那么计数器index就加一个单位,并且current指针往后移动。
index ++;
current = current.next;
} return -1;
};
// 我们既然有了indexOf和removeAt,这个remove方法我就不多说了。
this.remove = function (element) {
let index = this.indexOf(element);
console.log(index)
return this.removeAt(index);
};
// 下面的方法也都很简单,无需多说
this.isEmpty = function () {
return length === 0;
};
this.size = function () {
return length;
};
this.getHead = function () {
return head;
};
this.toString = function () {
let current = head,string = ''; while(current) {
string += current.element + (current.next ? 'n' : '');
current = current.next;
} return string;
};
this.print = function () {
console.log(this.toString());
};
} var list = new LinkedList(); list.append(1);
list.append(2);
list.append(3);
list.append(4);
list.append(5);
list.print();//1n2n3n4n5
list.insert(2,99);
list.print();//1n2n99n3n4n5
list.removeAt(1);
list.print();//2n3n4n5

  这就是基本的链表实现方式了。大家在实践的时候可以先去掉注释,自己思索一遍敲一遍代码,然后回过头来带着疑问看注释。我相信会有不小的帮助。

  那么这一篇尽量不写的那么长。到这里就告一段落。下一篇文章会详细的介绍一下双向链表以及其实现的方式。

  最后,由于本人水平有限,能力与大神仍相差甚远,若有错误或不明之处,还望大家不吝赐教指正。非常感谢!

用js来实现那些数据结构07(链表01-链表的实现)的更多相关文章

  1. 用js来实现那些数据结构(栈01)

    其实说到底,在js中栈更像是一种变种的数组,只是没有数组那么多的方法,也没有数组那么灵活.但是栈和队列这两种数据结构比数组更加的高效和可控.而在js中要想模拟栈,依据的主要形式也是数组. 从这篇文章开 ...

  2. 用js来实现那些数据结构—目录

    首先,有一点要声明,下面所有文章的所有内容的代码,都不是我一个人独立完成的,它们来自于一本叫做<学习JavaScript数据结构和算法>(第二版),人民邮电出版社出版的这本书.github ...

  3. 用js来实现那些数据结构及算法—目录

    首先,有一点要声明,下面所有文章的所有内容的代码,都不是我一个人独立完成的,它们来自于一本叫做<学习JavaScript数据结构和算法>(第二版),人民邮电出版社出版的这本书.github ...

  4. js数据结构与算法--单链表的实现与应用思考

    链表是动态的数据结构,它的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成. 现实中,有一些链表的例子. 第一个就是寻宝的游戏.你有一条线索,这条线索是指向寻找下一条线 ...

  5. (js描述的)数据结构 [数组的一些补充](1)

    (js描述的)数据结构 [数组的一些补充](1) 1. js的数组: 1.优点:高度封装,对于数组的操作就是调用API 2.普通语言的数组: 1.优点:根据index来查询,修改数据效率很高 2.缺点 ...

  6. JavaScript 数据结构与算法3(链表)

    学习数据结构的 git 代码地址: https://gitee.com/zhangning187/js-data-structure-study 1.链表 本章学习如何实现和使用链表这种动态的数据结构 ...

  7. FreeRTOS数据结构(一)--链表和链表项

    结构体定义 /*链表结构体*/ typedef struct xLIST { listFIRST_LIST_INTEGRITY_CHECK_VALUE /*用于链表完整性检查*/ configLIST ...

  8. 数据结构——基于java的链表实现(真正理解链表这种数据结构)

    原创不易,如需转载,请注明出处https://www.cnblogs.com/baixianlong/p/10759599.html,否则将追究法律责任!!! 一.链表介绍 1.什么是链表? 链表是一 ...

  9. 自己动手实现java数据结构(二) 链表

    1.链表介绍 前面我们已经介绍了向量,向量是基于数组进行数据存储的线性表.今天,要介绍的是线性表的另一种实现方式---链表. 链表和向量都是线性表,从使用者的角度上依然被视为一个线性的列表结构.但是, ...

随机推荐

  1. Java虚拟机之Java内存区域

    Java虚拟机运行时数据区域 ⑴背景:对于c/c++来说程序员来说,需要经常去关心内存运行情况,但对于Java程序员,只需要在必要时关心内存运行情况,这是因为在Java虚拟机自动内存管理机制的帮助下, ...

  2. (译文)React----React应用程序流式服务端渲染

    好处 React16推出了流式服务端渲染,它允许你并行地分发HTML片段.这样可以让渲染 出的首字节有意义的内容给用户速度更快. (例子1,上面部分是一次性转换,下面是流渲染,两种方式) 而且相对re ...

  3. 20145237 实验一 逆向与Bof基础

    20145237 实验一 逆向与Bof基础 1.直接修改程序机器指令,改变程序执行流程 此次实验是下载老师传给我们的一个名为pwn1的文件. 首先,用 objdump -d pwn1 对pwn1进行反 ...

  4. Linux进程间通信--信号量

    信号量绝对不同于信号,一定要分清,关于信号,上一篇博客中已经说过,如有疑问,请移驾! 信号量 一.是什么   信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件 ...

  5. 实验二Java面向对象程序设计实验报告(2)

    实验二 Java面向对象程序设计 实验概述: 课程:程序设计与数据结构 班级:1623班 姓名: 邢天岳 学号:2309 指导老师:娄老师 王老师 实验日期:2017.4.16 实验名称: Java面 ...

  6. GPUImage实战问题解决

    在项目中遇到了使用完GPUImage以后,内存不释放的问题,翻阅官方API,找到了解决方法: deinit{ GPUImageContext.sharedImageProcessingContext( ...

  7. centos7 编译安装greenplum5.7

    一.配置系统 安装是以一个主节点,三个子节点进行安装.gp是在github上下载的5.7的源码.地址https://github.com/greenplum-db/gpdb/tree/5.7.0. 1 ...

  8. 解决IE8下opacity属性失效问题

    由于opacity属性存在兼容性问题,所以在IE8下,用opacity来设置元素的透明度,会失效,从而导致页面的样式问题. 在IE8及其更早的浏览器下,我们可以使用filter属性,来代替opacit ...

  9. maven常见问题处理(3-2)maven打包时跳过测试的几个方法

    运行mvn install时跳过Test方法一:<project> [...] <build> <plugins> <plugin> <group ...

  10. linux下Tab及shell 补全python

    Python自动补全 Python自动补全有vim编辑下和python交互模式下,下面分别介绍如何在这2种情况下实现Tab键自动补全. vim python自动补全插件:pydiction 可以实现下 ...