【数据结构初阶】算法的复杂度讲解

目录

数据结构前言 

1、什么是数据结构?

2、什么是算法?

算法的时间复杂度和空间复杂度

1、算法效率

2、时间复杂度

2.1 时间复杂度的概念

2.2 大O的渐进表示法

2.3 常见时间复杂度计算举例

3、空间复杂度

4、复杂度的oj练习


数据结构前言

1、什么是数据结构?

专业术语:数据结构是计算机存储组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。

自我理解:数据结构研究的是数据在计算机中如何存储,是计算机的底层逻辑,也是每一位程序员需要修炼的内功。

2、什么是算法?

如果说数据结构是数据的存储方式,那算法就是一系列的计算方法。一种算法定义了一个良好的计算过程,这个计算过程可以将一大批输入的数据转化为一批 “ 有迹可循 ” 的输出结果。

算法的时间复杂度和空间复杂度

1、算法效率

衡量一个算法的好坏,我们需要知道算法的效率,而算法效率可从两个维度来衡量:时间和空间,即时间复杂度和空间复杂度

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。

2、时间复杂度

2.1 时间复杂度的概念

一个程序在不同配置的计算机环境下运行的时间是不一样的,而程序运行时我们无法保证环境都一模一样,为了抛开运行环境的因素,我们关心的是程序运行的大概次数,而不是具体的多少秒。

因此,时间复杂度是指算法中基本操作的执行次数,它是一个函数(数学中的函数式,不是C语言中的函数),既然是大概执行次数,我们求的是函数的一个量级,即对结果影响最大的项。

2.2 大O的渐进表示法

大O符号:用于描述函数渐进行为的数学符号。

我们使用大O的渐进表示法来刻画大概执行次数。

推导大O阶方法:

  1. 只保留最高阶项
  2. 去掉最高阶项的系数
  3. 用常数1取代加法常数

注:加法常数指的是在算法分析中,执行次数固定不变的加法操作。即 O(1) 不是表示执行次数为1次,而是常数次。

例如:

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 < 3 * N; ++k)
	{
		++count;
	}

	int M = 100;
	while (M--)
	{
		++count;
	}

	printf("%d\n", count);
}

Func1执行的基本操作次数:F(N) = N^2 + 3*N + 100

Func1的时间复杂度为:O(N^2) 

另外,有些算法的时间复杂度存在最好平均最坏情况

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

最好情况:1次找到

最坏情况:N次找到

平均情况:N/2次找到

而实际中,我们关注更多的是算法的最坏运行情况,所以数组中搜索数据的时间复杂度为O(N)。

2.3 常见时间复杂度计算举例

实例1:

void Func2(int N)
{
	int count = 0;
	for (int k = 0; k < 10000 * N; ++k)
	{
		++count;
	}

	int M = 500000;
	while (M--)
	{
		++count;
	}

	printf("%d\n", count);
}

Func2执行的基本操作次数为:F(N) = 10000*N + 500000

首先,我们只保留最高阶项,常数为N的0次项,则保留N的1次项10000*N。

其次,我们要去掉最高阶项的系数,不管系数多大,在N为无穷大面前都不值一提。

所以,Func2的时间复杂度为:O(N)

实例2:

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

Func3执行的基本操作次数为:F(N) = M + N

两个未知数都应保留,Func3的时间复杂度为:O(M + N)

此题若设置前提:M >> N,则时间复杂度为O(M),反之为O(N)。

实例3:

void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 10000; ++k)
	{
		++count;
	}

	printf("%d\n", count);
}

Func4基本操作执行了10000次,为常数次,则Func4的时间复杂度为:O(1)。

实例4:

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

首先,最好的情况是有序,而程序不知道是否有序,也需要每个数字判断一遍,所以实例4基本操作最好情况执行N次

最坏的情况是完全逆序,第1个数字需交换N-1次,第2个数字交换N-2次......第N-1个数字交换1次,第N个数字交换0次,显然这是一个等差数列,总共有N个数字,求和得执行次数为(N*(N+1)) / 2 次,通过推导大O阶方法以及时间复杂度一般看最坏情况实例4的时间复杂度为:O(N^2)。

实例5:

int BinarySearch(int* a, int n, int x)
{
	assert(a);

	int begin = 0;
	int end = n - 1;
	
	while (begin <= end)
	{
		int mid = begin + (end - begin) / 2;
		if (a[mid] < x)
			begin = mid + 1;
		else if (a[mid] > x)
			end = mid - 1;
		else
			return mid;
	}

	return -1;
}

最好的情况是一次就找到,O(1);

最坏的情况是区间折半缩到只有一个值的时候,要么找到,要么找不到。假设折半了x次,折半查找了多少次就除了多少个2,则有N / 2^x = 1  ==>   N = 2^x  ==>  x = logN (logN在算法分析中表示以2为底,N的对数),所以实例5的时间复杂度为O(logN)

实例6:

//阶乘递归
long long Fac(siz_t N)
{
	if (0 == N)
		return 1;

	return Fac(N - 1) * N;
}

函数递归调用了N次,所以实例6的时间复杂度为O(N)

实例7:

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

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

3、空间复杂度

空间复杂度也是一个数学表达式,是指算法运行过程中临时额外占用的存储空间大小。但并不是计算程序占用了多少bytes的空间,而是计算变量的个数。计算规则和时间复杂度类似,也使用大O渐进表示法。

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时显式申请的额外空间来确定。

  • 显式申请的额外空间:除了上述在栈上自动分配的空间之外,有些算法和函数还需要在运行时动态地分配额外的空间。 例如:使用malloc函数在堆上分配内存,或者创建新的数据结构等。这类空间的分配通常取决于输入数据的大小或其他运行时条件,因此其大小在编译时是不确定的。
  • 空间复杂度的主要关注点:分析一个算法的空间复杂度是,我们主要关注的是这些显式申请的额外空间,因为它们的大小通常是随输入数据的变化而变化的。对于栈上的空间,由于它是固定的,因此在分析空间复杂度时通常不考虑这部分。 例如:考虑一个简单的递归函数,它只需要在栈上分配局部变量和参数的空间,但没有显式地分配额外的堆空间。在这种情况下,该函数的空间复杂度通常被认为是O(1),即与输入数据的大小无关。

实例1:

//计算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+1 个存放 long long 类型值的内存空间,空间复杂度为O(N)

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

实例2虽然没有动态分配内存空间,但是在栈上为定义的3个变量(end, exchange, i)分配了空间,即使用了常数个空间,所以空间复杂度为O(1)

实例3:

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

	return Fac(N - 1) * N;
}

每一次函数的调用都重新开辟了一块空间。递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)。

实例4:

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

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

实例4:时间复杂度为 O(2^n) ;空间复杂度为:O(N)。

ps:时间一去不复返,不可重复利用 !!

        空间用了以后归还,可以重复利用

4、复杂度的oj练习

练习1:消失的数字oj链接. - 力扣(LeetCode)

思路一:排序+二分查找 ==> 时间复杂度O(N*logN)

思路二:异或  ==>  O(N)

int missingNumber(int* nums, int numsSize){
    int val = 0;
    for(int i = 0; i < numsSize; ++i)
    {
        val ^= nums[i];
    }

    for(int i = 0; i <= numsSize; ++i)
    {
        val ^= i;
    }

    return val;
}

val 依次跟数组中的每个数字异或,再跟0到n之间的数字异或,除了消失的数字只出现一次,所有的数字都出现两次。

思路三:公式计算 :0-n求和 - 数组中的值  ==>  O(N)

int missingNumber(int* nums, int numsSize) {
	int sum = numsSize * (numsSize + 1) / 2;
	for (int i = 0; i < numsSize; ++i)
	{
		sum -= nums[i];
	}

	return sum;
}

练习2:轮转数组oj链接. - 力扣(LeetCode)

思路一:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

思路二:

输入: nums = [1,2,3,4,5,6,7], k = 3

输出: [5,6,7,1,2,3,4]

前n-k个逆置:4 3 2 1 5 6 7

后k个逆置:    4 3 2 1 7 6 5

整体逆置:      5 6 7 1 2 3 4

void reverse(int* nums, int begin, int end)
{
	while (begin < end)
	{
		int tmp = nums[begin];
		nums[begin] = nums[end];
		nums[end] = tmp;

		++begin;
		--end;
	}
}

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

时间复杂度:O(N)

空间复杂度:O(1)

思路三:

使用额外的数组,拷贝过去

void rotate(int* nums, int numsSize, int k) {
	if (k > numsSize)
		k %= numsSize;
	int* tmp = (int*)malloc(sizeof(int) * numsSize);
	//拷贝后k个数字
	memcpy(tmp, nums + numsSize - k, sizeof(int) * k);
	//拷贝前numsSize-k个
	memcpy(tmp+k, nums, sizeof(int) * (numsSize-k));
	//拷贝到原数组
	memcpy(nums, tmp, sizeof(int) * numsSize);

	free(tmp);
	tmp = NULL;
}

时间复杂度:O(N)

空间复杂度:O(N)

写到最后的话:小编也是进修阶段,如果发现有错误或对你造成困扰的地方,欢迎指正和交流讨论,我们一起共同进步!如果你觉得还不错或对你有帮助的话,可以给小编一个鼓励的小心心呀~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值