【java学习及数据结构初识】无头双向链表的实现【详解篇11】

什么是双向链表?

双向链表是双向的,它也是由节点组成的,与单链表不同的是,双向链表每一个节点有三个域,分别是data域,next域,prev域,他们的位置没有严格的顺序要求,双向链表的节点也是有地址的。

首先我们先看一下双向链表长什么样子,下面这个就是一个无头双向链表:

image-20210902223754989

  • 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即可,因为只产生这第一个节点,所以它既是头也是尾。

如图:image-20210903012526252

2.如果不是第一次插入,就需要把要插入的节点插到已有节点的前头,此时已有节点就变成了尾结点,假设要插入的节点为node,需要将要插入的节点和已有节点绑起来,所以需要改的地方有node.next=head; head.prev=node;head的前驱改完之后,只需要让head向前移动即可,即head=node;而tail是不需要动的,tail第一次指的节点也是将来的尾结点。
如图:image-20210903004042359
头插法代码示例:

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向前移动
             }
         }

头插法测试结果:

image-20210903005101031 ## 尾插法

双向链表的尾插法也需要考虑两种情况:

第一种:是否是第一次插入;

第二种:如果还不是第一次插入,考虑要修改的值有哪些?

1.先分析一下是否是第一次插入,如果是第一次插入,此时没有一个节点,head和tail都是空的,那么让head和tail都指向要插入的这个节点node即可,因为只产生这第一个节点,所以它既是头也是尾。

如图:image-20210903012650578

2.如果不是第一次插入,就只需要把要插入的节点插到tail节点的后头,假设要插入的节点为node,需要将要插入的节点和已有节点绑起来,所以只需要改的地方有tail.next= node; node.prev=tail; node的前驱改完之后,只需要让tail向后移动(移到node所指的位置)即可,即tail=node;改完之后就可以串起来了。

如图:image-20210903012259316

尾插法代码示例:

//尾插法
          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;
             }
          }

尾插法测试结果:

image-20210903012939676

任意位置插入,第一个数据节点为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个位置。

image-20210903021740982

画图分析中间位置插入的逻辑和代码:

image-20210903114432545

 //写一个函数/方法 供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;
          }

测试结果如下:

image-20210903030621790

查找是否包含关键字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
          }

测试打印结果:image-20210903014943287

删除第一次出现关键字为key的节点

先不考虑头,如果想要删除中间数据,该如何删除?

首先要先找到要删除的这个数据,定义一个cur从头开始走,找到你要删除的这个数据,如果走到null还没找到就返回-1,如果找到了要删除的这个数据,那么cur所指向的这个节点就是当前要删除的节点,要想删除这个节点,只需要将它前面的节点和它后面的节点绑起来即可,所以需要改的地方有cur.prev.next=cur.next;
cur.next.prev=cur.prev,此时还需要定义一个循环条件,即while(cur!=null)

画图解释逻辑分析如图:

image-20210903120737977

如果要删除的节点为头节点,则要判断当前要删除的数据是不是头节点,如果是头节点,就将head移到下一个节点的位置,成为新的头节点,此时这个新的头节点的前驱也需要改,即将新的头节点的前驱置为null即可,此时要删除的节点就被删除了。

画图解释逻辑分析如图:

image-20210903125904107

如果要删除的是尾巴节点的话,此时如果还执行这句代码的话cur.next.prev=cur.prev;就会出现空指针异常,所以只需要让tail指向要删除的这个尾巴节点的前驱,即tail=cur.prev;

画图解释逻辑分析如图:image-20210903130134204

 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;//没有找到要删除的数据
    }

测试打印结果:

image-20210903154347580

删除所有值为key的节点

上面删除第一次出现的关键字为key的节点,删除之后,return返回,这里删除所有值为key的节点,删除之后不return了,一直走到cur为null的时候,说明链表都遍历完了,所以该删除的也都删除完了。

image-20210911082742363

**空指针异常的解决方法:**再写一个判断语句,判断如果当head不等于null时,再让head的前驱等于null.

image-20210911083028135

完整代码如下:

 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;
            }
        }
    }

打印测试结果:(注意可能有误!)

image-20210911003449827

得到单链表的长度

public int size(){
             int count=0;//定义一个计数器count
             Node cur=this.head;//cur从头开始走
             while(cur!=null){
                 count++;
                 cur=cur.next;//cur接着往后走
             }
             return count;//结果返回count
          }

测试打印结果:image-20210903015114222

打印双向链表

打印双向链表代码示例:

public void display(){
             Node cur=this.head;
             while(cur!=null){
                 System.out.print(cur.data+" ");
                 cur=cur.next;//继续让cur往后走,不然就会死循环
             }
              System.out.println();
          }

清空链表

如何预防内存泄漏呢?
直接将head置为空?对不对测试一下试试看!
操作 步骤如图:image-20210911091255418

通过以上分析,我们知道,仅仅将head置为null是不能满足条件的,因为虽然把head置为null了,但是head所指向的节点还由下一个节点引用着,后一个节点仍然是保存head的前驱信息的,head不引用他所指向的节点了,但是麻烦的是后面的节点在引用前一个节点,导致head这个节点回收不了,依次下去所有节点都无法回收。

所以clear需要重新写。如何重写呢?逻辑是怎样的呢?需要让一个一个节点进行释放,如何一个一个节点进行释放呢?如图分析:

image-20210911091546812

暂不完整代码如下:

 //清空链表
    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,就会出现如图所示:image-20210911092626114

然后搜索Node,发现此时Node只剩一个,如图所示。

image-20210911092753497

这一个是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已找不到了,此时就表明,目的达成了,清除成功。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值