• 第一章 时间复杂度和空间复杂度


    初阶数据结构

    第一章 时间复杂度和空间复杂度



    前言

    本系列文章,将基于C语言实现一些初阶的数据结构,目的是帮助大家熟悉C语言的同时能够入门数据结构。


    一、时间复杂度

    1、什么是时间复杂度?

    时间复杂度,顾名思义,计算的是代码运行所花费的时间。但是我们知道,一个程序是在电脑上运行的,而电脑不同,同一个程序的运行时间也是不同的。同样的一套程序,我们用十年前的电脑和现在的电脑分别运行,最终的结果肯定是不同的。
    所以,此处所说的时间复杂度并不是真正的代码运行所需的时间,而是一个程序中 代码所运行的次数 。然后将这个次数写成一个 数学函数表达式。此时,这个表达式就是这个程序的时间复杂度。

    例如下面代码:

    #include
    int main()
    {
    	int n;
    	scanf("%d",&n);
    	for(int i=0;i<n;i++)
    	{
    		printf("%d ",i);
    	}
    	int m=10;
    	while(m--)
    	{
    		printf("hehe\n");
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    我们来算一算上面代码的运行次数,即时间复杂度。
    在这里插入图片描述
    在计算代码的运行次数的时候,我们一般忽略掉循环的头部和尾部。
    所以,我们通过上面的图示我们发现,这个程序的时间复杂度为:F(n)= n+13

    2、大O的渐进表示法

    我们看下面的代码:

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们计算一下上面代码的执行次数:
    在这里插入图片描述
    我们发现最终的运行次数为:F(n)=N*N+2N+13
    我们不妨给这里的N进行赋值操作:

    • N=10 F(N)=133
    • N=100 F(N)=10213
    • N=1000 F(N)=1002013
      我们发现随着我们N数值的增长,N*N的占比越来越大,剩余项的大小完全可以忽略掉。
      所以我们此时只需要保留该函数表达式中的最高次项,其余完全可以忽略掉。基于这种想法,这里就出现了一种方法叫做大O渐进表示法,也可以成为大O记法

    大O记法的方式:

    - 仅仅保留最高次项。

    - 最高次项的系数为1。

    - 假设代码的运行次数为常数,那么统一记作O(1) 。


    那么上述代码的运行次数的函数表达式可以简化为:O(N2)

    另外有些时候,算法的运行次数是不确定的,例如我们利用遍历的方式在长度为N的数组中寻找一个数字,那么运气好的话,我们要找的数字就是第一个元素,此时算法的复杂度是O(1)。倘若我们运气不好的话,我们的要找的元素就是最后一个元素。此时我们算法的时间复杂度就是O(N)
    那么我们在最坏和最好的情况之间取一个平均值,即N/2,但我们利用大O记法记录时,依旧是O(N)

    从上面这个例子中,我们就能发现,我们算法的时间复杂度可以分为三种情况:

    • 最坏情况:任意输入规模的最大运行次数(上界)
    • 平均情况:任意输入规模的期望运行次数
    • 最好情况:任意输入规模的最小运行次数(下界)

    3、常见的时间复杂度例题

    例一

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    例二

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    例三

    void Func4(int N)
    {
    int count = 0;
    for (int k = 0; k < 100; ++ k)
    {
     ++count;
    }
    printf("%d\n", count);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    例四

    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;
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    例五

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    例六

    long long Factorial(size_t N)
    {
    return N < 2 ? N : Factorial(N-1)*N;
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    例七

    long long Fibonacci(size_t N)
    {
    return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    我们发现上面的题目中,很多题可以从表面上看出来其时间复杂度,但是有一些题目,例如:冒泡排序、斐波那契、二分查找等等算法,需要我们通过其背后的思想,通过画图等方式算出其时间复杂度,不能想当然。

    二、空间复杂度

    1、什么是空间复杂度?

    空间复杂度是对一个算法在运行过程中临时占用的存储空间大小的量度,空间复杂度不是程序占用了多少字节,而是创建变量的个数。空间复杂度的表示方法依旧采用大O渐进表示法。

    2、例题:

    (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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    分析:
    在这里插入图片描述
    我们来分析一下上述代码:
    这个算法是为了实现排序。无论采用什么排序算法,函数中的形参都是存在的。所以形参并不是我们为了实现这个算法而创建的。那么为了实现冒泡排序的算法,我们临时创建的变量有图中的三个。注意:for循环内的exchange不断地重复创建,只算一次。我们可以理解成这个变量在循环的过程中不断地创建销毁,使用的都是一块空间。综上,这个代码的是时间复杂度是O(1)

    (2)例二:

    // 计算阶乘递归Factorial的空间复杂度?
    long long Factorial(size_t N)
    {
    	return N < 2 ? N : Factorial(N-1)*N;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    (3)例三:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    (4)例四:

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

    在这里插入图片描述
    我们发现斐波那契函数递归展开来以后是非常庞大的。呈现指数型增长!而每个函数的内部的空间复杂度都是O(1)。难道空间复杂度是
    O(2n)吗?其实为了节约空间,系统不会直接展开所有,而是如图中的绿线所示,每次展开一条递归路线,当这个递归路线到底销毁后再开启另外一条。而每一条的空间复杂度是O(N)* O(1)=O(N)。而这些空间是重复利用的。所以递归最终的空间复杂度是 O(N)

  • 相关阅读:
    大模型、实时需求推动湖仓平台走向开放
    Python进程间的通信之管道通信:os.pipe
    SELECT COUNT(*) 会造成全表扫描吗?
    理解回归_多元线性回归_最大似然函数_最大密度函数_标准差_方差_数据离散程度---人工智能工作笔记0020
    【无标题】
    Linux操作系统学习:day05
    如何用Jmeter对数据库执行压力测试
    【LeetCode】【剑指offer】【剪绳子(二)】
    Vue学习笔记
    基于51单片机的多功能时钟温度计proteus仿真原理图
  • 原文地址:https://blog.csdn.net/weixin_72060925/article/details/127199794