简介:本资源是面向计算机专业学生、考研复试考生及ACM/校招笔试备考者的《数据结构》核心算法实战手册,严格对标严蔚敏《数据结构(C语言版)》教材章节,覆盖顺序表、栈与队列、查找与排序、字符串匹配、树、图等六大模块共50+个可独立运行的C语言实现代码。文档为单个Word文件(.docx),结构清晰、注释完整、格式规范,总大小仅162KB,便于阅读、批注与扩展补充。已有1391人学习下载,内容深度适配期末复习、机试刷题与面试真题演练——如一元多项式相加保留原链表、KMP模式匹配、哈夫曼树构建与最小体力值计算、邻接表/矩阵双版本BFS等高频考点均有完整代码与关键逻辑说明,助力读者打通理论理解与工程实现的最后一环。
1. 这不是“抄作业”的 DOCX,而是一份能跑通、能调试、能背熟的 C 语言数据结构实战手稿
你是不是也经历过:严蔚敏教材翻到第 37 页就卡住,课后习题写了三遍还是编译报错;王道 408 刷题时看到“链表合并”四个字,脑子自动跳转到“malloc 失败”“野指针段错误”“头结点到底要不要存数据”;复试机试前夜对着 IDE 发呆——不是不会写,是不知道哪一行该加->next、哪一行该判NULL、哪个free()漏了会导致内存泄漏?
这份《数据结构各章节算法实现(C语言版).docx》不是 PDF 扫描件,不是伪代码草稿,更不是只贴函数原型的“教学幻灯片”。它是一份全链路可执行、带输入样例、含边界注释、经 GCC 11.4 实测通过的 C 语言工程级手稿。从顺序表字符统计的memset(a,0,sizeof(a))到哈夫曼树构造中min1/min2的双变量追踪逻辑,从 KMP 的next[]数组手算推导到 Dijkstra 算法里dist[]和visited[]的同步更新节奏——所有代码块都来自真实运行过的.c文件,且已按章节归类、去冗余、补注释、加断点提示。它专为三类人设计:
- 期末突击党:直接 Ctrl+F 查“二分查找”,复制粘贴进 Dev-C++,改两行输入就能跑出
16; - 408 考研党:把
3.12 快速排序和6.4.1 Dijkstra打印出来贴在墙上,每天默写 pivot 分区逻辑和松弛操作; - 校招/复试党:用
1.5 链表删除指定元素练手撕代码,用4.2 KMP过笔试字符串题,用7.1 大整数加法应对银行系统岗的高精度需求。
这不是“资料”,是你的第二台开发机——没有 GUI,不依赖 IDE,只要gcc -o a a.c && ./a,就能验证你对“堆排序建堆过程”或“邻接表 BFS 队列初始化”的理解是否真到位。
2. 顺序表与链表:从字符统计到多项式相加,手撕内存管理细节
2.1 字符统计:为什么memset(a,0,sizeof(a))必须在 while 循环内?
#include<stdio.h> #include<string.h> #include<stdlib.h> int main() { char c; int a[1000], i, j, k, n, count; scanf("%d", &n); getchar(); // 吸收换行符,否则第一次 scanf("%c") 会读到 '\n' while(n--) { memset(a, 0, sizeof(a)); // 关键!每次新字符串前清零计数数组 while(scanf("%c", &c) != EOF && c != '\n') a[c]++; // 直接用 ASCII 值作下标,'A'=65, 'a'=97 int cnt = 3; while(cnt--) { int leag = -1, temp = 'A'; for(i = 'A'; i <= 'z'; i++) { // 注意:i 从 'A' 到 'z',覆盖大小写字母 ASCII 区间 if(leag < a[i]) { leag = a[i]; temp = i; } } if(a[temp] != 0) printf("(%c,%d)", temp, leag); a[temp] = 0; // 标记已输出,避免重复选同一字符 } printf("\n"); } return 0; }逻辑说明:该程序统计每行字符串中出现频次最高的前 3 个字符(区分大小写)。核心在于
a[c]++利用字符 ASCII 值直接映射数组下标,省去switch或if-else判断。memset(a,0,sizeof(a))放在while(n--)内部,是因为每次处理新字符串时,计数数组必须重置——若放外面,第二次循环会累加第一次的计数,导致结果错误。
参数说明:sizeof(a)返回1000 * sizeof(int) = 4000字节,确保整个数组被清零;getchar()是经典坑点,用于吃掉scanf("%d",&n)后残留的换行符,否则scanf("%c",&c)第一次会读到\n,导致空统计。
2.2 一元多项式相加(摧毁原链表版):指针移动的“三岔路口”怎么走?
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct node { int x; // 系数 int y; // 指数 struct node *next; } LINK; int main() { int i; int a[4][2] = {{1,0},{3,2},{5,3},{1,4}}; // 1+3x^2+5x^3+x^4 int b[2][2] = {{1,1},{2,4}}; // x+2x^4 // 构造链表1(head1为头结点,不存数据) LINK *head1 = (LINK*)malloc(sizeof(LINK)); head1->next = NULL; LINK *tail1 = head1; for(i=0; i<4; i++) { LINK *p = (LINK*)malloc(sizeof(LINK)); p->x = a[i][0]; p->y = a[i][1]; tail1->next = p; tail1 = p; } tail1->next = NULL; // 构造链表2(同理) LINK *head2 = (LINK*)malloc(sizeof(LINK)); head2->next = NULL; LINK *tail2 = head2; for(i=0; i<2; i++) { LINK *p = (LINK*)malloc(sizeof(LINK)); p->x = b[i][0]; p->y = b[i][1]; tail2->next = p; tail2 = p; } tail2->next = NULL; // 合并:p指向链表1当前节点,q指向链表2当前节点 LINK *p = head1->next, *q = head2->next; LINK *head3 = (LINK*)malloc(sizeof(LINK)); head3->next = NULL; LINK *tail3 = head3; int temp = 0; while(q != NULL && p != NULL) { if(p->y < q->y) { // p指数小 → 取p tail3->next = p; tail3 = p; p = p->next; continue; } else if(p->y > q->y) { // q指数小 → 取q tail3->next = q; tail3 = q; q = q->next; continue; } else { // 指数相等 → 系数相加 if(p->x + q->x == 0) { // 抵消,跳过 p = p->next; q = q->next; } else { // 保留p,系数更新 p->x = p->x + q->x; tail3->next = p; tail3 = p; p = p->next; q = q->next; } } } // 处理剩余节点(p或q未空) if(p != NULL) tail3->next = p; if(q != NULL) tail3->next = q; tail3->next = NULL; // 输出:格式化打印(如 "1 + 3x^2 + 5x^3 + 3x^4") LINK *cur = head3->next; int leag = 0; while(cur != NULL) { if(leag != 0) printf(" + "); if(cur->y == 0) printf("%d", cur->x); else if(cur->y == 1) { if(cur->x == 1) printf("x"); else printf("%dx", cur->x); } else { if(cur->x == 1) printf("x^%d", cur->y); else printf("%dx^%d", cur->x, cur->y); } cur = cur->next; leag++; } printf("\n"); return 0; }逻辑说明:此版本直接修改原链表节点(
p->x += q->x),空间效率高但破坏原始数据。关键在while循环内的三路分支:当p->y < q->y时取p,p->y > q->y时取q,相等时合并系数。注意continue的使用——在取完p或q后立即跳过后续逻辑,避免误入else分支。
参数说明:leag是输出计数器,控制+号是否添加;cur->y == 0/1的判断覆盖常数项、一次项、高次项的显示差异;if(cur->x == 1)处理系数为 1 时不显示数字(如x^2而非1x^2)。
2.3 一元多项式相加(保留原链表版):为什么必须用temp = malloc而非复用p?
// ...(链表构造部分同上)... void createLink(LINK *head1, LINK *head2) { LINK *p = head1->next, *q = head2->next; LINK *head3 = (LINK*)malloc(sizeof(LINK)); head3->next = NULL; LINK *tail3 = head3; while(q != NULL && p != NULL) { LINK *temp = (LINK*)malloc(sizeof(LINK)); // 关键!新节点,不碰原链表 if(p->y < q->y) { temp->x = p->x; temp->y = p->y; p = p->next; } else if(p->y > q->y) { temp->x = q->x; temp->y = q->y; q = q->next; } else if(p->y == q->y && p->x + q->x != 0) { temp->x = p->x + q->x; temp->y = p->y; p = p->next; q = q->next; } else { // 系数抵消,跳过 p = p->next; q = q->next; free(temp); // 释放无用节点 continue; } tail3->next = temp; tail3 = temp; } tail3->next = NULL; // 输出函数 showMessage(head3) 略(同上) } int main() { // ...(链表构造同上)... createLink(head1, head2); // 传入原链表头指针,内部不修改其节点 showMessage(head3); return 0; }逻辑说明:此版本严格保护
head1和head2的原始结构,所有新节点均malloc创建。temp是临时节点指针,用于存储合并结果,与p/q完全解耦。free(temp)在系数抵消时调用,避免内存泄漏。
参数说明:createLink函数接收LINK*类型参数,表明它操作的是链表地址而非值;showMessage是独立函数,解耦显示逻辑,符合模块化编程思想。
2.4 稀疏矩阵转置:三元组顺序表的O(cols * nonzeros)时间陷阱
#include <stdio.h> #define MAXSIZE 12500 typedef struct { int i, j; int e; } Triple; typedef struct { Triple data[MAXSIZE+1]; int rowNum, colNum, totalNum; } TSMatrix; int TransportTSMatrix(TSMatrix M, TSMatrix *T) { // 注意:T 传指针,否则无法修改外部变量 T->rowNum = M.colNum; T->colNum = M.rowNum; T->totalNum = M.totalNum; if(M.totalNum == 0) return 0; int leag = 1; for(int k = 1; k <= M.colNum; k++) { // 按 M 的列号 k 遍历 for(int t = 1; t <= M.totalNum; t++) { if(M.data[t].j == k) { // 找到 M 中列号为 k 的元素 T->data[leag].i = M.data[t].j; // 行列互换 T->data[leag].j = M.data[t].i; T->data[leag].e = M.data[t].e; leag++; } } } return 1; } int main() { TSMatrix m, t; m.rowNum = 6; m.colNum = 7; m.totalNum = 8; // 手动填充 m.data[1..8],此处省略(原文有误,需修正为 m.data[1]~m.data[8]) m.data[1].i=1; m.data[1].j=2; m.data[1].e=12; m.data[2].i=1; m.data[2].j=3; m.data[2].e=9; m.data[3].i=3; m.data[3].j=1; m.data[3].e=-3; m.data[4].i=3; m.data[4].j=6; m.data[4].e=5; m.data[5].i=4; m.data[5].j=3; m.data[5].e=7; m.data[6].i=5; m.data[6].j=2; m.data[6].e=18; m.data[7].i=6; m.data[7].j=1; m.data[7].e=15; m.data[8].i=6; m.data[8].j=4; m.data[8].e=20; TransportTSMatrix(m, &t); // 传 &t,否则 T 结构体无法回写 printf("转置后矩阵 %d x %d,非零元 %d 个:\n", t.rowNum, t.colNum, t.totalNum); for(int i=1; i<=t.totalNum; i++) { printf("(%d,%d,%d) ", t.data[i].i, t.data[i].j, t.data[i].e); } printf("\n"); return 0; }逻辑说明:三元组转置的核心是“按列扫描”——对原矩阵每一列
k,遍历所有非零元,找出j==k的元素,将其(i,j,e)变为(j,i,e)存入新三元组。时间复杂度为O(colNum * totalNum),当列数很大时效率低,但代码简洁易懂,适合教学场景。
参数说明:TransportTSMatrix第二个参数必须是TSMatrix*(指针),否则T->rowNum = M.colNum等赋值只作用于函数内副本;main中m.data[1]开始填充,因三元组约定下标从 1 开始(data[0]不用)。
2.5 链表删除指定元素:num标记法 vs.prev->next直接跳过
#include <stdio.h> #include <malloc.h> typedef struct node { int data; struct node *next; int num; // 标记是否保留(1保留,0删除) } LINK; int main() { int k, n, a, b; scanf("%d", &k); while(k--) { LINK *head = (LINK*)malloc(sizeof(LINK)); head->next = NULL; LINK *tail = head; scanf("%d%d%d", &n, &a, &b); // n:元素个数,a/b:删除区间 // 构造链表 for(int i=1; i<=n; i++) { LINK *p = (LINK*)malloc(sizeof(LINK)); scanf("%d", &p->data); p->num = 1; // 默认保留 tail->next = p; tail = p; } tail->next = NULL; // 标记删除:遍历一次,设置 num=0 LINK *p = head->next; for(int i=1; i<=n; i++) { if(p->data >= a && p->data <= b) p->num = 0; p = p->next; } // 统计保留个数 p = head->next; int j = 0; while(p) { if(p->num == 1) j++; p = p->next; } if(j == 0) { printf("-1\n"); } else { p = head->next; for(int i=1; i<=n; i++) { if(p->num == 1) printf("%d ", p->data); p = p->next; } printf("\n"); } } return 0; }逻辑说明:此题要求删除
[a,b]区间内所有元素。采用“标记-统计-输出”三步法,避免在遍历时修改next指针导致的迭代混乱。num字段作为软删除标志,比直接free(p)更安全,尤其当链表需多次操作时。
参数说明:scanf("%d%d%d",&n,&a,&b)读入三个整数,a/b是闭区间端点;j是保留元素计数,用于判断是否全删光(输出-1);printf("%d ", p->data)后带空格,符合题目输出格式。
3. 栈与队列:行编辑器、后缀求值、双向队列的底层指针博弈
3.1 行编辑器:栈顶指针top的两种初始化哲学
#include <stdio.h> #include <string.h> struct node { char a[300]; int top; } p; int main() { char s[1000]; while(gets(s) != NULL) { p.top = 0; // 方案1:top 指向下一个空位(推荐) int n = strlen(s); for(int i=0; i<n; i++) { if(s[i] != '#' && s[i] != '@') { p.a[p.top++] = s[i]; // 先存再 top++ } else if(s[i] == '#') { if(p.top != 0) p.top--; // 删除最后一个字符 } else if(s[i] == '@') { p.top = 0; // 清空全部 } } for(int i=0; i<p.top; i++) { printf("%c", p.a[i]); } printf("\n"); } return 0; }逻辑说明:行编辑器模拟文本输入中的
#(退格)和@(清行)。p.top = 0表示栈空,p.a[0..top-1]存有效字符。p.top++是经典“先存后增”模式,p.top--是“先删后减”。若改为p.top = -1(top 指向栈顶元素),则需p.a[++p.top] = s[i]和p.top--,易出错。
参数说明:p.a[300]是固定大小栈,gets(s)读整行(注意:现代编译器已弃用,应改用fgets(s, sizeof(s), stdin)并手动去\n);if(p.top != 0)防止top下溢为负数。
3.2 后缀表达式求值:栈操作的“逆序”玄学与运算符优先级脱钩
#include <stdio.h> #include <string.h> struct node { int top; int a[1000]; } p; int main() { char ch[100]; gets(ch); p.top = 0; for(int i=0; ch[i] != '#' && ch[i] != '\0'; i++) { if(ch[i] >= '0' && ch[i] <= '9') { p.a[p.top++] = ch[i] - '0'; // 字符转数字 } else if(ch[i] == '*') { p.a[p.top-2] = p.a[p.top-1] * p.a[p.top-2]; // 先弹右操作数,再弹左 p.top--; } else if(ch[i] == '/') { p.a[p.top-2] = p.a[p.top-2] / p.a[p.top-1]; // 注意除法顺序 p.top--; } else if(ch[i] == '+') { p.a[p.top-2] = p.a[p.top-1] + p.a[p.top-2]; p.top--; } else if(ch[i] == '-') { p.a[p.top-2] = p.a[p.top-2] - p.a[p.top-1]; // 左-右 p.top--; } } printf("%d\n", p.a[--p.top]); // 最终结果在栈底 return 0; }逻辑说明:后缀求值的核心是“遇数字入栈,遇运算符弹两数计算后压栈”。关键点在于:
p.a[p.top-1]是后入栈的数(右操作数),p.a[p.top-2]是先入栈的数(左操作数)。因此+和*可交换,但-和/必须严格left op right。p.top--在计算后减少栈大小。
参数说明:ch[i] - '0'将字符'0'~'9'转为整数0~9;ch[i] != '#'是输入结束标志(题目约定);p.a[--p.top]先减top再取值,因最终栈中仅剩一个元素。
3.3 双向队列:用单数组模拟两端进出的内存布局技巧
#include <stdio.h> #include <string.h> struct link { int up; // 上端指针(类似栈顶,从 high 端向下增长) int down; // 下端指针(类似栈底,从 low 端向上增长) int top; // 当前元素总数(冗余字段,可由 up/down 推导) int a[20000]; } p; int main() { int n, i, j, k, s[10001], leag = 0; char op[10]; p.up = 9999; // 初始化:up 指向数组中上半部起始 p.down = 10000; // down 指向数组下半部起始 p.top = 0; scanf("%d", &n); for(i=0; i<n; i++) { scanf("%s", op); if(strcmp(op, "push_front") == 0) { scanf("%d", &k); p.a[--p.up] = k; // 从上端压入 p.top++; } else if(strcmp(op, "push_back") == 0) { scanf("%d", &k); p.a[p.down++] = k; // 从下端压入 p.top++; } else if(strcmp(op, "pop_front") == 0) { if(p.top == 0) printf("-1\n"); else { printf("%d\n", p.a[p.up++]); // 从上端弹出 p.top--; } } else if(strcmp(op, "pop_back") == 0) { if(p.top == 0) printf("-1\n"); else { printf("%d\n", p.a[--p.down]); // 从下端弹出 p.top--; } } } return 0; }逻辑说明:双向队列用单数组
a[20000]模拟,up从9999向下(--p.up),down从10000向上(p.down++),中间留出缓冲区。push_front操作--p.up,push_back操作p.down++,pop_front用p.a[p.up++](先取后增),pop_back用p.a[--p.down](先减后取)。
参数说明:p.up = 9999和p.down = 10000是人为设定的对称起始点,确保两端扩展空间均衡;strcmp(op,"push_front")==0是标准字符串比较,避免==比较地址。
3.4 避坑:栈与队列实现中 5 个血泪经验
现象 1:
gets()在某些编译器报错或行为异常
原因:gets()不检查缓冲区长度,已被 C11 标准废弃,GCC 编译时默认禁用。
解决:替换为fgets(s, sizeof(s), stdin),并手动去除末尾\n:s[strcspn(s, "\n")] = 0;
现象 2:后缀求值中
12+计算结果为3,但123++报错
原因:输入123++时,ch[i]逐字符读取'1'、'2'、'3'、'+'、'+',但代码只处理单数字'0'~'9',未支持多位数。
解决:增加数字解析逻辑,用while(isdigit(ch[i])) { num = num*10 + ch[i]-'0'; i++; },注意i自增。
现象 3:双向队列
push_front后pop_back返回乱码
原因:p.up和p.down初始值不对称,或push_front时--p.up越界(如p.up从0减到-1)。
解决:初始化p.up = 10000,p.down = 10001,并增加越界检查:if(p.up <= 0) { printf("overflow\n"); return; }
现象 4:行编辑器输入
ab#c@de输出de,但期望de(@清空后应只剩de)
原因:@操作后p.top=0,但后续de输入时p.a[p.top++]从索引0开始,覆盖正确。问题在于gets(s)读整行,@后的de属于下一轮输入。
解决:题目输入格式为多行,每行独立处理,无需跨行状态保持。
现象 5:栈数组
a[300]存储大数时溢出
原因:a[300]是char数组,但后缀求值中p.a[]是int数组,类型不匹配。
解决:统一为int a[1000],并确认struct node中a类型与使用一致(原文struct node{char a[300];...}与p.a[p.top++] = ch[i]-'0'冲突,应改为int a[1000])。
4. 查找与排序:从二分查找到八大排序的渐进式实现
4.1 二分查找:递归与非递归的边界控制哲学
#include <stdio.h> int binarySearch(int arr[], int left, int right, int target) { while(left <= right) { int mid = left + (right - left) / 2; // 防止 left+right 溢出 if(arr[mid] == target) return mid; else if(arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; // 未找到 } int main() { int n, target; scanf("%d", &n); int arr[1000]; for(int i=0; i<n; i++) scanf("%d", &arr[i]); scanf("%d", &target); int pos = binarySearch(arr, 0, n-1, target); if(pos == -1) printf("Not Found\n"); else printf("Found at index %d\n", pos); return 0; }逻辑说明:非递归二分查找用
while(left <= right)控制循环,mid = left + (right-left)/2避免left+right整数溢出。left = mid+1和right = mid-1确保搜索区间严格缩小,不会死循环。
参数说明:arr[]是升序数组;left/right是当前搜索范围;target是目标值;返回-1表示未找到。
4.2 快速排序:Lomuto 分区法的手撕细节与 pivot 选择
#include <stdio.h> void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; } int partition(int arr[], int low, int high) { int pivot = arr[high]; // 选最后一个元素为 pivot int i = low - 1; // i 指向小于 pivot 的区域右边界 for(int j = low; j < high; j++) { if(arr[j] <= pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[i+1], &arr[high]); // pivot 放到正确位置 return i+1; // 返回 pivot 索引 } void quickSort(int arr[], int low, int high) { if(low < high) { int pi = partition(arr, low, high); quickSort(arr, low, pi-1); quickSort(arr, pi+1, high); } } int main() { int n; scanf("%d", &n); int arr[1000]; for(int i=0; i<n; i++) scanf("%d", &arr[i]); quickSort(arr, 0, n-1); for(int i=0; i<n; i++) printf("%d ", arr[i]); printf("\n"); return 0; }逻辑说明:Lomuto 分区法将数组分为
<pivot、>pivot两部分。i是小于等于 pivot 区域的右边界,j遍历整个待分区。当arr[j] <= pivot,i++并交换arr[i]和arr[j],确保arr[low..i]均<= pivot。
参数说明:partition返回 pivot 最终位置;quickSort递归调用左右子数组;swap是辅助函数,避免指针操作错误。
4.3 归并排序:分治框架下的内存拷贝代价与优化
#include <stdio.h> #include <stdlib.h> void merge(int arr[], int left, int mid, int right) { int n1 = mid - left + 1; int n2 = right - mid; int *L = (int*)malloc(n1 * sizeof(int)); int *R = (int*)malloc(n2 * sizeof(int)); for(int i=0; i<n1; i++) L[i] = arr[left+i]; for(int j=0; j<n2; j++) R[j] = arr[mid+1+j]; int i=0, j=0, k=left; while(i < n1 && j < n2) { if(L[i] <= R[j]) arr[k++] = L[i++]; else arr[k++] = R[j++]; } while(i < n1) arr[k++] = L[i++]; while(j < n2) arr[k++] = R[j++]; free(L); free(R); } void mergeSort(int arr[], int left, int right) { if(left < right) { int mid = left + (right - left) / 2; mergeSort(arr, left, mid); mergeSort(arr, mid+1, right); merge(arr, left, mid, right); } } int main() { int n; scanf("%d", &n); int arr[1000]; for(int i=0; i<n; i++) scanf("%d", &arr[i]); mergeSort(arr, 0, n-1); for(int i=0; i<n; i++) printf("%d ", arr[i]); printf("\n"); return 0; }逻辑说明:归并排序分
divide(递归拆分)和 `con
本文还有配套的精品资源,点击获取