算法之时间复杂度---数据结构

简介: 算法之时间复杂度---数据结构

前言:


 在计算机中,如何判断一个算法的好与坏,我们使用时间复杂度来评判。时间复杂度不是一个精确的值,而是大致描绘算法的时间效率。大家目前写的代码对于计算机来说都是可以很快就跑完的,因为都是比较简单的常数阶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代替所有加法常数


大O表示法

最高阶对结果影响最大,关注最高阶。

最高阶的常数省略掉

所有加法常数都用数字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。


结语:希望读者读完能有所收获!对数据结构有进一步的认识!✔


 读者对本文不理解的地方,或是发现文章内容上有误等,请在下方评论留言告诉博主哟~,也可以对博主提出一些文章改进的建议,感激不尽!最后的最后!


 ❤求点赞,求关注,你的点赞是我更新的动力,一起进步吧。

相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
64 1
|
2月前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
152 4
|
5天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
23 2
|
21天前
|
存储 运维 监控
探索局域网电脑监控软件:Python算法与数据结构的巧妙结合
在数字化时代,局域网电脑监控软件成为企业管理和IT运维的重要工具,确保数据安全和网络稳定。本文探讨其背后的关键技术——Python中的算法与数据结构,如字典用于高效存储设备信息,以及数据收集、异常检测和聚合算法提升监控效率。通过Python代码示例,展示了如何实现基本监控功能,帮助读者理解其工作原理并激发技术兴趣。
54 20
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
124 23
|
2月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
69 20
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
74 1
|
2月前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
72 0