我当年考研复习数据结构,翻开王道的书,第一页就是绪论。说实话,第一遍我基本没看懂:这一章不就是在介绍“什么是数据结构”吗?后面写代码的时候不就知道了吗?后来我才发现,这一章是整个数据结构的坐标系,后面的线性表、树、图、排序算法,统统都在围绕这一章奠定的概念转。如果你现在也觉得绪论又碎又抽象,这篇内容就是写给你的。
这一章解决三个问题:数据到底怎么组织、算法好坏怎么评价、时间空间复杂度怎么算。它既是考研408和期末考试的起点,也是面试手写代码的底层逻辑。不管你是正在准备考研、应付期末,还是自学编程想补基础,把绪论和算法分析吃透,后面会省很多力气。
1. 绪论到底在讲什么:先搞清楚数据结构的研究对象
1.1 从“数据”到“数据结构”的几个层次
第一次看到数据、数据元素、数据项、数据对象这几组词,我心里是拒绝的。它们不是完全一样吗?后来我用图书馆的比喻才理顺。
数据就是图书馆里所有的书,是原始的、没整理的信息载体。数据元素是某本具体的书,是对一个个体进行描述和处理的单位。数据项是书封面的书名、作者、出版社这些字段,相当于一个人身上的姓名、年龄、职业。数据对象则是同一类书的集合,比如所有计算机类书籍。
为什么要分这么细?因为后边实现顺序表、链表、二叉树时,操作的粒度就是数据元素。你写代码的时候,是在处理一个个的数据元素,而不是抽象的一堆字节。很多同学在写插入、删除操作时不知道函数参数该传“元素”还是“节点指针”,根源就是没分清数据元素和数据项的关系。
数据结构定义里那句“相互之间存在一种或多种特定关系的数据元素的集合”,重点在“关系”。单看一个数据元素没有结构意义,只有多个元素之间存在联系,才谈得上“结构”。所以绪论里反复强调逻辑结构和存储结构,本质上都在描述元素之间的关系。
1.2 逻辑结构、存储结构:一个管“怎么看”,一个管“怎么存”
逻辑结构是数据元素之间的抽象关系,有四种:集合、线性、树形、图状。线性结构一对一,树形一对多,图状多对多。这个分类只关心“谁和谁有关系”,不关心你在内存里怎么放。
存储结构也叫物理结构,是逻辑结构在计算机里的表示方法,主要四种:顺序存储、链式存储、索引存储、散列存储。顺序存储把逻辑相邻的元素放到物理相邻的空间,链式存储用指针串联逻辑上相邻的元素,索引存储额外建一张索引表,散列存储通过散列函数直接定位。
我刚学的时候总觉得逻辑结构可以和存储结构对应起来,其实不对。一个线性表既可以用顺序存储写成顺序表,也可以用链式存储写成链表。逻辑关系是“线性”的,但物理实现方式完全可以不同。考试就爱拿这种对应关系出辨析题,比如“链式存储结构只能用于线性结构”,这句话就是错的,因为树、图也可以用链式存储实现。
这里我踩过比较久的一个坑:以为“线性表”和“顺序表”是一个东西。其实线性表是逻辑结构,顺序表只是线性表在顺序存储下的具体实现。链表则是线性表在链式存储下的实现。概念后面跟一个“表”字,往往已经混入了存储方式,做题时眼睛要尖一点。
1.3 抽象数据类型(ADT)与算法特性
抽象数据类型是一个三元组:数据对象、数据关系和基本操作集合。说白了,你定义了一个List,规定它能insert、delete、search,但不规定底层是用数组还是链表。这就是“抽象”,使用者只关心行为,不关心实现。
我一直觉得ADT是软件工程里的“接口思维”。你先定义好这个类型能干什么,再去写内部实现,这样写出来的代码可以替换实现而不影响使用。绪论里讲ADT,并不是为了让你背定义,而是让你建立“先设计后编码”的习惯。
另外,算法必须满足五个特性:有穷性、确定性、可行性、输入和输出。有穷性不是说程序不能死循环,而是必须能在有限步内结束;确定性是同样的输入必须得到同样的结果。有些同学把“程序”和“算法”混在一起,程序可以是死循环的,算法不行。这个区别也常考,务必记牢。
再加上一条容易漏的:算法的“可行性”不光指步骤能实现,还要求每一步在现有计算机能力范围内可执行。比如一个理论上正确的算法,如果每次都要遍历整个宇宙的数据才能算出下一步,那就没有实际可行性。考试虽然不考这么细,但面试聊算法时很加分。
2. 算法分析的核心:时间复杂度和空间复杂度
2.1 为什么不直接数运行时间,而要用O记号
刚开始我特别不理解,机器跑得快慢不一样,时间复杂度怎么准确衡量?后来明白了,我们关心的是算法随数据规模n增大的增长趋势,不是具体毫秒。同一个算法在小数据上可能跑得飞快,换到大数据就卡死,而两个算法在小数据上几乎没差别,到十万、百万级别才拉开差距。
于是有了Big-O,它描述的是上界,表达“最坏情况下增长不超过某个量级”。常见的时间复杂度从低到高排列:O(1)、O(log n)、O(n)、O(n log n)、O(n²)、O(n³)、O(2ⁿ)、O(n!)。对数阶常出现在二分查找和平衡树,n log n常出现在快速排序、归并排序等。看到有人把O(n log n)误写成O(log n)就想提醒,这是一个非常典型的错误。
理解Big-O时可以用一个生活类比:假设你每月工资按n²增长,而房贷按n增长,n足够大之后工资一定碾压房贷,但工资的具体系数是1还是100,只影响你在哪个城市生活,不影响“工资增速比房贷快”这个结论。复杂度分析就是剥掉系数和低阶项,只比较增速。
2.2 复杂度计算:把循环和递归当成“计价器”
计算时间复杂度不需要高深数学,本质就三步:
- 找出基本操作,也就是循环体里执行最频繁的语句。
- 统计基本操作的执行次数,用n表示。
- 忽略常数系数、低阶项,只留下最高阶项。
比如:
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { ++count; } }外层循环n次,内层循环n次,基本操作count++执行n²次,时间复杂度就是O(n²)。如果内层j从1到i,那就是n(n+1)/2次,忽略低阶项和系数,还是O(n²)。这里很多人困惑的点:n(n+1)/2展开后是(1/2)n² + (1/2)n,低阶项n相对于n²在n很大时可以忽略,常数1/2也不论了。
再举一个容易翻车的例子:
for (int i = 1; i <= n; i *= 2) { ++count; }循环变量每次乘以2,i的取值是1、2、4、8……直到超过n,所以循环次数是log₂n,时间复杂度O(log n)。很多人被这种循环绕晕,本质上就是数“循环变量增加到n需要几步”。
递归的时间复杂度稍微绕一点。简单递归比如:
int fact(int n) { if (n <= 1) return 1; return n * fact(n - 1); }递归调用n次,每次加上常数乘法,整体是O(n)。
复杂点可以画递归树,比如T(n)=2T(n/2)+n,递归树每一层都贡献n,共有log₂n层,所以总时间复杂度O(n log n)。这个递推式对应归并排序,408很喜欢拿它出选择题。
这里我想强调一个判断标准:做题时不要只数代码里有几层循环。循环层数是表象,真正的执行次数由基本操作和循环条件共同决定。比如for (int i = 1; i <= n; i++)和for (int i = 1; i <= n; i += 2),前者n次,后者约n/2次,化简后都是O(n),但如果你直接回答“一层循环就是O(n)”而不写推导,遇到分步循环、条件跳过、斐波那契数列这类问题就会错得毫无防备。
2.3 空间复杂度:别忘了递归栈
空间复杂度描述算法运行过程中临时占用的存储空间随n变化的情况。不计算输入本身占用的空间,只计算额外空间。
比如遍历数组求最大值,只需要一个临时变量,空间复杂度O(1),属于原地算法。但如果写递归,每次递归调用都会占用栈帧。递归求阶乘需要n层调用栈,空间复杂度是O(n);归并排序合并时需要一个辅助数组,空间复杂度O(n)。很多同学算时间时很熟练,算空间时却忽略递归栈,这是失分重灾区。考试碰到递归一定要额外看一眼。
我后来总结了一个口诀:迭代看辅助变量,递归看栈帧深度。迭代算法的空间一般就是几个临时变量,很少随n增长;递归算法则默认要按递归深度考虑栈空间。像二叉树的递归遍历,时间O(n),空间最坏O(n)(树退化成链),平均O(log n)。如果不考虑栈帧,很多空间复杂度的坑你根本看不到。
再补充一个“递归工作栈”的细节:函数调用不是只存一个返回值,还要保存参数、局部变量、返回地址等。所以空间复杂度的常数可能比较大,但考试只关心量级,你只要判断出“会不会随n线性增长”就够了。
3. 考研和期末怎么复习这一章:从概念到做题的落地路径
3.1 这一章的考点地图
先列出绪论这一章在试卷里的主要考点,方便对照复习:
| 考点 | 考试形式 | 重要程度 |
|---|---|---|
| 数据结构三要素(逻辑结构、存储结构、运算) | 选择/简答 | 高 |
| 逻辑结构四种分类的辨析 | 选择 | 中 |
| 存储结构四种分类和对应关系 | 选择 | 中 |
| 算法五特性 | 选择 | 低-中 |
| 时间复杂度计算 | 选择/大题第一小问 | 极高 |
| 空间复杂度计算 | 选择/填空 | 高 |
如果你在备战考研,这一章直接考大题的几率不高,但它衍生出的复杂度计算几乎每一道算法题都会用到。期末的话,老师喜欢在填空题考“数据结构的三要素”,简答题考“逻辑结构和物理结构的区别”。
另外提醒一下:有些学校自命题会把“抽象数据类型的三元组”单独拎出来考名解。这种题目不难,但背的时候别忘了基本操作集合,很多人只写数据对象和关系,白白丢分。
3.2 手算复杂度的标准流程
我总结了一套做题固定流程,保证不会乱。
第一,拿到一段代码,先找最深层的操作,通常是最内层循环里的语句。
第二,假设输入规模为n,分析该操作执行次数。要注意循环变量是否受前面的操作影响。
第三,将执行次数写成关于n的多项式。
第四,取最高阶项,去掉系数。如果执行次数是常数,直接就写O(1)。
一个经典题:
for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { ++count; } }第二层循环次数从0到i-1,总次数是0+1+2+...+(n-1)=n(n-1)/2,最高阶是n²,所以O(n²)。你可以用这个去检验做题时是不是只看循环层数来猜答案。
有时还会遇到“基本操作不固定”的情况,比如查找一个数在不在数组里,找到了就退出。这时候就要分最好、最坏、平均来分析。最好自然是第一个元素就命中,O(1);最坏是遍历完整个数组,O(n);平均如果每个位置等概率,约n/2次,O(n)。很多学校喜欢考这种“变长循环”的分析,你需要在每一步把执行次数表达成n的函数。
为什么我说“写推导”比“猜答案”更重要?因为阅卷和复习反馈都比你想的更残酷。你直接选O(n²),老师看不到你的思路,错了也不知道错在哪。你写下求和公式,就算最后化简失误,老师还能看到你的模型能力。所以平时做题一定要把“n怎么来”写在草稿纸上,哪怕最后答案不对,你回看时也能定位问题。
3.3 408统考和自命题的侧重点差异
如果你是考408,绪论里的概念题分值不高,但后面的算法题会不断使用复杂度分析。408非常喜欢给一个排序算法或查找算法,让你分析最好、最坏、平均时间复杂度,所以你在绪论就要掌握“最好最坏平均”的概念。平均复杂度有时候是概率期望,比如快速排序平均O(n log n),最坏O(n²)。复习的时候要把这些结论落实到每个具体算法。
如果是自命题,常见的形式是比较几段程序的复杂度并写推导过程。老师更看重你“会不会算”。所以别只背答案,一定要动手推导,把n的表达式写出来,再化成O记号。这样做一题顶十题。
还有一个容易被忽视的点:408和自命题都爱考“O(1)空间”这个概念。别以为递归算法也可能是O(1),只要有递归栈,就不可能O(1)。所以题目问“原地排序”的时候,默认指不能额外开和n相关的数组,但可以允许几个临时变量,这是面试也常问的边界。
如果你想延伸阅读,严蔚敏的《数据结构》、王道的《数据结构考研复习指导》、大话数据结构都可以参考。严蔚敏的C语言版本逻辑很严谨,王道适合刷题,大话数据结构胜在通俗。但别贪多,吃透一本,再用第二本做补充就够了。
4. 常见问题与避坑指南
4.1 常见问题速查表
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
| 把O(n log n)写成O(log n) | 对归并排序/快排复杂度不熟 | 记住几个核心算法的复杂度 |
| 算复杂度时忽略了低阶项,导致符号错误 | 没有先写出完整多项式 | 先写求和表达式,再去掉低阶项 |
| 看到递归就头大,不会算空间 | 没意识到递归调用栈也占空间 | 每次递归调用画一层栈帧 |
| 逻辑结构=存储结构 | 没有区分抽象和物理 | 用数组实现链表的例子帮助理解 |
| 分不清最好、最坏、平均 | 没理解“输入顺序影响执行次数” | 拿冒泡排序举例 |
| 把算法和程序划等号 | 没记住算法五特性 | 对比死循环程序 |
这个速查表是我把历年真题错题汇总出来的。你会发现大部分错误不是笨,而是概念边界不清。概念一旦打通,刷题正确率会有一次很明显的提升。
4.2 我踩过的坑和一些实操心得
第一个坑:死记概念。第一遍复习时我把“数据结构是相互之间存在一种或多种特定关系的数据元素的集合”背得滚瓜烂熟,结果做题还是错。后来我把这句话拆成“数据元素+关系”才理解。关系分逻辑关系和存储关系,逻辑关系靠模型,存储关系靠内存布局。
第二个坑:只求答案,不写推导。考研我前期做题图快,直接看答案,导致后期看到代码不知道怎么想。后来我给自己定规矩:每一道复杂度题都至少写三行推导,n怎么来,求和公式怎么化简,最终怎么取O。坚持半个月后正确率明显上来。
第三个坑:忽略递归的栈深度。有一次做一道“递归求数组最大值”的题目,我觉得时间O(n),空间也就O(1),结果答案是O(n)。原因就是递归调用会占n层栈。从那以后我看递归都会先画栈,再算空间。
第四个坑:把“平均复杂度”和“最坏复杂度”混为一谈。比如快速排序,你学了平均O(n log n),但最坏是O(n²)。有次做模拟题,题目明确问你“快速排序最坏时间复杂度”,我下意识写了O(n log n),丢了题还觉得自己没错。一定要把每个算法的“平均、最好、最坏”分开记忆,别揉成一个复杂度。
结合我的经验,建议你用一个“问题循环法”:先拿简答题考察概念,再用代码题练计算,最后用回顾法把这一章的知识点串联起来。比如学到二叉树时,回来想想二叉树遍历的时间复杂度为什么是O(n),空间又为什么是O(h)。这样才是真正的学透。
4.3 后续章节怎么衔接
绪论不是单独存在的章节。它的三个主题分别对应后续内容:逻辑结构分类对应后面的线性表、树、图;存储结构对应顺序表与链表的实现;复杂度分析贯穿排序查找和所有算法。我见过不少同学学完线性表还不知道为什么数组支持随机访问,而链表不行,本质就是因为没有把物理结构的概念内化。
所以在复习顺序上,我的建议是:第一遍学绪论时不必深挖所有定义,能分清几个基础概念就行;等学完线性表和二叉树后,一定要回头再看一遍绪论。这时候你会发现很多抽象概念变具体了。比如“顺序存储”在顺序表里就是数组下标连续,“链式存储”在链表里就是指针串联。回头再看,绪论就像地图一样,把你走过的路都标好了。
这里还涉及一个“数据结构实验报告”里常见的问题:很多实验让实现顺序表和链表,你会发现实验报告模板都要先写“逻辑结构”再写“存储结构”。如果绪论概念不清,报告第一行就会写错。写报告时,顺序表的逻辑结构是线性表,存储结构是顺序存储;链表的逻辑结构也是线性表,存储结构是链式存储。这就是绪论最直接的落地场景。
最后说点个人体会
最后说一点个人的体会。数据结构这门课,绪论是最容易被忽略又最值得反复看的一章。我第一遍看的时候觉得它干巴巴,只会背“三要素”“五特性”。后来刷了几年真题才懂,所有算法题都逃不开复杂度分析,所有结构题都逃不开逻辑结构和物理结构的组合。
如果你现在也觉得这一章抽象,别急,带着问题往后学,学完线性表和树再回来看。相信我,绪论的含金量会随着你后面学到的内容越变越高。复习时间紧的话,先把复杂度计算练熟,这是最直接的得分点,概念题放到第二轮再背也不迟。