【数据结构入门】——时间复杂度与空间复杂度

本文深入探讨了算法的时间复杂度和空间复杂度,介绍了大O的渐进表示法,通过实例分析了常数阶、线性阶、对数阶、平方阶等不同阶的时间复杂度,并讨论了常见的排序算法如冒泡排序、二分查找等的时间复杂度。同时,文章也提到了空间复杂度,举例说明了如何计算算法在运行过程中的临时空间占用。最后,提供了几个练习题帮助读者巩固所学知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

If you give someone a program, you will frustrate them for a day, If you teach them how to program, you will frustrate them for a lifetime.(如果你交给某人一个程序,你将折磨他一整天,如果你教某人如何编写程序,你将折磨他一辈子。)

前言:从今天开始正式进入数据结构的学习,在此之前先认识一下两个重要的名词:时间复杂度和空间复杂度。

1. 时间复杂度

1.1 时间复杂度的概念

在计算机科学中,算法的时间复杂度是一个函数(数学表达式),它定量描述了该算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

1.2 大O的渐进表示法

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

有些算法的时间复杂度存在最好、平均和最坏的情况:

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

在实际中一般情况关注的是算法的最坏运行情况

1.2.1 常数阶

思考一下,下面这个算法的时间复杂度为什么不是O(3),而是O(1)?

int sum = 0, n = 100;		//执行1次
sum =1 + n) * n / 2;	//执行1次
printf("%d", sum);		//执行1次

那个算法的运行次数函数是f(n)=3,根据我们的推导大 O 阶方法,应该将常数项改为1,所以时间复杂度为O(1)

  注:单纯的分支结构其时间复杂度也是O(1)。

1.2.2 线性阶

下面这段代码,它的循环的时间复杂度为O(n)

int i;
for(i = 0; i < n; i++{
	/* 时间复杂度为O(1)的程序步骤序列 */
}

1.2.3 对数阶

int count = 1;
while (count < n)
{
	count = count * 2;
	/* 时间复杂度为O(1)的程序步骤序列 */
}

由于每次count乘以2之后就距离n更近一分。即有多少个2相乘后大于n,则会退出循环。由2x =n得到x=log2n。所以这个循环的时间复杂度为O(logn)

1.2.4 平方阶

下面的例子是一个循环嵌套,它的内循环我们刚才已经分析过时间复杂度为O(n)

int i,j;
for(i = 0; i < n; i++{
	for(j = 0; j < n; j++{                                      
		/* 时间复杂度为O(1)的程序步骤序列 */
	}                                      
}

外层循环也是同理,于是时间复杂度为O(n2)

那么下面这段代码呢?

int i,j;
for(i = 0; i < n; i++{
	for(j = i; j < n; j++/* 注意j = i而不是0 */
	{                                      
		/* 时间复杂度为O(1)的程序步骤序列 */
	}                                      
}

由于i=0时,内循环执行了n次,当i=1时,执行了n-1次,……当i=n-1时,执行了1次。所以总的执行次数为:
n + ( n − 1 ) + ( n − 2 ) + . . . + 1 = {n+(n-1)+(n-2)+...+1=} n+(n1)+(n2)+...+1= n ( n + 1 ) 2 {n(n+1) \over 2} 2n(n+1)= n 2 2 {n^2\over 2} 2n2+ n 2 {n\over 2} 2n
用推导大O阶的方法,没有加法常数不考虑,只保留最高阶项,即 n 2 2 {n^2\over 2} 2n2,去除与该项相乘的系数,即去除 1 2 {1\over 2} 21,于是时间复杂度为 O(n2)

2. 常见的时间复杂度

执行次数函数非正式术语
12O(1)常数阶
2n+3O(n)线性阶
3n2+2n+1O(n2)平方阶
5log2n+20O(logn)对数阶
2n+3nlog2n+19O(nlogn)nlogn阶
6n3+2n2+3n+4O(n3)立方阶
2nO(2n)指数阶

注:经常把log2n简写成logn
在这里插入图片描述
所消耗的时间从小到大依次是:
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

3. 时间复杂度相关例子

3.1.1 实例1

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

对于 Func1 ,可以计算基本执行次数为 100 ,是一个常数,所以用大 O 的渐进表示法为 O(1)

3.1.2 实例2

// 计算strchr的时间复杂度?
const char* strchr(const char* str, int character);

strstr函数即在字符串中查找某个字符。时间复杂度为O(N)

3.1.3 实例3

// 计算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;
	}
}

冒泡排序最坏情况动画
可以看到,最好的情况就是执行 N 次(不是 1 次原因是即使不进行元素交换,此算法也会对数组遍历检查)。最差的情况就是计算 1、2、3……N-2、N-1 这个等差数列之和,即 ( N − 1 ) ∗ N 2 {(N-1)*N\over2} 2(N1)N ,那么要表示时间复杂度,就要用大 O 的渐进表示法,即去除影响不大的项,O(N2) 。

3.1.4 实例4

// 计算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;
}

二分查找动画
假设查找x次后还剩一个元素,则 1 = N 2 x 1={N\over2^x} 1=2xN 即x = log2n,最坏情况为O(logn),最好情况为O(1)。

3.1.5 实例5

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

对于函数递归,我们只需要关心函数被调用几次即可。那么对于求阶乘的递归算法来说,函数被调用了 N 次,所以时间复杂度为 O(N)

3.1.6 实例6

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

斐波那契数列图解

4. 空间复杂度

  • 空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时(额外)占用存储空间大小的量度。

  • 空间复杂度不是程序占用了多少字节空间,而是变量(空间)的个数。即使想要计算程序占用了多大空间也没有意义。

  • 空间复杂度计算规则与时间复杂度一样,都使用大 O 渐进表示法。

5. 空间复杂度的相关例子

5.1.1 实例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;
	}
}

因为所占空间数为常数,所以空间复杂度为 O(1)

5.1.2 实例2

// 计算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;
}

占用的空间与参数 n 有关,栈区上的空间是可以重复利用的。所以空间复杂度为 O(N)

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

Fac 函数被调用 N+1 次,每次调用都开辟一个栈帧,而这个栈帧又是常数。但是调用几次又参数 N 决定,所以空间复杂度为 O(N)

注:在刚才的计算中,可以发现我们并没有把形参的所占空间给算进去。这里需要注意的是,函数的形参是完成算法的条件,并不是额外开辟的一个临时空间。

6. 复杂度的相关练习

6.1.1 练习1

1、设某算法的递推公式是T(n)=T(n-1)+n,T(0)=1,则求该算法中第n项的时间复杂度为()
A.O(n)
B.O(n^2)
C.O(nlogn)
D.O(logn)

解析:T(n) = T(n-1)+n =T(n-2)+(n-1)+n
=T(n-3)+(n-2)+(n-1)+n

=T(0)+1+2+…+(n-2)+(n-1)+n
=1+1+2+…+(n-2)+(n-1)+n

从递推公式中可以看到,第n项的值等于1到n的累加值,需要遍历n个元素
所以时间复杂度为n,选A。

6.1.2 练习2

2、消失的数字
题解:

int missingNumber(int* nums, int numsSize)
{
    int sum=0;
    for(int i=0;i<=numsSize;i++)
    {
        sum+=i;//求出 0~n 范围的和
    }
    int tmp=0;
    for(int i=0;i<numsSize;i++)
    {
        tmp+=nums[i];//求出数组和
    }
    return sum-tmp;
}

6.1.3 练习3

3、轮转数组

//三步翻转法
void reverse(int* nums, int left ,int right)
{
    while(left<right)
    {
        int tmp=nums[left];
        nums[left]=nums[right];
        nums[right]=tmp;

        left++;
        right--;
    }
}


void rotate(int* nums, int numsSize, int k)
{
    if(k>=numsSize)
    {
        k%=numsSize;
    }
    reverse(nums, 0, numsSize-k-1);
    reverse(nums, numsSize-k, numsSize-1);
    reverse(nums, 0, numsSize-1);
}

OK,以上就是本期知识点“复杂度”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟~
如果觉得收获满满,可以点点赞👍支持一下哟~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值