目录
前言:
1.时间复杂度
1.1时间复杂度的理解
1.2规模与基本操作执行次数
1.3大O渐进表示法
1.4计算基本操作的次数
2.常见的时间复杂度及其优劣比较
❤博主CSDN:啊苏要学习
▶专栏分类:数据结构◀
学习数据结构是一件有趣的事情,希望读者能在我的博文切实感受到,数据之间存在的关系,在对数据元素进行操作的时候,能心中有数,脑中有画!
前言:
在计算机中,如何判断一个算法的好与坏,我们使用时间复杂度来评判。时间复杂度不是一个精确的值,而是大致描绘算法的时间效率。大家目前写的代码对于计算机来说都是可以很快就跑完的,因为都是比较简单的常数阶O(1)或线性阶O(n),但如果写了一个很差劲的算法,加上规模很大(数据多),计算机看了都要落泪。
1.时间复杂度
1.1时间复杂度的理解
时间复杂度的概念,为什么需要有时间复杂度的概念?因为我们人在算术的时候,是需要耗费时间的;计算机在执行我们预设的指令也需要耗费时间,耗费时间长短就显的比较重要,在保证不错的前提下,我们期望计算机能很快跑出结果,而不用等上一段时间。
了解完时间复杂度的必要性,那我们如何算一个算法的时间复杂度呢?把一个程序放在计算机上跑起来,计算其用的时间?这个方法显然是不可行的,相信读者也想到了:不同计算机,硬件的性能和软件的加持是不一样的;即使是同一台计算机,你也不能完全保证在执行过程中,每一次计算机的状态和前一次是完全一样的;。
1.2规模与基本操作执行次数
假设计算机在处理每一条语句的时间是一个计算机单位时间,我们通过计算算法里基本操作的执行次数,使用大O(有点像极限)表示法来表示:
void Func(int N)
{
int count = 0;
for(int i = 0; i < N; i++)//执行n次
{
for(int j = 0; j < N; j++)
{
count++;//对于外层的每一次,里层都要执行n次
//n*n
}
}
for(int k = 0; k < 5N; k++)
{
count++;//5n
}
int a = 10;
while(a--)
{
count++;//10
}
printf("%d", count);
}
这里面count这个基本操作被执行了几次呢?
- 第一部分是两层循环:执行了n^2次;
- 第二部分是一层for循环:执行了5n次
- 第三部分是while循环:执行了10次
总共执行了N = n^2 + 5n + 10次,随着N(问题规模)的增大,执行的次数也会随之增大。我们来比较一下下面所列出的问题规模对应的需执行基本操作的次数:
- N = 10 、n = 160
- N = 100、n = 10510
- N = 1000、n = 1005010
随着问题规模的增大,n也在变大。
我们计算N = n^2的执行次数
N = 10、n = 100
N = 100、n = 10000
N = 1000、n = 1000000
对比5n+10这部分,n^2对执行次数的贡献是最大的,我们只考虑n^2,时间复杂度就是O(N^2),这就是大O渐进表示法。随着规模的增大,对算法时间效率高低影响最大的那一部分,就是该算法的时间复杂度。
大O渐近表示法:我们只关注执行次数表达式中对结果影响最大的那一项,也就是最高阶。
1.3大O渐进表示法
我们再通过分析代码来理解大O法表示时间复杂度以及总结大O表示的方法。
void Fun(int N)
{
int count = 0;
for(int k = 0; k < 2N; k++)
{
count++;//2n次
}
int n = 10;
while(n--)
{
count++;//10次
}
}
这段代码的执行次数是2n+10,时间复杂度是多少呢?大家可能说是O(2N),但真的是这样吗?其实应该是O(N)。不妨这样想,对于次数起最大影响的是由n引起的,如果n为0,2n就为0,就只有固定的10次。而n从0-10000,次数也就越来越多的,前面的2改变次数的多和少还是取决于n的大小。
大O渐近表示法:对于最高次数的项,与之相乘或相除的常数都是可以省略的,省略后就是大O表示法。
void Fun(int N, int M)
{
int count = 0;
for(i = 0; i < N; i++)
{
count++//n次
}
for(j = 0; j < M; j++)
{
count++;//m次
}
}
这里的时间复杂度是O(N+M),想表达的点是,我们习惯用N来表示执行的次数,不要因为习惯而阻碍了我们做出正确的判断。这里N和M都是未知数,对基本操作的次数都有影响,所以自然就都得写在大O里面。大O表示法可以有两个未知数,甚至多个,视情况而定。
如果条件里有说N远大于M,时间复杂度就变成了O(N);如果说N和M差不读,时间复杂度为O(N)或O(M),因为N+M相当于2N或2M,最高项前面的常数影响不大。
void Fun(int N)
{
int count = 0;
for(int i = 0; i < 100; i++)
{
count++;//100次
}
}
这里执行了100次,时间复杂度不是O(N),因为N我们没用到。那是不是O(100)呢?也不是,我们用1来表示大O表示法中的所有加法常数。所以这段代码的时间复杂度是O(1),表示执行常数次,O(1)不是说算法执行1次的意思,就好像执行100次写成O(1),不是说只执行一次,这里强调的是,不管规模怎么变,变的多大,这个算法执行完常数次就结束,是最优的算法!
大O渐近表示法:用常数1代替所有加法常数
最高阶对结果影响最大,关注最高阶。 |
最高阶的常数省略掉 |
所有加法常数都用数字1替代 |
例子:N = 2n^2 + n + 10。直接看出来时间复杂度是N^2,我想读者们应该都get到了。给出我们基本操作的次数,我们就可以很轻松的用大O表示法表示出时间复杂度,这个并不难,重要的一点是算出或者看出算法的执行次数。
1.4计算基本操作的次数
三种情况:
char* serch_ch(const char* str, char ch)
{
//从字符数组的头开始往后找
//在一个字符数组里查找一个想要查找的字符
while(*str != '\0')
{
if(*str == ch)
return str;
str++;
}
return NULL;
}
一个字符数组,长度为N,在字符数组里查找一个字符ch,到底需要查找多少次(基本执行次数)?
- 最好的情况:第一个字符就是我们要查找的,只进行一次,时间复杂度为O(1)
- 最差的情况:遍历完整个数组,可能在最后有这个字符,也可能没有,总之对长度为n的数组都找了一遍(基本操作次数:查找了n次),时间复杂度为O(N)。
- 平均的情况:要查找的数是随机的,数组里放的字符也是随机的,从概论上来讲,我们会在接近中间的位置找到,执行n/2次,时间复杂度还是O(N)。
当一个算法存在这三种情况的时候,我们看最坏的情况,做最坏的打算,为我们打底。如果读者看过修仙类的小说的话,我们一般会看到男主很厉害但又想着可能出现的最坏的情况,换句话说就是想的周全。
冒泡排序的基本操作次数:
冒泡排序就是将一组数字给排成升序。说到冒泡,大家有没有想到生活中煮水器在煮水的时候,那个水泡从底部升起来,从小泡变成大泡。抽象理解小泡是小的数字,升起来的大泡就是大的数字。因为是计算机的知识,我们引进来让大家提个兴趣,不是来讲物理滴(doge)。
一组数字 8 7 6 5 4 3 2 1。我们想排成1 2 3 4 5 6 7 8的升序序列。我们有思路、分析分析思路就可以了,可以不用敲代码就可以算出这个思路的时间复杂度。
因为这是一个长度为N的数字序列,将第一个数冒泡到最后,需要冒泡N次,这是第一趟冒泡。第一趟冒泡的结果就是,把一个数字放到了它该放在的位置上,接下里就只有N-1个数字需要排序了了。第二趟冒泡N-1次、第三趟冒泡N-2次、.......、第N趟冒泡1次。总共冒泡的次数N + (N - 1) + (N - 2) +.......+ 2 + 1是一个等差数列求和,首项加尾项乘项数除2--- (N+1)*N/2就是冒泡排序的基本操作次数。 时间复杂度O(N^2)。
二分查找的基本操作次数:
二分查找是在一个有序的数组里查找想要的数字。现在有一个数组的元素排列是 1 2 3 4 5 6 7 8 9。
如果不是,假设我们找的是9这个数字,这时候,因为这个数组是有序的,我们中间元素比要查找的小,中间下标左边的就不用再找了,left左下标变成指向mid+1的元素(就相当于少了一半的数据),再和right重新生成中间下标:
当left<=right的时候,就有元素可以找,当left大于了right,就找不到了,最后一次查找是在left = right的时候,此时无论找不找的到、都相当于把长度为N的有序数组给查遍了。查了几次呢?由于我们每次查,数组的元素少一半,对于数组长度N,我们得出2的x次方等于N。这里可以这样想,假设这个数组有8个元素,我用二分查找,每次查找都会去掉一半的数据,请问查几次可以查完,2的3次方等于8,我们知道查3次就够了。
所以X是基本操作此时 X = log以2为底N的对数。时间复杂度是O(logN)。
logN的意思是以2为底,N的对数,由于底数电脑上不太支持写出来,所以我们约定这样写。当然有些数还这样写lgN,实际上这个不合理的程度要大一点,我们尽量写成logN。
练习:递归求阶乘的时间复杂度:
long long Fac(size_t N)
{
return N < 2 ? N : Fac(N-1) * N;
}
这里使用递归实现求N的阶乘,这里的时间复杂度是多少呢?请看下面的画图:
N为几,Fac()函数就被调用几次,每一次函数都进行三次基本操作(判断、得到值、返回)。也就是说,Fac函数里的算术复杂度是O(1),而调用这个函数的时间复杂度是O(N)。也就是执行了3N次基本操作,所以时间复杂度是O(N)。
用for循环来理解也很容易,读者可以类比一下:
int main()
{
int count = 0;
int N = 0;
scanf("%d", &N);
for(i = 0; i < N; i++)//循环N次
{
for(j = 0; j < 3; j++)//i的每一个值都进行三次
{
count++;//3N次
}
}
return 0;
}
这里的循环N次就相当于递归调用是用了N次,每次里面都有三次基本操作,就是3N。
扩展话题:斐波那契数列的时间复杂度的计算
2.常见的时间复杂度及其优劣比较
常见的时间复杂度有:O(N^2)、O(N)、O(logN)、O(1)这几种
O(1)是最牛的时间复杂度,没有再厉害的了。O(logN)也是一个非常好的时间复杂度,它和O(N)是有个量级在的,我们来计算一下:
比如遍历数组查找一个元素,恰巧要找的数组元素在末尾,当N = 1000的时候,O(N)的算法需要执行1000次基本操作、O(logN)的算法只需要10次就够了,因为2的10次方是1024。
当N = 1000000(百万),O(N)需要百万次,O(logN)只需要20次,因为1024*1024我们简单看成1000*1000就够了。
当N = 1000000000(十亿),O(logN)仅需要30次就足亦!
讲到这里,我们就把时间复杂度的知识内容讲完啦。空间复杂度和算法的一些特性博主可能会另外出一篇,内容不多,而且空间复杂度和时间复杂度一样用大O表示法表示,也一样是估算,空间复杂度算的是程序额外占用空间的数量,比如创建一个int a;变量,占的数量就是1。
结语:希望读者读完能有所收获!对数据结构有进一步的认识!✔
读者对本文不理解的地方,或是发现文章内容上有误等,请在下方评论留言告诉博主哟~,也可以对博主提出一些文章改进的建议,感激不尽!最后的最后!
❤求点赞,求关注,你的点赞是我更新的动力,一起进步吧。