目录
我相信很多小伙伴在学习数据结构之前听到过时间复杂度和空间复杂度或者与其相关的词语,就觉得很难,听起来就很复杂,但时间复杂度和空间复杂度的“复杂”可不复杂哦!
时间复杂度
时间复杂度是一种用来衡量算法运行时间的度量。它描述了算法的运行时间随问题规模增长而增长的趋势。
通常使用大O记法来表示时间复杂度。大O记法表示算法运行时间的上界,即在最坏情况下算法的运行时间。常见的时间复杂度有:
- O(1):常数时间复杂度,表示算法的运行时间不随问题规模增长而增长,是最理想的情况。
- O(log n):对数时间复杂度,表示算法的运行时间随问题规模的增长而增长,但增长速度较慢。
- O(n):线性时间复杂度,表示算法的运行时间随问题规模的增长而线性增长。
- O(n^2):平方时间复杂度,表示算法的运行时间随问题规模的增长而平方增长,即算法的运行时间与问题规模的平方成正比。
- O(2^n):指数时间复杂度,表示算法的运行时间随问题规模的增长而指数增长,是最不理想的情况。
通过分析算法的时间复杂度,可以选择最优的算法来解决问题,以提高算法的效率和性能。
时间复杂度的计算方法
将算法的时间复杂度限制在O(N)的方法是使用线性扫描。也就是说,遍历输入的每个元素一次,并且在每个元素上执行常数时间的操作。以下是一个简单的例子:
假设我们有一个长度为N的整数数组A,我们想要计算数组中所有元素的和。
我们可以使用一个变量sum来记录和的值。我们初始化sum为0,并且对于数组中的每个元素A[i],我们将sum增加A[i]的值。
以下是使用O(N)时间复杂度的算法的示例代码:
int sum = 0; // 初始化和为0
for (int i = 0; i < N; i++) {
sum += A[i]; // 将数组中的每个元素累加到和中
}
该算法的时间复杂度是O(N),因为我们对数组中的每个元素执行了一次固定的操作(增加和的值)。
请注意,这个例子只是一个简单的说明,实际应用中的问题可能需要更复杂的操作。然而,使用线性扫描的基本思想是一样的:通过遍历输入的每个元素一次,并对每个元素执行常数时间的操作,我们可以使算法的时间复杂度保持在O(N)。
实际中在我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概的执行次数,那么我们在这使用大O的渐进表示法。
推导大O阶的方法:
1.用常数1取代运行时间中的所以加法常数。
比如下面这个代码:
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
--count;
}
printf("%d", count);
}
可以看出循环次数为100次,根据方法一我们最后可以确认时间复杂度为O(1)。
2.在修改后的运行次数函数里,只保留最高项
比如下面这个代码:
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N; k++)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d", count);
}
我们初步算出它的时间复杂度2*N+10,再结合我们的方法,我们必须考虑最坏的情况,当N无限增大时,10和*2的意义就不大了。所以这段代码最后的时间复杂度为O(N)。
3.如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
比如下面这个代码:
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(N+1)/2,结合方法三最后可以得出时间复杂度为N^2。
空间复杂度
空间复杂度是算法的一种度量,它表示算法执行所需的内存空间或存储空间的量度。对于一个算法,空间复杂度描述的是该算法所需的额外空间,不包括输入数据占用的空间。
空间复杂度可以通过以下几种方式进行度量:
-
常量空间复杂度(O(1)):表示算法所需的额外空间是一个常数,与输入数据的规模无关。例如,一个只使用固定数量的额外空间的算法,其空间复杂度为O(1)。
-
线性空间复杂度(O(n)):表示算法所需的额外空间与输入数据的规模n成正比。例如,一个需要使用一个数组来存储输入数据的算法,其空间复杂度为O(n)。
-
二维空间复杂度(O(n^2)):表示算法所需的额外空间与输入数据的规模n的平方成正比。例如,一个需要使用一个二维数组来存储两个输入数据的算法,其空间复杂度为O(n^2)。
-
递归空间复杂度:对于使用递归的算法,空间复杂度描述的是递归调用过程中所需的额外空间。递归空间复杂度的计算通常较为复杂,可以使用递归树模型来进行分析。
在进行算法分析和选择时,空间复杂度是一个重要的考虑因素。较低的空间复杂度意味着算法所需的额外空间较少,可以节省内存资源,并且在处理大规模数据时更加高效。
空间复杂度的计算方法
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没有太大意义,所以空间复杂度算的是变量的个数。空间复杂度的计算方法与时间复杂度的方法类似,也使用大O渐进表示法。
下面我举几个例子带你深入理解空间复杂度。
例子1:
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:用常数1取代运行时间中的所以加法常数。这个算法的空间复杂度就为O(1)。
例子2:
long long* Fibonacci(size_t n)
{
if (n == 0)
{
return NULL;
}
long long* fibonacci = (long long*)malloc(sizeof(long long));
fibonacci[0] = 0;
fibonacci[1] = 1;
for (int i = 2; i <= n; ++i)
{
fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2];
}
return fibonacci;
}
可以看出这是关于斐波拉契数列的算法,利用malloc开辟动态内存空间,那么数组里面元素的个数会达到N+1个,根据大O渐进法的方法2可以的出空间复杂度为O(N)。
总结与感悟
其实我学习数据结构之前也是对时间复杂度和空间复杂度有着很大的疑惑,刷题目的时候与时间复杂度和空间复杂度是离不开的,但学习过后才发现并不“复杂”。关于困扰我很久的问题也早已经解开,所以大家在日常的学习中,不要去害怕困难,需要去勇敢面对它,先行动,在一步一步优化,我相信结果会令你满意的!最近事情很匆忙,博客的发布需要耽搁几天啦。