在时间复杂度与空间复杂度的文章中,我们简要的讲述了这两者的概念。
时间复杂度
递归求解斐波那契额数列
今天我们来讲一个数据结构的类型——顺序表,在讲这个之前我们现在来看一道复杂度的题目:
long long Fib(size_t N)
{
if (N > 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
上述的代码让我们来求使用递归形式编写的求解斐波那契额数列的时间复杂度与空间复杂度。
经过画图分析我们可以了解到Fib函数是像树状一样展开的:
简单地计算一下时间复杂度为:2^0+2^1+2^2+...+2^(N-2)=2^(N-1)-1。有一点我们需要注意的就是我们这里就计算的复杂度是最坏情况下的,因为实际上在上图的树状图中右下角是没有数列的,而我们在计算的时候将其全部当做有数来进行的计算,因此我们计算出的时间复杂度O(2^N)是偏大的。
上述使用递归实现的斐波那契额数列有着非常大的时间复杂度,简单地估算一下:
2^30 10亿
2^40 1w亿
2^50 1000w亿
假设我们运行40次需要5s,那么我们运行50次就需要5000s,这是一个非常恐怖的数字。因此我们需要一种不同的计算方法。
循环求解斐波那契额数列
long long Fib_(size_t N)
{
int num1 = 1;
int num2 = 1;
int num3 = 1;
int ret = 1;
if (N < 3)
return 1;
for (int i = 2; i < N; i++)
{
num3 = num1 + num2;
ret = num3;
num1 = num2;
num2 = num3;
}
return ret;
}
上述使用循环求解Fib函数的时间复杂度就大大减少,只有O(N)。
空间复杂度
下面我们再来看下一下递归求解Fib函数的空间复杂度,该函数的空间复杂度是O(N)。
虽然跟时间复杂度一样,都开辟了很多的函数帧栈但是这个空间是可以反复利用的,我们可以简单的举几个例子来看一下。
void func1()
{
int a = 0;
printf("%p\n", &a); //00B6F9D4
}
void func2()
{
int b = 0;
printf("%p\n", &b); //00B6F9D4
}
int main(void)
{
func1();
func2();
return 0;
}
从上述的代码中我们就可以看出函数的帧栈是可以重复利用的。
总结:时间是累积的一区不复返;
空间是可以重复利用的。
下面我们来介绍一道LeetCode上的题:旋转数组
思路1
旋转k次:
void rotate(int* nunms, int numsSize, int k)
{
while(k--)
{
int ret = nums[numsSize - 1];
for(int i = 0; i > 0; i--)
{
nums[i] = nums[i-1];
}
nums[0] = ret;
}
}
思路二
以空间换时间:
k = k % numsSize;
// 变长数组
int tmp[numsSize];
// 后k个拷贝到前面
int j = 0;
for(int i = numsSize-k; i < numsSize; ++i)
{
tmp[j] = nums[i];
++j;
}
//前n-k个拷贝到后面
for(int i = 0; i < numsSize-k; ++i)
{
tmp[j] = nums[i];
++j;
}
for(int i = 0; i < numsSize; ++i)
{
nums[i] = tmp[i];//将数组拷贝回原数组
}
思路三
该方法是大佬想出来的,目前放在文章中供大家参考:
void reservse(int*a, int begin, int end)
{
while (begin < end)
{
int tmp = a[begin];
a[begin] = a[end];
a[end] = tmp;
++begin;
--end;
}
}
void rotate(int* nums, int numsSize, int k)
{
k = k % numsSize;
reservse(nums, 0, numsSize - k - 1);
reservse(nums, numsSize - k, numsSize - 1);
reservse(nums, 0, numsSize - 1);