数据结构绪论核心概念:逻辑结构、存储结构与时间复杂度解析
2026/9/7 18:38:05 网站建设 项目流程

第一次翻开《数据结构》教材的人,多数会在第一章“绪论”这儿快速翻过去,觉得全是概念、没有代码,背一背就完事。等我带过几轮考研复习和期末答疑之后才意识到,这门课学得扎实不扎实,其实在绪论阶段就已经定了型。很多人学到后面的二叉树、图,越学越乱,回过头来发现,不是后面的章节难,而是绪论里几个概念没拧清楚。

这篇文章不打算复述课本目录,而是把绪论里最容易让人犯迷糊的几个点拆开说:逻辑结构与存储结构到底怎么区分,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代进去看数量级差距:

nO(log n)O(n)O(n log n)O(n²)
10约310约30100
100约7100约70010000
1000约101000约100001000000
1000000约201000000约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 给复习者的一个核心建议

绪论这章没必要死背概念型的名词解释,背了也会忘。重点是有能力独立回答四个问题:

  1. 给定一个场景,能判断逻辑结构是集合、线性、树形还是图状。
  2. 能说清顺序存储和链式存储各自的成本,以及什么场景下用哪个。
  3. 能解释ADT和具体实现的关系,知道“接口与实现分离”是怎么回事。
  4. 看到一段代码,能快速算时间复杂度和额外空间复杂度。

这四件事会贯穿后面的每一章,现在练熟练了,后面学树和图的时候会轻松很多。

最后说一点我自己的体会。我当年学数据结构的时候,绪论也是囫囵吞枣翻过去的,直到复习考研才回头把这些概念抠明白,才发现原来后面所有章节都能用逻辑结构、存储结构、ADT、复杂度这四个维度去套。等到工作以后写业务代码,遇到“该用数组还是链表”“这个接口该暴露哪些操作”这类问题,脑子里自动就会浮现第一章这些东西。数据结构的第一章不是拿来背的,它是帮你换一种方式思考数据,这个习惯越早建立越好。

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

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

立即咨询