如何计算时间复杂度:
-
非递归算法,通常是计算算法中循环的执行次数。那么就直接设该代码循环了k次 , 找出规模n与k的关系,得到k的级数趋近后的式子,既为时间复杂度。
-
2. 如果是递归算法,且只进行一次递归调用,有以一种方法是先求出深度depth,求出每一次执行的时间复杂度T ,总的时间复杂度就是depth * T(和用下面的方法原理是一样的。。)
-
如果递归比较复杂,那么套用递归算法的时间复杂度公式:T[n] = aT[n/b] + f(n) ,T [1] = O(1)。f(n)代表每次执行循环代码的复杂度。然后用迭代法或者公式法等这些方法来求解。
常见的算法时间复杂度分析:
O(1)< O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) <O(n!) < O(n^n)
一:O(1):
程序段的执行时间是一个与问题规模n无关的常数,也就是算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
1.1简单循环实现
//与n无关的赋值或者判断操作
sum=0;
//与输入规模n无关的有限循环
for(i=1;i<=C;i++){
}
二:O(logn) :
注:logn底数默认为2,因为对数之间是可以互相转换,大概意思就是无论底数是什么的对数函数都可以化简成一个常数C×以2为底数的对数函数,然后级数趋近就都变成了log2(n)。
2.1简单循环实现
for (int i = 1; i <= n; i = i * 2) {
语句1;
}
这里我们可以把求时间复杂度看成求这个循环内语句一执行了多少次的问题。(但时间复杂度是一个级数趋近的变化概念)
假设该程序一共循环了k次,即第k次循环就会退出(注意增加条件为i = i * 2)
第1次 ,i = 1
第2次 ,i = 1*2 = 2^1
第3次 ,i = 122 = 2^2
第4次 ,i = 122*2 = 2^3
第k次 (最后一次),i = 122*2…*2 = 2^(k-1)
第k次的时候,必然满足i <= n的条件,也即必然满足等式 2^(k-1) = n ,便反求得k = logn + 1 ,故O(n)= logn;
类似的乘法增长循环求时间复杂度都可以设循环次数k然后倒推出k,进而级数趋近变换得到时间复杂度。
2.2二分查找/折半查找
同样假设这个循环一共执行了k次,每一次循环都会将规模n除以2;
第1次,规模为n,
第2次,规模为n/2,
第3次,规模为n/4,
第k次,规模为n/[2^(k-1)],
最后结束的时候,n就只有1了无法再分,必然满足 n/(2^k) = 1,然后化简得 k = logn。于是时间复杂度为logn。
//二分查找
low = 0;
high = a.lenth() = n;
while(low<=high){
mid=(low+high)/2;
if(goal==a[mid]){
flag=1;
break;
}
if(guess>a[mid]){
low=mid+1;
}
if(guess<a[mid]){
high=mid-1;
}
}
2.3平衡二叉树查找
树的结点数为n
理解一:
同样假设执行了递归函数k次,每执行一次都会把一个子树及以下的树抛弃,相当于把这个问题规模n给除以了2,那么同样结束时候会有n / 2^k = 1然后化简得 k = logn。于是时间复杂度为logn。
理解二:
如果递归函数中,只进行一次递归调用,即主函数没有循环调用递归函数。且递归深度为depth,在每个递归函数中,时间复杂度为T,则总体的时间复杂度为O(T * depth)。这个也很好理解,就是执行次数 * 每次的时间 = 总的时间。
解释:这里由于是平衡二叉树的原因,递归的深度depth其实就是该树的深度,也就是 depth = (logn) +1 (二叉树性质),每次的时间复杂度为T = O(1),故总的时间复杂度depth * T 化简后也就是O(logn)。
理解三:
递归算法的时间复杂度公式 T[n] = aT[n/b] + f(n) ,f(n)为单次执行的时间复杂度。
这里T[n] = T [ n/2],迭代的得到第k次递归有T[n] = T [ n / (2^k)] ; 最后一次递归必有T[1] = T [ n / (2^k)] ,也即 n / (2^k )=1 。故有k = logn,所以时间复杂度为:O(log n )。
//二叉树查找
//树的结点为n
BiTree Search(BiTree T,ElemType e){
if(T==NULL){
//递归返回空值
return NULL;
}else if(e == T->data){
//递归返回查找到的指针
return T ;
}else{
//令一个结点指针等于左边结点
BiTree temp1= Search(T->lchild,e);
//令一个结点指针等于右边结点
BiTree temp2= Search(T->lchild,e);
return temp1==NULL?temp2:temp1;
}
}
三:O(n):
3.1简单循环实现
假设循环了k次 ,则满足k = n,故时间复杂度为O(n)。
for (int i = 1; i < n; i++) {
语句1;
}
3.2阶乘
理解一:阶乘递归的函数执行了n+1,每次执行的复杂度为O(1);总时间复杂度为O(n),同样用depth * T 的方法也可以计算出来O(n);
理解二:循环递归一共循环了n次,时间复杂度为O(n)。
//递归
Factorial(int n){
if(n==0)
return 1;
else
return Factorial(n-1)*n;
}
//循环
sum = 1;
while(i<=n){
sum*=i;
++i;
}
3.3 链表合并 (一道很奇怪的问题,有人说选B也有选D的)
已知两个长度分别为m 和 n 的升序链表,若将它们合并为一个长度为 m+n 的降序链表,则最坏情况下的时间复杂度是( D )。
-
A O(n)
-
B O(m+n)
-
C O(min(m ,n))
-
D O(max(m,n))
参考代码:
while(i<A.length && j<B.length){ //循环两两比较,小的存入结果
if(A.data[i]<B.data[j])
C.data[k++]=A.data[i++];
else
C.data[k++]=B.data[j++];
}
//再链接上没比较完的顺序表、
/*语句*/
我觉得答案确实应该偏向于O(max(m,n));因为我所理解的时间复杂度更类似一个级数的收敛化简,循环跳出的条件为一个链表的长度,那么最坏的情况应该是单纯的max(m,n)。尽管最坏情况下有可能执行了m + n次判断。两个答案的级数上都是线性级别O(n)。
四、O(nlogn): 排序中容易看到
4.1 简单循环
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j = j + i){
/*语句*/
}
}
理解:
当i=1时,内循环需要执行n次;
当i=2时,内循环需要执行n/2次;
当i=3时,内循环需要执行n/3次;
当i=n-1时,内循环需要执行n/n-1次;
当i=n时,内循环需要执行n/n次;
所以内循环总执行次数:1+1/2+1/3+……+1/n=lnn (高数下册上有写的。。级数那一章) ;
外循环为n次;
故总的次数为内外循环次数相乘为 n * lnn ,所以总的时间复杂度为n logn !
4.2堆排序
//堆排序
int arr[n+1] = {0,5,4,1,2,6,8,7,3}; // n = 8;
int main(){
//自下向上对非叶结点进行adjust
for(i = n / 2 ; i >= 1 ; i--){
//范围n没变 结点主动变化
HeapAdjust(i,n);
}
//自上向下对每一个结点进行adjust
for(i = n ; i >= 1 ; i--){
swap(1,i);
//范围因为断尾所以主动变化 结点1一直不变
HeapAdjust(1,i-1);
}
}
//对第i个结点进行到范围为n的调整堆排序
int HeapAdjust(int i,int n){
int temp = arr[i],j;
//j 为左子节点,j+1为右子节点
for(j = i*2 ; j <= n ; j=j*2){
//能进入则j表示没有超过当前范围n ,即左节点存在
if(j+1 <= n && arr[j]<arr[j+1]){
//能进入这里则表示 j+1没有超过当前未排序范围,即右节点存在
//j指向左右结点最大值
j++;
}
if(arr[j] > temp ){
//能进入这里表示子节点够代替父节点,
//隐含了交换父子结点的操作 arr[j] = temp;
//大的子节点变成父节点
arr[i] = arr[j];
//那么交换后还要进行对交换成子节点的原父亲结点堆调整 i=j
i = j;
//1使i指向的新的待调整堆顶 2实现了后最一步统一交换
}else{
break;
}
arr[i] = temp;
}
}
1. 从第一个非叶子结点开始到第一个结点进行堆调整
2. 堆调整:从当前结点的两个儿子结点中取出一个最大的与之比较:
大就交换;并把当前结点变成与之交换的儿子结点,再次进行堆调整。
小就退出返回第一步,进行下一个处理
3.然后把堆顶和堆尾交换,断尾,再次对新堆顶进行堆调整,直到只剩一个。
分析:
main函数里第一个for共循环了 n/2 次,O(n);
第二个for共循环了 n次,分离里面的Heap Adjust(1,n)函数单独分析:假设这个Heap Adjust的for循环共循环了k次,初试条件 j = 2 增加条件为j*=2,则满足2^k = n;这个for的时间复杂度为O(logn);则第二个for总的为O(nlogn).
所以T= O(n) + O(nlogn);故总的时间复杂度为 O(nlogn)。
补充:利用了二叉树的性质,堆排序在最好最坏情况下都是 nlogn。空间复杂度为O(1)。
4.3快速排序
//快速排序
int a[10] = {2,10,12,11,1,5,7,3,4,9};
int n = 10;
void fastSort(int low,int high){
//如果只有一个数,无须处理
//这里不能仅仅是等于;
//因为在边界处相遇的话,low会比high大1;
if (low >= high){
return ;
}
//随便拿哪个数开始当标准都可以 只要你的规则能依次拿完所有数
//这里每次执行代码会都会默认把第一个数当成轴
int left = low , right = high , temp = a[left];
while ( left != right){
while ( left != right && a[right] >= temp ){
right--;
}
a[left] = a[right];
while ( left != right && a[left] <= temp ){
left++;
}
a[right] = a[left];
}
// a[left] = temp; left和right都可以。
a[right] = temp;
fastSort(low,left-1);
fastSort(left + 1, high);
}
/*
1.传统每次都固定从第一个开始拿来当轴数,有可能发生最坏情况 ;
2.一种安全的做法:随机选取 ,尽量避免出现最坏情况;
int i = rand()%(left,right)+left;
swap(a[i],a[left]);
fastSort(left,right);
3.中值法(三值中值法);
*/
如何得到这个nlogn的平均时间复杂度?
理解一:
在最优的情况下,每次拿来当轴的数的正确位置都恰好在中间,能平分整个数组;每一次递归调用该函数都会把问题分为一半,这样一样就又成了一颗二叉树,结点数为规模n,其深度为logn+1,每一次调用该递归函数的代价,也就是进行找到轴数的正确位置所花费的时间为O(n),故 递归深度 * 每一次的代价 = n( logn + 1) ,故其复杂度为O(nlogn).
理解二:这里先利用公式套出它的递归时间复杂度公式:也就是
T[n] = O(1) ; n=1
T[n] = 2T[n/2] + n ; n>=1
解释:在最优的情况下,每次拿来当轴的数的正确位置都恰好平分整个数组;每一次执行代码会将数组分成两个规模为n/2的问题,并且还要加上找出正确位置所花费的时间,这里为线性时间复杂度O(n),n是实际传入的规模,所以为T[n] = 2T[n/2] + n !
用迭代法求解:
T[n] = 2T[n/2] + n //第一次递归
T[n] = 2*2 T[ n/ (2^2) ] + n+n //第二次递归
T[n] = 2 * 2 * 2 T[ n/ (222) ] +n+n+n //第三次递归
T[n] = 2^k T[ n/ (2^k) ] +n * k //第k次递归
当执行最后一次递归时候,必然有 T [n/ (2^k)] = T[1] = 1,也即 n / (2^k)=1 ,反解k,得到 k = logn, 然后反带入得T【n】= n + n *logn ;
又因为当n >= 2时,有nlogn >= n (也就是logn > 1),所以取后面的 nlogn;
综上所述所以为O(nlogn)!
补充:
最差的情况时间复杂度为O( n^2 )。怎么理解:假设每一次取到的元素就是数组中最小/最大的,然后相当于每一次都排好一个极端元素的顺序,没有进行规模划分,类比冒泡排序。
平均时间复杂为O(nlogn)。
递归时压入栈的数据占用的空间为O(logn),这是其空间复杂度。
4.4归并排序
//归并排序
int mergeSort(int left,int right,int temp[]){
/*好像写不等于也可以 */
if(left != right){
int mid = (left + right) / 2;
mergeSort(left,mid,temp);
mergeSort(mid+1,right,temp);
merge(left,mid,right,temp);
}
}
int merge(int left,int mid,int right,int temp[]){
/*连接重排 left-mid 和 mid+1 - right 的数组*/
int i = left; // 初始化i, 左边有序序列的初始索引
int j = mid + 1; //初始化j, 右边有序序列的初始索引
int t = 0; // 指向temp数组的当前索引
while(i <= mid && j <= right){
if(arr[i]<arr[j]){
temp[t++] = arr[i++];
}else{
temp[t++] = arr[j++];
}
}
while(i <= mid){
temp[t++] = arr[i++];
}
while(j <= right){
temp[t++] = arr[j++];
}
/*将部分排好的数组temp 复制粘贴给arr的相应部分*/
int tempLeft = left;
i = 0;
while(i < t){
arr[tempLeft++] = temp[i++];
}
}
理解一: 和最优条件下的快速排序分析方法类似,mergeSort () 函数内部会把该次执行的规模n划分为两个规模为n/2的问题进行递归,也是可以画出一颗二叉树,其深度为logn+1;每一次递归都会执行 merge函数,这个函数代价很容易得出来,为O(n)n为当次传入的问题规模,第一次为n,第二次为n/2…然后总的时间复杂度为:depth * f(n) = (logn + 1 ) * n = O(nlogn).
理解二:得到其递归时间复杂度公式和最好情况快速排序一样,分析基本无差,参考上面的快速排序。
T[n] = O(1) ; n=1
T[n] = 2T[n/2] + n ; n>=1
…
最差和平均条件下都是nlogn的时间复杂度,这里更应关心其空间复杂度—临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n)。
五:O(n^2):排序中容易看到
5.1简单循环
for(i=0;i<n;i++){
for(j=0;j<n;j++){
/**/
}
}
两层循环,时间复杂度:O(n^2);
5.2冒泡排序
//最外层可以是n-1层
for(i = 0 ; i < n-1; i++){
//内层需要记住 存在j+1那么j最多只能取到倒数第二位;且趟循环总是那次循环的最后一个数换到最值,选择排序则是那一趟的第一个数与最值下标k交换;
for(j = 0; j < n-(i+1) ; j++ ){
if(arr[j] > arr[j+1])
swap(j,j+1);
}
}
理解:外循环一共会执行n-1次,而内循环条件为 j < n - (i + 1) ,按照循环规律依次写处内循环的循环次数:
当i=1时,内循环需要执行n-1次;
当i=2时,内循环需要执行n-2次;
当i=3时,内循环需要执行n-3次;
当i=n-2时,内循环需要执行n - (n - 2 + 1 ) = 1次;此时循环结束;
所以循环总执行次数:1 + 2 + 3 + … + n-1 = n(n-1) / 2 ;
故时间复杂度为O(N^2);
最坏最好情况下时间复杂度都是 (N^2);如何理解?
最好的情况就是上面推导的过程,也就是没有进swap进行交换,所以每一次内循环的代价都是0;所以为n(n-1) / 2;
最差情况下,也就是刚好倒序或者顺序,那么跟最好情况的区别就是每一次内循环都执行了swap函数,代价增加了三条交换语句,所以总时间复杂度增大为 3 *n(n-1) / 2 ,但其级数趋近后的式子仍然是O(n^2)!
平均情况当然也为O(N^2)咯。
5.3插入排序
//直接插入排序
#define n 8
int arr[n] = {8,7,6,5,4,3,2,1};
for( i = 1 ; i < n ; i++){
temp = arr[i];
for(j = i - 1 ; j >= 0 ;j--){
//直接插入
if(arr[j] > temp){
arr[j + 1] = arr[j];
}else{
break;
}
}
arr[j + 1] = temp;
}
分析:
最坏情况下:
i = 1 ,内循环 1次,
i = 2 ,内循环2次
…
i = n - 1 ,内循环n-1次;
故总的循环:1 + 2 + 3 + … + (n-1) = n *(n-1)/ 2;
故时间复杂度为O(n^2)!
5.4选择排序
//同冒泡 最外层可以是n-1层
for(i = 0 ; i < n - 1 ; i++){
k = i;
//让第一个数去换最值下标k
for(j = i + 1 ; j < n ; j++){
if(arr[k] > arr[j]){
k = j ;
}
}
if(k != i)
swap(i,k);
}
同冒泡排序一样,外循环一共会执行n-1次,而内循环条件为 j < n - (i + 1) ,按照循环规律依次写处内循环的循环次数:
当i=1时,内循环需要执行n-1次;
…
当i=n-2时,内循环需要执行n - (n - 2 + 1 ) = 1次;此时循环结束;
所以循环总执行次数:1 + 2 + 3 + … + n-1 = n(n-1) / 2 ; 故时间复杂度为O(N^2);
六:O(2^n)
6.1斐波那契的递归算法
long Fibonacci(int n) {
if (n == 0)
return 0;
else if (n == 1)
return 1;
else
return Fibonacci(n - 1) + Fibonacci(n-2);
}
有点像细胞分裂,每一次进入Fibonacci函数,就会分裂成两个小Fibonacci函数,这个两个小Fibonacci函数又会分裂出4个小小Fibonacci函数,至少要进入n次,故其时间复杂度为O(2^n)。
待更新