【C语言数据结构】第1章:绪论
每日一句
生活总会给你另一个机会,
这个机会叫明天。
目录
每日一句
一.数据结构的基本概念
1.数据相关核心概念
2.数据结构(data structure)
3.数据类型(data type)
4.数据结构的内容(逻辑结构、物理结构、操作集合)
(1)逻辑结构
(2)物理结构(存储结构)
顺序存储
链式存储
索引存储
散列存储(哈希存储)
(3)操作集合
二.抽象数据类型(ADT)
1.抽象的思想
2.抽象数据类型的定义
3.例子:ADT整数(课本表1-2)
4.ADT的意义
三.算法描述
1.算法与程序的关系
2.算法的描述方式
3.算法的5个重要特性
4.“好算法”的目标
四.算法分析
1.算法分析的分类
2.时间复杂度
3.空间复杂度
五.数据结构的C语言表示
1.算法的描述方式
2.用C语言表述抽象数据类型(ADT)
ADT的定义(伪码形式)
用C语言实现ADT(存储结构 + 操作函数)
3.基本操作的算法描述
总结
一.数据结构的基本概念
1.数据相关核心概念
- 数据(data):**计算机能处理的各种符号(数值、字符、图像、视频等),是 “计算机可识别的信息载体”。**对客观事物的描述,是计算机能输入并加工的符号集合(如数值、字符、图像、视频等),可理解为“计算机能识别和操作的信息”。
- 数据元素(data element):构成数据的 基本单位 ,程序中通常将其作为整体处理(如 “一条学生选课记录”“报刊订阅系统的一个节点”); 一个数据元素可包含多个 “数据项”。构成数据的基本单位,程序中通常将其作为整体处理。例如:学生选课表的“一行记录”、报刊订阅系统结构图的“每个节点”(如“登录”“用户”等)。
- 数据项(data item):数据中 不可再分割的最小单位。例如选课记录里的“学号”“课程名”“成绩”,一个数据元素可包含多个数据项(如“一条选课记录”包含学号、课程名等)。
- 数据对象(data object):由 性质相同的数据元素 组成的集合,是数据的子集。例如:整个学生选课表、“整数集合 ( D = {0, \pm1, \pm2, \dots} )”、“字母集合 ( C = {A,B,\dots,Z} )”。
2.数据结构(data structure)
数据结构是“相互之间存在特定关系的数据元素的集合”,经典定义为二元组 ( \text{Data_Structure} = (D, R) ),其中:
- ( D ) 是数据对象(要处理的数据集);
- ( R ) 是 ( D ) 上关系的有限集(数据元素之间的逻辑关联)。
简言之,数据结构 = 数据元素 + 元素间的关系。
3.数据类型(data type)
是“一个值的集合 + 定义在该集合上的一组操作”的总称。例如C语言的int
类型:取值范围为 (-32768 \sim +32767),支持加、减、乘、除等操作,规定了“能存储什么值”和“能对值做什么操作”。
高级语言的“整型、实型、字符型”等,本质是已实现的数据结构实例(因为它们定义了数据的组织方式和操作)。
4.数据结构的内容(逻辑结构、物理结构、操作集合)
数据结构研究三方面内容:逻辑结构、物理结构(存储结构)、对数据的操作。
(1)逻辑结构
数据元素之间的逻辑关系(与“存储方式”无关,只关注元素间的关联),分为4类基本结构:
- 集合结构:元素间仅存在“同属一个集合”的关系,无其他关联;
- 线性结构:元素间是一对一的关系(如排队,一人接一人);
- 树形结构:元素间是一对多的关系(如公司架构:老板管理多个经理,每个经理管理多个员工);
- 图形结构:元素间是多对多的关系(如社交网络:一人可关注多人,多人也可关注同一人)。
逻辑结构又可分为线性结构(所有元素排成一个序列)和非线性结构(元素与多个其他元素关联)。
(2)物理结构(存储结构)
逻辑结构在计算机内存中的实际存储方式,核心有两种:
物理结构指数据在计算机内存中的具体存储方式与实现逻辑,直接决定数据操作(插入、删除、查找等)的效率。根据存储方式的不同,主要分为以下四种:
顺序存储
- 定义:将数据元素存放在内存中连续的存储空间内,数据元素的逻辑顺序与物理存储顺序完全一致。
- 核心特点:
- 无需额外存储“元素间的关系信息”,存储密度高;
- 可通过“下标”随机访问元素(时间复杂度 ( O(1) ),如数组
arr[3]
可直接通过地址计算访问); - 插入/删除元素时,需移动大量后续数据(如数组中间插入元素,后续元素需整体后移,时间复杂度 ( O(n) ))。
- 示例:C语言中的数组(如
int arr[5] = {1,2,3,4,5}
),元素1
2
3
依次存储在内存连续地址(如0x100
0x104
0x108
)中。
链式存储
- 定义:数据元素存放在内存中非连续的存储空间内,通过给每个元素附加“指针(或引用)”,指向其逻辑上的相邻元素,以此建立元素间的关系。
- 核心特点:
- 插入/删除元素时,仅需修改指针指向(无需移动数据,时间复杂度 ( O(1) ));
- 无法“随机访问”(需从首元素开始依次遍历,时间复杂度 ( O(n) ));
- 指针会占用额外存储空间,存储密度低于顺序存储。
- 示例:单链表(每个节点包含“数据域”和“指针域”,如:
节点1的struct Node {int data; // 数据域:存储元素值struct Node* next; // 指针域:指向下一个节点 };
next
指针指向节点2,节点2指向节点3,通过指针串联非连续存储的元素)。
索引存储
- 定义:将数据分为两部分——数据区(存储所有数据元素)和索引表(存储“关键字-数据地址”的映射关系);通过关键字在索引表中快速查找对应数据的存储地址,再到数据区访问数据。
- 核心特点:
- 大幅提升查找效率(尤其海量数据,时间复杂度接近 ( O(1) ));
- 索引表会占用额外存储空间;
- 插入/删除数据时,需同步维护索引表(增加操作开销)。
- 示例:
- 图书馆的“图书索引”:索引表记录“书名 → 书架编号”,读者先查索引找到书架,再去书架取书;
- 数据库中的索引:如MySQL的B+树索引,通过索引快速定位表中数据的物理位置。
散列存储(哈希存储)
- 定义:通过“散列函数(哈希函数)”,将数据元素的“关键字”直接映射为其在内存中的存储地址,实现“关键字 → 地址”的直接对应。
- 核心特点:
- 查找、插入、删除效率极高(理想情况下时间复杂度 ( O(1) ));
- 可能出现“哈希冲突”(不同关键字映射到同一地址),需额外处理(如链地址法:冲突元素用链表串联;开放定址法:冲突时寻找下一个空闲地址);
- 散列函数的设计直接影响存储效率(需尽可能减少冲突)。
- 示例:
- 哈希表:如Java中的
HashMap
,通过hash(key)
计算元素存储位置,直接定位元素; - 密码学哈希:如MD5算法,将任意长度数据映射为固定长度的哈希值(用于数据完整性校验)。
- 哈希表:如Java中的
(3)操作集合
对数据的操作,如增加、删除、修改、查找、排序等。数据结构的完整定义是:“按逻辑组织的数据 + 存储方式 + 可执行的操作”的整体。
二.抽象数据类型(ADT)
1.抽象的思想
抽象是从同类事物中舍弃“个别、非本质的属性”,抽取“共同、本质的属性”。例如“四边形”:无论正方形、长方形还是不规则四边形,只要是“四条边组成的平面封闭图形”,就被抽象为“四边形”;在数据结构中,用抽象描述程序中数据的组织形式(如树、图的抽象定义)。
2.抽象数据类型的定义
抽象数据类型(Abstract Data Type,ADT)是“一组数据对象 + 元素间的结构关系 + 对数据的操作集合”,形式化表示为三元组:
ADT 抽象数据类型名 {数据对象D; // 要处理的数据集数据关系R; // 数据元素间的关系基本操作P; // 可对数据执行的操作
} ADT 抽象数据类型名
核心是只定义“逻辑特性和操作接口”,不关心“具体实现细节”。例如C语言的“整型”是基本类型,基于它可构造“栈、队列”等更复杂的ADT。
3.例子:ADT整数(课本表1-2)
ADT 整数 {数据对象:整数有序序列(从0到最大整数)、布尔值(True/False);基本操作:Zero(); // 类似构造函数,初始化整数Is_Zero(x); // 判断x是否为0,返回True/FalseAdd(x, y); // 两整数相加,返回和Equal(x, y); // 判断x与y是否相等,返回True/FalseSubtract(x, y);// 两整数相减,返回差Successor(x); // 返回x的下一个自然数,若x是最大整数则返回最大整数
} ADT 整数
ADT的关键:用户只需关心“如何调用操作”,无需关心“操作内部如何实现”(如Add(x,y)
是用硬件加法器还是软件模拟,用户无需知晓)。
4.ADT的意义
ADT实现了“信息隐藏”:内部实现细节(如用数组还是链表存储数据)对外屏蔽,仅暴露“操作接口”。这样带来两个好处:
- 修改内部实现时,只要接口不变,外部代码无需修改(如把“栈”的存储从数组换成链表,调用
push
/pop
的代码无需变更); - 软件更“模块化、可复用”,像“搭积木”一样组合ADT开发大型程序(如栈、队列的ADT可在多个场景重复使用)。
课本强调:“ADT抽象程度越高,软件复用程度越高”(如“图”的ADT比“整型”抽象程度高,能支持更复杂的场景)。
三.算法描述
1.算法与程序的关系
Niklaus Wirth的名言:“算法 + 数据结构 = 程序”——算法是“解决问题的步骤逻辑”,程序是“算法用具体编程语言的实现”。例如计算 ( 1 - \frac{1}{2} + \frac{1}{3} - \frac{1}{4} + \dots + \frac{1}{99} - \frac{1}{100} ),不同算法对应不同程序代码:
-
算法1:直接逐项相加/相减
#include <stdio.h> int main() {double sum = 0.0;int sign = 1; // 符号标记:1为正,-1为负for (int i = 1; i <= 100; i++) {sum += sign * 1.0 / i;sign = -sign; // 切换符号}printf("结果:%lf\n", sum);return 0; }
-
算法2:拆分为两个多项式相减 ( (1 + \frac{1}{3} + \frac{1}{5} + \dots + \frac{1}{99}) - (\frac{1}{2} + \frac{1}{4} + \frac{1}{6} + \dots + \frac{1}{100}) )
#include <stdio.h> int main() {double sum1 = 0.0, sum2 = 0.0;// 计算奇数项和for (int i = 1; i <= 99; i += 2) {sum1 += 1.0 / i;}// 计算偶数项和for (int i = 2; i <= 100; i += 2) {sum2 += 1.0 / i;}double sum = sum1 - sum2;printf("结果:%lf\n", sum);return 0; }
2.算法的描述方式
- 自然语言:用中文、英文等描述,优点是“简单易懂”,缺点是“歧义多、不严谨”(如“然后处理数据”,未明确处理逻辑)。
- 类语言(伪代码):介于自然语言和编程语言之间,更接近代码逻辑(如
for i from 1 to n
),比自然语言准确,且无需严格遵循某门语言的语法。 - 程序设计语言(如C语言):最准确,能直接运行,但“编写繁琐,受语言语法限制”。课本后续用C语言描述算法(因C语言类型丰富、执行效率高)。
3.算法的5个重要特性
算法是 “解决特定问题的有限、确定的步骤集合”,需满足 5 个基本特性
- 有穷性:对任意合法输入,算法必须在有限步骤内结束,且每个步骤的执行时间有限(死循环不是算法)。
- 确定性:每一条指令的含义明确无歧义,任何情况下算法只有“一条执行路径”(如“若 ( x>0 ) 则执行操作A,否则执行操作B”,条件清晰)。
- 可行性:算法中的操作都能通过“已实现的基本运算”(如加减乘除、赋值、比较)有限次完成。
- 输入:可以有0个或多个输入(输入是算法执行前的初始数据,若算法内部可生成数据,则无需外部输入)。
- 输出:至少有一个输出(算法的结果,如打印、返回值等,无输出的算法无意义)。
4.“好算法”的目标
设计算法时,追求以下目标:
- 正确性:能正确解决问题,分4个层次:
- 程序无语法错误;
- 对“合法输入”能得到符合要求的结果;
- 对“非法输入”能适当处理(如提示错误);
- 对“所有合法输入”都能得到正确结果(最难达到,通常验证关键场景)。
- 可读性:容易被人理解,便于交流、维护(如代码添加注释、逻辑清晰)。
- 健壮性:输入非法数据时,能“做出反应”(如提示“输入错误”),而非崩溃或给出错误结果。
- 高效率与低存储量需求:执行时间尽可能短(高效率),占用存储空间尽可能少(低存储量)——两者都与“问题规模”(如数据量大小)相关。
四.算法分析
数据结构的优劣通过算法效率体现,需分析算法的时间效率(执行时间)和空间效率(存储空间)。
1.算法分析的分类
- 算法分析:理论上评估效率,不依赖具体计算机(得到通用结论)。
- 性能测量:实际运行程序,统计执行时间和空间占用,但受硬件、软件环境影响大(如在低配电脑和高配电脑上,同一程序运行时间不同)。
课本重点讲解算法分析(以得到“与具体机器无关”的通用效率结论)。
2.时间复杂度
-
定义:算法的执行时间随问题规模 ( n ) 增长的趋势,用大O符号表示(大O表示“渐进上界”,即“最坏情况下的增长趋势”)。
算法执行时间 ≈ 编译时间 + 所有语句执行时间的总和。由于“编译时间与问题规模无关”,因此主要分析语句执行时间。 -
基本操作:与“问题规模”无关的操作(如赋值、比较、算术运算等)。算法的时间消耗,取决于“基本操作的执行次数”。
-
语句频度与时间复杂度:
- 语句频度:语句执行的次数,记为 ( T(n) );
- 时间复杂度:记为 ( T(n) = O(f(n)) ),表示“当 ( n ) 趋近于无穷大时,( T(n) ) 的增长趋势与 ( f(n) ) 一致”(低阶项、常数项可忽略,只关注最高阶项)。
-
经典例子:
-
① 单条操作:
++x;
执行次数为1,时间复杂度 ( O(1) )(常数阶)——无论 ( n ) 多大,执行时间基本不变。
-
② 一层循环:
for(int i=1; i<=n; i++) {++x; }
循环 ( n ) 次,执行次数为 ( n ),时间复杂度 ( O(n) )(线性阶)——( n ) 翻倍,时间也翻倍。
-
③ 两层循环:
for(int i=1; i<=n; i++)for(int j=1; j<=n; j++) {++x;}
循环 ( n^2 ) 次,执行次数为 ( n^2 ),时间复杂度 ( O(n^2) )(平方阶)——( n ) 翻倍,时间翻四倍。
-
-
课本案例:
-
矩阵相乘(例1-4):
#define N 100 // 假设矩阵大小为N×N void mult(int a[][N], int b[][N], int c[][N], int n) {for(int i=1; i<=n; i++)for(int j=1; j<=n; j++) {c[i][j] = 0;for(int k=1; k<=n; k++) {c[i][j] += a[i][k] * b[k][j];}} }
基本操作是“乘法”,执行次数约为 ( 2n^3 + 2n^2 + n )。当 ( n ) 很大时,低阶项(( 2n^2 )、( n ))和常数可忽略,时间复杂度 ( O(n^3) )。
-
选择排序(例1-5):
void select_sort(int a[], int n) {int j, k, temp;for(int i=0; i<n-1; i++) {j = i;for(k=i+1; k<n; k++) {if(a[k] < a[j]) j = k;}if(j != i) {temp = a[i];a[i] = a[j];a[j] = temp;}} }
基本操作是“比较”,执行次数约为 ( \frac{n(n-1)}{2} )(≈( \frac{n^2}{2} )),时间复杂度 ( O(n^2) )。
-
冒泡排序(例1-6):
void bubble_sort(int a[], int n) {int change, temp;for(int i=n-1; i>=1 && change; i--) {change = 0;for(int j=0; j<i; j++) {if(a[j] > a[j+1]) {temp = a[j];a[j] = a[j+1];a[j+1] = temp;change = 1;}}} }
- 最好情况(数组已排好序):执行次数为 ( n ),时间复杂度 ( O(n) );
- 最坏情况(数组逆序):执行次数为 ( \frac{n(n-1)}{2} ),时间复杂度 ( O(n^2) )。
通常分析最坏时间复杂度(保证算法在“最坏情况”下也能高效运行)。
-
-
常用时间复杂度的顺序:
( O(1) < O(\log_2 n) < O(n) < O(n \log_2 n) < O(n^2) < O(n^3) < O(2^n) )
越往后,( n ) 增大时执行时间增长越快,因此优先选择复杂度低的算法。
3.空间复杂度
-
定义:算法所需存储空间的度量,记为 ( S(n) = O(f(n)) ),其中 ( n ) 是问题规模。
-
空间组成:
- 固定空间需求:与输入无关的空间,如“指令存储空间”“简单变量”“固定大小的结构变量(如结构体)”“常量”等。
- 可变空间需求:与输入有关的空间,如“输入数据的存储空间”“动态分配的空间(如链表节点)”“递归调用的栈空间”等。
-
时间与空间的权衡:“时间成本和空间成本是矛盾的”:
- 时间换空间:如“压缩数据”(节省空间),但需要花费时间“压缩/解压”;
- 空间换时间:如“预先计算并存储结果”(节省时间),但需要更多存储空间。
五.数据结构的C语言表示
这部分是“抽象到具体”的关键,将“ADT、数据结构”用C语言落地实现。
1.算法的描述方式
描述算法的常见方式:自然语言、流程图、伪代码、N-S结构流程图、PAD图等。
课本选择**“以C语言为主,伪代码为辅”**的策略:
- 用C语言描述“可直接运行的数据结构实现”;
- 用伪代码描述“只含抽象操作的算法逻辑”(避免被C语言语法细节干扰,突出核心逻辑)。
2.用C语言表述抽象数据类型(ADT)
ADT的“实现”分为**“定义(逻辑层面)”和“代码(物理层面)”**两步。
ADT的定义(伪码形式)
ADT的定义只关注“逻辑要做什么”,不涉及具体语言语法,格式为:
ADT 抽象数据类型名 {数据对象: <对象的抽象定义>数据关系: <元素间关系的抽象定义>基本操作: <操作的抽象定义>
} ADT 抽象数据类型名
其中,“基本操作”的定义需明确前置条件(操作执行的前提)和后置条件(操作执行的结果)。
例子:ADT string(串的抽象定义,例1-7)
ADT string {数据对象: {S | S由字符组成,i=0,1,…,n-1,n≥0} // 串是“字符序列”数据关系: {R | R={<S₀,S₁>,<S₁,S₂>,…,<Sₙ₋₂,Sₙ₋₁>}} // 字符间的“顺序关联”关系基本操作:StrAssign(S, chars); // 赋值:将字符序列chars赋给串SStrDestroy(S); // 销毁:释放串S的存储空间StrCopy(S₁, S₂); // 复制:将串S₂的内容复制到S₁StrCompare(S₁, S₂); // 比较:比较串S₁和S₂的大小StrCombine(S, S₁, S₂); // 连接:将S₁和S₂连接,生成新串SStrReplace(S, S₁, S₂); // 替换:用S₂替换S中与S₁匹配的子串//... 其他操作
} ADT string
ADT定义是“逻辑抽象”,不关心“具体用C语言如何存储、如何编写函数”,只定义“要做什么”。
用C语言实现ADT(存储结构 + 操作函数)
ADT的“实现”需解决两个核心问题:数据怎么存(存储结构) + 操作怎么写(函数)。
-
存储结构的表示(
typedef
+ 结构体):
用C语言的typedef
(类型定义)和struct
(结构体),描述“数据的物理存储方式”。例子:串的动态数组存储(例1-8)
typedef struct string {char *str; // 指针,指向存储字符串的基地址(动态分配内存)int maxlength; // 动态数组可存储的最大字符数(总空间大小)int length; // 当前串的实际长度 } DString, *DStr;
解释:
typedef
将struct string
重命名为DString
(结构体类型),*DStr
是“指向DString的指针类型”(方便用指针操作串);char *str
是动态数组的核心:运行时通过malloc
分配空间,可灵活存储字符序列;maxlength
记录总空间大小,length
记录串的实际长度——这种设计能“按需分配、灵活管理”字符串空间。
补充:类型与变量的区别:
DString
是“类型名”(规定串的存储规范),只有定义DString s;
这样的变量时,才会真正分配存储空间。 -
用C函数实现ADT的操作:
ADT中的“基本操作”(如StrAssign
、StrCopy
),需编写为C函数,格式为:函数类型 函数名(函数参数表) {/* 算法说明 */ // 注释:解释函数要完成的逻辑语句序列 // 具体的C代码(赋值、循环、指针操作等) }
示例(简化的
StrAssign
逻辑):// 假设Status为int类型,1表示成功,0表示失败 typedef int Status; Status StrAssign(DString *S, char *chars) {/* 算法说明:将chars赋值给串S。成功返回1,失败返回0 */// 1. 释放S原有空间(若存在)if (S->str != NULL) free(S->str);// 2. 计算chars长度,分配新空间int len = strlen(chars);S->str = (char *)malloc((len + 1) * sizeof(char));if (S->str == NULL) return 0; // 内存分配失败// 3. 复制chars到S->str,并设置长度strcpy(S->str, chars);S->length = len;S->maxlength = len + 1;return 1; // 操作成功 }
补充:
- 参数传递:常用指针参数(如
DString *S
),利用C语言的“传地址”特性,直接修改传入变量的内容; - 返回状态:若函数需返回“操作是否成功”,可定义
typedef int Status;
(如1
表示成功,0
表示失败)。
- 参数传递:常用指针参数(如
3.基本操作的算法描述
课本中算法的“类C语言”描述具有以下特点:
- 格式:
函数类型 函数名(参数表)
,内部包含“注释(算法说明) + 语句序列”; - 参数:明确参数类型,辅助变量可通过注释说明(如
/* 字符指针p */
); - 返回值:若返回“操作状态”则用
Status
类型,若返回具体数据则用对应类型(如int
、char *
); - 指针传递:依赖C语言的指针特性,实现“修改传入变量”的效果(如修改串的内容、长度)。
总结
绪论搭建了“数据结构与算法”的整体框架:
- 明确“数据怎么组织”(逻辑结构 + 存储结构);
- 掌握“怎么抽象描述数据”(ADT);
- 理解“怎么用算法处理数据”(算法的描述、分析);
- 学会“怎么用C语言实现”(编程语言与数据结构的结合)。
每个概念都为后续“栈、队列、树、图”等具体数据结构的学习奠定基础,透彻理解绪论,后续学习会更加顺畅。