带你彻底吃透·算法复杂度

Posted kikokingzz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你彻底吃透·算法复杂度相关的知识,希望对你有一定的参考价值。

🌕写在前面


Hello🤗大家好啊,我是kikokingzz,名字太长不好记,大家可以叫我kiko哦~

从今天开始,我将正式开启一个新的打卡专题——【数据结构·水滴计划】,没错!这是今年上半年的一整个系列计划!本专题目的是通过百天刷题计划,通过题目和知识点串联的方式刷够1000道题完成对数据结构相关知识的全方位复习和巩固;同时还配有专门的笔记总结和文档教程哦!想要搞定,搞透数据结构的同学

🎉🎉欢迎订阅本专栏🎉🎉

🍊博客主页:kikoking的江湖背景🍊


🌟🌟往期必看🌟🌟

🔥【水滴计划】第一话·数据结构入门竟如此简单?🔥

目录

🌕写在前面

🍺知识点2:算法及其评价

🥝2.1 算法的基本概念

🍊1.什么是算法?

🍊2.数据结构与算法有什么关系?

🍊3.算法具有什么特性呢?

🍊4.怎样算一个好的算法?

🍊5.什么是算法的效率?

🥝2.2 算法的时间复杂度

🍊1.什么是时间复杂度?

🍊2.大O的渐进表示法

🍊3.时间复杂度有哪3种情况?

🍊4.计算复杂度的两条规则

📜实战案例

🥝2.3 算法的空间复杂度

🍊1.什么是空间复杂度?

📝LeetCode之消失的数字

📝LeetCode之旋转数组

📜水滴石穿

🌕写在最后

热爱所热爱的, 学习伴随终生,kikokingzz与你同在!❥(^_-)

🍺知识点2:算法及其评价

🥝2.1 算法的基本概念


🍊1.什么是算法?

算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每条指令表示一个或多个操作。


🍊2.数据结构与算法有什么关系?

程序 = 数据结构 + 算法

  • 数据结构:如何用数据正确地描述现实世界的问题(逻辑结构),并存入计算机(存储结构)。
  • 算法:如何高效地处理上述这些数据,以解决实际问题。

可见数据结构是将现实问题转化为电脑可以处理的数据,而算法则是求解问题的步骤;因此针对同一数据结构,求解问题的步骤方法可以不同,因此使用的算法也可以不同。


🍊3.算法具有什么特性呢?

(1)有穷性:一个算法必须总在执行有穷步后结束,且每一步都可在有穷时间内完成。

  • 注:算法必须是有穷的,而程序可以是无穷的,如手机上的CSDN是程序,而不是算法。

(2)确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得到相同的输出。

(3)可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。

(4)输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。

(5)输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。


🍊4.怎样算一个好的算法?

(1)正确性:算法能够正确地解决求解问题。

(2)可读性:算法应具有良好的可读性,以帮助人们理解。

(3)健壮性:输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其名的输出结果。

(4)高效率与低存储需求:时间复杂度低,空间复杂度低。


🍊5.什么是算法的效率?

江湖中,传闻,算法效率有时空之分,具体来说便是时间效率与空间效率之分:

  • 时间效率被称为时间复杂度——衡量一个算法的运行速度
  • 空间效率被称为空间复杂度——衡量一个算法所需的额外空间

在计算机发展早期,计算机存储容量很小,所以很在意空间复杂度;但是时光流逝,如今计算机的江湖之中,存储容量已到达极之巅峰,我们已无需特别关注算法复杂度了。


📜007.题目难度 ⭐️

007.一个算法应该是( )。
A.程序
B.问题求解步骤的描述
C.要满足五个基本特性
D.A和C

🍊详细题解:

A. 程序是一组计算机能识别和执行的指令,运行于电子计算机上,其不一定满足有穷性,如死循环、操作系统等。

C. 是算法的特性而不是它的基本定义。

算法:对特定问题求解步骤的一种描述,它是指令的有限序列,其中每条指令表示一个或多个操作;算法代表对问题求解步骤的描述,而程序则是算法在计算机上的实现。

✅正确答案:B 

 ✨✨✨我是分割线✨✨✨

🥝2.2 算法的时间复杂度


🍊1.什么是时间复杂度?

算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比,算法中的基本操作的执行次数,为算法的时间复杂度。

Q1:可以使用“算法先运行,事后统计运行时间”的方法来估算时间开销吗?

A1:使用如此方法是不客观的,主要有以下几个原因:

  • 机器性能越强,开销越少;如:超级计算机 vs 单片机。
  • 编程语言越高级,执行效率越低;如:Java写的程序要比用C写的效率低一些。
  • 和编译程序产生的机器指令质量有关。
  • 有些算法是不能事后统计的,如:导弹精准控制算法

🍊2.大O的渐进表示法

实际中我们计算时间复杂度只需要计算大概执行次数,那么我们使用大O的渐进表示法。

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

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


🍊3.时间复杂度有哪3种情况?

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

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

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


🍊4.计算复杂度的两条规则

(1)加法规则:多项相加,只保留最高阶的项,且系数变为1。

  • T(n,m) = T1(n) + T2(n) = O [ max ( f(n), g(m) ) ]

(2)乘法规则:多项相乘,都保留。

  • T(n,m) = T1(n) * T2(m) = O [ f(n) * g(m) ]

 记忆方式:常 < 对 < 幂< 指 < 阶


📜008.题目难度 ⭐️⭐️

008.某算法的时间复杂度为O(n^2)表明该算法的( )。
A.问题规模是(n^2)
B.执行时间等于(n^2)
C.执行时间与(n^2)成正比
D.问题规模与(n^2)成正比

🍊详细题解:

算法时间复杂度:O[ F(N) 意味着算法在任何情况下,规模为n的前提下,所花费的时间小于等于K*F(N),其中K是与N无关的常数。

A/D. 在评价一个算法的复杂度时,已经默认问题规模为n。

B. 执行时间不是等于(n^2),时间复杂度是该算法的一个上界(即时该算法的最坏情况),应当是小于等于。

C. 算法时间复杂度本身就是一个数量级问题,表示该算法的执行时间小于等于K*(n^2),即与(n^2)成正比。

✅正确答案:C

 ✨✨✨我是分割线✨✨✨ 

📜实战案例


📜案例1——复杂度相加运算

该例中有两个for循环,循环的上限分别为M+1次和N+1次(+1是因为跳出循环时还计算了一次),由大O的渐进表示法可以计算为:O(M+N+2) 近似为O(M+N),本题没有其他条件,不知道M和N的数量级大小,因此O(M+N)可以作为最终答案。

  • 当 M>>N时:时间复杂度为O(M)
  • 当 N>>M时:时间复杂度为O(N)


📜案例2——用1代替所有常数次

该案例中有一个for循环,循环的上限取决于条件判断的常数大小,此处是100,说明最多进行101次循环;如果这里的100改为100000000,也同样是进行常数次循环。因此依照大O的渐进表示法,用1替代所有常数,则其时间复杂度为O(1)

相对的O(1)并不是代表只执行1次,而是代表执行常数次,可以是100次、1000次,也可以是1000万次,不管次数多大,都是常数次!


📜案例3——strchr采用最坏时间复杂度

strchr函数功能为在一个串中查找给定字符的第一个匹配之处。函数原型为:

char *strchr(const char *str, int c);

即在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。


📜案例4——冒泡排序的时间复杂度

最好情况:所有数字已经按序排列,自身有序排列,只需要冒泡排序一遍,由于没有发生交换,因此exchange值不发生改变,仍然为0,最后跳出循环,因此一共就执行了(N-1)次交换,所以其时间复杂度为O(N)。

最坏情况:所有数字倒序排列,每一轮都需要冒泡排序,第一遍冒泡排序(N-1)次,将最大的数字安排到最后一位,然后继续从头开始进行冒泡排序,此时只需要交换(N-2)次,将次大的数安排在倒数第二位,按此顺序一直计算到最后交换1次,此时所有数字排序完成,总共排序了 [(N-1)+(N-2)+·····+ 2 + 1 ] 次,最终计算的时间复杂度为


📜案例5——二分查找的时间复杂度

二分查找法这里假设数组元素呈升序排列,将n个元素分成个数大致相同的两半,取a[n/2]与欲查找的x作比较,如果x=a[n/2]则找到x,算法终止;如 果x<a[n/2],则我们只要在数组a的左半部继续搜索x;如果x>a[n/2],则我们只要在数组a的右 半部继续搜索x。

最好情况:直接1次就找到了,常数次找到,因此时间复杂度为O(1)。

最坏情况:当找不到该数时,需要不断进行折半,一直折半到最后 begin==end 时,退出循环,此时一共执行了 次(具体计算步骤见下图)。

注:时间复杂度中,习惯将log2(N) 简写为 logN


📜案例6——递归阶乘的时间复杂度

(1)单次递归函数调用为O(1)时,就看它递归的次数:

本题在疯狂进行递归,每一次递归中包含了常数次的运算:1次逻辑判断运算和乘法运算,但从时间复杂度角度来看可以近似忽略为O(1)次;而真正占据时间复杂度的应当是其向下递归的次数,其递归次数取决于N,是幂函数,因此其时间复杂度为O(N)。

(2)单次递归函数调用不为O(1)时,就看他递归调用中次数的累加:

long long Fac(size_t N)

	if (0 == N)
		return 1;
	for (size_t i = 0; i < N; ++i)
	
		printf("%d", i);
	
	printf("\\n");
	return Fac(N - 1) * N;

对于上面这种变式情况,每次递归调用不是O(1),每次递归调用中的运算次数是变化的,第一次递归时,函数内部进行N次循环;递归到第二次时,函数内部进行N-1次循环;···;递归到第N-1次时,函数内部循环2次;通过计算我们可以得出:


📜009.题目难度 ⭐️

009.【2012统考真题】求整数n (n≥0)的阶乘的算法如下,其时间复杂度是( )。
    int fact(int n) 
        if(n<=1) return 1;
        return n*fact (n-1) ;
    

A.O(logn)        B.O(n)       C.O(nlogn)        D.O(n^2)

🍊详细题解:

本题就属于上面第(1)种情况,单次递归函数调用为O(1),其只包含了一次乘法运算,因此其时间复杂度就看它向下递归调用的次数,该例有n次递归,因此其时间复杂度为O(n)。

✅正确答案:B


📜案例7——斐波那契数列的时间复杂度

斐波那契数列的递归其实像是一个N-1层金字塔(当然是有残缺的金字塔),但我们计算复杂度时候真正关心的是数量级问题,残缺的那部分根本不会改变数量级,因此可以将其近似看成一个满元素的金字塔,因此从顶向下挨个计算,也就是计算一个等比数列求和,公比为2;最终计算出其时间复杂度为 :


📜案例8——三层循环的时间复杂度

我希望通过这题,可以让大家明白复杂度的核心是数量级的问题,在计算步骤上,只要不改变其最终数量级,可以通过省略和近似的方式来估算其复杂度:

for (i=1 ;i<=n ;i++)
    for(j=1 ;j<=i; j++)
        for(k=1; k<=j; k++)
            x++;

对于上题我们可以从内层向外层来看,首先最内层循环的次数为 j 次,次外层循环为 i 次,此时计算内部两层循环的次数为(1+2+3+4+·····+i)次,即 (1+i)*i/2,这时我们再计算最外层循环,由于最外层循环n次,i的取值范围为(1~n),因此其执行总次数为:

由上述计算过程可得其最后的时间复杂度为O(n^3)。

注意:通过这题我希望大家明白的一个道理是,时间复杂度并不是准确计算出来的一种度量,而是一种算法的最坏情况,或者是算法的一种数量级情况,对于上题,在计算过程中我都采取了局部的省略和近似,这是因为,这些省略和近似并不会改变复杂度的最终数量级,也就是n^3,通过这种省略和近似可以节约复杂度的计算时间。

 ✨✨✨我是分割线✨✨✨

🥝2.3 算法的空间复杂度


🍊1.什么是空间复杂度?

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


📜案例1——冒泡排序的空间复杂度

空间复杂度计算的是在程序运行过程中,为了满足程序需求,临时创建的空间个数。在下例冒泡排序中,总共开了三个空间,分别是变量end、exchange、i;尽管在这个循环中,exchange这个变量被循环使用,但是用的始终都是exchange这1个空间,只是使用次数在增加,使用的空间个数始终为1,没有发生改变,因此其时间复杂度为O(N),而空间复杂度为O(1)。

注意:使用了常数个空间==空间复杂度为O(1)


📜案例2——斐波那契数列的空间复杂度

下例中,我们单单计算fibArray动态开辟的空间就已经有(n+1)个了,其他剩下的变量开设的都是常数个,因此时间复杂度为O(N);我们这里只需要算一个大概的即可,计算开辟空间数最多的那个即可。

注意:空间复杂度的计算比时间复杂度的简单,一般来说不是O(1)就是O(N)。

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

📜案例3——计算阶乘递归Fac的空间复杂度

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

对于本题,递归出N个栈帧,栈帧开辟的空间是常数个,可以认为是O(1),因此决定其开辟空间个数的主要取决于其递归的次数,本题递归次数为N次,因此空间复杂度为O(N)。


📜案例4——计算斐波那契数列的空间复杂度

long long Fib(size_t N) 

     if(N < 3)
         return 1;
 
     return Fib(N-1) + Fib(N-2);

上例中我们不断调用,当我们一直递归调用到Fib(2)时,将Fib(2)的值返回给Fib(3),这时Fib(2)栈帧就销毁了,Fib(3)将接着调用Fib(1),这时Fib(1)使用了之前Fib(2)使用的空间,使用了同一块空间;同理在向上返回的时候将不断重复使用销毁过的空间,从Fib(2)到Fib(N)总共建立了N-1个栈帧;而后该开始递归调用Fib(N-2),而这块空间正是之前Fib(N-1)使用过的空间,即空间是可以回收复用的。因此本题的空间复杂度为O(N)。

注意:时间是累计的不可复用,而空间回收之后是可以重复利用的。

程序证明:

当调用完f1( )函数后,将f1中的空间释放;调用f2( )函数,由a和b的地址相同可见,f2( )使用的空间正是刚刚f1释放的空间,证明空间被重复利用了。

void f1()

	int a = 0;
	printf("%p\\n", & a);


void f2()

	int b = 0;
	printf("%p\\n", & b);


int main()

	f1(); //调用函数开始会创建栈帧;结束会销毁栈帧
	f2();
	return 0;


📜010.题目难度 ⭐️⭐️

010.下面的说法中,错误的是( )。
I.算法原地工作的含义是指不需要任何额外的辅助空间
II.在相同规模n下,复杂度为O(n)的算法在时间上总是优于复杂度为O(2")的算法
III.所谓时间复杂度,是指最坏情况下估算算法执行时间的一个上界
IV.同一个算法,实现语言的级别越高,执行效率越低
A.I                B.I、II            C.I、IV            D.III

🍊详细题解:

I. 算法原地工作是指算法所需的辅助空间是常量,即O(1)个空间。

II. 算法的负责度是一个宏观上的理解,O(n)的算法一定是优于O(2")的算法。

III. 时间复杂度总是考虑最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长。

IV. 越高级的语言,需要层层转换为低级语言给计算机读取,因此执行效率会降低。

✅正确答案:A

 ✨✨✨我是分割线✨✨✨

📝LeetCode之消失的数字



思路1-排序

思路:将这些数字进行挨个排序,如果后一个数字不是前一个数字+1,那么缺的数就是这两个数共同相邻的那个数。例如我们将数字排序后得到1,2,4,我们从前向后挨个比较相邻两个数,我们发现2后面的数字不等于2+1,因此我们得出缺少的数字是3。

优点:简单易想

缺点:时间复杂度太大,以下算法时间复杂度均未满足题目要求。

  • 采用冒泡排序—— 时间复杂度:
  • 采用快排qsort——时间复杂度:O(N*logN)

思路2-映射方式

思路:建立一个可以完整包含0~n的所有整数的数组并将其初始化为全-1;然后将输入的数字x放到对应下标为[x]的数组空间中(如数字6就放到下标为[6]的空间里);全部放完后,数组中元素值为-1对应的下标,就是缺失的数字。

缺点:这种方式有O(N)的空间复杂度。


思路3-异或^

前情提要:

1.异或运算中顺序发生改变,并不会影响最终结果:

 a^b^c = b^a^c = b^c^a = c^b^a =···

2.两个相同数字异或的最终结果为0;

a^a = b^b = 0

3.0和任意非零数x相异或,其结果等于非零数x;

0 ^ x = x

思路:对于本题,可先对0-n的连续数字异或一遍,接着对含有欠缺数字的数组中的每个数字异或,那么该数组中未欠缺的数字和0-n连续数字会异或2次,结果为0;而因为数组中缺少1位数字,原先0-n的连续数字中必有1位只参与异或1次,因此最后异或计算出的值即为缺失的数字。

代码实现:

int missingNumber(int* nums, int numsSize)
    int sum=0;
    for(int i=0;i<numsSize;++i)
    
        sum^=nums[i];//计算数列中的异或
    //时间复杂度为O(N)
    
    for(int j=0;j<numsSize+1;++j)
    
        sum^=j;//求0-n的异或
    //时间复杂度为O(N)
 
    return sum;

思路4-数列和相减

(0-n计算等差数列的和)-(数组中的值相加)
       O(1)               O(N) 

优点:算法时间复杂度满足√

代码实现:

int missingNumber(int* nums, int numsSize)
    int sum1=0 , sum2=0 ,ret=0;
    for(int i=0;i<numsSize;++i)
    
        sum2+=nums[i];//计算数列中的和
    //时间复杂度为O(N)
    sum1=numsSize*(numsSize+1)/2;//计算0-n的等差数列之和
    ret=sum1-sum2;
    return ret;

 ✨✨✨我是分割线✨✨✨

📝LeetCode之旋转数组


进阶:

  • 尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
  • 你可以使用空间复杂度为 O(1) 的原地算法解决这个问题吗?

思路1-依次右移动

思路:右旋K次,依次移动一个;当K=N时,等于数组没有发生旋转,当K=N+1时,等于数组旋转1次,即旋转的次数=(K%N);当(K%N=N-1)时,旋转的次数最多。

时间复杂度:旋转1次需要移动N次,旋转K次就需要移动N*K次,因此为O(K*N);当K的值为N-1时,复杂度为O(N^2),但是由于K也是未知数,因此该算法的时间复杂度为O(N*K)。

空间复杂度:没有额外开数组,用的是原数组,因此为O(1)。


思路2-额外开数组

思路:用空间来换时间,额外建立一个数组,将后K个数拷贝到新建数组的前端,将(n-K)个剩下的数,拷贝到新数组的后端。

时间复杂度:将从前向后挨个计算,将前(n-k)个数拷贝到新数组后(n-k)个位置,再将后k个数拷贝到新数组的前k个位置,计算量即为n,因此时间复杂度为O(N)。

空间复杂度:这时额外开了一个数组,因此空间复杂度为O(N)。


思路3-三轮逆置

思路:用空间来换时间,额外建立一个数组,将后K个数拷贝到新建数组的前端,将(n-K)个剩下的数,拷贝到新数组的后端。

时间复杂度:进行三轮逆置,因此时间复杂度的量级依然取决于元素个数n,时间复杂度为O(N)。

空间复杂度:没有额外开数组,用的是原数组,因此为O(1)。

代码实现:

void reverse(int*a, int left , int right)//创建一个逆置函数

    while(left<right)
    
        int tmp=a[left];
        a[left]=a[right];
        a[right]=tmp;
        ++left;
        --right;
    


void rotate(int* nums, int numsSize, int k)
    k=k%numsSize;
    reverse(nums,0,numsSize-k-1);//step1.逆置前n-k个
    reverse(nums,numsSize-k,numsSize-1);//step2.逆置后k个
    reverse(nums,0,numsSize-1);//step3.逆置整体n个

 ✨✨✨我是分割线✨✨✨

📜水滴石穿


📜011.题目难度 ⭐️

011.以下算法的时间复杂度为( )。
void fun(int n) 
	int i = 1;
	while (i <= n)
		i = i * 2;

A.O(n)        B.O(n^2)        C.O(nlogn)        D.O(logn)

🍊详细题解:

✅正确答案:D


📜012.题目难度 ⭐️

012.以下算法的时间复杂度为( )。
void fun(int n) 
	int i = 0;
	while (i * i * i <= n)
		i++;

A.O(n)        B.O(nlogn)        C.O(³√n)        D.O(√n)

🍊详细题解:

✅正确答案:C


📜013.题目难度 ⭐️

013.以下算法中最后一行语句的执行次数为( )。
	int m = 0, i, j;
	for (i = 1; i <= n; i++)
		for (j = 1; j <= 2 * i; j++)
			m++;
A.n(n+1)        B.n        C.n+1        D.n^2

🍊详细题解:

✅正确答案:A


📜014.题目难度 ⭐️⭐️

014.程序段如下:
	for (i=n-1;i>1;i--)
		for (j=1;j<i;j++)
			if (A[j]> A[j+1])
				A[j]与A[j+1]对换;

其中n为正整数,则最后一行语句的频度在最坏情况下是( )。
A.O(n)        B.O(nlogn)        C.O(n³)        D.O(n²)

🍊详细题解:

✅正确答案:D


📜015.题目难度 ⭐️⭐️

015.【2014统考真题】下列程序段的时间复杂度是( )。
count=0;
for (k=1 ; k<=n ; k*=2)
    for(j=1;j<=n;j++)
        count++;
A.O(logn)        B.O(n)        C.O(nlogn)        D.O(n^2)

🍊详细题解:

✅正确答案:C


📜016.题目难度 ⭐️⭐️⭐️

016.【2017统考真题】下列函数的时间复杂度是( )。
    int func(int n)
        int i=0,sum=0;
        while (sum<n) sum +=++i;
        return i;
    
A.O(logn)        B.O(√n)        C.O(n)        D.O(nlogn)

🍊详细题解:

✅正确答案:B


 📜017.题目难度 ⭐️⭐️⭐️

🍊详细题解:

🌕写在最后


数据结构的世界是相当丰富的,内容方向繁多,但只要一步一个脚印,跟随【水滴计划】水滴石穿吃透、搞懂、拿捏住数据结构是完全没有问题的!后期该系列还会有视频教程和经验分享,关于更多这方面的内容,请关注本专栏哦!

热爱所热爱的, 学习伴随终生,kikokingzz与你同在!❥(^_-)

以上是关于带你彻底吃透·算法复杂度的主要内容,如果未能解决你的问题,请参考以下文章

带你彻底吃透Spring

电信架构师带你一节课彻底吃透spring源码

带你彻底吃透Java NIO

带你彻底吃透Spring

带你彻底吃透Spring

从冰箱装大象到女娲造人,带你彻底吃透Python面向对象编程