当前位置: 首页 > news >正文

数据结构 双链表与LinkedList

本节目标:

  1. 认识并且能够实现一个双链表
  2. 认识LinkedList类并且知道如何去使用

1.双链表

概念

在数据结构中,双链表(Doubly Linked List) 是一种常见的线性数据结构,它由一系列节点组成,每个节点不仅包含数据域,还包含两个指针域,分别指向其前驱节点(前一个节点) 和后继节点(后一个节点)。这种结构相比单链表(仅含后继指针),在节点访问和操作上更加灵活,可以表示成这样:

相较于单链表,双链表多了一个前驱节点prev,它储存的是这个节点的前一个节点的地址,即它指向这个节点的前一个节点

实现双链表

在上一节中,我们已经实现不带头不循环的单链表了,那么这里实现的是不带头不循环的双链表。以下是这个双链表包含的一些操作方法:

// 不带头双向链表实现

public class MyLinkedList {

        //头插法

        public void addFirst(int data){ }

        //尾插法

        public void addLast(int data){}

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

        public void addIndex(int index,int data){}

        //判断双链表当中是否有包含key节点

        public boolean contains(int key){

                return false;

        }

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

        public void remove(int key){}

        //删除所有值为key的节点

        public void removeAllKey(int key){}

        //得到双链表的长度

        public int size(){

                return -1;

        }

        //打印双链表中的数据

        public void display(){}

        //清空双链表

        public void clear(){}

在实现这些操作之前,先创建节点类 ListNode:

public class ListNode {public int val;public ListNode prev;public ListNode next;public ListNode(int val) {this.val = val;}
}

还是老规矩,实现顺序由简到繁。

(1) 打印双链表中的数据

要求:将双链表中的数据一个个打印出来。

思路:实现方式与单链表的打印一样,通过遍历的方式,一个个去打印。

public class MyLinkedList {ListNode head;ListNode last;//打印双链表中的数据public void display() {ListNode cur = this.head;while (cur != null) {System.out.print(cur.val + " ");cur = cur.next;}System.out.println();}
}

因为双链表中前驱结点指向该节点的前一个节点,因此它能够从后往前访问,所以用 last

来表示双链表的尾节点。

(2) 得到双链表的长度

要求:通过这个方法获得双链表的长度。

思路:定义一个变量用于计数,通过遍历的方式进行计数,最后返回这个变量。

//得到双链表的长度public int size() {ListNode cur = this.head;int size = 0; while (cur != null) {size++;cur = cur.next;}return size;}

(3) 判断双链表当中是否有包含key节点

要求:判断双链表中是否有包含key的节点,如果有就返回true,否则返回false。

思路:依旧通过遍历的方式去一个个判断。

//判断双链表当中是否有包含key节点public boolean contains(int key){ListNode cur = this.head;while (cur != null) {if (cur.val == key) {return true;}cur = cur.next;}return false;}

(4) 头插法

要求:在双链表的头部位置插入一个新节点。

思路:这里与单链表不一样,需要考虑两种情况:1.插入前链表是空的;2.插入前链表不为空。

即下图所示:

//头插法public void addFirst(int data){ ListNode cur = new ListNode(data);if (this.head == null) {this.head = this.last = cur;}else {cur.next = this.head;this.head.prev = cur;this.head = cur;}}

进行测试:

public class Test {public static void main(String[] args) {MyLinkedList myLinkedList = new MyLinkedList();myLinkedList.addFirst(1);myLinkedList.addFirst(2);myLinkedList.addFirst(3);myLinkedList.display();}
}//运行结果
3 2 1 

符合预期。

(5) 尾插法

要求:在双链表的尾部插入一个新节点。

思路:这里也分两种情况,与头插法一样。需要注意的是,在链表不为空的情况下,我们应该对尾节点last进行操作,而不是头节点head。

//尾插法public void addLast(int data){ListNode cur = new ListNode(data);if (this.head == null) {this.head = this.last = cur;}else {this.last.next = cur;cur.prev = this.last;this.last = cur;}}

进行测试:

public class Test {public static void main(String[] args) {MyLinkedList myLinkedList = new MyLinkedList();myLinkedList.addLast(1);myLinkedList.addLast(2);myLinkedList.addLast(3);myLinkedList.display();}
}//运行结果
1 2 3 

符合预期。

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

要求:在双链表中合法的任一位置插入一个新节点,第一个节点为0号下标。

思路:可以分3种情况处理:头插、尾插和中间插入。头插与尾插已经解决了,因此现在只需要解决中间插入即可。如下图所示:

当然,在进行插入操作前,需要判断插入位置是否合法。

插入位置非法异常类:

public class InsertIllegalException extends RuntimeException {public InsertIllegalException() {}public InsertIllegalException(String str) {super(str);}
}

判断插入位置是否合法:

//判断插入位置是否合法private void isIllegal(int index) {if (index < 0 || index > size()) {throw new InsertIllegalException("插入位置不合法!");}}

插入操作:

//任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data){try {isIllegal(index);int len = size();//头插if (index == 0) {addFirst(data);return;}//尾插if (index == len) {addLast(data);return;}//中间插入ListNode newN = new ListNode(data);ListNode cur = this.head;while (index != 0) {cur = cur.next;index--;}newN.next = cur;cur.prev.next = newN;newN.prev = cur.prev;cur.prev = newN;}catch (InsertIllegalException e) {e.printStackTrace();}}

进行测试:

public class Test {public static void main(String[] args) {MyLinkedList myLinkedList = new MyLinkedList();myLinkedList.addLast(1);myLinkedList.addLast(2);myLinkedList.addLast(3);myLinkedList.display();myLinkedList.addIndex(1,100);myLinkedList.display();}
}//运行结果
1 2 3 
1 100 2 3 

符合预期。

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

要求:将双链表中第一次出现包含key的节点删掉。

思路:这里要分为三种情况:key在链表头部、key在链表尾部和key在链表中间,但在开始前需要判断链表是否为空。分析如下图所示:

//删除第一次出现关键字为key的节点public void remove(int key){if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false;  //用于标记链表中是否有key,有的话置为truewhile (cur != null) {//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;this.head.prev = null;}else {//位于中间或者尾节点cur.prev.next = cur.next;//位于尾节点if (cur.next == null) {this.last = this.last.prev;}else {//位于中间cur.next.prev = cur.prev;}}//删除完成!return;}cur = cur.next;}if (mark == false) {System.out.println("链表中没有" + key);}}

但是这里还有一个不足的地方,就是当链表中仅有一个节点时(即head == last且该节点的值为key),这时候 head.prev就会引发空指针异常,所以要对key在头节点的情况进行补充处理:

//删除第一次出现关键字为key的节点public void remove(int key){if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false;  //用于标记链表中是否有key,有的话置为truewhile (cur != null) {//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;}else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.last = null;}}else {//位于中间或者尾节点cur.prev.next = cur.next;//位于尾节点if (cur.next == null) {this.last = this.last.prev;}else {//位于中间cur.next.prev = cur.prev;}}//删除完成!return;}cur = cur.next;}if (mark == false) {System.out.println("链表中没有" + key);}}

进行测试:

public class Test {public static void main(String[] args) {MyLinkedList myLinkedList = new MyLinkedList();myLinkedList.addLast(1);myLinkedList.addLast(2);myLinkedList.addLast(3);myLinkedList.addLast(1);myLinkedList.display();myLinkedList.remove(1);myLinkedList.display();}
}//运行结果:
1 2 3 1 
2 3 1 

符合预期。

(8) 删除所有值为key的节点

要求:将链表中所有值为key的节点都删掉。

思路:只需要在删除第一次出现关键字为key的节点的方法的基础上进行修改即可,把return去掉,使得循环继续进行,继续找key的节点,继续删除,直到遍历结束,此时已将所有值为key的节点删除。

//删除所有值为key的节点public void removeAllKey(int key) {if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false;  //用于标记链表中是否有key,有的话置为truewhile (cur != null) {//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;} else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.last = null;}} else {//位于中间或者尾节点cur.prev.next = cur.next;//位于尾节点if (cur.next == null) {this.last = this.last.prev;} else {//位于中间cur.next.prev = cur.prev;}}}cur = cur.next;}if (mark == false) {System.out.println("链表中没有" + key);}}

不过这里可能会有一个潜在的问题,就是当我们删除某一个节点,cur指向的节点已经被移除,此时执行cur = cur.next可能会出现问题。举个例子:

现在有一个双链表:

2 <-> 3 <-> 2 <-> 4

现在要删掉所有的2

1.首先,cur指向第一个2,删除后head变为3节点;

2.接着执行cur = cur.next时,此时的cur仍然指向已经被删除的第一个 2 节点;

3.这个节点的next虽然可能还指向 3,但这是不安全的(已删除节点的引用应该被视为无效)

因此出于安全考虑,可以用一个变量记录cur的=指向的下一个节点。

//删除所有值为key的节点public void removeAllKey(int key) {if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false;  //用于标记链表中是否有key,有的话置为truewhile (cur != null) {ListNode curN = cur.next;//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;} else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.last = null;}} else {//位于中间或者尾节点cur.prev.next = cur.next;//位于尾节点if (cur.next == null) {this.last = this.last.prev;} else {//位于中间cur.next.prev = cur.prev;}}}cur = curN;}if (mark == false) {System.out.println("链表中没有" + key);}}

进行测试:

public class Test {public static void main(String[] args) {MyLinkedList myLinkedList = new MyLinkedList();myLinkedList.addLast(2);myLinkedList.addLast(1);myLinkedList.addLast(2);myLinkedList.addLast(3);myLinkedList.addLast(2);myLinkedList.display();myLinkedList.removeAllKey(2);myLinkedList.display();}
}//运行结果:
2 1 2 3 2 
1 3 

符合预期。

(9) 清空双链表

要求:将双链表清空。

思路:粗暴的方式就不介绍了(直接将head和last置空),温柔的方式:通过遍历,一个个将节点的prev和next置空,最后将head和last置空。

//清空双链表public void clear() {ListNode cur = this.head;while (cur != null) {ListNode curN = cur.next;cur.prev = null;cur.next = null;cur = curN;}this.head = null;this.last = null;}

进行测试:

public class Test {public static void main(String[] args) {MyLinkedList myLinkedList = new MyLinkedList();myLinkedList.addLast(2);myLinkedList.addLast(1);myLinkedList.addLast(2);myLinkedList.addLast(3);myLinkedList.addLast(2);myLinkedList.display();myLinkedList.clear();myLinkedList.display();}
}//运行结果:
2 1 2 3 2 

符合预期。

到此,我们就成功实现了一个不带头不循环的双链表。

完整代码

public class MyLinkedList {ListNode head;ListNode last;//打印双链表中的数据public void display() {ListNode cur = this.head;while (cur != null) {System.out.print(cur.val + " ");cur = cur.next;}System.out.println();}//得到双链表的长度public int size() {ListNode cur = this.head;int size = 0;while (cur != null) {size++;cur = cur.next;}return size;}//判断双链表当中是否有包含key节点public boolean contains(int key){ListNode cur = this.head;while (cur != null) {if (cur.val == key) {return true;}cur = cur.next;}return false;}//头插法public void addFirst(int data){ListNode cur = new ListNode(data);if (this.head == null) {this.head = this.last = cur;}else {cur.next = this.head;this.head.prev = cur;this.head = cur;}}//尾插法public void addLast(int data){ListNode cur = new ListNode(data);if (this.head == null) {this.head = this.last = cur;}else {this.last.next = cur;cur.prev = this.last;this.last = cur;}}//判断插入位置是否合法private void isIllegal(int index) {if (index < 0 || index > size()) {throw new InsertIllegalException("插入位置不合法!");}}//任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data){try {isIllegal(index);int len = size();//头插if (index == 0) {addFirst(data);return;}//尾插if (index == len) {addLast(data);return;}//中间插入ListNode newN = new ListNode(data);ListNode cur = this.head;while (index != 0) {cur = cur.next;index--;}newN.next = cur;cur.prev.next = newN;newN.prev = cur.prev;cur.prev = newN;}catch (InsertIllegalException e) {e.printStackTrace();}}//删除第一次出现关键字为key的节点public void remove(int key){if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false;  //用于标记链表中是否有key,有的话置为truewhile (cur != null) {//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;}else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.last = null;}}else {//位于中间或者尾节点cur.prev.next = cur.next;//位于尾节点if (cur.next == null) {this.last = this.last.prev;}else {//位于中间cur.next.prev = cur.prev;}}//删除完成!return;}cur = cur.next;}if (mark == false) {System.out.println("链表中没有" + key);}}//删除所有值为key的节点public void removeAllKey(int key) {if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false;  //用于标记链表中是否有key,有的话置为truewhile (cur != null) {ListNode curN = cur.next;//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;} else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.last = null;}} else {//位于中间或者尾节点cur.prev.next = cur.next;//位于尾节点if (cur.next == null) {this.last = this.last.prev;} else {//位于中间cur.next.prev = cur.prev;}}}cur = curN;}if (mark == false) {System.out.println("链表中没有" + key);}}//清空双链表public void clear() {ListNode cur = this.head;while (cur != null) {ListNode curN = cur.next;cur.prev = null;cur.next = null;cur = curN;}this.head = null;this.last = null;}
}

2.LinkedList

2.1 概念

LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。

在集合框架中,LinkedList也实现了List接口,具体如下:

【说明】

  1. LinkedList实现了List接口
  2. LinkedList的底层使用了双向链表
  3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
  4. LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
  5. LinkedList比较适合任意位置插入的场景

2.2 使用

构造方法

LinkedList的构造方法:

方法解释
LinkedList()无参构造
public LinkedList(Collection <? extends E> c)使用其他集合容器中元素构造List

举例:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;public class Test {public static void main(String[] args) {//使用第一种构造方法,构造一个空的LinkedListLinkedList<Integer> linkedList = new LinkedList<>();//List<Integer> linkedList = new LinkedList<>();这种写法等于上一条语句,//因此LinkedList实现了List接口,因此可以用List去接收//可以插入数据linkedList.addFirst(1);linkedList.addFirst(2);linkedList.addFirst(3);System.out.println(linkedList);System.out.println("=============");//使用第二种构造方法List<Integer> list = new ArrayList<>();  //创建一个ArrayList,并往里面插入一些数据list.add(1);list.add(2);list.add(3);System.out.println(list);System.out.println("=============");//可以ArrayList构造LinkedListList<Integer> list1 = new LinkedList<>(list);System.out.println(list1);}
}//运行结果:
[3, 2, 1]
=============
[1, 2, 3]
=============
[1, 2, 3]

其他常用方法

方法解释
boolean add(E e)尾插 e
void add(int index, E element)将 e 插入到 index 位置
boolean addAll(Collection <? extends E> c)尾插 c 中的元素
E remove(int index)删除 index 位置元素
boolean remove(Object o)删除遇到的第一个 o
E get(int index)获取下标 index 位置元素
E set(int index, E element)将下标 index 位置元素设置为 element
void clear()清空
boolean contains(Object o)判断 o 是否在线性表中
int indexOf(Object o)返回第一个 o 所在下标
int lastIndexOf(Object o)返回最后一个 o 的下标
List subList(int fromIndex, int toIndex)截取部分 list

LinkedList的遍历

LinkedList的遍历与ArrayList的遍历一样,有三种方法,分别是通过for遍历、通过for-each遍历和通过迭代器遍历。具体请看:

数据结构 ArrayList与顺序表-CSDN博客

2.3 ArrayList与LinkedList的区别

不同点ArrayListLinkedList
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持:O(1)不支持:O(N)
头插需要搬移元素,效率低O(N)只需修改引用的指向,时间复杂度为O(1)
插入空间不够时需要扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁

到此,双链表与LinkedList的内容完结。感谢您的观看,如有不对的地方还请指出,谢谢!

http://www.dtcms.com/a/325605.html

相关文章:

  • 18.WEB 服务器
  • 超算中心的一台服务器上有多少个CPU,多少个核?
  • JVM基础【Java】
  • 力扣164:最大间距
  • 深入理解与灵活应用 std::optional
  • vue3中的子组件向父组件通信和父组件向子组件通信
  • python --nacos相关
  • MSE ZooKeeper:Flink高可用架构的企业级选择
  • 《图解技术体系》New generation CMDB resource model framework
  • 自然语言处理实战:用LSTM打造武侠小说生成器
  • 【AI论文】R-Zero:从零数据起步的自进化推理大语言模型
  • JavaScript 中如何实现大文件并行下载
  • AI(2)-神经网络(激活函数)
  • 支持小语种的在线客服系统,自动翻译双方语言,适合对接跨境海外客户
  • NY185NY190美光固态闪存NY193NY195
  • 《深度剖析前端框架中错误边界:异常处理的基石与进阶》
  • pom.xml父子模块配置
  • 深入理解Android Kotlin Flow:响应式编程的现代实践
  • 部署open-webui到本地
  • TDengine IDMP 基本功能(1.界面布局和操作)
  • 某地渣库边坡自动化监测服务项目
  • 企业高性能web服务器1
  • FPGA实现Aurora 8B10B图像视频传输,基于GTX高速收发器,提供4套工程源码和技术支持
  • 新手向:Python实现数据可视化图表生成
  • LVPECL、LVDS、LVTTL、LVCMOS四种逻辑电平标准的全面对比
  • DDoS 攻击成本测算:从带宽损耗到业务中断的量化分析
  • FPGA硬件设计1 最小芯片系统-Altera EP4CE10F17C8、Xilinx xc7a100t
  • 邬贺铨院士:AI与数字安全融合是数字化建设核心驱动力
  • 使用TextureView和MediaPlayer播放视频黑屏问题
  • 设计模式(三)——观察者模式