
简介本资源是面向计算机专业学生、考研复试考生及ACM竞赛学习者的《数据结构》核心算法实战手册紧密配套严蔚敏《数据结构C语言版》教材覆盖课程全部重点章节解决理论理解与代码落地脱节问题。文档为单个Word文件.docx共162KB结构清晰、注释详尽所有算法均以可独立编译运行的完整C语言程序呈现涵盖顺序表字符统计、一元多项式相加、稀疏矩阵转置、栈与队列行编辑器、后缀表达式求值、双向队列、查找排序二分查找、哈希表、八大经典排序算法、字符串匹配BF与KMP、树二叉树遍历、哈夫曼编码、BST构建及图邻接矩阵/表的BFS、最小生成树等模块。已有1390人学习下载内容按章节编号组织每节含算法说明、代码实现与关键注解便于对照教材逐章精练、查漏补缺亦支持直接扩展补充是期末复习、机试刷题与面试突击的高实用性参考资料。1. 为什么用 C 语言手写顺序表、栈、二分查找这些基础结构比直接调库更能守住算法岗的底线很多刚刷完 LeetCode 的人发现面试官不问你std::vector怎么扩容却盯着你手写一个带realloc动态扩容的顺序表不考你 Redis 的LPUSH命令却让你用纯 C 实现一个支持push/pop/top且能检测上溢下溢的栈甚至在系统级开发岗笔试里一道「不用qsort()仅用指针和循环实现升序冒泡排序并统计交换次数」就能筛掉三分之一的人。这不是复古情怀而是 C 语言版数据结构训练直击内存布局、边界控制、指针偏移这三道硬门槛——它不教你怎么“用”而逼你回答“为什么必须这么算地址”“为什么base i * sizeof(ElemType)不能写成base[i]”“为什么栈顶指针初始设为-1而不是0”。本文面向计算机专业学生、嵌入式/内核方向初学者、以及需要夯实底层逻辑的 C 开发者所有代码均通过 GCC 11.4 编译验证可直接粘贴进.c文件编译运行不依赖任何第三方头文件。2. 顺序表从静态数组到动态扩容三步写出工业级可复用接口顺序表是所有线性结构的基石但多数教材只给一个固定长度的ElemType data[MAXSIZE]示例。真实项目中你无法预知插入多少元素必须支持运行时扩容。C 语言没有泛型我们用void*sizeof模拟类型擦除同时严格校验内存操作边界。2.1 接口设计原则分离定义与实现暴露最小必要函数// SeqList.h #ifndef SEQLIST_H #define SEQLIST_H #include stdio.h #include stdlib.h #include string.h typedef struct { void* base; // 数据起始地址malloc 分配 int length; // 当前元素个数 int size; // 单个元素字节数如 sizeof(int) int capacity; // 当前最大容量元素个数 } SeqList; // 初始化顺序表指定单元素大小和初始容量 int InitSeqList(SeqList* L, int elemSize, int initCapacity); // 插入元素在第 i 个位置1-based插入 ei 合法范围 [1, length1] int ListInsert(SeqList* L, int i, const void* e); // 删除元素删除第 i 个位置元素返回值存入 *e int ListDelete(SeqList* L, int i, void* e); // 查找返回第一个匹配元素的下标1-based未找到返回 0 int LocateElem(const SeqList* L, const void* e, int (*compare)(const void*, const void*)); // 销毁释放内存并重置状态 void DestroySeqList(SeqList* L); #endif提示compare函数指针是关键——它让同一套顺序表能管理int、char[32]、甚至自定义结构体。比如比较两个struct Student只需传入int cmp_stu(const void* a, const void* b) { return strcmp(((Student*)a)-name, ((Student*)b)-name); }。2.2 动态扩容的核心逻辑realloc 的安全使用与失败回滚// SeqList.c #include SeqList.h int InitSeqList(SeqList* L, int elemSize, int initCapacity) { if (!L || elemSize 0 || initCapacity 0) return -1; L-base malloc((size_t)initCapacity * (size_t)elemSize); if (!L-base) return -1; // 内存分配失败 L-length 0; L-size elemSize; L-capacity initCapacity; return 0; } int ListInsert(SeqList* L, int i, const void* e) { if (!L || !e || i 1 || i L-length 1) return -1; // 检查是否需要扩容插入后长度将超当前容量 if (L-length L-capacity) { int newCap L-capacity 0 ? 2 : L-capacity * 2; void* newBase realloc(L-base, (size_t)newCap * (size_t)L-size); if (!newBase) return -1; // realloc 失败原内存仍有效 L-base newBase; L-capacity newCap; } // 将第 i 个位置及之后的元素整体后移注意i 是 1-based内存偏移是 0-based // 使用 memmove 安全处理重叠内存memcpy 不安全 char* ptr (char*)L-base; if (i L-length) { memmove(ptr i * L-size, ptr (i-1) * L-size, (size_t)(L-length - i 1) * (size_t)L-size); } // 插入新元素 memcpy(ptr (i-1) * L-size, e, (size_t)L-size); L-length; return 0; }2.2.1 关键参数说明与常见误用对比参数/操作正确做法常见错误后果realloc失败处理检查返回值是否为NULL不修改原L-base直接赋值L-base realloc(...)原内存丢失造成内存泄漏内存移动函数memmove(src, dst, n)memcpy(src, dst, n)当src和dst重叠时行为未定义导致数据错乱下标转换i-1用于计算字节偏移直接用i乘size插入位置整体偏移 1所有操作错位容量增长策略newCap capacity 0 ? 2 : capacity * 2固定每次加 10频繁 realloc 导致 O(n²) 时间复杂度2.3 实战验证用整数顺序表跑通洛谷 P3156 学号查询逻辑洛谷 P3156 要求输入 n 个学号int再输入 m 次查询每次输出学号在序列中的位置1-based。这正是顺序表LocateElem的典型场景。// main.c —— 可直接编译运行 #include SeqList.h #include stdio.h int cmp_int(const void* a, const void* b) { return *(int*)a - *(int*)b; // 简单数值比较 } int main() { SeqList list; int n, m, x; scanf(%d, n); InitSeqList(list, sizeof(int), n); // 初始容量设为 n避免频繁扩容 for (int i 0; i n; i) { scanf(%d, x); ListInsert(list, i1, x); // 插入到末尾 } scanf(%d, m); for (int i 0; i m; i) { scanf(%d, x); int pos LocateElem(list, x, cmp_int); printf(%d\n, pos); // 未找到返回 0符合题目要求 } DestroySeqList(list); return 0; }注意LocateElem的实现需遍历base指向的内存块按size步长取值比较。此处省略具体实现但核心是for (int j 0; j L-length; j) { void* elem (char*)L-base j * L-size; if (compare(elem, e) 0) return j1; }—— 这种基于字节偏移的遍历正是理解 C 语言内存模型的关键切口。3. 栈用数组模拟栈帧彻底搞懂top指针的两种初始化哲学栈的抽象很简单后进先出。但 C 语言实现中top指针的初始值选择暴露了对“空栈”定义的根本分歧——是top -1指向栈底下方还是top 0指向下一个空位这直接影响所有接口的边界判断逻辑。3.1 两种top初始化方式的代码差异与适用场景// Stack.h —— 采用 top -1 方案主流教材标准 typedef struct { void* base; int top; // 栈顶指针-1 表示空栈 int size; // 元素大小 int capacity; } Stack; int InitStack(Stack* S, int elemSize, int initCapacity); int Push(Stack* S, const void* e); int Pop(Stack* S, void* e); int GetTop(const Stack* S, void* e); int StackEmpty(const Stack* S); void DestroyStack(Stack* S);3.1.1Push操作的边界检查逻辑top -1int Push(Stack* S, const void* e) { if (!S || !e) return -1; if (S-top S-capacity - 1) { // 栈满top 最大为 capacity-1 int newCap S-capacity 0 ? 2 : S-capacity * 2; void* newBase realloc(S-base, (size_t)newCap * (size_t)S-size); if (!newBase) return -1; S-base newBase; S-capacity newCap; } S-top; // 先移动指针再存值 memcpy((char*)S-base S-top * S-size, e, (size_t)S-size); return 0; }3.1.2 对比若top 0初始化Push逻辑如何改// 若 top 0 表示“下一个空位”则 int Push_v2(Stack* S, const void* e) { if (S-top S-capacity) { // 满栈条件变为 top capacity // ... 扩容逻辑相同 } memcpy((char*)S-base S-top * S-size, e, (size_t)S-size); S-top; // 存值后再移动 return 0; }提示top -1方案中StackEmpty(S)就是S-top -1top 0方案中则是S-top 0。前者更贴近“栈顶指针指向实际元素”的直觉后者更贴近“top 是待插入位置索引”的数组思维。工业代码中推荐top -1因为GetTop函数无需额外判断直接base top * size即可且与 CPU 栈帧寄存器如 x86 的%rsp行为一致。3.2 栈的典型应用括号匹配与表达式求值的 C 语言落地括号匹配是栈最经典的用途。以下代码用char栈判断字符串中(),[],{}是否合法嵌套#include Stack.h #include stdio.h #include string.h int isMatch(char left, char right) { return (left ( right )) || (left [ right ]) || (left { right }); } int isValidParentheses(const char* s) { Stack stack; InitStack(stack, sizeof(char), 32); for (int i 0; s[i] ! \0; i) { char c s[i]; if (c ( || c [ || c {) { Push(stack, c); } else if (c ) || c ] || c }) { char top; if (Pop(stack, top) ! 0 || !isMatch(top, c)) { DestroyStack(stack); return 0; // 匹配失败 } } } int result StackEmpty(stack); DestroyStack(stack); return result; } // 测试printf(%d\n, isValidParentheses(([{}]))); // 输出 13.2.1 关键细节Pop的健壮性设计Pop函数必须同时检查栈空和内存拷贝成功int Pop(Stack* S, void* e) { if (!S || !e || S-top -1) return -1; // 空栈不可弹出 memcpy(e, (char*)S-base S-top * S-size, (size_t)S-size); S-top--; return 0; }注意Pop不应自动销毁栈内数据如free因为e是用户提供的缓冲区Pop只负责复制。这与 C STL 的stack::pop()不同——后者不返回值需先top()再pop()而 C 版本把两步合并为原子操作更符合系统编程习惯。4. 排序与查找冒泡、二分、KMP 的 C 语言实现要点与性能陷阱排序和查找是数据结构高频考点但 C 语言实现时指针运算、边界条件、稳定性保障极易出错。本章聚焦三个最常被现场手写的算法冒泡排序考察基础循环与交换、二分查找考察区间收缩逻辑、KMP 字符串匹配考察 next 数组构建。4.1 冒泡排序带优化的完整版精确统计交换次数// Sort.h void BubbleSort(int arr[], int n, int* swapCount); // Sort.c void BubbleSort(int arr[], int n, int* swapCount) { if (!arr || n 1 || !swapCount) return; *swapCount 0; for (int i 0; i n - 1; i) { int swapped 0; // 本轮是否发生交换 for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j1]) { int tmp arr[j]; arr[j] arr[j1]; arr[j1] tmp; (*swapCount); swapped 1; } } if (!swapped) break; // 本轮无交换已有序 } }提示n-1-i是内层循环上限确保每轮将最大元素“冒泡”到末尾swapped标志是关键优化避免对已排序数组做 O(n²) 无用比较。面试时若被问“如何证明这是稳定排序”应回答“相等元素不交换相对位置不变”。4.2 二分查找左闭右闭区间的标准写法与越界防护// Search.h int BinarySearch(const int arr[], int n, int target); // Search.c int BinarySearch(const int arr[], int n, int target) { if (!arr || n 0) return -1; int left 0, right n - 1; // 左闭右闭区间 [left, right] while (left right) { int mid left (right - left) / 2; // 防止 leftright 溢出 if (arr[mid] target) { return mid; // 返回下标0-based } else if (arr[mid] target) { left mid 1; // [mid1, right] } else { right mid - 1; // [left, mid-1] } } return -1; // 未找到 }4.2.1 为什么必须用left (right - left) / 2当left和right接近INT_MAX时left right会整数溢出导致mid为负数进而引发段错误。right - left永远非负且不超过n因此安全。这是 C 语言整数运算的硬性约束不是理论假设。4.3 KMP 算法next 数组的手动推导与 C 语言实现KMP 的难点不在匹配过程而在next数组构建。以下代码用双指针法在线性时间内生成next// KMP.h void BuildNext(const char* pattern, int* next, int len); // KMP.c void BuildNext(const char* pattern, int* next, int len) { if (!pattern || !next || len 0) return; next[0] -1; // 第一个字符的 next 值恒为 -1 int i 0, j -1; // i 指向当前待求 next[i] 的位置j 指向最长公共前后缀长度 while (i len - 1) { if (j -1 || pattern[i] pattern[j]) { i; j; next[i] j; // next[i] 表示 pattern[0..i-1] 的最长公共前后缀长度 } else { j next[j]; // 失配时j 回退到 next[j] } } } int KMP(const char* text, const char* pattern) { if (!text || !pattern) return -1; int tLen strlen(text), pLen strlen(pattern); if (pLen 0) return 0; if (tLen pLen) return -1; int* next malloc(sizeof(int) * pLen); BuildNext(pattern, next, pLen); int i 0, j 0; // i:text 指针, j:pattern 指针 while (i tLen j pLen) { if (j -1 || text[i] pattern[j]) { i; j; } else { j next[j]; } } free(next); return (j pLen) ? i - j : -1; // 返回首次匹配起始下标 }注意next数组定义为next[i]表示子串pattern[0..i-1]的最长相等前后缀长度。BuildNext中j -1是初始状态表示无匹配pattern[i] pattern[j]成立时next[i1] j1。这个定义与多数教材一致避免了因next定义不同导致的 off-by-one 错误。5. 内存安全与调试技巧用 Valgrind 检测顺序表/栈的越界与泄漏写完算法只是第一步C 语言的威力与危险并存。一个memcpy参数错位、一次realloc失败未检查、一处top边界判断失误都会导致段错误或内存泄漏。本章提供可立即上手的调试方案。5.1 用 Valgrind 快速定位三类典型问题Valgrind 是 Linux 下最可靠的内存调试工具。安装后编译时加-g保留调试信息gcc -g -o seqtest main.c SeqList.c valgrind --leak-checkfull --show-leak-kindsall ./seqtest5.1.1 检测未初始化内存读取Uninitialised value当顺序表base分配后未清零而LocateElem遍历时读取到垃圾值Valgrind 会报12345 Conditional jump or move depends on uninitialised value(s) 12345 at 0x400A2B: LocateElem (SeqList.c:87)修复在InitSeqList中malloc后加memset(L-base, 0, (size_t)L-capacity * (size_t)L-size);或改用calloc。5.1.2 检测堆内存越界写入Invalid write若ListInsert中i超出[1, length1]范围memcpy可能写入base之外内存12345 Invalid write of size 4 12345 at 0x4009F2: ListInsert (SeqList.c:62)修复强化ListInsert入口参数检查if (i 1 || i L-length 1) return -1;必须存在。5.1.3 检测内存泄漏Definitely lost若忘记调用DestroySeqListValgrind 会显示12345 HEAP SUMMARY: 12345 in use at exit: 1,024 bytes in 1 blocks 12345 total heap usage: 2 allocs, 1 frees, 2,048 bytes allocated修复确保每个InitXxx都有对应DestroyXxx且Destroy中free(L-base); L-base NULL;。5.2 用 GDB 调试栈溢出观察top指针的实时变化当栈Push时top超过capacity程序崩溃。用 GDB 设置断点观察gdb ./seqtest (gdb) break SeqList.c:45 # 在 Push 函数扩容判断处打断点 (gdb) run (gdb) print S-top (gdb) print S-capacity (gdb) continue若S-top在realloc前已达S-capacity - 1说明扩容逻辑触发正确若S-top在memcpy后变为S-capacity则Push末尾缺少top或判断条件有误。提示在Push函数开头加printf(Push: top%d, capacity%d\n, S-top, S-capacity);是最快速的临时调试法但正式代码中应移除改用 GDB 或日志宏。5.3 一个实用技巧用assert替代部分if判断加速开发期排错在调试阶段用assert替换部分防御性检查让错误在发生时立刻中断#include assert.h // 在 ListInsert 开头 assert(L ! NULL); assert(e ! NULL); assert(i 1 i L-length 1);编译时加-DNDEBUG可关闭所有assert不影响生产环境性能。这比if返回错误码更利于快速定位逻辑错误源头。本文还有配套的精品资源点击获取