1、算法效率
1.1、算法效率的概念
算法效率是指算法执行所需的时间和资源。 一个高效的算法能够在较短的时间内完成任务,使用较少的内存和处理器资源。 与低效的算法相比,高效的算法能够显著提高程序的响应速度和整体性能。
1.2、如何衡量一个算法好坏
从算法效率的概念角度出发,算法的好坏主要跟时间和资源有关,所以通常衡量一个算法的好坏取决于时间和资源,也就是我们下面需要学习的时间复杂度和空间复杂度。
下面例举斐波那契数列的例子。
使用递归实现斐波那契数列。
long long Fib(int N) { if (N < 3) return 1; return Fib(N - 1) + Fib(N - 2); }
根据时间大O渐进表示法计算得出,时间复杂度为O(N),空间复杂度也为O(N),那为什么是这样的结果呢,接下来我们通过复杂度的详解来解释这个答案!
1.3、算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
1.3、算法度在校招中的考察
显而易见算法度在校招的考察还是比较重要的,接下来就正式进入复杂度的学习吧!
2、时间复杂度
2.1、时间复杂度的概念
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的 时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个 算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所 花费的时间与其中语句的执行次数成正比例,算法中的基本操作的**执行次数,为算法的时间复杂度**。
简而言之时间复杂度就是程序指令的执行次数。
接下来通过实际的代码计算一下时间复杂度。
// 请计算一下Func1中++count语句总共执行了多少次? 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); }
Func1 执行的基本操作次数 :
F(N)=N^2+2*N+10
N = 10 F(N) = 130
N = 100 F(N) = 10210
N = 1000 F(N) = 1002010
由此可见N的数值越大,与N^2的关联越大,是不是可以就大致认为执行次数是N ^2呢?
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
2.2、大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
但是是不是所有情况都能通过去掉影响不大的项来确定时间复杂度呢?
例如:在一个长度为N数组中搜索一个数据x。
我们应该怎么确定时间复杂度呢?
这时我们就出现了最坏情况、平均情况最好情况。
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
在一个长度为N数组中搜索一个数据x。
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。
总结:如果一个程序能用大O渐进表示法则复杂度用大O渐进表示,否则用当前程序的最坏情况表示复杂度。
2.3、常见时间复杂度计算举例
1.
// 计算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); }
因为时间复杂度实质是程序执行的次数。
1.分析该程序执行次数,由上述代码可知Func2函数执行了一个单独的for循环(共执行2N次),然后执行了一个单独的while循环(共执行10次)。所以Func2函数的执行次数为(2N+10)次
2.根据大O渐进表达法,只保留最高项(2*N),去除最高项的系数后(N),就是最终结果。因此用大O渐进表示法的时间复杂度为O(N)。
2.
// 计算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); }
因为时间复杂度实质是程序执行的次数。
1.分析该程序执行次数,由上述代码可知Func3函数执行了一个单独的for循环(共执行M次),然后执行了一个单独的for循环(共执行N次)。所以Func2函数的执行次数为(M+N)次
2.根据大O渐进表达法,只保留最高项,但是此处M和N都是未知数,不确定哪个影响更大,所以执行次数就是最终结果。因此用大O渐进表示法的时间复杂度为O(M+N)。
3.
// 计算Func4的时间复杂度? void Func4(int N) { int count = 0; for (int k = 0; k < 100; ++k) { ++count; } printf("%d\n", count); }
因为时间复杂度实质是程序执行的次数。
1.分析该程序执行次数,由上述代码可知Func4函数执行了一个单独的for循环(共执行100次)。由于Func4函数的执行次数不会随着N的改变而改变,所以Func4函数的执行次数为常数次。
2.根据大O渐进表达法,用常数1取代运行时间中的所有加法常数。因此用大O渐进表示法的时间复杂度为O(1)。
4.
const char* strchr(const char* str, int character); //此函数是在str这个字符串中查找与character相等的字符。 //函数思想是遍历整个字符串
因为时间复杂度实质是程序执行的次数。
1.分析该程序执行次数,由上述代码可知strchr函数执行了一个字符查找函数,函数思想是将字符串遍历查找,执行的次数是不统一的,所以该处需要用最坏情况法计算时间复杂度。(假设字符串长度为N)
2.根据最坏情况分析,最坏的情况是最后一个字符才查找到想要的结果,那么实际执行的次数是N次,所以时间复杂度是O(N)。
5.
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; } }
因为时间复杂度实质是程序执行的次数。
1.分析该程序执行次数,由上述代码可知BubbleSort函数执行了一个冒泡排序算法函数,函数思想是将相邻数据交换,执行的次数是不统一的,所以该处需要用最坏情况法计算时间复杂度。
2.根据最坏情况分析,最坏的情况是降序,那么实际执行的次数是N-1+N-2+…+1次,根据等差数列的求和公式得出,最终的执行次数是(N*(N-1))/2。
3.根据大O渐进表达法,只保留最高项((1/2)N^2),去除最高项的系数后
(N^2),就是最终结果。所以用大O渐进表示法的时间复杂度是O( N ^2)。
6.
// 计算BinarySearch的时间复杂度? int BinarySearch(int* a, int n, int x) { assert(a); int begin = 0; int end = n - 1; // [begin, end]:begin和end是左闭右闭区间,因此有=号 while (begin <= end) { int mid = begin + ((end - begin) >> 1); if (a[mid] < x) begin = mid + 1; else if (a[mid] > x) end = mid - 1; else return mid; } return -1; }
因为时间复杂度实质是程序执行的次数。
1.分析该程序执行次数,由上述代码可知BinarySearch函数执行了一个二分查找算法函数,函数思想是查找数据时,查找一次筛选一半的数据,直到找到想要的结果为止,由此可见执行的次数是不统一的,所以该处需要用最坏情况法计算时间复杂度。
2.根据最坏情况分析,最坏的情况是最后一次才找到结果,假设实际执行的次数是x,数据个数为N,有N/2/2…/2=1,即
2^x=N
x=log N(以2为底)
所以时间复杂度是O(logN)。
7.
// 计算阶乘递归Fac的时间复杂度? long long Fac(size_t N) { if (0 == N) return 1; return Fac(N - 1) * N; }
因为时间复杂度实质是程序执行的次数。
1.分析该程序执行次数,由上述代码可知Fac函数执行了求阶层算法函数,函数思想是参数不等于0则进行递归,直到参数等于0才停止,所以函数递归的顺序是Fac(N-1),Fac(N-2)…Fac(0),总共N次。
所以时间复杂度是O(N)。
8.
// 计算斐波那契递归Fib的时间复杂度? long long Fib(size_t N) { if (N < 3) return 1; return Fib(N - 1) + Fib(N - 2); }
因为时间复杂度实质是程序执行的次数。
1.分析该程序执行次数,由上述代码可知该函数时求斐波那契数,求一个斐波那契时需要知道前两个数,也就是需要递归调用两次函数。递归的次数如下图。
上图是大概的情况,但是计算起来并不方便,所以用下图表示更好计算执行次数。
最终的执行次数为等比数列的求和减去没有数据的一块(假设为X)
所以最终的执行次数为2^N-1-X
根据大O渐进表达法,只保留最高项(N^2),因此用大O渐进表示法的时间复杂度为O(N ^2)。
- 实例1基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)
- 实例2基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)
- 实例3基本操作执行了100次,通过推导大O阶方法,时间复杂度为 O(1)
- 实例4基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为 O(N)
- 实例5基本操作执行最好N次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最坏,时间复杂度为 O(N^2)
- 实例6基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN) ps:logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。
- 实例7通过计算分析发现基本操作递归了N次,时间复杂度为O(N)。
- 实例8通过计算分析发现基本操作递归了2^N次,时间复杂度为
O(2 ^N)。
3、常见时间复杂度OJ练习
3.1、消失的数字
链接: 3.1消失的数字OJ链接:https://leetcode-cn.com/problems/missing-number-lcci/
int missingNumber(int* nums, int numsSize) { int sum1=0; int sum2=0; for(int i=0;i<numsSize;i++) { sum1+=nums[i]; } for(int i=0;i<numsSize+1;i++) { sum2+=i; } return sum2-sum1; }
int missingNumber(int* nums, int numsSize) { int sum1 = 0; for (int i = 0; i < numsSize; i++) { sum1 ^= nums[i]; } for (int i = 0; i < numsSize + 1; i++) { sum1 ^= i; } return sum1; }
总结
本篇博客就结束啦,谢谢大家的观看,如果公主少年们有好的建议可以留言喔,谢谢大家啦!