【Java数据结构】通过Java理解和实现——无头双向链表

简介: 在《通过Java理解和实现——顺序表和单链表》一文中已经讲完了顺序表和单链表,接下来就是双向链表了,其实和单链表非常相似,需要注意的就是一些小细节

10.png【Java数据结构】通过Java理解和实现——无头双向链表


🍉无头双向链表

🌵双链表概念及结构

🍌无头双向非循环链表接口实现(注释非常详细,我👴👴都能看懂)

🍈打印双链表

🍈头插法插入

🍈尾插法插入

🍈查找是否包含关键字key在双链表当中

🍈得到双链表的长度

🍈任意位置插入,第一个数据节点为0号下标

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

🍈删除所有值为key的节点

🍈清空链表

🍉单链表和双链表到底有啥区别


🍉无头双向链表


🌵双链表概念及结构


【为什么引入双链表?】

 单链表的结点中只有一个指向其后继的指针,使得单链表要访问某个结点的前驱结点时,只能从头开始遍历,访问后驱结点的复杂度为O(1),访问前驱结点的复杂度为O(n)。为了克服上述缺点,引入了双链表。

 双链表的结点中有两个指针prev和next,分别指向前驱结点和后继结点。

11.png

无头双向非循环链表接口实现


(注释非常详细,我👴👴都能看懂)


先写两个类,一个是链表类(包含有一个【可变的头结点】和【可变尾节点】和【实现各种功能的接口】,因为是无头链表,所以这个头结点和尾节点是可变的),另一个是节点类(成员属性有value和prev(前驱)和next(后驱))

12.png

接下来的接口都是写在链表类里的,因为链表是一个对象,我们要实现的就是一个链表对象所有的功能


🍈打印双链表

打印链表其实和打印顺序表类似,遍历链表就好了,不过要注意一个点,这里需要引入一个局部变量cur来代替头节点去遍历,因为头结点在没添加或者删除节点之前是固定的,不要让头结点变来变去

//打印双链表
   public void display() {
       //和单链表的打印方式是一样的
       ListNode cur = this.head;//利用临时变量cur代替头结点遍历
       while (cur != null){
           System.out.print(cur.val+" ");
           cur = cur.next;//依次往后走
       }
       System.out.println();
   }


🍈头插法插入


顾名思义,头插法就是从头部插入节点,使新创建的节点成为新的头结点,这里需要额外考虑一个点,就是头结点是否存在(链表是否为空),(注意,不光要设置后驱还要注意前驱,这比单链表多一个前驱节点)

//头插法
   public void addFirst(int data) {
       ListNode node = new ListNode(data);//创建一个新节点,储存要插入的数据
       if (this.head==null){//判断头结点是否为空,头结点空说明链表为空
           //头结点null属于第一次插入,直接将头结点尾节点都指向插入节点即可
           this.head=node;
           this.last=node;
       }else{//如果不是第一次插入
           node.next=this.head;//将原头结点变成新插入节点的后驱,实现头插
           head.prev=node;//再将新插入节点变成原头结点的前驱
           this.head=node;//最后将新插入的节点设置成头结点
       }
   }


🍈尾插法插入

尾插法和头插法类似,必须先判断链表是否为空(判断头结点是否为null),双链表区别于单链表,不用每次遍历来找尾节点,双链表本身就有一个尾节点last,我们只需要找到这个last,然后将新节点插入即可

//尾插法
   public void addLast(int data){
       ListNode node = new ListNode(data);//创建一个新节点,储存要插入的数据
       if (this.head==null){//判断头结点是否为空,头结点空说明链表为空
           //头结点null属于第一次插入,直接将头结点尾节点都指向插入节点即可
           this.head=node;
           this.last=node;
       }else{//如果不是第一次插入
           this.last.next=node;//将新结点变成原尾节点的后驱,实现尾插
           node.prev=last;//再将原尾节点变成新插入节点的前驱
           this.last=node;//最后将新插入节点设置成尾节点
       }
   }


🍈查找是否包含关键字key在双链表当中


传入关键字key,依旧是引入局部变量cur遍历链表,哪个节点的value等于key了,说明链表里有这个关键字,返回true,否则返回false,和单链表一模一样

//查找是否包含关键字key是否在单链表当中
   public boolean contains(int key){
       ListNode cur = this.head;//引入局部变量cur遍历链表
       while(cur != null){
           if(cur.val==key){//如果当前节点的val等于关键字
               return true;//返回true
           }
           cur = cur.next;//否则继续往后遍历
       }
       return false;//遍历完成都没有找到key值,返回false
   }


🍈得到双链表的长度


依旧采取引用局部变量cur来遍历链表,还要多设置一个局部变量size来计数,只要节点不为null,size就+1,最后返回size的值就是链表长度了(其实和单链表也一模一样)

//得到双链表的长度
   public int size() {
       int size=0;//设置一个计数变量用于计数
       ListNode cur = this.head;//利用cur代替头结点遍历链表
       while (cur != null){//遍历链表,节点不为空
           size++;//计数器+1
           cur = cur.next;//往后走一步
       }
       return size;//遍历完链表返回计数器的值,就是链表长度了
   }


🍈任意位置插入,第一个数据节点为0号下标


插入原理:临时变量cur代替头结点,cur先走到要插入的位置,然后将新节点插到cur和cur前一节点的中间,替代了cur的原位置,实现按位置插入节点

//任意位置插入,第一个数据节点为0号下标
   public void addIndex(int index,int data){
       ListNode node = new ListNode(data);//创建一个新节点储存要插入的数据
       ListNode cur = this.head;//临时变量cur代替头结点遍历链表
       if (index > 0 && index <size()) {//插入位置不是头也不是尾
           for (int i = 0; i < index; i++){//先让cur走到要插入的位置处
               cur = cur.next;
           }
           cur.prev.next=node;//将插入位置前一节点的后驱指向新节点
           node.prev=cur.prev;//将新节点的前驱指向插入位置的前一节点即 cur.prev
           node.next=cur;//将新节点后驱指向当前cur,也就实现了插入,插到cur和cur前一节点的中间,替代了cur的原位置
       }
       if (index ==size()){//插入位置在尾部
           addLast(data);//直接尾插法
       }
       if (index==0){//插入位置在头部
           addFirst(data);//直接头插法
      }
       display();//打印增加后的链表
   }


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


首先判断头结点是否为null(链表是否为空),然后找要删除的节点,要删除的节点有三种情况

①关键字在头节点:将头节点下一个节点设置为新头节点

②关键字在尾巴结点:将尾节点上一个节点设置为新尾节点

③关键字不在头结点:则将key节点的前一个节点的后驱指向key节点的后一个节点,key节点的后一节点的前驱指向key节点前一节点


//删除第一次出现关键字为key的节点
   public void remove(int key){
       ListNode cur = this.head;//设置cur代替头节点遍历链表
       while(cur != null){
           if(cur.val==key){//如果找到了要删除的点,分以下三种情况
               if(cur==head){//在头部
                   head=head.next;//将要删除的节点的下一节点设为新头节点
                   if (head==null) {//如果这个双链表只有一个元素,要检查
                       break;
                   }
                   head.prev=null;//如果不止一个元素,则将头结点的前驱指向置空
                   break;
               }
               if (cur==last){//在尾部
                   last=last.prev;//要删除节点的上一节点设为新尾节点
                   last.next=null;//将新尾节点后驱置空
                   break;
               } >               if (cur.prev!=null && cur.next!=null){//在中间
                   cur.prev.next=cur.next;//将key节点前一节点的后驱指向key节点的后一节点
                   cur.next.prev=cur.prev;//将key节点后一节点的前驱指向key节点的前一节点
                   break;
               }
           }
           cur=cur.next;//如果没找到目标点则往后走一步
       }
       display();//打印删除后的双链表
   }


🍈删除所有值为key的节点


和上边删除第一次出现的key类似(注释可以看上边删除第一次出现的key),只不过在删除节点过后不要break,让遍历循环继续进行

//删除所有值为key的节点
   public void removeAllKey(int key){
       ListNode cur = this.head;
       while(cur != null){
           if(cur.val==key){
               if(cur==head){//在头部
                   head=head.next;
                   if (head==null) {//只有一个元素,要检查
                       break;
                   }
                   head.prev=null;
               }
               if (cur==last){//在尾部
                   last=last.prev;
                   last.next=null;
               }
               if (cur.prev!=null && cur.next!=null){//在中间
                   cur.prev.next=cur.next;
                   cur.next.prev=cur.prev;
               }
           }
           cur=cur.next;
       }
       display();
   }


🍈清空链表


暴力清空,直接将头尾结点置空,这样整个链表就无法找到了

//清空链表
   public void clear() {
       head=null;
       last=null;
   }


🍉单链表和双链表到底有啥区别


一、指代不同

1、双向链表:也叫双链表,是链表的一种,每个数据结点中都有两个指针,分别指向直接后继和直接前驱

2、单向链表:是链表的一种,其特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始。

二、优点不同

1、双向链表:从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点。

2、单向链表:单个结点创建非常方便,普通的线性内存通常在创建的时候就需要设定数据的大小,结点的访问方便,可以通过循环或者递归的方法访问到任意数据。

三、缺点不同

1、双向链表:增加删除节点复杂,需要多分配一个指针存储空间。

2、单向链表:结点的删除非常方便,不需要像线性结构那样移动剩下的数据,但是平均的访问效率低于线性表。



相关文章
|
25天前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
42 1
|
28天前
|
存储 Java
告别混乱!用Java Map优雅管理你的数据结构
【10月更文挑战第17天】在软件开发中,随着项目复杂度增加,数据结构的组织和管理至关重要。Java中的Map接口提供了一种优雅的解决方案,帮助我们高效、清晰地管理数据。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,有效提升了代码质量和维护性。
81 2
|
28天前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
60 2
|
11天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
32 6
|
16天前
|
存储 Java 索引
Java中的数据结构:ArrayList和LinkedList的比较
【10月更文挑战第28天】在Java编程世界中,数据结构是构建复杂程序的基石。本文将深入探讨两种常用的数据结构:ArrayList和LinkedList,通过直观的比喻和实例分析,揭示它们各自的优势与局限,帮助你在面对不同的编程挑战时做出明智的选择。
|
18天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
45 4
|
19天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
20天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
24天前
|
存储 算法 Java
Java 中常用的数据结构
【10月更文挑战第20天】这些数据结构在 Java 编程中都有着广泛的应用,掌握它们的特点和用法对于提高编程能力和解决实际问题非常重要。
24 6
|
26天前
|
存储 Java 开发者
Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效
【10月更文挑战第19天】在软件开发中,随着项目复杂度的增加,数据结构的组织和管理变得至关重要。Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,帮助开发者告别混乱,提升代码质量。
26 1