文章目录
什么是双向链表?
双向链表是双向的,它也是由节点组成的,与单链表不同的是,双向链表每一个节点有三个域,分别是data域,next域,prev域,他们的位置没有严格的顺序要求,双向链表的节点也是有地址的。
首先我们先看一下双向链表长什么样子,下面这个就是一个无头双向链表:
- next存放下一个节点的地址;
- 单链表不包含它的前驱信息的,但双链表包括每一个节点,它不仅包含下一个节点的信息,也包含它前一个节点的信息;
- 无头双向链表也是由一个一个节点构成的,每一个节点有三个域,所以可以定义一个Node类(节点类),来存储这三个域。然后在提供构造方法来传参。
节点类代码示例:
class Node{//定义一个节点类
public int data;//int类型data,保存的是数据
public Node next;//Node类型的next,保存的是后继信息
public Node prev;//Node类型的prev,保存的是前驱信息
//next和prev引用类型,不用初始化就默认为null
//提供构造方法
public Node(int data){
this.data=data;//把data初始化为在构造的时候传的值
}
}
无头双向链表的实现
头插法
插入之前的逻辑分析:
- 无论怎样在插入之前,首先需要new一个节点,new完之后,会调用带有一个参数的构造方法,并传一个值进来,对应的data值为你传过来的数据,此时这个节点的地址也产生了,由于next和prev在new这个节点之前,没有被初始化,所以他们的第一个值默认为null,当这个节点产生之后,可以给它起个名字为node.
- 初始之前,head和tail都是指向空的。
双向链表的头插法需要考虑两种情况:
第一种:是否是第一次插入;
第二种:如果还不是第一次插入,考虑要修改的值有哪些?
1.先分析一下是否是第一次插入,如果是第一次插入,此时没有一个节点,head和tail都是空的,那么让head和tail都指向要插入的这个节点node即可,因为只产生这第一个节点,所以它既是头也是尾。
如图:
2.如果不是第一次插入,就需要把要插入的节点插到已有节点的前头,此时已有节点就变成了尾结点,假设要插入的节点为node,需要将要插入的节点和已有节点绑起来,所以需要改的地方有node.next=head; head.prev=node;head的前驱改完之后,只需要让head向前移动即可,即head=node;而tail是不需要动的,tail第一次指的节点也是将来的尾结点。
如图:
头插法代码示例:
public class DoubleLink {
//双向链表也有头,用head去标志
public Node head;//定义一个Node类型的head,用来标志双向链表的头
public Node tail;//标志双向链表的尾巴,定义它有一个好处是,如果采用尾插法的时候,不需要再遍历一次双向链表,直接使用tail.next即可
//头插法
public void addFirst(int data){
Node node=new Node(data);//new一个节点,调用带有一个参数的构造方法,并传一个值进来
if(this.head==null){//如果是第一次插入,head指向为null
this.head=node;
this.tail=node;
}else{//如果不是第一次插入
node.next=this.head;
this.head.prev=node;
this.head=node;//让head向前移动
}
}
头插法测试结果:

双向链表的尾插法也需要考虑两种情况:
第一种:是否是第一次插入;
第二种:如果还不是第一次插入,考虑要修改的值有哪些?
1.先分析一下是否是第一次插入,如果是第一次插入,此时没有一个节点,head和tail都是空的,那么让head和tail都指向要插入的这个节点node即可,因为只产生这第一个节点,所以它既是头也是尾。
如图:
2.如果不是第一次插入,就只需要把要插入的节点插到tail节点的后头,假设要插入的节点为node,需要将要插入的节点和已有节点绑起来,所以只需要改的地方有tail.next= node; node.prev=tail; node的前驱改完之后,只需要让tail向后移动(移到node所指的位置)即可,即tail=node;改完之后就可以串起来了。
如图:
尾插法代码示例:
//尾插法
public void addLast(int data){
Node node=new Node(data);
if(this.head==null){
this.head=node;
this.tail=node;
}else{
this.tail.next = node;
node.prev=this.tail;
this.tail=node;
}
}
尾插法测试结果:

任意位置插入,第一个数据节点为0号下标
在单向链表中,如果想要往某个位置插入一个节点的话,比如想要往2号位置插入一个节点,则首先需要找到2号位置的前一个节点,因为需要让2号位置的前一个节点的next等于要插入的这个节点。但是双向链表还需要这样找吗?不需要了!为什么呢?
解题思路:
1、判断Index的合法性
- 负数不可以插:index<0- 可以往等于单链表长度的位置插新节点,但是不可以超过它的长度:index>size()
2、判断是否为第一次插入,因为第一次就可以插到0号位置。(往0号位置插的情况,其实就是头插法)
调用头插法即可
if(index==0){//往0号位置插的情况,其实就是头插法 addFirst(data);//调用头插法 return ; }
3、如果不是第一次插入,要看一下是不是插入到最后位置。(插入到最后位置的情况,其实就是尾插法)
调用尾插法即可
if(index==size()){//插入到最后位置的情况,其实就是尾插法 addLast(data);//调用尾插 return ; }
4、如果是中间位置插入,则需考虑4个位置,如图,蓝色圈中的就为需要考虑的4个位置。
画图分析中间位置插入的逻辑和代码:
//写一个函数/方法 供addIndex调用
private void checkIndex(int index){//为什么写成private私有的呢?
// 因为这个函数只是为当前类提供的,不需要在类外进行访问,所以写成private私有的
//1、判断合法性
if(index<0 || index>size()){//index表示要插入的节点
throw new RuntimeException("index不合法");//抛出异常
}
}
//写一个函数,定义一个cur,先找到index位置的地址,找到之后返回它的地址
private Node searchIndex(int index){//传一个index,找到了的话,返回index位置的地址
Node cur=this.head;//定义一个cur
while(index!=0){//先找到index位置的地址
cur=cur.next;
index--;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){//带有返回值的要有返回值,如先默认返回false,后面具体返回什么根据逻辑再作更改
checkIndex(index);//调用checkIndex方法/函数,如果它不合法,就会出错,那么下面的代码就不会被执行了,到这就结束了。
if(index==0){//往0号位置插的情况,其实就是头插法
addFirst(data);
return ;
}
if(index==size()){//插入到最后位置的情况,其实就是尾插法
addLast(data);
return ;
}
Node cur=searchIndex(index);//往中间位置插
Node node=new Node(data);
node.next=cur;
node.prev=cur.prev;
cur.prev.next=node;
cur.prev=node;
}
测试结果如下:

查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
Node cur=this.head;//定义一个cur从头开始走,遍历单链表
while(cur!=null){//如果单链表不为null,说明没遍历完
if(cur.data==key){//每次遍历的时候判断cur的data是否等于要查找的关键字key
return true;//如果找到返回true
}
cur=cur.next;//如果没找到cur继续往后走,直到遇到null
}
return false;//遍历完还没找到,返回false
}
测试打印结果:
删除第一次出现关键字为key的节点
先不考虑头,如果想要删除中间数据,该如何删除?
首先要先找到要删除的这个数据,定义一个cur从头开始走,找到你要删除的这个数据,如果走到null还没找到就返回-1,如果找到了要删除的这个数据,那么cur所指向的这个节点就是当前要删除的节点,要想删除这个节点,只需要将它前面的节点和它后面的节点绑起来即可,所以需要改的地方有cur.prev.next=cur.next;
cur.next.prev=cur.prev,此时还需要定义一个循环条件,即while(cur!=null)
画图解释逻辑分析如图:
如果要删除的节点为头节点,则要判断当前要删除的数据是不是头节点,如果是头节点,就将head移到下一个节点的位置,成为新的头节点,此时这个新的头节点的前驱也需要改,即将新的头节点的前驱置为null即可,此时要删除的节点就被删除了。
画图解释逻辑分析如图:
如果要删除的是尾巴节点的话,此时如果还执行这句代码的话cur.next.prev=cur.prev;就会出现空指针异常,所以只需要让tail指向要删除的这个尾巴节点的前驱,即tail=cur.prev;
画图解释逻辑分析如图:
public int remove(int key) {//返回值置为int,找到返回要删除的数字,找不到返回-1
Node cur = this.head;
while (cur != null) {
if (cur.data == key) {//如果找到了要删除的数据
int oldData = cur.data;//定义一个要返回的值oldData,并记录下来
if (cur == this.head) {//如果找到的要删除的这个数据是头结点
this.head = this.head.next;
this.head.prev = null;
} else {//如果不是头结点
cur.prev.next = cur.next;
if (cur.next != null) {//如果要删除的不是尾巴节点的话,就执行下面这句代码
cur.next.prev = cur.prev;
} else {//如果要删除的是尾巴节点的话,只需要移动tail,就执行下面这句代码
this.tail = cur.prev;
}
}
return oldData;
} else {//如果没找到,cur就继续往后找
cur = cur.next;
}
}
return -1;//没有找到要删除的数据
}
测试打印结果:

删除所有值为key的节点
上面删除第一次出现的关键字为key的节点,删除之后,return返回,这里删除所有值为key的节点,删除之后不return了,一直走到cur为null的时候,说明链表都遍历完了,所以该删除的也都删除完了。
**空指针异常的解决方法:**再写一个判断语句,判断如果当head不等于null时,再让head的前驱等于null.
完整代码如下:
public void removeAllKey(int key) {//返回值置为void
Node cur = this.head;
while (cur != null) {
if (cur.data == key) {//如果找到了要删除的数据
if (cur == this.head) {//如果找到的要删除的这个数据是头结点
this.head = this.head.next;
if (this.head != null) {//如果head不等于null,执行里面的代码,等于空就不执行,防止出现空指针异常
this.head.prev = null;
}
} else {//如果不是头结点
cur.prev.next = cur.next;
if (cur.next != null) {//如果要删除的不是尾巴节点的话,就执行下面这句代码
cur.next.prev = cur.prev;
} else {//如果要删除的是尾巴节点的话,只需要移动tail,就执行下面这句代码
this.tail = cur.prev;
}
}//上面删除第一次出现的关键字为key的节点,删除之后,return返回,这里删除所有值为key的节点,删除之后不return了,一直走到cur为null的时候,说明链表都遍历完了,所以该删除的也都删除完了。
cur = cur.next;
}
}
}
打印测试结果:(注意可能有误!)

得到单链表的长度
public int size(){
int count=0;//定义一个计数器count
Node cur=this.head;//cur从头开始走
while(cur!=null){
count++;
cur=cur.next;//cur接着往后走
}
return count;//结果返回count
}
测试打印结果:
打印双向链表
打印双向链表代码示例:
public void display(){
Node cur=this.head;
while(cur!=null){
System.out.print(cur.data+" ");
cur=cur.next;//继续让cur往后走,不然就会死循环
}
System.out.println();
}
清空链表
如何预防内存泄漏呢?
直接将head置为空?对不对测试一下试试看!
操作 步骤如图:
通过以上分析,我们知道,仅仅将head置为null是不能满足条件的,因为虽然把head置为null了,但是head所指向的节点还由下一个节点引用着,后一个节点仍然是保存head的前驱信息的,head不引用他所指向的节点了,但是麻烦的是后面的节点在引用前一个节点,导致head这个节点回收不了,依次下去所有节点都无法回收。
所以clear需要重新写。如何重写呢?逻辑是怎样的呢?需要让一个一个节点进行释放,如何一个一个节点进行释放呢?如图分析:
暂不完整代码如下:
//清空链表
public void clear() {
while(this.head!=null){
Node cur=this.head.next;
this.head.prev=null;
this.head.next=null;//一个节点进行释放
this.head=cur;
}
}
修改完代码之后,再次调试,重复1——7的步骤,打开vs,就会出现如图所示:
然后搜索Node,发现此时Node只剩一个,如图所示。
这一个是tail,因为虽然都为null了,但是最后这个节点还是由tail在引用,如果想要清除全部节点,则需要在执行完循环出来之后,将tail置为null。
完整代码如下:
//清空链表
public void clear() {
while(this.head!=null){
Node cur=this.head.next;
this.head.prev=null;
this.head.next=null;//一个节点进行释放
this.head=cur;
}
this.tail=null;
}
再次修改完代码之后,再次调试,重复1——7的步骤,打开vs,搜索Node,发现Node已找不到了,此时就表明,目的达成了,清除成功。