【数据结构】深入理解单链表与通讯录项目实现

文章目录

  • 一、单链表的概念及结构
    • 1.1 什么是单链表?
    • 1.2 节点的组成
    • 1.3 单链表的特点
  • 二、单链表的实现
    • 2.1 类型定义
    • 2.2 基础工具函数
      • 1. 链表打印函数
      • 2. 节点创建函数
    • 2.3 单链表的核心操作
      • (1)插入操作
        • 1. 尾插(SLTPushBack)
        • 2. 头插(SLTPushFront)
        • 3. 指定位置前插入(SLTInsert)
        • 4. 指定位置后插入(SLTInsertAfter)
      • (2)删除操作
        • 1. 尾删(SLTPopBack)
        • 2. 头删(SLTPopFront)
        • 3. 删除指定节点(SLTErase)
        • 4. 删除指定节点后的数据(SLTEraseAfter)
      • (3)其他常用操作
        • 1. 查找(SLTFind)
        • 2. 销毁链表(SLTDes)
    • 测试代码与运行结果
      • 二级指针的使用总结
  • 三、单链表的实际应用:实现通讯录
    • 3.1 数据结构设计
    • 3.2 核心功能实现
      • (1)初始化通讯录
      • (2)添加联系人
      • (3)删除联系人
      • (4)保存与销毁
  • 四、总结

单链表作为数据结构中的重要成员,在计算机科学领域有着广泛的应用。它凭借灵活的内存管理和高效的插入删除操作,成为许多算法和系统的基础组件。

一、单链表的概念及结构

1.1 什么是单链表?

单链表是一种物理存储结构非连续、非顺序的线性数据结构,其数据元素的逻辑顺序通过节点间的指针链接来实现。形象地说,单链表的结构类似于火车车厢:

  • 每节车厢(节点)独立存在
  • 车厢之间通过连接装置(指针)关联
  • 可以灵活地增加或移除车厢(节点)而不影响其他部分

1.2 节点的组成

单链表的每个节点包含两个关键部分:

  • 数据域:存储当前节点的数据(可以是整型、字符型、自定义类型等)
  • 指针域:保存下一个节点的地址(相当于"下一节车厢的钥匙")

在这里插入图片描述

用C语言结构体表示如下:

struct SListNode {int data;                  // 数据域(以整型为例)struct SListNode* next;    // 指针域,指向 next 节点
};

1.3 单链表的特点

  • 逻辑连续性:节点通过指针链接,在逻辑上形成连续的序列
  • 物理离散性:节点在内存中通常不连续存储,由操作系统动态分配
  • 动态性:无需预先分配固定大小的内存,可根据需要动态申请和释放节点
  • 遍历性:必须从表头开始,通过指针依次访问后续节点,无法随机访问

二、单链表的实现

2.1 类型定义

在实现操作函数前,首先需要定义单链表节点的数据类型和节点结构,具体如下:

typedef int SLTDataType; // 定义单链表存储的数据类型,此处为inttypedef struct SListNode
{SLTDataType data; // 节点存储的数据struct SListNode* next; // 指针,用于保存下一个节点的地址
}SLN;

这种定义方式允许我们灵活处理不同类型的数据,只需修改SLTDataType的定义即可。

2.2 基础工具函数

在实现核心操作前,我们需要先实现一些基础工具函数,用于节点创建和链表打印。

1. 链表打印函数

void SLTPrint(SLN* phead)
{SLN* pcur = phead;  // 遍历指针while (pcur != NULL){printf("%d -> ", pcur->data);pcur = pcur->next;  // 移动到下一个节点}printf("NULL\n");  // 链表结束标识
}

2. 节点创建函数

SLN* SLTBuyNode(SLTDataType x)
{SLN* newNode = (SLN*)malloc(sizeof(SLN));  // 分配节点内存if (newNode == NULL)  // 内存分配失败检查{perror("malloc fail!");exit(1);  // 异常退出程序}newNode->data = x;  // 初始化数据域newNode->next = NULL;  // 初始化指针域return newNode;  // 返回创建的新节点
}

功能:创建一个新节点,为其分配内存并初始化数据和指针域。使用malloc分配内存后必须检查是否分配成功。

2.3 单链表的核心操作

单链表的核心操作包括插入(头插、尾插、指定位置插)和删除(头删、尾删、指定位置删)两大类,下面我们逐一解析。

(1)插入操作

1. 尾插(SLTPushBack)

在这里插入图片描述

void SLTPushBack(SLN** pphead, SLTDataType x)
{assert(pphead);  // 确保二级指针不为空SLN* newNode = SLTBuyNode(x);  // 创建新节点// 空链表if (*pphead == NULL){*pphead = newNode;  // 新节点成为头节点}// 非空链表else{// 找到尾节点SLN* ptail = *pphead;while (ptail->next)  // 当 next不为NULL时继续移动{ptail = ptail->next;}ptail->next = newNode;  // 尾节点指向新节点}
}

为什么使用二级指针?

  • 当链表为空时,我们需要修改头指针本身(让它指向新节点),而不是修改头指针指向的内容
  • 一级指针只能修改指向的内容,二级指针才能修改指针本身

调用示例

SLN* plist = NULL;  // 初始化空链表
SLTPushBack(&plist, 1);  // 传入plist的地址
SLTPushBack(&plist, 2);
2. 头插(SLTPushFront)
  1. 创建新节点
  2. 新节点的next指针指向原来的头节点
  3. 更新头指针,使其指向新节点

在这里插入图片描述

void SLTPushFront(SLN** pphead, SLTDataType x)
{assert(pphead);  // 确保二级指针不为空SLN* newNode = SLTBuyNode(x);  // 创建新节点newNode->next = *pphead;  // 新节点指向原来的头节点*pphead = newNode;  // 新节点成为新的头节点
}
3. 指定位置前插入(SLTInsert)
  1. 找到pos的前一个节点prev
  2. prev的next指向新节点
  3. 让新节点的next指向pos在这里插入图片描述
void SLTInsert(SLN** pphead, SLN* pos, SLTDataType x)
{assert(pphead && *pphead && pos);  // 参数合法性检查// 如果pos是头节点,直接调用头插if (pos == *pphead){SLTPushFront(pphead, x);}else{SLN* prev = *pphead;  // 用于找到pos的前一个节点SLN* newNode = SLTBuyNode(x);  // 创建新节点// 找到pos的前一个节点while (prev->next != pos){prev = prev->next;}// 插入新节点prev->next = newNode;newNode->next = pos;}
}
4. 指定位置后插入(SLTInsertAfter)

在这里插入图片描述

void SLTInsertAfter(SLN* pos, SLTDataType x)
{assert(pos);  // 确保pos不为空SLN* newNode = SLTBuyNode(x);  // 创建新节点// 先连接新节点和pos的下一个节点newNode->next = pos->next;// 再连接pos和新节点pos->next = newNode;
}

注意:必须先将新节点连接到pos的下一个节点,再将pos连接到新节点,顺序不能颠倒。

(2)删除操作

1. 尾删(SLTPopBack)

在这里插入图片描述

void SLTPopBack(SLN** pphead)
{// 确保二级指针和链表不为空assert(pphead && *pphead);// 链表只有一个节点if ((*pphead)->next == NULL){free(*pphead);  // 释放头节点*pphead = NULL;  // 头指针置空}// 链表有多个节点的情况else{SLN* prev = *pphead;  // 记录尾节点的前一个节点SLN* ptail = *pphead;  // 用于找到尾节点// 找到尾节点while (ptail->next){prev = ptail;ptail = ptail->next;}free(ptail);  // 释放尾节点prev->next = NULL;  // 前节点的next置空}
}

注意

  • 必须找到尾节点的前一个节点
  • 释放尾节点内存后,要将前节点的next置空
  • 特殊处理只有一个节点的情况
2. 头删(SLTPopFront)

在这里插入图片描述

void SLTPopFront(SLN** pphead)
{assert(pphead && *pphead);  // 不能对空指针解引用,因此pphead不能为空// 链表不能为空,因此*pphead不能为空// 保存头节点的下一个节点SLN* next = (*pphead)->next;free(*pphead);  // 释放头节点*pphead = next;  // 更新头节点
}

注意:不能直接释放头节点后再找下一个节点,因为释放后指针就失效了,必须先保存下一个节点的地址。

3. 删除指定节点(SLTErase)
  1. 找到pos的前一个节点prev
  2. prev的next指向pos的next,跳过pos节点
  3. 释放pos节点的内存
    在这里插入图片描述
void SLTErase(SLN** pphead, SLN* pos)
{assert(pphead && *pphead && pos);  // 参数合法性检查// 如果pos是头节点,直接调用头删if (pos == *pphead){SLTPopFront(pphead);}else{SLN* prev = *pphead;  // 找到pos的前一个节点while (prev->next != pos){prev = prev->next;}prev->next = pos->next;  // 跳过pos节点free(pos);  // 释放pos节点内存pos = NULL;  // 避免野指针}
}
4. 删除指定节点后的数据(SLTEraseAfter)

删除指定节点pos后面的节点:
在这里插入图片描述

void SLTEraseAfter(SLN* pos)
{assert(pos && pos->next);  // 确保pos和pos->next不为空SLN* del = pos->next;  // 要删除的节点pos->next = del->next;  // pos指向删除的下一节点free(del);  // 释放内存del = NULL;  // 避免野指针
}

(3)其他常用操作

1. 查找(SLTFind)

查找指定数据的节点:遍历链表,返回第一个数据域等于x的节点地址,未找到则返回NULL

SLN* SLTFind(SLN* phead, SLTDataType x)
{SLN* pcur = phead;  // 遍历指针while (pcur)  // 遍历整个链表{if (pcur->data == x)  // 找到目标数据return pcur;  // 返回节点地址pcur = pcur->next;  // 移动到下一个节点}return NULL;  // 未找到返回NULL
}
2. 销毁链表(SLTDes)

释放链表所有节点的内存:

void SLTDes(SLN** pphead)
{assert(pphead && *pphead);  // 参数合法性检查SLN* pcur = *pphead;  // 遍历指针while (pcur){SLN* next = pcur->next;  // 保存下一个节点地址free(pcur);  // 释放当前节点pcur = next;  // 移动到下一个节点}*pphead = NULL;  // 头指针置空,避免野指针
}

注意:销毁链表时必须逐个释放每个节点的内存,不能直接释放头节点,否则会导致内存泄漏。

测试代码与运行结果

我们通过test函数测试上述所有操作:

void test()
{SLN* plist = NULL;  // 初始化空链表// 尾插测试SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);printf("尾插后: ");SLTPrint(plist);  // 1 -> 2 -> 3 -> 4 -> NULL// 头插测试SLTPushFront(&plist, 5);SLTPushFront(&plist, 6);printf("头插后: ");SLTPrint(plist);  // 6 -> 5 -> 1 -> 2 -> 3 -> 4 -> NULL// 尾删测试SLTPopBack(&plist);printf("尾删后: ");SLTPrint(plist);  // 6 -> 5 -> 1 -> 2 -> 3 -> NULL// 头删测试SLTPopFront(&plist);printf("头删后: ");SLTPrint(plist);  // 5 -> 1 -> 2 -> 3 -> NULL// 查找测试SLN* ret = SLTFind(plist, 5);if (ret != NULL)printf("找到了数据5的节点\n");// 指定位置前插入测试SLTInsert(&plist, ret, 99);printf("指定位置前插入后: ");SLTPrint(plist);  // 99 -> 5 -> 1 -> 2 -> 3 -> NULL// 指定位置后插入测试SLTInsertAfter(ret, 88);printf("指定位置后插入后: ");SLTPrint(plist);  // 99 -> 5 -> 88 -> 1 -> 2 -> 3 -> NULL// 删除指定节点测试SLTErase(&plist, ret);printf("删除指定节点后: ");SLTPrint(plist);  // 99 -> 88 -> 1 -> 2 -> 3 -> NULL// 删除指定节点后的数据测试SLN* ret1 = SLTFind(plist, 88);SLTEraseAfter(ret1);printf("删除指定节点后的数据后: ");SLTPrint(plist);  // 99 -> 88 -> 2 -> 3 -> NULL// 销毁链表SLTDes(&plist);printf("销毁链表后: ");SLTPrint(plist);  // NULL
}

二级指针的使用总结

在单链表操作中,是否需要使用二级指针遵循以下原则:

  • 当操作需要修改头指针的值时(如头插、头删、尾插空链表、销毁链表等),必须使用二级指针
  • 当操作不需要修改头指针的值时(如打印、查找、指定位置后插入等),使用一级指针即可

三、单链表的实际应用:实现通讯录

单链表的动态性使其非常适合实现通讯录功能,以下是核心实现思路:

3.1 数据结构设计

// 联系人信息结构体
typedef struct PersonInfo {char name[NAME_MAX];   // 姓名char sex[SEX_MAX];     // 性别int age;               // 年龄char tel[TEL_MAX];     // 电话char addr[ADDR_MAX];   // 地址
} PeoInfo;// 链表节点定义(数据域为联系人信息)
typedef struct SListNode {PeoInfo data;           // 存储联系人信息struct SListNode* next; // 指针域
} SLTNode;
typedef struct SListNode contact;  // 通讯录类型别名

3.2 核心功能实现

(1)初始化通讯录

从本地文件加载历史数据到链表:

void InitContact(contact** con) {LoadContact(con);  // 从 contact.txt 读取数据
}

(2)添加联系人

通过尾插法将新联系人插入链表:

void AddContact(contact** con) {PeoInfo info;// 输入联系人信息(姓名、性别、年龄等)scanf("%s %s %d %s %s", info.name, info.sex, &info.age, info.tel, info.addr);SLTPushBack(con, info);  // 尾插操作
}

(3)删除联系人

根据姓名查找并删除节点:

void DelContact(contact** con) {char name[NAME_MAX];printf("请输入要删除的姓名:");scanf("%s", name);contact* pos = FindByName(*con, name);  // 查找节点if (pos) {SLTErase(con, pos);  // 删除节点printf("删除成功!\n");}
}

(4)保存与销毁

退出时将数据保存到文件并销毁链表:

void DestroyContact(contact** con) {SaveContact(*con);  // 保存数据到文件SListDestroy(con);  // 销毁链表,释放内存
}

四、总结

单链表作为一种基础且灵活的数据结构,其核心价值在于:

  • 动态内存管理:无需预先分配固定大小的空间,节省内存
  • 高效插入删除:在已知节点位置时,插入删除操作时间复杂度为 O(1)
  • 广泛的适用性:作为子结构支撑哈希表、图等复杂数据结构,也可直接用于实现通讯录、任务队列等应用

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

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

相关文章

《Python学习之字典(一):基础操作与核心用法》

坚持用 清晰易懂的图解 代码语言,让每个知识点变得简单! 🚀呆头个人主页详情 🌱 呆头个人Gitee代码仓库 📌 呆头详细专栏系列 座右铭: “不患无位,患所以立。” Python学习之字典(…

[安洵杯 2019]Attack

BUUCTF在线评测BUUCTF 是一个 CTF 竞赛和训练平台,为各位 CTF 选手提供真实赛题在线复现等服务。https://buuoj.cn/challenges#[%E5%AE%89%E6%B4%B5%E6%9D%AF%202019]Attack流量分析题,浏览的时候发现攻击者上传信息页面, 直接搜索 flag 就…

复合机器人食品分拣生产线:一体化控制系统引领高效柔性新食代

在食品工业高速发展的今天,面对种类繁多、形态各异的原料分拣需求,以及日益严格的卫生安全与效率要求,传统的固定式分拣设备已难以胜任。复合机器人食品分拣生产线凭借其融合移动(AMR)与操作(机械臂&#x…

二十七、动态SQL

动态SQL介绍动态SQL&#xff1a;if与where标签动态案例-动态更新EmpMapper&#xff08;接口&#xff09;中对应代码块 //动态更新员工public void update2(Emp emp);EmpMapper.xml中对应代码块 <!-- 动态更新员工--><update id"update2">update emp<s…

AI可行性分析:数据×算法×反馈=成功

3.1 从场景到AI可行性分析:需求拆解为“数据+算法+反馈” 核心公式: AI可行性 = 数据可获得性 算法适配性 反馈闭环性 (任一要素为0则需求不可行) 一、传统需求 vs AI需求本质差异 需求文档对比(电商案例) 维度 传统需求文档(购物车功能) AI需求文档(商品推荐系…

【图论】分层图 / 拆点

大多数都是同一个套路&#xff0c;将图拆开成几个图&#xff0c;每一层都对应着一个不同的状态&#xff0c;比如把到点 i 的状态拆成经过了 j 次操作所得的 xx 结果&#xff0c;一般数据不会很大 目前遇到的可分为 3 类&#xff1a; ①.给你最多 k 次操作&#xff0c;求 xx 结…

VS Code配置MinGW64编译MATIO库

VS Code 使用 MinGW64 编译 C 代码并配置 MATIO 库的完整步骤 1. 安装 MSYS2 下载 MSYS2 访问 MSYS2 官网下载安装包&#xff08;选择 x86_64 版本&#xff09;默认安装路径&#xff1a;C:\msys64 更新 MSYS2 包数据库 打开 MSYS2 MinGW 64-bit&#xff08;注意不是 MSYS&…

【前端Vue】使用ElementUI实现表单中可选择可编辑的下拉框

由于项目在vue的开发框架下&#xff0c;因此使用ElementUI组件库进行实现。我希望可选择可编辑的下拉框右侧有跟下拉框一样的箭头&#xff0c;并且在未输入任何内容时&#xff0c;点击该框体会出现选择列表进行填充数据的选择&#xff0c;点击选中数据后列表消失&#xff0c;数…

每日五个pyecharts可视化图表-line:从入门到精通 (4)

欢迎来到pyecharts折线图系列的第四篇文章&#xff01;在前三篇中&#xff0c;我们已经掌握了多种折线图类型&#xff0c;包括基本折线图、平滑折线图、雨量流量关系图、多X轴折线图、堆叠区域图和阶梯图等。在本文中&#xff0c;我们将继续探索五种更高级的折线图类型&#xf…

MySQL中的字符串函数

目录 一、字符串【分割】函数&#xff1a;SUBSTRING_INDEX() SUBSTRING_INDEX函数 练习题 统计每种性别的人数 提取博客URL中的用户名 截取出年龄 SQL83 商品id数据清洗统计 SQL250 查找字符串中逗号出现的次数 二、字符串【截取】函数&#xff1a;SUBSTRING() 基本语…

CodeBuddy IDE深度体验:AI驱动的全栈开发新时代

在人工智能技术迅猛发展的今天&#xff0c;开发者工具正在经历一场深刻的变革。腾讯推出的CodeBuddy IDE作为全球首个“产设研一体”的AI全栈高级工程师工具&#xff0c;重新定义了开发者的日常工作流程。 从需求分析到设计、编码、部署&#xff0c;CodeBuddy通过AI能力将传统…

实现Android图片手势缩放功能的完整自定义View方案,结合了多种手势交互功能

主要功能特点&#xff1a;支持双指手势缩放图片&#xff0c;通过ScaleGestureDetector实现平滑的缩放效果25双击图片可切换初始大小和中等放大比例16使用Matrix进行图像变换&#xff0c;保持缩放中心点为手势焦点位置57自动缩放动画通过Runnable实现渐进式变化1限制最小和最大缩…

uni-app实战教程 从0到1开发 画图软件 (橡皮擦)

一、本期内容简述1. 开发内容上一期&#xff0c;我们一起学习了如何进行绘画&#xff0c;本期我们将学习如何擦除我们所绘画的内容&#xff0c;也就是“橡皮擦”功能。首先&#xff0c;我们应该明确需求&#xff0c;橡皮擦可以擦除掉我们绘画的内容。2. 开发需求所以开发需求&a…

《A Practical Guide to Building Agents》文档学习

《A Practical Guide to Building Agents》文档总结 该文档是一份面向产品和工程团队的实用指南&#xff0c;旨在帮助团队探索并构建首个基于大语言模型&#xff08;LLM&#xff09;的智能体&#xff08;Agent&#xff09;&#xff0c;提炼了大量客户部署经验&#xff0c;提供了…

OpenCV图像注册模块

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 注册模块实现了参数化图像配准。所实现的方法是直接对齐&#xff08;direct alignment&#xff09;&#xff0c;即&#xff0c;它直接使用像素值来…

模型驱动与分布式建模:技术深度与实战落地指南

摘要 在AI、云原生与全球化协作的大潮中&#xff0c;模型驱动架构&#xff08;MDA&#xff09;与分布式建模不再是概念&#xff0c;而是支撑复杂系统设计与持续演化的核心引擎。本文从元模型、模型转换引擎&#xff0c;到协同协议、冲突解决算法&#xff0c;再到AI辅助建模与自…

计算机基础速通--数据结构·图的基础应用二(基础图算法)

如有问题大概率是我的理解比较片面&#xff0c;欢迎评论区或者私信指正。 最近了解到了一个新的改变和提高自己的方法时刻记录不论多小的事情都记下&#xff0c;我目前用了4天&#xff0c;之前感觉一天天忙死但没啥收获&#xff0c;但是记录了之后知道自己的时间花在了哪里&…

设计模式-策略模式 Java

模式概述 策略模式是一种行为型设计模式&#xff0c;它通过定义一系列可互换的算法&#xff0c;并将每个算法封装成独立类&#xff0c;使客户端能够根据需要动态切换算法 简单代码示例 // 1. 抽象策略接口 interface PaymentStrategy {void pay(int amount); }// 2. 具体策略实…

【机器学习深度学习】客观评估训练程度

目录 前言 一、什么是客观评估&#xff1f; 二、客观评估的两大核心方法 1. 判别式评测&#xff08;Discriminative Evaluation&#xff09; 2. 生成式评测&#xff08;Generative Evaluation&#xff09; 三、为什么客观评估成本更高&#xff1f; 1.训练目标收紧 2.训…

Linux软件编程:线程间通信

目录 一、线程间通信基础 1. 概念 2. 通信基础&#xff1a;共享空间 二、互斥锁&#xff08;Mutex&#xff09; 1. 概念 2. 使用流程 3. 函数接口 三、死锁 1. 概念 2. 死锁产生的 4 个必要条件 3. 避免死锁的方法 四、信号量&#xff08;Semaphore&#xff09; 1…