第一次翻开《数据结构》教材的人,多数会在第一章“绪论”这儿快速翻过去,觉得全是概念、没有代码,背一背就完事。等我带过几轮考研复习和期末答疑之后才意识到,这门课学得扎实不扎实,其实在绪论阶段就已经定了型。很多人学到后面的二叉树、图,越学越乱,回过头来发现,不是后面的章节难,而是绪论里几个概念没拧清楚。
这篇文章不打算复述课本目录,而是把绪论里最容易让人犯迷糊的几个点拆开说:逻辑结构与存储结构到底怎么区分,ADT抽象到什么程度才算够,时间复杂度的计算怎么避免出错,以及考试里绪论章节常见的坑。内容按照严蔚敏《数据结构(C语言版)》和计算机统考408的知识体系来写,期末复习、考研备考、自学入门都能对着用。
1. 第一章绪论看着简单,其实它是整本书的“使用说明书”
1.1 一个小例子看清“数据结构+算法”是怎么回事
先给一个很常见的题:给定一个整数数组,要求找出第二大的数。最简单粗暴的思路是把数组排序,然后取倒数第二个元素。用快速排序的话,时间复杂度是O(n log n)。但仔细一想,为了找第二大的数去把整个数组排好序,这里有不少浪费:我们根本不关心其他元素的大小顺序。
更好的做法是一次扫描,维护两个变量:一个保存当前最大值max,一个保存当前第二大的值second。每读到一个新的数x,先和max比较:如果x大于max,就把原来的max降级为second,x成为新的max;否则再看x是否大于second,大于就更新second。一趟扫完,答案就出来了,时间复杂度O(n),额外空间O(1)。同一个问题,两种做法差了整整一个数量级。
这就是数据结构与算法这门课存在的意义。程序员圈子里那句“数据结构 + 算法 = 程序”,很多人会背,但未必真正理解。数据结构的“结构”,不是代码目录结构,而是数据元素之间怎么组织、怎么关联;算法是解决特定问题的步骤。绪论就是先让你建立这两个基本概念,后面的章节再逐个展开。
1.2 概念名词和C语言代码的对应关系
严蔚敏教材开头会抛出一串术语:数据、数据元素、数据项、数据对象。初次接触的人容易被绕晕,其实这些概念在C语言里都能找到对应物:
- 数据:程序的输入、输出和中间结果,可以是一个整型数组、一堆结构体变量、一个文件里的记录。
- 数据元素:数据的基本单位,在C语言里常常对应一个数组元素或一个结构体变量。
- 数据项:构成数据元素的不可分割的最小单位,对应结构体里的一个成员变量。
- 数据对象:性质相同的数据元素的集合,比如int a[100]这100个元素整体可以看成一个数据对象。
打个生活化的比方:图书馆所有藏书是数据,其中某一本书是数据元素,这本书的作者字段、书号字段是数据项,所有“小说类”图书放在一起就是数据对象。数据结构定义中那句“相互之间存在一种或多种特定关系的数据元素的集合”,关键在“关系”两个字。后面学的线性表、栈、队列、树、图,本质上是在研究不同的“关系模型”。
1.3 第一章给后续章节铺的底子
如果你手边有严蔚敏教材的目录,翻一下就能看到:线性表、栈和队列、串、数组和广义表、树和二叉树、图。每个主题都按固定套路展开——逻辑结构定义、存储结构实现、基本操作实现、算法分析。说白了,这就是绪论那套总框架在不同数据组织方式上的投影。
我建议你学完每一章之后,都用三句话复盘:这种数据结构对应的逻辑关系是什么?通常有哪几种存储实现?核心操作的时间复杂度是多少?把这三句话练成本能,整个数据结构的学习框架就立住了。绪论提供的就是这套复盘方法。
2. 逻辑结构:判断依据不是图形像什么,而是元素之间是什么关系
2.1 四种逻辑结构的本质区别
逻辑结构分为四类:集合结构、线性结构、树形结构、图状结构(也叫网状结构)。很多初学者靠“画出来的图长什么样”去判断,这是最容易翻车的地方。正确的判断方式只有一个——看元素之间存在什么关系。
- 集合结构:元素之间除了“属于同一个集合”之外,别无其他关系。好比一个兴趣小组的成员名单,成员之间没有前后顺序、没有层级。
- 线性结构:元素之间存在一对一的关系,有唯一的第一个元素和唯一的最后一个元素,每个元素最多有一个直接前驱和一个直接后继。排队打饭是最典型的生活场景。
- 树形结构:元素之间存在一对多的关系。一个节点可以有多个孩子,但只能有一个父节点。公司组织架构就是一棵树。
- 图状结构:元素之间存在多对多的关系。任意两个元素之间都可能有关联。微信好友关系、城市间的交通网络都是图。
判断一个给定场景属于哪种逻辑结构,不要看形态像不像,要看关系允不允许。比如“课程先修关系”,一门课可能有多门先修课,同时它又可能是很多后续课程的前置,多对多,属于图结构。
2.2 “逻辑结构与存储结构无关”为什么是高频考点
这句话在绪论的选择题和判断题里出现频率极高,但很多人理解不了。既然线性结构有前驱后继的关系,怎么可能和存储无关?关键在于:逻辑结构描述的是抽象关系,不考虑数据在计算机里怎么放;存储结构描述的是“关系在计算机里的表示方式”。
举个例子,一个班级的成绩单按学号排列,逻辑上是一条线性表。你可以把它存进一段连续内存里,也就是C语言的数组;也可以用链表,让每个节点通过指针串起来,节点在内存里可以东一个西一个。两种实现的逻辑结构完全相同,都是线性结构,但存储结构完全不同。
所以考试见到“线性结构只能采用顺序存储”“一种逻辑结构只能对应一种存储结构”这类说法,直接判错。
2.3 选择题和判断题里常见的干扰项
理论说多了容易虚,直接看几个典型判断:
- “循环队列是一种逻辑结构。”这是错的。循环队列是用数组实现的队列,队列才是逻辑结构,“循环”只是存储实现上的一种技巧。
- “二叉树的逻辑结构是树形结构。”正确。
- “链表是一种逻辑结构。”这句话有歧义。严格来说,链表是线性表的链式存储实现,逻辑上属于线性结构,存储上属于链式存储。如果选项里说链表是逻辑结构,那就是错的。
- “数据的逻辑结构与存储结构一一对应。”错。两种结构是独立的维度。
这些就是绪论章节判断选择题最常见的出题套路,本质上都在考察你对“逻辑”和“存储”两个层次的区分。
3. 顺序存储与链式存储:两条物理路线的真实差距
3.1 顺序存储:连续内存带来的优势和代价
顺序存储就是把逻辑上相邻的元素放在物理上也相邻的存储单元里。C语言的数组就是最典型的例子,假设声明了int a[5],编译器会分配一段连续的空间,每个元素占4字节。此时要访问a[i],不需要从头找,直接通过基地址加偏移量算出地址,随机访问的时间复杂度是O(1)。
但这个“连续”也带来了明显代价:在数组的中间位置插入一个元素,得先把后续所有元素往后挪一位;删除元素则要往前挪。最坏情况下要移动n个元素,时间复杂度O(n)。顺序存储的所有优点和短板,都写在“连续性”这三个字上。
3.2 链式存储:用指针把分散内存串起来
链式存储不要求物理连续,每个节点除了存数据,还要存一个指向下一个节点的指针。C语言里典型的节点定义长这样:
typedef struct LNode { int data; struct LNode *next; } LNode;每个节点可以散落在内存的任何位置,next字段记录下一个节点的地址。这样插入和删除操作只需要修改相关节点的指针指向,不需要移动其他元素,在已知前驱节点的情况下,插入和删除的时间复杂度是O(1)。
但链式存储也有自己的短板。想访问第k个元素,必须从头节点开始一个一个往后走,随机访问的时间复杂度是O(n)。此外,每个节点都要额外存储一个指针,存储密度低于顺序存储。当数据本身很大时,多出来的指针开销可能很可观。
3.3 一张表看懂“到底选谁”
我在辅导的时候经常听到“链表一定比数组好”的说法,这是特别大的误解。选哪种存储结构,核心标准是看你的程序里哪种操作更频繁。
| 操作场景 | 顺序存储 | 链式存储 |
|---|---|---|
| 按位置随机访问 | 强,O(1) | 弱,O(n) |
| 在头部或中间频繁插入删除 | 弱,需要移动大量元素 | 强,修改指针即可 |
| 事先知道最大长度 | 方便,直接开数组 | 无所谓,动态分配 |
| 内存分配方式 | 需要一段连续大空间 | 不需要连续,但每个节点多一个指针 |
实际工程里往往是混合的:哈希表解决“快速查找”的问题,链表解决“频繁增删”的问题,数组解决“连续访问”的问题。没有绝对优劣,只有适合不适合。
4. 抽象数据类型(ADT):绪论里最被低估的设计思想
4.1 ADT三要素,以及为什么要“封装”
严蔚敏书里用Circle这个例子讲了抽象数据类型的定义,很多初学者看完只觉得是形式主义。其实ADT特别重要。它的三要素是:数据对象、数据关系、基本操作。
比如栈这个ADT:数据对象是一组同类型元素;数据关系是线性关系,但限制只能在一端操作;基本操作包括入栈、出栈、读栈顶元素、判断栈空。ADT描述的是“它是什么、能做什么”,完全不关心“内部怎么实现”。这就是封装思想,调用者只需要知道这几个操作的名字和用途,不需要知道底层用的是数组还是链表。
这种思想在工程里的价值,和工作接口非常像。一个函数或者一个模块只要对外公布接口,内部实现随便换,调用方不受影响。绪论把ADT放在前面,就是想让你从课程一开始就建立“接口与实现分离”的思维方式。
4.2 用C语言模拟一个ADT的示范
C语言没有class,但依然可以写出“抽象”的味道。以栈为例,头文件里放类型定义和操作声明:
#define MAX_SIZE 100 typedef struct { int data[MAX_SIZE]; int top; } Stack; void InitStack(Stack *s); int Push(Stack *s, int x); int Pop(Stack *s, int *x); int IsEmpty(Stack *s);调用者只需要包含这个头文件,使用InitStack、Push、Pop这些函数,完全不需要关心栈内部是用数组还是链表。将来如果想让栈支持动态扩容,只需要修改实现文件,上层调用代码一行都不用动。这就是ADT封装带来的好处。
4.3 为什么说ADT是后面所有章节的组织方式
第3章的栈和队列、第4章的串、后面的树和图,教材全部按照“抽象数据类型+存储实现+基本操作+应用场景”的方式来组织。如果绪论的ADT概念没吃透,后面看到“ADT Graph”“ADT BinaryTree”的定义时,会觉得这只是在堆名词。但实际上,它是把现实问题翻译成代码的第一步:先定义数据对象和操作,再考虑怎么实现。
到考研答题的时候,这种思想也很有用。题目让你“设计一个数据结构支持某几种操作”,第一步绝不是直接写结构体,而是先想清楚逻辑上需要哪些数据对象、哪些操作,再选存储结构。思路清晰了,代码只是最后落地的一步。
5. 算法分析:绪论里真正需要动笔算的部分
5.1 算法的五个特性和好算法的四个评价维度
算法要满足五个基本特性:有穷性、确定性、可行性、有输入、有输出。注意输入可以是零个,但输出至少一个。无限循环不叫算法,因为不满足有穷性。
评价一个算法的好坏,通常看四个方面:正确性、可读性、健壮性、时间与空间效率。前三点靠编码习惯和测试保证,第四点是数据结构课程反复训练的重点。绪论的算法分析部分,本质上就是让你学会用数学语言描述“快慢”和“省不省空间”。
5.2 时间复杂度本质是“操作次数的数量级”
时间复杂度不是程序跑起来有多少秒,而是基本操作执行次数随问题规模n增长的变化趋势。书上的大O记号不用死抠极限定义,抓住一句话就行:只保留最高阶项,忽略常数系数和低阶项。
直接看三个例子。
int sum = 0; for (int i = 1; i <= n; i++) { sum += i; }核心操作sum += i执行了n次,增长趋势和n成正比,时间复杂度O(n)。
int cnt = 0; for (int i = 1; i <= n; i *= 2) { cnt++; }每次i翻倍,执行的次数大约是log2 n次。很多人看到这个循环“就跑了几次”,觉得该写成O(1),其实不对。复杂度必须写成关于问题规模n的函数。n越大,执行次数按对数增长,记作O(log n)。
int cnt = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { cnt++; } }两层循环都跑满n次,执行次数是n²,时间复杂度O(n²)。如果是冒泡排序,内层循环次数会递减,总的比较次数是n(n-1)/2,保留最高阶并去掉系数,依然是O(n²)。
5.3 增长速度快慢,用数据直观感受
光说O(log n)和O(n²)太抽象,把n代进去看数量级差距:
| n | O(log n) | O(n) | O(n log n) | O(n²) |
|---|---|---|---|---|
| 10 | 约3 | 10 | 约30 | 100 |
| 100 | 约7 | 100 | 约700 | 10000 |
| 1000 | 约10 | 1000 | 约10000 | 1000000 |
| 1000000 | 约20 | 1000000 | 约20000000 | 基本无法接受 |
n到100万的时候,O(n)的算法还能正常运行,O(n²)的算法基本要跑到天荒地老。这就是为什么面试、考研题反复追问复杂度,也是为什么“找第二大数”那个例子值得细品:同样的功能,算法选得好不好,实际体验天差地别。
5.4 空间复杂度,以及“空间换时间”的思路
空间复杂度衡量的是算法运行时额外占用的内存随n的增长情况。注意“额外”两个字,输入数据本身占的空间不算在内。
举个实例:把数组a[0]到a[n-1]逆置。最直接的方式是开一个新数组b[n],把a的元素倒着复制进去,然后再拷回来,额外空间O(n)。如果改成首尾交换,用一个临时变量存中间值,额外空间就是O(1)。有时候为了追求时间效率,会故意多开空间。比如哈希表的典型思想,就是用一大块内存把查找操作从O(n)降到平均O(1)。考试里经常同时问时间和空间复杂度,做题时先想清楚“额外空间”是什么,再下笔。
6. 绪论在期末卷和408考研卷里的实际出题方式
6.1 选择判断题的高频“坑”清单
根据我看到的历年卷子,绪论常考的点非常集中:
- 数据结构的定义:相互之间存在一种或多种特定关系的数据元素的集合。注意是数据元素,不是数据项。
- 存储结构按照严蔚敏教材分为四种:顺序存储、链式存储、索引存储、散列存储。判断题说“存储结构只有顺序和链式”是错的。
- 时间复杂度与硬件、编程语言无关,只看基本操作次数随n的增长。程序在超级计算机上跑得快,不代表复杂度低。
- 链式存储不一定比顺序存储更优,优劣看操作场景。
- 算法必须有穷性,“死循环”不是算法。
这些知识点都不难,但组合在一起非常容易扣分。建议复习时自己动手整理一个“判断题错因清单”,把每一题错在哪里标注清楚,考前过一眼就能避免很多低级失误。
6.2 算法分析大题:写清三步不丢分
很多学校期末卷会把复杂度分析当大题出,给一小段代码,让你写时间复杂度。我见过不少学生结果对了但步骤分丢光,原因是只写了一个答案,没有过程。规范的做法是分三步:
第一步,找基本操作,通常是循环体内部那条反复执行的语句;第二步,算执行次数,写出求和表达式;第三步,化简为大O形式。
来看一个典型:
for (i = 0; i < n; i++) { for (j = i + 1; j < n; j++) { if (a[i] > a[j]) { count++; } } }内层循环的次数随i变化:当i为0时执行n-1次,i为1时执行n-2次,总的执行次数为(n-1)+(n-2)+...+1+0,等于n(n-1)/2。最高阶是n²/2,去掉系数,时间复杂度O(n²)。考试这样写,步骤清晰,阅卷老师给分也痛快。
6.3 给复习者的一个核心建议
绪论这章没必要死背概念型的名词解释,背了也会忘。重点是有能力独立回答四个问题:
- 给定一个场景,能判断逻辑结构是集合、线性、树形还是图状。
- 能说清顺序存储和链式存储各自的成本,以及什么场景下用哪个。
- 能解释ADT和具体实现的关系,知道“接口与实现分离”是怎么回事。
- 看到一段代码,能快速算时间复杂度和额外空间复杂度。
这四件事会贯穿后面的每一章,现在练熟练了,后面学树和图的时候会轻松很多。
最后说一点我自己的体会。我当年学数据结构的时候,绪论也是囫囵吞枣翻过去的,直到复习考研才回头把这些概念抠明白,才发现原来后面所有章节都能用逻辑结构、存储结构、ADT、复杂度这四个维度去套。等到工作以后写业务代码,遇到“该用数组还是链表”“这个接口该暴露哪些操作”这类问题,脑子里自动就会浮现第一章这些东西。数据结构的第一章不是拿来背的,它是帮你换一种方式思考数据,这个习惯越早建立越好。