手撕Java集合——链表

news2024/11/24 11:57:03

链表

  • 一、链表概念特性
  • 二、不带头单向非循环链表实现
    • 🍑1、定义结点
    • 🍑2、打印链表
    • 🍑3、使用递归逆序打印链表
    • 🍑4、头插
    • 🍑5、尾插
    • 🍑6、指定位置插入
    • 🍑7、查找是否包含关键字key是否在单链表当中
    • 🍑8、删除第一次出现关键字为key的节点
    • 🍑9、删除所有值为key的节点
    • 🍑10、得到单链表的长度
    • 🍑11、清空链表
  • 三、无头双向非循环链表
  • 四、Java集合中的 LInkedList
    • 🍑1、LinkedList构造方法
    • 🍑2、LinkedList常用方法
  • 五、线性表总结:LinkedList和ArrayList区别

一、链表概念特性

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。

特点:

  1. 上图每个数据块是一个结点,这些结点一般是从堆上申请来的。
  2. 链式结构在逻辑上是连续的,但物理上不一定连续。

链表结构由【单向/双向;循环/非循环;带头/不带头】几种形式进行组合,共有2×2×2=8种不同结构,不过我们常用的主要是 单向不带头非循环链表双向不带头非循环链表 。对于前者来说,结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。对于后者,在Java的集合框架库中LinkedList底层实现就是无头双向非循环链表。

接下来我们就以不带头单向非循环链表和不带头双向循环链表进行展开,分别模拟实现它们的主要功能。

二、不带头单向非循环链表实现

首先给出功能接口(实现时元素类型为int):

//1.打印链表
public void display() {}

//2.递归逆序打印链表
public void displayReverse(SingleLinkedList.ListNode node) {}

//3.头插法
public void addFirst(int data) {}

//4.尾插法
public void addLast(int data) {}

//5.任意位置插入,第一个数据节点为0号下标
public boolean addIndex(int index, int data) {}

//6.查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {}

//7.删除第一次出现关键字为key的节点
public void remove(int key) {}

//8.删除所有值为key的节点
public void removeAllKey(int key) {}

//9.得到单链表的长度
public int size() {}

//10.清空链表
public void clear() {}

🍑1、定义结点

这里是将结点看做单链表SingleLinkedList的一部分,所以将结点定义为链表类-SingleLinkedList的内部类。
注意: 这里的head结点并不表示链表带有单独头结点(不管链表是否为空,均含有一个头结点),而是用来表示该链表的第一个结点。

public class SingleLinkedList {
	static class ListNode {
    	public int val;//存储的数据
    	public ListNode next;//存储下一个节点的地址
		//构造方法
    	public ListNode(int val) {
        	this.val = val;
    	}
	}
	
	public ListNode head;//表示当前链表的头结点
	
	//一些功能接口……
}
	

🍑2、打印链表

思路:从头结点开始向后变历链表,直到结点为空,打印完毕。

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

🍑3、使用递归逆序打印链表

思路:利用递归思想,先递后归,实现倒序打印链表。

    public void displayRecu(ListNode node) {
        if (node == null) {
            return;
        }
        if (node.next == null) {
            System.out.print(node.val+" ");
            return;
        }
        displayRecu(node.next);
        System.out.print(node.val+" ");
    }

🍑4、头插

    //头插法【比较简单,整体也没有特殊情况,head=null也没问题!】
    public void addFirst(int data) {
        ListNode newNode = new ListNode(data);
        newNode.next = head;
        head = newNode;
    }

🍑5、尾插

    //尾插法【需要考虑头结点为null的情况】
    public void addLast(int data) {
        ListNode newNode = new ListNode(data);
        ListNode cur = head;
        //如果头结点为空,直接赋值
        if (head == null) {
            head = newNode;
            return;
        }
        //找到尾结点
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = newNode;

    }

🍑6、指定位置插入

	//下标合法性检测
    private void checkIndex(int index) {
    	//这里的size()是自定义函数(后面讲解)
        if (index < 0 || index > size()) {
            throw new IndexOutOfException("下标越界!");
        }
    }

    //找到 index-1位置的节点的地址
    private ListNode findIndexSubOne(int index) {
        ListNode cur = head;
        int count = 0;
        while (count != index - 1) {
            cur = cur.next;
            count++;
        }
        return cur;
    }

    //指定位置插入
    public boolean addIndex(int index, int data) throws IndexOutOfException {
        //判断坐标合法性
        checkIndex(index);

        //index=0这种情况需要特殊处理一下【1.head=null,index=0;2.head!=null,index=0】
        if (index == 0) {
            addFirst(data);
            return true;
        }
        //尾插不用特殊考虑,以下包含了尾插

        //中间插入
        ListNode newNode = new ListNode(data);
        ListNode cur = findIndexSubOne(index);
        newNode.next = cur.next;
        cur.next = newNode;
        return true;
    }

🍑7、查找是否包含关键字key是否在单链表当中

思路: 遍历链表,比对value值即可

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

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


    //删除第一次出现关键字为key的节点
    public void remove(int key) {
        //链表一个结点都没有
        if (head == null) {
            System.out.println("链表为空!");
            return;
        }
        //判断头结点的val
        if (head.val == key) {
            head = head.next;
            return;
        }
        //找到key的前驱结点
        ListNode cur = head;
        while (cur.next != null) {
            if (cur.next.val == key) {
                cur.next = cur.next.next;
                return;
            }
            cur = cur.next;
        }
        System.out.println("未找到关键字" + key);
    }

🍑9、删除所有值为key的节点

思路: 这个算法相比于“删除第一次出现关键字为key的节点”只需略微调整(划去return;单独判断头结点)
注意:这里头结点的判断放在了最后,因为这里是删除所有值为key的节点,由于head结点的特殊性(head结点的value=key),且head后对key值的删除处理具有同一性,所以这里就先一并处理head后结点,最后单独判断head结点。对于“删除第一次出现关键字为key的节点”,由于此时的场景是处理第一次出现的key值,所以就必须先处理head结点。

    //删除所有值为key的节点
    public void removeAllKey(int key) {
        //为空情况
        if (head == null) {
            return;
        }
        //头结点后存在key值的情况
        ListNode cur = head;
        while (cur.next != null) {
            if (cur.next.val == key) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        //头结点单独判断
        if (head.val == key) {
            head = head.next;
        }
    }

🍑10、得到单链表的长度

思路: 遍历链表,返回count计数值即可。

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

🍑11、清空链表

思路: 在Java中,当一个对象没有任何引用指向时,Java自带的垃圾回收机制会自动删除该对象。因此, 只要将链表的头节点赋值为空,链表的所有节点就会被视为不再被引用,可以通过垃圾回收机制来删除它们。

具体来说,当链表的头节点变成了null,链表的所有节点就不再有任何引用指向它们,这意味着这些节点成为Java内存中的"孤儿"节点,也就是不再被外部引用的节点。这时,Java垃圾回收机制会自动触发,将不再被引用的节点删除。

需要注意的是:如果链表中的节点存在其他引用指向它们,那么这些节点就不会被视为垃圾,也不会被自动删除。因此,在清空链表前需要确保没有其它引用指向链表中的任何一个节点,否则这些节点会一直被占用,浪费内存空间。

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

三、无头双向非循环链表

双向不带头非循环链表,与上述单向不带头非循环链表的主要差别体现在功能接口的实现上,特别是删除和插入操作时,不需要在保存前驱结点,可根据当前结点进行操作。

下面是无头双向非循环链表的代码实现,很多功能接口和上述单链表的实现多有雷同,并且下面代码中给出了详细的注释,大家可以参考理解:

// 2、无头双向链表实现
public class MyLinkedList {
    static class Node {
        // 数据
        public int val;
        // 前驱结点
        public Node prev;
        // 后继结点
        public Node next;
        // 结点构造
        public Node(int val) {
            this.val = val;
        }
    }
    // 当前头结点
    public Node head;
    // 当前尾结点
    public Node last;

    //打印双向链表(同单向链表)
    public void display() {
        Node cur = head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    //头插法(思路同单向链表)
    public void addFirst(int data) {
        Node newnode = new Node(data);
        if (head == null) {
            head = newnode;
            last = newnode;
        } else {
            newnode.next = head;
            head.prev = newnode;
            head = newnode;
        }
    }

    //尾插法(思路同单向链表)
    public void addLast(int data) {
        Node newnode = new Node(data);
        if (head == null) {
            head = newnode;
            last = newnode;
        } else {
            last.next = newnode;
            newnode.prev = last;
            last = newnode;
        }
    }

    //得到双向链表的长度(同单链表)
    public int size() {
        int count = 0;
        Node cur = head;
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        return count;
    }
    
    //任意位置插入(注:第一个数据节点为0号下标)
    public void addIndex(int index, int data) {
        // 判断坐标合法性
        if (index<0 || index>size()) {
            throw new IndexOutOfException("坐标非法!");
        }
        // 构建新节点
        Node newnode = new Node(data);
        // 处理头插尾插特殊情况
        if (index==0) {
            addFirst(data);
            return;
        }
        if (index==size()) {
            addLast(data);
            return;
        }
        // 处理在中间插入情况(这里体现了双向链表的优势)
        // 将cur结点指向当前坐标
        Node cur = head;
        while (index!=0) {
            cur = cur.next;
            index--;
        }
        // 修改新节点的 next 域
        newnode.next = cur;
        // 修改 cur 结点的前驱结点的 next 域
        cur.prev.next = newnode;
        // 修改新节点的 prev 域
        newnode.prev = cur.prev;
        // 修改 cur 结点的 prev 域
        cur.prev = newnode;
    }

    //查找是否包含关键字key是否在单链表当中(同单链表)
    public boolean contains(int key) {
        Node cur = head;
        while (cur!=null) {
            if (cur.val==key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    //删除第一次出现关键字为key的节点(体现双向链表优势)
    public void remove(int key) {
        Node cur = head;
        while (cur!=null) {
            if (cur.val==key) {
                //如果只有一个结点(单链表不存在last所以不存在这一步)
                if (head == last) {
                    head = null;
                    last = null;
                    return;
                }
                //如果cur是头结点
                if (cur == head) {
                    head = head.next;
                    head.prev = null;
                    return;
                }
                //如果cur是尾结点
                if (cur == last) {
                    last = last.prev;
                    last.next = null;
                    return;
                }
                // 如果cur是中间结点
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
                return;
            }
            cur = cur.next;
        }
    }

    //删除所有值为key的节点[对上面代码进行改进]
    public void removeAllKey(int key) {
        Node cur = head;
        while (cur!=null) {
            //满足条件开始删除
            if (cur.val==key) {
                if (head == last) {
                    // 如果只有1个结点
                    head = null;
                    last = null;
                    return;
                } else if (cur == head) {
                    // 如果cur是头结点,并且节点数必定大于等于2
                    head = head.next;
                    if (head!=null) {
                        head.prev = null;
                    }
                } else if (cur == last){
                    // 如果是尾巴结点
                    last = last.prev;
                    last.next = null;
                } else {
                    // 如果是中间结点
                    cur.prev.next = cur.next;
                    cur.next.prev = cur.prev;
                }
            }
            cur = cur.next;
        }
    }

    //双向链表的清空操作
    public void clear() {
        Node cur = head;
        while (cur!=null) {
            Node curNext = cur.next;
            cur.prev=null;
            cur.next=null;
            cur = curNext;
        }
        head=null;
        last=null;
    }
}

四、Java集合中的 LInkedList

java.util包下为我们提供了LinkedList集合类,它的底层就是一个无头双向非循环链表结构。并且这里的 LinkedList是以泛型方式实现的,使用时必须要先实例化,同时,它也实现了 List 接口,可以通过 List 接口接收LinkedList对象,并使用List方法操作LinkedList对象:

🍑1、LinkedList构造方法

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

🍑2、LinkedList常用方法

方法描述
boolean add(E e)在链表尾部插入元素 e
void add(int index, E element)将元素 element 插入到指定的 index 位置。
void addFirst(E e)在该列表开头插入指定的元素。
boolean addAll(Collection<? extends E> c)将集合 c 中的所有元素尾部插入链表。
E remove(int index)删除指定 index 位置的元素,并返回被删除的元素。
boolean remove(Object o)删除遇到的第一个等于 o 的元素,若删除成功则返回 true
E get(int index)获取指定 index 位置的元素。
E set(int index, E element)将指定 index 位置的元素替换为新的元素 element,并返回原元素。
void clear()清空链表中的所有元素。
boolean contains(Object o)判断链表中是否包含元素 o,如果存在则返回 true
int indexOf(Object o)返回第一个出现的元素 o 的索引,如果不存在则返回 -1
int lastIndexOf(Object o)返回最后一个出现的元素 o 的索引,如果不存在则返回 -1
List subList(int fromIndex, int toIndex)截取链表部分元素,并返回一个新的 List 对象。

五、线性表总结:LinkedList和ArrayList区别

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

说明: ArrayList支持随机访问,LinkedList不支持随机访问"的意思是指在数据结构中,ArrayList可以通过索引直接访问和获取元素,而LinkedList不能通过索引进行直接访问和获取。

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

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

相关文章

C#,数值计算——Dynpro的计算方法与源程序

给定向量nstate&#xff0c;其整数值是每个状态中的状态数阶段&#xff08;第一和最后阶段为1&#xff09;&#xff0c;并给定函数成本&#xff08;j&#xff0c;k&#xff0c;i&#xff09;返回在阶段i的状态j和的状态k之间移动的成本阶段i1&#xff0c;此例程返回与nstate长度…

(杭电多校)2023“钉耙编程”中国大学生算法设计超级联赛(8)

1005 0 vs 1 双端队列暴力模拟,时间复杂度为O(n*T) 首先预处理0的右边第一个0的下标,1的右边第一个1的下标,0的左边第一个0的下标,1的左边第一个1的下标 然后进行模拟 如果当前是zero的轮次,那么就看双端队列的两端 如果两头都是1,那么one赢,如果1头是0,1头是1,那么只能选择0 如…

概率图模型(Probabilistic Graphical Model,PGM)

概率图模型&#xff08;Probabilistic Graphical Model&#xff0c;PGM&#xff09;&#xff0c;是一种用图结构来描述多元随机变量之间条件独立性的概率模型。它可以用来表示复杂的概率分布&#xff0c;进行有效的推理和学习&#xff0c;以及解决各种实际问题&#xff0c;如图…

计算机基础概论

一、计算机的组成 1.计算机组成的五大部件 &#xff08;1&#xff09;运算器&#xff1a;也叫算术逻辑单元&#xff0c;完成对数据的各种常规运算&#xff0c;如加减乘除&#xff0c;也包括逻辑运算&#xff0c;位移&#xff0c;比较等。 &#xff08;2&#xff09;控制器&a…

掌握Python的X篇_34_Python朗读文字

各种广告中说python是人工智能的主宰&#xff0c;其实这更多是噱头的成分&#xff0c;但是python确实可以做很多的事情&#xff0c;本篇将会介绍利用pythonAI平台来合成声音。今天将会用到的是百度。 文章目录 1. baiToVoice2. 注册appid3. 合成代码 1. baiToVoice 使用百度A…

详解Mysql——第一篇/连接查询

mysql的连接查询&#xff0c;相必在网上都能找到很多的教程&#xff0c;博主今天不做老话常谈&#xff0c;不走重复路线 1.建表 –1.学生表 Student(s_id,s_name,s_birth,s_sex) –学生编号,学生姓名, 出生年月,学生性别 –2.课程表 Course(c_id,c_name,t_id) – –课程编…

ubuntu18.04下配置muduoC++11环境

1.安装muduo依赖的编译工具及库 Cmake sudo apt-get install cmakeBoost sudo apt-get install libboost-dev libboost-test-devcurl、c-ares DNS、google protobuf sudo apt-get install libcurl4-openssl-dev libc-ares-dev sudo apt-get install protobuf-compiler libp…

【单片机毕业设计2-基于stm32c8t6的智能台灯/书桌系统】

【单片机毕业设计2-基于stm32c8t6的智能台灯/书桌系统】 前言一、功能介绍二、硬件部分三、软件部分总结 前言 &#x1f525;这里是小殷学长&#xff0c;单片机毕业设计篇2 基于stm32的智能台灯/智能书桌系统 &#x1f9ff;创作不易&#xff0c;拒绝白嫖&#xff08;有需可点击…

JavaWeb中Json传参的条件

JavaWeb中我们常用json进行参数传递 对应的注释为RequestBody 但是json传参是有条件的 最主要是你指定的实体类和对应的json参数能否匹配 1.属性和对应的json参数名称对应 2.对应实体类实现了Serializable接口&#xff0c;可以进行序列化和反序列化&#xff0c;这个才是实体类转…

【Minecraft】Fabric Mod开发完整流程4 - 自定义物品方块以及食物、燃料

目录 自定义物品与方块自动侦测矿藏工具工具功能实现执行结果 自定义音乐方块自定义食物自定义燃料 自定义物品与方块 自动侦测矿藏工具 探测器纹理下载地址&#xff1a; https://url.kaupenjoe.net/mbkj57/assets 众所周知&#xff0c;正经人永远不喜欢常规套路挖矿&#xff0…

一种改进的低导通电阻和开关损耗4H-SiC沟槽栅MOSFET

目录 标题&#xff1a;An Improved 4H-SiC Trench-Gate MOSFET With Low ON-Resistance and Switching Loss摘要信息解释ICP-RIELPCVDac电容的串并联 研究了什么文章的创新点文章的研究方法文章的结论 标题&#xff1a;An Improved 4H-SiC Trench-Gate MOSFET With Low ON-Resi…

阿里云账号注册入口_账户注册详细流程(图文)

阿里云账号怎么注册&#xff1f;阿里云账号支持手机号注册、阿里云APP注册、支付宝和钉钉多种注册方式&#xff0c;账号注册后需要通过实名认证才可以购买或使用云产品&#xff0c;阿里云百科来详细说下不同途径注册阿里云账号图文流程&#xff1a; 目录 阿里云账号注册流程 …

考虑分布式电源的配电网无功优化问题研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

ubuntu18.04安装NFS并启动NFS(mount挂载)

首先得把虚拟机网络更改成桥接模式&#xff0c;并把网段设置成与Windows同一网段&#xff0c;可以参考我的这篇博文http://t.csdn.cn/kRmNl ubuntu18.04安装NFS并启动NFS 终端输入指令&#xff1a;sudo apt install nfs-kernel-server 在ubuntu 18.04 下创建一个mount 共享的…

从C语言到C++_32(哈希的应用)位图bitset+布隆过滤器+哈希切割

目录 1. 位图 1.1 位图的概念 1.2 位图的实现 1.3 位图解决海量数据面试题 完整BitSet.h和two_bitset: 1.4 位图的优缺点 2. 布隆过滤器 2.1 布隆过滤器的概念 2.2 布隆过滤器的实现 完整 BloomFilter.h 和测试 2.3 布隆过滤器的优缺点和应用 3. 哈希切割&#xff…

AI 绘画Stable Diffusion 研究(七) 一文读懂 Stable Diffusion 工作原理

大家好&#xff0c;我是风雨无阻。 本文适合人群&#xff1a; 想要了解AI绘图基本原理的朋友。 对Stable Diffusion AI绘图感兴趣的朋友。 本期内容&#xff1a; Stable Diffusion 能做什么 什么是扩散模型 扩散模型实现原理 Stable Diffusion 潜扩散模型 Stable Diffu…

克隆你的声音,只需要你 5 秒钟的语音,就能生成你说出来的任何话,免费开源使用,细思极恐

克隆你的声音,只需要你 5 秒钟的语音,就能生成你说出来的任何话,免费开源使用,细思极恐。可联系作者帮忙部署使用。 Voice Cloning This repository is an implementation of Transfer Learning from Speaker Verification to Multispeaker Text-To-Speech Synthesis (SV…

AcWing算法提高课-5.1.1哥德巴赫猜想

宣传一下 算法提高课整理 CSDN个人主页&#xff1a;更好的阅读体验 原题链接 题目描述 哥德巴赫猜想的内容如下&#xff1a; 任意一个大于 4 4 4 的偶数都可以拆成两个奇素数之和。 例如&#xff1a; 8 3 5 8 3 5 835 20 3 17 7 13 20 3 17 7 13 20317713 …

多线程与高并发--------原子性、可见性、有序性

二、并发编程的三大特性 一、原子性 1.1 什么是并发编程的原子性 JMM&#xff08;Java Memory Model&#xff09;。不同的硬件和不同的操作系统在内存上的操作有一定差异的。Java为了解决相同代码在不同操作系统上出现的各种问题&#xff0c;用JMM屏蔽掉各种硬件和操作系统带…

腾讯云CVM服务器2核2g1m带宽支持多少人访问?

腾讯云2核2g1m的服务器支持多少人同时访问&#xff1f;2核2g1m云服务器短板是在1M公网带宽上&#xff0c;腾讯云服务器网以网站应用为例&#xff0c;当大规模用户同时访问网站时&#xff0c;很大概率会卡在公网带宽上&#xff0c;所以压根就谈不上2核2G的CPU内存计算性能是否够…