时间复杂度的简单计算方法
参考数据结构实现的黑书
1. 时间复杂度 1.1 概念 算法的渐进时间复杂度,简称时间复杂度
事前分析
算法采用的策略和方案 编译产生代码质量 问题的输入规模 机器执行指令的速度 将核心操作的次数与输入规模关联 
1.1.1 函数渐进增长 给定两个函数 $f(n)$ 和 $g(n)$ ,如果存在一个整数 $N$ 使得对于所有 $n >N$ , $f(n)$ 总是比 $g(n)$ 大,那么称 $f(n)$ 的渐进增长快于 $g(n)$
算法函数中的常数、系数可以忽略 算法函数中最高次幂越小,算法效率越高 
### 1.1.2 大O记法 > 语句总的执行次数 $T(n)$ 是关于问题规模 $n$ 的函数,进而分析 $T(n)$ 随着 $n$ 的变化情况并确定 $T(n)$ 的量级。记为 $T(n) = O(f(n))$ > > 表示随着问题规模 $n$ 的增大,算法执行时间的增长率和 $f(n)$ 的增长率相同 - $f(n)$ 表示问题规模 n 的某个函数 - 执行次数 $\iff$ 执行时间 **规则** - 用1代替运行时间中的所有常数、系数 - 在修改后的次数中,只保留高阶项 **1-100求和**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 累加 int  main () {	int  sum = 0 ; 	int  n = 100 ; 	for (int  i = 0 ; i < n;++i){ 		sum += i; 	}	 	printf ("sum=%d" ,sum); } # 执行 2 n+3  次 # 大O记法 O(n) # ------------------------------------------- # 高斯公式 void  main () {	int  sum = 0 ; 	int  n = 100 ; 	sum = (n+1 )n/2 ; 	printf ("sum=%d" ,sum); }  # 执行3 次 # 大O记法 O(1 ) 
#### 最坏情况 > 直接查找
1 2 3 4 5 6 7 8 9 10 int  search (int  num) {	int  arr[] = [11 ,10 ,8 ,9 ,7 ,22 ,23 ,0 ]; 	 	for (int  i = 0 ;i < arr.length;++i){ 		if (num == arr[i]) 			return  i; 	} 	return  -1 ; } 
**最好情况:** - 查找的第一个数字就是期望数字—— $O(1)$ **最坏情况:** - 查找的最后一个数字,才是期望数字——$O(n)$ **平均情况:** - 任何数字查找的平均代价为 ——$O(n)$ ### 1.1.3 时间复杂度排序 $O(1)< O(logn) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!)  < O(n^n)$
1.2 简单时间复杂度分析 1.2.1 非函数调用 线性阶
非嵌套线性阶,随输入规模的扩大,呈 线性增长 
1 2 3 4 5 int  sum = 0 ;int  n = 100 ;for (int  i = 0 ; i <= n;++i){	sum += i; } 
平方阶
二重循环嵌套
1 2 3 4 5 6 7 int  sum = 0 ;int  n = 100 ;for (int  i = 1 ;i <= n;++i){	for  (j = 1 ;j <= n;++j){ 		sum += i; 	} } 
立方阶
三重循环嵌套
对数阶
对数阶,由于输入规模n的增大,不管底数多少,增长率都相同,所以忽略底数
1 2 3 4 int  i=1 ,n=100 ;while (i < n){	i = i*2 ; } 
循环次数:$x = log_2n$ 时间复杂度:$O(logn)$
常数阶
1.2.3 函数调用 1 2 3 4 5 6 7 8 9 10 void  main () {    int  n = 100 ;     for (int  i = 0 ;i < n;++i){ 	    show(i);	     } } void  show (int  i) {    printf ("%d" ,i); } 
1 2 3 4 5 6 7 8 9 10 11 12 void  main () {	int  n = 100 ; 	for (int  i = 0 ;i < n;++i){ 		show(i);	 	} } void  show (int  i) {	for (int  j = 0 ;j < i;++j){		 		printf ("%d" ,i); 	}	 } 
1.3 计算方法 1.3.0 常规 for 循环一次 for 循环至多是该 for 循环包含的 内语句运行次数  乘 迭代次数  嵌套的 for 循环 顺序语句 if/else 语句1.3.1 循环主体中的变量参与循环条件的判断 找出主体语句中与 $T(n)$ 成正比的循环变量,将之代入条件运算
1 2 3 4 int  i = 1 ;while (i <= n){	i = i*2 ; } 
执行次数 $t$ ,有 $2^t \leq n$ ,得 $T(n) = O(log_2n)$ 1 2 3 4 int  y = 5 ;while ((y+1 )*(y+1 )<=n){	y = y+1 ; } 
执行次数 $t = y-5$ ,有 $y=t+5$,代入得 $t < \sqrt{n}-6$,即 T(n) = O($\sqrt{n}$) 运算时间中的对数 如果一个算法用时间 $O(1)$ 将问题的大小削减为原来的一部分,那么该算法就是$O(logN)$ 如果用常数时间把问题减少一个常数,那么这种算法就是 $O(N)$ 折半查找 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int  BinarySearch (const  ElementType A[],ElementType X,int  N)  {	int  Low,Mid,High; 	Low = 0 ,High = N-1 ; 	while (Low <= High){ 		Mid = (Low+High)/2 ; 		if (A[Mid] < X){ 			Low = Mid + 1 ; 		}else { 			if (A[Mid] > X){ 				High = Mid-1 ; 			else  				return  Mid;  		} 	} 	return  NotFound;  } 
分析:提供了 $O(logN)$ 以内的查找操作
欧几里得算法 定理:如果 $M > N$ ,则 $M \mbox{ mod N} < \frac{M}{2}$
1 2 3 4 5 6 7 8 9 unsigned  int  Gcd (unsigned  int  M,unsigned  int  N)  {	unSigned int  Rem; 	while (N > 0 ){ 		Rem = M % N; 		M = N; 		N = Rem; 	} 	return  M; } 
幂运算 $O(logn)$ 1 2 3 4 5 6 7 8 9 10 long  int  Pow (long  int  X,unsigned  int  N)  {	if (N == 0 ) 		return  1 ; 	if (N == 1 ) 		return  X; 	if (isEven (N)) 		return  Pow (X*X,N/2 ); 	else   		return  Pow (X*X,N/2 ) * X; } 
1.3.2 循环主体中的变量与循环条件无关 采用 归纳法  或 累计循环次数 
多层循环从内到外分析,忽略单步语句、条件判断语句,只关注主体语句的执行次数
递归程序——分治策略 递归程序四条准则 基准情形:有结束条件 不断推进:每次调用都朝向基准情形推进 设计法则:假设所有的递归调用都能进行 合成效益法则:避免重复计算 “分治”策略 ”分“:将大问题大致分为两大致相等的 幂次阶  子问题,用递归求解 “治”:将两个子问题的解合并到一起并可能再做些少量的附加工作(幂次阶 ),得到整个问题的解 主方法  :大小为 $n$ 的原问题分成若干个大小为 $\frac{n}{b}$ 的子问题,其中 $a$ 个子问题需要求解,而 $cn^k$ 是合并各个子问题需要的工作量。此时问题可表示为:
则其时间复杂度可相应的表示为:
推导 已知
1. 等号右边连续代入递归关系 
由 $k = logN$ 得
2. 叠缩求和 
用 N  去除递归关系中的两边,不断替换
将等号左边的所有相相加等于右边所有项的和,结果为
1.4 常见算法时间复杂度总结 描述 增长的数量级 说明 举例 常数级 1 普通语句 将两个数相加 对数级 $logN$ 二分策略 二分查找 线性级 $N$ 单层循环 找出最大元素 线性对数级 $NlogN$ 分治思想 归并排序 平方级 $N^2$ 双层循环 检查所有元素对 立方级 $N^3$ 三层循环 检查所有三元组 指数级 $2^N$ 穷举查找 检查所有子集 
1.5 最大子序列和问题的解 1.5.1 遍历所有子串,对子串的子序列依次求和 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int  MaxSubSequenceSum (const  int  A[],int  N)  {	int  ThisSum,MaxSum,i,j,k; 	MaxSum = 0 ; 	for (i = 0 ;i < N;++i){ 		for (j = i;j < N;++j){ 			ThisSum = 0 ; 			for (k = i;k <= j;++k) 				ThisSum += A[k]; 				if (ThisSum > MaxSum) 					MaxSum = ThisSum;	 		} 	} 	return  MaxSum; } 
时间复杂度为 $O(N^3)$
1.5.2 记录中间累加量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int  MaxSubSequenceSum (const  int  A[],int  N)  {	int  ThisSum,MaxSum,i,j,k; 	 	MaxSum = 0 ; 	for (i = 0 ;i < N;++i){ 		ThisSum = 0 ; 		for (j = i;j < N;++j){ 			ThisSum += A[k]; 			if (ThisSum > MaxSum){ 				MaxSum = ThisSum; 			}				 	} 	return  MaxSum; } 
时间复杂度为 $O(N^2)$
1.5.3 分治法 将序列分成大致相等的两部分。
最大子序列和可能在三处出现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int  MaxSubSum (const  int  A[],int  Left,int  Right)  {	int  MaxLeftSum,MaxRightSum; 	int  MaxLeftBorderSum,MaxRightBorderSum; 	int  LeftBorderSum,RightBorderSum; 	int  Center,i; 	if (Left == Right){ 		if (A[Left] > 0 ) 			return  A[Left]; 		else { 			return  0 ; 		} 	} 	 	Center = (Left+Right) / 2 ; 	MaxLeftSum = MaxSubSum (A,Left,Center); 	MaxRightSum = MaxSubSum (A,Center,Right); 	MaxLeftBorderSum = 0 ,LeftBorderSum = 0 ; 	for (i = Center;i >= Left;i--){ 		LeftBorderSum += A[i]; 		if (LeftBorderSum > MaxLeftBorderSum) 			MaxLeftBorderSum = LeftBorderSum; 	} 	MaxRightBorderSum = 0 ,RightBorderSum = 0 ; 	for (i = Center;i >= Left;i--){ 		RightBorderSum += A[i]; 		if (RightBorderSum > MaxRightBorderSum) 			MaxRightBorderSum = RightBorderSum; 	} 	return  Max3 (MaxLeftSum,MaxRightSum,MaxLeftBorderSum+MaxRightBorderSum); } int  MaxSubSequenceSum (Const A[],int  N)  {	return  MaxSubSum (A,0 ,N-1 ); } 
时间复杂度为 $O(NlogN)$
分治法  时间复杂度一般都是若干个代价为 $logN$ 的子问题与一个处理函数的代价和
1.5.4 最简 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int  MaxSubSequenceSum (const  int  A[],int  N)  {	int  ThisSum,MaxSum,j;	 	ThisSum = MaxSum = 0 ; 		for (j = 0 ;j < N;++j){ 			ThisSum += A[k]; 			if (ThisSum > MaxSum){ 				MaxSum = ThisSum; 			}else  if (ThisSum < 0 ){ 				ThisSum = 0 ; 			}				 	} 	return  MaxSum; } 
时间复杂度为 $O(N)$
空间复杂度 算法所需的辅助空间数量级
运行过程中变量占用的空间 辅助数组  递归工作栈 递归深度  原地工作 算法所需的辅助空间为常量