1. 如何衡量一个算法的好坏?
在计算机中衡量一个算法的好坏,就是看这个算法执行起来的效率,算法的效率又分为两种:时间效率和空间效率。时间效率被称为时间复杂度,用来衡量一个算法的运行速度;空间效率被称为空间复杂度,用来衡量一个算法所需要的额外空间。
2. 什么是时间复杂度?
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。时间复杂度是用来衡量一个算法的运行速度的。
3. 时间复杂度为什么不使用时间来衡量而使用基本语句的运行次数来衡量?
因为从理论上来讲,时间是不能被算出来的,想要知道一个算法的运行时间,必须把程序放在机器上执行之后才能知道,如果想要知道很多算法的时间复杂度,那么必须把每个算法都上机测试一遍,这样会很麻烦。而一个算法的运行时间是和算法中的语句的执行次数是成正比的,所以用算法中的基本操作的执行次数来表示算法的时间复杂度。
4. 时间复杂度的大O渐进表示法
因为我们在计算时间复杂度的时候,并不一定要计算精确地执行次数,而只需要大概执行次数,我们使用大O渐进表示法。
O:是用来描述函数渐进行为的数学符号
大O渐进表示法具体方法:
(1)用常数1取代运行时间中的所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
5. 时间复杂度的:最优、平均、最差情况,为什么时间复杂度看的是最差情况?
因为我们要考虑最坏的情况,如果一个事情最坏的结果我们都可以接受,那么平均的和最优的就更可以接受了,算法也一样。
6. 如何求解:二分查找、递归求阶乘、递归斐波那契的时间复杂度?
(1)二分查找
int BinarySearch(int* arr,int size,int key) {
int left= 0;
int right=size - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (key > arr[mid]) {
left = mid + 1;
}
else if (key < arr[mid]) {
right = mid - 1;
}
else
return mid;
}
return -1;
}
二分查找每次都取数组中的中间元素来和目标元素比较,假设现在一个有序数组中有N个元素,每次都要将目标元素和当前区间的中间元素相比较,直到找到目标元素结束程序
最好情况:是1次就找到了
最坏情况:是每次和当前区间中间元素相比较,当前区间元素个数是N/2/2/2/2/2/2/2…=1,直到当前区间只剩下一个元素,比较完最后一个元素就结束 。这样看可能看不出来一共执行了多少次,我们可以把整个过程反过来看,最后一次当前区间肯定是只有一个元素,每次乘以2,直到当前区间元素个数为N
12222*2…*2=N
2^m=N
m=log2^N
O(m)=O(log2^N)
一共要执行log2^N 次,根据大O的渐进表示法,二分查找的时间复杂度是O(log2^N)。
(2)递归求阶乘
long long Factorial(size_t N) {
return N < 2 ? N :Factorial(N-1)*N;
}
//基本语句
if (N < 2) {
return 1;
}else {
return Factorial(N-1)*N;
}
递归函数的一条语句执行的次数=单词递归函数中执行次数 * 递归函数总的递归次数
N的阶乘可以表示为:
N !
(N - 1) ! * N
(N - 2) ! * (N - 1) * N
(N - 3) ! * (N - 2) * (N - 1) * N
…
想要求N的阶乘,就必须知道N-1的阶乘,想要知道N-1的阶乘就必须知道N-2的阶乘,依次类推,最后必须知道第一项的阶乘(1的阶乘就是1),才可以求第二项的阶乘,依次返回就可以求出N的阶乘了
在每次递归的过程中基本语句执行了一次,递归函数一共要递归N次,基本语句就执行了N*1次,递归求阶乘的时间复杂度就是O(N)。
(3)递归斐波那契函数
long long Fibonacci(size_t N) {
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
假设要求第六项,计算的方式如上图,计算F(6)是调用了递归函数1次,F(5)和F(4)是2次,接下来是4次,最后是8次。
总共调用了递归函数次数=2^0 + 2^1 + 2^2 + 2^3
第n项的调用递归函数次数=2^0 + 2^1 + 2^2 + …2(n-1)=2(n-4)
每次递归函数中调用了基本语句1次
根据大O的渐进表示法,递归菲波那切数列的时间复杂度就是O(2^N)。
7. 什么是空间复杂度?
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的衡量
8. 如何求空间复杂度? 普通函数&递归函数
空间复杂度虽然是衡量算法在运行过程中临时占用存储空间的大小,但是也并不是要求这个算法占用了多少bytes的空间,这个没有太大的意义,所以空间复杂度算的是变量的个数,用的也是大O的渐进表示法来表示。
普通函数的空间复杂度:
计算冒泡排序的空间复杂度
void BubbleSort(int* arr,int size) {
int bound = 0;
for (bound = 0; bound < size; bound++) {
for (int cur = size - 1; cur > bound; cur--) {
if (arr[cur] < arr[cur - 1]) {
swap(&arr[cur], &arr[cur - 1]);
}
}
}
}
在这里冒泡排序创建变量占用了额外空间,这个空间大小是常数,使用大O的渐进表示法,所以冒泡排序的空间复杂度为O(1)。
递归函数的空间复杂度:
计算递归求阶乘的空间复杂度:
long long Factorial(size_t N) {
return N < 2 ? N :Factorial(N-1)*N;
}
用递归的方法求N的阶乘,就是先求出前一项的阶乘,直到求出第一项的阶乘,再依次返回,就可以求出第N项的阶乘。每次调用都需要开辟新的空间,总共调用了N次,开辟了N个空间,每个空间大小是常数个大小,根据大O的渐进表示法,所以递归求阶乘的空间复杂度是O(N)。
由此我们也可以得出递归函数空间复杂度的求解方法:
递归函数的空间复杂度=递归函数调用的深度*单个函数需要的空间
9. 分析递归斐波那契数列的:时间、空间复杂度,并对其进行优化,伪递归优化—>循环优化
long long Fibonacci(size_t N) {
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
递归斐波那契数列的时间复杂度前面已经分析过了是O(2^N)
空间复杂度=递归函数调用的深度(N-1)*单个函数所用的空间(常数1)=O(N)
鉴于递归来求斐波那契数列的时间、空间复杂度太大了,可以进行优化
long long Fibonacci(long long first, long long second, int N) {
if (N < 3) {
return 1;
}
if (N == 3) {
return first + second;
}
if (N>3) {
return Fibonacci(second, first + second, N - 1);
}
}
优化后的算法通常被称为尾递归,将单次计算的结果缓存起来,传递给下次调用,相当于自动累积
时间复杂度是O(N)
空间复杂度是O(N)
注:尾递归有时候在特定环境下会产生编译器优化,即不会再为尾递归函数调用下一级函数时开辟新栈,而是直接在旧函数的内存块上进行修改),这时它的空间复杂度为O(1)
long long fib(size_t n)
{
long long n1 = 0,n2= 1,n3=n;
int i = 0;
for(i = 2;i<=n;i++)
{
n3 = n1+n2;
n1 = n2;
n2 = n3;
}
return n3;
}
用循环来求斐波那契数列
时间复杂度O(N)
空间复杂度O(1)
10. 常见时间复杂度
O(1)<O(N)<O(logN)<O(N^2)<O(N ^3)<O(2 ^N)