01时间复杂度和空间复杂度

Posted 再吃一个橘子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了01时间复杂度和空间复杂度相关的知识,希望对你有一定的参考价值。

 目录

1.本章重点

2.什么是时间复杂度和空间复杂度

2.1算法效率

2.2时间复杂度的概念

2.3空间复杂度的概念

2.4复杂度计算在算法中的意义

3.如何计算常见算法的时间复杂度

 3.1大O的渐进表示法

 4.如何计算常见算法的空间复杂度

 5.有复杂度要求的算法题练习

5.1消失的数字

 5.2旋转数组


1.本章重点

  • 什么是时间复杂度和空间复杂度?
  • 如何计算常见算法的时间复杂度和空间复杂度?
  • 有复杂度要求的算法题练习
     

2.什么是时间复杂度和空间复杂度

2.1算法效率

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。

时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。
所以我们如今已经不需要再特别关注一个算法的空间复杂度。

2.2时间复杂度的概念

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

2.3空间复杂度的概念

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

2.4复杂度计算在算法中的意义

小总结

时间复杂度不算时间,算次数。

空间复杂度不算空间,算变量个数。

3.如何计算常见算法的时间复杂度

 3.1大O的渐进表示法

引子1:时间复杂度O(N^2)

// 计算Func1的时间复杂度?
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);
}

引子2:时间复杂度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);
}

引子3:时间复杂度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);
}

解释:因为我们不知道N和M谁的阶大,只能这样写。所以也可以得出个结论,大O表示法括号中的未知数不仅仅是1个,也可以是多个

引子4:时间复杂度O(1)

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

解释:用常数1取代运行时间中的所有加法常数。

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

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

推导大O阶方法

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


使用大O的渐进表示法以后,Func1的时间复杂度为:
N = 10     F(N) = 100
N = 100     F(N) = 10000
N = 1000      F(N) = 1000000

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数
另外有些算法的时间复杂度存在最好、平均和最坏情况:

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


例如:在一个长度为N数组中搜索一个数据x

  • 最好情况:1次找到      O(1)
  • 平均情况:N/2次找到   O(N/2)
  • 最坏情况:N次找到      O(N)

在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。
举个例子:(   取最悲观的预期   

时间复杂度O(N)

//计算strchr的时间复杂度?
const char* strchr(const char* str, char character)
{
	while (*str != '\\0')
	{
		if (*str == character)
			return str;
		++str;
	}
	return NULL;
}

解释

我们假设字符串是“defghsjhsq”,我们想找d,可能一下就找到了O(1);我们想找q,得需要遍历到这个字符串的最后才能找到;我们想找x,遍历完了还依旧是没有找到O(N)。而我们计算时间复杂度是采取最悲观预期的,所以会采用O(N),而不是O(1)。

O(1)和O(N)区别:

O(1)意味着:随着字符串变长,时间复杂度是不变的。

O(N)意味着:随着字符串变长,时间复杂度是线性增加的。

计算冒泡排序时间复杂度:时间复杂度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;
	}
	return 0;
}

 解释

 计算折半查找时间复杂度:时间复杂度O(log2N)(2是以2为底,可恶的Markdown打不出来)

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int begin = 0;
	int end = n;
	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;
}

 解释:(    悟!!  )

注意

算法的复杂度分析中,因为很多地方不好写底数,所以我们喜欢省略简写成logN( 表示以2为底数 ),咳咳,在此没有批评谁的意思。在很多书本中,或者网上资料会写成O(lgN)来表示以2为底,严格来说这样写是不对的,因为这和数学上相违背了(数学上表示以10为底数)。

计算阶乘的时间复杂度:时间复杂度O(N)

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

解释

常见的复杂度

O(1)     O(N)     O(N/2)     O(logN)

复杂度对比

这个图你是不是感觉咋好像少了一条线嘞?哈哈哈,是因为O(logN)和O(1) 基本重合了。什么原因呢?O(logN)是不是感觉怎么能和O(1)相比呢?

来,我们举个例子:

那么这样说的话,解决  查询中国14亿人口(已经排好序的) 中的一个人  的问题,(暴力解法需要14亿次),用折半查找需要的时间则很少很少,那么最多需要多少次呢?   31次

那么最多需要31次就可以查询到14亿人口中的一个人。

 4.如何计算常见算法的空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少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;
	}
}

解释

共有变量end ,   exchange  ,  i  ( 形参:n,a[ ]),所以空间复杂度是O(1)。

你可能会问:每调用一次,变量不都是又用了一次吗,不计算吗?

其实啊,时间是累积的,空间是不累计的,循环走了N次,重复利用的是一个空间

引子2:空间复杂度O(N)

// 计算Fibonacci的空间复杂度?
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;
}

 注释:(long long*)malloc((n + 1)意思是开辟了(n+1)空间的long long类型的数组。(      见知识     动态内存管理     )

解释

n ,  fibArray , fibArray[0]  , fibArray[1] , i还有n+1,所以准确的空间复杂度是n+6,大O表示法则是O(N)。

引子3:空间复杂度O(N)

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

 解释

那你可能会问,函数栈帧创建完会在递归返回去的时候,再一个一个销毁的呀!为啥是O(N)呢?

其实啊,我们也说了,要取的是最悲观的预期,在递归的时候,一个一个创建的空间都在同时使用,所以是O(N)。 

 5.有复杂度要求的算法题练习

5.1消失的数字

 我们想到的是有如下两种思路,再次我也不再赘述,我们来研究一下第三种思路叭~~

 思路3

int missingNumber(int* nums, int numsSize)
{
    int x = 0;
    //先跟数组中的值异或运算
    for(int i = 0; i < numsSize; i++)
    {
        x ^= nums[i];
    }

    //再跟[0,N]之间的数异或
    for(int j = 0 ; j < numsSize+1 ; j++)
    {
        x ^= j;
    }
    return x;
} 

 5.2旋转数组

思路1:循环K次

时间复杂度为O(N*K) 

void rotate(int* nums, int numsSize, int k)
{
    //时间复杂度为O(N*K)
    while(k--)
    {
        int temp = nums[numsSize - 1];
        for(int end = numsSize - 2 ; end >= 0 ; end--)
       {
             nums[end+1] = nums[end];
       }
        nums[0] = temp;
    }
    
}

 说明:时间复杂度太大。

思路2:空间换时间 

时间复杂度是O(N)

思路3(   天才想起来的,我是废狗!  )

 我们写出如下代码:

void Reverse(int* nums , int left , int right)
{
    while(left < right)
    {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        left++;
        right--;
    }
}

void rotate(int* nums, int numsSize, int k)
{
    Reverse(nums, numsSize-k, numsSize-1);
    Reverse(nums, 0, numsSize-k-1);
    Reverse(nums, 0, numsSize-1);
}

运行结果却失败了,原因是因为我们的numsSize-k若是小于0怎么办??left是负数下标??显然不符合题意,所以我们需要一个判断条件,当k大于nums的时候,循环一圈还能回来,举个栗子:nums = [1 , 2 , 3 ,4 , 5 ,6 , 7],k = 10次,那么即:移动10次,而元素一共7个,也就是饶了一圈又回来了,即:移动3次。

以上是关于01时间复杂度和空间复杂度的主要内容,如果未能解决你的问题,请参考以下文章

这段代码的时间和空间复杂度是多少?

01 | 复杂度分析(上):如何分析统计算法的执行效率和资源消耗?

01时间复杂度和空间复杂度

关于代码片段的时间复杂度

以下代码片段的时间复杂度是多少?

以下代码片段的算法复杂度