C语言版本数据结构02:复杂度

23 篇文章 0 订阅
19 篇文章 0 订阅

今天开始我们就进入到数据结构的第一个知识板块,相信大家在学校学习或者在一些数据结构的书上看到的第一章肯定是关于复杂度的讲解,可见从现在开始我们写代码就不能一味着讲究“解决问题”了,我们还要考虑效率的问题,那么什么是效率呢?可以理解为该方法对于一个问题的解决消耗,如果在准确的前提下消耗的资源、时间更短,说明该方法的效率更高。移到编程上来说这个方法就叫做算法,那么如何形象的理解算法的效率呢?

算法效率

如何衡量一个算法的效率呢?

我们肯定编程里有一种方法叫做递归,就是一种将问题大化小,小化了的方法,那如果我们用递归来实现斐波那契数列的求和它的效率的多少呢?

long long Fib(int N)
{
     if(N < 3)
         return 1;
 
     return Fib(N-1) + Fib(N-2);
}

这样几句简单的代码相信大家肯定以为它的效率很高,但是殊不知简洁的代码效率一定高吗?我们先来看一下关于复杂度的定义:

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

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);
}

我们来一起算一下,前两个for循环N一共执行了N^2次,所以++count一共执行了N^2次,第二个for循环++count执行了2*N次,第三个while循环++count又执行了10次,所以++count一共执行了N^2+2*N+10次,那这个式子我们就可以暂时理解为是上面Func1的时间复杂度,那么毕竟N是随机数,那么随着N的变化,这个式子会发生什么变换呢?

我们可以看到随着N不断变大,F(N)变大的趋势在不断变大,那么究竟是什么影响这个式子变化如此大的呢?我们看到在这个式子里最高阶是2,2N和10的阶数都是1,我们知道指数的增长是爆炸式的,所以N^2也就是这个式子变化如此巨大的原因,但是在编程的世界里又不能和数学完全等同,因为我们要考虑到每台计算机的硬件软件不同,每个人编程环境也会有不同,所以我们要做一定的取舍,把一些并不会对算法结果起到决定作用的因素剔除掉,只留下最作用最大的,经过上面的分析所以显而易见我们留下的就是N^2,那么Func1的时间复杂度正确来说就是N^2,在编程里我们计算时间复杂度用的是大O渐进表示法,所以Func1的时间复杂度准确表示方式:O(N^2)。

2.大O的渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。

推导大O阶方法: 1、用常数1取代运行时间中的所有加法常数。 2、在修改后的运行次数函数中,只保留最高阶项。 3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

另外我们也发现当N=1或者N=2时好像Func1的运算时间好像都不太低,差不了多少,如果用计算机来计算的话也就是几毫秒的事,但是编程里又规定了,在计算时间复杂度的时候不能只考虑最优情况,不能把你需要解决的问题的例子拿来测试你的算法,即不能拿一个准确的数字来计算,这样算出来的时间复杂度一定是错的,算法的时间复杂度一定考虑的都是最坏的情况,如果最坏情况下你写的算法依旧可以运行的很快,那无论用什么例子来测你的算法效率都不会低,这才是好算法。

3.常见时间复杂度举例

// 计算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);
}

经过上面学习的大O渐进表示法相信大家能很快的算出Func2的时间复杂度,就是2N+10,我们又知道常数项作为影响算法结果很小的因素可以去掉,所以10和系数2都可以去掉,而N作为影响算法计算结果最大的因素便不可忽略,所以Func2的时间复杂度就是: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);
}

Func3看似有两个未知数,但是我们知道编译器在执行的时候是依次执行,执行完一个for再执行另一个for,所以Func3的时间复杂度是两个O(N),算下来还是O(N)。

// 计算Func4的时间复杂度?
void Func4(int N)
{
     int count = 0;
     for (int k = 0; k < 100; ++ k)
     {
         ++count;
     }
     printf("%d\n", count);
}

我们看到Func4里将N换成了100,我们又知道平常常数是会被忽略的,那么只有一个常数时该如何做呢?由于100是一个可知数,我们在计算可知数的时间复杂度时通常都用O(1)来表示,这就代表此算法的时间复杂度非常低,只有常数次。

//计算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;
 }

冒泡排序相信大家已经不陌生了,但是大家在C语言的学习中肯定没有考虑过冒泡排序的效率,我们知道第一个fo的end如果考虑最坏情况要计算n次,第二个for循环中的i根据end的变化来变化,end会不断变小,i的计算也会不断减小,那么冒泡排序的时间复杂度其实就是一个1~end的递增数列的前n项和,再用大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;
}

二分查找想必是每个编程初学者都要学习的算法,二分查找以高效著称,那么有多高效呢?我们知道二分查找计算一次折一半,那计算N次查找的次数也就是log2N,由于log不可省略,所以用大O表示法还是O(log2N)。

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
     if(0 == N)
         return 1;
 
     return Fac(N-1)*N;
}

递归算法如果要计算复杂度的话就要看其递归了几次,我们知道这个算法的结束条件就是当N==0时,所以从N到0,最坏情况下也递归了N次,所以Fac的时间复杂度是O(N)。

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
     if(N < 3)
         return 1;
 
     return Fib(N-1) + Fib(N-2);
}

又回到了我们的斐波那契数列求和的递归算法,我们可以看到该算法递归是两条分支向下递归,如同二叉树一般,第一层只有N(2^0),第二层有N-1+N-2(2^1),第三层有N-2+N-3 N-3+N-4(2^2).....一共执行了2^N次,所以时间复杂度为O(2^N)。

3.空间复杂度

学完时间复杂度后,相信大家猜都能才出来空间复杂度是什么,就是该算法所用内存空间的大小,我们还是照例先来看空间复杂度的概念:

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。

空间复杂度不是程序占用了多少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;
     }
}

算完冒泡排序的时间复杂度,我们来算一下它的空间复杂度,由于每次进入循环都要创建一个exchange和调用一次Swap,等到后面我会专门开一篇来讲调用函数时的堆栈,调用函数也是会有堆栈的开辟的,所以冒泡排序的空间复杂度也是O(1)。

// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
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;
}

我们可以看到fibArray开辟了n个动态内存,所以很显然它的空间复杂度是O(N)。

//计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
     if(N == 0)
         return 1;
 
     return Fac(N-1)*N;
}

递归算法由于每次都需要向下进行展开,所以再不断地使用空间,调用了N次,所以也就开辟了N个空间,所以空间复杂度也就是O(N)。

4. 常见复杂度对比

我们知道了时间复杂度和空间复杂度的计算原则了,那么这些复杂度有没有排名呢?究竟要什么养的复杂度才算是低,什么算是高?

一般算法常见的复杂度如下:

我们可以看到当N的阶数越高花费的时间和空间就越高,指数情况下会直线向上发展, 所以我们再写算法时要注意不要写成指数阶的复杂度。

好了,关于复杂度的讲解就告一段落,大家可以去做一些有复杂度要求的题目,可能会想必之前做题会很别扭,因为不只要求解决问题还要高效,我给大家留两个题的链接,大家可以去试试。

消失的数字OJ链接:https://leetcode-cn.com/problems/missing-number-lcci/

旋转数组OJ链接:https://leetcode-cn.com/problems/rotate-array/

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值