算法效率
我们所说的算法效率包括两种,一种是时间效率,一种是空间效率,顾名思义就是一个算法所用的时间和它所占的额外空间,但是问题来了,每个计算机的性能都是不一样,因此同一个代码在不同电脑上的时间效率也会不一样,所以到底要用哪一台电脑测出来的结果为准呢?
因此为了更好地计算一个算法的时间效率和空间效率,人们决定用数学的方法来算出一个大概的量级来代表算法的效率,我们将其称为时间复杂度和空间复杂度,虽然这样算出来的结果可能和精确值差了不少,但是我们要明白,电脑在运算方面是远远快于人类的,所以这些小误差是影响不了什么的。
时间复杂度
定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
方法:大O的渐进表示法;大O符号(Big O notation):是用于描述函数渐进行为的数学符号
推导大O阶方法
①用常数1取代运行时间中的所有加法常数。
②在修改后的运行次数函数中,只保留最高阶项。
③如果最高阶项存在且不是1,则去除与该项相乘的常数。得到的结果就是大O阶。
特殊情况:有些算法的时间复杂度存在最好、平均和最坏情况:
①最坏情况:任意输入规模的最大运行次数(上界);
②平均情况:任意输入规模的期望运行次数;
③最好情况:任意输入规模的最小运行次数(下界);
在实际开发中,遇到了特殊的情况的话,我们一般是关注算法的最坏时间复杂度的;因为如果你的代码会出现问题,那么最有可能导致问题的原因就是最坏时间复杂度,我认为这个和木桶效应的例子是类似的。
例题:下面举出了一些例子,算算时间复杂度是多少?
//第一题: 计算Func1的时间复杂度? 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^2+2*N+10;通过推导大O阶方法知道,时间复杂度为 O(N^2) //第二题: 计算Func2的时间复杂度? 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); }
//基本操作执行了2N+10次;通过推导大O阶方法知道,时间复杂度为 O(N) //第三题: 计算Func3的时间复杂度? void Func3(int N, int M){ int count = 0; for (int k = 0; k < M; ++ k){ ++count; } for (int k = 0; k < N ; ++ k){ ++count; } printf("%d\n", count); }
//基本操作执行了M+N次;有两个未知数M和N,时间复杂度为 O(N+M) //第四题: 计算Func4的时间复杂度? void Func4(int N){ int count = 0; for (int k = 0; k < 100; ++ k){ ++count; } printf("%d\n", count); }
//基本操作执行了10次;通过推导大O阶方法,时间复杂度为 O(1) //第五题: 计算strchr的时间复杂度?这个是在字符串中查找字符的函数 const char * strchr ( const char * str, int character );
//基本操作执行最好1次,第一个字符就是要查找的;最坏N次,最后一个字符就是要查找的;时间复杂度一般看最坏,时间复杂度为 O(N) //第六题: 计算BubbleSort的时间复杂度? void BubbleSort(int* a, int n){ assert(a); for (size_t end = n; end > 0; --end){ int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]){ Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
//基本操作执行最好N次,本身是有序的,但是需要执行一次进行检查;最坏执行了(N*(N+1)/2次,数列是倒序排列的;通过推导大O阶方法+时间复杂度一般看最 坏,时间复杂度为 O(N^2) //第七题: 计算BinarySearch的时间复杂度? int BinarySearch(int* a, int n, int x){ assert(a); int begin = 0; int end = n-1; while (begin < end){ int mid = begin + ((end-begin)>>1); if (a[mid] < x) begin = mid+1; else if (a[mid] > x) end = mid; else return mid; } return -1; }
//基本操作执行最好1次,要查找的就是在中间位置;最坏O(logN)次,每一次都会排除掉一半的数,所以是对2求对数;时间复杂度为 O(logN) ps:logN在算法分析中表示是底 数为2,对数为N。有些地方会写成lgN。 //第八题: 计算阶乘递归Factorial的时间复杂度? long long Factorial(size_t N){ return N < 2 ? N : Factorial(N-1)*N; }
//通过计算分析发现基本操作递归了N次;时间复杂度为O(N)。 //第九题: 计算斐波那契递归Fibonacci的时间复杂度? long long Fibonacci(size_t N){ return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2); }
//通过计算分析发现基本操作递归了2^N次,第一次需要求F(N-1),F(N-2),第二次需要求F(n-2)、f(N-3),F(N-3)、F(N-4),以此类推,会进行2^N次;时间复杂度为O(2^N)。
总结一些常见的阶级:
1、O(1):常数阶。这是效率最高的,但是实际开发中不常见,可遇不可求;
2、O(logn):对数阶。对数阶的效率是次于常数阶的,这是我们日后工作中所需要追求的极致效率;例如二分法,因此二分法的思维适用于很多事情上,可以很明显的提高效率;
3、O(n):线性阶。线性阶的效率是次于对数阶的,虽然没有那么厉害,但是这是实际工作中最常见的,我们如果达不到对数阶,也要尽量达到线性阶;例如一层循环;
4、O(n ^ 2):平方阶的效率比上面的都要低一些,上面三种虽然效率排前三,但是并不容易做到,因此如果能达到平方阶的话,我们也认为这个算法是好的,至少是有效的;例如二层循环;
5、O(2 ^ n):指数阶,这是效率最低的,它所占用的时间呈指数级增长;如果n的数值较小,我们是可以在一定的时间内计算出结果的,但是n一旦取值较大,那么我们将会等待漫长的时间才能完成运算;所以这样的算法是不能投入应用的,是需要改进的,而如何改进,这就要看对于数据结构和算法的学习理解程度了;例如递归就是一种很常见的且很容易出现这样情况的算法,所以在实际开发中能不使用递归就不使用递归。
空间复杂度
定义:空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。我们计算空间复杂度不是要算清程序到底占用了多少 bytes的空间,(因为在计算机发展的早期,计算机的存储容量很小,所以对空间复杂度很是在乎,但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度,所以计算的这么精细没什么意义)而是计算变量的个数。
方法:空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。需要注意的是,时间是不可以复用的,但是空间可以复用,因此在计算空间复杂度的时候,要把这点考虑上,有时候实际上使用的空间可能比你想象得要少。
注意:在实际开发中,由于现在计算机存储容量发展的越来越快,而且时间的效率变得越发重要,所以相比空间复杂度我们往往更加关注于时间复杂度,在有些时候,我们甚至会牺牲空间复杂度,来追求时间效率上的极致。
例题:下面举出了一些例子,算算空间复杂度是多少?
// 第一题:计算BubbleSort的空间复杂度? void BubbleSort(int* a, int n){ assert(a); for (size_t end = n; end > 0; --end){ int exchange = 0; for (size_t i = 1; i < end; ++i){ if (a[i-1] > a[i]){ Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } } //使用了常数个额外空间,所以空间复杂度为 O(1) // 第二题:计算Fibonacci的空间复杂度? long long* Fibonacci(size_t n){ if(n==0) return NULL; long long * fibArray =(long long *)malloc((n+1) * sizeof(long long)); fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n ; ++i) { fibArray[i ] = fibArray[ i - 1] + fibArray [i - 2]; } return fibArray ; } //在堆上动态开辟了N个空间,空间复杂度为 O(N) // 第三题:计算斐波那契递归Fibonacci的空间复杂度? long long Fibonacci(size_t N){ return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2); } //先算的是F(N-1)、F(N-2),但是这两个也分先后顺序,应该先算F(N-1),等到F(N-1)全部递归完之后,才会算F(N-2);而在算F(N-1)下面过程时也是一样的过程,会分先后计算,所以最长的调用栈为F(N)-->F(1),所以空间复杂度为O(N); // 第四题:计算阶乘递归Factorial的空间复杂度? long long Factorial(size_t N){ return N < 2 ? N : Factorial(N-1)*N; } //递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)
浙公网安备 33010602011771号