在数据结构的学习旅程中,算法犹如闪耀的灯塔,为我们指引解决问题的方向。今天,让我们一同深入探索《数据结构》C 语言版第 3 版中第一章第四节 —— 算法和算法分析的精彩内容。
1.4.1 算法的定义及特性
算法,简洁来说,是解决特定问题的一系列明确、有限且可执行的步骤。它如同精心撰写的剧本,每个步骤都有着清晰的目的和顺序。
算法具有五个关键特性:
- 有穷性:算法必须在有限的步骤内完成任务,不能陷入永无止境的循环。想象一下,如果计算两个数之和的算法一直运行,永远得不到结果,那将毫无意义。
- 确定性:算法的每一个步骤都有确切的定义,不存在模棱两可的情况。以欧几里得算法求最大公约数为例,“用较大数除以较小数” 这一操作清晰明确,不会让人产生误解。
- 输入:算法可以有零个或多个输入,这些输入是算法处理的对象。例如在排序算法中,待排序的数组就是输入数据。
- 输出:算法执行结束后必须有一个或多个输出,即问题的求解结果。如计算圆面积的算法,最终会输出根据半径计算出的面积值。
- 可行性:算法中的每一步操作都应是计算机能够有效执行的基本操作,如四则运算、逻辑判断、数据传输等。
1.4.2 评价算法优劣的基本标准
一个优秀的算法,就像一把锋利的宝剑,能够高效地解决问题。评价算法优劣通常从以下几个方面考量:
- 正确性:这是算法的基石,算法必须能够正确地解决特定问题,满足所有预期的输入输出要求。例如,用于计算个人所得税的算法,必须依据正确的税率和计算规则得出准确结果。
- 可读性:算法的代码应简洁明了,注释充分,便于开发者理解和维护。当团队协作开发大型项目时,可读性强的算法能极大提高沟通和修改代码的效率。
- 健壮性:面对非法输入或异常情况,算法应具备良好的容错能力,不会轻易崩溃。比如在一个处理用户输入年龄的算法中,当用户输入负数时,算法应能给出合理提示,而非出现程序错误。
- 高效性:包含时间高效性和空间高效性。时间高效性指算法运行速度快,能在较短时间内得出结果;空间高效性表示算法执行过程中占用的内存空间少。在资源有限的环境中,高效性尤为重要。
1.4.3 算法的时间复杂度
1. 问题规模和语句频度
问题规模是指算法所处理数据的数量。例如排序算法中待排序元素的个数,搜索算法中数据集的大小等。语句频度则是指算法中每条语句被重复执行的次数。它反映了语句在算法执行过程中的执行频率。
2. 算法的时间复杂度定义
算法的时间复杂度是用来衡量算法执行时间随问题规模增长而增长的量级。通常用大 O 记号表示,它关注的是随着问题规模 n 的增大,算法执行时间的渐近增长趋势,忽略常数项和低阶项的影响。例如,若算法的执行时间 T (n) 与问题规模 n 的关系为 T (n)=3n² + 2n + 1,当 n 足够大时,2n + 1 的影响相对 3n² 可忽略不计,此时该算法的时间复杂度为 O (n²)。
3. 算法的时间复杂度分析举例
以常见的冒泡排序算法为例,它通过多次比较相邻元素并交换位置,将最大(或最小)的元素逐步 “冒泡” 到数组末尾。假设数组长度为 n,其核心代码如下:
for (i = 0; i < n - 1; i++) {
for (j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
外层循环执行 n - 1 次,内层循环在第 i 次外层循环时执行 n - i - 1 次。通过计算可得,总的比较和交换操作次数约为 n (n - 1)/2,忽略常数项和低阶项,冒泡排序的时间复杂度为 O (n²)。
4. 最好、最坏和平均时间复杂度
- 最好时间复杂度:在最理想的输入情况下,算法执行的时间复杂度。例如在有序数组中查找某个元素,若使用顺序查找算法,当要查找的元素恰好在数组第一个位置时,只需比较一次,此时最好时间复杂度为 O (1)。
- 最坏时间复杂度:在最糟糕的输入情况下,算法执行的时间复杂度。如上述冒泡排序,当数组完全逆序时,需要进行最多的比较和交换操作,时间复杂度达到 O (n²),这就是冒泡排序的最坏时间复杂度。
- 平均时间复杂度:考虑所有可能输入情况下,算法执行时间的平均值。计算平均时间复杂度较为复杂,需要对各种输入情况及其出现概率进行分析。一般情况下,我们更关注最坏时间复杂度,因为它能为算法在最差情况下的性能提供保障。
1.4.4 算法的空间复杂度
算法的空间复杂度用于衡量算法在执行过程中所需的额外存储空间,同样用大 O 记号表示。它主要考虑除输入数据本身占用空间外,算法在运行时临时占用的辅助空间。
例如,在一个简单的交换两个变量值的算法中:
void swap(int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
这里只引入了一个临时变量 temp,无论输入数据规模如何,额外占用的辅助空间都是一个固定大小,所以该算法的空间复杂度为 O (1)。
而对于一些递归算法,如计算斐波那契数列的递归算法,每一次递归调用都需要在栈中保存一些信息(如局部变量、返回地址等),随着递归深度的增加,栈空间的占用也会增加,其空间复杂度与递归深度有关。
算法的时间复杂度和空间复杂度往往相互制约。在实际应用中,需要根据具体场景,在时间效率和空间效率之间进行权衡,选择最合适的算法。