算法效率
如何衡量一个算法的好坏?一般都是从时间复杂度和空间复杂度两个维度来衡量的。
时间复杂度主要衡量的是一个算法的运行快慢,而空间复杂度主要衡量的一个算法运行所需要的额外空间。
时间复杂度
时间复杂度的概念
时间复杂度的定义:算法中的基本操作的执行次数,为算法的时间复杂度。
就是找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
比如计算下面中++count语句总共被执行了多少次:
c
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;
}
printf("%d\n", count);
}
如果在数学中使用函数表达式来求这个语句的执行次数的话得到的结果如下:
在实际中我们计算时间复杂度的时间时,我们比不一定要计算精确的执行次数,而只需要大概执行次数,在表示上我们可以使用大O的渐进表示法。
所以可以这么理解大O的渐进表示法,就是抓大头,也就是取决定量级的那一项。
即上述问题的时间复杂度就是O(N^2)
大O的渐进表示法
- 用O(1)表示所有的常数时间复杂度。
- 只保留决定的量级的项。
- 取出确定量级项中的系数。
同时在时间复杂度中存在最好、平均和最坏的情况:
**最坏的情况:**任意输入规模的最大运行次数(上限)
**平均的情况:**任意输入规模的期望次数
**最好的情况:**任意输入规模的最小运行次数(下限)
但是在实际中一般关注的是算法中的最坏运行情况。
常见的时间复杂度举例
例1:
计算Func2的时间复杂度
c
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2*N; k++)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
上述代码精确的时间复杂度是Func(N)=2N+10,使用大O的渐进表示法的是O(N);
_因为决定量级的项就是2 N,同时还有除去系数那么结果就是O(N)_
例2:
计算Func3的时间复杂度
c
void Func3(int N,int M)
{
int count = 0;
for (int k = 0; k < N; k++)
{
count++;
}
for (int k = 0; k < M; k++)
{
count++;
}
printf("%d\n", count);
}
如果在没有前提条件下的话那么上述代码的时间复杂度就是O(M+N);如果说M远大于N那么时间复杂度就是O(M),反之就是O(N)。
例3:
计算Func4的时间复杂度
c
void Func4(int N)
{
int count = 0;
for (int i = 0; i < 100; i++)
{
++count;
}
printf("%d\n", count + N);
}
上述代码的时间复杂度是O(1),因为O(1)不是表示1次,而是表示常数次。
为什么常数次可以用1表示?
- 因为常数是整数类型所以常数的大小是受到类型的限制
- 因为cpu对于运算万亿级一下的次数都是非常快的
例4:
计算strchr的时间复杂度
c
const char * strchr ( const char * str, int character );
strchr是c语言中的一个库函数,它的作用就是在str字符数组中查找一个字符,它所使用的方法如下:
c
while(*str)
{
}
通过while循环来查找字符数组中的指定字符。
它有3种情况
- 最好的情况就是一次就找到了
- 找了差不多一半的次数
- 找到了最后一次
因为在实际中我们关注的是算法的最坏情况所以数组中搜索数据的时间复杂度是O(N)。
所以说时间复杂度的计算是一个保守稳健的预期。
例5:
计算BubbleSort的时间复杂度
c
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(N^2),这是冒泡排序其精确的时间复杂度是(N-1)*N/2,此数学表达式化解成大O的渐进表达式之后就是O(N^2)。
例6:
计算BinarySearch的时间复杂度
c
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) >> 1);
if (a[mid] < x)
begin = mid + 1;
else if (a[mid] > x)
end = mid - 1;
else
return mid;
}
return -1;
}
上述代码中的时间复杂度是O(logN)
在大O的渐进表达式中只有以2为底的才能用logN表示
例7:
计算阶乘递归Fac的时间复杂度
c
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1) * N;
}
上述代码中的时间复杂度是O(N)
总结:递归算法时间复杂度是多次调用的次数累加之和
例8:
计算斐波那契递归Fib的时间复杂度
c
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
上述代码中的时间复杂度是O(2^N)
思路1:排序+遍历(下一个数不等于下一个数据+1,这个下一个数就是消失的数字)
时间复杂度:O(logN*N)
c
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;
}
思路2:使用等差数列计算0到N相加的结果减去数组中的值,结果就是消失的数字
时间复杂度:O(N)
思路3:使用异或
空间复杂度
空间复杂度是对一个算法在运行过程中额外临时占用存储空间大小的量度。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没有太大的意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O的渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
例1:
计算Bubblesort的空间复杂度
c
void Bubblesort(int *a,int n)
{
assert(a);
for (size_t i = n; i > 0; i--)
{
int exchange = 0;
for (size_t j = 1; j < i; j++)
{
if (a[j-1]>a[j])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange==0)
{
break;
}
}
}
上述代码的空间复杂度是O(1),因为只是额外的创立了三个变量,根据 大O的渐进表示法可以得到空间复杂度O(1)。
例2:
计算Fibonacci的空间复杂度
c
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;
}
上述代码的空间复杂度是O(N),因为在代码中使用malloc函数创建了N+1个空间,所以它的空间复杂度是O(N)。
例3:
计算阶乘递归Fac的空间复杂度
c
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
上述代码的空间复杂度是O(N),解释如下:
从图可以看出一共创建了N+1个空间,所以得到的空间复杂度是O(N)。
例4:
计算斐波那契递归Fib的空间复杂度
c
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
上述代码的空间复杂度是O(N),解释如下:
注意:时间是可以累积叠加的,空间是可以重复利用的。
递归的层次太深,容易栈溢出。