【数据结构】【1】时间复杂度和空间复杂度

前言

我们总说谁谁谁写的代码效率低、效率不高,那是依照什么说的低呢?

今天我们就聊一聊有关效率的话题。

1、时间复杂度

1.1 什么是时间复杂度

  • 时间复杂度,看见名字我们可以大胆的猜一下时间复杂度什么意思?我们可以根据字面意思进行理解,时间复杂度就是代码运行时间复杂的程度,我们先这么理解。
  • 其实呢,我们所说的效率问题一般都出现在写的算法实现过程中,我们来打个比方,看下面这个图,我有一个有序数组里面存放的是1~10的整数,我想在里面找某一个数,假如我们要在下面这个数组中找数字4,我们应该如何去找呢?

  • 思路一:我们想一上来对数组进行遍历。
#include <stdio.h>
int main()
{
	int num = 7;//我们要找的数字
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		if (num == arr[i])
			printf("找到了,下标是:%d", i);
	}
	return 0;
}

        但是,我们想一想,如果要找的数在数组的前面,是不是寻找起来挺快的,如果要找的数字在数组的最后呢?我们是不是需要将数组整个遍历一遍。 如果找7的话我们从前循环就需要循环7次,如果数组里面有n个数字,从里面找一个数字是不是就更多了,我们在考虑算法效率的时候,我们应当以最坏的运行结果来考虑,假如这个数字就在最后一个那么就需要循环N次。N就是我们这个算法的执行次数

这个算法的基本执行次数就是算法的时间复杂度


 1.2 时间复杂度计算

        实际中我们计算时间复杂度的时候,不需要精确到每一次,而只需大概执行的次数。我们用的是大O的渐进表示法,顾名思义这是一个趋近于的问题。

 我们给出来大O渐进表示法的方法

  1. 用常数1取代运行时间中所有的加法常数
  2. 在修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶项存在而且不是1,我们就把这个高阶项前面乘的常数去掉。

        根据上面求时间复杂的方法, 我们实际的算一下函数Func1()时间复杂度是多少。

        所以Func1()这个函数执行一共有 N^2 + 2*N + 10 次,

        根据上面的算式,Func1()函数的时间复杂度为O(N^2)。

        我们来详细的分析一下为什么是O(N^2),怎么得到的这个结果。

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

        这句是什么意思呢?就是如果N是一个确定的数那么O(1)和O(1000000)都是一样的,时间复杂度就为O(1)。

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

        这句是什么意思呢?刚才我们分析了这个函数一共执行了 (N^2 + 2*N + 10)次,我们看到执行次数中有N^2和N,那么N^2是比N更高一阶的,所以只保留N^2,后面的通通不要,这就是大哥影响力大,大哥说了算。

  • 如果最高阶项存在而且不是1,我们就把这个高阶项前面乘的常数去掉

        这句又是什么意思呢?如果这个高阶项是2N那么前面乘不乘这个2效果是一样的,可以把N看成是无穷,这样乘2还是无穷乘不乘影响不大。

        这样我们就知道如何求时间复杂度了。

1.3 练习

我们看一下leetcode上有一道题

题目:数组nums包含从0n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?

 首先呢,他要求我们时间复杂度O(N)内完成。那么好,我们来分析一下。

        数组arr包含从0~n的数字,但是里面缺了一个,我们少画一点,就比方说就是上面这个数组arr,我们可以很直观的看出来,arr数组中缺少的是2。

思路一:我们先进行排序,将数组arr变成一个有序的数组,然后我们使用循环进行比较,看看是缺少哪一个,我们就按照最基础的冒泡排序,冒泡排序我们知道有两层循环,第一层是n-1次循环,第二层是n每次少比较一个元素,第一次循环比较(n-1)个元素,一共有(n-1)次循环,(n-1) + (n-2)+.....+2+1 = (n-1)(n-1+1)/2,我们保留最高阶,并且去掉倍数,冒泡排序的时间复杂度为O(N^2)。思路一已经超过题目要求了,故不行。

思路二:将这n+1个数加起来,然后减去数组n个数相加,就得到缺少哪一个数,0+1+4+3+5+6 = 19;这个数组是少一个数,所以正常的话应该是n+1个数,所以1+2+3+4+5+6 = 21; 21 -9 = 2;所以我们知道缺少的是2。我们只需要考虑数组遍历相加的时候的时间复杂度,很容易,遍历一遍数组时间复杂度就是O(N)。满足我们的要求。我们写一下这个代码。

#include <stdio.h>
int main()
{
	int ret = 0;
	int sum = 0;
	int arr[] = { 0,1,4,3,5,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		ret += arr[i];
	}
	for (int i = 0; i <= 6; i++)
	{
		sum += i;
	}
	printf("%d", sum - ret);
	return 0;
}

注意,这里面虽然有两个循环,但是N+N=2N,时间复杂度还是O(N),这个得注意一下。

思路三:我们在C语言的时候学过异或,异或是相同为0不同为1,利用异或这个特性我们可以将数组内的数和0~n的所有数进行异或,那么最后那个数就是缺少的数。时间复杂度跟上面思路二的时间复杂度一样,都是两层外部循环。我们写一下这个代码。

#include <stdio.h>
int main()
{
	int ret = 0;
	int arr[] = { 0,1,4,3,5,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		ret ^= arr[i];
	}
	for (int i = 0; i <= 6; i++)
	{
		ret ^= i;
	}
	printf("%d", ret);
	return 0;
}

我们再来看一下二分查找的时间复杂度,我们就不看实例代码,我举个例子吧,就相当于一张纸,每一次对折一次,算一次查找,如果没找到再对折一次,这样就缩小判断的区间。那么二分查找的时间复杂度是多少呢,我们还是看纸的那个例子,每一次分成两份,那么我们循环了x次才找到,2^x=N,那么时间复杂度就为O(LogN)(2为底,默认写成LogN)。

1.4 递归的时间复杂度

递归时间复杂度 = 递归循环的次数*递归嵌套的循环次数

下面这个代码是求斐波那契数列的代码,我们算一下它的时间复杂度。

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

简单说一下什么是斐波那契数列,如同 1 1 2 3 5 8 13,后一个数等于前面两个数相加

 

通过一步一步推,我们可以看见,最后斐波那契数列通过递归的方式最后的时间复杂度是O(2^N),我们测试一下,发现当N为50的时候计算机求解过程就非常的慢,指数增长实在是太快了。我们可以看出来斐波那契数列递归展开特别像二叉树。


2、空间复杂度

1.1 什么是空间复杂度

空间复杂度是一个算法在运行时临时占用存储空间大小,具体就是创建了几个变量。

也是使用大O渐进表示法

1.2 练习

 空间复杂度,主要看算法运行时所用到的临时变量个数,我们抓住这个点来做几道题。

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

计算一下BubbleSort函数的空间复杂度,我们上来就开始找在函数内部开辟的空间,size_end、size_i、Swap函数、exchange,我们发现里面创建了3个变量,调用了swap函数,因为以上的变量在循环中只创建了一次,所以只占用了3个空间,swap函数调用的时候会开辟函数栈帧,但是这个函数是个局部作用,swap函数再调用完之后就会销毁,而销毁之后的空间还可以再利用,因此这个函数所占用的空间是常数个空间,我们知道用大O渐进表示法时,里面是常数直接就是O(1),所以这个函数的空间复杂度是O(1)。

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

计算一下Fibonacci()函数的空间复杂度,这个是斐波那契数列的循环版,我们开始找函数内部开辟了多少空间,malloc开辟了n+1个空间,那后面不用看了,后面都是小弟远远低于n的影响力,所以这个函数的空间复杂度为O(N)。

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

计算Factorial()函数的空间复杂度,很明显上面这是一个递归,每一次递归都会创建一个size_t N,而且不会在递归的时候销毁,所以Factorial()函数的空间复杂度就是函数递归的深度,递归一次创建一个变量,所以空间复杂度就是O(N)。

 注意!

递归的空间复杂度一般就是递归的深度。

空间复杂度一般就是O(1)或者O(N)。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值