链表顾名思义就是一种链式结构,画出草图
由图可见,每个节点链接成一条链。每个节点都由自身的数据和指向下一个节点的引用组成。
通过数组认识到链表
数组是我们常用的一种数据存储容器,但是数组也有很多缺点:
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<position-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,形成双重循环。
图:
在单向循环链表和双向循环链表中,封装增加和删除等方法,与不循环的时候原理也是相似的,这里就不再展示了。注意头尾处的区别。