时间复杂度
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。
算法中的基本操作的执行次数,为算法的时间复杂度。
下面这个代码的执行次数是多少呢?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i) //执行了 N*N 次
{
for (int j = 0; j < N ; ++ j)
{
++count;
}
}
for (int k = 0; k < 2 * N ; ++ k) //执行了2N次
{
++count;
}
int M = 10; //执行了10次
while (M--)
{
++count;
}
printf("%d\n", count);
}
上述代码总执行次数是:F(N) = N^2 +2N +10次
N = 10 F(N) = 130
N = 100 F(N) = 10210
N = 1000 F(N) = 1002010
而当N越大,后面两项的影响就越小,所以在实际中只需要估算大概执行次数,一般使用的是大O渐进表示法;所以,由于F(N)的最后两项影响不大,只保留N^2,
使用大O渐进表示法后时间复杂度:O(N) = N^2;
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
1.若去掉N^2的代码,那时间复杂度会是多少呢?请看代码:
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k) //2N+M次
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
准确执行次数为:2N+10
当N越大,10对结果的影响就越小,而N无限大的时候 2N 和 N 是没区别的
时间复杂度:O(N)
在实际应用中,当存在N,且有常数项和N的常数系数时,一般省略常数
2.若执行次数是确定的呢?请看代码:
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
//O(1)
总执行次数是:100次
在实际中,对于确定的常数,用O(1)表示
O(1)表示的是,执行常数次
3.有两个未知数时时间复杂度是多少呢?
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++ k)
{
++count;
}
for (int k = 0; k < N ; ++ k)
{
++count;
}
printf("%d\n", count);
}
总执行次数是M+N次,当用大O表示法时有三种情况:
1.M和N不确定时,时间复杂度是 O(M+N)
2.当 M>>N时,时间复杂度是 O(M),反之则是O(N)
3.M和N接近相同时,O(M)或O(N)
上面几种情况推导了大O表示法的注意事项:
- 用常数1取代运行时间中的所有加法常数。(执行常数次用 O(1) )
- 在修改后的运行次数函数中,只保留最高阶项。(出现次方平方等,只保留最高阶)
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。(与未知数相乘的常数次舍去)
- 当有多个不确定的因素时,要考虑最坏、最好、平均的情况,在实际中一般情况关注的是算法的最坏运行情况
所以通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
练习1:冒泡排序的时间复杂度
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end) 每趟使 “每趟最大的数” 移动到后面,一共移动总数-1躺
{
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;
}
}
练习2:二分查找的时间复杂度
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;
else
return mid;
}
return -1;
}
可以正着推也可以反着推,看怎么好理解(可以对折折纸理解)
当数组是有序的时候,二分查找的速度是很快的:
N=1000 ,X大概是2^10 ——————2^10 = 1024
N=100W,20
N=10亿, 30
N=14亿, 31,接近20 亿
前提是二分查找是有序的,查找的时间复杂度很快
练习3:递归阶乘的时间复杂度
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
F(N) -> F(N-1)->F(N-2)…1
每次执行一次,一共执行N层,所以时间复杂度就是 O(N)
练习4:斐波那契递归的时间复杂度
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
实际求斐波那契时,递归是很慢的,可以用循环
求递归时间复杂度实际算的就是递归次数
上面几种时间复杂第中,O(N)最快,O(log2N)次之,O(N)大部分情况,O(2^N)很慢
空间复杂度
空间复杂度是对一个算法在运行过程中临时额外占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
冒泡排序的空间复杂度:
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;
}
}
只额外定义了int exchange、 size_t end 和 size_t i ,而 i 是执行完一层后销毁,在次执行时在创建,所以空间复杂度是 O(1)
递归阶乘的空间复杂度
// 计算阶乘递归Factorial的空间复杂度?
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
而且每层里也并没有申请额外变量,所以空间复杂度为:O(N)
斐波那契循环的空间复杂度
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 ;
}
开辟了N块空间:
malloc((n+1)开辟了N块,每块长度为sizeof(long long),并且没有额外的临时变量 所以是 O(N)
且时间长度也是N,循环中执行了 N-2次, 所以是 O(N)
斐波那契递归的空间复杂度
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
是O(N) 为什么呢?
程序在执行的时候先执行左边的 Fibonacci(size_t N),直到最底层也就是第N层之后就会返回,在执行右边的 Fibonacci(size_t N),这个过程中,先是在栈区申请内存,每层申请一块,返回时销毁,在执行右边时重复这个过程,实际就是 从第一层一直到第N层,每层申请一块内存,返回时系统收回,所以空间复杂度就是O(N)
所以不管执行多少次,实际就是递归的深度是多少,因为栈帧会随着创建随着销毁,最大就是到最低层创建之后销毁
栈帧的空间复杂度就取决于递归的深度
总结
大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数
时间复杂度:
- 用常数1取代运行时间中的所有加法常数。(执行常数次用 O(1) )
- 在修改后的运行次数函数中,只保留最高阶项。(出现次方平方等,只保留最高阶)
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。(与未知数相乘的常数次舍去)
- 当有多个不确定的因素时,要考虑最坏、最好、平均的情况,在实际中一般情况关注的是算法的最坏运行情况
空间复杂度:
1.空间复杂度是对一个算法在运行过程中临时额外占用的变量个数
2.由于栈帧的特性,递归函数的空间复杂度取决于深度,因为栈帧使用时创建使用完销毁(例如斐波那契的递归空间复杂度)