单向链表结构

 链表结构简介

        链表结构是一种用比较特殊的数据结构类型,它也是线性数据结构中的一种,但是与栈结构等线性数据结构不同,它的内部结构并不是一个简单的存储空间,而是一个带有指向性质的单元。要理解链表结构要弄清楚两个问题,第一个就是链表结构的存储方式是怎样的,第二个就是链表结构的模型该如何描述。

        先说第一个问题,链表结构的存储方式。链表结构对数据存储的时候并不是简单的将这个数据存放到一个指定的空间中,而是将数据存储到一个属于链表结构的节点当中。这个节点就是链表的一个组成部分,在这个节点中,不仅存储了这个元素还存储了下一个元素或者上一个元素的地址,通过这些储存的地址就可以找到当前元素的上一个元素或者下一个元素。

        了解完链表结构的存储方式,那么链表结构的模型就比较好建立了。最简单的模型就是链子,不论是铁链还是狗链都能帮助我们理解链表结构。因为链表结构就相当于是用一根无形的绳子将内存中的数据串了起来,就像是一根链子一样,所以称之为链表结构。但是要注意的是,虽然我们可以将链表结构想象成一根铁链,但是在实际内存中,链表结构中相邻的两个元素在内存中并不是相邻的。只是通过上一个节点中存储的地址能找到下一个节点中存储的元素而已。

        综上,链表结构是由许多节点组成的,每一个节点中都应包含数据部分和地址部分两个部分。其中数据部分用来存储该节点的实际数据,而地址部分用来存储上一个或者下一个节点的地址。

        链表结构可以分为单向链表、双向链表和双向循环链表三种,其中单向链表和双向链表是比较常见的链表结构,双向循环链表不太常见,简单了解即可。

单向链表

        单向链表是链表结构中最为简单的一类,它的特点是链表中节点的链接方向是单向的,也就是说访问链表中的元素时只能从头开始一个一个的寻找,不能直接锁定指定元素的位置,也不能从尾向前访问元素。故能够判断出单向链表的结构中节点存储的内容应该为节点存储的元素以及下一个节点的地址。

        了解了单向链表的结构以及存储模式,就可以创建单向链表的具体对象了。目前,在java中是找不到一个合适的结构来描述链表结构的,因此需要自行创建链表实现类。考虑到单向链表和双向链表的除了在存储结构上纯在些许差异之外其余并无太多不同,并且它们都应该具有对内部储存元素进行操作的方法,因此为了提高代码的复用性,可以将链表结构中的方法抽象出来创建一个接口,这样在实现单向链表或者双向链表类的时候只需要实现接口中的方法就能够实现对应的操作方法了。大大降低了代码的复杂程度。根据链表结构中应该具备的操作元素的方法,创建了以下接口:

package com.data.demo;

/**
 * 基于链表结构存取元素的API定义
 * @param <E>
 */

public interface MyList <E>{
    void add (E element);
    E get (int index);
    int size();
    E remove(int index);
}

此接口中定义了四个抽象方法,对应了添加元素、获取元素、获取元素个数以及删除元素四个功能。

        创建完链表结构的接口类,就可以创建一个单向链表类来实现这个接口类了。由于向单向链表中添加元素之前并不能确定添加的元素类型,因此定义的单向链表类应该是一个泛型类,它当中的需要参数的方法应该为泛型方法,这也说名了上面定义的接口为泛型接口的原因。

        在单向链表结构中应该具备这样一个部分,这个部分是用来描述节点内容的。而且很容易发现,节点功能只会在单向链表类中被使用,在其它类中不会被使用。这个描述符合内部类的特征,因此可以在单向链表类中定义一个内部类来存储节点信息。将这个内部类定义为内部泛型类,它的名称为Node,在Node类型中item变量用来存储当前节点的元素,定义一个变量next用来指向下一个节点对象的地址,并且还要提供一个全参构造方法。具体实现如演示代码所示。

        以上定义了单向链表中的用于描述节点的结构,接下来就需要对单向链表中的具体方法做出实现了。

实现添加元素的方法

        结合上文所说,单向链表访问元素必须从头节点开始,不能从尾节点或者链表当中的任意节点开始,因此在实现单向链表中的具体方法之前需要确定单向链表头节点的位置。因此最简单的方法就是定义一个私有的Node类型的成员变量来存储单向链表中头节点的位置。这里将这个变量定义为 head。此外,操作元素的方法中还涉及了元素个数的变动,也应该添加一个私有变量来记录单向链表中的元素个数变动,此处为size。

        随后实现单行链表中添加元素的方法add。这个方法的作用是将指定元素添加到单向链表结构中,根据对单向链表结构的描述,这个方法可以分为三步实现。第一步,创建节点,将传入方法的元素添加到创建的节点当中;第二步,找到尾节点;第三步,实现节点挂接。

        这三步中比较难理解的是第二步和第三步。第二步找到尾节点的原因是添加元素是将元素添加到单向链表的尾节点之后,因此需要先找到尾节点。而在单向链表中,访问元素是从头节点开始的,所以要找到尾节点就得从头节点开始不断向下访问节点,直到访问的节点不再指向下一个元素为止,就找到了尾节点。也就是说,当next的地址为null时,尾节点就找到了。根据描述,第二步应该用一个死循环来实现,具体实现如演示代码中的getTail方法所示。这里要注意,循环结构中的this.node = head;的意思是将head的值赋给一个新的变量node,这样做的目的是因为真正头节点的位置是不能改变的。其次循环实现是通过移动node指向的地址来实现的。如果不满足要求,就像node.next也就是下一个节点的地址传给node,直到node的地址为空。

        第三步实现节点的挂接就稍微好理解点了,因为是在尾节点添加元素,所以新添加的元素成为了尾节点,原来的尾节点不再是尾节点,所以原来节点中指向下一个节点的地址不应为空,而应是新添加的节点。这就是节点挂接的简单描述,具体实现为

               if(tail == null){
                    this.head = node;
                  }else{
                    tail.next = node;

                }

其中tail == null代表没有尾节点,即整个单向链表均为空,这时添加的节点就是头节点,即只需要将添加的节点赋给头节点即可。在拥有尾节点的情况下用代码tail.next = node;即可将尾节点指向的下一个节点指向添加的节点,节点挂接的操作就完成了。当然,添加有元素会使得单向链表中的元素个数发生变动,因此还需要对元素变动进行记录。至此,添加元素的方法就完成了。

实现获取元素的方法

        获取元素的方法是getNode,这个方法需要传入一个int类型的参数作为元素索引,通过索引来获取元素。这里可能会有人提出一个疑问,链表结构存在索引吗?为什么能够通过索引来过去元素?我们先暂时将这个问题搁浅,实现获取元素的方法之后自然就清楚了。

        由于getNode方法是通过索引来获取指定位置的元素的,所以在获取元素之前要先检查给定的索引是否合法。因为编写的单向链表类中的元素是从0这个索引开始的,所以索引的范围为[0,size),即最小为0,最大为元素个数减1。通过单向链表实现的接口能发现,要实现的四个方法中不止getNaoe方法用到了元素索引,所以对索引合法性的判断最好编写成一个独立的方法。在演示代码中体现为checkIndex方法。在这个方法中,如果索引合法则会执行获取索引所指向的元素,如果不合法则会抛出索引异常的的提示。

        当给定的索引合法之后就可以获取索引指向的元素了,那么其实可以发现,单向链表并没有索引,那么这个索引哪儿来的呢?其实这里要联系到添加元素到单向链表的操作,可以发现,添加元素时提供的方法时从尾节点添加的,也就是说这个单向链表中的元素有一个默认顺序,这个顺序就是添加元素的顺序。那么,如果把元素添加的元素作为单向链表中元素的“虚假的索引”就可以通过这个“虚假的索引”来获取单向链表中的元素了。这个想法在getNode这个方法得到了充分的体现。

                                private Node getNode(int index){
                                        Node node = this.head;
                                        for(int i = 0;i < index;i++){
                                            node = node.next;
                                        }
                                        return node;
                                      }

在方法getNode方法的代码中,由于单向链表只能从头节点开始访问元素的限制,所以先将头节点赋给一个新的变量node,随后通过循环结构来访问单向链表中的元素。根据上面描述的原理,获取索引为index的元素等价于获取第index个添加到单向链表中的元素,所以只要将索引index作为循环是否终止的判断条件即可获取到指定索引的元素。如此,先前搁浅的问题便得到了解决,同时获取指定位置的元素的方法也得到了实现。

删除指定索引位置的元素

        删除元素的方法为remove,这个方法仍然需要传入一个索引,此外它还会返回删除的元素的值。由于这个方法有一个索引,所以需要调用方法checkIndex来检验传入索引的合法性。此外也要获取到当前索引指向的节点,以方便后续操作以及返回当前节点中的元素。

        接下来阐述单向链表中删除元素的索引。通过上面的描述知道,单向链表中的元素是通过节点中存储的地址值来链接在一起的,那么如果能够将某个元素上的这个中链接关系删除,就能将这个元素从这个单向链表中移除。所以这里涉及到三个操作,移除上一个节点指向当前节点的地址值,将上一个节点中存储的地址值指向当前节点的下一个节点以及将当前节点存储的地址值置空。以上的这三个操作就是在单项链表中删除指定元素的具体操作,但是在具体操作时又可以分为删除的元素是否为头节点的情况,如果删除的元素时头节点,则只需要将下一个元素赋给头节点,随后将要删除的节点存储的地址置空即可。而当要删除的元素不是头节点时,上面的三个操作就缺一不可了。以上的描述体现在代码中具体为:

                        if(this.head == node){
                            this.head = node.next;
                        }else{
                            Node<E> temp = this.head;
                            for (int i = 0; i < index-1; i++) {
                                temp = temp.next;
                              }
                            temp.next = node.next;
                        }
                        node.next = null;

在这个代码中要注意的是获取前一个节点只需要索引减一即可。然后就是注意中间变量temp的合理运用。当然由于删除了元素,所以记录元素个数的变量也要发生相应的变化。

获取元素个数的方法

        获取元素个数的方法极为简单,因为在添加元素和删除元素的操作中都实现了元素个数的变化,并且对元素个数的变化进行了相应记录。所以在这个方法中只需要将记录元素个数的变量size返回即可。

演示代码

        实现了上述的所有方法,接下来要对其进行验证,所以开辟一个main方法,在main方法中实例化一个单向链表类并测试以上的四个方法。具体实现如下所示:

package com.data.demo;

/**
 *基于单项链表实现元素存取的容器类
 * @param <E>
 */
public class MySinglyLinkedList<E> implements MyList<E> {
    //定义单向链表中的节点对象
    class Node<E>{
         private E item;//存储元素
        private Node next;//存储下一个节点对象的地址
        Node(E item,Node next){
            this.item = item;
            this.next = next;
        }
    }
    private Node head;//存放链表的头节点
    private int size;//记录元素个数
    //添加元素
    @Override
    public void add(E element) {
        //创建节点
        Node<E> node = new Node<>(element,null);
        //找到尾节点
        Node tail = getTail();
        //节点挂接
        if(tail == null){
            this.head = node;
        }else{
            tail.next = node;
        }
        //记录元素个数
        this.size++;

    }

    //寻找尾节点的方法
    private Node getTail(){
        //判断头节点是否存在
        if(this.head == null){
            return  null;
        }
        Node node  = this.head;
        while(true){
           if(node.next == null)
               break;
           node = node.next;
        }
        return node;
    }
    //获取元素
    @Override
    public E get(int index) {
        //校验index的合法性
        this.checkIndex(index);
        //根据位置获取节点元素
        Node<E> node = this.getNode(index);
        //返回节点中的元素
        return node.item;
    }

    /**
     * 校验index合法性的方法
     * @return
     */
private void checkIndex(int index){
    if(!(index < this.size && index >= 0)){
        throw new IndexOutOfBoundsException("Index:"+index+"Size:"+this.size);
    }
}

    /**
     * 根据位置获取节点
     * @return
     */
    private Node getNode(int index){
        Node node = this.head;
        for(int i = 0;i < index;i++){
            node = node.next;
        }
        return node;
    }
    //获取元素个数
    @Override
    public int size() {
        return this.size;
    }

    //删除元素
    @Override
    public E remove(int index) {
        //校验index的合法性
        this.checkIndex(index);
        //根据位置找到节点对象
        Node<E> node = getNode(index);
        //获取该节点对象中的元素
        E item = node.item;
        //将该节点对象从单向链表中删除
            //判断该节点是否为头节点
        if(this.head == node){
            this.head = node.next;
        }else{
            Node<E> temp = this.head;
            for (int i = 0; i < index-1; i++) {
                temp = temp.next;
            }
            temp.next = node.next;
        }
        node.next = null;
        //记录元素个数
        this.size--;
        //返回该元素
        return item;
    }

    public static void main(String[] args) {
        MySinglyLinkedList<String> mySinglyLinkedList = new MySinglyLinkedList<>();
        mySinglyLinkedList.add("a");
        mySinglyLinkedList.add("b");
        mySinglyLinkedList.add("c");
        mySinglyLinkedList.add("d");
        mySinglyLinkedList.add("e");
        System.out.println(mySinglyLinkedList.size());
        System.out.println(mySinglyLinkedList.remove(2));
        for (int i = 0; i < mySinglyLinkedList.size; i++) {
            System.out.println(mySinglyLinkedList.get(i));
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

仰邦BX.K协议对接

背景 使用BX 6K控制卡控制诱导屏显示剩余车位数&#xff0c;由于控制卡和服务端不在一个局域网内&#xff0c;所以不能使用官网提供的案例&#xff0c;官网提供的案例为控制卡为TCP Server&#xff0c;服务端为TCP Client&#xff0c;因此需要开发此程序&#xff0c;服务端左右…

Python爬虫实战案例——王者荣耀皮肤抓取

大家好&#xff0c;我是你们的老朋友——南枫&#xff0c;今天我们一起来学习一下该如何抓取大家经常玩的游戏——王者荣耀里面的所有英雄的皮肤。 老规矩&#xff0c;直接上代码&#xff1a; 导入我们需要使用到的&#xff0c;也是唯一用到的库&#xff1a; 我们要抓取皮肤其…

【Linux】TCP协议【下二】{流量控制/滑动窗口/延迟应答/捎带应答/拥塞控制}

文章目录 1.流量控制--利用“窗口大小”字段协商数据量大小1. 1第一次的时候&#xff0c;怎么保证发送数据量是合理的1.2第三次握手ack的时候&#xff0c;可以携带数据&#xff01;1.3流量控制&#xff0c;属于可靠性还是属于效率&#xff1f; 2.滑动窗口--利用滑动窗口解决批量…

UE5 动画蓝图

文章目录 一、State Machines二、Blend Spaces三、Aim Offset四、Montage 初步介绍 Unreal Engine 5 Tutorial - Animation Blueprint Part 1: State Machines (youtube.com) Unreal Engine 5 Tutorial - Animation Blueprint Part 2: Blend Spaces (youtube.com) Unreal Engi…

读人工智能全传01图灵的电子大脑

1. 人工智能 1.1. 人类对人工智能的梦想&#xff0c;可以追溯到很久很久以前 1.1.1. 从古希腊开始&#xff0c;铁匠之神赫菲斯托斯(Hephaestus)拥有赋予金属物品生命的能力 1.1.2. 从16世纪的布拉格开始&#xff0c;传说中伟大的拉比在那里用黏土制作了一个傀儡魔像&#xf…

使用patch-package自动修改node_modules中的内容/打补丁

背景 在使用VuePress搭建个人博客的过程中&#xff0c;我需要使用到一个用来复制代码块的插件uepress-plugin-nuggets-style-copy。 问题&#xff1a;插件可以正常安装&#xff0c;但是启动会报错。通过查看错误信息&#xff0c;定位是插件中的copy.vue文件出现错误&#xff0c…

【实战场景】记一次UAT jvm故障排查经历

【实战场景】记一次UAT jvm故障排查经历 开篇词&#xff1a;干货篇&#xff1a;1.查看系统资源使用情况2.将十进制进程号转成十六进制3.使用jstack工具监视进程的垃圾回收情况4.输出指定线程的堆内存信息5.观察日志6.本地环境复现 总结篇&#xff1a;我是杰叔叔&#xff0c;一名…

仿论坛项目--初识Spring Boot

1. 技术准备 技术架构 • Spring Boot • Spring、Spring MVC、MyBatis • Redis、Kafka、Elasticsearch • Spring Security、Spring Actuator 开发环境 • 构建工具&#xff1a;Apache Maven • 集成开发工具&#xff1a;IntelliJ IDEA • 数据库&#xff1a;MySQL、Redi…

Docker拉取失败,利用 Git将 Docker镜像重新打 Tag 推送到阿里云等其他公有云镜像仓库里

目录 一、开通阿里云容器镜像服务 二、Git配置 三、去DockerHub找镜像 四、编写images.txt文件 ​五、演示 六、其他注意事项 最近一段时间 Docker 镜像一直是 Pull 不下来的状态&#xff0c;想直连 DockerHub 是几乎不可能的。更糟糕的是&#xff0c;很多原本可靠的国内…

Vue+ElementUi实现录音播放上传及处理getUserMedia报错问题

1.Vue安装插件 npm install --registryhttps://registry.npmmirror.com 2.Vue页面使用 <template><div class"app-container"><!-- header --><el-header class"procedureHeader" style"height: 20px;"><el-divid…

密码学及其应用 —— 密码学的经典问题

1. 古典密码学问题 1.1 问题1&#xff1a;破解凯撒密码 1.1.1 问题 凯撒密码是最简单的单字母替换加密方案。这是一种通过将字母表中的字母固定向右移动几位来实现的加密方法。解密下面的文本&#xff0c;该文本通过对一个去除了空格的法语文本应用凯撒密码获得&#xff1a; …

layui-按钮

1.用法 使用 用button标签 type"button" class"layui-button" 效果&#xff1a; 2.主题设置 前面都要加上layui-bin 3.尺寸设置 可以叠加使用&#xff01; 4.圆角设置 加一个layui-bin-radius 5.按钮图标设置 里面加一个i标签 加class"layui-…

借教室(题解)

P1083 [NOIP2012 提高组] 借教室 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 思路&#xff1a;二分前缀和 我们将和质检员那题差不多&#xff0c;只需要将候选人二分即可 #include<bits/stdc.h> using namespace std; #define int long long int n,m; int r[100000…

【操作与配置】VSCode配置Python

Python环境配置 可以参见&#xff1a;【操作与配置】Python&#xff1a;CondaPycharm_pycharmconda-CSDN博客 官网下载Python&#xff1a;http://www.python.org/download/官网下载Conda&#xff1a;Miniconda — Anaconda documentation VSCode插件安装 插件安装后需重启V…

disql使用

进入bin目录&#xff1a;cd /opt/dmdbms/bin 启动disql&#xff1a;./disql&#xff0c;然后输入用户名、密码 sh文件直接使用disql&#xff1a; 临时添加路径到PATH环境变量&#xff1a;在当前会话中临时使用disql命令而无需每次都写完整路径&#xff0c;可以在执行脚本之前…

Eclipse + GDB + J-Link 的单片机程序调试实践

Eclipse GDB J-Link 的调试实践 本文介绍如何创建Eclipse的调试配置&#xff0c;如何控制调试过程&#xff0c;如何查看修改各种变量。 对 Eclipse 的要求 所用 Eclipse 应当安装了 Eclipse Embedded CDT 插件。从 https://www.eclipse.org/downloads/packages/ 下载 Ecli…

20240628模拟赛总结

cf好了 让我们开始 T1 Two Regular Polygons 判断能不能构造出题中要求的正多边形 关键是n%m0 Two Regular Polygons #include<bits/stdc.h> using namespace std; int t; int n,m; int main() {cin>>t;for(int i1;i<t;i){cin>>n>>m;if(n%m0)co…

MySQL 代理层:ProxySQL

文章目录 说明安装部署1.1 yum 安装1.2 启停管理1.3 查询版本1.4 Admin 管理接口 入门体验功能介绍3.1 多层次配置系统 读写分离将实例接入到代理服务定义主机组之间的复制关系配置路由规则事务读的配置延迟阈值和请求转发 ProxySQL 核心表mysql_usersmysql_serversmysql_repli…

【C++】相机标定源码笔记- 标定工具库测试

标定工具库测试 一、计算相机内参&#xff1a;对两个相机进行内参标定&#xff0c;并将标定结果保存到指定的文件中 采集图像&#xff1a;相机1-16张 相机2-17张 定义保存相机1/2内参的文件(.yml)路径。 定义相机1/2采集的图片文件夹路径。定义相机1/2存储文件名的向量获取文件…

作为图形渲染API,OpenGL和Direct3D的全方位对比。

当你在网页看到很多美轮美奂的图形效果&#xff0c;3D交互效果&#xff0c;你知道是如何实现的吗&#xff1f;当然是借助图形渲染API了&#xff0c;说起这个不就不得说两大阵营&#xff0c;OpenGL和Direct3D&#xff0c;贝格前端工场在本文对二者做个详细对比。 一、什么是图形…