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

《单链表学习手册:从原理到代码实现(含头插 / 尾插 / 销毁)》

目录

一. 单链表(Singly Linked List)

1.1 单链表的核心构成

1.2 单链表的核心特性

二. 单链表的打印(带头结点)

三. 单链表的尾插

四. 单链表的头插

五. 单链表的尾删

六. 单链表的头删

七. 单链表的查找

八. 单链表在指定位置插入元素

8.1 在指定位置之后插入元素

8.2 在指定位置之前插入元素

九. 单链表在指定位置删除元素

9.1 删除指定位置之后的元素

9.2 删除指定位置之前的元素

十. 单链表的销毁


一. 单链表(Singly Linked List)

单链表是线性表的一种重要存储结构,与数组的 “连续存储” 不同,它采用 “离散存储” 模式 —— 通过指针(或引用) 将分散在内存中的节点串联成一个有序序列,是数据结构中解决 “动态扩容” 和 “高效插入 / 删除” 问题的基础结构。

1.1 单链表的核心构成

单链表的最小单元是节点(Node),整个链表由 “头节点”“数据节点” 和 “尾节点” 共同组成,结构如下:

1. 节点(Node)的结构

每个节点包含两个核心部分,用结构体(C/C++)或类(Java/Python)定义,逻辑结构如下:

字段(Field)作用
数据域(Data)存储当前节点的实际数据(如整数、字符串、对象等,根据业务需求定义)。
指针域(Next)存储下一个节点的内存地址(或引用),是连接节点的 “纽带”。
// 定义单链表节点(存储整数数据)
typedef struct Node {int data;          // 数据域:存储整数struct Node *next; // 指针域:指向后一个节点
} Node;

2. 链表的整体结构

单链表的节点按 “线性顺序” 串联,有明确的 “起点” 和 “终点”:

  • 头节点(Head Node)
    • 链表的 “入口”,用于定位整个链表(若没有头节点,链表会丢失)。
    • 分为 “带头节点” 和 “不带头节点” 两种设计:
      • 带头节点:头节点的data域无实际意义(或存链表长度),next指向第一个数据节点,可避免插入 / 删除首节点时修改头指针,代码更统一。
      • 不带头节点:头节点即第一个数据节点,data域存储有效数据。
  • 数据节点:链表的核心,data域存储有效数据,next指向后续节点。
  • 尾节点(Tail Node)
    • 链表的 “最后一个节点”,其next指针永远指向 NULL(空),是判断链表是否遍历到末尾的标志。

1.2 单链表的核心特性

单链表的特性由其 “离散存储 + 指针连接” 的结构决定,与数组形成鲜明对比:

特性维度单链表(Singly Linked List)数组(Array)
存储方式离散存储(节点分散在内存,靠指针连接)连续存储(元素在内存中占用连续地址空间)
访问效率只能顺序访问(从表头遍历到目标节点,时间复杂度 O (n))支持随机访问(通过下标直接定位,O (1))
插入 / 删除已知前驱节点时,效率高(仅需修改指针,O (1))插入 / 删除需移动后续元素,效率低(O (n))
内存扩容动态扩容(无需预先分配内存,新增节点时申请即可)静态扩容(需预先分配固定大小,满后需迁移)
内存利用率无内存浪费(节点数量 = 数据量)可能有内存浪费(预分配空间未用完)

二. 单链表的打印(带头结点)

单链表打印的核心思路

打印单链表的本质是顺序遍历:从链表的第一个有效节点开始,依次访问每个节点,输出其数据域的值,直到遇到尾节点(next指针为NULL)。

/*** 打印单链表所有节点的数据* @param head 链表的头节点(带头节点)*/
void printLinkedList(Node* head) {// 1. 处理空链表情况if (head == NULL) {printf("链表为空(头节点不存在)\n");return;}// 2. 初始化遍历指针,指向第一个数据节点Node* current = head->next;// 3. 若链表无数据节点(空链表)if (current == NULL) {printf("链表为空(无数据节点)\n");return;}// 4. 遍历并打印所有数据printf("链表数据:");while (current != NULL) {printf("%d ", current->data); // 输出当前节点数据current = current->next;      // 指针后移}printf("\n"); // 打印结束后换行
}

代码说明

  1. 空链表处理

    • 若头节点headNULL:说明链表未初始化,直接提示错误。
    • head->nextNULL:说明链表已初始化但无数据节点,提示 “空链表”。
  2. 遍历逻辑

    • current指针从第一个数据节点开始遍历,每次循环输出current->data
    • 循环结束条件:current == NULL(到达尾节点之后)。
  3. 输出格式

    • 数据之间用空格分隔,便于阅读。
    • 打印完毕后换行,避免后续输出与链表数据混淆。

三. 单链表的尾插

尾插是指在链表的最后一个节点(尾节点)之后插入新节点,使新节点成为新的尾节点。

实现思路

  1. 创建新节点并初始化数据域和指针域(next = NULL
  2. 遍历链表,找到当前的尾节点(next = NULL 的节点)
  3. 将尾节点的 next 指针指向新节点
/*** 尾插法:在链表末尾插入新节点* @param head 头节点* @param data 要插入的数据*/
void insertAtTail(Node* head, int data) {// 1. 创建新节点Node* newNode = (Node*)malloc(sizeof(Node));newNode->data = data;newNode->next = NULL;  // 新节点将成为尾节点,next设为NULL// 2. 找到尾节点Node* current = head;while (current->next != NULL) {  // 遍历到尾节点的前一个节点current = current->next;}// 3. 插入新节点current->next = newNode;
}

四. 单链表的头插

头插是指在链表的第一个数据节点之前插入新节点,使新节点成为第一个数据节点。

实现思路

  1. 创建新节点并初始化数据域
  2. 将新节点的 next 指针指向当前的第一个数据节点(head->next
  3. 将头节点的 next 指针指向新节点
/*** 头插法:在链表头部插入新节点* head 头节点* data 要插入的数据*/
void insertAtHead(Node* head, int data) {// 1. 创建新节点Node* newNode = (Node*)malloc(sizeof(Node));newNode->data = data;// 2. 插入新节点(两步操作顺序不可颠倒)newNode->next = head->next;  // 新节点指向原来的第一个数据节点head->next = newNode;        // 头节点指向新节点,使其成为第一个数据节点
}

五. 单链表的尾删

尾删是指删除链表的最后一个节点(尾节点),使倒数第二个节点成为新的尾节点。

实现思路

  1. 处理空链表情况(无数据节点时无法删除)
  2. 遍历链表,找到尾节点的前驱节点(倒数第二个节点)
  3. 保存尾节点的地址(用于释放内存)
  4. 将前驱节点的 next 指针设为 NULL(使其成为新尾节点)
  5. 释放原尾节点的内存
/*** 尾删法:删除链表末尾的节点* @param head 头节点* @return 0表示成功,-1表示失败(空链表)*/
int deleteAtTail(Node* head) {// 1. 判断链表是否为空if (head->next == NULL) {printf("链表为空,无法删除\n");return -1;}// 2. 找到尾节点的前驱节点Node* current = head;while (current->next->next != NULL) {  // 循环条件:当前节点的下下个节点不为NULLcurrent = current->next;}// 3. 保存尾节点地址并删除Node* tailNode = current->next;  // 尾节点current->next = NULL;            // 前驱节点成为新尾节点free(tailNode);                  // 释放内存return 0;
}

六. 单链表的头删

头删是指删除链表的第一个数据节点,使第二个数据节点成为新的第一个数据节点。

实现思路

  1. 处理空链表情况(无数据节点时无法删除)
  2. 保存第一个数据节点的地址(用于释放内存)
  3. 将头节点的 next 指针指向第二个数据节点(firstNode->next
  4. 释放被删除节点的内存
/*** 头删法:删除链表头部的节点* @param head 头节点* @return 0表示成功,-1表示失败(空链表)*/
int deleteAtHead(Node* head) {// 1. 判断链表是否为空if (head->next == NULL) {printf("链表为空,无法删除\n");return -1;}// 2. 保存第一个数据节点的地址Node* firstNode = head->next;// 3. 删除节点head->next = firstNode->next;  // 头节点指向第二个数据节点free(firstNode);               // 释放第一个数据节点的内存return 0;
}

七. 单链表的查找

单链表的查找本质是顺序遍历:从第一个数据节点开始,逐个比对节点的数据域与目标值,直到找到匹配节点或遍历完整个链表。

步骤分解:

  1. 定义遍历指针,初始指向第一个数据节点(head->next,基于带头节点设计)。
  2. 循环遍历链表:
    • 若当前节点数据与目标值匹配,返回该节点(或其位置信息)。
    • 若不匹配,指针后移(current = current->next)。
  3. 若遍历结束仍未找到,返回NULL(表示查找失败)。
typedef struct Node {int data;          // 数据域(以整数为例)struct Node* next; // 指针域
} Node;
/*** 在单链表中查找首个数据为target的节点* @param head 头节点* @param target 目标值* @return 找到的节点指针,未找到返回NULL*/
Node* findNode(Node* head, int target) {// 处理空链表if (head == NULL || head->next == NULL) {return NULL;}// 遍历指针从第一个数据节点开始Node* current = head->next;// 顺序遍历查找while (current != NULL) {if (current->data == target) {return current; // 找到目标节点,返回指针}current = current->next; // 指针后移}// 遍历结束未找到return NULL;
}

代码说明

  1. 空链表处理

    • 若头节点headNULL(链表未初始化),或head->nextNULL(无数据节点),直接返回NULL或 0。
  2. 遍历逻辑

    • 从第一个数据节点(head->next)开始遍历,确保不包含头节点的无效数据。
    • 每次循环先判断当前节点是否匹配,再移动指针,避免遗漏节点。
  3. 返回值设计

    • 返回节点指针:便于后续对该节点进行修改(如更新数据、删除节点等)。
    • 返回位置索引:适合需要展示 “第几个元素” 的场景(如用户交互)。

八. 单链表在指定位置插入元素

单链表在指定位置的插入分为两种情况:在指定位置之前插入和在指定位置之后插入。这两种操作的核心都是通过指针调整来实现节点的插入,但遍历的终点和指针修改逻辑有所不同。以下基于带头节点的链表设计详细讲解。

8.1 在指定位置之后插入元素

实现思路

在第pos个节点之后插入新节点,步骤如下:

  1. 先检查位置pos的合法性(pos ≥ 1且不超过链表长度)
  2. 遍历链表,找到第pos个节点(记为p
  3. 创建新节点newNode,设置其数据域
  4. 调整指针:
    • 新节点的next指向p的下一个节点(newNode->next = p->next
    • pnext指向新节点(p->next = newNode
/*** 在第pos个节点之后插入元素* @param head 头节点* @param pos 位置(从1开始计数)* @param data 要插入的数据* @return 0表示成功,-1表示失败(位置非法)*/
int insertAfterPos(Node* head, int pos, int data) {// 1. 检查位置合法性(pos至少为1)if (pos < 1) {printf("位置不合法\n");return -1;}// 2. 找到第pos个节点Node* p = head->next;  // 从第一个数据节点开始int currentPos = 1;    // 当前位置计数while (p != NULL && currentPos < pos) {p = p->next;currentPos++;}// 若p为NULL,说明pos超过链表长度if (p == NULL) {printf("位置超过链表长度\n");return -1;}// 3. 创建新节点Node* newNode = (Node*)malloc(sizeof(Node));newNode->data = data;// 4. 插入新节点(顺序不可颠倒)newNode->next = p->next;p->next = newNode;return 0;
}

8.2 在指定位置之前插入元素

实现思路

在第pos个节点之前插入新节点,步骤如下:

  1. 检查位置pos的合法性(pos ≥ 1
  2. 遍历链表,找到第pos-1个节点(记为p,即目标位置的前驱节点)
    • pos=1,则前驱节点为头节点head
  3. 创建新节点newNode,设置其数据域
  4. 调整指针:
    • 新节点的next指向p的下一个节点(newNode->next = p->next
    • pnext指向新节点(p->next = newNode
/*** 在第pos个节点之前插入元素* @param head 头节点* @param pos 位置(从1开始计数)* @param data 要插入的数据* @return 0表示成功,-1表示失败(位置非法)*/
int insertBeforePos(Node* head, int pos, int data) {// 1. 检查位置合法性(pos至少为1)if (pos < 1) {printf("位置不合法\n");return -1;}// 2. 找到第pos-1个节点(前驱节点)Node* p = head;        // 从头部开始(可能是头节点)int currentPos = 0;    // 当前位置计数(头节点视为位置0)while (p != NULL && currentPos < pos - 1) {p = p->next;currentPos++;}// 若p为NULL,说明pos超过链表长度+1if (p == NULL) {printf("位置超过链表长度\n");return -1;}// 3. 创建新节点Node* newNode = (Node*)malloc(sizeof(Node));newNode->data = data;// 4. 插入新节点newNode->next = p->next;p->next = newNode;return 0;
}

总结

指定位置的插入操作核心是找到正确的前驱节点调整指针关系

  • 之后插入:前驱是第pos个节点
  • 之前插入:前驱是第pos-1个节点

两种操作的时间复杂度均为 O (n),因为需要遍历到目标位置。实现时需特别注意边界情况(如pos=1、空链表、位置超出范围)的处理,以及指针修改的顺序,避免链表断裂。

九. 单链表在指定位置删除元素

9.1 删除指定位置之后的元素

实现思路

删除第pos个节点之后的节点(即第pos+1个节点),步骤如下:

  1. 检查位置pos的合法性(pos ≥ 1且第pos个节点存在)
  2. 遍历链表,找到第pos个节点(记为p
  3. 检查p的下一个节点是否存在(即待删除节点是否存在)
  4. 保存待删除节点的地址(用于释放内存)
  5. 调整指针:p->next = 待删除节点->next
  6. 释放待删除节点的内存
/*** 删除第pos个节点之后的元素* @param head 头节点* @param pos 基准位置(从1开始计数)* @return 0表示成功,-1表示失败*/
int deleteAfterPos(Node* head, int pos) {// 1. 处理空链表if (head == NULL || head->next == NULL) {printf("链表为空,无法删除\n");return -1;}// 2. 检查位置合法性if (pos < 1) {printf("位置不合法\n");return -1;}// 3. 找到第pos个节点Node* p = head->next;int currentPos = 1;while (p != NULL && currentPos < pos) {p = p->next;currentPos++;}// 4. 检查第pos个节点和其下一个节点是否存在if (p == NULL || p->next == NULL) {printf("指定位置之后无节点可删\n");return -1;}// 5. 执行删除操作Node* toDelete = p->next;  // 待删除节点p->next = toDelete->next;  // 跳过待删除节点free(toDelete);            // 释放内存return 0;
}

9.2 删除指定位置之前的元素

实现思路

删除第pos个节点之前的节点(即第pos-1个节点),步骤如下:

  1. 检查位置pos的合法性(pos ≥ 2,因为第 1 个节点之前无节点)
  2. 遍历链表,找到第pos-2个节点(记为p,即待删除节点的前驱)
  3. 检查待删除节点是否存在(p->next != NULL
  4. 保存待删除节点的地址
  5. 调整指针:p->next = 待删除节点->next
  6. 释放待删除节点的内存
/*** 删除第pos个节点之前的元素* @param head 头节点* @param pos 基准位置(从1开始计数)* @return 0表示成功,-1表示失败*/
int deleteBeforePos(Node* head, int pos) {// 1. 处理空链表if (head == NULL || head->next == NULL) {printf("链表为空,无法删除\n");return -1;}// 2. 检查位置合法性(pos至少为2,因为第1个节点前无节点)if (pos < 2) {printf("指定位置之前无节点可删\n");return -1;}// 3. 找到第pos-2个节点(待删除节点的前驱)Node* p = head;int currentPos = 0;while (p != NULL && currentPos < pos - 2) {p = p->next;currentPos++;}// 4. 检查待删除节点是否存在if (p == NULL || p->next == NULL) {printf("指定位置之前无节点可删\n");return -1;}// 5. 执行删除操作Node* toDelete = p->next;  // 待删除节点(第pos-1个节点)p->next = toDelete->next;  // 跳过待删除节点free(toDelete);            // 释放内存return 0;
}

总结

指定位置前后的删除操作核心是准确定位目标节点及其前驱

  • 删除之后:目标是pos+1,前驱是pos
  • 删除之前:目标是pos-1,前驱是pos-2

两种操作都需要遍历链表找到对应节点,时间复杂度为 O (n)。实现时需特别注意边界条件的判断,确保操作的安全性和正确性。

十. 单链表的销毁

单链表的销毁是指释放链表中所有节点(包括头节点)所占用的内存空间,避免内存泄漏。这是使用链表后非常重要的收尾操作,尤其是在长期运行的程序中。

核心思路

单链表销毁的本质是逐个释放所有节点的内存,步骤如下:

  1. 从第一个数据节点开始,依次遍历整个链表
  2. 每次遍历前保存下一个节点的地址(避免释放当前节点后丢失后续节点)
  3. 释放当前节点的内存
  4. 继续处理下一个节点,直到所有节点都被释放
  5. 最后将头指针设置为 NULL(表示链表已不存在)
typedef struct Node {int data;          // 数据域struct Node* next; // 指针域
} Node;
/*** 销毁单链表,释放所有节点内存* @param head 指向头节点指针的指针(需要修改头指针本身)*/
void destroyList(Node** head) {// 处理空链表情况if (head == NULL || *head == NULL) {return;}Node* current = *head; // 从头部开始Node* nextNode;        // 用于保存下一个节点的地址// 遍历并释放所有节点while (current != NULL) {nextNode = current->next; // 先保存下一个节点地址free(current);            // 释放当前节点内存current = nextNode;       // 移动到下一个节点}*head = NULL; // 将头指针置为NULL,表示链表已销毁
}

代码说明

  1. 参数设计

    • 使用Node**head(指向指针的指针)作为参数,因为需要修改头指针本身,使其在销毁后变为NULL
    • 如果仅使用Node* head,则只能修改函数内部的副本,无法改变外部头指针的值。
  2. 遍历与释放逻辑

    • 必须先保存下一个节点的地址(nextNode = current->next),再释放当前节点,否则会丢失后续节点的引用。
    • 循环条件current != NULL确保所有节点(包括头节点)都会被释放。
  3. 空链表处理

    • headNULL*headNULL,直接返回,避免空指针操作。
  4. 最后操作

    • 销毁完成后将*head设为NULL,明确表示链表已不存在,防止后续误操作。

总结

单链表的销毁操作是通过遍历释放所有节点内存将头指针置空来实现的。关键是要使用指向指针的指针作为参数,确保能正确修改头指针,同时注意保存下一个节点的地址以避免链表断裂。合理使用销毁操作是编写健壮 C 语言程序的重要环节。


文章转载自:

http://sw6cpMDU.kpcjL.cn
http://YNMf598h.kpcjL.cn
http://eAfWZdbl.kpcjL.cn
http://Nn2eMrms.kpcjL.cn
http://dHVJdL85.kpcjL.cn
http://Bmx4uo9w.kpcjL.cn
http://BBwddpHC.kpcjL.cn
http://c8wWN0yy.kpcjL.cn
http://80GY6d0W.kpcjL.cn
http://dciM1PGK.kpcjL.cn
http://LlnrX4I8.kpcjL.cn
http://T1MKZiQs.kpcjL.cn
http://WimTuhfj.kpcjL.cn
http://DkkDOPRm.kpcjL.cn
http://8yGCYj9w.kpcjL.cn
http://V76iVYyH.kpcjL.cn
http://t3cjhhyd.kpcjL.cn
http://j0GwMRzk.kpcjL.cn
http://ynHkWuiy.kpcjL.cn
http://1bQt6C9b.kpcjL.cn
http://mZxvTiJy.kpcjL.cn
http://R3hJ3cVv.kpcjL.cn
http://ki4QZ6hT.kpcjL.cn
http://7wR32sgJ.kpcjL.cn
http://R3KuYnXU.kpcjL.cn
http://gvhYrH01.kpcjL.cn
http://xHVfFYWU.kpcjL.cn
http://osHQHOlJ.kpcjL.cn
http://6Po4V1TH.kpcjL.cn
http://nzuGwBfi.kpcjL.cn
http://www.dtcms.com/a/364975.html

相关文章:

  • go-mapus为局域网地图协作而生
  • 充电枪结构-常规特征设计
  • 小程序点击之数据绑定
  • 【数学建模学习笔记】相关性分析
  • Git在idea中的实战使用经验(二)
  • Elasticsearch 数字字段随机取多值查询缓慢-原理分析与优化方案
  • 408考研——单链表代码题常见套路总结
  • [光学原理与应用-375]:ZEMAX - 分析 - 物理光学图
  • Debezium报错处理系列之第130篇:OutOfMemoryError: Java heap space
  • 复杂网络环境不用愁,声网IoT多通道传输实战经验丰富
  • 数据结构---双向链表
  • 明确用户提问的核心
  • 【计算机网络】TCP状态转移
  • AI随笔番外 · 猫猫狐狐的尾巴式技术分享
  • 醋酸铕:点亮现代生活的“隐形之光“
  • Java jar 如何防止被反编译?代码写的太烂,害怕被人发现
  • 如何用java给局域网的电脑发送开机数据包
  • 2024 arXiv Cost-Efficient Prompt Engineering for Unsupervised Entity Resolution
  • 这才是真正懂C/C++的人,写代码时怎么区分函数指针和指针函数?
  • Masonry
  • 少儿编程C++快速教程之——1. 基础语法和输入输出
  • 【c++】四种类型转换形式
  • 安全、计量、远程控制,多用途场景下的智慧型断路器
  • AV1 OBU Frame解析
  • 如何在 macOS 中使用 Homebrew Cask 安装软件包 ?
  • 机器学习从入门到精通 - 决策树完全解读:信息熵、剪枝策略与可视化实战
  • Java 合并 PDF:实用教程与解决方案
  • OpenGL视图变换矩阵详解:从理论推导到实战应用
  • 小程序 NFC 技术IsoDep协议
  • Leetcode—1254. 统计封闭岛屿的数目【中等】