【Java 数据结构】单向链表和双向链表的实现 (LinkedList)

news2024/11/25 10:55:39

🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!

人生格言:当你的才华撑不起你的野心的时候,你就应该静下心来学习!

欢迎志同道合的朋友一起加油喔🦾🦾🦾
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个🐒嘿嘿
谢谢你这么帅气美丽还给我点赞!比个心


目录

一.单向链表的实现

1. MySingleList的大概实现框架

2. addFirst--头插

3. addLast--尾插

4. addIndex--任意位置插入

5. contains--查找是否包含关键字key

6. remove--删除第一次出现的key

7. removeAllkey--删除所有key

8. (1)求单链表的长度;(2)打印单链表;(3)清除单链表

双向链表的简单介绍

二、双向链表的实现

1.基本框架的构建

2.打印链表

3.查找链表长度

4.头插法

5.尾插法

6.任意位置插入

7.查找是否存在关键词key

8.删除第一次出现的关键词key

9.删除所有关键词key

10、清空链表

三. 缺陷与区别(ArrayList&LinkedList)



链表的简单介绍 

链表是一种在物理上非连续的存储结构。在单向链表中,每一个节点都是一个对象,其中包含了数据和引用两部分,通过引用指向下一个节点。这种结构相比线性存储结构要复杂,并且由于增加了指针(引用)域导致内存开销更大,但它不像数组那样需要预先知道数据规模,可以充分利用计算机的内存空间。 

一.单向链表的实现

首先我们和ArrayList一样,将MySingleList单独定义为一个Java文件,然后每一个结点我们将它定义成一个静态内部类,这样就方便我们访问结点的成员,,还是和ArrayList一样,我们再定义一个Test类用来测试我们的单链表,写一个函数可以测试一下

  无头单向非循环链表的实现:

1. MySingleList的大概实现框架

public class MySingleList {
    static class ListNode {
        public int value;
        public ListNode next;
 
        public ListNode(int value) {
            this.value = value;
        }
    }
 
    //简单的创建单链表
    public void createList() {
        ListNode listNode1 = new ListNode(23);
        ListNode listNode2 = new ListNode(22);
        ListNode listNode3 = new ListNode(23);
 
        listNode1.next = listNode2;
        listNode2.next = listNode3;
 
        this.head = listNode1;
    }
 
    public ListNode head;
    
    public void addFirst(int data){}
    
    public void addLast(int data){}
    
    public void addIndex(int index,int data){}
    
    public boolean contains(int key){return false;}
    
    public void remove(int key){}
    
    public void removeAllKey(int key){}
    
    public int size(){return -1;}
    
    public void display(){}
    
    public void clear(){}
}

2. addFirst--头插

    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        node.next = head;
        head = node;
    }

 头插没啥细节点,以上图做辅助理解,我就不多赘述了,,


3. addLast--尾插

//尾插法
    public void addLast(int data) {
        ListNode node = new ListNode(data);
        //1.链表为空
        if(this.head == null) {
            this.head = node;
        } else {
            //2.链表不为空
            ListNode cur = this.head;
            while(cur.next != null) {
                cur = cur.next;
            }
            cur.next = node;
        }
    }

 这里要注意链表为空的时候,只需要将head指向node即可;


4. addIndex--任意位置插入

    public void addIndex(int index,int data) throws MySingleListIndexOutOfException{
        //1.先检查插入位置是否合法
        checkAddIndex(index);
        //2.分两种情况:1.头插 2.中间位置和尾插
        ListNode node = new ListNode(data);
        if(this.head == null) {
            this.head = node;
            return;
        }
        if(index == 0) {
            addFirst(data);
            return;
        }
        ListNode cur = findAddIndexSubOne(index);
        node.next = cur.next;
        cur.next = node;
    }
    private void checkAddIndex(int index) {
        if(index < 0 || index > this.size()) {
            throw new MySingleListIndexOutOfException("任意位置插入时,index不合法!");
        }
    }
    //找到待插入位置的前一个结点
    private ListNode findAddIndexSubOne(int index) {
        ListNode cur = this.head;
        while(index - 1 != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }

 任意位置插的注意事项:1.先要判断下标是否合法;2.要分两种情况。


5. contains--查找是否包含关键字key

    public boolean contains(int key) {
        if(this.head == null) {
            System.out.println("链表为空!");
            return false;
        }
        ListNode cur = this.head;
        while(cur != null) {
            if(cur.value == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

6. remove--删除第一次出现的key

//删除第一次出现关键字为key的节点
    public void remove(int key) {
        //1.判断有无结点
        if(this.head == null) {
            System.out.println("链表为空,不能删除!");
            return;
        }
 
        //2.删第一个
        if(this.head.value == key) {
            this.head = this.head.next;
            return;
        }
        //3.删后面的
        ListNode cur = this.head;
        cur = removeSubOne(key,cur);
        if(cur == null) {
            System.out.println("链表中没有这个元素!");
            return;
        }
        cur.next = cur.next.next;
    }
    private ListNode removeSubOne(int key, ListNode cur) {
        while(cur.next != null) {
            if(cur.next.value == key) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

 删除函数的注意事项:1.判空  2.分两种情况:删头和删剩下的,


7. removeAllkey--删除所有key

//方法一:时间复杂度O(N^2)
public void removeAllKey1(int key) {
        //1.判断有无结点
        if(this.head == null) {
            System.out.println("链表为空,不能删除!");
            return;
        }
        
        //处理中间和尾巴
        ListNode cur = this.head;
        while(cur != null) {
        //removeSubOne函数在上一个删除方法里头
            cur = removeSubOne(key,cur);
            if(cur != null) {
                cur.next = cur.next.next;
            }
        }
        
        //处理头
        if(this.head.value == key) {
            this.head = this.head.next;
        }
}
//方法二:时间复杂度O(N),只遍历一遍链表
public void removeAllKey2(int key){
        //特殊情况,首结点的值为key的处理情况(这里我们选择处理方式二)
        //处理方式一,直接将位于链表前面所有值为key的结点删除,更新头结点
//        while(head.value == key) {
//            head = head.next;
//        }
        if(head == null) {
            return;
        }
 
        Node pre = head;//前指针
        Node cur = head.next;//游标
        while(cur != null) {
            if(cur.value == key) {
                //删除当前cur结点
                pre.next = cur.next;
                cur = cur.next;
            }else{
                //不删除当前cur结点
                pre = cur;
                cur = cur.next;
            }
        }
        
        //处理方式二,在整个链表删除(结点值为key的)完毕后直接将首结点(值为key)更新
        if(head.value == key) {
            head= head.next;
        }
}

方法一:调用前写过的removeSubOne方法

1.如果先处理头,则需要写成循环,因为当链表所有结点都是待删除的情况时,一个if条件语句处理不了

2.while循环里面的条件不能写成cur.next == null,因为removeSubOne函数如果没找到待删除    的结点,它返回的是一个null,如果写成cur.next != null,则可能会报空指针异常

方法二:只遍历一遍链表,时间复杂度O(N)

使用前后指针解决这个问题, 前指针指向值为key的结点的前驱结点, 而后指针用来标识是否某个结点的值为key。


接下来就是几个简单的函数,也很重要,大家都能看得懂:

8. (1)求单链表的长度;(2)打印单链表;(3)清除单链表

    //得到单链表的长度
    public int size() {
        ListNode cur = this.head;
        int count = 0;
        while(cur != null) {
            count++;
            cur = cur.next;
        }
        return count;
    }
    public void display() {
        ListNode cur = this.head;
        while(cur != null) {
            System.out.print(cur.value+" ");
            cur = cur.next;
        }
        System.out.println();
    }
    public void clear() {
        this.head = null;
    }

 这里解释一下遍历链表循环的结束条件:(head不能动,否则打印一次就找不到头结点了)

 这里说说清除函数,我这种方式是比较暴力,也可以用温柔的方式:

用cur结点保存head的next,然后将head的val和next不断置为0或空,然后两个"指针"不断往后走

拓展:用递归和栈分别打印链表(栈后面文章有讲) 

//递归打印链表
public void disPlay(ListNode pHead) {
    if(pHead == null) {
        return;
    }
    if(pHead.next == null) {
        System.out.println(pHead.val + " ");
        return;
    }
    disPlay(pHead.next);
    System.out.println(pHead.val + " ");
}
//用栈打印链表
public void disPlay2(ListNode pHead) {
    Stack<ListNode> stack =new Stack<>();
    ListNode cur =pHead;
    while (cur != null) {
        stack.push(cur);
        cur =cur.next;
    }
    //遍历栈
    while (!stack.isEmpty()) {
        ListNode top = stack.pop();
        System.out.println(top.val + " ");
    }
}

双向链表的简单介绍

双向链表,顾名思义和单向链表很相似,均为链表,两者之间的操作也十分相近,最明显的不同之处就是双向链表的单个结点带有两个指针域,分别指向前后两个元素。

二、双向链表的实现

1.基本框架的构建

节点的结构如图:

 首先,在实现各项操作前,应该首先实现结点的构建,以静态内部类来实现。
代码如下:

    //实现静态内部类,用于实现结点
    static class ListNode{
        public int val;

        //定义存放头尾结点的域
        public ListNode next;
        public ListNode prev;

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

接下来,我们需要了解应该实现哪些不同的方法,如下:

1.打印链表
2.查找链表长度
3.头插法
4.尾插法
5.任意位置插入
6.查找是否包含关键词key
7.删除第一次出现的key结点
8.删除全部key结点
9.清空链表

注:
首先定义出 head 指针和 tail 指针,分别指向头尾。

    public ListNode head;
    public ListNode tail;

下面,我将会详细进行逐一实现。

2.打印链表

      public void display(){
            ListNode cur = head;
            while(cur != null){
                System.out.print(cur.val+" ");
                cur = cur.next;
            }
        }

3.查找链表长度

        public int size(){
            int count = 0;
            ListNode cur = head;
            while(cur != null){
                count++;
                cur = cur.next;
            }
            return count;
        }

4.头插法

简单分析:
这里有两个需要考虑的地方。

1.当链表起始时为空时
2.当链表有元素时

代码如下:

        public void addFirst(int data){
        //申请一个新的节点
            ListNode node = new ListNode(data);
            //当不存在元素时
            if(head == null){
                head = node;
                tail = node;
            }else{
            //当存在元素时
                head.prev = node;
                node.next = head;
                head = node;
            }
        }

详细分析:
这里主要分析情况 2。
分析如图:

5.尾插法

简单分析:
同样,这里也有两个需要考虑的地方。

1.当链表为空时。
2.当链表中存在元素时。

代码实现:

        public void addLast(int data){
            ListNode node = new ListNode(data);
            if(head == null){
                head = node;
                tail = node;
            }else{
                tail.next = node;
                node.prev = tail;
                tail = node;
            }
        }

详细分析
这里主要分析情况 2 。

6.任意位置插入

简单分析
这里有需要考虑的三个地方。

1.插入的位置 index 是否合法
2.插入的 index 位置是否在头尾
3.插入的 index 位置位于一般位置

代码如下:

        public void addIndex(int index,int data){
            ListNode node = new ListNode(data);
            //1.判断index的合法性
            if(index < 0 || index > size()){
                System.out.println("index不合法");
                throw new IndexWrongFulException("index不合法");
            }
            //2.判断是头插还是尾插
            if(index == 0){
                addFirst(data);
                return;
            }
            if(index == size()){
                addLast(data);
                return;
            }
            //3.找到index位置的结点地址
            ListNode cur = find(index);
            cur.prev.next = node;
            node.next = cur;
            node.prev = cur.prev;
            cur.prev = node;
        }

 注:
1.这里判断合法性使用了异常判断类
代码如下:

public class IndexWrongFulException extends RuntimeException{
    public IndexWrongFulException(String message) {
        super(message);
    }
}

2.在寻找 index 位置时,定义了 find 方法,
代码如下:

        private ListNode find(int index){
            ListNode cur = head;
            while(index != 0){
                cur = cur.next;
                index--;
            }
            return cur;
        }

详细分析:
这里主要分析一般情况下在链表中间插入的情况

注:紫色数字为指针顺序。

7.查找是否存在关键词key

简单分析:
这个方法难度不大,只需要遍历寻找即可。

代码如下:

        public boolean contains(int key){
            ListNode cur = head;
            while(cur != null){
                if(cur.val == key){
                    return true;
                }
                cur = cur.next;
            }
            return false;
        }

8.删除第一次出现的关键词key

简单分析:
这里有三个地方需要考虑。

1.当要删除的结点为一个单独的头结点
2.要删除的节点恰好为尾部结点
3.要删除的结点的位置为链表中的一般位置

代码如下:

        public void remove(int key){
            ListNode cur = head;
            while(cur != null){
            //循环寻找 key 的值
                if(cur.val == key){
                	//当要删除的值是头节点
                    if(cur == head){
                        head = head.next;
                        //判断删除的是不是单独的一个头节点
                        if(head != null){
                            head.prev = null;
                        }else{
                            tail = null;
                        }
                    }else{
                    //一般情况删除
                        cur.prev.next = cur.next;
                        //判断删除的是否为尾部结点
                        if(cur.next != null){
                            cur.next.prev = cur.prev;
                        }else{
                            this.tail = cur.prev;
                        }
                    }
                    return;
                }
                cur = cur.next;
            }
        }

详细分析:
1.删除头节点

 2.删除中间节点

 3.删除尾部结点

9.删除所有关键词key

简单分析:
这个方法实现非常简单,只要将上一个方法中的 return 删除即可。

代码实现:

        public void remove(int key){
            ListNode cur = head;
            while(cur != null){
                if(cur.val == key){
                    if(cur == head){
                        head = head.next;
                        if(head != null){
                            head.prev = null;
                        }else{
                            tail = null;
                        }
                    }else{
                        cur.prev.next = cur.next;
                        if(cur.next != null){
                            cur.next.prev = cur.prev;
                        }else{
                            this.tail = cur.prev;
                        }
                    }
                }
                cur = cur.next;
            }
        }

10、清空链表

简单分析:

双向链表的清空并非是简单的将 head 和 tail 置为 null ,而是要将所有结点的指向全部置为 null

代码如下:

        public void clear(){
            ListNode cur = head;
            while(cur != null){
                ListNode curNext = cur.next;
                cur.next = null;
                cur.prev = null;
                cur =curNext;
            }
        }

至此,所有的方法实现结束。


三. 缺陷与区别(ArrayList&LinkedList)

我们之前学了顺序表,但是在某些方面,它存在着许多不足,由于其底层是一段连续的空间,当ArrayList任意位置插入或删除元素的时候,就需要将后续元素整体往前或者往后移动,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入删除比较多的场景,而这些问题链表都可以解决。

区别:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/425947.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

android studio 页面布局(2)

<?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app"http://schemas.android.com/apk/res-auto"xmlns:tools"http://schemas.android.com/too…

【数据挖掘与商务智能决策】第九章 随机森林模型

9.1.3 随机森林模型的代码实现 和决策树模型一样&#xff0c;随机森林模型既可以做分类分析&#xff0c;也可以做回归分析。 分别对应的模型为随机森林分类模型&#xff08;RandomForestClassifier&#xff09;及随机森林回归模型&#xff08;RandomForestRegressor&#xff…

Vue.js 2.0 组件

什么是组件&#xff1f; 组件&#xff08;Component&#xff09;是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素&#xff0c;封装可重用的代码。在较高层面上&#xff0c;组件是自定义元素&#xff0c; Vue.js 的编译器为它添加特殊功能。在有些情况下&#xff0c;组件也…

《花雕学AI》19:比较ChatGPT与新Bing在文章润色方面的应用优势与测试案例

引言&#xff1a; 文章润色是指对已经写好的文章进行修改、优化或完善的过程&#xff0c;以提高文章的质量和效果。文章润色涉及到多方面的内容&#xff0c;如语言表达、逻辑结构、文献引用、格式规范等。文章润色对于提升写作水平、提高论文发表率、增加学术影响力等都有重要意…

JavaScript【趣味】做一个网页版2048

文章目录&#x1f31f;前言&#x1f31f;先看效果&#xff08;粉丝特权哈哈&#xff09;&#x1f31f;代码实现&#x1f31f;页面布局 【index.html】&#x1f31f;样式文件【2048.css】&#x1f31f;index.html 里用到的JS文件&#x1f31f;jquery.min.js&#x1f31f;util.js…

300元左右的蓝牙耳机哪个好?300左右音质最好的蓝牙耳机

无线耳机是人们日常生活中必不可少的设备&#xff0c;无论是听音乐化石看电影都能获得身临其境的感觉&#xff0c;由于科技真在发展中&#xff0c;不断地的发生变化&#xff0c;百元价位就可以感受到不错的音色&#xff0c;下面小编整理了几款300左右音质表现不错的蓝牙耳机。 …

Linux 、Android将在汽车舞台上开战

导读在 CES 2017 上&#xff0c;AGL 宣布&#xff0c;Mercedes-Benz 的母公司 Daimler 正式加入。这是第十家汽车制造商加入 AGL&#xff0c;也是第一家德国公司加入 AGL。AGL&#xff08;Automotive Grade Linux&#xff09;&#xff0c;是 Linux 基金会的一个相互协作的开源组…

mallox勒索病毒数据恢复|金蝶、用友、管家婆、OA、速达、ERP等软件数据库恢复

目录 前言&#xff1a; 一、mallox勒索病毒及xollam勒索病毒的特点 二、mallox勒索病毒及xollam勒索病毒的影响 三、mallox勒索病毒及xollam勒索病毒数据恢复服务 四、mallox勒索病毒及xollam勒索病毒加密数据库恢复案例 五、以下是预防mallox勒索病毒及xollam勒索病毒安全…

解读CANDT测试项-采样点测试

原标题&#xff1a;解读CANDT测试项-采样点测试 一、为什么要进行采样点测试&#xff1f; 本文引用地址&#xff1a;http://www.eepw.com.cn/article/202004/411611.htm 为了保证有效的通信&#xff0c;对于一个只有两个节点的CAN网络&#xff0c;其两边距离不超过最大的传输…

day12 共享内存(内存映射的使用、注意事项、进程间通信、systemV共享内存)

内存映射的基本使用 概念&#xff1a; 功能共享内存可以通过mmap&#xff08;&#xff09;映射普通文件。 是一个磁盘文件与内存中的一个缓冲区相映射&#xff0c;进程可以像访问普通内存一样对文件进行访问&#xff0c;不必在调用read 、write。 mmap&#xff08;&#xf…

ChatGPT 与 MindShow 一分钟搞定一个PPT

前言 PPT制作是商务、教育和各种场合演讲的重要组成部分。然而&#xff0c;很多人会花费大量时间和精力在内容生成和视觉设计方面。为了解决这个问题&#xff0c;我们可以利用两个强大的工具——ChatGPT和MindShow&#xff0c;来提高制作PPT的效率。 一、ChatGPT 与 MindShow…

JUC-01 线程的创建和状态转换

本次我们主要讲三个问题 线程是什么&#xff1f;线程有哪些状态&#xff1f;各状态间的转换了解吗&#xff1f;创建线程的3种方法你都了解吗&#xff1f; 1. 线程是什么&#xff1f;&#xff08;了解即可&#xff09; 进程&#xff1a; 进程是一个具有一定独立功能的程序在一…

四次挥手刨根问底19问详解,全网最全

1.请描述一下TCP连接的四次挥手过程&#xff1f; 回答&#xff1a;TCP连接的四次挥手过程包括以下步骤&#xff1a; 步骤1&#xff1a;客户端向服务器端发送一个FIN报文段&#xff0c;请求关闭连接。 步骤2&#xff1a;服务器端收到FIN报文段后&#xff0c;向客户端发送一个…

python列表,元组和字典

1、python列表 1.1.列表的定义 list是一种有序的集合、基于 链表实现,name[ ] ,全局定义:list2list([ ])。 1.2下标索引 python不仅有负索引也有正索引。正索引从0开始,负索引从-1开始。这两个可以混用,但指向还是那个位置 a[0]a[-9]//length为10的数组a1.3列表的切片 列表可…

navicat如何使用orcale(详细步骤)

目录前言操作1.连接数据库2.建库问题总结前言 看过我昨天文章的兄弟姐妹都知道最近接手另一个国企项目&#xff0c;数据库用的是orcale。实话实说&#xff0c;也有快三年没用过orcale数据库了。 这期间问题不断&#xff0c;因为orcale日渐消沉&#xff0c;网上资料也是真真假…

UE4 回放系统升级到UE5之后的代码报错问题解决

关键词&#xff1a; UE4 回放系统 升级 UE5 报错 DemoNetDriver GetDemoCurrentTime GetDemoTotalTime 背景 照着网上教的UE4的回放系统&#xff0c;也叫重播系统&#xff0c;英文Replay。做完了&#xff0c;测试运行正常&#xff0c;可升级到UE5却报了一堆 WorldSetting 和 …

(20230417)最大数合并区间重新排列单词间的空格 按奇偶排序数组 II 数组形式的整数加法

最大数&#xff08;回顾等级&#xff1a;值得&#xff0c;已达最优解&#xff09; 来源&#xff1a;自己LeetCode刷题 usa long cmp(const void* e1, const void* e2) {int* p1(int*)e1;int* p2(int*)e2;long n110;long n210;while(*p1>n1){n1*10;}while(*p2>n2){n2*1…

利用AOP实现统一功能处理

目录 一、实现用户登录校验 实现自定义拦截器 将自定义的拦截器添加到框架的配置中&#xff0c;并且设置拦截的规则 二、实现统一异常处理 三、实现统一数据格式封装 一、实现用户登录校验 在之前的项目中&#xff0c;在需要验证用户登录的部分&#xff0c;每次都需要利…

RK3568平台开发系列讲解(环境篇)使用USB线缆升级固件

🚀返回专栏总目录 文章目录 一、进入升级模式1.1、硬件方式进入Loader模式1.2、软件方式进入Loader模式二、安装烧写工具2.1、Windows操作系统2.2、Linux操作系统沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍了如何将主机上的固件,通过USB数据线烧录到…

Typora(Mckbook版)的使用方法

1、标题&#xff08;⌘ 数字&#xff09; 一级标题&#xff1a;&#xff08;⌘ 1&#xff09; 二级标题&#xff1a;&#xff08;⌘ 2&#xff09; 三级标题&#xff1a;&#xff08;⌘ 3&#xff09; 四级标题&#xff1a;&#xff08;⌘ 4&#xff09; ... 六级标…