数据结构2---------->时间复杂度

一、算法的效率:

1.如何正确的衡量一个算法的好坏呢?

请看下面的斐波拉契数列:

我先简单介绍一下斐波拉契数列:

斐波那契数列(黄金分割数列),它是由数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……即这个数列从第三项开始,每一项都等于前两项之和

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

上面是用C语言写的一个斐波那契数列求解。可以看到,这个实现的方式是递归,代码也比较简结。那么,现在有一个问题需要我们去思考,代码简洁就一定好吗?那么我们应该如何衡量一个算法的好与坏呢?

二、时间复杂度

1.衡量算法的好坏

算法在编写好可执行程序后,运行时需要消耗一定的时间资源和空间资源。因此,衡量算法的好坏,一般可以从时间和空间这两个维度去进行衡量。学过数据结构的老铁就明白了,今天我们要讲的就是时间复杂度和空间复杂度。这一篇博客我想先把时间复杂度给大家先说清楚,后面的空间复杂度会在下一篇博客进行讲解。

2.时间复杂度的含义

时间复杂度主要衡量的是一个算法运行的快慢。它的含义是:在计算机科学中,算法的时间复杂度是一个函数(这里的函数是指数学里的函数式,而非我们学的编程语言里的函数),它定量描述了一个算法的运行时间。算法的运行时间在理论上来说,它是算不出来的,因为只有在机器上跑程序,才能知道。

3.补充知识

那么问题来了,2G的i3 CPU和现在的16G的i9CPU都使用冒泡排序跑10w个数据运行的时间肯定是不一样的。但是,有一个刷题网站,叫Leetcode,它在运行的时候都是认定为同一台机器,但是也和当前电脑的负载有关。所以,有时候你会发现为什么同一个题同一个算法在不同时间的运行效率击败人数不一样。有时候,我们会在这里看到0ms,它并不是说不耗时间,而是指消耗时间小于1ms。这是为什么呢?因为,在这里的时间是用整数来计算的。

4.++count

总而言之,算法中的基本操作的执行次数就是算法的时间复杂度。下面有一段用C语言写的代码,请大家先计算一下里面的++count语句执行了多少次。(通常情况下,++count的运行速度是大于count++的)

void Function(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;
}

答案是F(N)=N^2+2*N+10(F(N)是一个带未知数的函数式)

解析:第一个for循环++count运行了N^2次,第二个for循环++count运行了2*N次,第三个循环++count运行了10次。(这里是最基本的循环次数判断,相信优秀的老铁们已经知道了)

现在我来简单就算一下,N在10、100、1000时候的取值。

当N=10时,F(N)=130;

当N=100时,F(N)=10210;

当N=1000时,F(N)=1002010;

如果学过高等数学里的极限,那么大家会发现当N趋近于无穷大的时候,2N+10相对于N^2是非常小的。那么就可以近似将F(N)表示为F(N)=N^2。在实际中,我们计算时间复杂度的时候,我们其实不一定要算精确的执行次数,而只需要大概的执行次数就好。(这里大家能看懂的话就太好啦,下面要讲的大O的渐进表示法就和这个有一点点关系)

三、大O的渐进表示法

为了更好地表示这种执行次数,我们需要用到大O的渐进表示法。

1.大O符号的含义:

用于描述函数的渐进行为的数学符号。渐进表示方法是一种估算的方法,算的是一个大概,算的是量级,划分的是档次。就好比我们可以将有钱人分为4种,超级富豪、富豪、中产阶级、普通人。像一个拥有1000亿资产的大佬和一个拥有2000亿资产的大佬他们都是超级富豪,我们关注的点是他们是富豪,而非他们的资产谁多谁少。

常见的大O的划分档次有:O(1)、O(N)、O(N^2)、O(N^3)、O(NlogN)、O(logN)

2.使用大O的渐进表示方法时,我们应该注意一下三点:

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

这种情况通常是函数项只有常数的时候。

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

我们看刚刚的那段用C语言写的一段代码,为了方便大家观看,我这里弄了过来。

void Function(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;
}

刚刚我们也计算过,++count的运行次数是F(N)=N^2+2*N+10 。如果用大O的渐进表示法,上面代码的时间复杂度就是O(N^2)

在这种情况下,我们看看F(N)

当N=10时,F(N)=100;

当N=100时,F(N)=10000;

当N=1000时,F(N)=1000000;

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

c.如果最高阶存在且不是1,那么去掉与这个项目相乘的常数,得到的结果就是大O阶渐进法表示的结果。
void Function2(int N)
{
    int count=0;
    for(int elcmacom=0;elcmacom<N;++elcmacom)
    {
        for(int elcmacom_1=0;elcmacom_1<2N;++elcmacom_1)
        {
            ++count;
        }
    }
}

大家看看,简单看完这个Function2函数后,会发现它的执行次数是F(N)=2N^2。大O阶渐进法来表示这里的时间复杂度,就是O(N^2)。

四、大O渐进表示法的例子

下面我们来看几段代码,希望它们有助于你更好理解大O阶渐进法。

1.函数Function3:O(M+N)

void Function3(int M,int N)
{
    int count=0;
    for(int elcmacom=0;elcmacom<M;++elcmacom)
    {
            ++count;
    }
    for(int elcmacom_1=0;elcmacom_1<N;++elcmacom_1)
    {
            ++count;
    }
}

细心的老铁可能发现了,你这里写错了,应该写成O(M)或者O(N)嘛!但是,我想说的是,凡事我们都应该考虑周到一点。如果M和N的大小关系没给,还真得写成O(M+N)。那么,认为是O(M)的原因在于:M远大于N;认为是O(N)的原因在于:N远大于M

2.函数Function4:O(1)

void Function4(void)
{
    int count=0;
    for(int elcmacom=0;elcmacom<100;++elcmacom)
    {
            ++count;
    }
}

O(1)的原因是:F(N)=100,运行次数是一个常数,那么它的时间复杂度当然是O(1)啦。

需要注意的一点是:O(1)并不是指运行次数为1次,而是代表常数次。

可能大家会有这样的想法:如果把循环判断条件改为:elcmacom<10000,难道还是O(1)吗?答案是,当然是O(1)。因为CPU的运行速度是很快的!当然没必要去钻牛角尖哈,把循环判断条件改为elcmacom<100000000000000,这样搞首先我想问问,C语言中的int整型表示数的范围是多少?再决定要不要这样去做。

3.对于大O渐进表示法的补充:

a.算法情况介绍:

有些算法存在最好、最坏、平均的情况:

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

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

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

比如,我们要在一个数组长为M的数组里寻找一个我们要找的数。

那么,最好的情况是:1次;最坏的情况是:M次;平均的次数;M/2

b.实际情况的使用

在实际情况中,我们通常关注的是算法的最坏运行情况。(这里有点像心理学上的预期管理,要最坏的情况,做保守的预测。比如,我们上完课后去和自己的好朋友去下馆子,一起约好到那里去,中途我们可能会遇到堵车、老师拖堂等等情况,我们需要把这些情况考虑进去然后再约定一个好的时间,这样就不会造成让对方等你的情况了,而且更多时候会有空余时间,如果在冬天,在这种空余时间下可以帮ta买一杯奶茶,这样更能增强好感)

3.Function5:O(N)

抱歉,我讲偏了。回到正题,大家学习C语言的时候是否还记得strchr这个函数,用得可能不多。它的功能是在一个字符串里找字符,下面我简单写一下它的实现过程。

const char* strchr(const char* str,int character)
{
    while(*str)
    {
        if(*str==character)
        {
            return str;
        }
        else
        {
            ++str;
        }
    }
}

我们知道,在运气好的时候,我们可以一次或者常数次就找到这个字符,这时候时间复杂度是O(1),运气一般的时候,我们可能要找N/2次,运气很差的时候我们可能需要找N次。为了保险起见,我们把这个算法的时间复杂度认为是O(N)。

4.Function6:O(N^2)

void Function6(int* a,int n)//BubbleSort冒泡排序
{
    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;
        }
    }
}

 冒泡排序的思想:前一个数大于后一个数进行交换。

那么它的次数是:

第一次:N-1

第二次:N-2

......

最后一次:1

按照高中学的等差数列求和,我们可以得到:((首项+尾项)/2)

F(N)=(N-1+1)*(N-1)/2

所以,就有了O(N^2)

5.Function7:O(logN)

int Function7(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-1;
        else 
            return mid;
    }
    return -1;
}
a.二分查找什么情况下最坏?

1.区间只有一个值

2.找不到

b.二分查找的本质:

二分查找的本质是缩小区间。(最开始是N个数,找一次缩小一半)

那么找了多少次呢?除了多少个2,就找了多少次。

假设我们找了x次

N=2^x

解得,x=logN(以2为底,N的对数)

c.关于logN的写法:

说到这个logN,大家都知道高中不存在这种写法。但是在时间复杂度里,它是表示以2为底N的对数。如果是其他的底数,那么正常写就好了。

有些书籍也将logN写成lgN,也是表示为以2为底N的对数。(这个注意一下就好,不建议这样写)

五、时间复杂度的运用

下面我们来看Leetcode上的一道题

链接:https://leetcode-cn.com/problems/missing-number-lcci/

说这个题的目的在于,我们要学会用时间复杂度去分析题目,找到最优解。

思路1:

1.先排序,后查找

如果不等于前一个加1,那么后一个就是消失的数字。

使用的方法:

a.冒泡排序:时间复杂度为O(N^2)

b.qsort-------->本质是快排,时间复杂度为O(NlogN)

思路2:

2.异或法(异或的意思是相同为1,相异为0)

x先和0~n中的所有值进行异或,再和数组中的所有的值进行异或。

F(N)=N+1+N=2N+1

时间复杂度为:O(N)

思路3:

3.计算0~n的累加和(为了更快的计算,我们可以直接用等差数列的公式求解),再用这个累加和依次去减数组中的值

时间复杂度:O(N)

比较以上3种方法,我们会发现3>2>1,使用方法3是最优的。

int missingNumber(int* nums, int numsSize){
    int x = numsSize;
    int i=0;
    int sum=x*(x+1)/2;
    for(i=0;i<numsSize;i++)
    {
        sum-=nums[i];
    }
    return sum;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值