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

数据结构 -- 单向链表的特点、操作函数

数据结构中的线性表:链式存储

我们先回忆一下线性表的基本概念:它是一种 “一对一” 的有序数据结构,比如排队的人、一串数字(1,2,3,4),每个元素(除了第一个和最后一个)都有唯一的前一个和后一个元素。

链式存储,是线性表的一种实现方式,它和我们之前可能听过的 “顺序存储”(用数组实现)最大的区别是:数据不一定要存在连续的内存空间里

一、链式存储的核心:“节点” 和 “指针”

想象一下生活中的场景:

  • 你有一串钥匙,每把钥匙(相当于 “数据”)都用绳子串起来,前一把钥匙的绳子连着后一把钥匙 —— 这串钥匙就是 “顺序存储”(数据连续)。
  • 但如果是一串散落的珠子,每个珠子上除了有一颗珍珠(数据),还有一个小钩子,钩子勾着下一个珠子 —— 这就是 “链式存储”。

在链式存储里:

  • 节点:每个元素被包装成一个 “节点”,里面包含两部分:

    1. 实际的数据(比如一个人的姓名、年龄);
    2. 一个 “指针”(地址),指向它的下一个节点。
  • 链表:由多个这样的节点通过指针连接起来,就形成了 “链表”。

二、链表的特点(和数组对比)
  1. 内存不连续
    数组的元素必须存在连续的内存空间里,比如数组 [1,2,3] 占用地址 0x100、0x101、0x102
    链表的节点可以分散在内存的各个地方,只要通过指针能找到下一个节点就行。

  2. 大小灵活
    数组一旦创建,大小就固定了(比如定义 int arr[5] 就只能存 5 个元素),想扩容很麻烦。
    链表的大小可以随时变化,想加元素就新创建一个节点,勾在链表上;想删元素就把指针 “断开”,不需要提前固定大小。

  3. 插入 / 删除方便
    数组中插入一个元素,后面的所有元素都要往后挪(比如在 [1,2,3] 的第 2 个位置插 4,变成 [1,4,2,3],2 和 3 都要移动)。
    链表中插入元素,只需要改两个指针:新节点的指针指向 “下一个节点”,前一个节点的指针指向 “新节点”,其他元素不用动。
    (删除同理,只需要把前一个节点的指针跳过要删的节点,指向它的下一个节点就行)

  4. 访问元素效率低
    数组可以通过下标直接访问元素(比如 arr[2] 直接定位到第 3 个元素)。
    链表没有下标,想访问第 n 个元素,必须从第一个节点开始,顺着指针一个个往后找(像 “顺着钩子找珠子”),所以找元素比数组慢。

三、常见的链表类型
  1. 单链表
    每个节点只有一个指针,指向后一个节点,最后一个节点的指针为 NULL(表示没有下一个)。
    比如:A -> B -> C -> NULL

  2. 双向链表
    每个节点有两个指针:一个指向前一个节点,一个指向后一个节点。
    比如:NULL <- A <-> B <-> C -> NULL
    优点:既能往后找,也能往前找,删除时更方便(不用回头找前一个节点)。

  3. 循环链表
    最后一个节点的指针不指向 NULL,而是指向第一个节点,形成一个环。
    比如:A -> B -> C -> A
    优点:适合需要 “循环遍历” 的场景(比如多人轮流报数)。

单向链表的操作函数

linklist.h
#ifndef _LINKLIST_H_
#define _LINKLIST_H_/*** @file linklist.h* @brief 人员信息链表的头文件,定义了数据结构和相关操作函数** 本文件声明了用于存储人员信息的链表数据结构,* 以及对该链表进行创建、插入、删除、查找、修改等操作的函数接口*/// 定义人员信息的结构体
typedef struct person
{char name[32];  // 姓名,最多31个字符(包含字符串结束符'\0')char sex;       // 性别,通常用'M'表示男性,'F'表示女性int age;        // 年龄int score;      // 分数,可根据实际需求调整含义
} DATATYPE;         // 定义结构体别名,方便作为链表数据类型使用// 链表节点的结构体
typedef struct node
{DATATYPE data;       // 数据域:存储实际的数据(人员信息)struct node *next;   // 指针域:指向链表中下一个节点的地址,最后一个节点为NULL
} LinkNode;             // 链表节点的别名// 链表管理的结构体
typedef struct list
{LinkNode *head;  // 指向链表第一个节点的指针,空链表时为NULLint clen;        // 当前链表中有效节点(元素)的个数
} LinkList;          // 链表管理结构的别名/*** @brief 创建一个新的空链表* @return 成功返回指向新链表的指针,失败返回NULL*/
LinkList *CreateLinkList();/*** @brief 在链表头部插入一个新元素* @param list 指向链表的指针* @param data 指向要插入的人员信息数据的指针* @return 成功返回0,失败返回-1*/
int InsertHeadLinkList(LinkList *list, DATATYPE *data);/*** @brief 在链表尾部插入一个新元素* @param list 指向链表的指针* @param data 指向要插入的人员信息数据的指针* @return 成功返回0,失败返回-1*/
int InsertTailLinkList(LinkList *list, DATATYPE *data);/*** @brief 在链表指定位置插入一个新元素* @param list 指向链表的指针* @param data 指向要插入的人员信息数据的指针* @param pos 插入位置(从0开始计数)* @return 成功返回0,失败返回-1*/
int InsertPosLinkList(LinkList* list, DATATYPE* data, int pos);/*** @brief 遍历并显示链表中所有元素的信息* @param list 指向链表的指针* @return 成功返回显示的元素个数,失败返回-1*/
int ShowLinkList(LinkList *list);/*** @brief 根据姓名查找链表中的元素* @param list 指向链表的指针* @param name 要查找的姓名* @return 成功返回指向找到的节点的指针,失败返回NULL*/
LinkNode *FindLinkList(LinkList *list, char *name);/*** @brief 根据姓名删除链表中的元素* @param list 指向链表的指针* @param name 要删除的元素的姓名* @return 成功返回0,失败返回-1*/
int DeleteLinkList(LinkList *list, char *name);/*** @brief 根据姓名修改链表中元素的信息* @param list 指向链表的指针* @param name 要修改的元素的姓名* @param data 指向新的人员信息数据的指针* @return 成功返回0,失败返回-1*/
int ModifyLinkList(LinkList *list, char *name, DATATYPE *data);/*** @brief 销毁链表,释放所有节点占用的内存* @param list 指向链表的指针* @return 成功返回0,失败返回-1*/
int DestroyLinkList(LinkList *list);/*** @brief 判断链表是否为空* @param list 指向链表的指针* @return 链表为空返回1,不为空返回0,参数错误返回-1*/
int IsEmptyLinkList(LinkList *list);/*** @brief 获取链表中元素的个数* @param list 指向链表的指针* @return 成功返回元素个数,失败返回-1*/
int GetSizeLinkList(LinkList *list);#endif  // !_LINKLIST_H_  // 预处理指令结束,注意原代码中的"!1"是笔误,已修正
linklist.c
#include "linklist.h"   // 包含链表头文件,使用其中定义的结构体和函数声明
#include <stdio.h>      // 标准输入输出库,用于打印信息
#include <stdlib.h>     // 标准库,提供malloc、free等内存管理函数
#include <string.h>     // 字符串处理库,提供memcpy、strcmp等函数/*** @brief 创建一个空的链表* * 为链表管理结构体分配内存,并初始化头指针和节点计数* * @return 成功返回指向链表的指针,内存分配失败返回NULL*/
LinkList *CreateLinkList()
{  // 为链表管理结构(LinkList)分配内存LinkList *ll = malloc(sizeof(LinkList));if (NULL == ll)  // 检查内存分配是否成功{perror("CreateLinkList malloc");  // 打印错误信息(如"CreateLinkList malloc: 内存不足")return NULL;}// 初始化空链表:头指针指向NULL(无节点),有效节点数为0ll->head = NULL;ll->clen = 0;return ll;  // 返回创建的空链表
}/*** @brief 在链表头部插入新节点* * 创建新节点并插入到链表的头部(头插法),新节点成为新的头节点* * @param list 指向链表的指针(链表管理结构)* @param data 指向要插入的人员信息数据的指针* @return 成功返回0,内存分配失败返回1*/
int InsertHeadLinkList(LinkList *list, DATATYPE *data)
{// 为新节点(LinkNode)分配内存LinkNode *newnode = malloc(sizeof(LinkNode));if (NULL == newnode)  // 检查内存分配是否成功{perror("InsertHeadLinkList malloc");  // 打印错误信息return 1;  // 返回错误码}// 复制数据到新节点的数据域(将data指向的人员信息拷贝到newnode->data)memcpy(&newnode->data, data, sizeof(DATATYPE));newnode->next = NULL;  // 初始化新节点的指针域(暂时指向NULL)// 如果链表非空(有节点),新节点的next指向原头节点if (!IsEmptyLinkList(list)){newnode->next = list->head;}// 更新链表头指针为新节点(新节点成为新的头)list->head = newnode;list->clen++;  // 链表有效节点数加1return 0;  // 返回成功码
}/*** @brief 判断链表是否为空* * 通过链表的有效节点数判断链表状态* * @param list 指向链表的指针* @return 链表为空返回1(真),非空返回0(假)*/
int IsEmptyLinkList(LinkList *list)
{return 0 == list->clen;  // 节点数为0则为空链表
}/*** @brief 遍历并打印链表中所有节点的信息* * 从链表头节点开始,依次访问每个节点,打印人员信息(姓名、性别、年龄、分数)* * @param list 指向链表的指针* @return 始终返回0(成功)*/
int ShowLinkList(LinkList *list)
{LinkNode *tmp = list->head;  // 定义临时指针,从 head 开始遍历// 循环遍历所有节点:当 tmp 不为 NULL 时(未到链表尾部)while (tmp){// 打印当前节点的人员信息printf("name:%s sex:%c age:%d score:%d\n", tmp->data.name,   // 姓名tmp->data.sex,    // 性别tmp->data.age,    // 年龄tmp->data.score); // 分数tmp = tmp->next;  // 移动到下一个节点}return 0;  // 返回成功码
}/*** @brief 在链表尾部插入新节点* * 创建新节点并插入到链表的尾部(尾插法),若链表为空则直接调用头插法* * @param list 指向链表的指针* @param data 指向要插入的人员信息数据的指针* @return 成功返回0,内存分配失败返回1*/
int InsertTailLinkList(LinkList *list, DATATYPE *data)
{// 如果链表为空,直接调用头插法(尾部即头部)if (IsEmptyLinkList(list)){return InsertHeadLinkList(list, data);}else  // 链表非空,查找尾部节点{LinkNode *tmp = list->head;// 循环找到最后一个节点:最后一个节点的 next 为 NULLwhile (tmp->next){tmp = tmp->next;  // 移动到下一个节点}// 为新节点分配内存LinkNode *newnode = malloc(sizeof(LinkNode));if (NULL == newnode)  // 检查内存分配{perror("InsertTailLinkList malloc");  // 打印错误信息return 1;  // 返回错误码}// 复制数据到新节点memcpy(&newnode->data, data, sizeof(DATATYPE));newnode->next = NULL;  // 尾部节点的 next 始终为 NULL// 将原尾部节点的 next 指向新节点(新节点成为新的尾部)tmp->next = newnode;}list->clen++;  // 有效节点数加1return 0;  // 返回成功码
}/*** @brief 在链表指定位置插入新节点* * 支持在链表的头部(pos=0)、尾部(pos=链表长度)或中间位置插入节点* * @param list 指向链表的指针* @param data 指向要插入的人员信息数据的指针* @param pos 插入位置(从0开始计数,0为头部,n为长度时为尾部)* @return 成功返回0,位置无效或内存分配失败返回1*/
int InsertPosLinkList(LinkList *list, DATATYPE *data, int pos)
{int len = GetSizeLinkList(list);  // 获取当前链表的长度(有效节点数)// 检查插入位置是否合法:pos 必须 ≥0 且 ≤链表长度if (pos < 0 || pos > len){fprintf(stderr, "InsertPosLinkList pos error\n");  // 打印位置错误信息return 1;  // 返回错误码}// 插入位置为头部(pos=0),直接调用头插法if (0 == pos){return InsertHeadLinkList(list, data);}// 插入位置为尾部(pos=链表长度),调用尾插法else if (pos == len){return InsertTailLinkList(list, data);}// 插入位置为中间(0 < pos < len)else{LinkNode *tmp = list->head;// 移动 tmp 到目标位置的前一个节点(pos-1 位置)int off = pos - 1;  // 需要移动的次数while (off--)  // 循环 off 次,每次移动到下一个节点{tmp = tmp->next;}// 为新节点分配内存LinkNode *newnode = malloc(sizeof(LinkNode));if (NULL == newnode){perror("InsertposLinkList malloc");  // 打印错误信息return 1;}// 复制数据到新节点memcpy(&newnode->data, data, sizeof(DATATYPE));newnode->next = NULL;  // 临时初始化指针域// 插入新节点:// 1. 新节点的 next 指向 tmp 的下一个节点(原 pos 位置的节点)newnode->next = tmp->next;// 2. tmp 的 next 指向新节点(新节点插入到 tmp 之后)tmp->next = newnode;}list->clen++;  // 有效节点数加1return 0;  // 返回成功码
}/*** @brief 获取链表的有效节点数(长度)* * 直接返回链表管理结构中存储的节点计数* * @param list 指向链表的指针* @return 链表的有效节点数*/
int GetSizeLinkList(LinkList *list)
{return list->clen;  // 返回 clen 成员(已维护的节点数)
}/*** @brief 根据姓名查找链表中的节点* * 遍历链表,通过字符串比较匹配姓名,返回第一个匹配的节点指针* * @param list 指向链表的指针* @param name 要查找的姓名(字符串)* @return 找到返回对应节点的指针,未找到返回NULL*/
LinkNode *FindLinkList(LinkList *list, char *name)
{LinkNode *tmp = list->head;  // 从 head 开始查找// 遍历所有节点while (tmp){// 比较当前节点的姓名与目标姓名(strcmp返回0表示相等)if (0 == strcmp(tmp->data.name, name)){return tmp;  // 找到匹配节点,返回指针}tmp = tmp->next;  // 移动到下一个节点}return NULL;  // 遍历完链表未找到,返回NULL
}/*** @brief 根据姓名删除链表中的节点* * 支持删除头节点和中间节点,释放节点内存并调整链表连接* * @param list 指向链表的指针* @param name 要删除的节点对应的姓名* @return 成功返回0,链表为空返回1*/
int DeleteLinkList(LinkList *list, char *name)
{// 若链表为空,无法删除,打印错误信息if (IsEmptyLinkList(list)){fprintf(stderr, "DeleteLinkList empty list\n");return 1;}LinkNode *tmp = list->head;  // 从 head 开始查找// 情况1:删除的是头节点(头节点姓名匹配)if (0 == strcmp(tmp->data.name, name)){list->head = list->head->next;  // 头指针指向原头节点的下一个节点free(tmp);  // 释放原头节点的内存list->clen--;  // 有效节点数减1}// 情况2:删除的是中间节点或尾节点else{// 遍历查找目标节点的前一个节点(通过 tmp->next 匹配姓名)while (tmp->next){if (0 == strcmp(tmp->next->data.name, name)){LinkNode *del = tmp->next;  // 记录要删除的节点tmp->next = tmp->next->next;  // 跳过要删除的节点(连接前后)free(del);  // 释放删除节点的内存list->clen--;  // 有效节点数减1break;  // 完成删除,退出循环}tmp = tmp->next;  // 移动到下一个节点}}return 0;  // 返回成功码
}/*** @brief 根据姓名修改链表中节点的信息* * 先通过姓名查找节点,找到后更新节点的人员信息* * @param list 指向链表的指针* @param name 要修改的节点对应的姓名* @param data 指向新的人员信息数据的指针* @return 成功返回0,未找到节点返回1*/
int ModifyLinkList(LinkList *list, char *name, DATATYPE *data)
{// 查找目标节点LinkNode *tmp = FindLinkList(list, name);if (tmp == NULL)  // 未找到节点{printf("modify error: node not found\n");  // 打印错误信息return 1;  // 返回错误码}// 复制新数据到找到的节点(覆盖原有数据)memcpy(&tmp->data, data, sizeof(DATATYPE));return 0;  // 返回成功码
}/*** @brief 销毁链表,释放所有分配的内存* * 依次释放链表中所有节点的内存,最后释放链表管理结构的内存* * @param list 指向链表的指针* @return 始终返回0(成功)*/
int DestroyLinkList(LinkList *list)
{LinkNode *tmp = list->head;  // 从 head 开始释放// 循环释放所有节点while(tmp){list->head = list->head->next;  // 头指针后移(保存下一个节点的地址)free(tmp);  // 释放当前节点的内存tmp = list->head;  // 移动到下一个节点}free(list);  // 释放链表管理结构的内存return 0;  // 返回成功码
}
main.c
#include <stdio.h>
#include "linklist.h"  // 包含链表操作的头文件,使用其中定义的结构体和函数/*** @brief 主函数,测试链表的各种操作* @param argc 命令行参数个数(未使用)* @param argv 命令行参数数组(未使用)* @return 程序执行成功返回0*/
int main(int argc, char** argv)
{// 创建一个空链表LinkList* ll = CreateLinkList();// 初始化人员信息数组,存储5个人员的姓名、性别、年龄、分数DATATYPE data[] = {{"zhangsan", 'm', 20, 80},   // 数据0:张三,男,20岁,80分{"lisi", 'f', 23, 84},       // 数据1:李四,女,23岁,84分{"wangmaizi", 'f', 32, 90},  // 数据2:王麦子,女,32岁,90分{"guanerge", 'm', 50, 91},   // 数据3:关二哥,男,50岁,91分{"liubei", 'm', 51, 92},     // 数据4:刘备,男,51岁,92分};// 以下是头插法测试代码(已注释)// InsertHeadLinkList(ll, &data[0]);  // 头部插入张三// InsertHeadLinkList(ll, &data[1]);  // 头部插入李四(成为新头)// InsertHeadLinkList(ll, &data[2]);  // 头部插入王麦子(成为新头)// ShowLinkList(ll);  // 显示头插后的链表// 测试尾插法:从链表尾部插入数据printf("-----------insert tail--------------------\n");InsertTailLinkList(ll, &data[0]);  // 尾部插入张三(链表为空时等同于头插)InsertTailLinkList(ll, &data[1]);  // 尾部插入李四(张三之后)InsertTailLinkList(ll, &data[2]);  // 尾部插入王麦子(李四之后)ShowLinkList(ll);  // 显示尾插后的链表内容// 测试指定位置插入:在索引1的位置插入关二哥printf("-----------insert pos--------------------\n");InsertPosLinkList(ll, &data[3], 1);  // 插入位置1(原张三之后、李四之前)ShowLinkList(ll);  // 显示插入后的链表内容// 测试查找功能:查找不存在的姓名"zhang1san"char want_name[] = "zhang1san";  // 要查找的姓名(故意写错,确保不存在)LinkNode* tmp = FindLinkList(ll, want_name);if (NULL == tmp){printf("cant find %s\n", want_name);  // 未找到时提示}else{printf("name:%s score:%d\n", tmp->data.name, tmp->data.score);  // 找到时打印信息}// 测试删除功能:删除姓名为"lisi"的节点printf("-----------del--------------------\n");DeleteLinkList(ll, "lisi");  // 删除李四ShowLinkList(ll);  // 显示删除后的链表内容// 测试修改功能:将"wangmaizi"的信息修改为刘备的数据printf("-----------modify--------------------\n");ModifyLinkList(ll, "wangmaizi", &data[4]);  // 王麦子的信息替换为刘备ShowLinkList(ll);  // 显示修改后的链表内容// 销毁链表,释放所有内存(防止内存泄漏)DestroyLinkList(ll);return 0;
}

运行结果:

尾插法插入 3 个节点
依次插入zhangsan、lisi、wangmaizi,链表顺序为插入顺序:
plaintext
-----------insert tail--------------------
name:zhangsan sex:m age:20 score:80
name:lisi sex:f age:23 score:84
name:wangmaizi sex:f age:32 score:90指定位置插入节点
在索引 1 的位置插入guanerge(原zhangsan之后),新顺序为:
zhangsan → guanerge → lisi → wangmaizi
plaintext
-----------insert pos--------------------
name:zhangsan sex:m age:20 score:80
name:guanerge sex:m age:50 score:91
name:lisi sex:f age:23 score:84
name:wangmaizi sex:f age:32 score:90查找不存在的节点
查找zhang1san(不存在),输出提示:
plaintext
cant find zhang1san删除节点
删除lisi后,链表顺序变为:
zhangsan → guanerge → wangmaizi
plaintext
-----------del--------------------
name:zhangsan sex:m age:20 score:80
name:guanerge sex:m age:50 score:91
name:wangmaizi sex:f age:32 score:90修改节点信息
将wangmaizi的信息替换为liubei的数据,结果为:
plaintext
-----------modify--------------------
name:zhangsan sex:m age:20 score:80
name:guanerge sex:m age:50 score:91
name:liubei sex:m age:51 score:92  // 原wangmaizi已被修改为liubei的信息

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

相关文章:

  • 使用segment-anything将目标检测label转换为语义分割label
  • 数据结构:二叉树oj练习
  • 实现进度条
  • 【大模型早期融合的非标记化架构】
  • 学习strandsagents的http_request tool
  • 【上升跟庄买入】副图/选股指标,动态黄色线由下向上穿越绿色基准线时,发出买入信号
  • Ubuntu 20 各种网卡配置IP的方法
  • 【PyTorch】多对象分割项目
  • 别再手动处理字符串!Python 正则表达式实战手册(入门到精通)
  • 【深度学习新浪潮】Meta 开源最新视觉大模型 DINOv3,该模型有哪些技术亮点?
  • 【数据结构】使用队列解决二叉树问题
  • CentOS安装SNMPWalk
  • C++高频知识点(二十二)
  • 算法题Day3
  • 理解MCP:开发者的新利器
  • 从零开始理解一个复杂的 C++/CUDA 项目 Makefile
  • React学习(六)
  • 梅森公式计算传递函数及结构图转换为信号流图过程
  • STM32-FreeRTOS快速入门指南(中)
  • HJ3 明明的随机数
  • 数据结构——双链表
  • 人工智能细分方向全景图:从入门到专精的技术路径
  • AI出题人给出的Java后端面经(十⑨)(日更)
  • 零成本上线个人网站 | Cloudflare Pages 全流程实战指南
  • A股大盘数据-20250819 分析
  • redis基础----通用命令
  • 脑电分析——ICA原理、ICALabel成分与伪迹之间一对多的关系
  • 从合规到主动免疫:大模型内容风控的创新与实践
  • 【PyTorch】单对象分割项目
  • Seata笔记