【数据结构与算法】之链表详解

链表是一种常用的数据结构,它是一种线性数据结构,但与数组不同,它并非连续存储数据,而是通过指针将数据节点连接起来。每个节点都包含数据域和指向下一个节点的指针域。这种结构赋予链表独特的优势和局限性,使其在某些场景下优于数组,在另一些场景下则相对逊色。本文将深入探讨链表,包括单向链表、双向链表和循环链表,并分析其优缺点。

1. 概念概述

链表由多个节点组成,每个节点包含两个部分:

  • 数据域 (data):存储实际数据,可以是任何数据类型。

  • 指针域 (next):指向链表中下一个节点的地址。

如上图,链表的第一个节点称为头节点 (head),最后一个节点称为尾节点 (tail)。尾节点的指针域通常指向 null,表示链表的结束。

2. 链表的特点

优点:

  • 动态分配内存: 链表可以动态分配内存,不必事先指定大小,可以根据需要添加或删除节点。

  • 灵活插入和删除: 在链表中间插入或删除节点,只需修改指针即可,时间复杂度为 O(1)。

  • 无需连续内存: 链表的节点可以在内存中分散存储,不需要连续的内存空间。

缺点:

  • 随机访问困难: 链表无法像数组一样通过下标直接访问元素,需要遍历才能找到指定位置的节点。

  • 空间开销: 每个节点都需要额外的指针域存储地址,导致空间开销相对较大。

  • 维护复杂: 链表的操作需要仔细维护指针,容易出错。

3. 链表的 Java 实现

为了更好地理解链表,我们先定义一个简单的节点类:

public class Node {// 数据域public int data;// 指针域public Node next;// 构造函数public Node(int data) {this.data = data;this.next = null; // 初始化时,指向null}
}

接下来,我们将实现一个包含常见方法的单向链表类:

public class SinglyLinkedList {// 头节点private Node head;// 初始化空链表public SinglyLinkedList() {head = null;}// 检查链表是否为空public boolean isEmpty() {return head == null;}// 获取链表长度public int size() {int count = 0;Node current = head;while (current != null) {count++;current = current.next;}return count;}// 在链表头部插入节点public void insertAtHead(int data) {Node newNode = new Node(data);newNode.next = head; // 新节点的next指向原头节点head = newNode; // 更新头节点}// 在链表尾部插入节点public void insertAtTail(int data) {Node newNode = new Node(data);if (isEmpty()) {head = newNode; // 空链表,直接将新节点设为头节点return;}Node current = head;while (current.next != null) {current = current.next; // 遍历到最后一个节点}current.next = newNode; // 将最后一个节点的next指向新节点}// 在指定位置插入节点public void insertAtIndex(int index, int data) {if (index < 0 || index > size()) {return; // 非法索引}if (index == 0) {insertAtHead(data); // 如果索引为0,直接调用插入头部方法return;}Node newNode = new Node(data);Node current = head;for (int i = 0; i < index - 1; i++) {current = current.next; // 遍历到指定位置的前一个节点}newNode.next = current.next; // 新节点的next指向原指定位置的节点current.next = newNode; // 指定位置的前一个节点的next指向新节点}// 删除链表头部的节点public void deleteAtHead() {if (isEmpty()) {return; // 空链表,无需删除}head = head.next; // 将头节点指向下一个节点}// 删除链表尾部的节点public void deleteAtTail() {if (isEmpty()) {return; // 空链表,无需删除}if (head.next == null) { // 只有一个节点的情况head = null;return;}Node current = head;while (current.next.next != null) {current = current.next; // 遍历到倒数第二个节点}current.next = null; // 将倒数第二个节点的next指向null}// 删除指定位置的节点public void deleteAtIndex(int index) {if (index < 0 || index >= size()) {return; // 非法索引}if (index == 0) {deleteAtHead(); // 如果索引为0,直接调用删除头部方法return;}Node current = head;for (int i = 0; i < index - 1; i++) {current = current.next; // 遍历到指定位置的前一个节点}current.next = current.next.next; // 将指定位置的前一个节点的next指向指定位置的下一个节点}// 查找链表中第一个值为data的节点public Node search(int data) {Node current = head;while (current != null) {if (current.data == data) {return current; // 找到节点,返回}current = current.next; // 继续遍历}return null; // 未找到,返回null}// 打印链表public void printList() {Node current = head;while (current != null) {System.out.print(current.data + " "); // 打印节点数据current = current.next; // 继续遍历}System.out.println(); // 换行}
}

4. 链表的基本操作图解

以上代码示例展示了单向链表的一些基本操作,光看代码看不出所以然,这里通过图示进行详解,包括:

4.1 插入: 在头部、尾部或指定位置插入节点。

#头插法:将新节点插入至第一个节点的位置

a.演示将新节点6插入第一节点位置,也就是插入节点1的位置

b.先让6的next指向原来的第一个节点,也就是指向节点1

c.然后让head的next指向新插入的节点6,此时原来节点1就会与head自动断开

d.成功插入

#尾插法:这里不做直接演示,具体原理是 -> 直接让原本的最后一个节点的next指向要插入的节点,然后再让这个新节点的next指向null,成为新的尾节点。

#中间插入:将新节点插入到链表的中间任意位置

a.将新节点6插入索引1的位置,也就是插入到节点2的位置

b.先让新节点6的next指向节点2

c.然后让节点1指向新节点6,这里有个细节是,插入时,还要知道插入位置的前一个节点的位置

d.成功插入

4.2 删除: 删除头部、尾部或指定位置的节点。

a.删除节点6

b.让被删除的节点6的前一个节点1,让节点1指向节点2,这里有一个细节是,删除时,你要知道你的前一个节点的位置和后一个节点的位置,在这就是,删除节点6,要知道节点1、2的位置。若是删除最后一个节点,只需让它的前一个节点的next指向null就好

c.删除成功

4.3 查找: 查找链表中值为data的节点。

4.4 遍历: 打印所有节点的数据。

5. 各个操作的时间复杂度

操作时间复杂度说明
插入头部O(1)只需修改头指针和新节点的指针
插入尾部O(n)需要遍历链表找到尾节点
插入指定位置O(n)需要遍历链表找到指定位置
删除头部O(1)只需修改头指针
删除尾部O(n)需要遍历链表找到尾节点
删除指定位置O(n)需要遍历链表找到指定位置
查找O(n)需要遍历链表查找目标节点
遍历O(n)需要访问所有节点

6. 链表的局限性

尽管链表具有动态性、灵活性和无需连续内存的优势,但它也有一些局限性:

  • 随机访问困难: 无法像数组一样通过下标直接访问元素,需要遍历才能找到指定位置的节点。

  • 空间开销: 每个节点都需要额外的指针域存储地址,导致空间开销相对较大。

  • 维护复杂: 链表的操作需要仔细维护指针,容易出错。

7. 双向链表

单向链表只能从头节点开始遍历,无法从尾节点反向遍历。双向链表则通过添加一个指向前一个节点的指针域 (prev) 来解决这个问题。

节点组成:

public class DoublyLinkedList {// 头节点private Node head;// 尾节点private Node tail;public DoublyLinkedList() {head = null;tail = null;}// 检查链表是否为空public boolean isEmpty() {return head == null;}// 获取链表长度public int size() {int count = 0;Node current = head;while (current != null) {count++;current = current.next;}return count;}// 在链表头部插入节点public void insertAtHead(int data) {Node newNode = new Node(data);if (isEmpty()) {head = newNode;tail = newNode;} else {newNode.next = head; // 新节点的next指向原头节点head.prev = newNode; // 原头节点的prev指向新节点head = newNode; // 更新头节点}}// 在链表尾部插入节点public void insertAtTail(int data) {Node newNode = new Node(data);if (isEmpty()) {head = newNode;tail = newNode;} else {tail.next = newNode; // 原尾节点的next指向新节点newNode.prev = tail; // 新节点的prev指向原尾节点tail = newNode; // 更新尾节点}}// 在指定位置插入节点public void insertAtIndex(int index, int data) {if (index < 0 || index > size()) {return; // 非法索引}if (index == 0) {insertAtHead(data); // 如果索引为0,直接调用插入头部方法return;}if (index == size()) {insertAtTail(data); // 如果索引等于链表长度,直接调用插入尾部方法return;}Node newNode = new Node(data);Node current = head;for (int i = 0; i < index - 1; i++) {current = current.next; // 遍历到指定位置的前一个节点}newNode.next = current.next; // 新节点的next指向原指定位置的节点newNode.prev = current; // 新节点的prev指向指定位置的前一个节点current.next.prev = newNode; // 原指定位置节点的prev指向新节点current.next = newNode; // 指定位置的前一个节点的next指向新节点}// 删除链表头部的节点public void deleteAtHead() {if (isEmpty()) {return; // 空链表,无需删除}if (head == tail) { // 只有一个节点的情况head = null;tail = null;return;}head = head.next; // 将头节点指向下一个节点head.prev = null; // 更新头节点的prev指向null}// 删除链表尾部的节点public void deleteAtTail() {if (isEmpty()) {return; // 空链表,无需删除}if (head == tail) { // 只有一个节点的情况head = null;tail = null;return;}tail = tail.prev; // 将尾节点指向前一个节点tail.next = null; // 更新尾节点的next指向null}// 删除指定位置的节点public void deleteAtIndex(int index) {if (index < 0 || index >= size()) {return; // 非法索引}if (index == 0) {deleteAtHead(); // 如果索引为0,直接调用删除头部方法return;}if (index == size() - 1) {deleteAtTail(); // 如果索引为最后一个节点,直接调用删除尾部方法return;}Node current = head;for (int i = 0; i < index; i++) {current = current.next; // 遍历到指定位置的节点}current.prev.next = current.next; // 指定位置前一个节点的next指向指定位置的下一个节点current.next.prev = current.prev; // 指定位置下一个节点的prev指向指定位置的前一个节点}// 查找链表中第一个值为data的节点public Node search(int data) {Node current = head;while (current != null) {if (current.data == data) {return current; // 找到节点,返回}current = current.next; // 继续遍历}return null; // 未找到,返回null}// 打印链表public void printList() {Node current = head;while (current != null) {System.out.print(current.data + " "); // 打印节点数据current = current.next; // 继续遍历}System.out.println(); // 换行}
}

双向链表的优点:

  • 能够从头节点和尾节点进行双向遍历。

  • 可以更方便地删除节点,因为可以访问该节点的前一个节点。

双向链表的缺点:

  • 每个节点需要额外的 prev 指针域,导致空间开销更大。

8. 循环链表

循环链表是一种特殊的链表,它的尾节点的指针域指向头节点,形成一个循环。

public class CircularLinkedList {// 头节点private Node head;public CircularLinkedList() {head = null;}// 检查链表是否为空public boolean isEmpty() {return head == null;}// 获取链表长度public int size() {if (isEmpty()) {return 0;}int count = 1;Node current = head.next; // 从头节点的下一个节点开始遍历while (current != head) { // 当遍历到头节点时停止count++;current = current.next;}return count;}// 在链表头部插入节点public void insertAtHead(int data) {Node newNode = new Node(data);if (isEmpty()) {head = newNode;newNode.next = head; // 形成循环} else {newNode.next = head; // 新节点的next指向原头节点Node current = head;while (current.next != head) { // 遍历到最后一个节点current = current.next;}current.next = newNode; // 最后一个节点的next指向新节点head = newNode; // 更新头节点}}// 在链表尾部插入节点public void insertAtTail(int data) {Node newNode = new Node(data);if (isEmpty()) {head = newNode;newNode.next = head; // 形成循环} else {Node current = head;while (current.next != head) { // 遍历到最后一个节点current = current.next;}current.next = newNode; // 最后一个节点的next指向新节点newNode.next = head; // 新节点的next指向头节点}}// 在指定位置插入节点public void insertAtIndex(int index, int data) {if (index < 0 || index > size()) {return; // 非法索引}if (index == 0) {insertAtHead(data); // 如果索引为0,直接调用插入头部方法return;}if (index == size()) {insertAtTail(data); // 如果索引等于链表长度,直接调用插入尾部方法return;}Node newNode = new Node(data);Node current = head;for (int i = 0; i < index - 1; i++) {current = current.next; // 遍历到指定位置的前一个节点}newNode.next = current.next; // 新节点的next指向原指定位置的节点current.next = newNode; // 指定位置的前一个节点的next指向新节点}// 删除链表头部的节点public void deleteAtHead() {if (isEmpty()) {return; // 空链表,无需删除}if (head.next == head) { // 只有一个节点的情况head = null;return;}Node current = head;while (current.next != head) { // 遍历到最后一个节点current = current.next;}head = head.next; // 将头节点指向下一个节点current.next = head; // 最后一个节点的next指向新的头节点}// 删除链表尾部的节点public void deleteAtTail() {if (isEmpty()) {return; // 空链表,无需删除}if (head.next == head) { // 只有一个节点的情况head = null;return;}Node current = head;while (current.next.next != head) { // 遍历到倒数第二个节点current = current.next;}current.next = current.next.next; // 将倒数第二个节点的next指向倒数第三个节点}// 删除指定位置的节点public void deleteAtIndex(int index) {if (index < 0 || index >= size()) {return; // 非法索引}if (index == 0) {deleteAtHead(); // 如果索引为0,直接调用删除头部方法return;}if (index == size() - 1) {deleteAtTail(); // 如果索引为最后一个节点,直接调用删除尾部方法return;}Node current = head;for (int i = 0; i < index - 1; i++) {current = current.next; // 遍历到指定位置的前一个节点}current.next = current.next.next; // 将指定位置的前一个节点的next指向指定位置的下一个节点}// 查找链表中第一个值为data的节点public Node search(int data) {if (isEmpty()) {return null;}Node current = head;do {if (current.data == data) {return current; // 找到节点,返回}current = current.next;} while (current != head);return null; // 未找到,返回null}// 打印链表public void printList() {if (isEmpty()) {return;}Node current = head;System.out.print(current.data + " "); // 打印头节点数据current = current.next;while (current != head) { // 当遍历到头节点时停止System.out.print(current.data + " "); // 打印节点数据current = current.next;}System.out.println(); // 换行}
}

循环链表的优点:

  • 可以实现一些特殊的数据结构,例如循环队列。

  • 可以更方便地遍历链表,因为可以从任何节点开始遍历。

循环链表的缺点:

  • 维护复杂度更高,需要特别注意循环条件。

9. 总结

链表是一种灵活、动态的数据结构,适合存储动态变化的数据。单向链表、双向链表和循环链表各有优劣,开发者需要根据实际情况选择最合适的链表类型。

需要注意的是,以上代码示例只是简单演示了链表的基本操作,且只使用Java实现,实际应用中可能需要根据具体需求添加其他方法,并进行更完善的错误处理和性能优化。

希望本文能让各位看官更好的掌握链表,感谢各位的观看,下期见,谢谢~

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

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

相关文章

九种排序,一次满足

我们在算法题进行练习提升时&#xff0c;经常会看到题目要求数据从大到小输出&#xff0c;从小到大输出&#xff0c;前一半从小到大输出&#xff0c;后一半从大到小输出等&#xff0c;那么这时候就需要用到排序算法&#xff0c;通过排序算法将数据按照一定的顺序进行排序。本文…

解决PyCharm 2023 Python Packages列表为空

原因是因为没有设置镜像源 展开 > 之后&#xff0c;这里 点击齿轮 添加一个阿里云的源 最后还需要点击刷新 可以选择下面的任意一个国内镜像源&#xff1a; 清华&#xff1a;https://pypi.tuna.tsinghua.edu.cn/simple 阿里云&#xff1a;http://mirrors.aliyun.com/…

设计模式之-策略模式配合枚举

1、定义枚举接收不同的参数使用不同的handler, 2、定义个handerl接口&#xff0c;统一方法处理&#xff0c;每个handler实现该接口 public interface IMethodHandler<T, R> {/*** 处理统一入口** param req*/R process(T req); } java3、定义一个简单工厂统一处理 Comp…

送给正在入行的小白:最全最有用的网络安全学习路线已经安排上了,零基础入门到精通,收藏这一篇就够了

在这个圈子技术门类中&#xff0c;工作岗位主要有以下三个方向&#xff1a; 安全研发安全研究&#xff1a;二进制方向安全研究&#xff1a;网络渗透方向 下面逐一说明一下。 第一个方向&#xff1a;安全研发 你可以把网络安全理解成电商行业、教育行业等其他行业一样&#xf…

k8s 1.28.2 集群部署 harbor v2.11.1 接入 MinIO 对象存储

文章目录 [toc]提前准备什么是 HarborHarbor 架构描述Harbor 安装的先决条件硬件资源软件依赖端口依赖 Harbor 在 k8s 的高可用Harbor 部署Helm 编排YAML 编排创建 namespace导入镜像部署 Redis部署 PostgreSQL部署 Harbor core部署 Harbor trivy部署 Harbor jobservice部署 Ha…

RTSP流图片采样助手(yolov5)

在监控和视频分析领域&#xff0c;实时采样视频流中的图像数据是十分重要的。本文将介绍一个基于Python和Tkinter构建的RTSP流图片采样助手的设计与实现&#xff0c;旨在简化RTSP流的采样过程&#xff0c;并支持根据用户定义的特殊标签进行筛选。 项目概述 该项目的主要功能包…

Data+AI下的数据湖和湖仓一体发展史

DataAI下的数据湖和湖仓一体发展史 前言数据湖的“前世今生”AI时代的救星&#xff1a;湖仓一体湖仓一体实践演进未来趋势&#xff1a;智能化、实时化结语 前言 数据湖&#xff1f;湖仓一体&#xff1f;这是什么高科技新名词&#xff1f; 别急&#xff0c;我们慢慢聊。想象一…

ICT产业新征程:深度融合与高质量发展

在信息时代的浪潮中&#xff0c;每一场关于技术革新与产业融合的盛会都闪耀着智慧的光芒&#xff0c;引领着未来的方向。9月25日&#xff0c;北京国家会议中心内&#xff0c;一场聚焦全球信息通信业的顶级盛事——第32届“国际信息通信展”&#xff08;PT展&#xff09;隆重拉开…

Maven基于构建阶段分析多余的依赖

基于构建阶段 test compile 实现依赖分析 执行maven 命令: mvn dependency:analyze 关注:Maven-dependency-plugin 分析结果: [INFO] --- maven-dependency-plugin:2.10:analyze (default-cli) impl --- 配置依赖未使用的依赖项&#xff1a; [INFO] --- maven-dependency-…

Linux基础项目开发day2:量产工具——输入系统

文章目录 前言一、数据结构抽象1、数据本身2、设备本身3、input_manager.h 二、触摸屏编程1、touchscreen.c 三、触摸屏单元测试1、touchscreen.c2、上机测试 四、网络编程netiput.c 五、网络单元测试1、netiput.c2、client.c3、上机测试 六、输入系统的框架1、框架思路2、inpu…

数据库设计与开发—初识SQLite与DbGate

一、SQLite与DbGate简介 &#xff08;一&#xff09;SQLite[1][3] SQLite 是一个部署最广泛、用 C 语言编写的数据库引擎&#xff0c;属于嵌入式数据库&#xff0c;其作为库被软件开发人员嵌入到应用程序中。 SQLite 的设计允许在不安装数据库管理系统或不需要数据库管理员的情…

sublime配置(竞赛向)

我也想要有jiangly一样的sublime 先决条件 首先&#xff0c;到官网上下载最新的sublime4&#xff0c;然后在mingw官网上下载最新的mingw64 mingw64官网&#xff1a;左边菜单栏点击dowloads,然后选择MinGW-W64-builds(可能会有点慢)——然后有时候会变成选LLVM-minGW,接着选择…

linux c国际化

一种locale表示一种文化的各种数据的表示或显示方式&#xff0c;一种locale分成多个部分&#xff0c;不同的部分由category表示&#xff0c;每一种category下面定义了很多关键字keyword locale -a 查看所有支持的locale&#xff0c; locale 不带参 查看当前locale的各个categ…

大语言模型怎么写好提示词,看这篇就够了

对于任何输入&#xff0c;大语言模型都会给出相应的输出&#xff0c;这些输入都可以成为提示词&#xff0c;通常&#xff0c;提示词由指令和输入数据组成&#xff0c;指令是任务&#xff0c;输入数据是完成的要求&#xff0c;其中指令应该明确&#xff0c;用词不能模棱两可&…

centos7.9升级rockylinux8.8

前言 查看centos的版本 &#xff0c;我这台服务器是虚拟机,下面都是模拟实验 升级前一定要把服务器上配置文件&#xff0c;数据等进行备份 [rootlocalhost ~]#cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) [rootlocalhost ~]#uname -a Linux jenkins_ser…

【C++进阶】AVL树的实现

1. AVL的概念 AVL树是最先发明的⾃平衡⼆叉查找树&#xff0c;AVL是⼀颗空树&#xff0c;或者具备下列性质的⼆叉搜索树&#xff1a;它的左右⼦树都是AV树&#xff0c;且左右⼦树的⾼度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树&#xff0c;通过控制⾼度差去控制平衡…

SLM201A系列24V, 15mA - 60mA单通道线性恒流LED驱动芯片 灯带灯条解决方案

SLM201A系列型号&#xff1a; SLM201A15aa-7G SLM201A20aa-7G SLM201A25aa-7G SLM201A30aa-7G SLM201A35aa-7G SLM201A40aa-7G SLM201A45aa-7G SLM201A50aa-7G SLM201A55aa-7G SLM201A60aa-7G SLM201A 系列产品是用于产生单通道、高…

基于FPGA的以太网设计(一)

以太网简介 以太网&#xff08;Ethernet&#xff09;是一种计算机局域网技术。IEEE组织的IEEE 802.3标准制定了以太网的技术标准&#xff0c;它规定了包括物理层的连线、电子信号和介质访问控制的内容。以太网是目前应用最普遍的局域网技术&#xff0c;取代了其他局域网标准如…

【unity小技巧】Unity6 LTS版本安装和一些修改和新功能使用介绍

文章目录 前言安装新功能变化1、官方推荐使用inputsystem进行输入控制2、修复了InputSystem命名错误导致listen被遮挡的bug3、自带去除unity启动画面logo功能4、unity官方的behavior行为树插件5、linearVelocity代替过时的velocity方法待续 完结 前言 2024/10/17其实unity就已…

gitlab:ssh设置

我用的是window&#xff0c;先打开终端&#xff1a; 1、输入 ssh-skygen 执行 然后输入路径&#xff0c;路径地址就是后面括号内的内容 2、然后直接下一步下一步即可&#xff0c;像上面那样就成了 3、打开公钥&#xff0c;复制 4、打开gitlab&#xff0c;在我的 Edit profil…