【数据结构和算法】(基础篇二)——链表

链表

数组最麻烦的地方就是其在建立之后大小固定,对于增删数据很不方便。链表的出现解决了这个问题,链表的元素不是在内存中连续存储的,而是通过指针链接在一起的节点集合,这样的设计让链表有了动态的大小。链表是树和图结构的基础。

链表由一个个的节点构成,这些节点设计的不同会产生不同的特性和使用场景,链表主要有三种类型:单链表、双向链表和环形链表。

单链表

因为链表的内存不是连续的,为了能够让上一个节点找到下一个节点的位置,每个节点除了存储自身数据之外,还需要存储指向下一个节点的地址,这两部分称为data数据域和next指针域。

单链表的设计中,还可以根据需要选择有没有头尾节点,头尾节点的存在可以简化插入和删除操作,比如要删除第一个节点的话,有头结点的情况让其next域指向第二个节点即可;通常头节点经常使用作为指示链表的开始,尾节点让其next域为null即可。头节点的存在也可以用于判断一个链表是否为空(看其是否指向null)。

在这里插入图片描述

链表的逻辑结构示意图:

在这里插入图片描述

【示例】创建一个单链表用来存放梁山的108好汉,完成数据的增删改查,需要有两种添加数据的方法,一种是直接在链表尾部添加数据,另一种是按照英雄的座次排名进行插入数据

单链表结尾增加数据:让新节点的next指向null,遍历得到原本的尾节点,让尾节点的next指向新节点

单链表中间增加数据:先找到需要增加数据的位置,比如要在下标为n的位置增加一个节点,此时让新节点先指向n+1个节点,然后让第n个节点指向新节点。这个顺序不能反了,如果先让第n个节点指向新节点,则n+1后面的节点就都找不到了。

public class SingleLinkedListDemo {public static void main(String[] args) {//TestHeroNode hr1 = new HeroNode(1,"宋江", "及时雨");HeroNode hr2 = new HeroNode(2,"卢俊义", "玉麒麟");HeroNode hr3 = new HeroNode(3,"吴用", "智多星");HeroNode hr4 = new HeroNode(4,"林冲", "豹子头");SingleLinkedList sll = new SingleLinkedList();sll.addByOrder(hr1);sll.addByOrder(hr4);sll.addByOrder(hr3);sll.addByOrder(hr2);sll.list();System.out.println("修改后的数据-------------------");HeroNode nhr = new HeroNode(2,"卢俊义1", "玉麒麟1");sll.update(nhr);sll.list();System.out.println("删除后的数据-------------------");sll.del(hr1);sll.list();sll.del(hr2);sll.list();sll.del(hr3);sll.list();sll.del(hr4);sll.list();}
}class SingleLinkedList{// 头节点HeroNode head = new HeroNode(0,"","");public SingleLinkedList() {this.head = head;}// 添加数据public void add(HeroNode heroNode){// 先使用一个临时变量找到链表尾,将数据添加到链表尾HeroNode temp = head;while (true){if (temp.next == null){// 找到了链表尾break;}// 没找到链表尾就继续遍历temp = temp.next;}// 在循环结束后,得到的temp就处于链表尾temp.next = heroNode;}// 按照英雄排名的顺序插入数据public void addByOrder(HeroNode heroNode){HeroNode temp = head;boolean flag = false;while (true){if (temp.next == null){ //已经把整个链表都遍历玩了break;}if (temp.next.no > heroNode.no){  // 就是正确的位置break;} else if (temp.next.no == heroNode.no) {  // 已经存在相同数据flag = true;break;}temp = temp.next;}if (flag){  //System.out.printf("您要插入的第%d位的英雄数据已经存在,添加数据失败\n", heroNode.no);}else{heroNode.next = temp.next;temp.next = heroNode;}}// 修改链表中的信息public void update(HeroNode newheroNode){// 根据英雄的排名(no)修改信息if(head.next == null){System.out.println("链表为空,不能修改");return;}HeroNode temp = head;boolean flag = false;while (true){if (temp.next==null){// 遍历结束,没有找到对应的nobreak;} else if (temp.no == newheroNode.no) {// 找到正确的位置flag = true;break;}temp = temp.next;}if (flag){temp.name = newheroNode.name;temp.nickname = newheroNode.nickname;}else {System.out.printf("链表中没有排名为 %d 的英雄,无法修改", newheroNode.no);}}// 删除链表中的数据public void del(HeroNode target){HeroNode temp = head;boolean flag = false;while (true){if (temp.next.no == target.no){// temp的下一个数据就是需要删除的数据flag = true;break;}temp = temp.next;}if (flag){// 如果没有变量指向被删除的内存空间,它就会被垃圾回收机制回收掉temp.next = temp.next.next;}else {System.out.println("你所找的数据不在链表中,无法删除");}}// 查看链表中的数据public void list(){if (head.next == null){System.out.println("链表为空,没有数据");return;}HeroNode temp = head;while (true){if (temp.next == null){// 找到了链表尾break;}// 没找到链表尾就继续遍历System.out.println(temp);temp = temp.next;}System.out.println(temp);}
}class HeroNode{int no;String name;String nickname;HeroNode next;public HeroNode(int no, String name, String nickname) {this.no = no;this.name = name;this.nickname = nickname;}@Overridepublic String toString() {return "HeroNode{" +"no=" + no +", name='" + name + '\'' +", nickname='" + nickname + '\'' +'}';}
}

【面试算法题目】单链表的逆序打印

思路:方法一,先用上面的方法把单链表反转,再进行打印,但是会破坏原来的链表结构,不推荐使用;方法二,使用栈先入后出的特性实现来实现

    // 单链表逆序打印public static void reversePrint(HeroNode head){// 判断是否为空if (head.next == null){return;}// 创建一个栈Stack<HeroNode> stack = new Stack();// 遍历HeroNode cur = head.next;while (cur != null){stack.add(cur);cur = cur.next;}// 从栈中取数据while (stack.size()>0){System.out.println(stack.pop());}}

双向链表

单向链表中的每一个节点都只能够指向下一个节点,也就是说,只能进行单向的遍历。双向链表可以解决这个问题,双向链表(Doubly Linked List)是一种线性数据结构,其中的每个元素都是一个节点,每个节点包含三个部分:数据、指向其前一个节点的指针和指向其后一个节点的指针。与单向链表相比,双向链表中的节点具有额外的指向前一个节点的链接,这使得在链表中进行前后移动都变得可能。

  • 插入:可以在链表的头部、尾部或任意位置插入一个新的节点。插入时需要更新新节点的前后指针以及相邻节点的前后指针。
  • 删除:可以从链表中删除一个指定节点。删除时需要更新被删除节点的前一个节点和后一个节点的指针。
  • 遍历:可以从前向后或从后向前遍历链表,访问每一个节点。
  • 查找:在链表中查找特定值的节点,可以通过遍历来实现。
  • 反转:双向链表可以很容易地反转,只需要交换所有节点的前后指针即可。

【示例】创建一个双向链表完成上面的需求

public class DoubleLinkedListDemo {public static void main(String[] args) {HeroNode2 hr1 = new HeroNode2(1,"宋江", "及时雨");HeroNode2 hr2 = new HeroNode2(2,"卢俊义", "玉麒麟");HeroNode2 hr3 = new HeroNode2(3,"吴用", "智多星");HeroNode2 hr4 = new HeroNode2(4,"林冲", "豹子头");DoubleLinkedList doubleLinkedList = new DoubleLinkedList();doubleLinkedList.addByOrder(hr1);doubleLinkedList.addByOrder(hr3);doubleLinkedList.addByOrder(hr2);doubleLinkedList.addByOrder(hr4);doubleLinkedList.list();/*System.out.println("删除最后一个数据");doubleLinkedList.del(hr4);doubleLinkedList.list();System.out.println("修改第二个数据后:");HeroNode2 hr5 = new HeroNode2(2,"卢俊义2", "玉麒麟2");doubleLinkedList.update(hr5);doubleLinkedList.list();*/}
}class DoubleLinkedList{// 头节点HeroNode2 head = new HeroNode2(0,"","");//返回头节点public HeroNode2 getHead() {return head;}public DoubleLinkedList() {this.head = head;}// 按照英雄座次排序插入节点public void addByOrder(HeroNode2 heroNode){HeroNode2 temp = head;boolean flag = false;while (true){if (temp.next == null){ //已经把整个链表都遍历玩了break;}if (temp.next.no > heroNode.no){  // 就是正确的位置,在temp的后面插入数据break;} else if (temp.next.no == heroNode.no) {  // 已经存在相同数据flag = true;break;}temp = temp.next;}if (flag){  //System.out.printf("您要插入的第%d位的英雄数据已经存在,添加数据失败\n", heroNode.no);}else{if (temp.next != null){temp.next.pre = heroNode;heroNode.next = temp.next;}temp.next = heroNode;heroNode.pre = temp;}}// 修改节点数据public void update(HeroNode2 newheroNode){// 根据英雄的排名(no)修改信息if(head.next == null){System.out.println("链表为空,不能修改");return;}HeroNode2 temp = head;boolean flag = false;while (true){if (temp.next==null){// 遍历结束,没有找到对应的nobreak;} else if (temp.no == newheroNode.no) {// 找到正确的位置flag = true;break;}temp = temp.next;}if (flag){temp.name = newheroNode.name;temp.nickname = newheroNode.nickname;}else {System.out.printf("链表中没有排名为 %d 的英雄,无法修改", newheroNode.no);}}// 删除数据,自我删除public void del(HeroNode2 target){// 直接自我删除,从第一个有效数据节点开始HeroNode2 temp = head.next;boolean flag = false;while (temp != null){if (temp.no == target.no){// temp的下一个数据就是需要删除的数据flag = true;break;}temp = temp.next;}if (flag){// 如果没有变量指向被删除的内存空间,它就会被垃圾回收机制回收掉temp.pre.next = temp.next;// 只有不是尾节点时才执行if (temp.next !=null){temp.next.pre = temp.pre;}}else {System.out.println("你所找的数据不在链表中,无法删除");}}// 添加数据到链表尾public void add(HeroNode2 heroNode){// 先使用一个临时变量找到链表尾,将数据添加到链表尾HeroNode2 temp = head;while (true){if (temp.next == null){// 找到了链表尾break;}// 没找到链表尾就继续遍历temp = temp.next;}// 在循环结束后,得到的temp就处于链表尾temp.next = heroNode;heroNode.pre = temp;}// 遍历public void list(){// 先判断链表是否为空if (head.next == null){System.out.println("链表为空,没有数据");return;}HeroNode2 temp = head;while (true){if (temp.next == null){// 找到了链表尾break;}// 没找到链表尾就继续遍历System.out.println(temp.next);temp = temp.next;}}
}class HeroNode2{int no;String name;String nickname;HeroNode2 next;HeroNode2 pre;HeroNode2 head;public HeroNode2(int no, String name, String nickname) {this.no = no;this.name = name;this.nickname = nickname;}@Overridepublic String toString() {return "HeroNode2{" +"no=" + no +", name='" + name + '\'' +", nickname='" + nickname + '\'' +'}';}
}

环形链表

环形链表(Circular Linked List)是一种特殊的链表形式,其中最后一个节点的“next”指针不是指向None,而是指向链表的头节点,形成了一个闭环。这种结构使得从链表的任何一点出发都能通过“next”指针遍历整个链表并回到起点,从而提供了连续访问所有元素的能力。

环形链表可以是单向的,也可以是双向的。在双向环形链表中,除了每个节点都有一个指向前一个节点的“prev”指针外,最后一个节点的“next”指针会指向头节点,而头节点的“prev”指针也会指向最后一个节点。

创建环形单链表:先创建一个象征链表逻辑开始的头节点,最开始的头结点也应该指向自己,在链表为空时,head.next指向head本身,可以作为一种标志,表明链表当前没有其他节点。头结点除了在添加第一个数据节点时都不应该进行改动。

添加数据到环形单链表末尾时,需要一个临时变量作为链表的逻辑结尾,temp.next == first表示到达逻辑结尾

添加数据到环形单链表中间的时候,需要先进行遍历找到要添加的前一个位置

遍历环形单链表:使用一个临时变量实现,当temp.next == first表示遍历结束

【示例】约瑟夫环问题:假设有一群人围成一圈,从某个人开始报数,每数到第m个人,就将这个人从圈中移除,然后从下一个人继续报数,直到只剩下最后一个人为止。问题要求确定在初始状态下,哪个人会是最后存活下来的。

  1. 创建一个辅助变量helper指向first的前一个,用来帮助实现出队列(因为单链表的删除数据需要将辅助变量指向前一个)
  2. 在开始出队列之前,先把first指向指定的位置,helper也要同步移动
  3. 根据指定的数数个数,进行循环,每次的first指向的节点就是需要被删除的节点,first = first.next; helper.next = first.
package dataStructure.list;public class Josephues {public static void main(String[] args) {// 测试创建和遍历循环链表CircleLinkedList circleLinkedList = new CircleLinkedList();circleLinkedList.add(5);circleLinkedList.showBoys();circleLinkedList.countBoy(1,2,5);}
}class CircleLinkedList{// 先创建first变量Boy first = null;/*** 约瑟夫出圈问题* @param startNo   从哪里开始数* @param countNo   每次数几个数* @param sum    最初的总节点个数*/public void countBoy(int startNo, int countNo, int sum){// 数据校验if(first == null || startNo < 1 || startNo > sum || countNo > sum){System.out.println("输入参数有误");return;}// 创建辅助变量,置于队尾Boy helper = first;while (true){helper = helper.getNext();if (helper.getNext() == first){break;}}// 将first移动到指定的位置,即startNofor (int i = 0; i < startNo-1; i++) {first = first.getNext();helper = helper.getNext();}// 进行出队列,每次循环countNo次,直到链表中只剩下一个节点(helper == first)while (true){for (int i = 0; i < countNo-1; i++) {first = first.getNext();helper = helper.getNext();}//此时的first 指向的就是要出队列的节点System.out.printf("小孩%d出圈\n", first.getNo());first = first.getNext();helper.setNext(first);if (helper == first){break;}}System.out.println("队列中的最后一个孩子编号为:" + first.getNo());}// 添加数据, nums 表示需要添加的个数public void add(int nums){if (nums < 1){System.out.println("nums的值不正确");return;}Boy curboy = null;for (int i = 1; i <= nums ; i++) {// 根据i作为no创建boy对象Boy boy = new Boy(i);if (i == 1){ //第一个节点需要覆盖掉firstfirst = boy;first.setNext(first);curboy = first;}curboy.setNext(boy);boy.setNext(first);curboy = boy;}}// 遍历public void showBoys(){// 判断链表是否为空if(first == null){System.out.println("链表中没有数据了。。。");return;}// 使用一个临时变量辅助遍历Boy curboy = first;while (true){System.out.println("当前小孩的编号为:" + curboy.getNo());if (curboy.getNext() == first){break;}curboy = curboy.getNext();}}
}class Boy{private int no;private Boy next;public int getNo() {return no;}public Boy getNext() {return next;}public void setNext(Boy next) {this.next = next;}public Boy(int no) {this.no = no;}
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Windows11 WSL2 Ubuntu编译安装perf工具

在Windows 11上通过WSL2安装并编译perf工具&#xff08;Linux性能分析工具&#xff09;可以按以下步骤进行。perf工具通常与Linux内核一起发布&#xff0c;因此你需要确保你的内核版本和perf版本匹配。以下是安装和编译perf的步骤&#xff1a; 1. 更新并升级系统 首先&#x…

Unity数据持久化 之 Json序列化与反序列化

语法规则可以看这篇文章&#xff1a;Unity数据持久化 之 Json 语法速通-CSDN博客 Q:Unity是通过什么来对Json文件进行处理的&#xff1f; A:JsonUtility&#xff1a;Unity 提供了 JsonUtility 类&#xff0c;用于将对象序列化为 JSON 字符串或将 JSON 字符串反序列化为对象。…

大数据-72 Kafka 高级特性 稳定性-事务 (概念多枯燥) 定义、概览、组、协调器、流程、中止、失败

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

DC-5靶机渗透测试

DC-5靶场 文章目录 DC-5靶场信息收集漏洞发现漏洞利用 --- 日志文件包含漏洞利用 --- 文件包含过滤器链的RCEshell反弹权限提升 信息收集 使用--scriptvuln扫描发现了一个thankyou.php界面 感觉会有问题&#xff0c;前往访问网站信息 漏洞发现 来到thankyou.php界面&#xff…

haproxy详解

目录 一、haproxy简介 二、什么是负载均衡 2.1 负载均衡的类型 2.2.1 硬件 2.2.2 四层负载均衡 2.2.3 七层负载均衡 2.2.4 四层和七层的区别 三、haproxy的安装及服务信息 3.1 示例的环境部署&#xff1a; 3.2 haproxy的基本配置信息 3.2.1 global 配置参数介绍 3…

sleuth+zipkin分布式链路追踪

概述 结构图 常见的链路追踪 cat zipkin pinpoint skywalking sleuth Sleuth介绍 Trace Span Annotation 使用Sleuth 添加依赖 <!--sleuth--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starte…

运维工具的衍化对运维工作的新挑战

运维工具的衍化对运维工作产生了深远的影响&#xff0c;这些影响体现在多个方面&#xff0c;包括提升运维效率、优化资源配置、增强故障应对能力、促进团队协作与沟通&#xff0c;以及面临新的挑战如数据安全和隐私保护等。运维工具的衍化对运维工作带来了多方面的新挑战&#…

Yolo-World初步使用

Yolo v8目前已经支持Yolo-World&#xff0c;整理一下初步使用步骤。 使用步骤 1 先下载Yolo-World的pt文件&#xff0c;下载地址&#xff1a;GitHub - AILab-CVC/YOLO-World: [CVPR 2024] Real-Time Open-Vocabulary Object Detection 官网应该是点这里&#xff08;有个笑脸…

【C#】读取与写入txt文件内容

在 C# 中读取和写入文本文件内容是一个常见的任务。以下是使用几种不同方法读取和写入文本文件的示例。 一、读取txt文件内容 1.1 使用 StreamReader using System; using System.IO;class Program {static void Main(){string filePath "C:\path\to\your\file.txt&qu…

QT多语言工具实现支持生成ts文件,ts文件和xlsx文件互转

一. 工具介绍 1.如果你是Qt项目,为多语言发愁的话,看到这篇文件,恭喜你有福啦!工具截图如下:​ 2.在项目开发的过程中,尽量将所有需要翻译的文本放在一个文件中,qml翻译用一个文件,cpp用一个,如下: test.h #pragma once /******************************************…

Java面试篇(线程池相关专题)

文章目录 1. 为什么要使用线程池2. 线程池的核心参数和线程池的执行原理2.1 线程池的核心参数2.2 线程池的执行原理 3. 线程池中常见的阻塞队列3.1 常见的阻塞队列3.2 ArrayBlockingQueue 和 LinkedBlockingQueue 的区别 4. 如何确定线程池的核心线程数4.1 应用程序中任务的类型…

【代码随想录】长度最小的子数组——滑动窗口

本博文为《代码随想录》的学习笔记。原文链接&#xff1a;代码随想录 题目 原题链接&#xff1a;长度最小的子数组 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl1, ..., numsr-1, nums…

历史库,成本与性能如何兼得?| OceanBase应用实践

随着数据量的迅猛增长&#xff0c;企业和组织在数据库管理方面遭遇的挑战愈发凸显。数据库性能逐渐下滑、存储成本节节攀升&#xff0c;以及数据运维复杂性的增加&#xff0c;这些挑战使得DBA和开发者在数据管理上面临更大的压力。 为了应对这些挑战&#xff0c;对数据生命周期…

uni-app学习笔记

一、下载HBuilder https://www.dcloud.io/hbuilderx.html 上述网址下载对应版本&#xff0c;下载完成后进行解压&#xff0c;不需要安装&#xff0c;解压完成后&#xff0c;点击HBuilder X.exe文件进行运行程序 二、创建uni-app项目 此处我是按照文档创建的uni-ui项目模板…

DWG图纸识别工作

DWG图纸识别工作 目的&#xff1a;完成从DWG图纸中数据的提取&#xff0c;在数据提取之前先要对DWG图纸进行识别。得到某个图层的数据。 最终完成图纸建筑外轮廓线坐标数据的提取。 1、 DWG图纸&#xff0c;通过AutoCAD软件导出 DXF文件 2、 DXF文件上传到服务端&#xff0c;…

Java设计模式(适配器模式)

定义 将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。 角色 目标抽象类&#xff08;Target&#xff09;&#xff1a;目标抽象类定义客户所需的接口&#xff08;在类适配器中&#xff0c;目标抽象类只能是接口&#xff09;。 适配器类…

XJTUSE-离散数学-图论

概述 图的定义 几个定义&#xff0c;不赘述 多重图&#xff1a;有平行边存在 简单图&#xff1a;无平行边 无自环 子图 and 补图 完全图的概念 结点的度 入度&#xff0c;出度 奇结点、偶结点 定理&#xff1a;对于无向图&#xff0c;奇结点的个数为偶数 图的同构 必…

Golang 并发编程

Golang 并发编程 Goroutine 什么是协程 创建 Goroutine 主 goroutine &#xff08;main函数&#xff09;退出后&#xff0c;其它的工作 goroutine 也会自动退出 package mainimport ("fmt""time" )func myFunc() {i : 0for {ifmt.Println("func: …

MySQL:表的设计原则和聚合函数

所属专栏&#xff1a;MySQL学习 &#x1f48e;1. 表的设计原则 1. 从需求中找到类&#xff0c;类对应到数据库中的实体&#xff0c;实体在数据库中表现为一张一张的表&#xff0c;类中的属性对应着表中的字段 2. 确定类与类的对应关系 3. 使用SQL去创建具体的表 范式&#xff1…

从“抠图”到“抠视频”,Meta上新AI工具SAM 2。

继2023年4月首次推出SAM&#xff0c;实现对图像的精准分割后&#xff0c;Meta于北京时间2024年7月30日推出了能够分割视频的新模型SAM 2&#xff08;Segment Anything Model 2&#xff09;。SAM 2将图像分割和视频分割功能整合到一个模型中。所谓“分割”&#xff0c;是指区别视…