当前位置: 首页 > news >正文

【数据结构】链表 --- 单链表

单链表

  • 一、链表
    • 1.链表
    • 2.单向链表
  • 二、单向链表的核心概念
    • 2.1 两种基础结构对比
    • 2.2 节点的本质:链表的“最小单元”
  • 三、单向链表的核心操作实现
    • 3.1 插入操作:优先绑定后链,避免断链
      • (1)头插法:在链表头部插入节点
      • (2)尾插法:在链表尾部插入节点
      • (3)任意位置插入:指定下标插入节点
    • 3.2 删除操作:找到前驱节点,跳过待删节点
      • (1)删除第一次出现的指定值节点
      • (2)删除所有值为指定值的节点
    • 3.3 其他常用操作
      • (1)查找节点:判断值是否存在
      • (2)清空链表:释放所有节点引用
      • (3)遍历打印:输出链表所有数据
    • 3.4 测试用例:
  • 四、单向链表的常见问题与优化
    • 4.1 核心问题:遍历循环条件的选择
    • 4.2 不带头链表的缺陷与优化
    • 4.3 时间复杂度分析
  • 五、总结

一、链表

1.链表

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的
在这里插入图片描述

在这里插入图片描述
我们可以从图上看到:

  1. 链式结构再逻辑上是连续的,但是在物理上不一定是连续的
  2. 现实的节点一般都是从堆上申请出来的
  3. 所以那么在其申请出的空间,是按照一定的策略来进行分配的,两次的申请的空间也可能连续,也可能不连续的

但是实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1.单向或双向
在这里插入图片描述
2.带头或者不带头
在这里插入图片描述
3.循环或者非循环
在这里插入图片描述
但是虽然有这么多的链表结构,但是我们重点掌握两种:

  • 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多

  • 无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表

2.单向链表

这期内容,我们优先重点讲述单向链表。
在 Java 数据结构体系中,链表是与数组互补的重要线性结构。相较于数组的连续内存存储,链表通过节点间的引用关系实现数据逻辑排序,尤其在频繁插入、删除场景中展现出更高效率

二、单向链表的核心概念

单向链表是由多个“节点”组成的线性结构,每个节点仅包含数据域一个引用域(next),next 指向链表中的下一个节点,尾节点的 next 始终为 null。根据是否包含“头节点”,单向链表可分为两种常见类型:

2.1 两种基础结构对比

单向链表的核心差异在于是否存在“哨兵节点”(头节点),两种结构的特性对后续增删查改操作影响极大,具体对比如下:

结构类型核心特点头引用(head)作用适用场景
不带头非循环链表无专门头节点,第一个节点直接存储数据指向首元节点(第一个数据节点),插入/删除首元节点时需修改 head 指向作为复杂结构的子结构(如哈希桶、图的邻接表),笔试面试高频考点
带头非循环链表有专门“头节点”(不存储数据),首元节点是头节点的 next始终指向头节点,插入/删除时无需修改 head,仅操作头节点的 next需频繁在头部操作的场景,代码实现更稳定(避免 head 空指针问题)

2.2 节点的本质:链表的“最小单元”

节点是链表的基础组成单元,本质是一个封装了数据和引用的 Java 类,而链表是由多个节点组成,所以创建一个节点类来执行链表的增删查改,而节点是链表的最小单元,只不过这个最小的单元使用类进行封装起来的,而其中的引用域的成员变量是让节点形成链表的关键。
那么它的设计直接决定了链表能否正确串联,核心要点如下:

  • 数据域:存储具体业务数据(如 intString 或自定义对象);
  • 引用域(next):存储下一个节点的地址,是节点间“串联”的关键;
  • 实现方式
    1. 内部类实现:节点类作为链表类的内部类,可直接访问链表的私有成员,代码紧凑,适合单一链表场景;
    2. 独立类实现:节点类单独定义,可被多个数据结构复用(如不同链表共享同一节点类),灵活性更高。

节点类代码示例(内部类实现)

// 链表类
public class SingleLinkedList {// 节点内部类:封装数据和next引用static class ListNode {int val;          // 数据域ListNode next;    // 引用域:指向下一个节点// 节点构造方法public ListNode(int val) {this.val = val;}}public ListNode head; // 链表的头引用:指向首元节点(不带头)或头节点(带头)// 链表构造方法:初始化空链表public SingleLinkedList() {this.head = null; // 不带头链表:初始head为null// 若为带头链表,需初始化头节点:this.head = new ListNode(0);(数据无意义)}
}

三、单向链表的核心操作实现

单向链表的操作围绕“节点引用的修改”展开,核心是避免断链。以下以“不带头非循环链表”为例,实现常用操作(带头链表仅需调整头节点相关逻辑,原理一致)。

 //无头单向非循环链表实现方法接口
public interface IList {//头插法void addFirst(int data);//尾插法void addLast(int data);//任意位置插入void addIndex(int index,int data);//查找是否含有关键字key是否在单链表中boolean contains(int key);//删除第一次出现的关键字为key的节点void remove(int key);//删除所有值为key的节点void removeAllKey(int key);//得到单链表的长度int size();//清空单链表的所有长度void clear();//打印单链表void dispaly();
}

3.1 插入操作:优先绑定后链,避免断链

插入的核心原则:先连接新节点与后续节点,再修改前驱节点的next。若先修改前驱节点的next,会导致后续节点地址丢失,造成断链。

(1)头插法:在链表头部插入节点

  • 场景:快速在链表开头添加数据(如栈的“压栈”操作);
  • 步骤:
    1. 创建新节点 newNode
    2. 新节点的 next 指向原首元节点(head);
    3. 头引用 head 指向新节点;
    4. 链表长度 size 加 1。

代码实现

/*** 头插法:在链表头部插入数据* @param data 待插入的数据*/
public void addFirst(int data) {ListNode newNode = new ListNode(data);// 新节点的next指向原首元节点(即使原链表为空,head为null,也不影响)newNode.next = head;// head更新为新节点,成为新的首元节点head = newNode;
}

(2)尾插法:在链表尾部插入节点

  • 场景:按顺序添加数据;
  • 步骤:
    1. 创建新节点 newNode
    2. 若链表为空(head == null),直接让 head 指向新节点;
    3. 若链表非空,遍历找到尾节点(cur.next == null 的节点);
    4. 尾节点的 next 指向新节点;

代码实现

/*** 尾插法:在链表尾部插入数据* @param data 待插入的数据*/
public void addLast(int data) {ListNode newNode = new ListNode(data);// 情况1:链表为空,直接让head指向新节点if (head == null) {head = newNode;return;}// 情况2:链表非空,遍历找到尾节点ListNode cur = head;while (cur.next != null) { // 循环条件:cur不是尾节点cur = cur.next;}// 尾节点的next指向新节点cur.next = newNode;
}

在这里插入图片描述

(3)任意位置插入:指定下标插入节点

  • 场景:在链表的第 index 个位置插入数据(下标从 0 开始,0 为头节点,size 为尾节点后);
  • 前置检查:index 需满足 0 ≤ index ≤ size,如果index > 0 || index > size,那么便抛出非法参数异常;
  • 步骤:
    1. index == 0,直接调用头插法;
    2. index == size,直接调用尾插法;
    3. 否则,遍历找到第 index-1 个节点(前驱节点);
    4. 新节点的 next 指向前驱节点的 next(后续节点);
    5. 前驱节点的 next 指向新节点。

代码实现

/*** 任意位置插入:在第index个位置插入数据(下标从0开始)* @param index 插入位置* @param data 待插入的数据* @throws IndexIlegal 若index非法,抛出异常*/public void addIndex(int index, int data) {try {//前置检查,判断下标:checkIndex(index);//1.当插入位置为0(头部):if (index == 0){addFirst(data);return;}//2.当插入位置为末尾:if (index == size()){addLast(data);return;}//3.中间位置的插入:ListNode node = new ListNode(data);ListNode cur = head;while(index - 1 != 0){//判断链表元素插入的位置cur = cur.next;index--;//当index自动-1,直到与条件成假时,证明要到了插入了的链表的位置}/*// 找到前驱节点(index-1)也可以使用for循环ListNode cur = head;for (int i = 0; i < index - 1; i++) {cur = cur.next;}*//*cur.next = node;node.next = cur.next;这么操作不可以,链表是根据引用来进行操作的,而不是去依靠其他的,所以先绑定了前链,而插入位置的后链会发生断链,由于是引用的丢失*/// 先连后链,再连前链node.next = cur.next;cur.next = node;}catch (IndexIlegal e){System.out.println("插入位置不合法");e.printStackTrace();}}
public class IndexIlegal extends RuntimeException{public IndexIlegal() {super();}public IndexIlegal(String message) {super(message);}
}

在这里插入图片描述

3.2 删除操作:找到前驱节点,跳过待删节点

删除的核心逻辑:找到待删节点的前驱节点,让前驱节点的 next 指向待删节点的 next,待删节点因失去引用会被 Java 垃圾回收机制回收,俗称就是断链重连。

(1)删除第一次出现的指定值节点

  • 场景:删除链表中第一个值为 key 的节点;
  • 步骤:
    1. 若链表为空,直接返回(无节点可删);
    2. 若首元节点的值为 key,直接让 head 指向 head.next(删除首元节点);
    3. 否则,遍历找到前驱节点(prev.next.val == key 的节点);
    4. 若未找到 key,返回(无该节点);
    5. 前驱节点的 next 指向待删节点的 next。

代码实现

	/*** 删除第一次出现值为key的节点* @param key 待删除节点的值*/public void remove(int key) {// 情况1:判断链表为空if(head == null) {return;}// 情况2:头节点就是待删节点if(head.val == key) {head = head.next;return;}// 情况3:中间位置删除;待删节点在中间或尾部,找到前驱节点curListNode cur = findNodeOfKey(key);//查找key位置前一个元素curif(cur == null) {return;}// 跳过待删节点:cur.next指向待删节点的下一个节点ListNode del = cur.next;cur.next = del.next;}
	//查找key位置前一个元素curprivate ListNode findNodeOfKey(int key) {ListNode cur = head;// 循环条件:cur的下一个节点不是null,且值不等于keywhile (cur.next != null) {if(cur.next.val == key) {
// 循环结束后:要么cur.next为null(未找到key),要么cur.next.val == key(找到)return cur;}cur = cur.next;}return null;}

在这里插入图片描述

(2)删除所有值为指定值的节点

  • 场景:删除链表中所有值为 key 的节点(如清理重复数据);
  • 技巧:使用“快慢指针”(prev 为慢指针,cur 为快指针),cur 遍历链表,prev 维护非 key 节点的尾部;
  • 步骤:
    1. 若链表为空,直接返回;
    2. 初始化 prev = headcur = head.next
    3. 遍历 cur
      • cur.val == keyprev.next = cur.next(删除 cur),cur 后移;
      • cur.val != keyprevcur 均后移;
    4. 遍历结束后,检查首元节点是否为 key(若首元节点是 key,需单独删除)。

代码实现

/*** 删除所有值为key的节点* @param key 待删除节点的值*/
public void removeAllKey(int key) {// 情况1:链表为空if (head == null) {return;}// 步骤1:删除中间和尾部的key节点(头节点暂不处理)ListNode prev = head;ListNode cur = head.next;while (cur != null) {if (cur.val == key) {prev.next = cur.next; // 删除curcur = cur.next;} else {prev = cur; // 只有当cur不是key时,prev才后移cur = cur.next;}//cur = cur.next;// cur始终后移,可以这么写,因为if和else都具有这个语句}// 步骤2:检查头节点是否为key(若为key,单独删除)if (head.val == key) {head = head.next;}
}

提示:

  • 在删除头节点的时候,需要注意当头节点和下一个节点,都是为值key的节点,如果我们先直接用head来进行断链操作,不论是否有return,会造成节点的删除缺少,所以我们需要把这个头节点的删除的放在循环体的最后
  • 最后删除头节点,是因为或许会有头和第二个节点都是具有相同的值,那么在处理的时候,会无法删除头节点,所以在我们把第二个到最后的节点进行遍历一遍后,那么再去单独判断头节点进而去删除

在这里插入图片描述

3.3 其他常用操作

(1)查找节点:判断值是否存在

/*** 判断链表中是否包含值为key的节点* @param key 待查找的值* @return 存在返回true,否则返回false*/
public boolean contains(int key) {ListNode cur = head;while (cur != null) {if (cur.val == key) {return true;}cur = cur.next;}return false;
}

(2)清空链表:释放所有节点引用

  • 注意:不能直接 head = null(会导致中间节点引用未释放,垃圾回收无法回收),需逐个置空 next
  • 步骤:
    1. 遍历链表,记录当前节点的下一个节点;
    2. 将当前节点的 next 置为 null
    3. 最后将 head 置为 nullsize 置为 0。
/*** 清空链表:释放所有节点引用*/
public void clear() {ListNode cur = head;while (cur != null) {//如果先修改置为空,那么不能链接引用了,//所以我们先定义一个值来进行保存起来,便于链接引用ListNode curN = cur.next; // 先记录下一个节点cur.next = null; // 置空当前节点的nextcur = curN; // 移到下一个节点}head = null; // 头引用置空,链表为空
//所有的除了头节点的地址之外均置为空,然后我们手动对头节点来进行置空处理
}

注意:单向链表无法从后置空

(3)遍历打印:输出链表所有数据

/*** 遍历链表,打印所有节点的值*/
public void display() {ListNode cur = head;while (cur != null) {System.out.print(cur.val + " ");cur = cur.next;}System.out.println();
}

3.4 测试用例:

public static void main(String[] args) {MySingleLinkedList list = new MySingleLinkedList();list.addFirst(01);list.addFirst(12);list.addFirst(23);list.addFirst(34);list.addFirst(45);list.addFirst(45);list.display();list.addLast(56);list.addLast(67);list.addLast(78);list.addLast(89);list.addLast(910);list.addLast(45);list.display();System.out.println(list.contains(1));list.addIndex(9,20);list.display();list.remove(20);list.remove(89);list.display();list.removeAllKey(45);list.display();}

在这里插入图片描述

四、单向链表的常见问题与优化

4.1 核心问题:遍历循环条件的选择

遍历链表时,curcur.next 的选择直接影响逻辑正确性,需根据场景区分:

  • cur != null:遍历所有节点(如查找、打印、清空),此时 cur 会访问到尾节点的下一个节点,即是null
  • cur.next != null:需找到尾节点的前驱(如尾插、删除尾部节点),cur没有进入循环体中,所以此时 cur 最终指向尾节点。

错误示例:尾插时用 cur != null 循环,会导致 cur 指向 null,无法执行 cur.next = newNode(空指针异常)。

4.2 不带头链表的缺陷与优化

不带头链表在删除/插入首元节点时,需修改 head 引用,容易出现空指针问题。优化方案:

  • 改用带头链表:初始化一个不存储数据的头节点,head 始终指向头节点,所有操作均围绕头节点的 next 展开,避免 head 频繁修改;
  • 示例:带头链表的头插法:
  // 带头链表初始化:head指向头节点(数据无意义)public SingleLinkedList() {this.head = new ListNode(0); // 头节点this.size = 0;}// 带头链表头插法:无需修改head,只需操作head.nextpublic void addFirst(int data) {ListNode newNode = new ListNode(data);newNode.next = head.next;head.next = newNode;size++;}

4.3 时间复杂度分析

单向链表的操作效率与节点位置相关,具体如下:

操作时间复杂度说明
头插/头删O(1)仅需修改head或头节点的next
尾插/尾删O(n)需遍历到尾节点
任意位置插入O(n)需遍历到指定位置的前驱
查找节点O(n)需遍历所有节点(最坏情况)

五、总结

单向链表是 Java 中最基础的链表结构,其核心是节点引用的正确维护。通过本文的学习,你需要掌握:

  1. 两种结构差异:不带头链表(灵活,适合子结构)与带头链表(稳定,适合独立使用);
  2. 核心操作逻辑:插入优先绑定后链,删除找到前驱节点,避免断链;
  3. 常见问题解决:遍历循环条件选择、空指针预防、效率优化。

这期内容我们讲述了链表的基础单向链表,下一节的内容为大家讲述的Java的双向链表LinkedList,这将为后续的复杂数据结构(如哈希表、图)的学习打下基础;那么这期的内容就先到这里了,如果有什么细节上的不足和缺陷,欢迎大家可以在评论区中指正和批评!谢谢大家!

http://www.dtcms.com/a/398810.html

相关文章:

  • ArcGIS JSAPI 高级教程 - 自由绘制线段、多边形
  • 【2025最新】ArcGIS 点聚合功能实现全教程(进阶版)
  • Express使用教程(二)
  • 大模型部署基础设施搭建 - Docker
  • 芜湖建设机械网站企业管理系统软件下载
  • 永嘉县住房和城乡规划建设局网站自助贸易网
  • 华为云学习笔记(1):ECS 实例操作与密钥登录实践
  • 有一次django开发实录
  • RISC-V 中的 Wait For Interrupt 指令 (wfi) 详解
  • 前端核心框架vue之(指令案例篇1/5)
  • 企业静态网站源码增城建设局网站
  • 网站兼容9公司logo和商标一样吗
  • 题解:AT_abc206_e [ABC206E] Divide Both
  • 链改2.0总架构师何超秘书长重构“可信资产lPO与数链金融RWA”
  • 网站开发技术包括网站建设专业培训
  • 无人机航拍WiFi图传模块,16公里实时高清图传性能和技术参数
  • 视频元素在富文本编辑器中的光标问题
  • 企业网站内容如何搭建推荐做木工的视频网站
  • grounding dino 源码部署 cuda12.4 开放词汇目标检测(Open-Vocabulary Object Detection, OVOD)模型
  • 一个虚拟主机可以做几个网站吗毕设做网站心得体验
  • Spring使用SseEmitter实现后端流式传输和前端Vue数据接收
  • 湖南省新闻最新消息十条深圳seo网站推广方案
  • 语音交互接待服务机器人深度推荐
  • 创建学校网站吗网站搭建工具的种类
  • Linux-ARM-裸机开发-开发环境搭建
  • 2025年校园招聘平台怎么选?
  • 如何把视频放到自己的网站ftp怎么重新上传网站
  • minio文件迁移
  • 网站反链接是什么意思wordpress 侧边收起
  • docker-desktop 分发版不支持用户数据挂载