数据结构(2):LinkedList和链表[1]

news2024/9/17 8:46:55

 下面我们来介绍一种新的数据结构,链表。

我们曾经讨论过顺序表。它的数据存储在物理和逻辑上都是有逻辑的。而我们今天要学习的链表,则在物理结构上非连续存储,逻辑上连续。

1.链表的认识

链表由一个一个的节点组成。

我们可以想象一列火车,每一节车厢都被前面一节拉着,也拉着后面一节(头尾除外)。

我们的链表与火车近似,我们可以将每一个节点当作一节车厢,它除了存储自己的数据之外,还能带领我们找到链接在它后面的一个节点,这样“一节一节”把所有数据串联起来。

单个节点是这样的结构:

地址指的是下一个节点的位置,这样,我们就可以像火车一样把他们串联起来:

(水平有限,意思到了即可)

链表也分很多种类,它可以是双向/单向,不带头/带头,非循环/循环。我们先着重讨论,单项不带头非循环链表。

2.单项不带头非循环链表的实现

同样我们也要先明白链表的实现,我们来先创建一个自己的SingleLinkedList类。

然后,我们来讨论对于节点的定义,我们需要再定义一个节点Listnode类,需要注意,它要被定义在链表这个类里面,它属于一个内部类。它里面要存放值和下一个节点地址,我们可以这样操作:

public class SingleLinkedList {
    static class Listnode{
        public int val;//节点值域
        public Listnode next;//下一个节点的地址

        public Listnode(int val) {
            this.val = val;
        }
    }
    public Listnode head;//表示当前链表头节点
}

这样我们就完成了对节点的定义,下面我们就要实现链表的各种操作。包括但不限于:创建链表、遍历链表、插入数据(头插/尾插)、删除所有值为k的节点、清空链表等等。同样,我们先把每个方法大的框架定义出来,再一个个实现。

public class SingleLinkedList {
    static class Listnode{
        public int val;//节点值域
        public Listnode next;//下一个节点的地址

        public Listnode(int val) {
            this.val = val;
        }
    }
    public Listnode head;//表示当前链表头节点

    //头插法
    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 clear() {
    }
    //遍历打印
    public void display() {}
}

creatlist():

首先我们可以手动创建链表,一个节点一个节点进行定义。它并不属于链表的方法,但可以帮助我们理解。我们先创建几个节点,给他们赋值,再用.next一个一个链接,具体可以这样实现:

 public void creatlist(){
        Listnode listnode1=new Listnode(1);
        Listnode listnode2=new Listnode(2);
        Listnode listnode3=new Listnode(3);
        Listnode listnode4=new Listnode(4);
        Listnode listnode5=new Listnode(5);

        listnode1.next=listnode2;
        listnode2.next=listnode3;
        listnode3.next=listnode4;
        listnode4.next=listnode5;

        this.head=listnode1;
    }

不要忘了设置头节点,那么相当于我们已经手动创建出来了一个链表。它大概是这样的:

那么我们的链表就是长这个样子,我们后续操作会基于这种结构进行讲解。

dispaly():

遍历链表,我们要做的就是打印当前节点的值,之后成功走到下一个节点,直到最后,我们可以定义一个新的节点对象,让它指向头节点,从头节点开始一步一步往后走,这样既不会改变头节点的指向,也不会改变链表本身的结构。

public void display() {
        Listnode cur=head;
        while(cur!=null){
            System.out.println(cur.val);
            cur=cur.next;//cur往后走
        }
    }

我们可以测试一下

public static void main(String[] args) {
        SingleLinkedList list=new SingleLinkedList();
        list.creatlist();
        list.display();
    }

这就证明向后一步一步走这个策略是没问题的,我们后续很多操作都要遍历链表,我们要始终贯彻这一思想。

插入数据--头插:

头插就是让插入的节点在最开头。我们要贯彻一件事,不管怎么插,头节点永远是第一个节点。那么我们的思想是先将值赋给一个新的节点,让这个节点指向原来的头节点,再改变头节点的指向,这样就可以将新节点与原来的链表连接起来,并且保证了头节点的正确性。

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

插入数据--尾插:

这个就会简单一些,我们只需要定义一个cur,让它从头开始往后走,当走到最后一个时(此时是cur.net==null!),把它和新节点连接起来就可以了。

 //尾插法
    public void addLast(int data){
        Listnode node=new Listnode(data);
        Listnode cur=head;
        while(cur.next!=null){
            cur=cur.next;
        }
        cur.next=node;
    }

插入数据--任意位置插入

首先我们要保证pos位置的合法性(头节点位置为0),然后当我们插入时,我们要记录前后两个节点的信息,我们要同时保证前面的可以连接上新节点,并且保证新节点可以连接上后面的,不改变链表的连贯性。所以,我们要找到插入节点的前一个节点的信息,才能方便操作。顺便,当index==0/index==size(),我们可以进行头插尾插,所以我们可以把获取链表长度这个方法写出来。

public void addIndex(int index,int data){
        Listnode node=new Listnode(data);
        Listnode cur=findIndexSubOne(index);
        if(index==0){
            addFirst(data);
            return;
        }
        if(index==size()){
            addLast(data);
            return;
        }
        cur.next=node.next;
        node.next=cur;
    }
    private Listnode findIndexSubOne(int index){
        //找到想要删除/插入节点位置的前一个节点
        Listnode cur=head;
        while(index-1!=0){
            cur=cur.next;
        }
        return cur;
    }
public int size(){
        Listnode cur=head;
        int cnt=0;
        while(cur!=null){
            cnt++;
            cur=cur.next;
        }
        return cnt;
    }

我们重点体会这样两行代码:

cur.next=node.next;
node.next=cur;

这两行是插入的关键。第一行是让新插入节点的后继是后面的节点,第二行则是改变前面节点的指向,指向新插入的节点,这样就既与前面链接,也与后面连上了,且没有改变结构,我们也就实现了中间节点的插入。

contains():

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

这个就很简单了,我们遍历一下一带而过就可以了。

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

remove():

删除分为两种,一种是删除第一次出现的关键字,一种是删除链表里所有的关键字的节点。它们的思想都是一样的。我们删除的思想是:找到要删除的关键字,把它跳过,就可以了。下面我们来实现一下:

 public void remove(int key){
        Listnode cur=head.next;
        Listnode pre=head;//要删除节点的前驱
        while(cur!=null){
            if(cur.val==key){
                pre.next=cur.next;
                return;
            }else{
                pre=cur;
                cur=cur.next;
            }
        }
        //删除头节点
        if(head.val==key){
            head=head.next;
        }
    }
public void remove(int key){
        Listnode cur=head.next;
        Listnode pre=head;//要删除节点的前驱
        while(cur!=null){
            if(cur.val==key){
                pre.next=cur.next;
            }else{
                pre=cur;
                cur=cur.next;
            }
        }
        //删除头节点
        if(head.val==key){
            head=head.next;
        }
    }

我们定义一个cur,一个prev,代表当前节点和它的前驱,如果cur走到了要删除的节点,就让prev.next=cur.next,进行跳过。如果没有,就让两“人”都往前走一步,直到最后。当然,这里没有考虑头节点被删除的情况,我们单独实现了一下。

clear():

最后是清空链表,很简单,直接让头节点为空就可以了,这样链表就不存在了。

public void clear() {
        this.head=null;
    }

那么我们就完成了,下面是完整的代码实现:

public class SingleLinkedList {
    static class Listnode{
        public int val;//节点值域
        public Listnode next;//下一个节点的地址

        public Listnode(int val) {
            this.val = val;
        }
    }
    public Listnode head;//表示当前链表头节点
    public void creatlist(){
        Listnode listnode1=new Listnode(1);
        Listnode listnode2=new Listnode(2);
        Listnode listnode3=new Listnode(3);
        Listnode listnode4=new Listnode(4);
        Listnode listnode5=new Listnode(5);

        listnode1.next=listnode2;
        listnode2.next=listnode3;
        listnode3.next=listnode4;
        listnode4.next=listnode5;

        this.head=listnode1;
    }
    //头插法
    public void addFirst(int data){
        Listnode node=new Listnode(data);
        node.next=head;
        head=node;
    }
    //尾插法
    public void addLast(int data){
        Listnode node=new Listnode(data);
        Listnode cur=head;
        while(cur.next!=null){
            cur=cur.next;
        }
        cur.next=node;
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        Listnode node=new Listnode(data);
        Listnode cur=findIndexSubOne(index);
        if(index==0){
            addFirst(data);
            return;
        }
        if(index==size()){
            addLast(data);
            return;
        }
        cur.next=node.next;
        node.next=cur;
    }
    private Listnode findIndexSubOne(int index){
        //找到想要删除/插入节点位置的前一个节点
        Listnode cur=head;
        while(index-1!=0){
            cur=cur.next;
        }
        return cur;
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        Listnode cur=head;
        while(cur!=null){
            if(cur.val==key)
                return true;
            cur=cur.next;
        }
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key){
        Listnode cur=head.next;
        Listnode pre=head;//要删除节点的前驱
        while(cur!=null){
            if(cur.val==key){
                pre.next=cur.next;
                return;
            }else{
                pre=cur;
                cur=cur.next;
            }
        }
        //删除头节点
        if(head.val==key){
            head=head.next;
        }
    }
    //删除所有值为key的节点
    public void removeAllKey(int key){
        Listnode cur=head.next;
        Listnode pre=head;//要删除节点的前驱
        while(cur!=null){
            if(cur.val==key){
                pre.next=cur.next;
            }else{
                pre=cur;
                cur=cur.next;
            }
        }
        //删除头节点
        if(head.val==key){
            head=head.next;
        }
    }
    //得到单链表的长度
    public int size(){
        Listnode cur=head;
        int cnt=0;
        while(cur!=null){
            cnt++;
            cur=cur.next;
        }
        return cnt;
    }
    //清空链表
    public void clear() {
        this.head=null;
    }
    //遍历打印
    public void display() {
        Listnode cur=head;
        while(cur!=null){
            System.out.println(cur.val);
            cur=cur.next;//cur往后走
        }
    }
}

和顺序表一样,我们使用链表也不需要自己重新写,java已经帮我们写好了,我们只需要直接使用就可以了,下面我们来学习链表的使用。

3.单链表的使用

 public static void main(String[] args) {
        List<Integer>list=new LinkedList<>();
        list.add(1);//头插
        list.add(2);
        list.add(3);
        for( int x:list){
            System.out.println(x);
        }
    }

我们可以像这样对单链表对象进行构造,并使用其中的方法,方法大部分都与我们实现的没有太大区别。

4.小结

这篇文章我们主要讨论的是单链表的实现,其中定义cur节点进行遍历这个思想十分重要。单链表十分重要,我们下篇文章会找一些常见典型的单链表的题,通过对题目的的分析加深大家对单链表的印象。

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

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

相关文章

乐鑫安全制造全流程

主要参考资料&#xff1a; 【乐鑫全球开发者大会】DevCon24 #10 &#xff5c;乐鑫安全制造全流程 乐鑫官方文档Flash加密: https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/security/flash-encryption.html 【ESP32S3】使用 Flash 下载工具完成 Flash 加密功能…

C++ | Leetcode C++题解之第394题字符串解码

题目&#xff1a; 题解&#xff1a; class Solution { public:string src; size_t ptr;int getDigits() {int ret 0;while (ptr < src.size() && isdigit(src[ptr])) {ret ret * 10 src[ptr] - 0;}return ret;}string getString() {if (ptr src.size() || src[…

C语言 | Leetcode C语言题解之第393题UTF-8编码验证

题目&#xff1a; 题解&#xff1a; static const int MASK1 1 << 7; static const int MASK2 (1 << 7) (1 << 6);bool isValid(int num) {return (num & MASK2) MASK1; }int getBytes(int num) {if ((num & MASK1) 0) {return 1;}int n 0;in…

windows电脑自动倒计时关机

今天聊一聊其他的。我时不时的有一个需求&#xff0c;是关于在windows电脑上定时关机。 不知道怎么地&#xff0c;我好几次都忘了这个自动定时关机的终端命令&#xff0c;于是每一次都要去网上查。 1.鼠标右击【开始菜单】选择【运行】或在键盘上按【 WinR】快捷键打开运行窗口…

【变化检测】基于STANet建筑物(LEVIR-CD)变化检测实战及ONNX推理

主要内容如下&#xff1a; 1、LEVIR-CD数据集介绍及下载 2、运行环境安装 3、STANet模型训练与预测 4、Onnx运行及可视化 运行环境&#xff1a;Python3.8&#xff0c;torch1.12.0cu113 likyoo变化检测源码&#xff1a;https://github.com/likyoo/open-cd 使用情况&#xff1a…

力扣周赛:第414场周赛

&#x1f468;‍&#x1f393;作者简介&#xff1a;爱好技术和算法的研究生 &#x1f30c;上期文章&#xff1a;[首期文章] &#x1f4da;订阅专栏&#xff1a;力扣周赛 希望文章对你们有所帮助 本科打ACM所以用的都是C&#xff0c;未来走的是Java&#xff0c;所以现在敲算法还…

探索未来住宿新体验:酒店智能开关引领的智慧生活

酒店智能开关作为智慧酒店的重要组成部分&#xff0c;正悄然改变着我们的旅行住宿方式&#xff0c;让每一次入住都成为一场科技与舒适的完美邂逅。 智能开关&#xff1a;重新定义酒店房间的每一个角落 传统酒店中&#xff0c;房间的灯光、空调、窗帘等设备的控制往往依赖于手动…

LCD字符图片显示——FPGA学习笔记11

一、字模显示原理 字模数据&#xff1a;将这个0/1矩阵按照屏幕扫描的顺序以字节的形式体现。 取模软件设计&#xff1a; 点阵数要按照实际情况填写 二、实验任务 本节的实验任务是通过开发板上的RGB TFT-LCD接口&#xff0c;在RGB LCD液晶屏的左上角位置从上到下依次显示图片以…

【数据结构】希尔排序(缩小增量排序)

目录 一、基本思想 1.1 引入希尔排序的原因 1.2 基本思想 二、思路分析 三、gap分组问题 四、代码实现 4.1 代码一&#xff08;升序&#xff09; 4.2 代码二&#xff08;升序&#xff09; 五、易错提醒 六、时间复杂度分析 七、排序小tips 一、基本思想 1.1 引入希尔…

Vue3:<Teleport>传送门组件的使用和注意事项

你好&#xff0c;我是沐爸&#xff0c;欢迎点赞、收藏、评论和关注。 Vue3 引入了一个新的内置组件 <Teleport>&#xff0c;它允许你将子组件树渲染到 DOM 中的另一个位置&#xff0c;而不是在父组件的模板中直接渲染。这对于需要跳出当前组件的 DOM 层级结构进行渲染的…

15.1 JDBC数据库编程1

目录 15 引言 15.1.1 数据库语言SQL 15.2 JDBC体系结构 15.2.1 JDBC访问数据库 15.2.2 JDBC API介绍 15 引言 数据库系统&#xff08;database system,DBS&#xff09;由一个互相关联的数据集合和一组用以访问这些数据的程序组成。这个数据集合通常称为数据库。 …

音频-语言大模型原理

重磅推荐专栏: 《大模型AIGC》 《课程大纲》 《知识星球》 本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经…

YOLOv8改进实战 | 注意力篇 | CloFormer: 注意力机制与卷积的完美融合CloAtention,即插即用

YOLOv8专栏导航:点击此处跳转 前言 YOLOv8 是由 YOLOv5 的发布者 Ultralytics 发布的最新版本的 YOLO。它可用于对象检测、分割、分类任务以及大型数据集的学习,并且可以在包括 CPU 和 GPU 在内的各种硬件上执行。 YOLOv8 是一种尖端的、最先进的 (SOTA) 模型,它建立在以前…

(C++) 6大作用域

文章目录 &#x1f365;前言&#x1f365;C 6大作用域&#x1f41f;块&#x1f41f;名字空间&#x1f41f;类&#x1f41f;函数参数&#x1f41f;枚举&#x1f41f;模板参数 ⭐END&#x1f31f;交流方式 &#x1f365;前言 在 C core guidelines 中有一个准则&#xff1a; ES.…

深入探索Unity协程:揭开CSharp迭代器背后的神秘面纱

协程是一种特殊类型的迭代器方法&#xff0c;允许你在多个帧之间分段执行代码。可以用来处理时间延迟、异步操作和顺序执行的任务&#xff0c;而不阻塞主线程。Unity协程的实现依赖于C#语言提供的迭代器相关的语言特性&#xff0c;所以想要弄清楚Unity协程的底层原理&#xff0…

web群集--nginx配置文件location匹配符的优先级顺序详解及验证

文章目录 前言优先级顺序优先级顺序(详解)1. 精确匹配&#xff08;Exact Match&#xff09;2. 正则表达式匹配&#xff08;Regex Match&#xff09;3. 前缀匹配&#xff08;Prefix Match&#xff09; 匹配规则的综合应用验证优先级 前言 location的作用 在 NGINX 中&#xff0…

Idea Mac代码调试常用快捷键~

Mac截图 commandShift4 idea英文大写转小写 commandShiftU 功能&#xff1a;查看类的实现和继承父类的方法 快捷键 fncommandF12 鼠标点击打开 功能&#xff1a;查看当前方法的上游方法 选中方法&#xff0c;controloptionH 功能&#xff1a;CommandB是查看本类的方法 功能&…

Matlab simulink建模与仿真 第十一章(端口及子系统库)【下】

参考视频&#xff1a;simulink1.1simulink简介_哔哩哔哩_bilibili 八、触发使能子系统 1、Enabled and Triggered Subsystem触发使能子系统概述 触发使能子系统其实是触发子系统和使能子系统二者的结合&#xff0c;当触发端口传来触发信号时&#xff0c;使能端口的输入需要大…

TitleBar:打造高效Android标题栏的新选择

在Android应用开发中&#xff0c;标题栏是用户界面的重要组成部分。一个好的标题栏不仅能够提升应用的专业感&#xff0c;还能增强用户体验。然而&#xff0c;传统的标题栏实现方式往往存在代码冗余、样式不统一、性能开销大等问题。今天&#xff0c;我们将介绍一个名为TitleBa…

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐&#xff1f; 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识&#xff0c;并举出了两个例子&#xff0c;我们再举出两个例子继续说明&…