链表顾名思义就是一种链式结构,画出草图

由图可见,每个节点链接成一条链。每个节点都由自身的数据和指向下一个节点的引用组成。
通过数组认识到链表
数组是我们常用的一种数据存储容器,但是数组也有很多缺点:
1、数组的创建通常需要申请一段连续的内存空间(一整块的内存), 并且大小是固定的(大多数编程语言数组都是固定的), 所以当当前数组不能满足容量需求时, 需要扩容。
2、而且在数组开头或中间位置插入数据的成本很高, 需要进行大量元素的位移。
针对于数组的缺点,我们引入一种数据结构——链表
相对于数组, 链表有一些优点:
1、内存空间不是比是连续的. 可以充分利用计算机的内存. 实现灵活的内存动态管理。
2、链表不必在创建时就确定大小, 并且大小可以无限的延伸下去。
3、链表在插入和删除数据时, 时间复杂度可以达到O(1). 相对数组效率高很多。
当然相比于数组,数组还是有一些优点,数组对成员的访问可以从选择的下标开始;而链表访问任何一个位置的元素时, 都需要从头开始访问.(无法跳过第一个元素访问任何一个元素);无法通过下标直接访问元素, 需要从头一个个访问, 直到找到对应的位置。
链表的常见操作
创建节点类和链表类
节点类有两个属性,自身数据和下个节点的引用
链表类有两个属性,头部和链表长度
- class Newnode {
- constructor(el) {
- this.data = el
- this.next = null
- }
- }
- class Linklist {
- constructor() {
- this.head = null
- this.length = 0
- }
- }
在链表类中封装各种方法:
append(element):向列表尾部添加一个新的项
代码实现:
- append(i) {
- //创建数据节点
- let newnode = new Newnode(i)
- //为空时就是头部
- if (this.head == null) {
- this.head = newnode
- } else {
- let current = this.head
- while (current.next) { //检测到下一个为空时,找到最后一个,跳出循环
- current = current.next
- }
- current.next = newnode
- }
- this.length++
- }
insert(position,element):向列表的特定位置插入一个新的项。
代码实现:
- insert(position, el) {
- //位置是否合法
- if (position < 0 || position > this.length || !Number.isInteger(position)) {
- return false
- }
- let newnode = new Newnode(el)
- //position==0时
- if (position == 0) {
- if (this.head == null) { //链表为空时
- this.head = newnode
- } else {
- //不为空时
- newnode.next = this.head
- this.head = newnode //设为头部
- }
- this.length++
- } else if (position == this.length) {
- //在尾部
- append(el)
- } else {
- //在任意位置
- let current = this.head
- let index = 0
- while (index < position - 1) {
- current = current.next //得到前一个
- index++
- }
- newnode.next = current.next
- current.next = newnode
- this.length++
- }
-
-
- }
removeAt(position):从列表的特定位置移除一项 。
代码实现:
- removeAt(position) {
- //位置是否合法
- if (position < 0 || position > this.length || !Number.isInteger(position)) {
- return false
- }
- //空时
- if (this.head == null) {
- return
- } else {
- if (position == 0) {
- let current = this.head
- this.head = current.next
- } else {
- //其他位置时
- let current = this.head
- let index = 0
- while (index < position - 1) {
- current = current.next
- index++
- }
- current.next = current.next.next
- }
- this.length--
- }
- }
indexof(element):返回元素在列表中的索引。如果列表中没有该元素则返回-1。
代码实现:
- indexof(el){
- let current = this.head
- let index=0
- while(index<this.length){
- if (current.data==el) {
- return index
- }else{
- current=current.next
- index++
- }
- }
- return -1
- }
remove(element):从列表中移除一项。
代码实现:
- remove(el){
- let index = this.indexof(el)
- this.removeAt(index)
- }
toString()转换为字符串。
代码实现:
- toString(){
- let current=this.head
- let index=0
- let str=""
-
- while(index<this.length){
- str+="-"+current.data
- index++
- current=current.next
- }
- str= str.substr(1)
- return str
- }
上述我们讲解的是单向链表,除了单向链表还有一个双向链表。
双向链表顾名思义是双向的,即可从头遍历到尾, 又可以从尾遍历到头。一个节点既有向前连接的引用, 也有一个向后连接的引用。
其解决了单向链表只能单向遍历,无法回到前一个节点的问题。
双向链表也有一些缺陷:
1、每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个.。也就是实现起来要困难一些;
2、相当于单向链表, 必然占用内存空间更大一些。
3、但是这些缺点和我们使用起来的方便程度相比, 是微不足道的。
画出结构图:

双向链表的应用
封装双向链表类和数据节点类
相比单向链表,双向链表类增加了一个尾部属性tail,指向链表尾部;数据节点类增加了一个prev属性指向前一个节点的引用。
代码实现:
- class Newnode{
- constructor(el){
- this.data=el
- this.prev=null
- this.next=null
- }
- }
- class DoubleLinklist{
- constructor(){
- this.head=null
- this.tail=null
- this.length=0
- }
- }
在双向链表类中封装各种功能:
功能与单向链表相似,方法内部做相应调整。
appen(el):在链表尾部添加元素
- //尾部添加节点
- append(el){
- let newnode=new Newnode(el)
- //链表为空时
- if (this.length==0) {
- this.head=newnode
- this.tail=newnode
- }else{
- //不为空时
- this.tail.next=newnode
- newnode.prev=this.tail
- this.tail=newnode //将新增的节点指向新节点
- }
- this.length++
- }
insert(position,el):在指定位置添加元素
- insert(position,el){
- //判断position是否符合
- if (position < 0 || position > this.length || !Number.isInteger(position)) {
- return false
- }
- let newnode=new Newnode(el)
- if (position==0) {
- if (this.length==0) {
- //链表为空时
- this.head=newnode
- this.tail=newnode
- }else{
- //不为空时
- this.head.prev=newnode
- newnode.next=this.head
- this.head=newnode
- }
-
- }else if(position==this.length-1){
- this.tail.next=newnode
- newnode.prev=this.tail
- this.tail=newnode
-
- }else{
- // 任意位置添加
- let current=this.head
- let index=0
- while(index
1){ - current=current.next
- index++
- }
-
- newnode.prev=current
- newnode.next=current.next
-
- current.next=newnode
- newnode.next.prev=newnode
- }
- this.length++
- }
removeAt(position):移除指定位置的元素
- removeAt(position) {
- //位置是否合法
- if (position < 0 || position > this.length || !Number.isInteger(position)) {
- return false
- }
- //空时
- if (this.length == 0) {
- return
- } else {
- if (position == 0) {
- //链首
- if (this.length==1) {
- this.head=null
- this.tail=null
- }else{
- let current = this.head
- current.next.prev=null
- this.head = current.next
- }
-
- }else if(position=this.length-1) {
- //链尾
- let current = this.tail
- current.prev.next=null
- this.tail=current.prev
- }
-
- else {
- //其他位置时
- let current = this.head
- let index = 0
- while (index < position - 1) {
- current = current.next
- index++
- }
- current.next = current.next.next
- current.next.prev=current
- }
- this.length--
- }
- }
indexof(element):返回元素在列表中的索引。如果列表中没有该元素则返回-1。
- indexof(el){
- let current = this.head
- let index=0
- while(index<this.length){
- if (current.data==el) {
- return index
- }else{
- current=current.next
- index++
- }
- }
- return -1
- }
remove(element):从列表中移除一项。
- remove(el){
- let index=this.indexof(el)
- this.removeAt(index)
- }
toString()转换为字符串,可正向输出也可逆向输出。
- // 正向
- toStringAfter(){
- let current=this.head
- let index=0
- let str=""
-
- while(index<this.length){
- str+="-"+current.data
- index++
- current=current.next
- }
- str= str.substr(1)
- return str
- }
-
- //逆向
- toStringPrevious(){
- let current=this.tail
- let index=this.length-1
- let str=""
-
- while(index>=0){
- str+="-"+current.data
- index--
- current=current.prev
- }
- str= str.substr(1)
- return str
- }
循环链表
在单向链表和双向链表之后,还有单向循环链表和双向循环链表,简单的介绍一下差异:
单向循环链表与单向链表的区别在于,链表的最后一个节点的next不再指向null,而是指向this.head,形成一个循环。
图解:

双向循环链表 其与双向链表的区别在于this.tail的next指向this.head,不再指向null;this.head的prev指向this.tail,形成双重循环。
图:

在单向循环链表和双向循环链表中,封装增加和删除等方法,与不循环的时候原理也是相似的,这里就不再展示了。注意头尾处的区别。