• 时间复杂度和空间复杂度


    一、算法的复杂度

    算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般
    是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度
    时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算
    机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计
    算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
    (它可以现在不重要,但是我们不能不会!!!!)

    二、时间复杂度

    1.时间复杂度概念

    时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一
    个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知
    道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个
    分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法
    的时间复杂度。

    下面我们看一下求程序所用时间的一个函数clock()
    由图可以看出Debug和Release运行的结果还是不一样的,Release是经过优化的
    在这里插入图片描述
    下面我们看一段代码,来初步了解一下

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

    下面是我们真实算出的结果,而在实际中只需要估读就够了遵循大O的渐进表示法
    在这里插入图片描述

    2.大O的渐进表示法

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

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

    使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)
    N = 10 F(N) = 100
    N = 100 F(N) = 10000
    N = 1000 F(N) = 1000000
    通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
    另外有些算法的时间复杂度存在最好、平均和最坏情况:
    最坏情况:任意输入规模的最大运行次数(上界)
    平均情况:任意输入规模的期望运行次数
    最好情况:任意输入规模的最小运行次数(下界)
    例如:在一个长度为N数组中搜索一个数据x
    最好情况:1次找到
    最坏情况:N次找到
    平均情况:N/2次找到
    在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

    3.常见时间复杂度计算举例

    3.1 实例1 【 O(N)】
    // 计算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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    此题应用的是只保留最高阶项,最高阶项存在且不是1,则去除与这个项目相乘的常数。
    实例1基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)

    3.2 实例2 【O(N+M)】
    
    // 计算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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    实例2基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)

    3.3 实例3 【O(1)】
    // 计算Func4的时间复杂度?
    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
    • 10

    用常数1取代运行时间中的所有加法常数
    实例3基本操作执行了100次,通过推导大O阶方法,时间复杂度为 O(1)

    3.4 实例4 【O(N)】
    // 计算strchr的时间复杂度?
    const char * strchr ( const char * str, int character );
    
    • 1
    • 2

    实例4基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为 O(N)

    3.5 实例5 【O(N^2)】
    
    // 计算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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    实例5基本操作执行最好N次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最
    坏,时间复杂度为 O(N^2)

    3.6 实例6 【O(logN)】
    // 计算BinarySearch的时间复杂度?
    int BinarySearch(int* a, int n, int x)
    {
    	assert(a);
    	int begin = 0;
    	int end = n - 1;
    	// [begin, end]:begin和end是左闭右闭区间,因此有=号
    	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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    实例6基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN) ps:logN在算法分析中表示是底
    数为2,对数为N。有些地方会写成lgN。(建议通过折纸查找的方式讲解logN是怎么计算出来的)
    在这里插入图片描述

    3.7 实例7 【O(N)】
    // 计算阶乘递归Fac的时间复杂度?
    long long Fac(size_t N)
    {
    	if (0 == N)
    		return 1;
    	return Fac(N - 1) * N;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    实例7通过计算分析发现基本操作递归了N次,时间复杂度为O(N)。
    这题可以进行一下扩展

    long long Fac(size_t N)
    {
    	if (0 == N)
    		return 1;
    
    	for (int i = 0; i < N; i++)
    	{
    		//……
    	}
    	return Fac(N - 1) * N;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    3.8 实例8【O(2^N)】
    // 计算斐波那契递归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

    因为下面有两个分支所以是以二倍的速度增长,所以有时候递归并不是很好
    在这里插入图片描述

    结果为【O(N^2)】

    三、空间复杂度

    空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
    空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
    空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
    注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因
    此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

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

    这里创建了三个变量,因为这里也遵循大O渐进表示法,所以结果为【O(1)】

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

    因为这里创建了一个动态数组,申请了N个变量所以空间复杂度【O(N)】

    实例3 【O(N)】
    // 计算阶乘递归Fac的空间复杂度?
    long long Fac(size_t N)
    {
    	if (N == 0)
    		return 1;
    	return Fac(N - 1) * N;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    实例3递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)

    四、复杂度的oj练习

    1.消失的数字

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述
    方法一代码:

    int missingNumber(int* nums, int numsSize){
        int num=numsSize*(numsSize+1)/2;
        for(int i=0;i<numsSize;i++)
        {
            num-=nums[i];
        }
        return num;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    方法二代码:

    int missingNumber(int* nums, int numsSize){
       int n=0;
       for(int i=0;i<numsSize;i++)
       {
           n^=nums[i];
       }
       for(int i=0;i<numsSize+1;i++)
       {
           n^=i;
       }
       return n;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    下面还有一种方法就是不符合题意,超出了时间复杂度
    在这里插入图片描述

    2.旋转数组

    在这里插入图片描述

    2.1 方法一 遍历(行不通,在本题干)

    超出时间限制
    在这里插入图片描述

    void rotate(int* nums, int numsSize, int k) {
        for (int i = 0; i < k; i++)
        {
            int n = nums[numsSize - 1];
            for (int j = numsSize - 1; j > 0; j--)
            {
                //n=arr[j];
                nums[j] = nums[j - 1];
            }
            nums[0] = n;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    2.2 方法二 反转法

    先将整体反转,之后在分为两部分反转

    在这里插入图片描述

    void reverse(int *nums,int left,int right)
    {
        //int temp=0;
        while(left<right)
        {
            int temp=nums[left];
            nums[left]=nums[right];
            nums[right]=temp;
            left++;
            right--;
        }
    }
    void rotate(int* nums, int numsSize, int k){
        k %= numsSize;
        reverse(nums,0,numsSize-1);
        reverse(nums,0,k-1);
        reverse(nums,k,numsSize-1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    2.3方法三 开辟数组法

    创建一个数组,将数字分为两个部分,放入数组
    在这里插入图片描述

    void rotate(int* nums, int numsSize, int k)
    {
    	k %= numsSize;
    	int *arr = (int *)malloc(sizeof(int)*numsSize);
    	int count = 0;
    	for (int i=numsSize-k; i < numsSize; i++)
    	{
    		arr[count] = nums[i];//将需要颠倒的先拷贝过去
    		count++;
    	}
    	for (int j = 0; j< numsSize - k; j++)
    	{
    		arr[count] = nums[j];//再将后面的拷贝过来
    		count++;
    	}
    	for (int j = 0; j < numsSize; j++)
    
    	{
    		nums[j] = arr[j];//拷贝回原数组
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    2.4 方法四 环状替换法

    环状替换法,即从起点开始,每次以K为单位进行跳跃式的替换,每个元素都会进行一次替换,进行了N次替换,即完成了数组的旋转;
    注意点:
    1.当下标+K大于数组长度的时候,就需要减去长度回到”起点“处;
    2.回到起点后,判断回来的位置是否和出发的位置重复,如果不重复,那么一直进行下去,直至完成N此跳跃,形成一个环圈;
    如果重合了,每次回到起点后就将起点完后挪动一个位置,直至完成N次跳跃;
    原文链接
    在这里插入图片描述

    在这里插入图片描述

    void rotate(int* nums, int numsSize, int k){
        k %= numsSize;
        int begin=0;
        int temp=0;
        int count=0;
        int num=k;
        int previous = nums[0];
        while(count<numsSize)
        {
            if(num<numsSize)
            {
                temp=nums[num];
                nums[num]=previous;
                num+=k;
                previous=temp;
                count++;
            }
            else
            {
                temp=nums[num-numsSize];
                nums[num-numsSize]=previous;
                previous=temp;
                count++;
                if (num - numsSize == begin)//回到起点的位置和出发位置相同
    			{
    				num = num - numsSize + 1;
    				begin=num;//继续标记出发位置
    				previous = nums[num];//重新储存起点
    				num += k;
    			}
    			else
    			{
    				num = num - numsSize + k;
    			}
            }
        } 
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    最后:文章有什么不对的地方或者有什么更好的写法欢迎大家在评论区指出

  • 相关阅读:
    【C语法学习】27 - strncat()函数
    Go语学习笔记 - 调用ffmpeg-api实现音频重采样
    ±15kV ESD 保护、3V-5.5V 供电、真 RS-232 收发器MS2232/MS2232T
    RTSP 和 RTMP原理 & 通过ffmpeg实现将本地摄像头推流到RTSP服务器
    降低模拟量信号干扰的10个有效方法
    Semantic Kernel 入门系列:💬Semantic Function
    WebView输入框软键盘遮挡问题(沉浸状态栏和adjustResize的冲突)
    “30岁,当一名游戏技术总监去”
    【无标题】
    linux 网络 cat /proc/net/dev 查看测试网络丢包情况
  • 原文地址:https://blog.csdn.net/m0_66599463/article/details/127477731