嵌入式学习日志——数据结构(一)

1 学习单向链表

1 数据结构与算法的关系

数据结构 描述数据的存储组织形式与操作逻辑 ,算法是 解决问题的步骤流程(操作数据的方法),二者协同支撑程序设计,是程序的 "骨架" 与 "灵魂"。

1.1 代码质量与效率的衡量(复杂度分析)

1、核心概念

  • 空间复杂度 :程序运行时 临时占用的存储空间 随数据量增长的趋势(关注内存开销)。

  • 时间复杂度 :程序 执行时间 随数据量(问题规模 n)增长的趋势(关注运行效率),反映算法的 "耗时增速"。

2、常见时间复杂度(由优到劣排序)

复杂度 含义与典型场景 效率对比(数据量大时)
O(1) 执行时间与数据量无关(如数组随机访问) 最优,耗时恒定
O(logn) 随数据量增长,耗时以 "对数级" 放缓(如二分查找) 次优,增速远慢于线性
O(n) 耗时随数据量线性增长(如数组遍历) 基础线性,数据量大时压力渐显
O(nlogn) 耗时增长介于线性与多项式之间(如归并排序) 优于多项式复杂度,适合较大数据规模
O(n2) 耗时随数据量平方级增长(如冒泡排序) 数据量大时急剧变慢,仅适用于小规模
O(n3) 耗时随数据量立方级增长(如三重嵌套循环) 比 O(n2) 更差,极少见实用场景
O(2n) 耗时指数级爆炸(如暴力枚举所有子集) 数据量稍大即无法运行,仅理论探讨

3、复杂度的本质:趋势与对比

  • 核心逻辑 :复杂度描述的是 "数据量增长时,耗时 / 空间的增长趋势",不是实际执行时间。

  • 对比示例 (数据量 n 增大时):

    • O(logn)≪O(n)(如 n=1000,log2​n≈10,耗时差距悬殊)

    • O(nlogn)<O(n2)(如 n=1000,nlogn≈10000,n2=1000000 )

1.2 数据结构的分类

1、逻辑结构(数据元素的 "逻辑关系")

类型 特点与典型结构 应用场景
线性结构 元素 "一对一" 排列(如数组、链表、队列) 需顺序 / 依次处理数据(如排队系统)
非线性结构 元素 "一对多" 或 "多对多" 关系 处理复杂关联数据
- 树结构 层级化 "一对多"(如二叉树、红黑树) 需快速查找、分层管理(如文件系统)
- 图结构 灵活 "多对多"(如社交网络关系图) 需表达复杂关联(如地图路径规划)

2、存储结构(数据在内存中的 "物理存储方式")

类型 特点与优缺点 典型结构
顺序存储 元素连续存放(依赖数组) 优点:随机访问快(通过下标 O(1)); 缺点:插入 / 删除需移动元素(O(n) 耗时)
链式存储 元素通过 "指针 / 引用" 分散连接(如链表) 优点:插入 / 删除只需修改指针(O(1)); 缺点:随机访问需遍历(O(n) 耗时)
散列存储 用哈希函数映射存储位置(如哈希表 HashMap 优点:插入、查询、删除接近 O(1); 缺点:需处理哈希冲突,占用额外空间
索引存储 建立 "索引表" 加速查找(如数据库索引) 优点:大幅提升查询效率; 缺点:占用额外空间维护索引,插入 / 删除需同步更新索引

2 链表

2.1链表与数组对比

类型 存储特性 插入 / 删除效率 访问效率 空间利用
数组 连续内存,需预分配固定空间 O(n)(需移动元素) O(1)(随机访问) 无碎片但可能有 "闲置空间浪费"
链表 离散内存,动态按需分配节点 O(1)(改指针即可) O(n)(需遍历) 可利用小碎片,但额外存储指针(有空间开销)
  • 数组优势:适合 "读多写少" 场景(如高频查询);

  • 链表优势:适合 "写多读少" 场景(如高频插入、删除)。

2.2 链表分类(按结构)

1、单向链表

  • 结构 :节点含 数据域 + 指向下一节点的指针只能单向遍历(从头→尾);

  • 示意图node1 → node2 → node3 → NULL(尾节点指针为 NULL )。

2、双向链表

  • 结构 :节点含 数据域 + 前驱指针(prev) + 后继指针(next)支持双向遍历(可向前、向后访问);

  • 优势:删除节点时,若已知节点位置,无需遍历找前驱(比单向链表更高效)。

3、循环链表

  • 结构 :尾节点指针指向头节点(形成环形);

  • 典型场景:Linux 内核 "任务调度链表"(循环遍历所有任务,无明确首尾)。

3 单向链表操作(C 语言实现)

3.1 核心操作

增(插入节点)、删(删除节点)、改(修改数据)、查(查找节点)、创建(初始化链表)、销毁(释放内存)。

3.2 链表节点定义(代码优化版)

// 1. 定义数据类型(灵活适配实际需求,如 int、float 等)

typedef int DataType;

// 2. 定义链表节点结构体

typedef struct Node {

DataType data; // 数据域:存储实际数据

struct Node *next; // 指针域:存放下一个节点的地址(连接作用)

} LinkNode;

关键说明

  • DataType:通过 typedef 抽象数据类型,后续若需修改数据类型(如换 float ),只需改一行代码;

  • next 指针:占 8Byte(64 位系统),是链表 "连接" 的核心。

3.3 "头节点" 设计(避坑关键)

  • 有头链表

    第一个节点不存实际数据 (作 "哨兵节点"),作用是统一操作逻辑(插入、删除时,无需判断 "链表是否为空")。

LinkNode *head = (LinkNode*)malloc(sizeof(LinkNode));

head->next = NULL; // 初始时,链表无实际节点

  • 无头链表(需注意边界):

    第一个节点存储实际数据 ,操作时需区分 "空链表"(head == NULL )和 "非空链表",容易出错。

3.4 单向链表操作

1、头文件规范(避重复定义)

// linklist.h

#ifndef LINKLIST_H // 标准保护宏(比 LINKLIST_H 更简洁)

#define LINKLIST_H

#include <stdio.h>

#include <stdlib.h> // malloc/free 依赖

// 数据类型抽象(灵活替换)

typedef int DataType;

// 链表节点结构

typedef struct Node {

DataType data; // 数据域

struct Node *next; // 指针域(指向下一节点)

} LinkNode;

// 函数声明

LinkNode* createEmptyList(); // 创建空链表(返回头节点)

void insertHead(LinkNode *head, DataType data); // 头插法

void destroyList(LinkNode *head); // 销毁链表(释放内存)

#endif

  • 保护宏用 LINKLIST_H(简洁且避免与系统宏冲突);

  • 头文件直接包含 stdlib.hmalloc 依赖,防止 .c 文件漏引报错)。

2、空链表创建

// linklist.c

#include "linklist.h"

LinkNode* createEmptyList()

{

LinkNode *pHead = (LinkNode*)malloc(sizeof(LinkNode));

if (pHead == NULL)

{

// 错误细化:区分"内存不足"还是"其他错误"

perror("fail to malloc");

return NULL;

}

// 空链表:头节点 next 指向 NULL(无实际节点)

pHead->next = NULL;

return pHead;

}

  • 强调 pHead->next = NULL(空链表的核心标志)。

3、头插法实现

void insertHead(LinkNode *head, DataType data)

{

// 1. 申请新节点内存

LinkNode *pNew = (LinkNode*)malloc(sizeof(LinkNode));

if (pNew == NULL)

{

perror("fail to malloc");

return;

}

// 2. 赋值:数据 + 指针

pNew->data = data;

// 关键逻辑:新节点指向原头节点的后继

pNew->next = head->next;

// 头节点指向新节点(完成插入)

head->next = pNew;

}

  • 必须先让 pNew->next = head->next,再修改 head->next(否则原链表会 "断开")。

4、尾插法实现

// 尾插法:在链表末尾插入新节点

// 参数:phead - 链表头节点(有头链表),tmpdata - 待插入的数据

// 返回值:0 - 成功,-1 - 失败

int insert_tail_linklist(linknode *phead, datatype tmpdata)

{

// 1. 创建新节点

linknode *ptmpnode = malloc(sizeof(linknode));

if (ptmpnode == NULL)

{

// 更明确的错误提示(perror会附加系统错误信息)

perror("fail to malloc");

return -1;

}

// 2. 初始化新节点数据和指针

ptmpnode->data = tmpdata;

ptmpnode->pnext = NULL; // 尾节点的next必须为NULL

// 3. 找到链表末尾(最后一个节点的next为NULL)

linknode *pprenode = phead;

// 循环条件:当前节点的next不为NULL(继续向后找)

while (pprenode->pnext != NULL)

{

pprenode = pprenode->pnext;

}

// 4. 将新节点链接到末尾

pprenode->pnext = ptmpnode;

return 0;

}

4、链表销毁(彻底释放内存)

void destroyList(LinkNode *head)

{

LinkNode *pCur = head->next; // 指向第一个实际节点

LinkNode *pTemp = NULL;

// 遍历释放所有节点(含头节点)

while (pCur != NULL)

{

pTemp = pCur; // 暂存当前节点

pCur = pCur->next; // 移动到下一个节点

free(pTemp); // 释放当前节点内存

}

free(head); // 释放头节点

head = NULL; // 防止野指针(上层需同步置空)

}

  • 若不释放内存,程序结束前会一直占用内存(内存泄漏),长期运行可能崩溃。

5、链表遍历(两种核心模式对比)

  1. 完整遍历(覆盖所有节点)

// 适用场景:访问链表所有实际节点(从头节点后继开始)

LinkNode *p = pHead->next; // pHead 为头节点(有头链表)

while (p != NULL)

{

// 操作当前节点:如打印数据、修改值等

printf("当前节点数据:%d\n", p->data);

p = p->next; // 移动到下一个节点

}

优势

  • 逻辑清晰,严格遍历所有实际节点 (头节点不存数据时,从 pHead->next 开始);

  • 终止条件 p != NULL 确保处理到链表末尾(最后一个节点 nextNULL )。

  1. 遍历到倒数第二个节点(找尾节点前驱)

LinkNode *p = pHead->next;

// 条件:当前节点存在,且下一个节点存在(非空)

while (p->next != NULL)

{

// 操作逻辑:如记录前驱、判断尾节点等

p = p->next;

}

// 循环结束后,p 指向「倒数第二个节点」(若链表长度 ≥2)

典型应用

  • 删除尾节点时,需通过前驱修改 next = NULL

  • 终止条件 p->next != NULL 避免访问 NULL->next(防止程序崩溃)。

6、链表删除操作

核心目标:从链表中删除指定元素(或满足条件的节点),需完成:

  • 找前驱:定位待删除节点的前一个节点;

  • 改指针:跳过待删除节点,维护链表连续性;

  • 释内存:释放节点空间,避免内存泄漏。

// 假设:pHead 为头节点(有头链表),删除数据等于 target 的节点

void deleteTargetNode(LinkNode *pHead, DataType target)

{

LinkNode *pPrev = pHead; // 前驱指针(从头节点开始,统一处理边界)

// 遍历找待删除节点的前驱

while (pPrev->next != NULL)

{

if (pPrev->next->data == target)

{

// 1. 标记待删除节点

LinkNode *pDel = pPrev->next;

// 2. 跳过待删除节点(维护链表连接)

pPrev->next = pDel->next;

// 3. 释放内存(避免泄漏)

free(pDel);

pDel = NULL; // 置空,防止野指针

// (若只删第一个匹配项,可加 break;删所有则继续遍历)

// break;

}

else

{

// 无匹配,移动前驱指针

pPrev = pPrev->next;

}

}

}

1. 前驱指针的作用(pPrev

  • 从头节点开始(pPrev = pHead),统一处理「删除首节点」场景(无需区分空链表或首节点,逻辑更简洁);

  • 若直接从 pHead->next 开始,删除首节点时需单独判断(易出错)。

2. 指针修改的顺序(pPrev->next = pDel->next

  • 必须先修改前驱的 next,再释放节点 → 确保链表连接不中断;

  • 错误顺序:先 free(pDel) 再改指针 → 访问 pDel->next 触发未定义行为。

3. 内存管理(free 与置空)

  • free(pDel) 释放节点占用的内存(避免内存泄漏);

  • pDel = NULL 防止后续代码误操作 pDel->data(触发段错误,提醒指针已失效)。

4. 边界场景处理

  1. 链表为空
  • 表现:pHead->next == NULL

  • 处理:pPrev->next != NULL 条件不满足,函数直接退出(安全无操作)。

  1. 待删除节点是首节点
  • 逻辑:pPrev 为头节点,pPrev->next 是第一个实际节点;

  • 处理:修改头节点的 next = pDel->next(自动跳过首节点,无需特殊判断)。

  1. 多个相同值节点(删除所有匹配项)
  • 需求:删除链表中所有 值等于 target 的节点;

  • 实现:找到匹配节点后,不执行 break ,继续遍历(pPrev 不移动,因 pPrev->next 已更新):

    while (pPrev->next != NULL)

    {

    if (匹配条件)

    {

    // 执行删除逻辑(不 break)

    }

    else

    {

    pPrev = pPrev->next; // 仅无匹配时移动

    }

    }

3.5 常见错误与解决方案

1、野指针访问(程序崩溃)

  • 错误行为:删除节点后,未 free 或未置空,后续代码误操作 pDel->data

  • 解决:严格执行 free(pDel) 并置 pDel = NULL

2、内存泄漏(长期运行崩溃)

  • 错误行为:删除节点时,只改指针但未 free 内存;

  • 解决:删除节点后必须调用 free 释放空间。

3、遍历死循环(链表无法终止)

  • 错误行为:删除节点后,pPrev 仍移动(如 pPrev = pPrev->next),导致跳过有效节点;

  • 解决:删除节点时,仅无匹配时移动 pPrev (匹配时不移动,因 pPrev->next 已更新)。

4 Makefile 高效编译

4.1 核心作用

  • 自动编译 :根据文件依赖关系(如 main.c 依赖 func.c ),只编译修改过的文件 (比手动 gcc 高效);

  • 简化命令 :把复杂编译指令(如 gcc -o main main.c func.c -Iinclude )写进 Makefile,执行 make 即可编译。

4.2 基础语法

目标: 依赖文件列表

main: main.c func.c

gcc -o main main.c func.c # 编译命令(必须以 [Tab] 开头!)

关键规则

  • make 会检查 "依赖文件" 的修改时间,只重新编译变化的文件;

  • 适合多文件项目(如链表实现中,list.c + main.c 分离时,用 Makefile 管理更方便)。

5 学习总结

  1. 指针操作错误

    • 未初始化指针(如 LinkNode *p; p->next = ... )→ 程序崩溃;

    • 忘记更新 next 指针(插入 / 删除后,链表 "断开")→ 数据丢失或遍历异常。

  2. 内存泄漏

    • 删除节点时,只修改指针,未释放内存free(node) )→ 长期运行会耗尽内存。
相关推荐
沐怡旸12 小时前
【算法】【链表】328.奇偶链表--通俗讲解
算法·面试
掘金安东尼16 小时前
Amazon Lambda + API Gateway 实战,无服务器架构入门
算法·架构
码流之上16 小时前
【一看就会一写就废 指间算法】设计电子表格 —— 哈希表、字符串处理
javascript·算法
快手技术18 小时前
快手提出端到端生成式搜索框架 OneSearch,让搜索“一步到位”!
算法
CoovallyAIHub2 天前
中科大DSAI Lab团队多篇论文入选ICCV 2025,推动三维视觉与泛化感知技术突破
深度学习·算法·计算机视觉
NAGNIP2 天前
Serverless 架构下的大模型框架落地实践
算法·架构
moonlifesudo2 天前
半开区间和开区间的两个二分模版
算法
moonlifesudo2 天前
300:最长递增子序列
算法
CoovallyAIHub2 天前
港大&字节重磅发布DanceGRPO:突破视觉生成RLHF瓶颈,多项任务性能提升超180%!
深度学习·算法·计算机视觉
CoovallyAIHub2 天前
英伟达ViPE重磅发布!解决3D感知难题,SLAM+深度学习完美融合(附带数据集下载地址)
深度学习·算法·计算机视觉