C语言之数据结构初级<时间和空间复杂度>

 本文已收录至《数据结构》专栏,欢迎大家 👍点赞 > 👆收藏 + 🫰关注


目录

🪧前言

✏️正文

📝算法衡量

📝时间复杂度

📝空间复杂度

📃总结 


🪧前言

在开始进入本节的介绍之前,我们需要了解数据结构是什么,我们都知道:“程序 = 数据结构+ 算法”。

那么什么是数据结构?数据结构是计算机存储和组织数据的方式,数据元素之间存在一种或多种特定关系的数据元素的集合。

什么是算法?我们解一道题所用到的过程和写下的解法就是算法,算法是用来实现某种功能的一块代码,专业一点说就是:程序取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

那么可能有朋友要问,我们为什么要学习数据结构?数据结构和算法是计算机领域非常重要且核心的课程,学好数据结构和算法思维可以为我们后面的高级编程打下坚固的基础。

数据结构和算法的学习就从现在开始,今天我们首先开始了解时间复杂度和空间复杂度在程序中的概念。


✏️正文

我们所写的代码是为了实现某一个功能,那么这块代码就是面向这个功能所研究出来的算法,对于同一个功能的实现有许多种不同的算法,我们取其中的最优解,那么我们以什么标准来评判算法的好坏呢?

📝算法衡量

我们先来分析一段代码:斐波那契数列

long long Fib(int N)
{ 
    if(N < 3) 
        return 1; 

    return Fib(N-1) + Fib(N-2); 
}

这段代码我们采用递归的形式求斐波那契数列,这样代码非常简洁,但是这样写好吗?

我们思考一下这段代码的运行方式:假设N=10

 我们列出一部分代码图,可以发现,递归计算斐波那契数列中产生了大量的重复计算,这些重复计算是不必要的,而且中递归下会占用大量的空间,而且因为存在重复的计算,程序所运行的时间也会非常长,当N足够大时还会造成栈溢出。对于这样的代码,使用递归显然是不会的,不妨改为迭代,那么再合适不过了。

long long FiB(int N)
{
    if (N < 3)
        return 1;

    int n = N;
    int a = 1;
    int b = 1;
    int c = 0;
    while (n >= 3)
    {
        c = b;
        b = a;
        a = b + c;
        n = n - 1;
    }
    return a;
}

通过这个例子可以说明:

算法在编写成可执行程序后,运行时需要耗费时间资源和内存资源 。因此衡量一个算法的好坏,一般 是从时间和空间两个维度来衡量的,即时间复杂度空间复杂度

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。

时间复杂度和空间复杂度我们使用大O的渐进表示法

大O符号:是用于描述函数渐进行为的数学符号。 推导大O阶方法:

1、用常数1取代运行时间中的所有加法常数。

2、在修改后的运行次数函数中,只保留最高阶项。

3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。 


📝时间复杂度

在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在计算机上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度

示例代码:

void fun(int N)
{
    int a = 0;

    //示例1
    a = 100;
    while (a--) {}

    //示例2
    a = N;
    while (a--) {}

    //示例3
    for (a = 0; a < N; ++a)
    {
        for (int i = 0; i < N; i++) {}
    }

}

我们分析上面的代码时间复杂度是多少,首先执行了100次while循环,接着执行力N次while循环,最后执行了N次for循环,其中每次for循环又执行了N次for循环,我们综合上面的分析,得出此函数的时间复杂度为F(N) = 100 + N + N^2。但在实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法

我们前面介绍过,常数项的次数我们记为1,但次函数中最高次并非是常数项,而是N^2,相当于N^2来说,100次和N次的变化对该函数的时间复杂度影响太小了,所以可以忽略不计,那么该函数的时间复杂度就为O(N^2)。

我们通过几个示例深入了解时间复杂度

  

示例1

void fun1(int M,int N)
{
     for (int k = 0; k < M; ++ k) {}
     
     for (int k = 0; k < N ; ++ k) {}
}

在示例1函数代码中,先执行了M次for循环,然后执行力N次for循环,所以fun1的时间复杂度为O(M+N)。


示例2

void fun2()
{
    int a = 10;
    while(a--) {}
}

在示例2函数代码中,执行了10次while循环,为常数项,所以fun2的时间复杂度为O(1)。


示例3

void fun3(int M)
{
    int m=M;
    while(m>1)
    {
        m /=2;
    }
]

在示例3函数代码中,while每循环一次,m就除以2,直到m=1,那么fun3的时间复杂度就呈

对数减小,所以fun3的时间复杂度为O(log M)。


示例4

int fun4(int a[N],int x)
{
    for(int i=0;i<N;++i)
    {
        if(a[i]==x)
          return i;
    }
}

在示例4函数代码中,我们有一个N个元素的数组,要查找x元素返回其下标,那么就会有三种情况,首先第一个元素a[0]就是目标元素,那么此时时间复杂度就为N(1),其次在数组的中间找到了该元素,那就是O(N/2),最后就是最坏的情况,在数组的最后一个,那么时间复杂度就是O(N)。我们分析这三种情况:

1.最好的情况:O(1)

2.平均情况:O(N/2)

3.最坏的情况:O(N)

我们一般考虑事情,都不是考虑事情最好的情况,而是做最坏的打算这样,哪怕情况再坏也不会超过我们预料的最坏的情况。所以fun4的时间复杂度为O(N)。

   

这个例子要说明的是

算法的时间复杂度存在最好、平均和最坏情况:

最坏情况:任意输入规模的最大运行次数(上界)

平均情况:任意输入规模的期望运行次数

最好情况:任意输入规模的最小运行次数(下界)

在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。


📝空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes(字节)的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。 空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

通过示例对空间复杂度的理解进行深入分析:

示例1

void sun1()
{
    int a=1;
    int b=2;
    int c=a+b;
    for(int i=0;i<=100;++i)
        for(int k=0;k<=10;++k)
            printf("%d\n",c);
}

在示例1中,我们有5个变量,分别是a,b,c,i,k,但这仍然是常数个空间,所以sun1函数的空间复杂度为O(1)。


示例2

void sun2(int N)
{
    int* arr=(int*)malloc(sizeof(int)*N);
}

示例2中,对一个指针开辟N个int类型的空间,所使用的空间复杂度为O(N)。


示例3

long long sun3(int n)
{
    if(n==0)
        return 0
    
    int a[10]={0};

    return sun3(n-1);
]

示例3中,使用递归创建N次数组,数组中的元素个数为常数个,所以个函数的空间复杂度为O(1),但是递归了n次,每次都创建常数个空间的数组,则是O(1*N)就是O(N)。


示例4

long long sun4(int M,int N)
{
    if(M==0)
        return 0;
    
    char* n=(char*)malloc(sizeof(char)*N)
    
    return sun4(N-1);
]

示例4中,使用递归创建N次N个字符型数组空间,递归了N次,每次创建的空间大小为N,所以sun4的空间复杂度为O(N^2),这已经是一个非常大的空间了,如果控制不佳则很容易导致栈溢出。

常见的复杂度:

1048576O(1)常数阶
2N+1024O(N)线性阶
N^2+NO(N^2)平方阶
12logNO(logN)对数阶
NlogN+NO(NlogN)NlogN阶
N^3+2^NO(N^3)立方阶
2^N+NO(2^N)指数阶

📃总结 

本次我们介绍了时间复杂度和空间复杂度,从这两个方面对算法进行分析,可以让我们进一步优化代码,是我们的算法更加高效。而且这也为我们后面开始正式学习数据结构打下了基础,因为我们后面的数据结构的学习会经常涉及算法的优化,从而提高整体执行效率。

本次时空复杂度的知识分享就暂时先到这里啦,喜欢的读者可以多多点赞收藏和关注。

如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!

🌟其他专栏文章阅读推荐🌟

C语言入门<操作符>_ARMCSKGT的博客-CSDN博客

 C语言入门<分支语句>_ARMCSKGT的博客-CSDN博客

 C语言入门<循环语句>_ARMCSKGT的博客-CSDN博客

🌹欢迎读者多多浏览多多支持!🌹

  • 13
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ARMCSKGT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值