严蔚敏这本《数据结构(C语言版 第2版)》第一章,看着是整本书最“软”的一章,全是概念、术语、数学式子,很多同学一周翻过去就急着去啃线性表。但以我带过不少考研和期末备考学生的经验看,第一章恰恰是最容易“埋雷”的地方。408统考和多数自主命题院校都爱在绪论部分抠概念细节,判断题里换一个词,答案就反过来了。更关键的是,后面第二章到第七章的所有代码,全部建立在这一章那套“逻辑结构、存储结构、算法分析”的框架之上——你在我这篇博文里花半小时把这章吃透,后面写代码题会顺很多。
这篇内容我打算这么安排:先把第一章的知识框架划出重点,再对课后习题按类型逐类精讲,接着用C语言把抽象概念落到具体代码里,最后整理一份易错点排查清单和复习路线参考。不管你是期末突击、考研一轮,还是自学入门,都可以直接对号入座。
1. 第一章到底在讲什么:不要低估这门“地基课”
1.1 全章节的知识框架
第一章在整本书里扮演的角色,相当于盖楼之前的“设计图纸”。它不直接教你写任何复杂的数据结构,而是回答几个根本问题:计算机里处理的数据长什么样?数据之间有哪些组织方式?这些组织方式在计算机里怎么落地?怎么评价一个数据结构配算法的方案好不好?
把这些点拆开,第一章的知识块一共四块:
- 数据相关术语:数据、数据元素、数据项、数据对象、数据结构。这一组概念纯粹是抠名词,但考试特别喜欢混着考。
- 逻辑结构的四种基本形态:集合、线性结构、树形结构、图状结构(网状结构)。这是从“数据元素之间的关系”角度去分类的。
- 存储结构的四种实现方式:顺序存储、链式存储、索引存储、散列存储。这是从“计算机内存里的实际摆放方式”角度去分类的。
- 算法与算法分析:算法的五个特性、算法设计的要求、时间复杂度、空间复杂度、渐进复杂度(大O记号)。
很多同学复习到这里觉得内容太“虚”,不如后面的栈、队列来得实在。但实际上,第一章的每一句话都在给后面章节做铺垫。比如逻辑结构与存储结构的关系没搞清楚,学到二叉树的时候就会混淆“完全二叉树是逻辑结构还是存储结构”这一类送命题。
1.2 必须吃透的三组核心概念
第一组是数据结构的三要素。课本上的定义是:数据结构包括数据的逻辑结构、数据的存储结构和数据的运算集合。这个“三要素”说法一定要刻在脑子里,因为它是全书分析任何一个数据结构的固定套路。学线性表、学树、学图,都是先看逻辑上是什么关系,再讨论怎么存储,最后讨论有哪些基本操作。
第二组是逻辑结构与存储结构的区别和联系。逻辑结构描述的是数据元素之间的抽象关系,跟计算机无关;存储结构则是逻辑结构在计算机里的物理实现。打个比方:逻辑结构像一张“通讯录关系图”,写着谁是张三的朋友、谁是谁的上级;存储结构则是你实际把这些人名记在纸上还是存在手机里,记的时候是按字母排序还是按加入时间排序。
第三组是抽象数据类型(ADT)的思想。ADT的核心是“封装”,即把数据对象及其操作定义成一个整体,使用者只关心操作“是什么”,不需要关心“怎么实现”。这一点在C语言里最直观的体现就是头文件加源文件的组织方式——头文件里放接口声明,源文件里放实现,外部调用者只include头文件。这个习惯如果从第一章就开始养成,后面写栈、队列的实验报告会特别顺畅。
2. 课后习题逐类精解:从概念题到算法题一网打尽
2.1 术语解释题:答题的标准“话术”
第一章课后题里最常见的一类,就是让你解释“数据”“数据元素”“数据项”“数据对象”“数据结构”这些名词。这类题看着简单,但很多同学丢分就丢在表述不严谨、层次不清楚。
我在复习时总结了一套答题模板,可以套在几乎所有术语题上:
- 先给定义(用教材原话或自己组织的准确表述)。
- 再举一个具体例子。
- 最后补充一句与相邻概念的关系,体现你理解层次清晰。
比如解释“数据元素”,可以这么答:数据元素是数据的基本单位,在程序中作为一个整体进行考虑和处理。例如在成绩管理系统中,一名学生的记录就是一个数据元素;它还可以由若干个数据项组成,比如学号、姓名、成绩就是数据项。数据项是构成数据元素的不可分割的最小单位。
解释“数据结构”时,要特别注意必须涵盖三要素:数据结构是相互之间存在一种或多种特定关系的数据元素的集合,包括逻辑结构、存储结构和数据的运算三个方面。如果只答“带结构的数据元素的集合”,能拿一半分,但不够完整。
2.2 判断题与选择题:出题人最爱埋的坑
第一章的判断题、选择题套路非常固定,翻来覆去就那几个陷阱,但每年都有一大批人踩。我把常见考点整理成了几个判断题,你先自己判断一下,再对比后面的解析。
判断一:数据元素是数据的最小单位。
这句话是错的。数据的最小单位是数据项,数据元素是数据的基本单位。这个“最小”和“基本”的区别就是考点。可以类比一下:一条员工记录(数据元素)由姓名、工号等字段(数据项)组成,字段才是不可以再分的。
判断二:数据的逻辑结构与存储结构是一一对应的。
这句话也是错的。一种逻辑结构可以用多种存储结构来实现。比如线性表,既可以用顺序存储(数组),也可以用链式存储(链表)。反过来,同一种存储结构也可以承载不同的逻辑结构。逻辑结构与存储结构是“一对多”的关系,不是“一一对应”。
判断三:算法的每一步操作必须有确切的定义,不能有二义性。
这是对的,对应算法的“确定性”特性。这里顺便把算法的五个特性一起记住:有穷性、确定性、可行性、输入、输出。注意“有穷性”说的是一个算法必须在执行有穷步之后结束,且每一步都在有穷时间内完成——因此死循环的代码不能叫算法。而“程序”不一定满足有穷性,比如操作系统在没有外部事件时会一直循环等待,这属于程序,不算算法。
判断四:算法可以用各种语言描述,因此算法最终可以由计算机直接执行。
前半句对,后半句错。用C语言、伪代码甚至自然语言写的都是“算法描述”,要变成计算机能执行的指令,还得经过编译、链接等步骤。算法与计算机语言无关,它的核心是求解步骤的逻辑;但算法实现需要具体的语言和运行环境。
判断五:时间复杂度为O(n)的算法一定比时间复杂度为O(n²)的算法快。
这句话错得离谱但特别常考。大O记号描述的是增长率,不是绝对运行时间。一个O(n)的算法在n很小时可能因为常数因子过大而比O(n²)的算法更慢。比如100n和3n²在n=10的时候,前者要1000次操作,后者只要300次。只有在n足够大时,渐近复杂度低的算法的优势才能体现出来。考试时遇到类似说法,只要出现“一定”“必然”“总是”,大概率是错的。
2.3 算法设计题:时间复杂度分析的规范写法
第一章末尾的算法设计题通常有两类:一类是要求写出某个简单问题的递归算法或非递归算法(比如求数组最大值、求数组元素之和、逆置数组),另一类是给出一段程序,要求计算它的时间复杂度。
先说第一类,以“求数组元素的最大值”为例,课后题的常见提法是写一个递归算法,并分析其时间复杂度。参考实现如下:
int findMax(int arr[], int n) { if (n == 1) { return arr[0]; // 递归出口:只有一个元素时它就是最大值 } int subMax = findMax(arr, n - 1); // 先求前n-1个元素的最大值 return (subMax > arr[n - 1]) ? subMax : arr[n - 1]; }这段代码的时间复杂度分析:规模为n的问题,递归调用规模为n-1的子问题,递归出口是n=1;所以一共递归n次,每次递归只做一次比较操作,时间复杂度为O(n)。需要注意的是,如果题目要求“不破坏原数组顺序”,这个递归版本是可以接受的;如果要求“高效”,可以考虑分治法(折半求最大值,再把两个最大值比较),时间复杂度同样为O(n),但递归深度是log₂n量级,栈空间更小。
再说第二类,典型程序段的时间复杂度分析。我给你列几个高频样例,最好自己先算再看答案:
样例一:
int i = 1; while (i <= n) { i = i * 2; }设执行次数为t,则循环结束时满足2的t次方大于n,所以t = log₂n + 1,时间复杂度为O(log₂n)。这类题的核心是“看循环变量的变化规律”,i每次都翻倍,就是对数级。
样例二:
int sum = 0; for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { sum += i * j; } }外层循环n次,内层循环n次,总执行次数为n²,时间复杂度为O(n²)。这里要注意区分“两层循环”不一定就是O(n²)——如果内层循环的上界是外层循环变量,比如j <= i,那总执行次数是1+2+...+n = n(n+1)/2,时间复杂度仍然是O(n²),但常数项不同,考试如果问“渐进时间复杂度”还是回答O(n²)。
样例三:
for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) { // 基本操作 } }内层循环次数随i变化,总执行次数为n(n+1)/2,仅看最高阶项且忽略系数,所以时间复杂度是O(n²)。这类“i到n、j到i”的结构几乎全是O(n²),可以当成经验直接记。
样例四(较难):
int count = 0; for (int i = 1; i <= n; i *= 2) { for (int j = 1; j <= n; j++) { count++; } }外层i每次翻倍,执行log₂n次;内层固定执行n次;相乘得到log₂n乘以n,即时间复杂度为O(nlog₂n)。这个复杂度在后面的排序算法里(归并排序、堆排序)会频繁出现,现在提前混个脸熟。
分析时间复杂度的整体思路是:先找基本操作(通常是循环体内最深层的那句),再计算它的执行次数关于n的表达式,最后用大O记号化简,只保留最高阶项且忽略系数。如果递归,则要写出递归方程,比如T(n) = T(n-1) + O(1)的解是O(n),T(n) = 2T(n/2) + O(n)的解是O(nlog₂n),这两个结论后面会反复用。
3. 用C语言落地第一章:把抽象概念写进代码
3.1 用结构体实现一个简单ADT
第一章讲ADT的时候,很多同学觉得抽象,其实用C语言实现一遍就全通了。以教材里常见的“复数”为例,ADT定义包括数据对象(实部和虚部)以及操作(构造复数、求实部、求虚部、复数的加法和乘法等)。
用C语言写,第一步是定义结构体作为数据对象;第二步是把操作写成函数,并把这些函数声明集中放在头文件里。头文件的写法大致如下:
// complex.h #ifndef COMPLEX_H #define COMPLEX_H typedef struct { double real; // 实部 double imag; // 虚部 } Complex; Complex createComplex(double r, double i); // 构建一个复数 double getReal(Complex c); // 返回实部 double getImag(Complex c); // 返回虚部 Complex addComplex(Complex a, Complex b); // 返回两复数之和 #endif对应的实现文件里再写函数体。这里最值得体会的是“封装”两个字:使用者在主函数里只需要include这个头文件、调用createComplex和addComplex,根本不需要关心Complex结构体内部用了double还是float,也不需要关心加法函数内部怎么算。这跟第一章ADT的“数据对象+数据操作”的定义方式是完全对应的。
我在做这个练习时踩过一个坑:结构体里的两个字段如果定义成float,做复数乘法时中间结果的精度会丢失。后来统一改成double才通过测试。这种细节课本不会写,但实际写代码时很容易暴露。
3.2 逻辑结构与存储结构在代码中的差别
还是用“线性表”举例。线性表是一种逻辑结构,元素之间是一对一的线性关系,有且仅有一个前驱和后继(首元素和尾元素除外)。这种逻辑关系既可以顺序存储(数组),也可以链式存储(指针)。
顺序存储的代码核心是定义数组加长度变量:
#define MAXSIZE 100 typedef struct { int data[MAXSIZE]; int length; } SqList;链式存储的核心是定义结点和指针:
typedef struct LNode { int data; struct LNode *next; } LNode;同一个“线性表”逻辑概念,在C语言里对应两种完全不同的实现。这就是为什么第一章反复强调“逻辑结构是抽象的,存储结构是具体的”——如果你在代码里看到的是一片连续内存,说明是顺序存储;如果看到的是结点之间靠next指针串联,说明是链式存储。
给个自检问题:一棵二叉树用数组存放(比如完全二叉树按层序存入数组),那它的存储结构是什么?答案是顺序存储,而不是树形存储。逻辑结构是树形,存储结构是顺序的,两个维度不能混为一谈。这类题几乎每年都考。
4. 易错点与考试踩坑实录
4.1 最常见的四类翻车现场
第一类:把“数据项”和“数据元素”说反。前面已经强调过一次,但这里还是要单独列出来——考试写错的两个词,概念题基本分全丢。建议记忆锚点:元素是“一条记录”,项是“记录里的一个字段”。
第二类:混淆“逻辑结构”与“存储结构”的分类。比如问“栈是什么结构”,很多同学会答“顺序结构”,这就错了。栈是逻辑结构里的线性结构,它强调的是后进先出的操作特性;至于用数组还是链表实现,那是存储结构的事。再比如“哈希表”的存储结构是散列存储,但它的逻辑结构通常还是线性结构。
第三类:时间复杂度只数循环不数递归调用栈。有些同学分析递归算法时,把“每次递归里的操作”算对了,但忘记递归本身会调用n次,导致少算一个n的量级。比如遍历单链表计算长度的递归版本,每次递归做一次“当前结点是否为NULL”的判断,递归n次,时间复杂度和迭代版本一样都是O(n),不能因为“只有一个函数体”就以为它是O(1)。
第四类:空间复杂度只算变量数组不算递归栈。第一章的空间复杂度通常考两个点:原地操作O(1)和递归栈O(n)。如果用递归求斐波那契数列第n项,代码很简单,但每次递归会压栈,空间复杂度是O(n)。不少同学只看程序里没开数组就说空间复杂度O(1),这是典型失误。
4.2 快速自查清单
考前复习第一章,建议用这份清单过一遍,能快速发现漏洞:
- 能否不看书说出数据结构三要素?
- 能否默写逻辑结构的四种类型并各举一个例子?
- 能否区分顺序存储、链式存储、索引存储、散列存储的优缺点?
- 能否判断一个给定的程序段的时间复杂度,包括单循环、双循环、对数型循环?
- 能否解释“算法的有穷性”与“程序的无限循环”之间的关系?
- 能否用结构体加函数的方式定义一个简单ADT并实现两个基本操作?
- 能否正确区分“数据的最小单位”和“数据的基本单位”?
这七条如果都能做到,第一章就算真正过关了。我自己的经验是,用这七条来自测比做十道题都高效,因为它是按知识板块覆盖的,不是零散地抽测。
5. 复习路线与配套资料怎么配合用
5.1 教材课后题与王道、天勤怎么搭配
很多同学手里除了教材还有王道的辅导书,复习第一章时经常陷入“不知道以哪个为准”的纠结。我的建议是:以教材的习题为主,因为考研自主命题院校的出题风格往往直接源自教材;王道的选择题可以当补充和自测,用来扩展题型覆盖范围。
具体操作可以这样:
第一遍先看教材正文,然后合上书自己说一遍三要素、逻辑结构分类、存储结构分类、算法五个特性,这一步比抄定义有用十倍。第二遍做教材课后题,概念题尽量口述作答,判断题要能说出“为什么对、为什么错”,算法题亲自动手写代码,别看一眼答案觉得会了就直接跳过。第三遍再用王道等资料里的选择题进行训练,目标是每道题都能给别人讲清楚。
5.2 给考研党的时间规划建议
如果你的目标是考研,第一章建议控制在3天以内,最多不超过4天。第一天:通读教材正文,边读边划出概念关键词,把课后的术语题全部口述一遍。第二天:集中做判断题、选择题,把错题对应的教材段落重新读一遍,并整理到错题本。第三天:把算法设计题自己写一遍代码,分析时间复杂度和空间复杂度,然后对比参考答案,重点看精妙之处。第四天:回顾错题本,再对照上面那七条自查清单过一遍,查漏补缺。
这里要注意一个误区:考研复习的时间非常宝贵,但第一章不能直接跳过,不能觉得“反正就是概念,后面再说”。因为后面每个章节都要用这里的术语体系,如果第一章没建立“逻辑结构-存储结构-运算集合”的分析框架,学到二叉树和查找时会频繁返工。
我在实际复习中有个体会:第一章投入的时间不会白费,它决定了你在学后面知识点时是不是“有框架地学”。很多人学到图的时候开始蒙,回头一看,就是因为没有用“逻辑结构是图状、存储结构可以用邻接矩阵或邻接表”这套框架去组织知识。把第一章吃透了,后面自然顺。
最后再分享一个小技巧:把第一章的关键概念做成一页纸的思维导图,不需要多漂亮,但一定要把“逻辑结构-存储结构-数据运算”三个维度作为主干,然后不断往上面挂细节。等你学完整本书,这张图就是你考前最后一晚的复习神器。