前言:
很感谢每一位关注二哥的朋友,在各位朋友的帮助下,二哥也学到了非常多的东西。也希望通过自己的学习到的知识,分享给每个需要的朋友。如果在文章中写的有错误的地方,还希望各位朋友多多指出。有更好的思路,想法也可以留言或者私聊我。不甚感激。
简介:
链表在数据结构中,是一个非常基础也是比较好理解的一个数据结构。每个不同的语言实现起来有细微差距,但是总体思路还是一样的。今天给大家介绍的是单向链表,单向链表和双向链表的区别就是少了一个父节点的引用。
链表的结构是由一个根节点包含两个字段,一个value当前节点的值,next下一个节点的引用。数组与链表非常相似,但是数组的结构是在内存中开辟出一整块空间,根据内容长度的不同来对这块空间进行扩容和减少空间。数组是紧密型的数据结构。通过索引对数据的存储,是一个有序的排列。相比链表,数组对了很多方法,也可以对数据进行不同的操作。
链表是对内存的堆中的一些引用进行串联成一个链子一样。不是有序的。
// 链表数据结构 [1, 2, 3 ]
let linkedList = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: null
}
}
}
在JavaScript中是不提供原生链表结构的,我们可以用代码自己模拟一个链表,链表需要有一个头部节点 head, 默认是null。我们要对这个链表添加一个节点,那就需要一个添加节点的方法,同时我们想知道这个链表是否包含某个节点,删除一个节点等等的一些功能,我列举一些常见的方法。
- addLast // 尾部添加一个节点
- addFirst // 添加一个头部节点
- isEmpty // 判断当前列表是否为空
- find // 查找指定元素
- findLast // 获取最后一个节点
- removeLast // 删除最后一个节点
- removeFirst // 删除第一个节点
- remove // 删除一个指定节点
- insert // 指定位置插入
- dir // 展开这个链表
现在我们知道了我们要实现的这个链表包含了那些功能,我们一个一个的来实现,首先我们需要一个链表的构造函数,成为一个容器来承载我们的链表也承载对应的方法。
class LinkedList{
constructor() {
// 添加一个长度属性
this.size = 0;
// 声明一个头部节点
this.head = null;
// 维护一个尾部节点(添加的时候非常便利)
this.tail = null;
}
}
我们的链表基本结构就有了,我们还需要一个生成节点的构造函数,来辅助我们每次生成一个节点。
class Node{
constructor( data ) {
// 节点的value
this.value = data;
// 节点的下一个引用指向
this.next = null;
}
}
我们现在准备工作已经差不多了。我们的链表也已经有了。但是就好像设计好了一个机器人,还没有给它设计动作指令,还只能看不能使用。那么我们从添加一个节点开始。
尾部添加节点,我们刚刚在设计linkedList的时候就预留了一个尾部节点的引用,这样在链表数据庞大的时候,我们不用遍历整个列表到最后一个节点去添加,这就会显得非常快捷,但是它初始值是null。证明还没有尾部节点。也就是我们根节点都还不存在,所以在添加节点的这个方法里。我们要做的事情是判断是否有根节点,有就用尾部节点去添加,没有就直接添加在根节点上,并且维护一下尾部节点,更新一下链表长度。
addLast( item ) {
// 对参数做一个非空验证
if ( !item ) { throw new Error('请输入一个正确的节点!') }
// 创建一个节点
let node = new Node(item);
// 判断链表是否为空
if ( this.size === 0 ) {
// 更新尾部节点,更新头部节点
this.tail = this.head = node;
} else {
// 设置尾部节点的引用
this.tail.next = node;
// 将最新的尾部节点更新到tail
this.tail = node
}
// 更新一下长度
this.size++;
}
刚刚实现了在链表的尾部添加一个节点,对应我们还可以在头部添加一个节点。思路和刚刚相差不大,因为我们有头部节点,能够直接获取到。
addFirst( item ) {
// 对参数做一个非空验证
if ( !item ) { throw new Error('请输入一个正确的节点!') }
// 创建一个node节点
let node = new Node(item);
// 判断当前链表是否为空
if ( this.size === 0 ) {
this.head = this.tail = node;
} else {
// 将整个链表作为新节点的next引用
node.next = this.head;
// 将node赋值给head
this.head = node;
}
// 更新一下长度
this.size++
}
头部添加一个节点也实现了。尾部添加一个节点也实现了。这个时候我们可以实现一下查找的方法。查找方法的主要作用是查看当前链表中是否包含某个节点,如果需要实现一个没有重复节点的链表,也可也在每次添加之前调用查找方法,看看添加的节点是否存在。我们这里就没有做类似的限制。
find( item ) {
// 对参数做一个非空验证
if ( !item ) { throw new Error('请输入一个正确的节点!') }
// 获取到整个节点
let linkedList = this.head;
// 循环判断
while ( linkedList && linkedList.next && linkedList.value !== item ){
linkedList = linkedList.next;
}
return linkedList
}
有了添加,查找。可以实现一下删除的功能首先实现一下头部删除,头部删除是比较简单的,找到第一个头部节点的引用返回,然后将头部指向头部节点的next引用就可以删除头部节点了。
removeFirst (){
// 定义一个返回值
let res = null;
// 判断当前链表不为空
if ( this.size !== 0 ) {
res = this.head;
// 跳过head节点,将head指向head的next
this.head = this.head.next;
res.next = null;
// 维护一下长度
this.size--;
// 判断删除完,链表为空,清空一下尾部节点的引用
this.tail = this.size ? this.tail : null;
} else {
// 如果链表为空,调用删除操作,就报一个警告
throw new Error('链表为空,不能执行删除操作!')
}
return res
}
删除头部节点实现了以后,可以把删除尾部节点也实现一下,删除尾部节点,需要先遍历到尾部节点的父级,然后更新尾部节点的引用。
removerLast() {
// 定义一个返回值
let res = null;
// 拿到所有节点
let linkedList = this.head;
// 判断当前链表是否只有一个节点
if ( this.size === 1 ) {
res = this.head;
this.head = this.tail = null;
res.next = null;
}
// 判断是链表为为空
if ( this.size === 0 ) {
// 如果链表为空,调用删除操作,就报一个警告
throw new Error('链表为空,不能执行删除操作!')
} else {
// 循环找倒数第二个节点
while ( linkedList && linkedList.next && linkedList.next.next ){
linkedList = linkedList.next;
}
res = linkedList.next;
// 将最后一个引用置为空
linkedList.next = null;
// 维护一下尾部节点
this.tail = linkedList;
// 维护一下长度
this.size--;
}
return res
}
有了前两个删除的方法的经验,我们来实现一下指定节点的删除。删除指定节点也是需要遍历链表,获取到指定元素的父级,跟删除最后一个节点有点相似。只是不判断下下级,而是判断下下级是否等于指定删除的元素。由于我们今天分享的这个版本,并没有对唯一性限制,所以删除的时候,默认删除在找到的第一个元素删除。
remover( item ){
// 定义一个返回值
let res = null;
// 对参数做一个非空验证
if ( !item ) { throw new Error('请输入一个正确的节点!') }
// 拿到整个链表
let linkedList = this.head;
// 判断链表只有一个节点
if ( this.size === 1 && linkedList.value === item ) {
res = linkedList;
// 维护一下头部节点和尾部节点
this.head = this.tail = null;
} else {
// 循环找到指定节点的父级
while ( linkedList && linkedList.next && linkedList.next.value !== item ) {
linkedList = linkedList.next;
}
// 判断是否存在
if ( linkedList && linkedList.next ) {
// 存下删除节点
res = linkedList.next;
// 删除指定节点
linkedList.next = linkedList.next.next;
} else {
throw new Error('链表中没有该节点,不能执行删除操作!')
}
}
// 判断一下删除节点是否为最后一个节点
this.tail = linkedList.next ? this.tail : null;
// 删除节点的next指向清空
res.next = null;
// 维护一下
this.size--;
return res
}
有时候我们可能需要判断一下这个链表是否为空,我们实现一下 isEmpty 方法,这个方法比较讨巧。因为只需要判断一下我们维护的size属性,就能知道是否为空了。虽然说我们这里构建的链表其实可以在整个链表的属性中找到size属性。这个验证是否为空的函数是可以不用出的。但是在真实开发过程中用到的时候,其实是不希望size属性被修改的。也不会被暴露出来。JavaScript中比较灵活,很多限制没有像java那么好做。我们这里就没有去对其做限制。
isEmpty() {
return this.size === 0
}
现在我们的链表其实基本功能都能够满足了。一般对一个数据的操作无非增删改查,目前我们都已经实现了。还可以增加一个插入的功能,插入是指定位置插入,如果找不到该位置,抛错,如果位置正确,插入的节点不正确也抛错。
insert ( item, ele ) {
// 验证两个参数不能为空
if ( !item || !ele ) { throw new Error('请输入一个正确的节点或者查找元素!') }
// 拿到整个链表
let linkedList = this.head;
// 循环查找匹配
while ( linkedList && linkedList.value !== item ) {
linkedList = linkedList.next;
}
// 验证结果,结果只能是两个,找到,没找到
if ( linkedList ) {
// 生成一个节点
let node = new Node(ele);
// 将匹配到的节点的next引用指向新节点的next引用
node.next = linkedList.next;
// 将新节点赋值给 匹配到的节点
linkedList.next = node;
// 判断一下是否是插入到了最后一个元素
this.tail = node.next ? this.tail : node;
// 维护一下长度
this.size++;
} else {
throw new Error('没有匹配到需要插入位置的元素!')
}
}
在平时也会关注到链表的第一项和链表的最后一项。在双向链表中,可以从头往后遍历,也可以从尾部朝前遍历。就需要直接拿到尾部节点,我们这个示例中没有涉及到双向链表。但是我们维护了一个尾部节点,获取链表最后一项还是比较方便的。
findLast() {
return this.tail
}
由于整个链表的结构比较复杂。查看起来并不是特别的方便,在文章开头的时候简单讲解了一下3个节点的展开面结构。已经不太利于阅读了。那如果我们链表长度是30,可能展开面就非常大了。这个时候我们就需要有一个方法来查看一下整个链表的数据了。
dir(){
// 定义一个返回值
let res = '';
// 获取整个链表
let linkedList = this.head;
res = linkedList ? linkedList.value : '';
// 循环获取整个链表的数据
while ( linkedList.next ) {
res += '->' + linkedList.next.value;
linkedList = linkedList.next;
}
// 返回数据
return res
}
链表可以实现的东西很多。是个非常不错的数据结构。比如可以用链表来实现一个微型的JavaScript中的数组,实现一个栈,实现一个队列。后期如果有空我再更新一章关于链表实现一个微型数组的文章。
关于链表的介绍和一些功能的实现就已经结束了,讲的不是很详细,如果有讲的不对的地方。代码规范,代码逻辑错误的地方,希望您可以多多指教。
关于上面介绍的链表的源代码我放到了gitee上,本来想放github上,不知道为啥今天上不去。我在上面也分享了一些在力扣上刷题的题解,今天分享了链表,我也对应找了一道 力扣上关于链表的中等题来实现。有兴趣的可以去看看,顺便点个小星星呀。谢谢
刚到掘金来。其他历史文章在公众号分享的,感兴趣的朋友可以关注一波公众号。以后也会同步更新公众号和掘金 公众号【 二哥前端学习之路 】
gitee地址:https://gitee.com/chengxinyingtianxia/leetcode
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!