☰
严蔚敏《数据结构》C语言代码实现:考研与期末复习的实战指南
2026/10/6 4:26:21 网站建设 项目流程

简介:严蔚敏版《数据结构》是计算机专业经典教材,这份C++实现资源将书中大量伪代码整理为可直接运行的程序,面向正在啃教材的大学生、考研复习者,以及需要快速上手经典算法的开发者。压缩包仅含1个doc文档,约521KB,但代码覆盖面广:数组线性表、链表、双向链表、顺序/链栈、顺序/循环/链队列、KMP算法、二叉树前中后序与层次遍历(递归与非递归)、前中序线索遍历、图的邻接表/十字链表/孩子兄弟法表示、Prim最小生成树、拓扑排序、快速排序、希尔排序和堆排序,均带注释。目前已有1549人学习该资源。拿到后可直接对照教材伪代码运行验证,省去从理论到编码的转换时间,也能通过注释和调试看清插入删除、遍历、建树、最小生成树等经典算法的边界处理细节,对期末复习、考研刷题和课程设计都有参考价值。

1. 严蔚敏《数据结构》代码实现:考研、期末复习为什么都绕不开它

数据结构这门课,最常被追问的一件事就是:代码到底怎么实现。严蔚敏版《数据结构(C语言版)》的代码实现资源,几乎每个计算机专业学生都绕不开——考研 408 里算法大题的原型、期末实验报告的模板、面试手撕代码的开胃菜,最后都指向这一套教材。很多人的真实状态是:书翻了三遍,Status、ElemType 都认识,InitList、CreateList 的函数名也熟悉,真到自己在 VS 里敲一遍,不是编译不过,就是运行崩溃、野指针满天飞。

这份资源把教材里的算法按章节做成能编译、能运行的 C/C++ 代码,覆盖线性表、栈、队列、树、图、查找、排序几大块,并补齐了教材里常被省略的初始化、遍历打印和演示用的 main 函数。它能解决三件事:期末周对着实验报告不知道写什么、考研 408 手写算法总差最后一步、以及自学时“看懂原理但写不出程序”的尴尬。适合正在备战考研、赶课程设计,以及工作后回来补数据结构与算法这门必修课的从业者。把它当一本能直接跑的参考答案,而不是又一本厚教材。

2. 先看骨架再动手:代码包按章节组织,别从 main 函数开始读

拿到代码包的第一件事不是双击某个 .c 文件直接开读,而是先看目录结构。严蔚敏教材的代码实现包,常见组织方式是每章一组文件:线性表对应 sqlist.c、linklist.c,栈和队列对应 stack.c、queue.c,树对应 bitree.c,图对应 mgraph.c、algraph.c,查找和排序各占一两个文件。有的版本还会带一个 C++ 版,把 ElemType 换成模板,文件扩展名变成 .cpp。选哪个版本,取决于你用的是 VS 还是 gcc、是只求过编译还是要交代码作业。

2.1 从 .h 头文件开始:先把 Status、ElemType 这些别名认全

严蔚敏教材代码的第一个门槛不是算法本身,而是自定义类型。打开头文件(以顺序表为例)你会看到:

/* sqlist.h —— 顺序表存储结构定义(教材原型风格) */ #define MAXSIZE 100 /* 表的最大容量,按题目要求修改 */ typedef int ElemType; /* ElemType:表中元素的数据类型,按需替换 */ typedef int Status; /* Status:函数返回值类型,本质就是 int */ #define OK 1 #define ERROR 0 #define OVERFLOW -2 typedef struct { ElemType data[MAXSIZE]; /* 用一维数组存放元素,下标从 0 开始 */ int length; /* 当前表的元素个数,不是容量 */ } SqList;

这里最关键的是三行:Status 是 int 的别名,所以Status InitList(SqList *L)本质上就是int InitList(...);OK、ERROR、OVERFLOW 实际是 1、0、-2,只是让代码看着有语义;ElemType 决定了你要操作的元素类型。你想验证“顺序表存学生信息”这类题目,只需要把typedef int ElemType换成你自己的结构体:

typedef struct { char id[20]; char name[32]; int score; } Student; /* 然后把其他文件里 ElemType 的 typedef 统一成 Student,或直接 typedef Student ElemType */

不把这几行别名记清楚,后面看到的 InitList、GetElem、LocateElem 全是符号,学完一章就忘。所以我一向建议从 .h 往 .c 读,而不是反过来。这版代码包里的公共头文件把所有 typedef 集中放好,先花五分钟认全再往下走,后面会顺畅很多。

2.2 哪些代码能直接跑、哪些只有函数片段:先分清再动手

教材正文里的算法很多是“节选”:插入、删除、查找各给一个函数,没有 main,也没有初始化调用。这份代码实现包补了演示用的 main,下载后基本会看到类似这样的可运行程序:

/* 演示顺序表插入、删除的 main 函数(带初始化) */ #include <stdio.h> #include "sqlist.h" int main(void) { SqList L; ElemType e; int i; InitList(&L); /* 不初始化就插入,length 是垃圾值 */ for (i = 1; i <= 3; i++) { ListInsert(&L, i, i * 10); /* 在第 i 个位置插入 10、20、30 */ } ListTraverse(L); /* 期望输出:10 20 30 */ ListDelete(&L, 2, &e); /* 删除第 2 个元素,被删值保存在 e */ ListTraverse(L); /* 期望输出:10 30 */ return 0; }

我的建议是,拿到每个算法文件后先跑一遍这个 main,别急着读函数实现。跑通了,说明头文件路径和编译方式没问题;跑挂了,第一反应去看 InitList 有没有把 length 置 0、ListInsert 的循环边界是不是写成了i > L->length。把“初始化 → 操作 → 遍历”当成固定测试骨架,以后每验证一个算法都用同一套路。代码包里的演示 main 也基本按这个模式写,做实验报告时可以直接把 main 微调成你的测试用例,省掉从零搭环境的时间。

2.3 编译器选型与配置:VS2019/2022、Dev-C++、gcc 三选一

这一版教材代码是 C89 时代写的,直接扔进新编译器会有一堆兼容问题。最常见的三个:VS 把 scanf、strcpy 列为不安全函数报 C4996;Dev-C++ 里 malloc 不强制类型转换导致 C++ 编译报错;gcc 环境下部分文件缺少#include <stdlib.h>导致隐式声明。三个环境的配置对照如下:

环境典型报错推荐处理
VS2019 / VS2022error C4996: 'scanf' was declared deprecated在源文件第一行加#define _CRT_SECURE_NO_WARNINGS,或项目属性关闭 SDL 检查
Dev-C++ 5.11(默认 C++ 编译)invalid conversion from 'void*' to 'ElemType*'把文件按 C 方式编译,或在 malloc 前加(ElemType *)强转
gcc / OJ 环境implicit declaration of function 'malloc'检查是否包含#include <stdlib.h>,把#include <malloc.h>换成前者

这里有个常见的错误处理:很多人报 C4996 后第一反应是改用 scanf_s,把代码里所有 scanf 替换一遍。我不建议这么干,教材代码、OJ 输入都按 scanf 写,本地换掉,提交时又是一堆不一致。最省事的是第一行加宏开关把安全警告关掉——考试和实验不考安全函数,考的是算法本身。

提示:如果代码包里的 .c 文件被改名成 .cpp,编译器会按 C++ 规则处理,malloc 的隐式类型转换问题会成批出现。复习阶段保持纯 .c 后缀,能少踩一半的编译坑。

3. 顺序表与单链表:把教材 ADT 变成能跑的程序,边界条件是第一关

顺序表和单链表是教材第 2 章的内容,也是实验报告里出现频率最高的代码。这两个东西单独看都不难,难在把“逻辑位置 i”和“数组下标 i-1”的换算搞定,以及把链表指针“先保存、再修改”的顺序记牢。下面按“顺序表 → 链表 → 带头结点对比”的顺序拆开讲。

3.1 顺序表的插入与删除:边界先于代码,移动元素从后往前

顺序表插入的教材典型实现长这样:

/* 在顺序表 L 的第 i 个位置插入新元素 e */ Status ListInsert(SqList *L, int i, ElemType e) { int j; if (i < 1 || i > L->length + 1) { /* 位置非法:小于表头 或 超过表尾+1 */ return ERROR; } if (L->length >= MAXSIZE) { /* 表已满,不能再插入 */ return ERROR; } for (j = L->length - 1; j >= i - 1; j--) { L->data[j + 1] = L->data[j]; /* 从最后一个元素开始整体后移 */ } L->data[i - 1] = e; /* 新元素放到第 i 个位置(下标 i-1) */ L->length++; /* 表长加 1 */ return OK; }

重点在三个地方。第一,位置 i 是从 1 开始数的逻辑位置,数组下标 i-1 才是物理位置,新手写插入最容易在这里差一位;第二,移动方向必须从后往前,如果从前往后移动,data[0] 会被覆盖,后面元素全部错位;第三,循环起止是j = length - 1到j = i - 1,也就是把下标 i-1 及之后的元素整体右移一格。删除是反向操作,循环改成从前往后移动,边界条件变成i < 1 || i > L->length,移动后记得 length--。你可以把删除函数当练习手写一遍,再和代码包里的 ListDelete 对照,重点看循环边界是否一致,这一步能帮你把“位置和下标”的换算彻底练熟。

3.2 单链表的尾插法:先存后继再改指针,防止断链

链表的入门操作是建立带头结点的单链表,教材里最常用的是尾插法:

/* 尾插法建立带头结点的单链表:输入 n 个元素 */ void CreateList_L(LinkList *L, int n) { LNode *p, *r; int i; *L = (LinkList)malloc(sizeof(LNode)); /* 创建头结点 */ (*L)->next = NULL; /* 头结点的 next 先置空 */ r = *L; /* r 永远指向当前尾结点 */ for (i = 0; i < n; i++) { p = (LNode *)malloc(sizeof(LNode)); /* 每轮分配一个新结点 */ if (p == NULL) { return; /* 分配失败要处理,别裸奔往下走 */ } scanf("%d", &p->data); /* 读入数据 */ p->next = NULL; /* 新结点作为尾巴,next 置空 */ r->next = p; /* 当前尾结点指向新结点 */ r = p; /* 更新尾结点为新结点,这是关键 */ } }

这套代码里,r是防止“断链”的核心。没有 r,每次都要从头遍历到末尾再插入,时间复杂度变成 O(n²);有了 r,尾指针跟着新结点走,插入永远是 O(1)。另一个容易踩的坑是 malloc 后不判空:本地可能永远分配成功,一到 OJ 大数据就可能段错误。我自己的习惯是每个 malloc 后紧跟一行判空,宁可多写三行,不赌系统内存。如果要实现“在指定位置插入结点”,核心就是先找到第 i-1 个结点 p,然后s->next = p->next; p->next = s;,这两行的顺序不能反——先改 p->next 的话,原来的后继结点就找不到了,链表当场断掉。这也是面试手撕里最经典的一个考点,值得在纸上走一遍。

3.3 带头结点与不带头结点:差一个头结点,差出一堆 if 分支

严蔚敏教材默认带头结点,但很多习题和 OJ 题给的是不带头结点的版本。两者的本质区别在于:带头结点时,“空表”也是有一个头结点存在的,插入位置 1 的操作和插入其他位置完全一样;不带头结点时,插入第 1 个位置必须单独处理“头指针本身要变”的情况。

操作带头结点不带头结点
空表判断L->next == NULLL == NULL
插入位置 1无需特殊处理要修改头指针L = s
删除位置 1无需特殊处理要修改头指针L = p->next
遍历起点从头结点的 next 开始直接从首结点开始

我的建议是:复习用带头结点版本(和教材、王道讲义一致),交 OJ 时看清题目描述,如果题目说“链表可能为空”或“头指针可能被修改”,基本就是无头结点版。两种写法都要能在一分钟内说出区别,408 的选择题偶尔会在这里设陷阱。如果你觉得教材定义太干,先翻《大话数据结构》里对应章节的图解把概念过一遍,再回到这份代码包验证,效率会高不少。

4. 二叉树到图:递归改非递归、邻接矩阵初始化的现场演示

树和图是数据结构里“看起来难、代码反而短”的章节。代码短是因为教材给的都是框架,真正要动手的部分在遍历顺序和存储结构初始化上。这一章挑二叉树非递归中序遍历、邻接矩阵创建、快排分段三个点展开,都是期末、考研里高频出题的位置。

4.1 中序遍历从递归改非递归:栈里存的是“还没访问的根”

递归版中序遍历人人都能背,非递归版才是期末考试和 408 的重点。核心思路是用一个栈手动模拟递归栈,规则只有三句话:一路向左入栈;出栈即访问;然后转向右子树。代码是教材原版风格:

/* 中序遍历二叉树的非递归算法:T 为根结点 */ void InOrderTraverse(BiTree T) { LinkStack S; /* 辅助栈,元素类型是 BiTree */ BiTree p; if (!T) { return; /* 空树直接返回 */ } InitStack(S); /* 先初始化栈 */ p = T; while (p || !StackEmpty(S)) { if (p) { Push(S, p); /* 根(及左子树根)先入栈不访问 */ p = p->lchild; /* 向左走到头 */ } else { Pop(S, p); /* 左子树为空,出栈 */ printf("%c ", p->data); /* 访问根结点 */ p = p->rchild; /* 转向右子树,继续循环 */ } } }

为什么先入栈不访问?因为中序遍历的顺序是“左-根-右”,栈顶元素只有左子树处理完才能出栈访问。你可以用手工走一遍 A(B(C,D), E):先把 A、B、C 依次入栈,C 的左子树为空,Pop C 并输出;再转 C 的右子树(空),下次循环继续 Pop B 输出;再转 B 的右子树 D,输出 D;最后栈里只剩 A,输出 A,转向 E,输出 E。整个过程是 C-B-D-A-E,和递归结果完全一致。这套“入栈不出栈、出栈才访问”的模型,放先序和后序同样成立,只是入栈和访问的时机换了位置,值得自己改写一遍。代码包里的 LinkStack 是教材的栈实现,直接拿来用就行,别自己重写一个。

4.2 图的邻接矩阵创建:不先清零,跑出来的全都是随机数

图这章的代码量最大,但最容易被忽略的是矩阵初始化。很多实验报告翻车都翻在“忘记把 arc 数组清 0”,局部变量数组默认是随机值,不清零就赋值,打印出来全是乱码。教材风格的创建函数如下:

/* 创建无向图的邻接矩阵存储结构 */ void CreateMGraph(MGraph *G) { int i, j, k, w; printf("输入顶点数和边数:\n"); scanf("%d%d", &G->numVertexes, &G->numEdges); for (i = 0; i < G->numVertexes; i++) { /* 读入顶点信息 */ scanf(" %c", &G->vexs[i]); /* 注意 %c 前有空格,跳过回车 */ } for (i = 0; i < G->numVertexes; i++) { /* 初始化矩阵,必须清零 */ for (j = 0; j < G->numVertexes; j++) { G->arc[i][j] = 0; } } for (k = 0; k < G->numEdges; k++) { /* 逐条读入边 */ printf("输入边(vi,vj)的下标 i, j 和权 w:\n"); scanf("%d%d%d", &i, &j, &w); G->arc[i][j] = w; G->arc[j][i] = w; /* 无向图是对称矩阵,只此一行 */ } }

这个函数里三个细节值得注意。第一,scanf(" %c", &G->vexs[i])里 %c 前的空格是为了吃掉上一次输入残留的回车符,不加这个空格,第一个顶点的字符会被换行符顶掉;第二,清零那段双重循环不能省,教材代码里写在 CreateMGraph 内,但很多改写者会漏掉;第三,无向图和有向图的区别只有一句话,多写G->arc[j][i] = w就是无向图,删掉就是有向图。做实验报告时,把对称赋值改成只在arc[i][j]处赋值,就能快速演示两种图在存储上的差异。

提示:你去对照代码包里 mgraph.c 的完整版就会发现,教材只给了 CreateMGraph 的骨架,而资源里往往还配了 LocateVex、PrintMGraph 这些辅助函数。跑图算法之前先把这两个函数跑通,后面调试 DFS、BFS、Prim 会省很多事。

4.3 快速排序的分段函数:408 手写排序过程就靠它验证

排序算法里,快排的分割函数 Partition 是考研 408 手写排序过程题的核心。教材的实现是覆盖式写法:

/* 快速排序一趟分段的实现:把 low 位置元素放到最终位置 */ int Partition(SqList *L, int low, int high) { int pivotkey; pivotkey = L->data[low]; /* 先用第 low 个元素当枢轴 */ while (low < high) { /* 从两端交替向中间扫描 */ while (low < high && L->data[high] >= pivotkey) { high--; /* 右侧找比枢轴小的,找到才停 */ } L->data[low] = L->data[high]; /* 小的覆盖到左侧空位 */ while (low < high && L->data[low] <= pivotkey) { low++; /* 左侧找比枢轴大的,找到才停 */ } L->data[high] = L->data[low]; /* 大的覆盖到右侧空位 */ } L->data[low] = pivotkey; /* 枢轴归位,此时 low == high */ return low; /* 返回枢轴最终位置 */ }

用 {5, 3, 8, 1, 7} 手动走一遍:pivotkey 取 5,右侧先找到 1 覆盖 5 的位置,序列变 {1, 3, 8, 1, 7};左侧再找到 8 覆盖右侧空位,变 {1, 3, 8, 8, 7};右侧继续扫没有更小的,循环结束,把 5 放回中间,得到 {1, 3, 5, 8, 7},枢轴 5 落在下标 2。这就是一趟快排的完整过程。考研数据结构复习时,我习惯把这段代码先在纸上跑一遍,再用程序打印每一步数组状态来核对,手写结果和运行结果一致,这道题才算真正过关。后面堆排序、归并排序的代码实现也按同一套方法验证,排序算法这章的资源基本都配了完整可运行的测试,不需要自己再搭壳子。

5. 避坑清单:编译报错、野指针与 OJ 判题不一致的五个经验

这一章不打算讲新算法,专门整理我在用这套代码实现包时踩过的、以及帮别人排过的高频问题,按“现象 → 原因 → 解决”的方式记,每条都能直接对号入座。

5.1 编译期翻车:C4996、隐式声明与 malloc 类型不匹配

现象:把代码包里的 .c 文件原封不动扔进 VS2019,编译报错 C4996,提示 scanf 被弃用;换 Dev-C++ 打开,又报 malloc 从 void* 到 ElemType* 的转换错误。

原因:教材代码是 C89 时代写的,而 VS 从 2015 年起默认把 scanf、strcpy 这族函数列为不安全 API;Dev-C++ 默认按 C++ 编译,malloc 返回 void*,在 C++ 里 void* 不能隐式转换为任何具体类型的指针,必须强转。

解决:VS 下在源文件第一行加#define _CRT_SECURE_NO_WARNINGS,注意必须放在所有 #include 之前,否则预处理顺序不对仍然报错;Dev-C++ 下保持文件后缀为 .c 并按 C 方式编译,或给 malloc 加(LNode *)强转。gcc 环境如果报 malloc、strcpy 隐式声明,去头文件里确认#include <stdlib.h>和#include <string.h>都在,别用#include <malloc.h>,它不是 C 标准头文件,OJ 很可能没有。

5.2 运行期崩溃:free 之后指针没置 NULL 的双重释放

现象:主函数里调用销毁链表的函数后程序没报错,但紧接着再调用一次遍历函数,输出乱码或直接弹“内存访问冲突”;如果连续调用两次销毁函数,干脆崩溃退出。

原因:free 只释放堆内存,不改变指针变量本身的取值。销毁函数内部释放完结点就返回了,主函数里的头指针仍然指向那块已被系统回收的内存,变成野指针,第二次访问属于未定义行为;连续两次 free 同一块内存,glibc 直接抛 double free。

解决:销毁函数里每释放一个结点,先把后继指针保存下来,释放后将头指针置 NULL;主函数调用销毁后,也手动把头指针置 NULL。我在代码包里见过好几种销毁写法,能跑和不能跑的差别就在这一行*L = NULL;。从那以后凡是涉及 free 的代码,我都习惯“释放后置空”,这个习惯带到工程代码里同样适用。

5.3 结果期翻车:OJ 判题与本地输出对不上

现象:本地 VS 里运行测试用例,输出完全符合教材;提交到 OJ 评测,要么答案错误 WA,要么运行时错误 Runtime Error,而同样的代码在本地就是好的。

原因:本地和 OJ 的环境差异叠加,最常见的三个——数组容量开小了,本地测试数据规模小没触顶,OJ 数据一到上限就越界写崩溃;scanf 没判断返回值,本地输入格式规范没问题,OJ 输入尾部多个空格或少一行,scanf 返回值和预期不符;printf 多输出了空格或换行,OJ 对输出格式极其敏感。

解决:数组大小按题目上限再加 5 个冗余位;读数据时用while (scanf("%d", &x) != EOF)或判断 scanf 返回值;输出时最后一个元素后面不跟空格,每行末只留一个换行符。这套经验在期末上机考试里同样管用,很多同学期末机考就差在这类细节上。另外,OJ 一般没有<conio.h>、<windows.h>这类头文件,代码包里凡是用了 system("pause") 的地方,交 OJ 前都要删掉,否则直接编译错误。

6. 自测与收尾:把课本代码固化进实验报告和 408 答题流程

代码能编译、能运行只是第一关,能不能把这份资源用出效果,取决于你拿到手之后怎么验证、怎么引用。我自己在复习和做实验报告时,固定流程就四步:第一步直接编译运行演示 main;第二步拿小规模数据跑一遍手工结果,和代码输出对比;第三步对着教材伪码逐函数核对差异;第四步把验证过的代码片段摘进实验报告或错题本。这套流程里,最重要是第二步的验收清单:

核心算法推荐自测数据预期输出
顺序表插入删除依次插入 10、20、30,再删第 2 个第一次遍历 10 20 30,第二次 10 30
尾插法建链表输入 1、2、3、4、5遍历输出 1 2 3 4 5,尾结点 next 为 NULL
中序非递归遍历二叉树 A(B(C,D),E)输出 C B D A E
快排一趟 Partition数组 {5,3,8,1,7}一趟后 {1,3,5,8,7},枢轴落在下标 2
KMP 求 next 数组模式串 "abab"(教材 1 起始下标约定)next 值为 {0,1,1,2}

做实验报告时,我不建议把整个 main 函数原样贴进去。常见写法是“算法思想(两三句话)→ 核心函数代码(只贴被测试的那一个)→ 测试截图 → 结果分析”四段式,表格里的验证数据正好当“结果分析”的素材。考研 408 复习则反过来,不要依赖运行结果,先用表格里的数据在草稿纸上写排序过程、遍历序列,再跑程序核对,这样才能逼出“能手动推导”的能力——考场没有编译器可用。

最后一个技巧:写测试用例的顺序要逆着实现写。也就是每准备实现一个函数,先把它的验收条件写出来,比如“删除第 2 个元素后 length 减 1、第 2 个位置变成原第 3 个元素”,再开始写代码。这能逼你把边界条件想清楚,而不是写完代码再找测试碰运气。从那以后,我每次拿到一份数据结构代码,都强制自己按“先编译、再跑手工用例、最后读源码”的顺序走一遍,顺序反过来的话,代码读得再明白,手一抖照样崩。这份代码实现资源,希望在期末周和考研路上帮你少救一次急、多续一口气。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询