时间复杂度主要衡量一个算法的运行快慢,空间复杂度主要衡量一个算法运行所需要的额外空间。下面举一个例子:
有一组数据,a胖想使用快速排序法对这组数据进行排序,而b瘦想使用冒泡排序法对其进行排序,最后二者对其进行运行时间的比较,最后发现a胖的快排法效率更高,那么如果想要进行算法的比较,还需要把代码写出来,效率就太慢了,有没有一种快捷方式进行算法的比较呢?当然有,那就是进行复杂度的计算。
所以,接下来介绍的时间复杂度和空间复杂度就是用来衡量算法的好坏。
时间复杂度(时间效率)的计算方法
首先来看下面一段代码
// 请计算一下Func1中++count语句总共执行了多少次?
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 < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
}
很容易知道这段代码执行了n*n+2*n+10次,但是这是一个函数的形式,如果代码再复杂一点,这个函数表达式就会更复杂,那么如果 n 趋近于无限大,那么这个函数表达式就可以把除了n^2以外的部分略掉不看。
进而引出了一种估算方法:大O的渐进表示法。大O符号(Big O notation)是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数(当n趋近于无限大,任何常数不会影响结果)。
2、在修改后的运行次数函数中,只保留最高阶项(其他项对结果影响不大)。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐近表示法,func1的时间复杂度为O(N^2)。
预期管理的概念及举例说明
接下来由一个例子引出预期管理的概念:a胖和b瘦是男女朋友,今天是情人节,a胖想约b瘦去看电影,a胖不加班的情况下六点可以去接b瘦看电影,但是公司今天可能会加班,公司加班a胖七点才能接b瘦,在这种情况下 a 胖和 b 瘦约会时间是几点合适呢?
当然是选择最坏的一种情况,也就是七点。算法也会出现这种情况,有些算法会出现最好和最坏以及平均等情况,此时考虑最坏的一种情况,这就叫做预期管理。
const char * strchr ( const char * str, int character );
如一个长度为N数组中搜索一个数据x ,最好的情况:1次找到,最坏的情况:N次找到,平均情况:N/2次找到。这里的时间复杂度为O(N),以最坏的情况为准。
二分查找的时间复杂度计算
二分查找的思想,假设一个数组有N个数,先在中间找一次,如果想要查找的值比中间值小,则在左边区间继续进行中间查找,如果还小,就继续取左边区间的中间值,以此类推。最坏的情况就是最后区间只有一个值。也就是最后一次查找 N/2/2/2.../2 = 1,可以总结规律:每次查找,区间缩小一半(N/2),查找多少次,就除多少次2。
假设查找x次得到最后只有一个值的区间(对应最坏的情况),此时 N = 1 * 2 * 2 * ... *2 = 2 ^ x。x = log2(N)---以二为底 N 的对数。
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;
}
阶乘递归的时间复杂度
long long Fac(size_t N)
{
if(1 == N)
return 1;
return Fac(N-1)*N;
}
上述是一个递归调用代码,Fac(N)需要调用Fac(N-1),而Fac(N-1)需要调用Fac(N-2),以此类推,最后一共调用了N次,每次调用进行两次运算,一次是if判断,一次是最后相乘。所以最后一共是O(2N),而2N可以将常数系数以及常数省略,最后一共是O(N)次。
递归时间复杂度计算方法和技巧:
每次递归调用的执行次数进行累加,一次递归调用可以看为是常数次,调用多少次就是多少个常数累加。
斐波那契数列的时间复杂度计算
空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。 空间复杂度不是程序占用了多少bytes的空间,而是变量的个数。 空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。也就是算额外开辟的空间,创建栈帧或者在堆区额外开辟的空间。
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
计算上述代码的空间复杂度,由于需要创建一个k和count实现算法。而参数N不是函数运行时新开辟的空间,所以空间复杂度为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;
}
}
再来看上面的冒泡排序,由于数组a是在实现排序算法前就已经开辟好了的,所以不算,在函数实现过程中,新创建的变量为 exchange、i、end 三个。所以空间复杂度为O(1)。
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;
}
看上述代码,Fibonacci函数里面有一个malloc数组开辟了n+1个空间,加上 fibArray 和 i 一共是 n+3 个,所以最终空间复杂度为O(N)。空间复杂度一般情况为O(1) 或 O(N)。
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
上述为递归函数,他的空间复杂度解释如下图所示:
普通函数算的是一次调用里面的变量,而递归每进行一次就会开辟一个栈帧,一个栈桢里面会开辟新的变量,所以最终递归的结果就是新开辟栈帧里面变量的累加和。即最终空间复杂度为O(n)。
复杂度练习
数组nums
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?(提供尽可能多的思路,用复杂度去分析最终实现最优解)
思路1:求和相减,n (1 + n ) / 2 - (a [0] + a [1] + ... + a [n]),最终得到的结果是缺少的数字。
int missingNumber(int* nums, int numsSize)
{
int N = numsSize;
int ret = N * (N+1) / 2;
for(int i = 0; i < N; ++i)
{
ret -= nums[i]
}
return ret;
}
该方法的时间复杂度为:O(n)
该方法的空间复杂度为:O(1)
思路2:qsort 排序
该方法的时间复杂度为:O(n * logn)
该方法的空间复杂度为:O(log n)
思路3:异或
利用异或的特点:
1. 0 ^ A = A
2. 0 ^ A ^ A = 0
将 0 与数组中每个元素以及0 ~ n分别进行异或计算 ;
由于性质2,可得结果 = 0 ^ 缺失数字 = 缺失数字(性质1)
int missingNumber(int* nums, int numsSize)
{
int N = numsSize;
int x = 0; //0和任何数异或还是任何数
//0 ^ nums[0] ^ nums[1] ^ nums[2] ^ nums[3] ^ ... ^ nums[N]
//0和数组里面所有元素进行异或操作,这个数组中少了我们要找的一个元素
for(size_t = 0; i < numsSize; ++i)
{
x ^= nums[i];
}
//再次对 0 ~ N 进行异或,最终找到缺少的元素
//类似于单身狗问题,缺少的元素只出现一次,其余元素均出现两次,最终结果为缺失的值
for(size_t j = 0; j < N+1; ++j)
{
x ^= j;
}
}
该方法的时间复杂度为:O(n)
该方法的空间复杂度为:O(1)