衡量算法效率的标准:时间复杂度和空间复杂度。
通俗地来讲,时间复杂度就是一个程序要被执行的次数,它是一个近似值,而不是执行的时间。空间复杂度,是程序执行过程中所占用的最大内存。
接下来通过两段代码来说明下如何计算一个程序的时间复杂度以及空间复杂度:
int SumMemory(int n) // 时间复杂度:共执行2n+5次,用大O表示法记为O(n);空间复杂度:4n+12字节,用大O表示法记为O(n)
{
int i = 0; // 时间复杂度:执行1次;空间复杂度:i为int型,占4个字节
int ret = 0; // 时间复杂度:执行1次;空间复杂度:4个字节
int * data = (int *)malloc(n * sizeof(int)); // 时间复杂度:执行1次;空间复杂度:4*n个字节
for (i = 0; i < n; i++) // 时间复杂度:循环n次,因此执行n次;空间复杂度:0,因为这部分内容是在CPU中运行,不占用内存
{
data[i] = i + 1;
}
for (i = 0; i < n; i++) // 时间复杂度:循环n次,因此执行n次;空间复杂度:0,因为这部分内容是在CPU中运行,不占用内存
{
ret += data[i];
}
free(data); // 时间复杂度:执行1次;空间复杂度:0,因为这部分内容是在CPU中运行,不占用内存
data = NULL; // 时间复杂度:执行1次;空间复杂度:4个字节
return ret;
}
这段代码一共执行了2n+5次,当n趋于无穷大时,5对于2n的影响少之又少,因此可以将其删掉,本质上来说,复杂度的计算是个近似运算,那么n的系数也可以置为1,只需比较n的幂次即可。
int SumLoop(int n) // 时间复杂度:共执行n+2次,用大O表示法记为O(n);空间复杂度:8个字节,用大O表示法记为O(1)
{
int i = 0; // 时间复杂度:执行1次;空间复杂度:i为int型,占4个字节
int ret = 0; // 时间复杂度:执行1次;空间复杂度:4个字节
for (i = 0; i < n; i++) // 时间复杂度:循环n次,因此执行n次;空间复杂度:0,因为这部分内容是在CPU中运行,不占用内存
{
ret += i;
}
return ret;
}
这段代码一共执行了n+2次,当n趋于无穷大时,2对于n的影响少之又少,因此可以将其删掉,本质上来说,复杂度的计算是个近似运算,只需比较n的幂次即可。
int SumFormula(int n) // 时间复杂度:执行2次,用大O表示法记为O(1);空间复杂度:4个字节,用大O表示法记为O(1)
{
int ret = 0; // 时间复杂度:执行1次;空间复杂度:4个字节
if (n > 0)
{
ret = (1 + n) * n / 2; // 时间复杂度:加减乘除运算只是一个运算指令,因此执行1次;空间复杂度:0,因为这部分内容是在CPU中运行,不占用内存
}
else
{
;
}
return ret;
}
这段代码一共执行了2次,本质上来说,复杂度的计算是个近似运算,将2看做1即可。
总结下推导O(n)的方法:
1.首先用常数1取代所有的加法常数;
2.只保留n的最高幂次;
3.若最高幂次存在且不为1,则将这个最高幂次的系数置为1即可。
最后分析下折半查找和菲波那切数列数列的时间复杂度以及空间复杂度:
a.折半(二分)查找
/*
* 函数名称:BinarySearch
*
* 函数功能:二分查找排好序的数组中的某个元素
*
* 入口参数:pArr, key, len
*
* 出口参数:mid
*
* 返回类型:int
*/
int BinarySearch(int * pArr, int key, int len) // 空间复杂度:12个字节,用大O表示法记为O(1)
{
int left = 0; // 空间复杂度:4个字节
int right = len - 1; // 空间复杂度:4个字节
int mid = 0; // 空间复杂度:4个字节
assert(NULL != pArr);
while (left <= right)
{
mid = (left & right) + ((left ^ right) >> 1);
if (pArr[mid] == key)
{
return mid;
}
else if (pArr[mid] > key)
{
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return -1;
}
时间复杂度的分析:假设有n个元素,经过第一次查找后,查找区间缩短为 n/2,经过第二次查找后,查找区间缩短为 n/4,……那么经过k次查找后,查找区间变为n/2^k(ps: n除上2的k次方)。不难发现,k其实就是循环次数,那么最终找到元素key时,n/2^k=1(ps: 这里等于1就表示找到了元素key),因此可以得出最坏情况下该算法的时间复杂度为O(log2n)。(ps: log2n表示以2为底,n的对数)
综上所述,折半(二分)查找的时间复杂度为O(log2n),空间复杂度为O(1)。
b.递归实现斐波那契数列
/*
* 函数名称:Fibonacci
*
* 函数功能:求第n个菲波那切数(递归)
*
* 入口参数:n
*
* 出口参数:1 or Fibonacci(n-1) + Fibonacci(n-2)
*
* 返回类型:int
*/
int Fibonacci(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return Fibonacci(n-1) + Fibonacci(n-2);
}
}
时间复杂度的分析:菲波那切数可以看做一个数结构,1分为2, 2分为4, 4分为8,...而要去求第n个斐波那契数,就需要被分解成2^n-1个数字,也就是需要执行2^n-1次,用大O表示法记为O(2^n)。
空间复杂度的分析:计算第n个菲波那切数,会调用n-2次Fibonacci()函数,每次调用都会开辟栈空间,那么就会占用4*(n-2)个字节,用大O表示法记为O(n)。
综上所述,递归实现菲波那切数列的时间复杂度为O(2^n),空间复杂度为O(n)。