数据结构和算法(四)之链表结构
数据结构和算法(四)之链表结构
一. 认识链表
链表和数组一样,可以用来存储一系列的元素,但是链表和数组的实现机制完全不同。
这一章中,我们就来学习一下另外一种非常常见的用于存储数据的线性结构:链表!
链表和数组
数组:
要存储多个元素,数组(或链表)可能是最常用的数据结构。
我们之前说过,几乎每一种编程语言都有默认实现数组结构,这种数组结构非常方便,提供了一个便利的
[]
语法来访问它的元素。但是数组也有很多缺点:
- 数组的创建通常需要申请一段连续的内存空间(一整块的内存),并且大小是固定的(大多数编程语言数组都是固定的),所以当当前数组不能满足容量需求是,需要扩容。(一般情况下是申请一个更大的数组,比如2倍。然后将原数组中的元素复制过去)
- 而且在数组开头或中间位置插入数据的成本很高,需要进行大量的位移。(尽管我们已经学过的JavaScript的
Array
类方法可以帮我们做这些事,但背后的原理依然是这样)。
链表
- 要存储多个元素,另外一个选择就是使用链表
- 但不同于数组,链表中的元素在内存中不必是连续的空间
- 链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针或者链接)组成。
- 相对于数组,链表有一些优点:
- 内存空间不是连续的。可以充分利用计算机的内存,实现灵活的内存动态管理。
- 链表不必在创建时就确定大小,并且大小可以无限的延伸下去。
- 链表在插入和删除数据时,时间复杂度可以达到
O(1)
。相对数组效率高很多。
- 相对于数组,链表有一些缺点:
- 链表访问任何一个位置的元素时,都需要从头开始访问。(无法跳过第一个元素访问任何一个元素)。
- 无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的问题。
什么是链表?
什么是链表呢?
其实上面我们已经简单的提过了链表的结构,我们这里更加详细的分析一下。
链表类似于火车:有一个火车头,火车头会连接一个节点,节点上有乘客,并且这个节点会连接下一个节点,以此类推。
链表的火车结构:
链表的数据结构:
给火车加上数据后的结构:
二. 链表封装
前面我们已经认识了链表结构,现在通过代码来封装自己的链表吧。
创建链表类
我们先来创建一个链表类
// 封装链表的构造函数 function linkedList (){// 封装一个Node类,用于保存每个节点信息function Node(element){this.element = elementthis.next = null}// 链表中的属性this.length = 0 // 链表的长度this.head = null // 链表的第一个节点 }
代码解析:
- 封装
LinkedList
的类,用于表示我们的链表结构。(和Java
中的链表同名,不同Java
中的这个类是一个双向链表,后面我们会讲解双向链表) - 在
LinkedList
类中有一个Node
类,用于封装每一个节点上的信息。(和优先级队列的封装一样) - 链表中我们保存两个属性,一个是链表的长度,一个是链表中第一个节点。
- 当然,还有很多链表的操作方法。
- 封装
链表的常见操作
- 我们先来认识一下,链表中应该有哪些常见的操作
append(element)
:向链表尾部添加一个新的项insert(position,element)
:向链表的特定位置插入一个新的项。remove(element)
:从链表中移除一项。indexOf(element)
:返回元素在链表中的索引。如果链表中没有该元素则返回-1
.removeAt(position)
:从链表的特定位置移除一项。isEmpty()
:如果链表中不包含任何元素,返回true
,如果链表长度大于0
则返回false
。size()
:返回链表包含的元素个数。与数组的length
属性类似。toString()
:由于链表项使用了Node
类,就需要重写继承自JavaScript
对象默认的toString
方法,让其只输出元素的值。
- 方法解读:
- 整体你会发现操作方法和数组非常类似,因为链表本身就是一种可以代替数组的结构。
- 但是某些方法实现起来有些麻烦,所以我们一个个来慢慢实现他们。
三. 链表操作
尾部追加数据(append
方法)
向链表尾部追加数据可能有两种情况:
- 链表本身为空,新添加的数据时唯一的节点。
- 链表不为空,需要向其他节点后面追加节点。
append
方法实现// 链表尾部追加元素方法 LinkedList.prototype.append = function (element){// 1.根据新元素创建节点let newNode = new Node(element)// 2.判断原来链表是否为空if(this.head === null){ // 链表尾空this.head = newNode}else{// 2.1.定义变量,保存当前找到的节点let current = this.headwhile(current.next){current = current.next}// 2.2.找到最后一项,将其next赋值为nodecurrent.next = newNode}// 3.链表长度增加1this.length++ }
代码解读:
首先需要做的是将
element
传入方法,并根据element
创建一个Node
节点。场景一:链表本身是空的,比如这种情况下我们插入一个
15
作为元素。场景二:链表中已经有元素了,需要向最后的节点的
next
中添加节点。这个时候要向链表的尾部添加一个元素,首先我们需要找到这个尾部元素。
记住:我们只有第一个元素的引用,因此需要循环访问链表,直到找到最后一个项。
找到最后一项后,最后一项的
next
为null
,这个时候不让其为null
,而是指向新创建的节点即可。最后,一定不要忘记将链表的
length+1
.
toString
方法
我们先来实现一下链表的
toString
方法,这样会方便测试上面的添加代码// 链表的toString方法 LinkedList.prototype.toString = function (){// 1.定义连个变量let current = this.headlet listString = ""// 2.循环获取链表中所有的元素while(current){listString += "," + current.elementcurrent = current.next}// 3.返回最终结果return listString.slice(1) }
方法解读:
- 该方法比较简单,主要是获取每一个元素
- 还是从
head
开头,因为获取链表的任何元素都必须从第一个节点开头。 - 循环遍历每一个节点,并且取出其中的
element
,拼接成字符串。 - 将最终字符串返回。
测试
append
方法// 测试链表 // 1.创建链表 let list = new LinkedList()// 2.追加元素 list.append(15) list.append(10) list.append(20)// 3.打印链表的结果 alert(list)
任意位置插入(insert
方法)
接下来实现另外一个添加数据的方法:在任意位置插入数据。
// 根据下标删除元素 LinkedList.prototype.insert = function (position,element){// 1.检测越界问题:越界插入失败if(position < 0 || position > this.length) return false// 2.找到正确的位置,并且插入数据let newNode = new Node(element)let current = this.headlet previous = nullindex = 0// 3.判断是否在链表第一个位置插入if(popsition === 0){newNode.next = currentthis.head = newNode}else{while(index++ < position){previous = currentcurrent = current.next}newNode.next = currentprevious.next = newNode}this.length++return true}
代码解读:
代码
1
的位置,我们处理了越界问题,基本传入位置信息时,都需要进行越界的判断。如果越界,返回false
,表示是数据添加失败。(因为位置信息是错误的,所以数据肯定是添加失败的)代码
2
的位置,我们定义了一些变量,后续需要使用它们来保存信息。代码
3
的位置进行了判断,这是因为添加到第一个位置和其他位置是不同的。添加到第一个位置:
添加到第一个位置,表示新添加的节点是头,就需要将原来的头节点,作为新节点的
next
另外这个时候的
head
应该指向新节点。
添加到其他位置:
如果是添加到其他位置,就需要先找到这个节点位置了。
我们通过
while
循环,一点点向下找。并且在这个过程中保存上一个节点和下一个节点。找到正确的位置后,将新节点的
next
指向下一个节点,将上一个节点的next
指向新的节点。
最后,不要忘记
length+1
返回
true
,表示元素插入成功了
测试
insert
的方式插入数据:// 测试insert方法 list.insert(0,100) list.insert(4,200) list.insert(2,300) alert(list) // 100,15,300,10,20,200
位置移除数据(removeAt
方法)
移除数据有两种常见的方式:
- 根据位置移除对应的数据
- 根据数据,先找到对应的位置,再移除数据
我们这里先完成根据位置移除数据的方式
// 根据位置移除节点 LinkedList.prototype.removeAt = function (position){// 1.检测越界问题:越界移除失败,返回nullif(position < 0 || position >= this.length) return null// 2.定义变量,保存信息let current = this.headlet previous = nulllet index = 0// 3.判断是否是移除第一项if(position === 0){this.head = current.next}else{while(index++ < position){previous = currentcurrent = current.next}previous.next = current.next}this.length--// 返回移除的数据return current.element }
代码解析:
代码
1
部分,还是越界的判断。(注意:这里越界判断中的等于length
也是越界的,因为下标值是从0开始的)代码
2
部分还是定义了一些变量,用于保存临时信息代码
3
部分进行判断,因为移除第一项和其他项的方式是不同的移除第一项的信息:
移除第一项,直接让
head
指向第二项信息既可以啦那么第一项信息没有引用指向,就在链表中不再有效,后面会被回收掉。
移除其他项的信息:
- 移除其他项的信息操作方式是相同的
- 首先,我们需要需要通过
while
循环,找到正确的位置。 - 找到正确位置后,就可以直接将上一项的
next
指向current
项的next
,这样中间的项就没有引用指向它,也就不再存在于链表后,后面会被回收掉。
测试
removeAt
方法// 测试removeAt方法list.removeAt(0)list.removeAt(1)list.removeAt(3)alert(list) // 15,10,20
获取元素位置(indexOf
方法)
根据元素获取它在链表中的位置
// 根据元素获取链表中的位置 LinkedList.prototype.indexOf = function (element){// 1.定义变量,保存信息let current = this.headindex = 0// 2.找到元素所在位置while(current){if(current.element === element){return index}index++current = current.next}// 3.来到这个位置,说明没有找到,则返回 -1return -1 }
代码解析:
- 代码
1
的位置还是定义需要的变量 - 代码
2
的位置,通过while
循环获取节点 - 通过节点获取元素和
element
进行对比,如果和传入element
相同,表示找到,直接返回inidex
即可。 - 如果没有找到,
index++
,并且指向下一个节点 - 到最后都没有找到,说明链表中没有对应的元素,那么返回 -1 即可
- 代码
indexOf
方法测试// 测试indexOf方法 alert(list.indexOf(15)) // 0 alert(list.indexOf(10)) // 1 alert(list.indexOf(20)) // 2 alert(list.indexOf(100)) // -1
根据元素删除
有了上面的
indexOf
方法,我们非常方便实现根据元素来删除信息// 根据元素删除信息 LinikedList.prototype.remove = function (element){let index = this.indexOf(element)return this.removeAt(index) }
代码解析:
第一步获取元素所在位置(已经封装好),根据位置移除元素
代码测试:
// 测试remove方法 list.remove(15) alert(list) // 10,20
其他方法实现(isEmpty
方法、size
方法)
isEmpty
方法// 判断链表是否为空 LinkedList.prototype.isEmpty = function (){return this.length == 0 }
size
方法// 获取链表长度 LinkedList.prototype.size = function (){return this.length }
获取第一个元素节点:(单向链表比较方便的操作
getFirst
方法)// 获取第一个节点 LinkedList.prototype.getFirst = function (){return this.head.element }
方法测试:
// 测试其他方法 alert(list.isEmpty()) // false alert(list.size()) // 2 alert(list.getFirst()) // 10
四. 完整代码
// 封装链表的构造函数
function LinkedList(){// 封装一个Node类,用于保存每个节点信息function Node(element){this.element = elementthis.next = null}// 链表中的属性this.length = 0this.head = null// 链表尾部追加元素的方法LinkedList.prototype.append = function (element){// 1.根据新元素创建节点let newNode = new Node(element)// 2.判断原来链表是否为空if(this.head === null){ //链表为空this.head = newNode}else{ // 链表不为空// 2.1.定义变量,保存当前找到的节点let current = this.headwhile (current.next){current = current.next}// 2.2.找到最后一项,将其next赋值为nodecurrent.next = newNode}// 3.链表长度增加1this.length++}// 链表的toString方法LinkedList.prototype.toString = function (){// 1.定义连个变量let current = this.headlet listString = ""// 2.循环获取链表中所有的元素while(current){listString += "," + current.elementcurrent = current.next}// 3.返回最终结果return listString.slice(1)}// 根据下标删除元素LinkedList.prototype.insert = function (position,element){// 1.检测越界问题:越界插入失败if(position < 0 || position > this.length) return false// 2.定义变量,保存信息let newNode = new Node(element)let current = this.headlet previous = nullindex = 0// 3.判断是否在链表第一个位置插入if(position === 0){newNode.next = currentthis.head = newNode}else{while(index++ < position){previous = currentcurrent = current.next}newNode.next = currentprevious.next = newNode}this.length++return true}// 根据位置移除节点LinkedList.prototype.removeAt = function (position){// 1.检测越界问题:越界移除失败,返回nullif(position < 0 || position >= this.length) return null// 2.定义变量,保存信息let current = this.headlet previous = nulllet index = 0// 3.判断是否移除第一项if(position === 0){this.head = current.next}else{while(index++ < position){previous = currentcurrent = current.next}previous.next = current.next}this.length--return current.element}// 根据元素获取链表中的位置LinkedList.prototype.indexOf = function (element){// 1.定义变量,保存信息let current = this.headindex = 0// 2.找到元素所在的位置while(current){if(current.element === element){return index}index++current = current.next}// 来到这个位置,说明没有找到,则返回-1return -1}// 根据元素删除信息LinkedList.prototype.remove = function (element){let index = this.indexOf(element)return this.removeAt(index)}// 判断链表是否为空LinkedList.prototype.size = function (){return this.length}// 获取第一个节点LinkedList.prototype.getFirst = function (){return this.head.element}}
数据结构和算法(四)之链表结构相关推荐
- java环形链表_数据结构和算法(四)Java实现环形链表
1. 数据结构和算法(四)Java实现环形链表 1.1 约瑟夫问题 约瑟夫问题:公元66年,约瑟夫不情愿地参与领导了犹太同胞反抗罗马统治的起义,后来起义失败,他和一些宁死不降的起义者被困于一个山洞之中 ...
- 数据结构与算法:企业级链表实现(超详细)
企业级链表介绍 如果我们使用原始的C语言写链表的话,数据类型是被固定死的,如果业务换了 需要另一种数据类型,我们又得重新在写一个链表,当然我们可以使用void* 万能指针,因为void* 可以接受任意 ...
- 数据结构与算法:单链表(利用万能指针实现对任意类型数据进行操作)
前言 C语言的指针真的很强大,万能指针更强大,可以指向任意类型的数据.在上篇博客 数据结构与算法:单链表(超详细实现)中用C语言实现了单链表的相关算法,不过却有局限性 只能针对某一种数据类型还是不够强 ...
- 数据结构和算法 第六天内核链表是链表的终结者
数据结构和算法 第六天内核链表是链表的终结者 第一章 内核链表图文讲解 第二章 内核链表代码详解 [1]list.h [2]list.c 内核链表不过是双向链表的封装,学起来 第一章 内核链表图文讲解 ...
- java数据结构与算法之双链表设计与实现
转载请注明出处(万分感谢!): http://blog.csdn.net/javazejian/article/details/53047590 出自[zejian的博客] 关联文章: java数据结 ...
- 数据结构与算法之单链表
数据结构与算法之单链表 //链表的实现/*实现单链表的 构建.数据添加.数据删除(返回元素所在位置).数据查找(返回元素所在的位置)的算法设计:*/ //链表的实现/*实现单链表的 构建.数据添加.数 ...
- 数据结构与算法:用链表实现无序列表。
目录 1.无序列表的介绍 2.开始构建无序列表 2.1Node类(节点) 2.2UnorderList类(无序列表) 2.2.1isEmpty()方法 2.2.2add()方法 2.2.3length ...
- Java数据结构和算法(四)--链表
日常开发中,数组和集合使用的很多,而数组的无序插入和删除效率都是偏低的,这点在学习ArrayList源码的时候就知道了,因为需要把要 插入索引后面的所以元素全部后移一位. 而本文会详细讲解链表,可以解 ...
- python数据结构与算法学习之基本结构
目录 线性结构Linear Structure 栈Stack 抽象数据类型Stack(ADT Stack) 栈的应用:简单括号匹配 栈的应用:通用括号匹配 栈的应用:进制转换 表达式转换 中缀表达法 ...
- 数据结构与算法 | 线性表 —— 链表
原文链接:wangwei.one/posts/java-- 链表 定义 逻辑结构上一个挨一个的数据,在实际存储时,并没有像顺序表那样也相互紧挨着.恰恰相反,数据随机分布在内存中的各个位置,这种存储结构 ...
最新文章
- SSL/TLS 协议简介与实例分析
- mysql 数据字典 php_php生成mysql数据库数据字典的程序代码
- [转]怎样看懂Oracle的执行计划
- linux gcc 制作动态库
- AngularJs通过路由传参解决多个页面资源浪费问题
- mysql系列:加深对脏读、脏写、可重复读、幻读的理解
- 深入理解 LINQ to SQL 生成的 SQL 语句
- Posix线程编程指南(4) 线程终止
- SqlServer2008镜像证书过期处理
- Java程序员从笨鸟到菜鸟之(五十八)细谈Hibernate(九)hibernate一对一关系映射...
- 利用UICollectionView实现瀑布流
- ubuntu 安装GPU黑屏 修改GRUB_安装Ubuntu 18.04系统的相关注意事项,及解决Ubuntu 双系统黑屏问题...
- JSP学习——EL表达式和JSTL学习小结
- CorelDraw x4无法打开的解决方法
- ZedGraph类库之基本教程篇
- eclipse安装中文补丁包
- 运筹系列63:使用ALNS求解大规模TSP问题
- IPV4与IPV6练习
- PDF如何裁剪页面,PDF裁剪页面的小技巧
- 常见的设计模式和应用场景