时间复杂度计算(以for循环为例)
本文理论内容来自严蔚敏版《数据结构(C语言版 第2版)》
*本文仅为复习时的总结,描述不准确、过程不严谨之处,还请理解
一、算法的相关概念
首先复习一下算法的定义及5个重要特性
其次是算法的评价标准
可以看到 时间复杂度 属于算法评价标准中的高效性
二、时间复杂度的相关概念
下面介绍一些时间复杂度的相关概念
如果不考虑计算机的软硬件等环境因素
影响算法时间代价的最主要因素是问题规模(用非负整数 n 表示,
)
问题规模是算法求解问题输入量的多少,是问题大小的本质表示
比如输入2个数排序、3个数排序、4个数排序,问题规模(n) 逐渐增大,执行时间也逐渐增加
再来看执行时间
对 语句的执行时间 进行拆解:
语句的重复执行次数 称作 语句频度(用
表示)
算法中能使语句重复执行的结构通常是循环结构,所以本文以for循环为例
为了简化分析,假设 每条语句执行一次所需的时间 均是 单位时间
所以 语句的执行时间=语句频度×单位时间
同理 算法的执行时间=所有语句频度之和×单位时间 (即提取公因式 "单位时间" )
由于 单位时间 是定值
因此 算法的执行时间 与 所有语句频度之和 成正比
对于稍微复杂一些的算法,计算所有语句频度之和通常是比较困难的
因此,我们引入基本语句的概念,用于度量算法的工作量
基本语句指的是对算法的执行时间贡献最大的语句,它的语句频度 与 算法的执行时间 成正比
*为了方便描述,例题均以for循环为例,仅写出主要代码
如果不太了解for循环可以先看我的这篇文章——
《 for、while、do while循环的流程图表示及相应continue、break的流程图表示 》
for循环可以理解为
for( 循环变量初始化 ; 循环条件 ; 循环变量自增 )
{循环体
}
举例:for循环求1到100的和
for( i=1; i<=100; i++ )
{sum+=i;
}
对应关系
【例1】有以下代码,输入n代表问题规模(在后续例题中,此点将不再赘述),计算for循环体语句频度之和
int n,i,j,k;
int a=0,b1=0,b2=0,c1=0,c2=0,c3=0;
scanf("%d", &n);
for(i=1; i<=n; i++)
{a++;for(j=1; j<=n; j++){b1++;b2++;for(k=1; k<=n; k++){c1++;c2++;c3++;}}
}
在第一层for循环体中,只有1条语句:a++;
循环变量 i 从1到n,循环共执行
次
所以第一层for循环体的语句频度=循环体语句条数 x 循环执行次数= 
在第二层for循环体中,有2条语句:b1++; b2++;
循环变量 i 从1到n,循环变量 j 从1到n,循环共执行
次
所以第二层for循环体的语句频度=循环体语句条数 x 循环执行次数= 
在第三层for循环体中,有3条语句:c1++; c2++; c3++;
循环变量 i 从1到n,循环变量 j 从1到n,循环变量 k 从1到n,循环共执行
次
所以第三层for循环体的语句频度=循环体语句条数 x 循环执行次数= 
所以for循环体语句频度之和为 
当问题规模(n)趋于无穷大时
因为3是常数,所以
与
同阶(或称
的数量级为
)
时间复杂度
用 语句频度的数量级 来表示(大
表示法)
大
表示法在数学上的定义:
和
都是定义在正整数集合上的函数
若存在正的常数:
和
,对于所有
, 均满足不等式 
则记作 
该定义用
来渐近表示时间复杂度 
从图中可以看出时间复杂度
的增长至多趋向于
的增长
换句话说,大
表示法
给出的是时间复杂度的一个渐近上界 
【例1】的语句频度之和为
,数量级为
,则时间复杂度为 
可以看到语句频度中起主要作用的是 
对应第三层for循环体的3条语句:c1++; c2++; c3++;
则这3条语句是对算法的执行时间贡献最大的语句(即基本语句)
所以计算时间复杂度时,无需计算所有语句的语句频度,只需计算基本语句的语句频度,得出数量级即可
计算时间复杂度的步骤:
1.找出基本语句
2.计算基本语句的语句频度
3.省略系数和无关项,得出数量级,即为时间复杂度
三、求和式及其运算规则
在计算时间复杂度之前,需要先了解求和式 
如果不太了解求和式可以先看我的这篇文章——《 求和式及其运算规则 》
求和符号
(读作sigma)
求和下限
:代表求和变量
从1开始递增
求和上限
:代表递增到n结束
求和表达式
: 一般是通项公式(只不过是把通项下标符号从 n 换成 i 而已)
求和变量
从1递增到n,则求和表达式
从
变化到 
求和式整体即从
累加到
: 
如果学过for循环就很好理解了
for(i=1; i<=n; i++)
{sum+=a[i];
}
时 :当前项
,求和sum=
,i++自增到2
时 :当前项
,求和sum=
,i++自增到3
时 :当前项
,求和sum=
,i++自增到4
……
时 :当前项
,求和sum=
,i++自增到n
时 :当前项
,求和sum=
,i++自增到n+1
时:i<=n为假,循环结束
最终求和sum的值: 
求和式的数乘规则(提取常数):
(其中c为常数)
求和式的加法规则(减法同理):
求和式的分段规则:
(其中
)
求和式的上下限变换规则:
上述规则为简便起见,求和上下限都是从1到n
实际遇到的求和式上下限不一定是从1到n
有可能是从0到k,从m到n+1,从0到n-1-i,……
但是均适用上述规则
四、嵌套无关循环和嵌套相关循环
下面区分一下 嵌套无关循环 和 嵌套相关循环
(有点类似于SQL的 不相关子查询 和 相关子查询)
【例2】嵌套无关循环,内层循环条件与外层循环变量无关
int n,i,j;
int a=0;
scanf("%d", &n);
for(i=1; i<=n; i++)
{for(j=1; j<=n; j++){a++;}
}
可以看到内层循环条件是 j<=n,j 从1到n,执行n次
无论外层循环变量 i 取何值,每轮内层循环都会执行n次
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
解释一下,这里的语句频度是怎么列式出来的
嵌套无关循环,所以内外层循环互不干扰,语句频度独立计算
因为外层循环条件是 i<=n ,所以 i 能取的最大值(即求和上限)是:n
因为外层循环变量初始化是 i=1,所以 i 能取的最小值(即求和下限)是:1
(通常情况, 求和下限 直接照抄 循环变量初始化 即可,后续不再赘述)
而内层循环可以看作是外层循环体的1条基本语句,则求和表达式(通项公式)是1
所以外层循环的语句频度为
因为内层循环条件是 j<=n ,所以 j 能取的最大值(即求和上限)是:n
内层循环体只有1条基本语句:a++; 则求和表达式(通项公式)是1
所以内层循环的语句频度为
只有当内外层循环都结束时,算法才结束
(即算法结束需要分内层循环、外层循环两个步骤)
根据统计学的乘法原理,需要把二者相乘得
语句频度是
,数量级为
,则时间复杂度为 
其实【例1】就是嵌套无关循环,再把【例1】的代码贴过来
int n,i,j,k;
int a=0,b1=0,b2=0,c1=0,c2=0,c3=0;
scanf("%d", &n);
for(i=1; i<=n; i++)
{a++;for(j=1; j<=n; j++){b1++;b2++;for(k=1; k<=n; k++){c1++;c2++;c3++;}}
}
按现在的步骤重新计算时间复杂度
1.基本语句:
c1++;
c2++;
c3++;
2.基本语句的语句频度:
3.时间复杂度:
解释一下,这里的语句频度是怎么列式出来的
嵌套无关循环,所以内外层循环互不干扰,语句频度独立计算
将前两层循环与第三层循环分开计算:
因为内层循环可以看作是外层循环体的1条基本语句
所以前两层循环可以直接参考【例2】,列式为
基本语句是对算法的执行时间贡献最大的语句
所以基本语句是第三层循环体中的3条语句:c1++; c2++; c3++;
所以第三层循环的求和表达式(通项公式)是3,列式为
将其展开,其实就是n个3相加,结果为3n
或者根据求和式的数乘规则,将常数提出
提取常数可以理解为乘法分配律的逆运算
将a=1, b=1, c=3代入得
只不过现在不是2项,而是n项
根据统计学的乘法原理,将前两层求和式与第三层求和式相乘得语句频度
虽然语句频度是
,但省略系数后,数量级为
,则时间复杂度为 
【例3】嵌套相关循环,内层循环条件与外层循环变量相关
int n,i,j;
int a=0;
scanf("%d", &n);
for(i=1; i<=n; i++)
{for(j=1; j<=i; j++){a++;}
}
可以看到内层循环条件是 j<=i,j 从1到 i,执行 i 次
当外层循环变量 i 的值发生改变时,内层循环的执行次数也会随之改变
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
解释一下,这里的语句频度是怎么列式出来的
因为是嵌套相关循环,内外层循环相关,不能分开计算
因为外层循环条件是 i<=n ,所以 i 能取的最大值(即求和上限)是:n
因为内层循环条件是 j<=i ,所以 j 能取的最大值(即求和上限)是:i
二层循环体只有1条基本语句,所以求和表达式(通项公式)是1
二层循环(双重循环)计算语句频度需要写成双重求和
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
(这里只不过是把求和上限从n变成 i,与前面本质相同,就是 i 个1相加)
将括号内的求和式替换为计算结果,双重求和变为
注意,当求和变量
与 通项公式
的下标 是相同符号时,代表累加的项随求和变量进行变化(如下所示)
本题比较特殊,通项公式就是求和变量: 
求和变量 i 从 1 到 n,则通项公式也是从 1 到 n,求和式整体即从1累加到n
此时变为等差数列求和,代入等差数列求和公式
得到语句频度
将其展开得
省略系数和无关项后,数量级为
,则时间复杂度为 
五、常见的时间复杂度
下面介绍常见的时间复杂度
将各时间复杂度当作函数,里面的自变量n就是问题规模
因为问题规模(n)是描述问题大小的非负整数 
所以比较时间复杂度只需看第一象限
比较时间复杂度,通常按问题规模(n)趋于无穷大(
)来比较
所以常见时间复杂度关系如下:
但是上图为了看清每个函数的趋势,对自变量问题规模(n)的取值范围过小,导致误解
误解一:对
和
的增长速率产生误解
下面将
和
单独画出,并扩大取值范围
可以看到
和
这两个函数在第一象限有两个交点
一个交点在
区间内(如果看不清可以看第一张函数图)
另一个交点在
区间内
在第二次相交后,
的增长速率明显要比
快
所以当问题规模(n)趋于无穷大时:
误解二:对
和
的增长速率产生误解
在第一张函数图中这两个函数图像几乎重合
下面将
和
单独画出来,并扩大取值范围
可以看到
和
这两个函数在第一象限有两个交点(4,2)和(16,4)
在第二次相交后,
的增长速率明显要比
快
所以当问题规模(n)趋于无穷大时:
题目选项可能会把
写成幂次形式
,上式变为:
(一)常数阶
【例4.1】常数阶
int i,x=0,s=0;
for(i=0;i<1000;i++)
{x++;s+=x;
}
这个例子是我特意挑的
因为它的for循环从0开始,循环条件是<
而前面的for循环都是从1开始,循环条件是<=
计算时间复杂度
1.基本语句:
x++;
s+=x;
2.基本语句的语句频度:
3.时间复杂度:
因为循环条件是 i<1000 ,所以 i 能取的最大值(即求和上限)是:1000-1=999
0—999和1—1000在循环的执行次数没有区别,都是1000次
如果反应不过来,那就将0—999同时加1
相当于在数轴上将0—999整体向右移1个单位,得到1—1000
比如1—5就执行5次,1—200就执行200次,1—1000就执行1000次
for循环有2条基本语句,所以求和表达式(通项公式)是2,列式为
根据求和式的上下限变换规则
本例的通项公式是常数2,没有下标(或者说无论下标如何变化,通项不变)
所以只变换求和上下限
在变换求和上下限后,也可根据求和式的数乘规则,提出常数再计算
算出语句频度为2000(常数),数量级为
,则时间复杂度为 
以后只要见到语句频度是常数,不带问题规模(n),那么时间复杂度就是 
【例4.2】常数阶 
int x=90,y=100;
while(y>0)
{if(x>100) {x=x-10;y--;} else {x++;}
}
这道题挺唬人,一时间还不好算出来
但是如果你看出循环变量初始化和循环条件都是常数,不带问题规模(n)
那么语句频度肯定是常数,一秒即可得出时间复杂度为 
计算时间复杂度
1.基本语句:
x=x-10;
y--;
x++;
2.基本语句的语句频度:
3.时间复杂度:
如果仍要算出语句频度,就得代入看逻辑了
【第1轮】
x==90,不满足x>100,进else:x++(自增到91)
x==91,不满足x>100,进else:x++(自增到92)
……
x==101,满足x>100,进if:x=x-10(减到91); y--(自减到99)
【第2轮】
x==91,不满足x>100,进else:x++(自增到92)
……
x==101,满足x>100,进if:x=x-10(减到91); y--(自减到98)
【第3轮】
x==91,不满足x>100,进else:x++(自增到92)
……
x==101,满足x>100,进if:x=x-10(减到91); y--(自减到97)
……
……
……
【第100轮】
x==91,不满足x>100,进else:x++(自增到92)
……
x==101,满足x>100,进if:x=x-10(减到91); y--(自减到0)
以此类推,可以看到,除第1轮有x==90,其余轮次均是 x从91增到101,每轮循环执行11次
有人看到执行11次可能反应不过来,那就将91—101同时减90
相当于在数轴上将91—101整体向左移90个单位,得到1—11,这肯定是11次
注意,每轮最后x==101时,执行的不是else中的1条语句,而是if中的2条语句
所以每轮的语句频度要比循环执行次数多1,即 
语句频度与循环执行次数不一定相等,因为循环体可以有多条语句
可以看到,每轮最后都会让y自减:y--
因为while循环条件是 y>0,循环变量 y 能取的最小值(即求和下限)是1
所以 y从100减到1,循环共执行100轮
100轮,每轮执行11+1=12次,计算语句频度
看起来似乎是正确的,但是前面说了除第1轮有x==90,其余都没有
所以还得把x==90时,多执行的1条语句:x++; 也算上,才是最终的语句频度
不过这只是探究罢了,实际上不需要算出语句频度,只要看出是常数,时间复杂度就是 
(二)线性阶
【例5】线性阶 
int n,i,t,a[100];
scanf("%d", &n);
for(i=0; i<n/2; i++)
{ t=a[i]; a[i]=a[n-1-i]; a[n-1-i]=t;
}
计算时间复杂度
1.基本语句:
t=a[i];
a[i]=a[n-1-i];
a[n-1-i]=t;
2.基本语句的语句频度:
3.时间复杂度:
首先说明一下,
这个符号代表向下取整
向下取整,如果不是整数,则只保留整数部分,忽略小数部分
向下取整,如果是整数,则仍是自身
,向下取整得 3
,向下取整得16
,整数向下取整,仍是自身
因为循环条件是 
但n不确定是奇数还是偶数,所以
不能确定是小数还是整数
下面分情况讨论
当n为奇数时,设
(x是整数)
代入循环条件
,得
,即 
因为x是整数,所以整数 i 的最大值 
前面设
,反之 
将其代入
得
奇数情况最大值 
当n为偶数时,设
(x是整数)
代入循环条件
,得
,即 
因为x是整数,所以整数 i 的最大值 
前面设
,反之 
将其代入
得
偶数情况最大值 
两种情况的最大值表达式不一样,如何统一呢
前面提到的
向下取整就派上用场了
先假设,最大值公式可用向下取整统一为,奇数情况最大值: 
因为该式是从奇数情况推导出的,所以奇数肯定没问题
代入验证偶数情况,如果计算结果与偶数情况最大值相同,说明可以统一
当n为偶数时,设
(x是整数)
代入上式得
进一步化简得
解释一下,因为
是整数,所以
也是整数
所以向下取整时,只保留整数部分:
,忽略小数部分: 
前面设
,反之 
将其代入得
将计算结果与偶数情况最大值
进行对照
可以看到,两式均为
,则偶数情况也成立,所以可以统一为 
再假设,最大值公式可用向下取整统一为,偶数情况最大值: 
因为该式是从偶数情况推导出的,所以偶数肯定没问题
代入验证奇数情况,如果计算结果与奇数情况最大值相同,说明可以统一
当n为奇数时,设
(x是整数)
代入上式得
进一步化简得
因为
是整数,所以
也是整数
所以向下取整时,只保留整数部分:
,忽略小数部分: 
前面设
,反之 
将其代入得 
将计算结果与奇数情况最大值
进行对照
可以看到,两式一个为
,另一个为
,则奇数情况不成立,所以不能统一
将n=1到10的计算结果列成表格,也能看出本式不符合
最终统一为奇数情况最大值 
所以 i 的最大值(求和上限)表示为
循环变量 i 的变化区间是
如果看不出执行次数,则同时+1
相当于在数轴上将
整体向右移1个单位,得 
则循环实际执行次数为
(这是因为 i 从0开始,所以执行次数要多1)
for循环有3条基本语句,所以求和表达式(通项公式)是3,列式为
根据求和式的上下限变换规则
本例的通项公式是常数3,没有下标,所以只变换求和上下限
在变换求和上下限后,也可根据求和式的数乘规则,提出常数再计算
省略系数和无关项后,数量级为
,则时间复杂度为 
(三)平方阶
【例6.1】平方阶 
int n,i,j;
int a=0;
scanf("%d", &n);
for(i=0; i<n; i++)
{for(j=0; j<i; j++){a++;}
}
本例与【例3】嵌套相关循环 略有不同,循环变量 i , j 不是从1开始,而是从0开始;循环条件不是<=,而是<
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
与【例3】嵌套相关循环 同理,内外层循环相关,不能分开计算
因为外层循环条件是 i<n ,所以 i 能取的最大值(即求和上限)是:n-1
因为内层循环条件是 j<i ,所以 j 能取的最大值(即求和上限)是:i-1
二层循环体只有1条基本语句,所以求和表达式(通项公式)是1
二层循环(双重循环)计算语句频度需要写成双重求和
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
根据求和式上下限变换规则
将其展开得
将括号内的求和式替换为计算结果,双重求和式变为
这里比较特殊,通项公式就是求和变量: 
求和变量 i 从 0 到 n-1,则通项公式也是从 0 到 n-1,求和式整体即从0累加到n-1
此时变为等差数列求和,代入等差数列求和公式
得到语句频度
将其展开得
除了上述解法,其实还可以使用求和式的上下限变换规则
求和上下限变换(+1),通项下标也对应变换(-1)
这里比较特殊,因为通项公式
,所以变换为 
再使用求和式的加法规则(减法同理)进行拆分
同样可以得到语句频度
将其展开得
省略系数和无关项后,数量级为
,则时间复杂度为 
【例6.2】平方阶
——冒泡排序(最坏情况)
int n,i,j,t,a[100];
scanf("%d", &n);
for(i=0; i<n; i++)
{scanf("%d", &a[i]);
}
for(i=0; i<n-1; i++)
{for(j=0; j<n-1-i; j++){if(a[j]>a[j+1]){t=a[j];a[j]=a[j+1];a[j+1]=t;}}
}
for(i=0; i<n; i++)
{printf("%d ", a[i]);
}
计算时间复杂度
1.基本语句:
t=a[j];
a[j]=a[j+1];
a[j+1]=t;
2.基本语句的语句频度:
3.时间复杂度:
代码中有三个for循环
第1个是一层for循环,输入数据,执行n次
第2个是二层for循环,冒泡排序,外层循环执行n-1次,且内层循环次数与 i 相关,所以执行次数肯定>n
第3个是一层for循环,输出数据,执行n次
假设当前冒泡排序为最坏情况(逆序),每次循环都会交换变量
所以基本语句是二层for循环体中的3条交换语句,则求和表达式(通项公式)是 3
基本语句所在的二层for循环是嵌套相关循环,内外层循环相关,不能分开计算
因为外层循环条件是 i<n-1 ,所以 i 能取的最大值(即求和上限)是:(n-1)-1=n-2
因为内层循环条件是 j<n-1-i ,所以 j 能取的最大值(即求和上限)是:(n-1-i)-1=n-2-i
二层循环(双重循环)计算语句频度需要写成双重求和
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
根据求和式的上下限变换规则
将其展开得
(这里只不过是把求和上限变成 n-1-i,与前面本质相同,就是 n-1-i 个1相加)
将括号内的求和式替换为计算结果,双重求和变为
根据求和式的数乘规则,将常数提出
根据求和式的加法规则(减法同理),进行拆分
将两个求和式分别计算:
第一个求和式
根据求和式的上下限变换规则,通项公式(n-1)与求和变量 i 无关,可以看作常数,所以只变换求和上下限
将其展开得
第二个求和式
将其展开得
将两式结果代入得到语句频度
将其展开得
省略系数和无关项后,数量级为
,则时间复杂度为 
【例6.3】
int n,m,i,j,a[10][10];
scanf("%d%d", &n, &m);
for(i=0; i<n; i++)
{for(j=0; j<m; j++) {a[i][j]=i*m+j;}
}
注意,本例的问题规模由n和m共同决定
计算时间复杂度
1.基本语句:a[i][j]=i*m+j;
2.基本语句的语句频度:
3.时间复杂度:
嵌套无关循环,所以内外层循环互不干扰,语句频度独立计算
因为外层循环条件是 i<n ,所以 i 能取的最大值(即求和上限)是:n-1
而内层循环可以看作是外层循环体的1条基本语句,则求和表达式(通项公式)是1
所以外层循环的语句频度为(上下限变换规则)
因为内层循环条件是 j<m ,所以 j 能取的最大值(即求和上限)是:m-1
内层循环体只有1条基本语句:a[i][j]=i*m+j; 则求和表达式(通项公式)是1
所以内层循环的语句频度为(上下限变换规则)
只有当内外层循环都结束时,算法才结束
(即算法结束需要分内层循环、外层两循环个步骤)
根据统计学的乘法原理,需要把二者相乘得语句频度
因为问题规模由n和m共同决定,不能合并,所以数量级为
,时间复杂度为 
(四)立方阶
【例7.1】立方阶
int n,i,j,k,a=0;
scanf("%d", &n);
for(i=1; i<=n; i++)
{for(j=1; j<=n; j++){for(k=1; k<=j; k++){a++;}}
}
可以看到,第二层循环条件是 j<=n,与第一层循环变量 i 无关,j 从1到 n,执行 n 次
无论第一层循环变量 i 取何值,第二层循环都会执行n次
可以看到,第三层循环条件是 k<=j,与第二层循环变量 j 相关,k 从1到 j,执行 j 次
当第二层循环变量 j 的值发生改变时,第三层循环的执行次数也会随之改变
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
第一层是嵌套无关循环,语句频度独立计算
因为第一层循环条件是 i<=n ,所以 i 能取的最大值(即求和上限)是:n
内层循环可以看作是外层循环体的1条基本语句,所以求和表达式(通项公式)是1
所以第一层循环的求和式为
第二、三层是嵌套相关循环,不能分开计算
因为第二层循环条件是 j<=n ,所以 j 能取的最大值(即求和上限)是:n
因为第三层循环条件是 k<=j ,所以 k 能取的最大值(即求和上限)是:j
内层循环体只有1条基本语句,所以求和表达式(通项公式)是1
所以第二、三层循环的求和式为
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
将括号内的求和式替换为计算结果
将其展开得到
这里比较特殊,通项公式就是求和变量: 
求和变量 j 从 1 到 n,则通项公式也是从 1 到 n,求和式整体即从1累加到 n
此时变为等差数列求和,代入等差数列求和公式
得到
只有当第一层和第二、三层循环都结束时,算法才结束
(即算法结束需要分第一层循环和第二、三层循环两个步骤)
根据统计学的乘法原理,需要把二者相乘得语句频度
将其展开得
省略系数和无关项后,数量级为
,则时间复杂度为 
【例7.2】立方阶
int n,i,j,k,a=0;
scanf("%d", &n);
for(i=1; i<=n; i++)
{for(j=1; j<=i; j++){for(k=1; k<=j; k++){a++;}}
}
可以看到,第二层循环条件是 j<=i,与第一层循环变量 i 相关,j 从1到 i,执行 i 次
当第一层循环变量 i 的值发生改变时,第二层循环的执行次数也会随之改变
可以看到,第三层循环条件是 k<=j,与第二层循环变量 j 相关,k 从1到 j,执行 j 次
当第二层循环变量 j 的值发生改变时,第三层循环的执行次数也会随之改变
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
嵌套相关循环,一、二、三层循环相关,不能分开计算
因为第一层循环条件是 i<=n ,所以 i 能取的最大值(即求和上限)是:n
因为第二层循环条件是 j<=i ,所以 j 能取的最大值(即求和上限)是:i
因为第三层循环条件是 k<=j ,所以 k 能取的最大值(即求和上限)是:j
第三层循环体只有1条基本语句,所以求和表达式(通项公式)是1
三层循环(三重循环)计算语句频度需要写成三重求和
三重求和可以加括号以区分
可以类比复合函数,先计算括号内的
将括号内的求和式替换为计算结果,三重求和变为双重求和
双重求和也可以加括号以区分
可以类比复合函数,先计算括号内的
这里比较特殊,通项公式就是求和变量: 
求和变量 j 从 1 到 i,则通项公式也是从 1 到 i,求和式整体即从1累加到 i
此时变为等差数列求和,代入等差数列求和公式
得到
将括号内的求和式替换为计算结果,双重求和变为
将分子展开得
根据求和式的线性规则(数乘规则、加法规则)
对多项式求和就是对其中每一项分别应用求和运算
这两个求和式的值都是已知的
第一个求和式
如果想了解如何推导出
求和的值,可以看我的这篇文章:《 求和式及其运算规则 》
第二个求和式
将两式结果代入得到语句频度
将其展开得
省略系数和无关项后,数量级为
,则时间复杂度为 
(五)对数阶
【例8.1】对数阶
int n,i,a=0;
scanf("%d", &n);
for(i=1; i<=n; i*=2)
{a++;
}
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
因为本例的循环变量自增不是以往的 i++,而是 i*=2
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环变量 i 的变化规律,将其当作数列
循环变量初始化:i=1,则首项
循环变量自增:i*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 i 随着通项公式改变
所以最后一次循环(第x次循环)时
(注:此处及后续提到的 "最后一次循环" 均是指能进循环体的 "最后一次循环")
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时取对数
移项,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
现在就可以列求和式计算了
for循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
【例8.2】对数阶
int n,i,a=0;
scanf("%d", &n);
for(i=2; i<n; i*=2)
{a++;
}
本例是对【例8.1】稍作修改,循环变量初始化:i=2,循环条件:i<n
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
因为本例的循环变量自增不是以往的 i++,而是 i*=2
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环变量 i 的变化规律,将其当作数列
循环变量初始化:i=2,则首项
循环变量自增:i*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 i 随着通项公式改变
所以最后一次循环(第x次循环)时
又因为循环条件是 
不等式两边不都是整数表达式,不好计算最后一次循环的x值
所以不追求列等式,而是直接将
代入不等式,得
不等式两边同时取对数,得到循环执行次数x的范围
但是循环执行次数只能是整数,所以x的最大值是
到这你肯定很疑惑,这个最大值是怎么来的
首先说明一下,
这个符号代表向上取整
向上取整,如果不是整数,则让整数部分加1,忽略小数部分
向上取整,如果是整数,则仍是自身
,向上取整得 4
,向上取整得17
,整数向上取整,仍是自身
下面解释最大值如何得出
将对数
作为一个整体
设其整数部分为m,小数部分为f,则
当小数部分不存在时(即
),则 
因为
,将对数整体代换得 
前面已推导出x的范围为
,将上式代入得 
此时不等式两边都是整数,所以当小数部分不存在时,x的最大值 
当小数部分存在时(即
),则 
因为
,将对数整体代换得 
前面已推导出x的范围为
,不等式放缩得 
此时不等式两边都是整数,所以当小数部分存在时,x的最大值 
可以用
向上取整将两种情况统一起来
当小数部分不存在时(即
),前面整体代换已得 
m是整数,所以向上取整后仍为m,即 
代入小数部分不存在时,x的最大值 
当小数部分存在时(即
),前面整体代换已得 
m是整数,则m+1也是整数,所以向上取整后为m+1,即 
代入小数部分存在时,x的最大值 
所以最终统一为
如果设
,可得出以下结论:
x是整数,不等式关系为
,则整数x的最大值
这个可以作为普遍结论,因为推导的全程都是把
当作一个整体
顺便提一下,这个结论对于【例5】线性阶 同样适用
再把【例5】的代码贴过来
int n,i,t,a[100];
scanf("%d", &n);
for(i=0; i<n/2; i++)
{ t=a[i]; a[i]=a[n-1-i]; a[n-1-i]=t;
}
根据上面的结论:
x是整数,不等式关系为
,则整数x的最大值
代入【例5】:
i 是整数,不等式关系为
,则整数 i 的最大值 
所以【例5】的语句频度也可以表示为
省略系数后,数量级为
,所以时间复杂度仍为 
将n=1到10的计算结果列成表格,也能看出本式是符合的
回到本题,现在就可以列求和式计算了
for循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
【例8.3】对数阶
int n,i,a=0;
scanf("%d", &n);
for(i=2; i<=n; i*=3)
{a++;
}
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
因为本例的循环变量自增不是以往的 i++,而是 i*=3
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环变量 i 的变化规律,将其当作数列
循环变量初始化:i=2,则首项
循环变量自增:i*=3,则公比
,即等比数列
等比数列通项公式:
则循环变量 i 随着通项公式改变
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时除以2
等式两边同时取对数
根据对数的减法法则
等式右边可以拆分为
代入原式得
移项,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
现在就可以列求和式计算了
for循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
则语句频度为:
其中
是常数
省略系数和无关项后,数量级为
,则时间复杂度为 
或根据对数的换底公式
上面的对数可以改写为
则语句频度改写为
其中
是常数
省略系数和无关项后,数量级为
,则时间复杂度为 
因为可以换底,所以 任意底数(k)的对数 都可以转化为 "常数 × 底数为2的对数"
(注:n是自变量,k是常数)
因为计算时间复杂度会省略系数,所以对数阶的时间复杂度均相同(即底数可以省略)
(六)线性对数阶
【例9】线性对数阶
int n,i,j,a=0;
scanf("%d", &n);
for(i=1; i<=n; i++)
{for(j=1; j<=n; j*=2){a++;}
}
本例其实就是将【例2】嵌套无关循环 和【例8.1】对数阶 结合起来
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
嵌套无关循环,所以内外层循环互不干扰,语句频度独立计算
因为外层循环条件是 i<=n ,所以 i 能取的最大值(即求和上限)是:n
而内层循环可以看作是外层循环体的1条基本语句,则求和表达式(通项公式)是1
所以外层循环的语句频度为
因为内层循环变量自增不是以往的 j++,而是 j*=2
所以循环变量 j 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环变量 j 的变化规律,将其当作数列
循环变量初始化:j=1,则首项
循环变量自增:j*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 j 随着通项公式改变
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时取对数所以求和
移项,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
现在就可以列求和式计算了
内层循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
所以内层循环的语句频度为:
根据统计学乘法原理,两式相乘得
省略系数和无关项后,数量级为
,则时间复杂度为 
(七)根号阶
【例10.1】根号阶 
int n,i,a=0;
scanf("%d", &n);
for(i=1; i*i<=n; i++)
{a++;
}
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
因为本例的循环条件不是以往的 i<=n,而是 i*i<=n
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环条件 i*i 的变化规律,将其当作数列
循环变量初始化:i=1
循环变量自增:i++
将循环变量 i 的值依次代入循环条件 i*i,探寻数列规律
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时开平方,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
现在就可以列求和式计算了
for循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
【例10.2】根号阶 
int n,i,a=0;
scanf("%d", &n);
for(i=2; i*i<n; i++)
{a++;
}
本例是对【例10.1】稍作修改,循环变量初始化:i=2,循环条件:i*i<n
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
因为本例的循环条件不是以往的 i<n,而是 i*i<n
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环条件 i*i 的变化规律,将其当作数列
循环变量初始化:i=2
循环变量自增:i++
将循环变量 i 的值依次代入循环条件 i*i,探寻数列规律
所以最后一次循环(第x次循环)时
又因为循环条件是 
不等式两边不都是整数表达式,不好计算最后一次循环的x值
所以不追求列等式,而是直接将
代入不等式,得
不等式两边同时开平方
移项,得到循环执行次数x的范围
根据【例8.2】中的结论:
x是整数,不等式关系为
,则整数x的最大值
代入本题:
x是整数,不等式关系为
,则整数x的最大值 
现在就可以列求和式计算了
for循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
【例10.3】根号阶 
int n,i,a=0;
scanf("%d", &n);
for(i=1; i<=sqrt(n); i++)
{a++;
}
本例与【例10.1】 如出一辙
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:
3.时间复杂度:
本例的循环条件不是以往的 i<=n,而是 i<=sqrt(n)
sqrt是平方根( square root )的缩写,sqrt(n)即为
所以循环条件可写为 
按理说 i 能取的最大值应该是
但是
不一定是整数,所以需要向下取整 
则最后一次循环时 
现在就可以列求和式计算了
for循环只有1条基本语句,所以求和表达式(通项公式)是1
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
【例10.4】根号阶 ![O(\sqrt[3]{n})=O(n^{\frac{1}{3}})](https://latex.csdn.net/eq?O%28%5Csqrt%5B3%5D%7Bn%7D%29%3DO%28n%5E%7B%5Cfrac%7B1%7D%7B3%7D%7D%29)
int n,i,a=0;
scanf("%d", &n);
for(i=1; i*i*i<=n; i++)
{a++;
}
计算时间复杂度
1.基本语句:a++;
2.基本语句的语句频度:![\sum\limits_{x=1}^{\lfloor \sqrt[3]{n} \rfloor} 1=\overbrace{1+\cdots +1}^{\lfloor \sqrt[3]{n} \rfloor}=\lfloor \sqrt[3]{n} \rfloor](https://latex.csdn.net/eq?%5Csum%5Climits_%7Bx%3D1%7D%5E%7B%5Clfloor%20%5Csqrt%5B3%5D%7Bn%7D%20%5Crfloor%7D%201%3D%5Coverbrace%7B1+%5Ccdots%20+1%7D%5E%7B%5Clfloor%20%5Csqrt%5B3%5D%7Bn%7D%20%5Crfloor%7D%3D%5Clfloor%20%5Csqrt%5B3%5D%7Bn%7D%20%5Crfloor)
3.时间复杂度:![T(n)=O(\sqrt[3]{n})=O(n^{\frac{1}{3}})](https://latex.csdn.net/eq?T%28n%29%3DO%28%5Csqrt%5B3%5D%7Bn%7D%29%3DO%28n%5E%7B%5Cfrac%7B1%7D%7B3%7D%7D%29)
因为本例的循环条件不是以往的 i<=n,而是 i*i*i<=n
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环条件 i*i*i 的变化规律,将其当作数列
循环变量初始化:i=1
循环变量自增:i++
将循环变量 i 的值依次代入循环条件 i*i*i,探寻数列规律
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时开立方,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
现在就可以列求和式计算了
for循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 ![O(\sqrt[3]{n})=O(n^{\frac{1}{3}})](https://latex.csdn.net/eq?O%28%5Csqrt%5B3%5D%7Bn%7D%29%3DO%28n%5E%7B%5Cfrac%7B1%7D%7B3%7D%7D%29)
(八)指数阶
【例11.1】指数阶
int f(int n)
{if(n==1) return 1;else return f(n-1)+f(n-1);
}
计算时间复杂度
1.基本语句:
return 1;
return f(n-1)+f(n-1);
2.基本语句的语句频度:
3.时间复杂度:
递归函数无法按常规方式计算语句频度
但是可以转换思路,将递归过程模拟为二叉树
一个递归函数当作二叉树的一个节点
本例的函数比较特殊,f(n)刚好递归调用的是 两个f(n-1)
从二叉树的视角来看,就是该节点连接下一层的两个孩子
所以每个节点(不算叶子节点)都有两个孩子,最终得到满二叉树
例如:f(4) 的递归满二叉树(如下图)
每个节点(不算叶子节点)都有两个孩子,最终得到满二叉树
当递归到叶子节点 f(1) 时,n==1,返回1,不向下递归
再看函数体,无论进 if 还是进 else,都只会执行1条语句
所以递归函数内基本语句的语句频度是1
因为是满二叉树,且每个节点(即递归函数)的语句频度是1
所以 语句频度之和 = 满二叉树的总节点数
如果了解满二叉树的性质,可以直接得出总节点数为 
不了解也没关系,下面逐步推导
我们知道二叉树只有1个根节点
对于满二叉树,每个节点(不算叶子节点)都有两个孩子
那么上层节点数与下层节点数是2倍关系,将 {节点数} 当作数列
第1层:根节点,节点数为 
第2层:节点数为 
第3层:节点数为 
第4层:节点数为 
第5层:节点数为 
……
第n层:节点数为 
可以看出其为等比数列
首项
,公比
等比数列通项公式 
求总节点数就是把第1层到第n层的所有节点数相加
其实相当于对 {节点数} 这个数列求和
代入等比数列求和公式
(其中 n 既是问题规模,又是满二叉树的层数)
至此推导出语句频度之和 = 满二叉树的总节点数 
省略系数和无关项后,数量级为
,则时间复杂度为 
【例11.2】指数阶
int f(int n)
{if(n==1 || n==2) return 1;else return f(n-2)+f(n-2);
}
计算时间复杂度
1.基本语句:
return 1;
return f(n-2)+f(n-2);
2.基本语句的语句频度:
3.时间复杂度:
递归函数无法按常规方式计算语句频度
但是可以转换思路,将递归过程模拟为二叉树
一个递归函数当作二叉树的一个节点
本例与【例11.1】有所不同
【例11.1】的递归过程是
而本例f(n)递归调用的是两个f(n-2),缺少了f(n-1)这一层
所以这颗二叉树是间隔一层向下递归的(仍是满二叉树,但层数不是n)
说一下奇偶数的性质
奇数 - 偶数 = 奇数, 则 奇数n - 2 = 奇数
偶数 - 偶数 = 偶数, 则 偶数n - 2 = 偶数
由于间隔递归,且n不确定是奇数还是偶数
所以最终可能在奇数 f(1)返回,也可能在偶数 f(2)返回
下面分情况讨论
当n为奇数时,最终在奇数 f(1)返回
f(n)递归过程如下
从后往前看,当作数列
则为等差数列
首项
,公差 
等差数列通项公式 
代入最后一项
移项除以2求得m的值
数列从
到
共m项,则递归过程有m层
所以奇数情况满二叉树的层数为 
再看函数体,无论进 if 还是进 else,都只会执行1条语句
所以递归函数内基本语句的语句频度是1
因为是满二叉树,且每个节点(即递归函数)的语句频度是1
所以奇数情况语句频度之和 = 满二叉树的总节点数
当n为偶数时,最终在偶数 f(2)返回
f(n)递归过程如下
从后往前看,当作数列
则为等差数列
首项
,公差 
等差数列通项公式 
代入最后一项
除以2求得m的值
数列从
到
共m项,则递归过程有m层
所以偶数情况满二叉树的层数为 
再看函数体,无论进 if 还是进 else,都只会执行1条语句
所以递归函数内基本语句的语句频度是1
因为是满二叉树,且每个节点(即递归函数)的语句频度是1
所以偶数情况语句频度之和 = 满二叉树的总节点数
类比【例5】的推导,可以使用
向下取整将两式统一
先假设,总节点数公式可用向下取整统一为,奇数情况总节点数: 
因为该式是从奇数情况推导出的,所以奇数肯定没问题
代入验证偶数情况,如果计算结果与偶数情况总节点数相同,说明可以统一
当n为偶数时,设
(x是整数)
代入上式得
进一步化简得
因为向下取整时,只保留整数部分:
,忽略小数部分: 
前面设
,反之 
将其代入得 
将计算结果与偶数情况总节点数
进行对照
可以看到,两式均为
,则偶数情况也成立,所以可以统一为 
再假设,总节点数公式可用向下取整统一为,偶数情况总节点数: 
因为该式是从偶数情况推导出的,所以偶数肯定没问题
代入验证奇数情况,如果计算结果与奇数情况总节点数相同,说明可以统一
当n为奇数时,设
(x是整数)
代入上式得
进一步化简得
因为向下取整时,只保留整数部分:
,忽略小数部分: 
前面设
,反之 
将其代入得 
将计算结果与奇数情况总节点数
进行对照
可以看到,两式一个为
,另一个为
,则奇数情况不成立,所以不能统一
所以最终统一为:
语句频度之和 = 满二叉树的总节点数 
省略系数和无关项后,数量级为
,则时间复杂度为 
因为 
而
可以写成 
所以有人认为时间复杂度应该是
根据大
表示法在数学上的定义得
大
表示法
给出的是时间复杂度的一个渐近上界 
所以上述两种均可渐近表示时间复杂度
不过为了方便统一理解,我还是倾向于 
【例11.3】指数阶
——斐波那契数列
int f(int n)
{if(n==1 || n==2) return 1;else return f(n-1)+f(n-2);
}
计算时间复杂度
1.基本语句:
return 1;
return f(n-1)+f(n-2);
2.基本语句的语句频度:![\frac{2}{\sqrt{5}} [(\frac{1+\sqrt{5}}{2})^{n}-(\frac{1-\sqrt{5}}{2})^{n}]-1](https://latex.csdn.net/eq?%5Cfrac%7B2%7D%7B%5Csqrt%7B5%7D%7D%20%5B%28%5Cfrac%7B1+%5Csqrt%7B5%7D%7D%7B2%7D%29%5E%7Bn%7D-%28%5Cfrac%7B1-%5Csqrt%7B5%7D%7D%7B2%7D%29%5E%7Bn%7D%5D-1)
3.时间复杂度:
由于左孩子 f(n-1)与右孩子 f(n-2)不在同一层
即左子树连续递归,右子树间隔递归
每个节点(即递归函数)的语句频度是1
所以依然是语句频度之和 = 二叉树的总节点数
虽然总节点数不好定量计算,但是我们可以定性分析
【例11.1】
【例11.2】
【例11.3】
可以分析出
所以得到时间复杂度关系
前面已得出【例11.1】和【例11.2】的时间复杂度均为
,即
则时间复杂度关系变为(类似于夹逼定理)
所以定性分析,斐波那契数列的时间复杂度也是 
斐波那契数列递推公式: 
通过特征方程推导可得通项公式: ![F_{n}=\frac{1}{\sqrt{5}} [(\frac{1+\sqrt{5}}{2})^{n}-(\frac{1-\sqrt{5}}{2})^{n}]](https://latex.csdn.net/eq?F_%7Bn%7D%3D%5Cfrac%7B1%7D%7B%5Csqrt%7B5%7D%7D%20%5B%28%5Cfrac%7B1+%5Csqrt%7B5%7D%7D%7B2%7D%29%5E%7Bn%7D-%28%5Cfrac%7B1-%5Csqrt%7B5%7D%7D%7B2%7D%29%5E%7Bn%7D%5D)
实际上,总节点数满足递推公式:
(即算上子树的根节点)
通过特征方程推导可得通项公式: ![2F_{n}-1=\frac{2}{\sqrt{5}} [(\frac{1+\sqrt{5}}{2})^{n}-(\frac{1-\sqrt{5}}{2})^{n}]-1](https://latex.csdn.net/eq?2F_%7Bn%7D-1%3D%5Cfrac%7B2%7D%7B%5Csqrt%7B5%7D%7D%20%5B%28%5Cfrac%7B1+%5Csqrt%7B5%7D%7D%7B2%7D%29%5E%7Bn%7D-%28%5Cfrac%7B1-%5Csqrt%7B5%7D%7D%7B2%7D%29%5E%7Bn%7D%5D-1)
所以有人认为时间复杂度应该是 
根据大
表示法在数学上的定义得
大
表示法
给出的是时间复杂度的一个渐近上界 
所以上述两种均可渐近表示时间复杂度
不过为了方便统一理解,我还是倾向于 
六、考研408真题
【2011年 第 1 题】设 n 是描述问题规模的非负整数,下列程序段的时间复杂度是(A)
x=2;
while(x<n/2) x=2*x;
A.
B.
C.
D.
本题与【例8.2】对数阶 相似,只不过循环条件不同
计算时间复杂度
1.基本语句:x=2*x;
2.基本语句的语句频度:
3.时间复杂度:
while循环可以理解为
循环变量初始化
while( 循环条件 )
{循环体【其中包含循环变量自增】
}
本例比较特殊,因为循环体只有1条语句
所以 x=2*x,既是循环体,也是循环变量自增
所以循环变量 x 不能反映实际的循环执行次数
设循环的执行次数为 k 次
为探寻循环变量 x 的变化规律,将其当作数列
循环变量初始化:x=2,则首项
循环变量自增:x*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 x 随着通项公式改变
所以最后一次循环(第k次循环)时
又因为循环条件是 
不等式两边都不是整数表达式,不好计算最后一次循环的x值
所以不追求列等式,而是直接将
代入不等式,得
不等式两边同时乘2
不等式两边同时取对数
移项,得到循环执行次数k的范围
根据【例8.2】中的结论:
x是整数,不等式关系为
,则整数x的最大值
代入本题:
k是整数,不等式关系为
,则整数k的最大值 
现在就可以列求和式计算了
while循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 k 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
【2012年 第 1 题】求整数
阶乘的算法如下,其时间复杂度是 (B)
int fact(int n)
{if(n<=1) return 1;else return n*fact(n-1);
}
A.
B.
C.
D.
计算时间复杂度
1.基本语句:
return 1;
return n*fact(n-1);
2.基本语句的语句频度:
3.时间复杂度:
递归函数无法按常规方式计算语句频度
但是可以转换思路,将递归过程模拟为二叉树
一个递归函数当作二叉树的一个节点
函数f(n)递归调用的是f(n-1)
从二叉树的视角来看,就是该节点连接下一层的一个孩子
所以每个节点(不算叶子节点)都只有一个孩子,最终得到"单叉"的二叉树
其实这棵"单叉"的二叉树就是线性表(或称二叉树退化为线性表)
纵向可能看不出来是线性表,改成横向就一目了然
例如:f(4)的递归二叉树(退化为线性表)如下图
再看函数体,无论进 if 还是进 else,都只会执行1条语句
所以递归函数内基本语句的语句频度是1
又因为退化为线性表,所以总节点数就是n
所以 语句频度之和 = 线性表总节点数 
省略系数和无关项后,数量级为
,则时间复杂度为 
【2013年 第 1 题】已知两个长度分别为m和n的升序链表,若将它们合并为一个长度为m+n的降序链表,则最坏情况下的时间复杂度是(D)
A.
B.
C.
D.
升序改降序:只需在遍历时,用头插法将元素插入新链表
比如原链表为1、2、3、4、5,遍历时用头插法插入新链表,新链表变为5、4、3、2、1
这样做的好处是,遍历还是正常顺序,不需要倒着来
链表合并的最坏情况:两链表交替遍历
举例:链表1是:1、3、5、7、9,链表2是:2、4、6
那么遍历合并为新链表时,二者就会交替遍历:1、2、3、4、5、6
然后新链表将链表1的剩余部分:7、9 并入
链表1的长度m=5,链表2的长度n=3
那么整个合并过程会将
个数并入新链表
如果m,n没有给出具体值,那么并入过程是
个数并入
数量级为
,所以时间复杂度为 
如果两个链表长度中的最大值用
表示,则
相加得
所以最多有
个数合并,省略系数后,数量级为
,则
所以时间复杂度也可以看作是 
前面是举例分析得出的,也可以拿代码来分析
原题没有代码,这里给出一段链表合并的代码(以不带头节点的链表为例)
《头插法实现两升序链表合并为降序链表》
//头插法实现两升序链表合并为降序链表
ListNode* merge(ListNode* L1, ListNode* L2)
{ListNode *head = NULL; // 新链表的头指针ListNode *temp; //临时指针while (L1 != NULL && L2 != NULL){// 比较两个链表当前节点的值if (L1->val <= L2->val){//取出L1节点temp = L1;L1 = L1->next;// 头插法:将L1节点插入新链表头部temp->next = head;head = temp;}else{//取出L2节点temp = L2;L2 = L2->next;// 头插法:将L2节点插入新链表头部temp->next = head;head = temp;}}// 处理剩余节点(L1更长)while (L1 != NULL){//取出L1节点temp = L1;L1 = L1->next;// 头插法:将L1节点插入新链表头部temp->next = head;head = temp;}// 处理剩余节点(L2更长)while (L2 != NULL){//取出L2节点temp = L2;L2 = L2->next;// 头插法:将L2节点插入新链表头部temp->next = head;head = temp;}return head;
}
计算时间复杂度
1.基本语句:
temp = L1; //或temp = L2;
L1 = L1->next; //或L2 = L2->next;
temp->next = head;
head = temp;
2.基本语句的语句频度:
3.时间复杂度:
假设两链表长度m>n(反之同理)
则链表1的前n项与链表2的n项,交替并入新链表(第一个while循环)
链表1剩余的m-n项,直接并入新链表(后面两个while循环二选一)
由于并入操作都是4条语句,所以不用区分是链表1还是链表2
交替并入时的语句频度 
剩余并入时的语句频度 
所以语句频度之和 
省略系数后,数量级为
,所以时间复杂度为 
如果两个链表长度的最大值用
表示,则
相加得
所以最多有
个数合并,省略系数后,数量级为
,则
所以时间复杂度也可以看作是 
有人可能会疑惑,为什么剩余部分并入还要耗费时间,用指针直接连上剩余部分不就行了?
这就是不审题的结果,题目要求是:两升序链表合并为降序链表
如果直接将剩余部分连上,岂不是把一段升序链表并入降序链表,那结果就不对了
剩余部分并入之所以耗费时间,是因为要用头插法将这部分升序链表变为降序链表
(下面仅作探究,与本题无关)
引申一下,假如题目要求改为:两升序链表合并为升序链表
这就跟上面提出的思路一样了,两链表交替并入,最后连接剩余部分
《尾插法实现两升序链表合并为升序链表》
//尾插法实现两升序链表合并为升序链表
ListNode* merge(ListNode* L1, ListNode* L2)
{ListNode *head = NULL; // 新链表的头指针ListNode *tail = NULL; // 新链表的尾指针//确定第一个节点if (L1->val <= L2->val){head = L1;L1=L1->next;}else{head = L2;L2=L2->next;}tail = head;// 尾指针指向第一个节点while (L1 != NULL && L2 != NULL){// 比较两个链表当前节点的值if (L1->val <= L2->val){// 将L1节点接入链表尾部tail->next = L1;L1 = L1->next;}else{// 将L2节点接入链表尾部tail->next = L2;L2 = L2->next;}}// 连接剩余链表if (L1 != NULL){tail->next = L1;}else{tail->next = L2;}return head;
}
计算时间复杂度
1.基本语句:
tail->next = L1; //或tail->next = L2;
L1 = L1->next; //或L2 = L2->next;
2.基本语句的语句频度:
3.时间复杂度:
假设两链表长度m>n(反之同理)
因为不带头节点,所以需要确定第一个节点,将链表1的第一个节点并入新链表(if...else)
链表1的n项与链表2的n项,交替并入新链表(while循环)
用指针直接连接链表1的剩余部分(if...else)
由于并入操作都是2条语句,所以不用区分是链表1还是链表2
第一个节点并入时的语句频度 
交替并入时的语句频度 
指针连接剩余部分时的语句频度 
所以语句频度之和 
省略系数和无关项后,数量级为
,所以时间复杂度为 
因为前面假设两链表长度m>n
如果两个链表长度的最小值用
表示,则
所以时间复杂度也可以看作是 
假设两链表长度m>n,可以得出以下结论:
合并后顺序相反(升+升→降 或 降+降→升),则时间复杂度为 
合并后顺序相同(升+升→升 或 降+降→降),则时间复杂度为 
【2014年 第 1 题】下列程序段的时间复杂度是(C)
count=0;
for(k=1; k<=n; k*=2) for(j=1; j<=n; j++) count++;
A.
B.
C.
D.
本题与【例9】线性对数阶 如出一辙,只不过内外循环互换了
计算时间复杂度
1.基本语句:count++;
2.基本语句的语句频度:
3.时间复杂度:
嵌套无关循环,所以内外层循环互不干扰,语句频度独立计算
因为内层循环条件是 j<=n ,所以 j 能取的最大值(即求和上限)是:n
内层循环只有1条基本语句,所以求和表达式(通项公式)是1
所以内层循环的语句频度为
因为外层循环变量自增不是以往的 k++,而是 k*=2
所以循环变量 k 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环变量 k 的变化规律,将其当作数列
循环变量初始化:k=1,则首项
循环变量自增:k*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 k 随着通项公式改变
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时取对数
移项,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
现在就可以列求和式计算了
内层循环可以看作是外层循环体的1条基本语句,则求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
所以外层循环的语句频度为:
根据统计学乘法原理,两式相乘得语句频度
省略系数和无关项后,数量级为
,则时间复杂度为 
【2017年 第 1 题】下列函数的时间复杂度是(B)
int func(int n)
{ int i=0,sum=0; while(sum<n) sum+=++i; return i;
}
A.
B.
C.
D.
计算时间复杂度
1.基本语句:sum+=++i;
2.基本语句的语句频度:
3.时间复杂度:
因为本例的循环条件不是以往的 i<n,而是 sum<n
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
while循环体【包含循环变量自增】只有1条基本语句:sum+=++i;
前置自增表达式 ++i 有两个效果:
一是让 i 自增为 i+1,二是把 i+1 作为整个自增表达式的值
所以 sum+=++i; 相当于分两步:
先执行++i; 让 i 自增为 i+1,后执行sum+=(i+1); 进行求和
所以sum始终是对 i+1 进行求和
为探寻 i+1 的变化规律,将其当作数列
循环变量初始化:i=0,则 i+1=1,首项
循环变量自增:++i,公差
,即等差数列
等差数列 {i+1} 的通项公式:
则等差数列 {i+1} 随着通项公式改变
因为sum始终是对 i+1 进行求和
所以可以将其看作是等差数列 {i+1} 的前n项和
代入前n项和公式:
注意,本题是while循环,初次循环判断的是sum的初值0,而初值与 i+1 无关,所以单设一个 
后续sum对 i+1 求和,则sum随着前n项和公式改变 (循环共执行x次,首项是
,则末项是
)
所以最后一次循环(第x次循环)时
又因为循环条件是 
不等式两边不都是整数表达式,不好计算最后一次循环的x值
所以不追求列等式,而是直接将
代入不等式,得
不等式两边同时乘2
不等式左边展开得
移项,得一元二次不等式
则各项系数
(其中n是问题规模,
)
根据一元二次函数图像、方程、不等式的对应关系
因为
,所以函数图像开口向上
因为
,所以方程有两实数根
因为
,所以不等式的解集位于两根之间
将
代入求根公式,得出两根
所以x的取值范围为 
根据【例8.2】中的结论:
x是整数,不等式关系为
,则整数x的最大值
代入本题:
x是整数,不等式关系为
,则整数x的最大值 
现在就可以列求和式计算了
while循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
其实做选择题没必要这么精确
上面已列出关于循环条件的不等式
把x-1近似为x,并将整个式子看作等式
得出循环执行次数的近似值
则语句频度近似值为
则时间复杂度为
【2019年 第 1 题】设 n 是描述问题规模的非负整数,下列程序段的时间复杂度是(B)
x=0;
while(n>=(x+1)*(x+1)) x=x+1;
A.
B.
C.
D.
本题与【例10.1】根号阶 相似,只不过循环变量初始化和循环条件不同
计算时间复杂度
1.基本语句:x=x+1;
2.基本语句的语句频度:
3.时间复杂度:
因为本例的循环条件不是以往的 x<=n,而是 n>=(x+1)*(x+1)
但循环条件通常把循环变量表达式写在前面,如果反过来写,就是(x+1)*(x+1)<=n
所以循环变量 x 不能反映实际的循环执行次数
设循环的执行次数为 k 次
为探寻循环条件 (x+1)*(x+1) 的变化规律,将其当作数列
循环变量初始化:x=0
循环变量自增:x=x+1
将循环变量 x 的值依次代入循环条件 (x+1)*(x+1),探寻数列规律
所以最后一次循环(第k次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时开平方,得到循环执行次数k的值
但是循环执行次数只能是整数,所以k还要向下取整
现在就可以列求和式计算了
while循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 k 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
【2022年 第 1 题】下列程序段的时间复杂度是(B)
int sum=0;
for(int i=1; i<n; i*=2)for(int j=0; j<i; j++)sum++;
A.
B.
C.
D.
本题看着与【2014年 第 1 题】类似,但实际上不同
2014年是嵌套无关循环,而本题是 嵌套相关循环
计算时间复杂度
1.基本语句:sum++;
2.基本语句的语句频度:
3.时间复杂度:
因为是嵌套相关循环,内外层循环相关,不能分开计算
但是可以先列出来求和式的上下限,再多重求和
因为内层循环条件是 j<i ,所以 j 能取的最大值(即求和上限)是:i-1
所以内层循环的求和式上下限列为
因为外层循环变量自增不是以往的 i++,而是 i*=2
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环变量 i 的变化规律,将其当作数列
循环变量初始化:i=1,则首项
循环变量自增:i*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 i 随着通项公式改变
所以最后一次循环(第x次循环)时
又因为循环条件是 
不等式两边不都是整数表达式,不好计算最后一次循环的x值
所以不追求列等式,而是直接将
代入不等式,得
不等式两边同时取对数
移项,得到循环执行次数x的范围
根据【例8.2】中的结论:
x是整数,不等式关系为
,则整数x的最大值
代入本题:
x是整数,不等式关系为
,则整数x的最大值 
令求和变量 x 从1到
,代表循环执行
次
所以外层循环的求和式上下限为:
二层循环体只有1条基本语句,所以求和表达式(通项公式)是1
二层循环(双重循环)计算语句频度需要写成双重求和
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
将括号内的求和式替换为计算结果,双重求和变为
注意,此时求和变量 x 与求和表达式 i 是不同符号,直接展开无意义
所以需要把 i 替换为关于求和变量 x 的表达式
前面已推导出最后一次循环(第x次循环)时
把
替换
将其展开得
此时变为等比数列求和
首项
,公比 
代入等比数列求和公式
得到语句频度
解释一下,根据对数的定义可知
因为
与
差距很小,所以
同时减1得
省略系数和无关项后,数量级为
,则时间复杂度为 
【2023年 第 1 题】下列对顺序存储的有序表(长度为n)实现给定操作的算法中,平均时间复杂度为
的是(D)
A.查找包含指定值元素的算法
B.插入包含指定值元素的算法
C.删除第
个元素的算法
D.获取第
个元素的算法
题目给定的是顺序存储,则有序表使用数组实现
typedef struct
{int data[MAXSIZE]; // 存储元素的数组int n; // 当前表长
} SeqList;
有序表一般采用折半查找(二分查找)算法
A.查找
// A. 查找指定值元素——折半查找(二分查找)
int search(SeqList *list, int value)
{int low = 0, high = list->n-1,mid;while (low <= high){mid = (low + high)/2;if (list->data[mid] == value)return mid; // 找到返回下标else if (list->data[mid] < value)low = mid+1;elsehigh = mid-1;}return -1; // 未找到
}
计算时间复杂度
1.基本语句:
mid = (low + high) / 2;
return mid;
low = mid + 1;
high = mid - 1;
2.基本语句的语句频度:![2\times[\frac{n+1}{n}log_{2}(n+1)-1] \approx 2\times[\frac{n}{n}log_{2}(n+1)-1] \approx 2\times[log_{2}(n+1)-1]](https://latex.csdn.net/eq?2%5Ctimes%5B%5Cfrac%7Bn+1%7D%7Bn%7Dlog_%7B2%7D%28n+1%29-1%5D%20%5Capprox%202%5Ctimes%5B%5Cfrac%7Bn%7D%7Bn%7Dlog_%7B2%7D%28n+1%29-1%5D%20%5Capprox%202%5Ctimes%5Blog_%7B2%7D%28n+1%29-1%5D)
3.时间复杂度:
本函数有两种结束方式:
一是查找失败,最终low>high,while循环结束,函数返回 -1
二是查找成功,list->data[mid] == value,函数返回 mid
再看基本语句
while循环体中,mid = (low + high) / 2; 这1条语句是必做的
其他三条语句通过if……else if……else判断后,三选一
所以每轮循环体执行1+1=2条基本语句
语句频度=基本语句条数 x 循环执行次数
已经得出基本语句条数为2条,如果再知道 循环执行次数 就能算出语句频度
下面只探究查找成功情况下的循环执行次数
这里需要引入 平均查找长度 的概念
从定义来看,平均查找长度其实就是平均比较次数
而每次比较,对应到代码,就是循环体中的if……else if……else判断
所以每执行一次循环体,就是一次比较
则 循环执行次数=平均比较次数=平均查找长度
所以求循环执行次数 转为 求平均查找长度
推导过程比较复杂,这里直接给出结果,折半查找的平均查找长度= 
如果想了解推导过程可以看我的这篇文章——《 折半查找的平均查找长度公式推导 》
所以循环执行次数=折半查找的平均查找长度= 
语句频度=基本语句条数 x 循环执行次数=基本语句条数 x 平均查找长度
(注:因为对数有小括号,所以整个平均查找长度用中括号括住,并不是向上取整或向下取整)
解释一下,因为n+1与n差距很小,所以
代入得
省略系数和无关项后,数量级为
,则时间复杂度为 
所以查找过程的时间复杂度是
,不是 
B.插入
// B. 插入指定值元素(保持有序)
int insert(SeqList *list, int value)
{if (list->n >= MAXSIZE){printf("表已满,无法插入\n");return 0;}int i = list->n-1;// 从后向前找到插入位置while (i >= 0 && list->data[i] > value){list->data[i+1] = list->data[i]; // 元素后移i--;}list->data[i+1] = value; // 插入新元素list->n++; // 表长增加return 1; // 成功插入
}
计算时间复杂度
1.基本语句:
list->data[i + 1] = list->data[i];
i--;
2.基本语句的语句频度:
3.时间复杂度:
因为是在数组中插入,需要给待插入元素留空位
所以从后往前遍历,将比它大的每个元素都向后移动一格
最后将待插入元素插到空位中,并改变表长
举例:插入值为5的元素
以待插入数据在中间为例
表长为n,表尾下标为n-1,则中间位置为 
则向后移动的元素有 
基本语句是循环体中的2条语句
所以语句频度为 
省略系数和无关项后,数量级为
,则时间复杂度为 
所以插入过程的时间复杂度是
,不是 
C.删除
// C. 删除第i个元素(0≤i≤n-1)
int delete (SeqList *list, int i)
{if (i < 0 || i > list->n-1){printf("位置非法\n");return 0;}// 将第i+1个位置及以后的元素前移for (int j = i+1; j < list->n; j++){list->data[j-1] = list->data[j];}list->n--; // 表长减1return 1; // 成功删除
}
计算时间复杂度
1.基本语句:list->data[j - 1] = list->data[j];
2.基本语句的语句频度:
3.时间复杂度:
删除与插入如出一辙,只不过一个向前移动,一个向后移动
因为是在数组中删除,只需要将待删除元素的位置覆盖掉
所以从前往后遍历,将比它大的每个元素都向前移动一格,并改变表长
举例:删除a[4]位置的元素
以待删除数据在中间为例
表长为n,表尾下标为n-1,则中间位置为 
则向前移动的元素有 
基本语句是循环体中的1条语句
因为此处为for循环,循环变量自增不在循环体中,所以基本语句只有1条
所以语句频度为 
省略系数和无关项后,数量级为
,则时间复杂度为 
所以删除过程的时间复杂度是
,不是 
D.获取
// D. 获取第i个元素(0≤i≤n-1)
int get(SeqList *list, int i)
{if (i < 0 || i > list->n-1){printf("位置非法\n");return -1; // 返回-1表示错误(假设元素均为非负整数)}return list->data[i]; // 返回元素值
}
计算时间复杂度
1.基本语句:return list->data[i - 1];
2.基本语句的语句频度:
3.时间复杂度:
获取元素仅需1条return语句,所以语句频度为 
省略系数和无关项后,数量级为
,则时间复杂度为 
所以获取过程的时间复杂度就是 
【2025年 第 1 题】下列程序段的时间复杂度是(B)
int count=0;
for(int i=0; i*i<n; i++)for(int j=0; j<i; j++)count++;
A.
B.
C.
D.
本例类似于将【例3】嵌套相关循环 和【例10.2】根号阶 结合起来
计算时间复杂度
1.基本语句:count++;
2.基本语句的语句频度:
3.时间复杂度:
因为是嵌套相关循环,内外层循环相关,不能分开计算
但是可以先列出来求和式的上下限,再多重求和
因为内层循环条件是 j<i ,所以 j 能取的最大值(即求和上限)是:i-1
所以内层循环的求和式上下限为
因为外层循环条件不是以往的 i<n,而是 i*i<n
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环条件 i*i 的变化规律,将其当作数列
循环变量初始化:i=0
循环变量自增:i++
将循环变量 i 的值依次代入循环条件 i*i,探寻数列规律
所以最后一次循环(第x次循环)时
又因为循环条件是 
不等式两边不都是整数表达式,不好计算最后一次循环的x值
所以不追求列等式,而是直接将
代入不等式,得
不等式两边同时开平方
移项,得到循环执行次数x的范围
根据【例8.2】中的结论:
x是整数,不等式关系为
,则整数x的最大值
代入本题:
x是整数,不等式关系为
,则整数x的最大值 
令求和变量 x 从1到
,代表循环执行
次
所以外层循环的求和式上下限为:
二层循环体只有1条基本语句,所以求和表达式(通项公式)是1
二层循环(双重循环)计算语句频度需要写成双重求和
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
将括号内的求和式替换为计算结果,双重求和变为
注意,此时求和变量 x 与求和表达式 i 是不同符号,直接展开无意义
所以需要把 i 替换为关于求和变量 x 的表达式
前面已推导出最后一次循环(第x次循环)时
等式两边同时开平方
把
替换
将其展开得
此时变为等差数列求和
首项
,公差 
代入等差数列求和公式
得到语句频度
解释一下,因为
与
差距很小,所以
同时平方得
最终
省略系数和无关项后,数量级为
,则时间复杂度为 
七、变形题
(下面是我自己想出来的一些变形题)
【变形题1】下列程序段的时间复杂度是(B)
int count=0;
for(int i=1; i*i<=n; i*=2)count++;
A.
B.
C.
D.
本题看着像是把【例8.1】对数阶 与【例10.1】根号阶 结合起来,但最终结果不是
因为平方只是将其指数变为2倍:
计算时间复杂度
1.基本语句:count++;
2.基本语句的语句频度:
3.时间复杂度:
因为本例的循环变量自增不是以往的 i++,而是 i*=2
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环变量 i 的变化规律,将其当作数列
循环变量初始化:i=1,则首项
循环变量自增:i*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 i 随着通项公式改变
为探寻循环条件 i*i 的变化规律,也将其当作数列
将上述循环变量 i 的值依次代入循环条件 i*i,探寻数列规律
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时取对数
等式两边同时除以2
移项,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
现在就可以列求和式计算了
for循环只有1条基本语句,所以求和表达式(通项公式)是1
令求和变量 x 从1到
,代表循环执行
次
则语句频度为:
省略系数和无关项后,数量级为
,则时间复杂度为 
【变形题2】下列程序段的时间复杂度是(D)
int count=0;
for(int i=1; i<=n; i++)for(int j=1; j<=i; j*=2)count++;
A.
B.
C.
D.
本题看着与【例9】线性对数阶类似,但内层循环条件不同
【例9】是嵌套无关循环,而本题是 嵌套相关循环
计算时间复杂度
1.基本语句:count++;
2.基本语句的语句频度:
3.时间复杂度:
因为是嵌套相关循环,内外层循环相关,不能分开计算
但是可以先列出来求和式的上下限,再多重求和
因为外层循环条件是 i<=n,所以 i 能取的最大值(即求和上限)是:n
所以外层循环的求和式上下限为
因为内层循环变量自增不是以往的 j++,而是 j*=2
所以循环变量 j 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环变量 j 的变化规律,将其当作数列
循环变量初始化:j=1,则首项
循环变量自增:j*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 j 随着通项公式改变
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时取对数
移项,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
令求和变量 x 从1到
,代表循环执行
次
所以内层循环的求和式上下限为:
二层循环体只有1条基本语句,所以求和表达式(通项公式)是1
二层循环(双重循环)计算语句频度需要写成双重求和
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
将括号内的求和式替换为计算结果,双重求和变为
向下取整不容易计算,将其近似为
根据求和式的加法规则,可以拆分为
下面将新求和式
展开,可以看到是多个对数求和
根据对数的加法法则
则上述求和可以转为
因为从1乘到n可以写作阶乘n!
所以替换后
则新求和式
则原求和式
根据斯特林公式,阶乘n!的近似值为
代入得
根据对数的加法法则
上式可拆分为
根据对数的幂法则
根号是
次幂,上式变为
将两项分开计算:
根据对数的加法法则
第一项可拆分为
根据对数的减法法则
第二项可拆分为
两项相加
则新求和式
则原求和式
令常数 
令常数 
语句频度近似值为
省略系数和无关项后,数量级为
,则时间复杂度为 
【变形题3】下列程序段的时间复杂度是(B)
int count=0;
for(int i=1; i*i<=n; i++)for(int j=1; j<=i; j*=2)count++;
A.
B.
C.
D.
本题看着与【变形题2】很像,但外层循环条件不同
计算时间复杂度
1.基本语句:count++;
2.基本语句的语句频度:
3.时间复杂度:
因为是嵌套相关循环,内外层循环相关,不能分开计算
但是可以先列出来求和式的上下限,再多重求和
因为外层循环条件不是以往的 i<=n,而是 i*i<=n
所以循环变量 i 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环条件 i*i 的变化规律,将其当作数列
循环变量初始化:i=1
循环变量自增:i++
将循环变量 i 的值依次代入循环条件 i*i,探寻数列规律
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时开平方,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
所以外层循环的求和式上下限为:
因为内层循环变量自增不是以往的 j++,而是 j*=2
所以循环变量 j 不能反映实际的循环执行次数
设循环的执行次数为 y 次
为探寻循环变量 j 的变化规律,将其当作数列
循环变量初始化:j=1,则首项
循环变量自增:j*=2,则公比
,即等比数列
等比数列通项公式:
则循环变量 j 随着通项公式改变
所以最后一次循环(第y次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时取对数
移项,得到循环执行次数y的值
但是循环执行次数只能是整数,所以y还要向下取整
令求和变量 y 从1到
,代表循环执行
次
所以内层循环的求和式上下限为:
二层循环体只有1条基本语句,所以求和表达式(通项公式)是1
二层循环(双重循环)计算语句频度需要写成双重求和
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
将括号内的求和式替换为计算结果,双重求和变为
向下取整不容易计算,将其近似为
注意,此时求和变量 x 与求和表达式中的 i 是不同符号,直接展开无意义
所以需要把 i 替换为关于求和变量 x 的表达式
前面已推导出最后一次循环(第x次循环)时
等式两边同时开平方(因为 i 的初值凑巧为1,所以二者才相等)
把
替换
根据求和式的加法规则,可以拆分为
下面将新求和式
展开,可以看到是多个对数求和
根据对数的加法法则
则上述求和可以转为
因为从1乘到
可以写作阶乘 
所以替换后
则新求和式
则原求和式
根据斯特林公式,阶乘n!的近似值为
则阶乘
的近似值为
代入得
根据对数的加法法则
上式可拆分为
根据对数的幂法则
根号是
次幂,上式变为
将两项分开计算:
根据对数的加法法则、幂法则
第一项可拆分为
根据对数的减法法则、幂法则
第二项可拆分为
两项相加
则新求和式
则原求和式
令常数 
令常数 
语句频度近似值为
省略系数和无关项后,数量级为
,则时间复杂度为 
【变形题4】下列程序段的时间复杂度是(C)
int count=0;
for(int i=1; i<=n; i++)for(int j=1; j*j<=i; j++)count++;
A. B.
C.
D.
乍一看,像是把【2025年 第 1 题】内外颠倒了,实则不然
计算时间复杂度
1.基本语句:count++;
2.基本语句的语句频度:![\begin{matrix} \sum\limits_{i=1}^{n} \sum\limits_{x=1}^{\lfloor \sqrt{i} \rfloor} 1=\sum\limits_{i=1}^{n} \lfloor \sqrt{i} \rfloor \approx \sum\limits_{i=1}^{n} \sqrt{i} \approx \int_{{1}}^{{n}}\sqrt{x}\ dx + \frac{\sqrt{1} + \sqrt{n}}{2} \approx \frac{2}{3}n^{\frac{3}{2}}+\frac{1}{2}n^{\frac{1}{2}}-\frac{1}{6}\\[1.5ex] \end{matrix}](https://latex.csdn.net/eq?%5Cbegin%7Bmatrix%7D%20%5Csum%5Climits_%7Bi%3D1%7D%5E%7Bn%7D%20%5Csum%5Climits_%7Bx%3D1%7D%5E%7B%5Clfloor%20%5Csqrt%7Bi%7D%20%5Crfloor%7D%201%3D%5Csum%5Climits_%7Bi%3D1%7D%5E%7Bn%7D%20%5Clfloor%20%5Csqrt%7Bi%7D%20%5Crfloor%20%5Capprox%20%5Csum%5Climits_%7Bi%3D1%7D%5E%7Bn%7D%20%5Csqrt%7Bi%7D%20%5Capprox%20%5Cint_%7B%7B1%7D%7D%5E%7B%7Bn%7D%7D%5Csqrt%7Bx%7D%5C%20dx%20+%20%5Cfrac%7B%5Csqrt%7B1%7D%20+%20%5Csqrt%7Bn%7D%7D%7B2%7D%20%5Capprox%20%5Cfrac%7B2%7D%7B3%7Dn%5E%7B%5Cfrac%7B3%7D%7B2%7D%7D+%5Cfrac%7B1%7D%7B2%7Dn%5E%7B%5Cfrac%7B1%7D%7B2%7D%7D-%5Cfrac%7B1%7D%7B6%7D%5C%5C%5B1.5ex%5D%20%5Cend%7Bmatrix%7D)
3.时间复杂度:
因为是嵌套相关循环,内外层循环相关,不能分开计算
但是可以先列出来求和式的上下限,再多重求和
因为外层循环条件是 i<=n,所以 i 能取的最大值(即求和上限)是:n
所以外层循环的求和式上下限为
因为内层循环条件不是以往的 j<=i,而是 j*j<=i
所以循环变量 j 不能反映实际的循环执行次数
设循环的执行次数为 x 次
为探寻循环条件 j*j 的变化规律,将其当作数列
循环变量初始化:j=1
循环变量自增:j++
将循环变量 j 的值依次代入循环条件 j*j,探寻数列规律
所以最后一次循环(第x次循环)时
又因为循环条件是 
所以最后一次循环时
,则
等式两边同时开平方,得到循环执行次数x的值
但是循环执行次数只能是整数,所以x还要向下取整
令求和变量 x 从1到
,代表循环执行
次
所以内层循环的求和式上下限为:
二层循环体只有1条基本语句,所以求和表达式(通项公式)是1
二层循环(双重循环)计算语句频度需要写成双重求和
双重求和可以加括号以区分
可以类比复合函数,先计算括号内的
将括号内的求和式替换为计算结果,双重求和变为
向下取整不容易计算,将其近似为
再展开,可以看到是多个根号求和
既不是等差数列也不是等比数列,这怎么办呢?
将根号看作
次幂,上式变为
等幂求和 可以用 欧拉—麦克劳林公式 近似计算
(欧拉—麦克劳林公式是连接 离散求和 与 连续积分 的桥梁)
欧拉—麦克劳林公式
* 修正项:
,其中
是伯努利数(如
,
)
* 剩余项:
,其中
, 代表取小数(保留小数部分,忽略整数部分)
只作近似计算,忽略修正项和剩余项,上式约等于
将
代入得语句频度
省略系数和无关项后,数量级为
,则时间复杂度为 
八、递归题
【递归题1 - 2026王道数据结构第11题】下列程序段的时间复杂度是(C)
int Func(int n)
{if(n==1) return 1;else return 2*Func(n/2)+n;
}
A.
B.
C.
D.
*为了方便描述,后续均用f(n)指代Func(n)
计算时间复杂度
1.基本语句:
return 1;
return 2*f(n/2)+n;
2.基本语句的语句频度:
3.时间复杂度:
递归函数无法按常规方式计算语句频度
但是可以转换思路,将递归过程模拟为二叉树
一个递归函数当作二叉树的一个节点
函数f(n)递归调用的是f(n/2)
从二叉树的视角来看,就是该节点连接n/2层的一个孩子
所以每个节点(不算叶子节点)都只有一个孩子,最终得到"单叉"的二叉树
其实这棵"单叉"的二叉树就是线性表(或称二叉树退化为线性表)
纵向可能看不出来是线性表,改成横向就一目了然
例如:f(8)的递归二叉树(退化为线性表)如下图
因为函数参数n定义为int类型
如果调用的实参不是整数,则会对其向下取整
所以调用
,实际上是调用
为了简化分析,仅考虑偶数情况,避免向下取整造成影响
当n为偶数时,f(n)递归过程如下
从前往后看,当作数列
则为等比数列
首项
,公比 
等比数列通项公式 
代入最后一项
等式两边同时除以n
倒数可以写成-1次幂
将等式左边展开
等式两边同时取对数
根据对数的幂法则
将幂次提出
等式两边同时取相反数
移项得
数列从
到
共m项,则递归过程有m层
又因为退化为线性表,所以总节点数就是 
再看函数体,无论进 if 还是进 else,都只会执行1条语句
所以递归函数内基本语句的语句频度是1
所以 语句频度之和 = 线性表总节点数 
省略系数和无关项后,数量级为
,则时间复杂度为 
或者使用主定理(Master定理)进行推导
主定理(Master定理)
一、递归函数的时间复杂度满足以下递推公式:
(其中
且
,
是递归结束条件)
:问题规模
:每次递归调用的子问题数量(即子递归调用次数)
:每个子问题的规模(必须相等)
:非递归操作的语句频度
令非递归操作的时间复杂度
这是简化分析,假定其时间复杂度为常见的幂函数,方便后续只比较指数
但如果时间复杂度确实不是幂函数,则需要整体比较
将
用时间复杂度
来近似估计,则递推公式变为
(其中
且
,
是递归结束条件)
递归操作的时间复杂度经复杂推导(过程略)可近似为
![]()
二、应用条件
1、子问题规模相等:原问题需被分割成规模相同的子问题(如归并排序的
分割)
2、非递归操作的时间复杂度:
中的
必须为常数,不能随
变化
三、主定理判断时间复杂度的三种情况
实际上是看递归操作
和 非递归操作
哪个占主导
1、递归主导
当
时,递归操作耗时占主导,则递归函数的时间复杂度
2、平衡状态
当
时,递归与非递归操作耗时相当,则递归函数的时间复杂度
3、非递归主导
当
时,非递归操作耗时占主导
还需满足条件:存在常数
,使得不等式
成立
则递归函数的时间复杂度
再把代码贴过来,对本题进行分析
int Func(int n)
{if(n==1) return 1;else return 2*Func(n/2)+n;
}
*为了方便描述,后续均用f(n)指代Func(n)
肯定有人一上来就直接列式
殊不知中了出题人的圈套,把 表达式求值 和 时间复杂度 混为一谈了
下面从头开始分析
1、先看子问题数量:
其实 子问题数量 就是 子递归的调用次数
当前递归函数f(n)只调用一次子递归f(n/2),所以子问题数量
,满足条件 
有人看到 2*f(n/2) 可能会误认为子问题数量 
这就是将 函数返回值 和 函数调用 混淆了
2*f(n/2) 只是让函数返回值参与运算,仅调用了一次子递归f(n/2)
假设当前子递归f(n/2)的返回值是4,则 2*f(n/2)=2*4=8
如果要调用两次子递归得把f(n/2)写两遍,比如 2*f(n/2)*f(n/2)
2、再看子问题规模:
从子递归调用f(n/2)可以看出子问题规模是
,则
,满足条件 
3、最后看非递归操作的时间复杂度:
看函数体,无论进 if 还是进 else,都只会执行1条非递归操作语句
所以非递归操作的语句频度是1,数量级为1,时间复杂度为
,则 
有人看到 +n 可能会误认为非递归操作的时间复杂度是 
这就是将 变量运算 和 语句频度 混淆了
2*Func(n/2)+n 这个加法运算整体只是1条语句,其语句频度是1
如果要实现非递归操作的时间复杂度是
,得把 +n 放到循环里面,比如
sum=2*Func(n/2);
for(i=1; i<=n; i++) { sum+=n; }
return sum;
通过上面的分析,得到正确的递推公式为
此时
,则
所以
,满足第二种情况(平衡状态)
则递归函数的时间复杂度 
因为对数阶可以省略底数,所以两种方式推导出的时间复杂度相同: 
【递归题2】下列程序段的时间复杂度是(C)
double f(int n)
{if(n==1) return 1;else{int i;double count=0;for(i=1; i<=n; i++){count+=f(n-1);}return count; }
}
A.
B.
C.
D.
计算时间复杂度
1.基本语句:
return 1;
count+=f(n-1);
2.基本语句的语句频度:
3.时间复杂度:
递归函数无法按常规方式计算语句频度
但是可以转换思路,将递归过程模拟为树(这里就不是二叉树了,而是n叉树)
一个递归函数当作树的一个节点
本题递归过程为
则递归树共有n层
f(n)是递归树的根结点,则第1层的节点数为:
f(n)通过循环递归调用n次 f(n-1),也就是n个 f(n-1)
从树的视角来看,就是该节点连接第2层的n个孩子,则第2层节点数为:
对于这 n 个f(n-1) ,其中每个f(n-1)通过循环递归调用n-1次 f(n-2),也就是n-1个 f(n-2)
从树的视角来看,就是第2层的每个节点都连接第3层的n-1个孩子,则第3层节点数为: 
如下图所示
为探寻节点数的规律,将 {节点数} 当作数列
第1层:根节点,节点数为 
第2层:节点数为 
第3层:节点数为 
第4层:节点数为 
第5层:节点数为 
……
第n层:节点数为
既不是等差数列也不是等比数列,这怎么办呢?
可以看到节点数从n逐渐累乘至2,有没有想到什么?
其实就是高中学过的排列数
所以{节点数} 数列的规律可以用排列数表示
第1层:根节点,节点数为 
第2层:节点数为 
第3层:节点数为 
第4层:节点数为 
第5层:节点数为 
……
第n层:节点数为 
再看函数体
①进 if 时,对应第n层的叶子节点f(1)
只执行1条语句,节点语句频度为1
②进else时,对应其他层的节点
进入for循环,for循环内执行非递归的加法语句,循环几次就执行几次
所以节点语句频度=循环次数,循环次数随问题规模改变
为探寻循环次数的规律,将 {循环次数} 当作数列
第1层:对应f(n),问题规模为n,循环次数为 
第2层:对应f(n-1),问题规模为n-1,循环次数为 
第3层:对应f(n-2),问题规模为n-2,循环次数为 
第4层:对应f(n-3),问题规模为n-3,循环次数为 
第5层:对应f(n-4),问题规模为n-4,循环次数为 
……
第n-1层:对应f(2),问题规模为2,循环次数为 
(为什么不写第n层?因为第n层对应f(1),进的是if,前面已得出节点语句频度为1)
可以看出 {循环次数} 数列是等差数列
首项
,公差 
等差数列通项公式 
所以节点语句频度=循环次数
(其中m是当前层数)
前面我们已经推导出 每层的节点数,这里又推导出 节点语句频度,则
当前层语句频度=当前层节点数 x 当前层节点语句频度=当前层节点数 x 当前层循环次数(即
)
为探寻当前层语句频度的规律,将 {当前层语句频度} 当作数列
第1层:当前层语句频度为
第2层:当前层语句频度为
第3层:当前层语句频度为
第4层:当前层语句频度为
第5层:当前层语句频度为
……
第n-1层:当前层语句频度为
第n层:当前层语句频度为 (对应 if 情况,直接代入1)
求整个递归函数的总语句频度,就是将第1层到第n层的当前层语句频度相加
相当于对 {当前层语句频度} 这个数列求和
用阶乘表示为
这里的 n! 与求和变量 i 无关,相当于常数,可以将其提出
为了简化分母表示,设 
因为 i 的变化规律是
所以 k 的变化规律是
因为有加法交换律,所以求和变量 从n-1到0 与 从0到n-1 的求和结果相同
将上式改为用求和变量 k 表示的新求和式
《高等数学》学过自然指数函数的麦克劳林展开为
当
时,即为自然对数底e
当 问题规模(n) 的取值很大时,可近似为
最终递归函数的总语句频度近似为
省略系数和无关项后,数量级为
,则时间复杂度为 
可以称作 阶乘阶,将其与指数阶
进行比较
可以看到
和
这两个函数在第一象限有两个交点
一个交点在
另一个交点在
区间内
在第二次相交后,
的增长速率明显要比
快
所以当问题规模(n)趋于无穷大时:
或者尝试使用主定理(Master定理)进行推导
再把代码贴过来,对本题进行分析
double f(int n)
{if(n==1) return 1;else{int i;double count=0;for(i=1; i<=n; i++){count+=f(n-1);}return count; }
}
下面从头开始分析
1、先看子问题数量:
其实 子问题数量 就是 子递归的调用次数
当前递归函数f(n)在for循环中调用n次子递归f(n-1),所以子问题数量
,满足条件 
2、再看子问题规模:
从子递归调用f(n-1)可以看出子问题规模是
,此时可以当作
,不满足条件 
所以不能使用主定理
3、最后看非递归操作的时间复杂度:
进 if 时,对应第n层的叶子节点f(1)
只执行1条语句,节点语句频度为1,数量级为1,时间复杂度为
,此时 
进else时,对应其他层的节点
进入for循环,for循环内执行非递归的加法语句,循环几次就执行几次
所以语句频度=循环次数,循环次数随问题规模改变
初始节点语句频度为n,数量级为n,时间复杂度为
,此时 