双向链表又称“双向循环链表”,相较于单链表,双向链表增加了一个指向前面一个结点的指针,链表内的结点不会存在NULL结点,文章有点长,但是干货满满,希望你能看完。
目录
一、初识双向链表
在双向链表中,每个结点都有两个指针域(prior和next)和一个数据域(data),第一个结点称”头结点“,头结点不存储数据,但它存储了指向下一个结点的地址和最后一个结点的地址,从而实现了双向和循环。反观单链表,只有一个next,每个节点只能访问自己的下一个结点,对于CRUD等操作都没有双向链表灵活。
二、双向链表初始化
来看上面这张图,对于一个空的双向链表只有一个头结点,上面说过了头结点不带有数据,因此空的双向链表的next指针存储的就是我自己,prior(前指针)也指向我自己,这就相当于一个空的循环链表,所以通过代码实现就是上图的:this.prior=this.next=this;
我们这一次用DoubleLinkList来描述一个双向链表,使用构造函数就是
function DoubleLinkList()
{
//初始化头结点
this.prior=this.next=this;
}
对,就是这么简简单单的一句话,每次实例化一个DoubleLinkList类的对象的时候都会做一遍这个初始化,自动生成一个空双向链表。
但是貌似还缺少什么?眼力见好的小伙伴可能已经发现了:每个节点不是说都有两个指针域和一个数据域么?对的,这也属于初始化工作的一部分,但是这部分的初始化我们用一个内部类去封装,就叫做结点类DoubleNode
//相当于内部类,保存数据域和两个指针域
function DoubleNode(data){
this.data=data;
this.prior=null;
this.next=null;
}
至此,初始化工作全部结束,接下来就可以写我们的业务逻辑:CRUD
三、添加结点
首先来简单讲一下增加结点的核心逻辑:
如图,在双向链表的任何一个位置插入结点的代码都是完全一样的,假设我们实例化得到了一个新结点node,我们想在p结点1和p.next结点3的位置中间把这个新结点node4插入进去,就需要破坏原有的1和3结点的next和prior,如何破坏呢?首先将node的prior指向p,再将node.next指向p.next,此时完成了node结点的prior和next指针的赋值,但是还没有破坏p和p.next的两个指针,再来将p.next.prior指向node,此时就破坏了上图的蓝色箭头(画了X的),最后将p.next指向node,就破坏了上图绿色箭头,至此新结点node就插进去了。四个步骤不能乱,否则某个结点的地址就会丢失
3.1头插法
OK,理解了上面的插入,接下来头插法就好理解了
//头插法
DoubleLinkList.prototype.insertDouListByHead=function(data){
const node = new DoubleNode(data);
let p=this;
//核心代码
node.prior = p;
node.next = p.next;
p.next.prior = node;
p.next = node;
}
3.2尾插法
尾插法类似,只是他需要寻找到最后一个结点,于是判断条件就是p.next!==this
DoubleLinkList.prototype.insertDouListByTail=function(data){
const node=new DoubleNode(data);
let p=this.next;
while(p.next!==this){
p=p.next;
}
//核心代码
node.prior = p;
node.next = p.next;
p.next.prior = node;
p.next = node;
}
3.3指定位置插入
指定位置插入,需要用户传入一个Pos位置,原理类似
//任意位置插入结点
DoubleLinkList.prototype.insertDouListByPos=function(data,Pos){
const node=new DoubleNode(data);
let p=this,i=0;
while(i<Pos-1 && p.next!==this){
i++;
p=p.next;
}
//核心代码
node.prior = p;
node.next = p.next;
p.next.prior = node;
p.next = node;
}
四、删除结点
再来看删除结点的核心逻辑:
相对于添加,删除似乎简单许多,主要是要破坏p结点的前后指针域,根据上图所画相信大家一目了然,哦~不过如此嘛!只不过不仅要破坏还要重新将p的前后两个结点连起来,就是上图红色的两个箭头,要不然就是删除包括p在内的后面的所有结点了!
4.1删除指定位置结点
//指定位置删除
DoubleLinkList.prototype.deleteNode=function(Pos){
if(this.prior===this && this.next===this){
console.log(new Error('链表为空,无法删除'));
return false;
}
let i = 0, p = this;
while (i < Pos && p.next !== this) {
i++;
p = p.next;
}
if (Pos > i || Pos < 1) {
console.log(new Error('该位置不存在'));
return false;
}
p.prior.next=p.next;
p.next.prior = p.prior;
}
这段代码头尾部分应该不用多讲,中间部分就是在寻找需要被删除的那个结点
4.2删除整表
//整表删除
DoubleLinkList.prototype.clear=function(){
if(this.prior===this && this.next===this){
console.log(new Error('链表为空,无法清空链表'));
return false;
}
let p=this.next;
let i=1;
while(p!==this){
this.deleteNode(1);
p=p.next;
}
}
删除整表就是从头结点的下一个结点开始一个一个删除,这里直接调用了上面的删除结点方法,每次删除第一个,直至全部删除。
五、获取结点
//获取指定位置的结点
DoubleLinkList.prototype.getNode=function(Pos){
if(this.prior===this && this.next===this){
console.log(new Error('链表为空,无法获取'));
return false;
}
let i = 0, p = this;
while (i < Pos && p.next !== this) {
i++;
p = p.next;
}
if (Pos > i || Pos < 1) {
console.log(new Error('该位置不存在'));
return false;
}
return p.data;
}
这个和删除结点没有太大的区别,唯一的区别就是最后只需要将获取结点的数据域返回出去即可。
附:完整源码:
'use strict'
//双向链表
const DoubleLinkList=function(){
const wm=new WeakMap();
//双向链表构造函数
function DoubleLinkList(){
//初始化头结点
this.prior=this.next=this;
//定义私有方法
const privates=wm.get(this) || {};
this.privateInsert=Symbol('private'); //私有方法
this.privateFind=Symbol('private'); //私有方法
privates[this.privateInsert]=privateInsert.bind(this); //插入
privates[this.privateFind]=privateFind.bind(this);
wm.set(this,privates);
}
//相当于内部类,保存数据域和两个指针域
function DoubleNode(data){
this.data=data;
this.prior=null;
this.next=null;
}
//将头插法、尾插法相同部分的代码抽取出来,进一步封装
function privateInsert(node,p) {
node.prior = p;
node.next = p.next;
p.next.prior = node;
p.next = node;
}
//寻找结点的公共部分,封装
function privateFind(Pos){
let i = 0, p = this;
while (i < Pos && p.next !== this) {
i++;
p = p.next;
}
if (Pos > i || Pos < 1) {
console.log(new Error('该位置不存在'));
return false;
}
return p;
}
//头插法
DoubleLinkList.prototype.insertDouListByHead=function(data){
const node = new DoubleNode(data);
let p=this;
wm.get(this)[this.privateInsert](node,p);
}
//尾插法
DoubleLinkList.prototype.insertDouListByTail=function(data){
const node=new DoubleNode(data);
let p=this.next;
while(p.next!==this){
p=p.next;
}
wm.get(this)[this.privateInsert](node,p);
}
//任意位置插入结点
DoubleLinkList.prototype.insertDouListByPos=function(data,Pos){
const node=new DoubleNode(data);
let p=this,i=0;
while(i<Pos-1 && p.next!==this){
i++;
p=p.next;
}
wm.get(this)[this.privateInsert](node,p);
}
//指定位置删除
DoubleLinkList.prototype.deleteNode=function(Pos){
if(this.prior===this && this.next===this){
console.log(new Error('链表为空,无法删除'));
return false;
}
let p=wm.get(this)[this.privateFind](Pos);
p.prior.next=p.next;
p.next.prior = p.prior;
}
//获取指定位置的结点
DoubleLinkList.prototype.getNode=function(Pos){
if(this.prior===this && this.next===this){
console.log(new Error('链表为空,无法获取'));
return false;
}
let p=wm.get(this)[this.privateFind](Pos);
return p.data;
}
//整表删除
DoubleLinkList.prototype.clear=function(){
if(this.prior===this && this.next===this){
console.log(new Error('链表为空,无法清空链表'));
return false;
}
let p=this.next;
let i=1;
while(p!==this){
this.deleteNode(1);
p=p.next;
}
}
return DoubleLinkList;
}();
双向链表的学习就到此结束了。
如果对你有用,别忘了点个收藏哦!