ARTICLE DETAIL

资讯详情

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

严蔚敏数据结构C语言版:配套代码移植与避坑完整指南

严蔚敏数据结构C语言版:配套代码移植与避坑完整指南 简介严蔚敏《数据结构与算法C语言》是计算机专业广泛使用的经典教材这份配套代码实现包将书中抽象的概念落地为可直接编译运行的C/C程序适合在校学生、考研备考者以及需要夯实算法功底的开发者。代码系统覆盖线性表、栈与队列、树与二叉树、图、查找与排序等核心章节并深入实现递归与分治、贪心、动态规划、回溯等经典算法源文件多以教材章节和算法编号命名头文件与实现分离便于按图索骥逐条对照学习。压缩包共416个文件以154个cpp、154个c和89个h为主体同时包含少量txt说明、dat测试数据以及Visual C工程配置文件整体仅494KB轻量易携带。目前已有1600余人浏览学习。读者既能借助C版本看清底层存储与指针操作细节也能参考C封装体会面向对象设计再配合现成工程文件直接编译运行省去手动搭建环境的繁琐整套代码如同一份可运行的算法手册无论自学、备考还是备课参考都很实用。1. 数据结构与算法C语言实现拿到严蔚敏配套代码后先看清它能解决什么很多人翻到严蔚敏《数据结构C语言版》线性表那一章照着书上敲第一段代码编译器直接报一屏错。这不怪手教材里写的是类C伪代码Status、ElemType、引用传参在标准C里都不存在。这套资源把这些伪代码逐章翻译成了能编译、能跑出结果的C代码覆盖顺序表、链表、栈、队列、串的KMP算法、二叉树、图的最短路径、查找与排序。适合三类人期末复习和考研党把它当对照材料拿教材的算法描述和真实C代码逐行对齐做课程设计的人直接用顺序表、链表、二叉树这些成品模块当作业底子刷力扣但C语法生疏的人把它当一本带完整代码的语法手册。它的核心价值不是省敲键盘而是让你看到每个算法从纸面到内存的真实边界。2. 把伪代码变成能编译的CStatus、ElemType与引用传参的三道坎2.1 教材伪代码和标准C到底差在哪严蔚敏书里最常见的函数签名是这样写的Status ListInsert(SqList L, int i, ElemType e) { // 算法2.4 }直接复制到.c文件里编译器会告诉你两个错误Status没有定义L不是合法的C语法。前者是因为教材把类型抽象掉了后者是因为C语言没有引用只有指针。C编译器能勉强收下L但大多数人交作业、跑算法题用的都是纯C环境所以第一道坎就是把L改成*L把函数内的L.访问改成L-访问。第二道坎是公共类型。教材里的Status、ElemType、OK、ERROR、OVERFLOW在书中用了一整页说明但具体到每个算法片段里它们只是符号没有定义。我一般会在工程里放一个common.h把所有公共类型和错误码集中定义#ifndef COMMON_H #define COMMON_H #include stdio.h #include stdlib.h #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 typedef int Status; typedef int ElemType; #endifStatus定义为int返回值用OK和ERROR区分成功失败这是最省事的做法也符合教材的约定。ElemType先取int顺序表、链表、栈的测试都用整数如果后面要存结构体、字符串只改这一行typedef调用方代码不用动。实际使用中考研和课设的顺序表几乎全是整数这个默认值足够跑通。2.2 最小工程怎么搭一个公共头文件加一个测试main常见做法是把代码按数据结构拆成模块而不是所有函数堆在一个文件里。我拿到这类资源后的第一步会先搭一个这样的文件结构common.h公共类型、宏、错误码sqlist.h/sqlist.c顺序表linklist.h/linklist.c单链表sqstack.h/sqstack.c顺序栈test_main.c所有验证性测试test_main.c先写一个最简单的环境验证程序确认头文件能搜到、Status类型能编译过#include common.h int main(void) { printf(数据结构C语言版代码实现环境验证\n); printf(Status size: %zu bytes\n, sizeof(Status)); return 0; }编译命令建议带上完整警告选项gcc -Wall -Wextra -stdc11 test_main.c -o test-Wall -Wextra会把教材代码移植时最常见的隐患揪出来比如整型变量当指针用、函数声明了没定义、比较有符号和无符号数。-stdc11是为了让for (int j ...)这种循环内声明变量的写法合法老古董-stdc89会在这里报错。这一步跑通后面所有章节的代码都在这个骨架上续写。3. 线性表与链表顺序表扩容和单链表头结点的边界设计3.1 顺序表动态分配代替教材里写死的MAXSIZE教材的顺序表定义是SqList加一个固定大小的数组这个结构在严蔚敏书里长这样ElemType *elem配合一个length成员。我的处理是给它补上一个listsize用动态内存替代写死的MAXSIZE#define LIST_INIT_SIZE 100 #define LISTINCREMENT 10 typedef struct { ElemType *elem; int length; int listsize; } SqList; Status InitList(SqList *L) { L-elem (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType)); if (L-elem NULL) return OVERFLOW; L-length 0; L-listsize LIST_INIT_SIZE; return OK; } Status ListInsert(SqList *L, int i, ElemType e) { if (i 1 || i L-length 1) return ERROR; if (L-length L-listsize) { ElemType *newbase (ElemType *)realloc( L-elem, (L-listsize LISTINCREMENT) * sizeof(ElemType)); if (newbase NULL) return OVERFLOW; L-elem newbase; L-listsize LISTINCREMENT; } for (int j L-length - 1; j i - 1; j--) { L-elem[j 1] L-elem[j]; } L-elem[i - 1] e; L-length; return OK; } Status ListDelete(SqList *L, int i, ElemType *e) { if (i 1 || i L-length) return ERROR; *e L-elem[i - 1]; for (int j i; j L-length; j) { L-elem[j - 1] L-elem[j]; } L-length--; return OK; }这里有一个必须钉死的约定i是逻辑位置从1开始数组下标从0开始。插入第i个位置数据实际放在elem[i-1]。插入时要从后往前移动元素删除时从前往后方向反了会覆盖还没处理的元素这就是顺序表最常见的错位来源。动态分配的意义在于教材里那个MAXSIZE写死以后插入第101个元素就直接失败而realloc能把顺序表当成一个会自己长大的动态数组。realloc失败返回NULL原内存还在所以一定要用newbase接收返回值不能直接L-elem realloc(...)否则内存泄漏加原指针丢失一次就翻车。3.2 单链表为什么创建函数必须用双重指针单链表的结点定义和教材一致带头结点。创建时用尾插法这是最直观的写法typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList; Status CreateListTail(LinkList *L, int n) { *L (LinkList)malloc(sizeof(LNode)); if (*L NULL) return OVERFLOW; (*L)-next NULL; LinkList tail *L; for (int i 0; i n; i) { LinkList p (LinkList)malloc(sizeof(LNode)); if (p NULL) return OVERFLOW; scanf(%d, p-data); p-next NULL; tail-next p; tail p; } return OK; } Status ListDelete(LinkList L, int i, ElemType *e) { LinkList p L; int j 0; while (p-next ! NULL j i - 1) { p p-next; j; } if (p-next NULL) return ERROR; LinkList q p-next; p-next q-next; *e q-data; free(q); return OK; }创建函数必须接收LinkList *L而不是LinkList L原因是函数内要malloc一个新的头结点并把它交还给调用方。C语言参数是值传递如果只传LinkList L函数内改的是形参副本调用方手里的头指针永远是NULL后续访问L-next直接段错误。调用时写CreateListTail(L, n)这与教材里的L是一一对应的。头结点的价值在删除操作里看得最清楚删除第1个元素时p从头结点开始p-next指向第1个元素不需要像无头链表那样单独写一段“删头”的分支逻辑。删除循环里的j从0开始计数配合j i - 1p最终停在要删结点的前驱上。边界条件是p-next NULL说明位置i超过了链表长度返回ERROR而不是硬删。4. 栈、串与KMPnext数组下标和串长存储的实操4.1 顺序栈top指针语义和realloc之后的位置修正顺序栈定义用base和top两个指针top指向栈顶元素的下一个位置这是严蔚敏版的约定。栈空时top base栈满时top - base stacksize压栈时把值写到*top然后toptypedef struct { ElemType *base; ElemType *top; int stacksize; } SqStack; Status InitStack(SqStack *S) { S-base (ElemType *)malloc(100 * sizeof(ElemType)); if (S-base NULL) return OVERFLOW; S-top S-base; S-stacksize 100; return OK; } Status Push(SqStack *S, ElemType e) { if (S-top - S-base S-stacksize) { S-base (ElemType *)realloc( S-base, (S-stacksize 10) * sizeof(ElemType)); if (S-base NULL) return OVERFLOW; S-top S-base S-stacksize; S-stacksize 10; } *S-top e; return OK; } Status Pop(SqStack *S, ElemType *e) { if (S-top S-base) return ERROR; *e *--S-top; return OK; }realloc之后有一个新手容易漏掉的细节扩容后S-base这个指针可能会变原来算出来的S-top还指向旧地址。所以必须先执行S-top S-base S-stacksize;把栈顶重新定位到新栈区的末尾再改stacksize。顺序栈在实际工程里经常用来做括号匹配下面是完整的验证函数int isBalanced(const char *s) { SqStack stack; InitStack(stack); for (int i 0; s[i] ! \0; i) { if (s[i] ( || s[i] [ || s[i] {) { Push(stack, s[i]); } else if (s[i] ) || s[i] ] || s[i] }) { ElemType top; if (Pop(stack, top) ERROR) return 0; if ((s[i] ) top ! () || (s[i] ] top ! [) || (s[i] } top ! {)) { return 0; } } } return stack.top stack.base ? 1 : 0; }这个函数把栈当成一个黑匣子遇左括号压栈遇右括号弹栈并检查配对。最后检查stack.top stack.base确保没有多余的左括号残留。教材里队列章节提到的双端队列在严蔚敏配套代码里一般不单独实现做题时用数组模拟更高效不必死磕。4.2 KMP算法next数组的两套下标约定必须分清KMP是这套资源里最容易让人对着教材和博客怀疑人生的地方。严蔚敏版的串结构是SString定义成char S[0]存串长字符从S[1]开始next数组下标从1开始void get_next(const char T[], int next[]) { int i 1, j 0; next[1] 0; while (i T[0]) { if (j 0 || T[i] T[j]) { i; j; next[i] j; } else { j next[j]; } } } int Index_KMP(const char S[], const char T[], int pos, const int next[]) { int i pos, j 1; while (i S[0] j T[0]) { if (j 0 || S[i] T[j]) { i; j; } else { j next[j]; } } if (j T[0]) return i - T[0]; return 0; }这套代码能跑的前提是S[0]和T[0]里放的是串长而char只有8位串长超过255就会溢出。很多人看网上博客学KMP用的却是标准C字符串风格下标从0开始next[0] -1长度用strlen单独存。两套代码长得完全不一样硬套必错。标准字符串版应该这么写void get_next_str(const char *T, int next[]) { int i 0, j -1; next[0] -1; int len (int)strlen(T); while (i len - 1) { if (j -1 || T[i] T[j]) { i; j; next[i] j; } else { j next[j]; } } }两套写法的对应关系整理成表对着改不容易乱对比项严蔚敏教材版标准C字符串版字符下标起点10串长存放位置T[0]strlen独立存储next初始值next[1] 0next[0] -1失败回退的哨兵j 0j -1主循环条件i T[0]i len - 1把教材版的T[0]和标准字符串的T[0]搞混是KMP匹配错位最常见的根因。我自己的习惯是课设和考研模拟用教材版刷LeetCode用标准字符串版同一份代码里绝不混用两套下标。5. 避坑指南严蔚敏代码移植到本机的五个翻车现场5.1 编译报错Status未定义scanf被嫌弃现象复制教材里的算法代码到新工程编译器报Status undeclared、OVERFLOW undeclaredMSVC下还会报scanf不安全。原因教材伪代码依赖一套公共类型定义散落在书的前面章节和配套代码的公共头文件里只复制算法片段的人必然缺定义。MSVC的scanf告警是C4996属于编译器认为你用了不安全的旧函数。解决在工程根目录放common.h把所有类型和宏集中在里面每个.c文件第一行#include common.h。被scanf告警烦到的在源文件顶部加#define _CRT_SECURE_NO_WARNINGS或者统一改用scanf_s但所有文件要一致不能一半用scanf一半用scanf_s。5.2 段错误链表创建后头指针还是NULL现象调用CreateListTail(L, 5)之后紧接着访问L-next程序直接崩掉在gdb里看到L的值还是0x0。原因函数形参是LinkList LC语言按值传递函数内部malloc修改的是形参副本调用方的L根本没被赋值。严蔚敏书里的L在C里必须写成LinkList *L。解决创建、销毁、反转这类需要修改头指针的函数统一使用双重指针。定义写成Status CreateListTail(LinkList *L, int n)函数内用(*L)-next访问调用时传L。5.3 下标错位插入第2个位置却变成了第1个现象顺序表中有3个元素调用ListInsert(L, 2, 99)后结果99出现在了下标0的位置原第1个元素跑到后面去了。原因位置i从1开始这个约定在插入函数里没有贯彻到底。插入第2个位置元素要放elem[1]移动循环却从elem[0]开始覆盖。解决插入和删除的边界条件统一插入用j i - 1从后往前移删除用j i从前往后移。写完用i 1和i length 1两个边界各测一次这两个位置最容易出问题测试过了就基本稳了。5.4 KMP的next数组算错T[0]里到底放什么现象get_next打印出来和教材答案一模一样但Index_KMP结果错匹配成功的串返回0。原因用标准C的char数组存串没有把strlen的结果放进T[0]。while (i T[0])读到的不是串长而是串第一个字符的ASCII码于是循环次数完全错乱。解决二选一。要么回到教材定义手动把T[0] strlen(T)填上注意串长不能超过255要么改写next数组的标准字符串版本哨兵值用-1彻底抛弃T[0]存长度的约定。混用是最大的坑。5.5 递归爆栈二叉树在退化链上崩掉现象用前序序列创建二叉树连续输入一串只有左子树的结点程序在递归创建到几千层时栈溢出窗口直接弹错误。原因二叉树创建、遍历、销毁三个函数都是递归实现树退化成单链表时递归深度等于结点数。默认栈空间有限Linux一般是8MBWindows在1MB左右几万层递归必爆。解决结点数少的时候递归没问题超过几千个就要换思路。中序遍历改成显式栈的迭代版本创建和销毁也可以改成后序迭代。做课设时如果树的形态不可控先估算深度不要默认递归一定没事。6. 改造一个自己的算法库测试驱动与模块化收尾6.1 用assert给每个结构体写验证函数这套代码里每个数据结构都能配一个独立的测试函数用assert把边界条件钉死在代码里。顺序表的测试可以这么写#include assert.h void test_sqlist(void) { SqList L; InitList(L); for (int i 1; i 5; i) { assert(ListInsert(L, i, i * 10) OK); } assert(L.length 5); ElemType e; assert(ListDelete(L, 3, e) OK); assert(e 30); assert(L.length 4); ListInsert(L, 1, 0); assert(L.elem[0] 0); assert(ListInsert(L, 99, 1) ERROR); }测试函数比调试器省时间的点在于每改一次边界条件直接跑一遍全部断言不对就立刻知道是哪一行。严蔚敏教材里的算法本身没错错都在边界处理上用断言把i1、ilength1、空表、满表四个场景全部锁死。6.2 模块化的三个收尾习惯一是函数命名统一前缀。顺序表全部叫InitList、ListInsert、ListDelete链表也用同一组名字和STL重名最多加个Sq前缀区分但一套代码里只能有一种风格。二是头文件里加编译开关让这套C代码以后能被C工程调用在common.h头部加这段#ifdef __cplusplus extern C { #endif /* 公共类型声明 */ #ifdef __cplusplus } #endif三是我每次拿到这类代码的第一轮只用一条命令编译加运行gcc -Wall -Wextra -g test_main.c sqlist.c linklist.c -o test ./test-g选项生成调试信息保证编译通过才执行测试。这三件事做完这套教材代码才真正变成你自己的工具库。这套严蔚敏代码我前后改了三轮第一轮让它能编译第二轮改对下标和指针第三轮才敢拿去跑算法题。从那以后我每次拿到一份教材代码都强制先写测试函数把边界条件用断言钉住再动主逻辑。希望帮到你。本文还有配套的精品资源点击获取
返回列表