资讯动态

严蔚敏数据结构C语言版:链表与排序算法实战指南

发布时间:2026/10/14 12:35:49 来源:尧图企业网站定制
简介《数据结构C语言版》是严蔚敏、吴伟民合著的经典教材面向高校计算机专业学生、考研备考者以及希望系统提升编程基础的自学者适合课程同步学习、考研专业课复习和面试前算法梳理等场景。全书系统讲解线性表、栈、队列、串、树、图、查找与排序等核心数据结构并以C语言给出典型算法实现帮助读者理解抽象数据类型与底层存储结构夯实算法设计与编码能力。压缩包内共1个文件为PDF格式大小约29.15MB内容为完整书籍电子版可直接在电脑或移动设备上阅读、检索与标注。目前已有4000余人学习浏览是计算机学习者常用的权威参考版本。借助这份PDF读者可系统复习课程内容、查阅经典算法代码与复杂结构讲解尤其适合考研专业课梳理和重难点研读弥补零散笔记的不足构建完整的数据结构知识体系。1. 严蔚敏数据结构C语言版不是拿来背的第一本书先把“劝退章节”挪到后面收到这份严蔚敏《数据结构C语言版》PDF第一件事不是翻正文而是先认清它的定位。大多数人被劝退的场景都一样从绪论看起先撞上时间复杂度再撞上抽象数据类型链表还没见面就晕了。它不是小说不该从头到尾读它是用来当字典、当题库用的。严蔚敏、吴伟民这版是老牌教材也是408考研绕不开的参考书把线性表、栈队列、串、树、图、查找、排序这些核心内容用类C伪代码讲完。适合三类人准备考研408的、期末复习数据结构课的、在写C但想系统补算法底子的。这篇直接拆它的打开方式、伪代码翻译、链表与排序落地写法、还有最容易翻车的几个坑。2. 先读目录再读正文严蔚敏这版的章节编排、伪代码和真C的差距2.1 十一章结构和408权重哪几章值得先看哪几章可以后放拿到PDF后先翻目录。严蔚敏、吴伟民《数据结构C语言版》的章节顺序是绪论、线性表、栈和队列、串、数组和广义表、树和二叉树、图、动态存储管理、查找、内部排序、外部排序。注意一点这十一个章节的考试权重差距很大阅读顺序不该照着页码来。章内容408与期末权重建议1绪论复杂度、ADT中常考概念选择放到写过程序之后再看2线性表高代码题主战场最先读配合代码练3栈和队列高第二个读4串低可跳过大部分5数组和广义表中记住压缩存储公式6树和二叉树高重点章节7图高重点章节8动态存储管理低用到再回看9查找高二叉排序树、哈希必看10内部排序高算法和稳定性都考11外部排序低了解归并思想这张表比很多目录页都有用。我一般拿到一本数据结构书先干的事就是把核心章标出来把“动态存储管理”这章往后放。这章讲的是可利用空间表、边界标识法、伙伴系统内容是好的只是和操作系统课程重叠更多408统考里很少以数据结构大题形式出现期末更是几乎不碰。2.2 阅读顺序先动手写线性表和栈队列回头再啃绪论这套教材的正确打开方式是从第2章线性表开始一路写到第3章栈和队列期间配合把代码敲一遍然后再翻回第1章看时间复杂度的定义。为什么这么绕因为“时间复杂度”不是一个能靠背诵建立的概念。我在第一次看这书时被绪论里的数学符号劝退后来写完冒泡排序和链表插入再回来看O(n)、O(n²)发现不过是数循环次数而已。先有代码经验再去读绪论你会发现那章讲的是你已经在做的事情的理论化表达。2.3 伪代码翻译成可编译C代码Status、ElemType、 的三步处理这版教材里的算法用的是类C描述不是标准C。你会在书里看到 Status ListInsert(LinkList L, int i, ElemType e) 这样的签名它不能直接塞进gcc编译。常见的翻译套路有三步Status改成intElemType替换成具体数据类型引用改成指针。教材之所以保留这种风格是为了让同一个算法能适配不同的数据类型和不同语言但落到键盘上就得自己动手。// 严版伪代码Status ListInsert(LinkList L, int i, ElemType e) // 翻译成可编译的C语言实现 typedef int ElemType; // 让ElemType有具体类型 typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList; // 在第i个位置插入元素e二级指针是因为可能改头指针 // 这里按严版教材习惯使用带头结点的链表 int ListInsert(LinkList *L, int i, ElemType e) { if (i 1) return 0; // 位置不合法 LNode *p *L; int j 0; while (p j i - 1) { // 找到第i-1个节点 p p-next; j; } if (!p) return 0; // 位置超过表长 LNode *s (LNode *)malloc(sizeof(LNode)); if (!s) return 0; s-data e; s-next p-next; // 先接后半段 p-next s; // 再接前半段 return 1; }这段代码里最需要理解的参数是 *L。严版伪代码写 L是因为插入位置可能在头节点之前需要修改调用方的头指针C语言里没有引用就用二级指针传头指针的地址。如果你能保证只在中间插入一级指针LNode *L也够用但教材算法普遍考虑的是通用情况。使用前要先创建头结点*L (LinkList)malloc(sizeof(LNode)); (*L)-next NULL;。malloc出来的节点必须检查是否为空这是很多初学翻车的起点后面避坑章会专门说。第3章的链表写法会换成不带头结点版两种风格在OJ、期末和408里都会遇到都要认得。3. 链表动手指针、动态内存分配和边界处理的C语言实现3.1 为什么先写链表树和图的结构都在复用这套思想数据结构这门课里最难跨过的坎是“地址”这个抽象概念。结构体里嵌套一个指向同类结构体的指针听起来绕写起来就三行代码。而后面第6章树的孩子兄弟表示法、第7章图的邻接表本质上都是链表这套“节点指针”的组合。所以我不建议跳着学先把链表写透后面是顺水推舟。链表这块要掌握的就三件事建表、删节点、释放内存每件都和指针的直接操作相关也是C语言内存管理最集中的体现。3.2 头插法和尾插法两种建表方式一个决定顺序一个决定效率链表的建表有两种经典写法区别在于新节点放哪里。头插法把每个新节点插在最前面代码短但得到的链表顺序和输入顺序相反尾插法需要额外维护一个tail指针代码多一点但能保持输入顺序。实际做题时两个都要手写因为有的题目明确要求“逆序输出”用头插法最省事。#include stdio.h #include stdlib.h typedef struct Node { int data; // 数据域 struct Node *next; // 指针域指向下一个节点 } Node; // 头插法新节点永远插在旧头前面 Node *createByHead(int arr[], int n) { Node *head NULL; for (int i 0; i n; i) { Node *s (Node *)malloc(sizeof(Node)); if (!s) exit(1); // malloc失败直接退出 s-data arr[i]; s-next head; // 新节点指向旧头 head s; // 新节点成为头 } return head; } // 尾插法需要tail指针记录链表末尾 Node *createByTail(int arr[], int n) { Node *head NULL, *tail NULL; for (int i 0; i n; i) { Node *s (Node *)malloc(sizeof(Node)); if (!s) exit(1); s-data arr[i]; s-next NULL; if (head NULL) { // 第一个节点 head s; } else { tail-next s; // 接到末尾 } tail s; // 更新尾指针 } return head; }参数上要留意的是 createByTail 里的 head 和 tail 两个指针。第一次插入时 head 和 tail 都指向同一个节点从第二次开始tail-next 负责把新节点挂上去然后 tail 自己往后移。很多人的错误是忘记给 s-next 置 NULL导致最后一个节点变成野指针遍历时不知道停在哪里。头插法没有这个问题因为 s-next 总会指向上一个头最后一次插入后它指向NULL顺序刚好反过来。3.3 按值删除节点头节点、尾节点和不存在的值三种情况一起处理删节点是链表中边界条件最多的操作。常见套路是设一个prev前驱指针跟着cur往后走找到目标后让prev-next绕过cur。但有一个例外如果删的是头节点prev是NULL不能通过prev-next改指针必须直接移动传入的头指针本身。所以删除函数要接收二级指针。// 删除第一个值为target的节点删除成功返回1 int deleteByValue(Node **head, int target) { if (*head NULL) return 0; // 空表没有可删的 Node *prev NULL; Node *cur *head; while (cur cur-data ! target) { // 边遍历边记前驱 prev cur; cur cur-next; } if (cur NULL) return 0; // 没找到 if (prev NULL) { *head cur-next; // 删的是头节点 } else { prev-next cur-next; // 中间或尾节点 } free(cur); // 释放目标节点 return 1; }这个函数的参数 **head 和前面 ListInsert 是同一个套路。如果你只传 Node *head删除头节点时函数内部只能把局部变量的head改掉调用方手里的头指针还是指向那块已被释放的内存后面再访问就是悬空指针。等值删除时你可能碰到删尾节点prev-next 拿到的是cur的next也就是NULL正好让链表正确结束。找不到值时cur会走到NULL再退出循环返回0这也是链表和数组操作的重要区别数组越界是错误链表走到NULL是正常结束。3.4 释放整条链表先记下next地址再free当前节点这是初学者最容易写错的一段代码。很多人第一次写释放函数时这么干while (head) { free(head); head head-next; }看着有模有样实际上一执行就段错误。原因很简单free(head)之后head指向的内存已经归还系统head-next 访问的是不该再访问的地方。// 释放整条链表先保存下一个地址再释放当前节点 void freeList(Node *head) { while (head ! NULL) { Node *next head-next; // 先把下一个地址存下来 free(head); // 释放当前节点 head next; // 指针再往前走 } }这个顺序就是“先存后删”。自由内存本身不复杂复杂的是别让指针指向已释放的块。如果你用Valgrind跑一遍程序常见报错是 Invalid read发生在释放后的遍历里把这块代码改成上面这种写法报错立刻消失。链表写到这里才算是真正把指针和内存两件事理顺了。4. 排序算法严版教材参数表和能直接跑的C实现4.1 冒泡排序的两层循环n-1和n-1-i都在解决什么问题冒泡排序在C语言题里出现频率极高但它不是最简单的代码而是最容易把边界写错的一题。外层循环决定第几轮内层循环决定这一轮比较到哪个位置。核心规则是每一轮都会把一个当前最大值送到最终位置所以下一轮可以少比较一个元素这就是内层条件 n-1-i 的来源。// 冒泡排序升序 void bubbleSort(int a[], int n) { for (int i 0; i n - 1; i) { // 最多n-1轮 int swapped 0; // 记录这轮有没有交换 for (int j 0; j n - 1 - i; j) { if (a[j] a[j 1]) { int t a[j]; a[j] a[j 1]; a[j 1] t; swapped 1; } } if (!swapped) break; // 一轮没交换说明已序 } }为什么外层不写成 i n因为n个元素排n-1轮之后最后一个元素位置已经是确定的不需要再排一轮。加了swapped标记之后最好情况下只跑外层第一轮时间复杂度从O(n²)降到O(n)这是408选择题很喜欢埋的考点。有一个常见误用是内层写成 j n - i会导致最后一次比较越界访问a[n-i]下标越界在C里不报错但会读到脏数据排序结果时好时坏这种问题在考场和OJ上都相当隐蔽。4.2 快速排序的partition枢轴、递归边界与三数取中快排在严版教材里是重点也是408代码题的高频考点。教材的经典实现是挖坑法取第一个元素作枢轴然后low和high两个指针交替扫描。这个逻辑经典的代价是如果数组已经有序每次取的第一个元素都是最小值分割出的左区间永远为空递归深度直接变n时间复杂度退化到O(n²)。我自己的习惯是加一个三数取中取low、high、mid三个位置的中位数换到开头把最坏情况拉回可控范围。// 三数取中把low、mid、high的中位数换到low位置 int medianOfThree(int a[], int low, int high) { int mid low (high - low) / 2; if (a[low] a[mid]) { int t a[low]; a[low] a[mid]; a[mid] t; } if (a[low] a[high]) { int t a[low]; a[low] a[high]; a[high] t; } if (a[mid] a[high]) { int t a[mid]; a[mid] a[high]; a[high] t; } return mid; } int partition(int a[], int low, int high) { int pivotIdx medianOfThree(a, low, high); int pivot a[pivotIdx]; int t a[pivotIdx]; a[pivotIdx] a[high]; a[high] t; // 藏到最右边 int i low; for (int j low; j high; j) { if (a[j] pivot) { int tmp a[i]; a[i] a[j]; a[j] tmp; i; } } a[high] a[i]; a[i] pivot; // 枢轴落位 return i; // 返回枢轴最终下标 } void quickSort(int a[], int low, int high) { if (low high) { // 递归终止条件 int p partition(a, low, high); quickSort(a, low, p - 1); // 左边再排 quickSort(a, p 1, high); // 右边再排 } }这段partition和严版教材的挖坑法思路等价但用的是扫描交换方式写起来更稳。参数low和high都是闭区间调用时传0和n-1别传n否则会越界访问。递归终止条件是low high也就是说区间里只剩一个或没有元素时不再排。如果在一台真实机器上跑递归深度可以加一个计数器观察我给学生的建议是把数组规模加到十万对比“直接取a[low]”和“三数取中”两种写法的运行时长差别立刻能看出来。4.3 排序参数表复杂度、稳定性一起背才有用排序这一章的考试重点从来不是代码本身而是“算法之间的对比”。下面这张表是严蔚敏教材和408真题反复出现的内容建议直接背下来。算法平均时间最坏时间辅助空间稳定性适用场景直接插入O(n²)O(n²)O(1)稳定数据量小、基本有序冒泡O(n²)O(n²)O(1)稳定教学、小规模快速排序O(nlogn)O(n²)O(logn)不稳定通用排序首选堆排序O(nlogn)O(nlogn)O(1)不稳定空间受限的大数据归并排序O(nlogn)O(nlogn)O(n)稳定外部排序、要求稳定背这张表有一个技巧不要单独背要串起来理解。快排平均最快但不稳定最坏还退化成n²堆排最坏也是nlogn且不用额外空间但稳定性丢了归并稳定但辅助空间最多。期末考试喜欢考的“稳定的排序有哪些”答案就是直接插入、冒泡、归并这三个因为它们的交换都发生在相邻元素之间没有跨距离交换所以相同元素的相对顺序不会被破坏。5. 避坑严蔚敏PDF自学路上的五个高频翻车点5.1 fgets读名字换行符悄悄进了链表节点现象用fgets读节点名字然后按顺序遍历打印链表每个名字后面都多出一个空行排序结果也变得不对。原因fgets把一整行读进缓冲区末尾的\n会保留下来。C语言里这个换行符就是普通字符排在空格前面比较字符串时自然捣乱。解决读完以后把结尾的换行符替换成字符串结束符。char buf[64]; fgets(buf, sizeof(buf), stdin); // 输入 zhang san 时buf末尾带\n buf[strcspn(buf, \n)] \0; // 把第一个\n改成字符串结束符strcspn返回的是缓冲区里第一个\n的下标把那个位置写成\0字符串就在那里结束。如果文件是Windows回车换行还要顺手处理\r常见写法是再判断一次buf[strlen(buf)-1] \r。这个坑在期末实验报告的数据读取环节特别常见处理完换行链表打印就正常了。5.2 free之后继续往前走段错误一闪一闪现象程序运行时有时正常偶尔段错误用Valgrind跑一次报Invalid read。原因典型的释放顺序写反了。写完链表释放函数时一边free当前节点一边访问它的nextfree之后那块地址已经不能碰访问到的可能是被回收的脏内存也可能直接崩溃。解决先存next再free当前节点。上一章freeList那个写法的核心就是先执行 Node *next head-next; 再执行 free(head);。从那以后我写所有涉及释放的动态结构都会先确认“保存后继地址”这句在free之前。这不是玄学是内存管理的基本纪律。5.3 scanf读不了带空格的名字现象输入“zhang san”当作节点名结果数据域里只有“zhang”“san”被留在了输入缓冲区下一次scanf直接读到它。原因scanf的%s在遇到空白字符就停止空格、制表符、回车都会断掉一次读取。解决数据行用fgets取代scanf。如果题目限定必须用scanf可以用scanf(%[^\n], buf)把整行读进来但要先清掉上一行留下的回车否则第一件事就是吞空。int c; while ((c getchar()) ! \n c ! EOF); // 清掉残留回车 scanf(%[^\n], buf); // 读到换行前为止这个写法的代价是格式串变长缓冲区溢出风险更大实际项目里我更愿意直接用fgets。期末考试里如果出了“读入带空格的字符串”这种题记得第一时间想到fgets而不是scanf。5.4 快排遇到已排序数组递归深度直接爆炸现象对一个已经排好序的大数组做快速排序程序没有死循环但运行时间比乱序数组还长小机器上直接栈溢出。原因严版教材的经典快排默认取第一个元素作枢轴。有序数组的第一个元素恰好是极值划分结果一边为空、一边是n-1个元素递归树退化成一条链深度变成n辅助空间从logn退化到n。解决加三数取中或者在一开始把数组随机打乱一次。前者是工程里的常见做法后者在408笔试里不推荐因为你不能保证输出稳定。另外可以把递归深度很小的小区间改用直接插入排序这样既省递归又利用了小数组插入排序更快的特性。这个优化在很多开源排序库里都有不只是快排的专利。5.5 VSCode里配C环境中文路径、环境变量和tasks.json现象VSCode装了C/C扩展也装了MinGW写完链表代码一点“运行”却提示找不到gcc或者编译过了但终端里中文乱码。原因常见的有三类MinGW的bin目录没加进系统PATHVSCode终端里找不到gcc项目放在“D:\资料\数据结构”这种中文路径下gcc解析路径时编码错乱代码文件是UTF-8编码Windows终端默认GBK中文字面量打印出来就是乱码。解决把MinGW的bin目录加到系统环境变量PATH项目目录改成纯英文。编译配置里用gcc的“-g”和“-o”参数在tasks.json里明确指定编译动作。# tasks.json里编译C程序的核心配置 command: gcc, args: [-g, -o, ${fileDirname}/${fileBasenameNoExtension}, ${file}], cwd: ${fileDirname}这个配置能让当前文件编译成同名的可执行文件。输出乱码的应急办法是在终端先执行chcp 65001切到UTF-8代码页或者把printf里的中文字面量改成英文再跑。这些都不影响链表代码本身的正确性但第一次跑崩的时候人很容易误以为是自己的算法写错了白白浪费时间排查环境所以单独列一条。6. 把PDF里的每一段伪代码变成你机器上能跑、能断点、能看内存的工程6.1 用gdb观察链表地址、数据和next指针一次看清看书上的链表示意图永远不如在调试器里看清楚一次。步骤很简单编译时加-g然后把断点设在main函数运行到链表创建完后用几个命令直接看结构。gcc -g -o list list.c # 带调试信息编译 gdb ./list break main run print head # 看头指针地址 print head-data # 看第一个节点的数据 x/4xw head # 连续查看头节点起始的4个内存单元print head-data输出的是第一个节点的数据x/4xw会按十六进制显示内存里连续的四个字能看到data和next的原始存储。建议你亲手盯着next的值再对比gdb里head-next的地址两边对上链表“节点指针”的模型算彻底建立了。6.2 和王道408、期末题库配合教材管定义题库管训练严蔚敏这版的定位是权威定义来源和算法参考但如果你直接拿它备考会感觉代码风格和考研题目有落差。我常用的搭配是先读教材里对应章节的概念定义再去王道408对应章节做题最后把真题代码题手写一遍。教材里的ADT描述适合理解王道题里的代码更接近考场要求。比如教材用Status和ElemType王道真题直接写int和Node*前者抽象后者直白。平时练习时我建议你写代码用直白风格答概念题时以教材定义为准比如“稳定排序”的判定标准教材说一句“相等元素相对次序不变”考试画个表就能拿分。6.3 我现在的默认动作每个算法读完就开一个main.c跑通它以前有过一段只看PDF不敲代码的时间等到写二叉树遍历时递归参数总是传反调试了很久才发现是对递归结束条件没手感。从那以后我每次拿到一份数据结构PDF都不会从第1页开始翻而是先跳到目录挑一个算法写一个能编译运行的main.c跑通一遍才开始下一章。链表、栈队列、树、图、查找、排序每个结构对应一个文件一个测试main至少跑正常输入和空表、单节点这两个边界输入。这个习惯保持了几年数据的结构就在你的机器上不只在纸上。如果你也正卡在严蔚敏这版的某一章希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑