<数据结构>时间复杂度和空间复杂度以及常见复杂度题型详解

复杂度常见题型详解

学数据结构一定要画图!!!!学会画图!!!!!

通过这一篇博客《新手小白初学者初步了解数据结构第一篇》我们已经知道了如今已经不需要再特别关注一个算法的空间复杂度。因此本篇博客将取少量空间复杂度题型讲解,重点主要放在时间复杂度上。在看这个博客之前强烈建议大家先去看一下上面的博客,因为本篇博客会涉及很多知识点都来自于上篇,在这里不再多做讲述。

在讲复杂度之前,先预习一下推导大O阶渐近法。因为时间复杂度和空间复杂度都可以用。

推导大O阶

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

时间复杂度

// 请计算一下Func1基本操作执行了多少次?
void Func1(int N)
{
	int count = 0;
    //执行 2 * N
	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}
	int M = 10;
    //执行 10次
	while (M--)
	{
		++count;
	}
	printf("%d\n", count);
}

因此,上面的Func1总执行 T(n)=2 * N + 10

根据推导大O阶第三条,如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。

所以Func1时间复杂度为O(N)。

// 请计算一下Func2基本操作执行了多少次?
void Func2(int N)
{
	int count = 0;
    // 执行 N*N 次
    // 外for每执行一次,内for也会执行一次
	for (int i = 0; i < N; ++i)					
	{
		for (int j = 0; j < N; ++j)
		{
			++count;
		}
	}
    // 执行 2*N 次
	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}
	int M = 10;
    //执行 10次
	while (M--)
	{
		++count;
	}
	printf("%d\n", count);
}

因此,上面的Func2总执行 T(n)=N2 + 2 * N + 10

又因为我们之前讲过,像此类公式只需要保留n的最高次项,也就是保留n的次方数最大的那一项。因为随着n的增大,后面项的增长远远不及n的最高次项大,所以直接省略低次项,接着最高次项系数也要去掉。

所以Func2时间复杂度为O(N2)。

// 计算Func3的时间复杂度
void Func3(int N, int M)
{
	int count = 0;
    // 执行 M 次
	for (int k = 0; k < M; ++k)
	{
		++count;
	}
    // 执行 N 次
	for (int k = 0; k < N; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}

上面的Func3时间复杂度为 O(M+N)。

为什么不是O(1)呢?因为M和N都是未知数

假设给了条件: M远大于N,那么Func3时间复杂度为 O(M),若M和N差不多大,那么Func3时间复杂度为 O(M) 或 O(N)

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

看过上一篇博客的,再不济看过本篇博客推导大O阶的已经知道Func4时间复杂度为 O(1)。

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

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

	return NULL;
}

这个编程是给了你一组字符串str,再给你一个指定的字符ch,让你在字符串中寻找指定的字符

例如我有一组字符串str,长度为11,如下图

很明显,如果我给的字符 ch = a 那时间复杂度就为 O(1)

如果我给的字符 ch = o 那时间复杂度就为 O(11)

那假设字符串长度为N呢?那最坏的情况是不是可以考虑到时间复杂度为O(N)呢

那如果我给的字符 ch = h 呢?那时间复杂度就是O(11/2)

同样,假设字符串长度为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;
	}

这个实际上是一个冒泡排序,要想知道冒泡排序的时间复杂度就要先搞清楚什么是冒泡排序?

相邻两个数两两相比较,前一个数字大于后一个数字则交换,否则进入下一组比较,以此类推,直到比较完毕。

R-C

第一次冒泡 :N

第二次冒泡 :N-1

第三次冒泡 :N-2

…….

第一次冒泡 :1

以此来看,冒泡排序实际也是一个等差数列,而等差数列公式:(首项 + 尾项) * 项数 / 2,因此冒泡排序次数为 (N + 1) * N / 2

所以,冒泡排序的最坏时间复杂度为 O(N2)。

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int low = 0;
	int high = n;
	while (low < high)
	{
		int mid = low + ((high - low) >> 1);
		if (a[mid] < x)
			low = mid + 1;
		else if (a[mid] > x)
			high = mid;
		else
			return mid;
	}
	return -1;
}

这个实际上是一个二分查找,要想知道二分查找的时间复杂度就要先搞清楚什么是二分查找?

静态查找表的关键字有序时,可以用二分查找来实现

二分查找也称折半查找,它是一种效率较高的查找方法。

key 值与中间元素比较,相等则成功;当 key 大则比较右半边,key 小则比较左半边。

分别用 low 和 high 来表示当前查找区间的下界和上界, mid 为区间的中间位置

image-20221028220634627

由此可见,二分查找最好的情况就是时间复杂度为 O(1)。

那最坏的情况下呢?譬如上图,假设找了 X 次,就是 1 * 2 * 2 * 2* 2 …= N,那就是2X = N,X=log2N,在算法中一般写成logN,因此,二分查找最坏情况下的时间复杂度为O(logN)。

// 计算阶乘递归Factorial的时间复杂度  N!

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

这个其实就是一个递归阶乘,N * ( N-1) * (N-2) * (N-3) * …… * 1

若 N = 5 时,如下图:

image-20221028231504325

通过计算分析发现基本操作递归了N次,时间复杂度为O(N)。

// 计算斐波那契递归Fib的时间复杂度
long long Fib(size_t N)
{
	if (N < 3)		//当N<3时,斐波那契数为1
		return 1;

	return Fib(N - 1) + Fib(N - 2);
}

在计算斐波那契递归Fib的时间复杂度之前,我们先了解一下什么是斐波那契数列:

查看源图像

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”。 斐波那契数列指的是这样一个数列: 0,1,1,2,3,5,8,13,21,34,55,89,144,……

这个就跟一个细胞分裂一样,是一个等比数列

image-20221029174940882

由此可见,斐波那契递归Fib的时间复杂度进行估算为 O(2N)。

空间复杂度

时间复杂度是累计的,空间复杂度不是累计

// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)	//1
	{
		int exchange = 0;				   //2
		for (size_t i = 1; i < end; ++i)	//3
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);	//前一个数字和后一个数字相互比较
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}

空间复杂度为 O(1)

这个开辟了三个空间,用大O渐进表示法就是O(1)。

//计算Fibonacci的空间复杂度?(循环)
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)											//1
{
	if (n == 0)
		return NULL;
//									O(N+6)
	long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));	//2
	fibArray[0] = 0;												    //3
	fibArray[1] = 1;													//4
	for (int i = 2; i <= n; ++i)										 //5
	{
		fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
	}
	return fibArray;
}

根据大O渐进表示法,Fibonacci的空间复杂度为O(N)

// 计算阶乘递归Fac的空间复杂度
long long Fac(size_t N)
{
    if(N == 0)
        return 1;
    return Fac(N-1)*N;
}

image-20221029174914578

每次调用都开辟一段空间,然后开辟N个空间,所以空间复杂度为O(N)

这里值得注意的是返回的时候会销毁栈帧。

到此为止了,再强调一遍**学数据结构一定要画图!!!!学会画图!!!!!**

image-20221029174529688

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

艾莜薇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值