数据结构基本概念和术语
-
数据:是能输入计算机且能被计算机处理的各种符号的集合。通常包括数字、字符、图像等各种形式。
-
数据元素:是数据的基本单位,在计算机程序中,数据元素通常作为一个整体进行考虑和处理。例如,整数、字符等都是数据元素。
-
数据项:构成数据元素的不可分割的最小单位。例如,在一个浮点数数据元素中,整数部分和小数部分可以看作是数据项。
-
数据对象:是性质相同的数据元素的集合,是数据的一个子集。数据对象通常描述一个数据元素的集合,具有某种共同的特征。
-
数据结构:数据元素不是孤立存在的,它们之间存在某种关系,数据元素之间的关系称为数据结构。数据结构定义了数据元素的排列方式以及它们之间的逻辑关系。
数据结构的分类
逻辑结构:
-
集合结构:数据元素之间除了同属于一个集合的关系外,没有其他关系。集合结构中的元素是无序的。
-
线性结构:数据元素之间存在一对一的线性关系。常见的线性结构包括线性表(如数组、链表)等。
-
树形结构:数据元素之间存在一对多的层次关系。树形结构通常用于表示层级关系,如文件系统中的目录结构。
-
图状结构:数据元素之间存在多对多的任意关系。图状结构用于表示更复杂的关系,如社交网络中的用户关系。
存储结构:
-
顺序存储结构:数据元素按顺序存储在连续的内存单元中。顺序存储通过数据元素的存储位置来表示逻辑关系。常见的数据结构有数组。
-
链接存储结构:数据元素存储在任意的存储单元中,每个数据元素包含一个指向下一个元素的指针,通过指针表示数据元素之间的逻辑关系。常见的数据结构有链表。
-
索引存储结构:在存储数据元素的同时,建立附加的索引表,以便通过索引快速查找数据。常见的数据结构有索引表、B树等。
-
散列存储结构:通过哈希函数直接计算出数据元素的存储地址。散列存储常用于实现哈希表,以便在常数时间内查找数据元素。
数据类型
数据类型是指一组性质相同的值的集合以及定义于这个值集合上的一组操作。数据类型为计算机程序提供了处理数据的抽象和操作接口。
-
基本数据类型:通常指由编程语言直接支持的数据类型,例如整数、浮点数、字符等。
-
抽象数据类型(ADT):抽象数据类型是对数据类型的进一步抽象,是由数据对象、数据关系以及操作这些数据的基本操作所组成的一个整体。抽象数据类型通常描述一个数据集合及其相关操作。
抽象数据类型(ADT)
抽象数据类型(Abstract Data Type,ADT)是对数据及其操作的封装,定义了数据对象的集合以及能够在这些数据对象上执行的操作。
ADT 抽象数据类型名 {
数据对象: <数据对象的定义>
数据关系: <数据关系的定义>
基本操作: <基本操作的定义>
}
数据对象:定义了构成ADT的数据元素和它们的属性。
数据关系:定义了数据对象之间的关系。
基本操作:定义了可以对数据对象进行的操作,例如插入、删除、查找等。
现在,假如需要声明一个表示虚数的抽象数据类型,并定义其操作。
typedef struct {
float real;
float imag;
} ComplexNumber;
ComplexNumber createComplex(float real, float imag) {
ComplexNumber c;
c.real = real;
c.imag = imag;
return c;
}
ComplexNumber addComplex(ComplexNumber c1, ComplexNumber c2) {
ComplexNumber result;
result.real = c1.real + c2.real;
result.imag = c1.imag + c2.imag;
return result;
}
算法的特性和设计要求
算法的特性
算法:算法是解决问题的一种方法或过程,其核心目的是将输入数据转化为输出结果。一个问题可以有多种算法。
一个算法必须具备以下五个重要特性:
-
有穷性:一个算法必须在有限的步骤内结束,且每一步操作都能在有限时间内完成。即算法执行的步数是有限的,不会无休止地运行下去。
-
确定性:算法中的每一条指令必须是明确的、没有歧义的。对于相同的输入,算法的执行过程必须是唯一的,且每一次执行的结果必须是相同的。
-
可行性:算法是可执行的,描述的操作必须能通过已有的基本操作实现,并且这些操作必须在有限的时间内完成。
-
输入:一个算法可以有零个或多个输入。输入数据是算法处理的起点,算法根据输入数据生成输出。
-
输出:一个算法必须至少有一个输出结果,且输出应当是算法所解决的问题的解答。
算法设计要求
-
正确性:
- 算法在给定的输入数据下,能够准确无误地得出预期的结果。正确性不仅要求处理正常数据时得到正确答案,还要能够应对边界情况、极端输入以及可能的特殊情况。
-
可读性:
- 算法应当容易理解和维护。因为算法不仅是计算机执行的指令,更是为了与人进行交流和共享而存在。清晰、简洁的表达方式有助于减少错误并方便他人修改和调试。
- 良好的可读性还包括清晰的命名、简洁的结构和恰当的注释。
-
健壮性:
- 健壮性要求算法能够合理处理异常或非法输入数据,防止因输入错误导致程序崩溃或产生无意义的输出。
- 当输入数据不符合预期时,算法应能够作出适当的反应,例如通过返回错误代码或提示信息等,而不是直接中断执行。
-
高效性:
- 高效性要求算法尽量减少资源的消耗,包括时间(时间复杂度)和空间(空间复杂度)。时间复杂度越低,表示算法越快;空间复杂度越低,表示算法占用的内存越少。
- 设计高效的算法通常意味着在给定问题的约束下,尽量优化操作步骤的执行次数,避免不必要的重复计算和冗余操作。
算法效率分析
算法的效率通常通过时间复杂度和空间复杂度来衡量:
-
时间复杂度:表示算法执行时间与输入规模之间的关系。常见的时间复杂度包括 O(1)(常数时间)、O(log n)(对数时间)、O(n)(线性时间)、O(n²)(平方时间)等。时间复杂度较低的算法通常更高效。
-
空间复杂度:表示算法在执行过程中所需要的额外空间量。空间复杂度较低的算法通常更节省内存。
循环次数和效率
- 在算法中,循环是影响时间复杂度的一个重要因素。对于嵌套循环,内层循环的次数通常是外层循环次数的乘积,因此时间复杂度往往呈现乘法增长(如 O(n²))。嵌套循环的次数越多,算法的时间复杂度越高。
- 在设计算法时,需要避免不必要的嵌套循环,并尽量优化循环的执行次数,以提高算法的效率。
法算耗费时间就是每条的循环次数相加。比较耗时时只比较for嵌套(次方)较多的算法就越差。
算法时间复杂度
时间复杂度是用来描述算法的执行时间随输入规模 n 的增长而变化的情况。通常通过一个辅助函数 f(n) 来表示,表示随着输入规模的增加,算法执行时间的增长速度。若存在一个函数 f(n),使得当 n 趋近于无穷大时,T(n)/f(n) 的极限值是一个常数(不等于零),则称 f(n) 是 T(n) 的同数量级函数:
T(n)=O(f(n))
其中,O 是一个表示数量级的符号,称为 渐进时间复杂度。也就是说,时间复杂度 O(f(n)) 描述了算法的执行时间与f(n) 增长率之间的关系。
时间复杂度的含义
时间复杂度O(f(n)) 描述了随着 n 的增加,算法的执行时间是如何增长的。常见的时间复杂度形式有:
- O(1):常数时间复杂度,表示无论输入数据规模如何,算法的执行时间保持不变。
- O(log n):对数时间复杂度,表示算法的执行时间随着输入规模的对数增长(例如二分查找)。
- O(n):线性时间复杂度,表示算法的执行时间与输入规模成正比(例如遍历数组)。
- O(n^2):平方时间复杂度,表示算法的执行时间与输入规模的平方成正比(例如冒泡排序、选择排序)。
- O(n^3)、O(2^n) 等更高复杂度的表示法用于描述更复杂的算法。
时间复杂度的分析
时间复杂度的关键在于求解循环次数,通常情况下,嵌套循环的时间复杂度为外层循环和内层循环次数的乘积。例如,双重嵌套的循环常常是 O(n^2),三重嵌套的循环是 O(n^3),依此类推。
- O(1):访问数组元素,如
arr[i]
。 - O(n):线性遍历数组,如
for (i = 0; i < n; i++)
。 - O(n^2):双重循环遍历二维数组,如
for (i = 0; i < n; i++) { for (j = 0; j < n; j++) }
。
算法空间复杂度
空间复杂度是用来描述一个算法在执行过程中所需存储空间的度量,通常以输入规模 n 为变量,表示算法在运行时占用内存的增长情况。空间复杂度记作:
S(n) = O(f(n))
其中,S(n) 表示算法的空间复杂度,f(n) 是一个表示内存需求随输入规模变化的函数。
空间复杂度的含义
空间复杂度反映了算法执行过程中所需的额外存储空间。常见的空间复杂度有:
- O(1):常数空间复杂度,表示算法所需空间与输入规模无关,通常是仅使用了有限的几个变量。
- O(n):线性空间复杂度,表示算法的空间需求与输入规模成正比,常见于需要存储与输入规模相同大小的数据结构(例如数组、链表等)。
- O(n^2):平方空间复杂度,表示算法的空间需求与输入规模的平方成正比,常见于需要存储二维数据结构(如二维矩阵)。
空间复杂度的分析
- O(1):例如,一个只用常量空间的算法,如交换两个变量的值。
- O(n):例如,一个数组或链表的存储空间。
- O(n^2):例如,二维矩阵的存储空间。