目录
本节目标
1、学会什么是算法效率
2、学会时间复杂度和空间复杂度的表示与计算方法
3、深入理解时间复杂度和空间复杂度
4、会独立计算C语言中常见算法的时空复杂度
一、算法效率
算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率
被称作空间复杂度。时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机
行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法
的空间复杂度。
二、时间复杂度
1、时间复杂度的概念
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一
个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知
道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个
分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
2、时间复杂度的表示方法
我们计算时间复杂度时不是计算算法运行的具体次数,而是用大O的渐进表示法来计算,其具体计算方法如下:
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
3、简单时间复杂度的计算
示例一
void Func(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
上面程序具体执行的次数:100
用大O的渐进表示法得出时间复杂度:O(1) (用常数1取代运行时间中的所有加法常数。)
示例二
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
上面程序具体执行的次数:N * N + 2*N + 10
用大O的渐进表示法得出时间复杂度:O(N^2) (只保留最高阶项)
示例三
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
上面程序具体执行的次数:2*N + 10
用大O的渐进表示法得出时间复杂度:O(N) (如果最高阶项存在且不是1,则去除与这个项目相乘的常数)
算法复杂度的三种情况:
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
平均情况:N/2次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。
4、复杂时间复杂度的计算
(1)冒泡排序的时间复杂度
void BubbleSort(int arr[], int n)
{
int i = 0;
int j = 0;
for (i = 0; i < n - 1; i++)
{
for (j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
具体执行次数:时间复杂度计算时以最坏情况为准,则假设数组开始是逆序的,那么第一次排序执行 n-1 次,第二次排序执行 n-2 次,第三次执行 n-3 次 … …,直到最后达成有序,所以冒泡排序的具体执行次数是一个等差数列,具体次数 = (首项+尾项)/2*项数 = (N^2-N)/2
用大O的渐进表示法得出时间复杂度:O(N^2)
(2)二分查找的时间复杂度
int BinarySearch(int arr[], int n, int x) //n元素个数 x:要查找的数
{
int left = 0;
int right = n - 1;
while (left < right)
{
int mid = (left + right) / 2;
if (arr[mid] > x)
{
right = mid - 1; //中间元素比x大就挪动right下标
}
else if (arr[mid] < x)
{
left = mid + 1; //中间元素比x小就挪动left下标
}
else
return mid; //找到就返回该元素所在的下标
}
return 0; //找不到就返回0
}
具体执行次数:和上面一样,这里考虑最坏情况,即数组中没有想找的元素,数组会从中间开始查找,每次排除掉一半的元素,直到把所有元素排除完,第一次排除后剩下 1/2 的元素,第二次排除后剩下 1/4 元素,第三次排除后剩下 1/8 元素 … …,设元素个数为N,查找次数为X,则 X * (½)^N = 1 -> (½)^N = X -> 具体次数:X = log2N
用大O的渐进表示法得出时间复杂度:O(logN)
注:因为在键盘上无法表示出log的底数,所有在时间复杂度中把log的底数2省略掉了,直接用logN表示log2N的时间复杂度。
(3)阶乘递归的时间复杂度
long long Factorial(int N)
{
return N < 2 ? N : Factorial(N - 1) * N;
}
具体次数:这里 n 调用 n-1 , n-1 调用 n-2 …,直到 n = 1,所以一共执行了 n-1 次。
用大O的渐进表示法得出时间复杂度:O(N)
以五的阶乘示例:
(4)斐波那契递归的时间复杂度
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}
具体次数:以上图为例,我们可以看到在数值大于2的时候,每一层的调用次数是以2的指数形式增长的,是一个等比数列,所以用大O的渐进表示法得出时间复杂度为:O(2^N)
五、不同时间复杂度效率的比较
我们可以看到当测试数据很大时 O(logN) 和 O(1) 的效率几乎是一样的,所以二分查找是一种效率很高的算法,但是它也有一个缺陷,那就是它操作的数组元素必须是有序的。
三、空间复杂度
1、空间复杂度的概念
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
2、空间复杂度的计算方法
空间复杂度的计算方法和时间复杂度非常相似,且都是用大O的渐进表示法表示。
具体计算方法如下:
- 用常数1取代运行过程中定义的常数个变量。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
3、常见空间复杂度的计算
(1)冒泡排序的空间复杂度
void BubbleSort(int arr[], int n)
{
int i = 0;
int j = 0;
for (i = 0; i < n - 1; i++)
{
for (j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
空间复杂度算的是定义的变量个数,这里定义了三个变量,所以空间复杂度是O(1)。
(2)二分查找的空间复杂度
int BinarySearch(int arr[], int n, int x) //n元素个数 x:要查找的数
{
int left = 0;
int right = n - 1;
while (left < right)
{
int mid = (left + right) / 2;
if (arr[mid] > x)
{
right = mid - 1; //中间元素比x大就挪动right下标
}
else if (arr[mid] < x)
{
left = mid + 1; //中间元素比x小就挪动left下标
}
else
return mid; //找到就返回该元素所在的下标
}
return 0; //找不到就返回0
}
和冒泡排序的空间复杂度一样,这里只定义了三个(常数个)变量,所以空间复杂度是O(1)。
(3)阶乘递归的空间复杂度
long long Factorial(int N)
{
return N < 2 ? N : Factorial(N - 1) * N;
}
在计算递归类空间复杂度度时,我们看递归的深度,这里我们每次调用Factorial 的时候都会在栈区上开辟一块空间,并且在 n = 1之前开辟的空间不会被释放掉,只有等到 n = 1,递归一步一步开始返回的时候开辟的空间才会被逐渐释放,所以这里调用的深度为 n - 1(递归 n - 1 次),所以空间复杂度为O(N)。
(4)斐波那契递归的空间复杂度
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}
要想解决这个问题,我们首先要清楚,斐波那契是逐个分支进行递归的,以上图为例,它会先递归6-5-4-3-2-1,再递归6-5-4-3-2,再递归6-5-4-2,以此类推,直到把最后一个分支递归完,同时我们又知道,递归调用时根据递归深度逐层在栈区上开辟空间,递归返回时又逐层释放空间,也就是说,当我们递归6-5-4-3-1时,递归深度(空间复杂度)为五,而当我们在递归6-5-4-3-1时,空间复杂度又会从0重新开始计算,而复杂度的计算看最坏情况,这里最多递归次数为 n - 1,所以空间复杂度为O(N)。
四、总结
- 时间复杂度和空间复杂度都是用大O的渐进表示法来表示。
- 时间复杂度看运算执行的次数,空间复杂度看变量定义的个数。
- 递归时时间复杂度看调用的次数,空间复杂度看调用的深度。
- 冒泡排序的时间复杂度为O(N^2),空间复杂度为O(1)。
- 二分查找的时间复杂度为O(logN),空间复杂度为O(1)。
- 阶乘递归的时间复杂度为O(N),空间复杂度为O(N)。
- 斐波那契递归的时间复杂度为O(2^N),空间复杂度为O(N)。