单向链表结构

news2024/10/6 21:57:40

 链表结构简介

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

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

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

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

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

单向链表

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

        了解了单向链表的结构以及存储模式,就可以创建单向链表的具体对象了。目前,在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.coloradmin.cn/o/1887018.html

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

相关文章

react_后台管理_项目

目录 1.运行项目 2. 项目结构 ①项目顶部导航栏 ②项目左侧导航栏 ③主页面-路由切换区 本项目使用的是 reacttsscss 技术栈。 1.运行项目 在当前页面顶部下载本项目&#xff0c;解压后使用编辑器打开&#xff0c;然后再终端输入命令&#xff1a; npm i 下载依赖后&am…

使用Python绘制动态螺旋线:旋转动画效果

文章目录 引言准备工作前置条件 代码实现与解析导入必要的库初始化Pygame绘制螺旋线函数主循环 完整代码 引言 螺旋线是一个具有美学和数学魅力的图形。通过编程&#xff0c;我们可以轻松创建动态旋转的螺旋线动画。在这篇博客中&#xff0c;我们将使用Python和Pygame库来实现…

【python脚本】批量检测sql延时注入

文章目录 前言批量检测sql延时注入工作原理脚本演示 前言 SQL延时注入是一种在Web应用程序中利用SQL注入漏洞的技术&#xff0c;当传统的基于错误信息或数据回显的注入方法不可行时&#xff0c;例如当Web应用进行了安全配置&#xff0c;不显示任何错误信息或敏感数据时&#x…

解决卡顿发热,超帧技术焕发中重载游戏动力

近几年&#xff0c;中国手游市场规模不断扩大&#xff0c;开发者通过在画面、玩法等方面的持续创新和打磨&#xff0c;推出更加精品化的产品。然而愈发精美的画质和复杂的玩法&#xff0c;也给硬件带来超高的负载&#xff0c;导致玩家在游戏过程中&#xff0c;频繁出现掉帧卡顿…

动态规划算法,完全零基础小白教程!不是计算机的都能学会!万字吐血详解。

目录 一、动态规划算法概念 题一 1、算法解析 1&#xff09;确定状态&#xff1a; ​2&#xff09;状态转移方程&#xff1a; ​3&#xff09;初始化&#xff1a; 4&#xff09;填表顺序&#xff1a; 5&#xff09;返回值&#xff1a; 2、代码 题二 1、算法解析 1、确…

你喜欢波段交易吗?

波段交易的核心在于精准捕捉市场中的长期趋势波动&#xff0c;以实现更为稳健的收益。与剥头皮和日内交易不同&#xff0c;波段交易者更倾向于持有交易头寸数日乃至数周&#xff0c;以更宽广的视角把握市场动态。 这种交易方式的优势在于&#xff0c;它降低了对即时市场反应的…

C - Popcorn(abs358)

题意&#xff1a;有n个摊子&#xff0c;m个爆米花&#xff0c;想花费最少去的店铺买到所有的口味的爆米花&#xff0c;找到每一列都为‘o’的最少行数。 分析&#xff1a;用dfs寻找最少路径 #include<bits/stdc.h> using namespace std; typedef long long ll; char x;…

【面试系列】AI研究员高频面试题及详细解答

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;欢迎订阅相关专栏&#xff1a; ⭐️ 全网最全IT互联网公司面试宝典&#xff1a;收集整理全网各大IT互联网公司技术、项目、HR面试真题. ⭐️ AIGC时代的创新与未来&#xff1a;详细讲解AIGC的概念、核心技术、…

内网渗透第四天!!!冲冲冲!!怎么绕过uac以及你会all劫持???不安全的服务路径以及服务权限,你会吗???

在第三天我们简单的说了一下绕过uac&#xff0c;但是我们使用的msf模块ask要对方管理员跟我们一起来进行操作&#xff0c;才可以进行提权的操作&#xff0c;这点就限制住了我们。我们今天来讲一下不用钓鱼的绕过的操作。 绕过uac&#xff1a; 使用uacme项目和msf联动来进行绕过…

和小红书一起参会! 了解大模型与大数据融合的技术趋势

在过去的两年中&#xff0c;“大模型”无疑成为互联网行业的焦点话题&#xff0c;曾经炙手可热的大数据架构似乎淡出公众视野。然而&#xff0c;大数据领域并未停滞不前&#xff0c;反而快速演进&#xff0c;传统依赖众多开源组件的大数据平台正逐步过渡到以融合与简化为核心特…

Xorbits inference操作实战

1.操作环境 序号软件版本备注1Windows1.版本&#xff1a;Windows 10 专业版2.版本号&#xff1a;21H23.操作系统内部版本&#xff1a;19044.18892Docker Desktop4.24.2 (124339)3WSLUbuntu 22.04 LTS4Python3.105CUDA12.16Dify0.6.6 Xorbits inference 是一个强大且通用的分布…

那些好用的 Vue3 的工具搭子!!【送源码】

2020 年 9 月 18 日 Vue3 的正式发布已经过去了大约 3 年 9 个月左右&#xff01;&#xff01;&#xff01; 随着 Vue3 版本的逐渐成熟&#xff0c;我们的前端世界也迎来了一系列令人振奋的更新和工具。Vue 生态圈的持续扩大&#xff0c;无疑为前端开发人员带来了前所未有的便…

C盘清理和管理

本篇是C盘一些常用的管理方法&#xff0c;以及定期清理C盘的方法&#xff0c;大部分情况下都能避免C盘爆红。 C盘清理和管理 C盘存储管理查看存储情况清理存储存储感知清理临时文件清理不需要的 迁移存储 磁盘清理桌面存储管理应用存储管理浏览器微信 工具清理 C盘存储管理 查…

鸿蒙开发设备管理:【@ohos.multimodalInput.touchEvent (触摸输入事件)】

触摸输入事件 设备上报的触屏事件。 说明&#xff1a; 本模块首批接口从API version 9开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import {Action,ToolType,SourceType,Touch,TouchEvent} from ohos.multimodalInput.touchEvent;…

【Redis7】零基础篇

1 课程概述 2 Redis入门概述 2.1 是什么 Redis是基于内存的KV键值对内存数据库 Redis&#xff1a;Remote Dictionary Server(远程字典服务)是完全开源的&#xff0c;使用ANSIC语言编写遵守BSD协议&#xff0c;是一个高性能的Key-Value数据库提供了丰富的数据结构&#xff0c…

STM32F1+HAL库+FreeTOTS学习3——任务创建(动态和静态两种)

STM32F1HAL库FreeTOTS学习3——任务创建&#xff08;动态和静态两种&#xff09; 任务创建API函数任务创建流程代码实现1. 动态任务创建和删除2. 静态任务创建和删除 上期我们学习了STM32移植FreeRTOS搭建基准工程&#xff0c;现在我们来学习任务创建 任务创建API函数 前面我们…

RK3568平台(USB篇)USB HID设备

一.USB HID设备简介 USB HID设备主要用于和计算机进行交互通信&#xff0c;典型的USB HID类设备包括USB键盘、USB鼠标、USB游戏手柄等等&#xff0c;这些都是日常生活中常见的设备。以USB接口的鼠标为例&#xff0c;打开计算机的“设备管理器”&#xff0c;可以在“鼠标和其他…

mac上使用finder时候,显示隐藏的文件或者文件夹

默认在finder中是不显示隐藏的文件和文件夹的&#xff0c;但是想创建.gitignore文件&#xff0c;并向里面写入内容&#xff0c;即便是打开xcode也是不显示这几个隐藏文件的&#xff0c;那有什么办法呢&#xff1f; 使用快捷键&#xff1a; 使用finder打开包含隐藏文件的文件夹…

Web缓存代理和CDN 内容分发网络

目录 1.WEB缓存代理 1.1 WEB缓存代理作用 1.2 常见WEB缓存代理 1.3 Nginx 配置 缓存代理 2. CDN内容分发网络 1.WEB缓存代理 1.1 WEB缓存代理作用 存储一些之前给访问过的&#xff0c;且可能要被再次访问的静态网页资源对象&#xff0c;使客户端可以直接从缓存代理服务器…

NSSCTF-Web题目19(数据库注入、文件上传、php非法传参)

目录 [LitCTF 2023]这是什么&#xff1f;SQL &#xff01;注一下 &#xff01; 1、题目 2、知识点 3、思路 [SWPUCTF 2023 秋季新生赛]Pingpingping 4、题目 5、知识点 6、思路 [LitCTF 2023]这是什么&#xff1f;SQL &#xff01;注一下 &#xff01; 1、题目 2、知识…