引言
在现代计算机科学和编程中,算法的效率至关重要。算法效率不仅影响程序的运行时间,还直接关系到程序的内存使用情况。为了评估和优化算法,我们常用两个主要指标:时间复杂度和空间复杂度。本文将详细介绍这两个概念,并通过C语言示例来解释它们的实际应用。
一、算法效率的基础
在算法设计中,我们先后追求以下两个层面的目标。
1. 找到问题解法:算法需要在规定的输入范围内可靠地求得问题的正确解。
2. 寻求最优解法:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。
也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度。
‧ 时间效率:算法运行速度的快慢。
‧ 空间效率:算法占用内存空间的大小。
简而言之,我们的目标是设计“既快又省”的数据结构与算法。时间效率和空间效率的评估可以帮助我们选择合适的算法来处理特定问题,并优化程序性能。时间复杂度和空间复杂度是用于衡量这两个方面的关键指标。
二、时间复杂度
1.概念
时间复杂度(Time Complexity)用来衡量算法执行所需时间如何随着输入规模的增长而变化。它帮助我们评估算法在处理大数据量时的表现。时间复杂度通常用大O符号表示,描述了算法在最坏情况下的运行时间。
O的渐进表⽰法
⼤O符号(Big O notation):是⽤于描述函数渐进⾏为的数学符号
💡 推导⼤O阶规则
1. 时间复杂度函数式 T(N) 中,只保留最⾼阶项,去掉那些低阶项,因为当 N 不断变⼤时,
低阶项对结果影响越来越⼩,当 N ⽆穷⼤时,就可以忽略不计了。
2. 如果最⾼阶项存在且不是 1 ,则去除这个项⽬的常数系数,因为当 N 不断变⼤,这个系数
对结果影响越来越⼩,当 N ⽆穷⼤时,就可以忽略不计了。
3. T(N) 中如果没有 N 相关的项⽬,只有常数项,⽤常数 1 取代所有加法常数。
2.常见类型
1.O(1) — 常数阶
常数阶时间复杂度指的是算法的运行时间与输入数据大小 𝑛 无关,即不随着 𝑛 的变化而变化。
在以下函数中,尽管操作数量 size 可能很大,但由于其与输入数据大小 𝑛 无关,因此时间复杂度仍为 𝑂(1) :
/* 常数阶 */ int constant(int n) { int count = 0; int size = 100000; int i = 0; for (int i = 0; i < size; i++) { count++; } return count; }
2.O(n) — 线性阶
线性阶时间复杂度指的是算法的运行时间随着输入规模n增加而以线性级别增长。
线性阶通常出现在单层循环中:
/* 线性阶 */ int linear(int n) { int count = 0; for (int i = 0; i < n; i++) { count++; } return count; }
遍历数组和遍历链表等操作的时间复杂度均为 𝑂(𝑛) ,其中 𝑛 为数组或链表的长度:
/* 线性阶(遍历数组) */ int arrayTraversal(int* nums, int n) { int count = 0; // 循环次数与数组长度成正比 for (int i = 0; i < n; i++) { count++; } return count; }
3.O(n^2) — 平方阶
平方阶时间复杂度指的是算法的运行时间随着输入规模n增加而以平方级别增长。
平方阶通常出现在嵌套循环中,外层循环和内层循环的时间复杂度都为 𝑂(𝑛) ,因此总体的时间复杂度为 𝑂(𝑛 2 ) :
/* 平方阶 */ int quadratic(int n) { int count = 0; // 循环次数与数据大小 n 成平方关系 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; } } return count; }
4.O(2^𝑛) — 指数阶
指数阶时间复杂度指的是算法的运行时间随着输入规模n增加而以指数级别增长。
生物学的“细胞分裂”是指数阶增长的典型例子:初始状态为 1 个细胞,分裂一轮后变为 2 个,分裂两轮后变为 4 个,以此类推,分裂 𝑛 轮后有 2 𝑛 个细胞。
指数时间复杂度通常出现于解决组合问题或递归深度较大的算法:
/* 指数阶(递归实现) */ int expRecur(int n) { if (n == 1) return 1; return expRecur(n - 1) + expRecur(n - 1) + 1; }
5.O(log 𝑛) — 对数阶
对数阶时间复杂度指的是算法的运行时间随着输入规模n增加而以对数级别增长。
与指数阶类似,对数阶也常出现于递归函数中。以下代码形成了一棵高度为 log𝑛 的递归树:
/* 对数阶(递归实现) */ int logRecur(int n) { if (n <= 1) return 0; return logRecur(n / 2) + 1; }
O(log 𝑛) 的底数是多少?
准确来说,“一分为 𝑚 ”对应的时间复杂度是 𝑂( log 𝑚 𝑛) 。而通过对数换底公式,我们可以得到具有不同底数、相等的时间复杂度:
也就是说,底数 𝑚 可以在不影响复杂度的前提下转换。因此我们通常会省略底数 𝑚 ,将对数阶直接记为 𝑂( log 𝑛) 。
3.总结
设输入数据大小为 𝑛 ,常见的时间复杂度类型如图 2‑9 所示(按照从低到高的顺序排列)。
O(1) < O(log n) < 𝑂(𝑛) < O(n log n) < O(2^𝑛) < O(2^𝑛) < O(𝑛!)
常数阶 < 对数阶 < 线性阶 < 线性对数阶 < 平方阶 < 指数阶 < 阶乘阶
三、空间复杂度
1.概念
空间复杂度(Space Complexity)衡量算法在执行过程中所需的额外内存空间如何随着输入规模的增长而变化。它描述了算法对内存的需求,通常也用大O符号表示。(这个概念与时间复杂度非常类似,只需将“运行时间”替换为“占用内存空间”。)
2.常见类型
1.O(1) — 常数阶
常数空间复杂度表示算法所需的额外内存空间不随输入规模变化。例如,交换两个变量的值:
#include <stdio.h> void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int main() { int x = 10, y = 20; swap(&x, &y); printf("x = %d, y = %d\n", x, y); return 0; }
2.O(n) — 线性阶
线性空间复杂度表示算法所需的额外内存空间与输入规模成正比。例如,创建一个数组:
#include <stdio.h> #include <stdlib.h> int *create_array(int size) { int *arr = (int *)malloc(size * sizeof(int)); for (int i = 0; i < size; i++) { arr[i] = i; } return arr; } int main() { int size = 5; int *arr = create_array(size); for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); free(arr); return 0; }
3.O(n^2) — 平方阶
平方空间复杂度表示算法的额外内存使用与输入规模的平方成正比。例如,创建一个二维矩阵:
#include <stdio.h> #include <stdlib.h> int **create_matrix(int n) { int **matrix = (int **)malloc(n * sizeof(int *)); for (int i = 0; i < n; i++) { matrix[i] = (int *)malloc(n * sizeof(int)); } return matrix; } void print_matrix(int **matrix, int n) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { printf("%d ", matrix[i][j]); } printf("\n"); } } int main() { int n = 3; int **matrix = create_matrix(n); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { matrix[i][j] = i * n + j + 1; } } print_matrix(matrix, n); for (int i = 0; i < n; i++) { free(matrix[i]); } free(matrix); return 0; }
四、总结
理解时间复杂度和空间复杂度是编写高效程序的基础。时间复杂度告诉我们算法的运行时间如何随输入规模变化,而空间复杂度则描述了算法对内存的需求。掌握这些概念可以帮助我们选择和优化算法,提高程序的性能。
希望本文能帮助你更好地理解算法复杂度。如果有任何问题或讨论,请在评论区留言!