算法复杂度详解( 超详细!)

前言:

  今天,小编正式开始学习了数据结构的正式内容,学习了算法复杂度相关的内容,为了加强对这个的了解,于是诞生了这一篇文章,下面废话不多说,开始进入复杂度的详解!

目录:

1.算法的效率

1.1.旋转数组习题讲解

1.2.复杂度的概念与分类

2.时间复杂度

2.1.函数T(N)

2.2.大O的渐进表示法

2.3.大O相关练习题的详解

3.空间复杂度

3.1.空间复杂度的计算方法

3.2.相关例题的详解

正文:

1.算法的效率

1.1.旋转数组习题讲解

  首先,读者朋友们肯定会很好奇旋转数组是什么东西,其实这是一个算法题所涉及到的内容,下面小编线给大家看一下这个题目:

    对于此题的详细内容上图已经给大家展示出来了,此题出自力扣,各位读者朋友们如果想要刷题的话,小编很推荐这个平台的题目,它的算法题还是有一定的难度的,学有余力的读者朋友可以看一下这个平台,小编这里先放上这个平台的链接:力扣 (LeetCode) 全球极客挚爱的技术成长平台

  废话不多说了,下面开始这个题的讲解:首先,这个题是指右轮数组中的数,其实就是把数组后k个元素放到开头,下面是小编画的图解,各位读者可以观看一下:

  所以我们可以保存数组最后一个数,通过while循环的方式,每此先把最后一个元素保存下来,然后在通过一次for循环来把第一个位置的数空出来(很像顺序表的头插操作),然后循环玩一次后,k减去一次,直到k变为0为止,这里便可以做到轮转操作,光说不展示代码等于白说,下面小编展示一下这个思路的代码展示:

void rotate(int* nums, int numsSize, int k) {
    int i = 0,end = 0;
    while(k--)
    {
        end = nums[numsSize - 1];
        for(i = numsSize - 1 ; i > 0 ; i--)
        {
            nums[i] = nums[i - 1];
        }
        nums[0] = end;    //三天没敲代码了差点没忘干净
    }
}

  可能许多读者朋友很好奇我为什么没有int main函数,其实这就是力扣题的特点,力扣的算法题,主要是让你补充完这个函数的,下面给大家展示一下做题页:

  可以这么想:力扣已经给你提供了int main函数,你接下来只需要把函数填充完,后续不用管,下面先来调试一下,看一下这个函数小编是否写对了:

  可以看出小编在测试的时候并没有错误,下面我们再试一下提交代码,看一下提交完代码后的结果是什么:

  可以看出,小编这个代码并没有通过,力扣也给出了这个题目的评判结果:这个题超出了时间限制,意思就是小编这个代码的运行时间过长,过于 复杂,导致这个题目出现了问题,所以说,小编这个代码并没有错,错的是运行时间过长,这个代码并没有符合这个题目运行时间要求,这里就涉及到了一个船新的知识点:时间复杂度出现的问题,下面开始进行复杂度的介绍!

1.2.复杂度的概念和分类

  算法在编写成可执行程序后,运⾏时需要耗费时间资源和空间(内存)资源 。因此衡量⼀个算法的
好坏,⼀般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
  时间复杂度 主要是用来衡量一个算法的运行快慢,而 空间复杂度 主要是衡量一个算法运行所需要的额外空间,在很早的时候,由于计算机发展的还不算完善,运行内存是很低的,从而导致程序员们在编写程序的时候,很看重空间复杂度,不过随着科技的发展,计算机也在进行飞一般的提升,现在,空间复杂度已经没有那么重要了,但是还是有一定作用的,现在很多企业的招聘依旧喜欢考有关空间复杂度的题目。
  对于上面题目的错误,超出时间限制,涉及到了时间,所以很明显是时间复杂度出现问题了,那么到底出现了什么问题呢?不要着急,下面跟随小编的步伐,开始今天的大头——时间复杂度的介绍:

2.时间复杂度

2.1.函数T(N)

  在计算机科学中,时间复杂度其实是用一个函数时T(N)来进行表示的,时间复杂度计算的是时间运算的时间效率,由于函数的运行时间受到编译环境,设备,编译器的影响,所以我们通常是不计算代码的运行时间的,并且运行时间只能写好程序以后再去调试,不能够去推算!

  那么,函数T(N)到底是什么呢?其实函数式T(N)计算的是函数的执行次数,下面,小编通过一组代码,来给大家对于这个T(N)的初步的理解:

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

  各位读者朋友先来自己计算一下这个代码运行次数(其实就是循环次数),下面小编来给大家计算一下这个代码的运行次数:

  首先,我们从第一个循环开始看,这个是个循环的嵌套,所以一共进行了N * N次,也就是N ^ 2次,之后我们在看下一个循环,很显然,循环了2 * N次,所以一共有2N次,在看下一个循环,根据题目的意思,所以这里其实循环了10次,所以一共有 N ^ 2 + 2 * N + 10次,可能由于很多读者朋友会疑惑为什么在进行创建变量的时候不算次数,因为这个次数太小,所以可以忽略掉,小编下面就直接忽略这个了!并且由于计算机计算的高效率,我们可以仅仅保留其中的最大项,也就是这里其实一共循环了N ^ 2次!

  所以通过这个题我们可以看出,平时我们在计算时间复杂度的时候,计算的也不是精确值,而是粗略估计(拿我们只保留最高阶为例),在上面小编对于运行次数的计算的时候,有些读者朋友可能已经注意了小编对于低阶数的省略,不重要性,下面,小编将会讲述本文的大头,我们对于运行次数(循环次数)的计算,复杂度可以通过大O的渐进表示法来表示!

2.2.大O的渐进表示法

  大O符号:是用于描述函数渐进行为的数学符号

  下面小编先来展示一下大O阶的规则(这里小编偷懒直接用到官方描述):

  上面的图片便就是来对于大O如何进行使用的描述,其实这部分的内容,考察的是各位读者朋友们的数学知识,所以各位读者朋友一定要好好的去学习数学知识,下面为了巩固大家对于这个大O渐进表示法的理解,这里小编将给出几个例子,来帮助大家去进行了解!

2.3.大O相关习题的讲解:

  2.3.1.代码1:

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

  老规矩,各位读者先自行做一下,小编稍后给大家进行讲解,下面小编将会给出这个题的详解:

  首先,我们先看第一个循环,一共循环了2N次,然后在看下一个循环,一共循环了10次,所以一共循环了2N + 10次,我们根据大O的第一个规则,所以我们要保留最高项,所以是2N,在看下一个规则,所以我们可以把最高项的系数去掉,所以最后得出来的结果是O(N)!对于这种复杂度的题,读者朋友们一定要好好的理解到,这对于我们进行一些算法题的练习中有很大的帮助!废话不多说,进入下一个题目的训练:

2.3.2.代码2

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

  老规矩,各位读者朋友先自行阅读一下,下面小编给出这个复杂度的解释:

  首先,我们先看第一个循环,一共循环了M次;在看下一个循环,一共循环了N次,所以这个代码一共运行了M + N次,由于我们并不知道M,N的大小关系(这里M,N都是变量,不要只认为大N是变量!),所以大O使用法我们都用不了,所以最后的结果应该是0(M + N)次。读者朋友们可不要在这里犯错误,下面继续进行下一个练习!

2.3.3.代码3

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

  下面小编直接给出解释了:

  首先我们日常看第一个循环,很显然这个循环一共循环了100次,所以运行次数一共循环了100次,这里我们用了大O的第三个规则,对于是常量的循环次数,我们统一用O(1)来表示。废话不多说,我们开始进行下一个代码的练习:

2.3.4.代码4

    const char* strchr(const char* str, int character) {
        const char* p_begin = s;
        while (*p_begin != character) {
            if (*p_begin == '\0')
                return NULL;
            p_begin++;
        }
        return p_begin;
    }

  下面给出解释:

  这个算是一个比较难的复杂度的情况,因为这个是用来进行查找字符的函数,所以我们要进行分情况来讨论,下面先来第一种情况:

  第一种情况是所找的字符就在开头几个,此时我们仅仅循环几次就好了,所以这个可以称之为最好的情况,所以复杂度应该是0(1).

  第二种情况是所要找的字符串在中间附近,所以此时一共大约循环了N / 2次,为了与下面的复杂度进行区分,这里的时间复杂度我们用0(2 / N)来表示。这个可以称之为中等情况

  第三种情况是所要找的字符串在最后或者根本就不存在所想要找的字符串,这里一共循环了N        次,所以可以称之为最坏的情况,所以时间复杂度是0(N)次。

  对于我们如何进行时间复杂度的筛选,这里小编直接给出结果了,我们在计算时间复杂度的时候,要计算最坏情况下的时间复杂度,如果最坏的时间复杂度下的代码都过了的话,那么很显然其他情况也可以,读者朋友一定要记得这个知识点哟~~~下面我们进入下一个代码时间复杂度的练习!

2.3.5.代码5

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

  可能有些细心读者朋友已经认出小编写的这个代码是什么了,没错,这个代码就是冒泡排序,小编在之前写过,有兴趣的读者朋友可以进小编主页进行查看,废话不多说,下面进入解析部分:

  这个代码其实也可以分为三种情况,不过小编之前说过,计算时间复杂度,我们就要去计算最坏的情况(此时end也为n),那么就是完整的进行一次循环,下面小编通过图文的方式来告诉各位这个题的循环次数:

  所以其实一共循环了N * (N - 1) / 2次,经过大O规则后,最后的出来的时间复杂度是0(N ^ 2),这个算是比较典型的题目,读者朋友们一定要好好的去理解!下面我们进入下一个题目的训练

2.3.6.代码6

    void func5(int n) {
        int cnt = 1;
        while (cnt < n) {
            cnt *= 2;
        }
    }

   这个题目乍一看很简单,很多读者朋友会脱口而出:是O(N)!如果真这么写,那么就出大错喽!下面小编给出这个题目的解析:

  这个题目最大的坑就是循环内部的内容,cnt * 2,并且此时while循环就是通过cnt来进行定义的,所以这里循环次数与内部是息息相关的,我们这里可以通过数学的思想,将 = 右边的内容变成N,左边的内容变成2 ^ X,此时其实是2 ^ x = N,我们两边同时取对数,可以求出此时x等于log N(以2为底,不过我们再写复杂度的时候,同样也可以把2,底数给省略掉,因为如果N过于大的话,底数多大已经无所谓了!),所以应该是0(log N)!各位读者朋友可不要跳进这个题目的陷阱里,好了,以上便是对于时间复杂度的一些练习题,下面我们将要进入空间复杂度的练习!

3.空间复杂度

3.1.空间复杂度的计算方法

  首先,空间复杂度的计算方法其实和时间复杂度是差不多的,它们都是用的大O的渐进表示法,只不过时间复杂度是来计算运行次数的,而空间复杂度是来计算变量的个数的(这里涉及到了动态内存的开辟等等),这里值得一提的是:函数在运行时所需要的栈空间在编译期间已经确定好了,因此空间复杂度主要是针对于函数在运行的时候所申请的额外空间所确定的!

3.2.相关例题的详解

3.2.1.代码1

    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等一些局部变量,此时可以直到开辟的空间为一个常量,所以最后计算的大O应该是O(1)!其实空间复杂度相较于时间复杂度算是比较简单的了; 下面我们进入下一个习题的讲解!

3.2.2.代码2

    long long Fac(size_t N) {
        if (N == 0)
            return 1;
        return Fac(N - 1) * N;
    }

  首先很显然,这是一个函数递归的知识,而且计算的是阶乘的知识,下面我们继续来计算这个题的空间复杂度:

  此时我们不难知道,这里这个函数被调用了N次,所以应该开辟了N个函数栈帧,每个栈帧都有着相应的常数项空间,所以我们可以得出结论,空间复杂度应该是O(N)!

总结:

  此时,小编已经讲完了大部分关于复杂度的知识点,对于这部分内容,其实各位读者朋友没必要去死磕关于复杂度类型的习题,这部分的知识可以做几个来自己了解一下,毕竟复杂度的习题很考验各位的数学能力,大家先知道这是什么就好,至于对于其的计算,其实也就是算法题会涉及这个,大家做题时注意一下就好,好了,如果文章有错误的地方,恳请您在评论区指出,可能很多读者朋友会很好奇旋转数组怎么写才对,小编会在下一篇文章写这部分内容,那么,我们下一篇博客见喽!

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值