ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言数据结构实战手稿:可运行、可调试、可背诵的算法实现

C语言数据结构实战手稿:可运行、可调试、可背诵的算法实现 简介本资源是面向计算机专业学生、考研复试考生及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文件且已按章节归类、去冗余、补注释、加断点提示。它专为三类人设计期末突击党直接 CtrlF 查“二分查找”复制粘贴进 Dev-C改两行输入就能跑出16408 考研党把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 循环内#includestdio.h #includestring.h #includestdlib.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 值作下标A65, a97 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}}; // 13x^25x^3x^4 int b[2][2] {{1,1},{2,4}}; // x2x^4 // 构造链表1head1为头结点不存数据 LINK *head1 (LINK*)malloc(sizeof(LINK)); head1-next NULL; LINK *tail1 head1; for(i0; i4; 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(i0; i2; 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时取pp-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[MAXSIZE1]; 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].i1; m.data[1].j2; m.data[1].e12; m.data[2].i1; m.data[2].j3; m.data[2].e9; m.data[3].i3; m.data[3].j1; m.data[3].e-3; m.data[4].i3; m.data[4].j6; m.data[4].e5; m.data[5].i4; m.data[5].j3; m.data[5].e7; m.data[6].i5; m.data[6].j2; m.data[6].e18; m.data[7].i6; m.data[7].j1; m.data[7].e15; m.data[8].i6; m.data[8].j4; m.data[8].e20; TransportTSMatrix(m, t); // 传 t否则 T 结构体无法回写 printf(转置后矩阵 %d x %d非零元 %d 个:\n, t.rowNum, t.colNum, t.totalNum); for(int i1; it.totalNum; i) { printf((%d,%d,%d) , t.data[i].i, t.data[i].j, t.data[i].e); } printf(\n); return 0; }逻辑说明三元组转置的核心是“按列扫描”——对原矩阵每一列k遍历所有非零元找出jk的元素将其(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 i1; in; i) { LINK *p (LINK*)malloc(sizeof(LINK)); scanf(%d, p-data); p-num 1; // 默认保留 tail-next p; tail p; } tail-next NULL; // 标记删除遍历一次设置 num0 LINK *p head-next; for(int i1; in; 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 i1; in; 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是保留元素计数用于判断是否全删光输出-1printf(%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; // 方案1top 指向下一个空位推荐 int n strlen(s); for(int i0; in; 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 i0; ip.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 -1top 指向栈顶元素则需p.a[p.top] s[i]和p.top--易出错。参数说明p.a[300]是固定大小栈gets(s)读整行注意现代编译器已弃用应改用fgets(s, sizeof(s), stdin)并手动去\nif(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 i0; 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~9ch[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(i0; in; 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.updown从10000向上p.down中间留出缓冲区。push_front操作--p.uppush_back操作p.downpop_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 个血泪经验现象 1gets()在某些编译器报错或行为异常原因gets()不检查缓冲区长度已被 C11 标准废弃GCC 编译时默认禁用。解决替换为fgets(s, sizeof(s), stdin)并手动去除末尾\ns[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 10000p.down 10001并增加越界检查if(p.up 0) { printf(overflow\n); return; }现象 4行编辑器输入ab#cde输出de但期望de清空后应只剩de原因操作后p.top0但后续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; // 防止 leftright 溢出 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 i0; in; 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避免leftright整数溢出。left mid1和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[i1], arr[high]); // pivot 放到正确位置 return i1; // 返回 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, pi1, high); } } int main() { int n; scanf(%d, n); int arr[1000]; for(int i0; in; i) scanf(%d, arr[i]); quickSort(arr, 0, n-1); for(int i0; in; i) printf(%d , arr[i]); printf(\n); return 0; }逻辑说明Lomuto 分区法将数组分为pivot、pivot两部分。i是小于等于 pivot 区域的右边界j遍历整个待分区。当arr[j] pivoti并交换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 i0; in1; i) L[i] arr[lefti]; for(int j0; jn2; j) R[j] arr[mid1j]; int i0, j0, kleft; 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, mid1, right); merge(arr, left, mid, right); } } int main() { int n; scanf(%d, n); int arr[1000]; for(int i0; in; i) scanf(%d, arr[i]); mergeSort(arr, 0, n-1); for(int i0; in; i) printf(%d , arr[i]); printf(\n); return 0; }逻辑说明归并排序分divide递归拆分和 con本文还有配套的精品资源点击获取
返回列表