文章目录
前言
本篇文章大部分内容摘自严蔚敏、李冬梅、吴伟民编著的由人民邮电出版社出版发行的《数据结构:C语言版|第2版》,少部分摘自《王道2025数据结构考研复习指导》,也有部分内容为本人的思考。
如果觉得本篇文章对你有帮助的话,不妨点赞、收藏加关注吧!😘❤️❤️❤️
1.1 数据结构的研究内容
简单的说,数据结构是一门研究非数值计算程序设计中的操作对象以及这些对象之间的关系和操作的学科。
1.2 基本概念和术语
1.2.1 数据、数据元素、数据项和数据对象
- 数据(Data):数据是客观事物的符号表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。
- 数据元素(Data Element):数据元素是数据的基本单位,在计算机中通常作为一个整体进行考虑和处理。
- 数据项(Data Item):数据项是组成数据元素的、有独立含义的、不可分割的最小单位。
- 数据对象(Data Object):数据对象是性质相同的数据元素的集合,是数据的一个子集。
1.2.2 数据结构
数据结构(Data Structure)是相互之间存在一种或多种特定关系的数据元素的集合。简单来说,数据结构是带“结构”的数据元素的集合,“结构”就是指数据元素之间存在的关系。
数据结构包括逻辑结构和存储结构两个层次。
一、逻辑结构
逻辑结构是从逻辑上描述数据,它与数据的存储无关,是独立于计算机的。因此,数据的逻辑结构可以看做是从具体问题抽象出来的数学模型。
数据的逻辑结构有两个要素:一是数据元素;二是数据元素之间的逻辑关系。
根据数据元素之间关系的不同特性,通常有四类基本结构:
- 集合结构:数据元素之间除了“属于同一集合”的关系外,没有其他任何关系。
- 线性结构:数据元素之间存在一对一的关系。
- 树结构:数据元素之间存在一对多的关系。
- 图结构(网状结构):数据元素之间存在多对多的关系。
其中,集合结构、树结构、图结构(网状结构)都属于非线性结构。
线性结构主要为线性表及线性表的变体(如栈、队列、数组、广义表等)。
二、物理结构(存储结构)
数据对象在计算机中的存储表示称为数据的存储结构,也称物理结构。把数据对象存储到计算机时,通常要求既要存储数据元素的数据,又要存储数据元素之间的逻辑关系,数据元素在计算机内用一个结点来表示。
数据元素在计算机中有两种基本的存储结构,分别是顺序存储结构和链式存储结构。
- 顺序存储结构:顺序存储结构是借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系,通常借助程序设计语言的数据类型来描述。顺序存储结构要求所有的元素依次存放在一片连续的存储空间中。
- 链式存储结构:相对于顺序存储结构,链式存储结构无需占用一整块存储空间。但为了表示结点之间的关系,需要给每个结点附加指针字段,用于存放后继元素的存储地址。所以链式存储结构通常借助于程序设计语言的指针类型来描述。
当然,在有些书籍中,也把顺序存储结构、链式存储结构、索引存储结构和散列存储结构作为数据元素的四种基本存储结构。
索引存储结构:索引存储结构在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。其优点是检索速度快;缺点是附加的索引表额外占用存储空间。另外,增加和删除数据时也要修改索引表,因而会花费较多的时间。
散列存储结构:散列存储指的是根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。其优点是检索、增加和删除结点的操作都很快;缺点是若散列函数不好,则可能出现元素存储单元的冲突,而解决冲突会增加时间和空间的开销。
存储密度:所谓存储密度是指数据元素本身所占用的存储量和整个结点结构所占用的存储量之比。
即 存储密度 = 数据元素本身占用的存储量 / 结点结构所占用的存储量
由于链式存储结构还需要额外存储结点附加指针字段,因此,一般来说,顺序存储结构的存储密度都要大于链式存储结构的存储密度。
1.2.3 数据类型和抽象数据类型
- 数据类型:
数据类型(Data Type)是高级程序设计语言中的一个基本概念,例如前文提到的顺序结构可以借助程序设计语言的数组类型描述,链式存储结构可以借助指针类型描述,所以数据类型和数据结构的概念密切相关。
简单来说,数据类型是一个值得集合和定义在这个值集上一组操作的总称。类型明显或隐含地规定了数据的取值范围、存储方式以及允许进行的运算。
- 抽象数据类型
抽象数据类型(Abstract Data Type,ADT)一般指由用户定义的、表示应用问题的数学模型,以及定义在这个模型上的一组操作的总称,具体包括三部分:数据对象、数据对象上关系的集合以及对数据对象的基本操作的集合。
抽象数据类型的定义格式如下:
ADT 抽象数据类型名{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
基本操作:<基本操作的定义>
}ADT 抽象数据类型名
其中,数据对象和数据关系的定义采用数学符号和自然语言描述,基本操作的定义格式为:
基本操作名 <参数表>
初始条件:<初始条件描述>
操作结果:<操作结果描述>
基本操作有两种参数:赋值参数只为操作提供输入值;引用参数以“&”打头,除可提供输入值外,还将返回操作结果。
“初始条件”描述了操作执行之前数据结构和参数应满足的条件,若初始条件为空,则省略。
“操作结果”说明了操作正常完成之后,数据结构的变化状况和应返回的结果。
1.3 抽象数据类型的表示与实现
运用抽象数据类型描述数据结构,有助于在设计一个软件系统时,不必首先考虑其中包含的数据对象,以及操作在不同处理器中的表示和实现细节,而是在构成软件系统的每个相对独立的模块上定义一组数据和相应的结构。这和
面向对象方法的思想是一致的,所以表示和实现抽象数据类型,最好用面向对象的方法,比如用C++语言的类描述比较方便、有效。
1.4 算法和算法分析
数据结构和算法之间存在着本质联系。在“数据结构”中,将遇到大量的算法问题,因为算法联系着数据在计算过程中的组织方式,为了描述实现某种操作,常常需要设计算法,因而算法是研究数据结构的重要途径
1.4.1 算法的定义及特性
算法(Algorithm)是为了解决某类问题而规定的一个有限长的操作序列。
一个算法必须满足以下5个重要特性。
- 有穷性:一个算法必须总是在执行有穷步后结束,且每一步都必须在有穷时间内完成。
- 确定性:对于每种情况下所应执行的操作,在算法中都有确切的规定,不会产生二义性,使算法的执行者或阅读者都能明确其含义及如何执行。
- 可行性:算法中的所有操作都可以通过已经实现的基本操作运算执行有限次来实现。
- 输入:一个算法有零个或多个输入。当用函数描述算法时,输入往往是通过形参表示的,在它们被调用时,从主调函数获得输入值。
- 输出:一个算法有一个或多个输出,它们是算法进行信息加工后得到的结果,无输出的算法么有任何意义。当用函数描述算法时,输出多用返回值或引用类型的形参来表示。
1.4.2 评价算法优劣的基本准则
一个算法的优劣应该从以下几个方面来评价。
- 正确性:在合理的数据输入下,能够在有限的运行时间内得到正确的结果。
- 可读性:一个好的算法,首先应便于人们的理解和相互交流,其次才是机器的可执行性。可读性强的算法有助于人们对算法的理解,而难懂的算法易于隐藏错误,且难于调试和修改。
- 健壮性:当输入的数据非法时,好的算法能适当地做出正确反应或进行相应处理,而不会产生一些莫名其妙的输出结果。
- 高效性:高效性包括时间和空间两个方面。时间高效是指算法设计合理,执行效率高,可以用时间复杂度来度量;空间高效是指算法占用存储容量合理,可以用空间复杂度来度量。时间复杂度和空间复杂度是衡量算法的两个主要指标。
1.4.3 算法的效率分析(时间复杂度、空间复杂度)
算法效率分析的目的是看算法实际是否可行,并在同一问题存在多个算法时,可进行时间和空间性能上的比较,以便从中挑出较优算法。
衡量算法销量的方法主要有两类:事前分析估算法和事后统计法。事后统计法需要先将算法实现,然后测算其时间和空间开销,缺陷明显,因此我们通常采用事前分析估算法,通过计算算法的渐近复杂度来衡量算法的效率。
时间复杂度
在不考虑计算机软硬件等环境因素,影响算法时间代价的最主要因素是问题规模。问题规模是算法求解求解问题输入量的多少,一般用整数n表示。
一个算法的执行时间大致上等于其所有语句执行时间的总和,而语句的执行时间则为该条语句的语句频度(Frequency Count,代表该语句重复执行的次数)乘以语句执行一次所需的时间。
设每条语句执行一次所需的时间均为单位时间t,则一个算法的执行时间可以用该算法中所有语句的频度之和来度量。
例如:
for(i = 1; i<=n; i++){ // 频度为 n + 1
for(j = 1; j<=n; j++){ // 频度为 n*(n+1)
c[i][j] = 0; // 频度为 n^2
for(k = 1; k<=n; k++){ // 频度为 n^2 * (n + 1)
c[i][j] = c[i][j]+a[i][k]*b[k][j]; // 频度为 n^3
}
}
}
该算法中所有语句频度之和是关于n的函数,我们用f(n)表示,则f(n) = 2n^3 + 3n^2 + 2*n + 1。
所以问题规模为n时,该算法的执行时间为f(n) * t
但由于语句的执行要由源程序经编译程序翻译成目标代码,目标代码经装配再执行,因此语句执行一次实际所需的时间与机器的软硬件环境密切相关。所以所谓的算法分析并非精确统计算法实际执行所需时间,而是针对算法中语句的执行次数做出估计,从中得到算法执行时间的信息。
当问题规模无限大时,我们会发现 f(n) / n^3 之比是一个不等于零的常数,即f(n) 和 n^3的数量级(Order of
Magnitude)相同,或者说f(n) 和 n^3同阶。
在这里,我们用"O"表示数量级,则算法的时间复杂度T(n) = O(f(n)) = O(n^3);
一般情况下,算法中的基本语句重复执行次数是问题规模n的某个函数f(n),我们把算法的时间量度记为T(n) = O(f(n))
,它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度(Time Complexity)
对于某些问题的算法,其基本语句的频度不仅仅与问题的规模相关,还依赖于其他因素,因此有时会对算法有最好、最坏以及平均时间复杂度的评价。
我们将算法在最好情况下的时间复杂度称为最好时间复杂度,指的是算法计算量可能达到的最小值。
算法在最坏情况下的时间复杂度我们称为最坏时间复杂度,指的是算法计算量可能达到的最大值;
算法的平均时间复杂度是指算法在所有可能的情况下,按照输入实例以等概率出现时,算法计算量的加权平均值。
在很多情况下,算法的平均复杂度难以确定,因此,通常只讨论在最坏情况下的时间复杂度,即分析在最坏情况下,算法执行时间的上界。
空间复杂度
关于算法的存储空间需求,类似于时间复杂度,我们采用渐进空间复杂度(Space Complexity)作为算法所需存储空间的度量,简称空间复杂度。
空间复杂度也是关于问题规模n的函数,记作:S(n) = O(f(n))
对于输入数据所占的具体存储量取决于问题本身,与算法无关,因此,我们只需要分析该算法在实现时所需要的辅助空间就可以了。
例如:
// 算法一 该算法只需要借助一个辅助变量t,因此空间复杂度为O(1)
for (i =0; i<n/2; i++){
t = a[i];
a[i] = a[n-i-1];
a[n-i-1] = t;
}
// 算法二 该算法需要借助辅助数组b[n],因此所需空间复杂度O(n)
for (i=0; i<n; i++) } b[i] = a[n-i-1];
for (i=0; i<n; i++) a[i] = b[i];
1.5 个人小结(不定期更新)
1.5.1 时间复杂度与空间复杂度的计算
对于时间复杂度与空间复杂度的计算,要牢记我们计算的最终结果是要得到一个数量级。
正因为计算的结果为数量级,因此实际上对于简单的复杂度计算问题,我们并不需要真正的算出具体的函数,而是可以通过观察直接得到结果。
例如前文中的算法:
for(i = 1; i<=n; i++){ // 频度为 n + 1
for(j = 1; j<=n; j++){ // 频度为 n*(n+1)
c[i][j] = 0; // 频度为 n^2
for(k = 1; k<=n; k++){ // 频度为 n^2 * (n + 1)
c[i][j] = c[i][j]+a[i][k]*b[k][j]; // 频度为 n^3
}
}
}
显然,该算法使用了3层for循环,且每个for循环执行的次数等于n,因此该算法的时间复杂度为O(n^3)。
再例如
for(i = 1; i<=1000; i++){
a[i] += 1
}
显然,该算法与问题规模n无关,for循环执行的次数始终等于1000,是一个常数,而常数的数量级为1,因此该算法的时间复杂度为O(1)
🙌至此全篇结束,如果觉得对你有帮助的话,不妨点赞、收藏加关注吧!😘❤️❤️❤️