《数据结构与算法》| 清华大学 | 第一章 | 复杂度及算法设计

概念

计算:即信息处理,是指借助某种工具,遵循一定规则,以明确而机械的形式进行的操作。

计算模型:即计算处理工具。

算法:在特定计算模型下,解决特定问题的指令序列(操作步骤),算法具有如下特征:
1、输入、输出
2、确定性 算法的每个基本操作步骤必须有确定的含义
3、可行性 算法中每个基本操作步骤都可以实现,并且在有限时间内完成
4、有穷性 对于任何输入,经过有限次基本操作之后都可以得到输出

好算法:最重要的是效率,速度尽可能快,存储空间尽可能小。当然前提是正确的算法,健壮、可读是另一方面。

算法运行平台

对于特定问题的不同算法,很难用实验统计的方式确定真正的效率,因为不同的算法可能更适应于不同规模和类型的输入。为了给出客观评价,需要抽象出一个理想的平台或者模型,不依赖任何外部的各种因素。比如图灵机(Turing Machine)模型和随机存取机(Random Access Machine)模型。

1. 图灵机模型

图灵机就是指一个抽象的机器,它有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色。有一个机器头在纸带上移来移去。机器头有一组内部状态,还有一些固定的程序。
在这里插入图片描述
在这里插入图片描述
纸带:依次均匀地划分为单元格各存有某一字符,初始均为’#’
读写头:总是对准某一单元格,并可读取或改写其中的字符,每经过一个节拍可转向左侧或右侧的领格
状态:TM总是处于有限状态的某一种,每经过一个节拍可按照规则转向另一状态

2. RAM模型

随机存储机(random access machine,RAM) 可以通过编号访问任意寄存器,避免读写头单格移动,极大提高读写效率及灵活性。同时,RAM模型是冯诺依曼结构的基础,访问所用的语言也是汇编语言的伪码形式。

在这里插入图片描述
常规操作:
R [ i ] < − c R [ i ] < − R [ R [ j ] ] R [ i ] < − R [ j ] + R [ k ] \qquad R[i] <- c\qquad R[i] <- R[R[j]]\qquad R[i] <- R[j] + R[k] R[i]<cR[i]<R[R[j]]R[i]<R[j]+R[k] R [ i ] < − R [ j ] R [ R [ i ] ] < − R [ j ] R [ i ] < − R [ j ] − R [ k ] \qquad R[i] <- R[j]\qquad R[R[i]] <- R[j]\qquad R[i] <- R[j] - R[k] R[i]<R[j]R[R[i]]<R[j]R[i]<R[j]R[k] I F R [ i ] = 0 G O T O I F R [ i ] > 0 G O T O \qquad IF\quad R[i] = 0\quad GOTO\qquad IF\quad R[i] > 0\quad GOTO IFR[i]=0GOTOIFR[i]>0GOTO

思考:在TM、RAM等模型中衡量算法效率,为何通常只需考察运行空间?空间呢?
答:早期的存储器价格昂贵,所以设计算法时需要考虑到时间和空间两个方面。但随着硬件行业的发展,现今存储器的容量越来越大,价格却越来越便宜,相比之下运行时间成为衡量算法性能的更重要的指标。

算法评价标准

考察算法的优劣应着眼于 正确性成本,其中正确性通过数学证明算法的功能与问题要求一致,一般不容易证明。

1. 复杂度函数

成本:时间成本和空间成本。度量的方法是将计算成本描述为求解问题实例的函数。但可能出现的实例太多,而问题实例的规模往往是决定计算成本的最主要因素,所以根据实例的规模进行概括复杂度函数

T A ( n ) = 用 算 法 A 求 解 某 一 问 题 规 模 为 n 的 实 例 , 可 简 化 为 T ( n ) T_{A} (n)= 用算法A求解某一问题规模为n的实例,可简化为T(n) TA(n)=AnT(n)
00
时间成本函数 T ( n ) T(n) T(n) 可通过计算基本操作次数得到,而空间成本函数 S ( n ) S(n) S(n) 可计算占用存储单元数得到,从而复杂度函数使得算法分析由一个复杂的统计问题,转化为一个明确的代数求和问题。

2. 大O记号

时间复杂度函数 T ( n ) = 5 n ⋅ [ 3 n ⋅ ( n + 2 ) + 4 ] + 6 T(n) = \sqrt{5n · [3n · (n+2)+ 4] + 6} T(n)=5n[3n(n+2)+4]+6 成功描述随着问题规模 n n n 的增长,某一算法时间成本的增长情况,但 T ( n ) T(n) T(n) 构成复杂难以给人留下直观印象。

O O O 记号((big O notation):描述函数渐进行为的函数,即用另一个更简单的函数 f ( n ) f(n) f(n) 描述一个函数数量级的渐近上界。例如 T ( n ) = O ( f ( n ) ) T(n) = O( f(n) ) T(n)=O(f(n)),即 ∃ c > 0 , 当 n 足 够 大 后 , T ( n ) < c ⋅ f ( n ) \exists c > 0,当 n 足够大后,T(n) < c · f(n) c>0nT(n)<cf(n)

在这里插入图片描述

大o曲线(即c·f(n))前面一段比T(n)小,但只要c足够大,一定会有大o曲线一直高于T(n),即 c·f(n) > T(n)

f ( n ) f(n) f(n) 计算方法:放大常数项计算结果,最后忽略常系数 O ( c f ( n ) ) = O ( f ( n ) ) O(cf(n)) = O(f(n)) O(cf(n))=O(f(n)),忽略低次项 O ( n a + n b ) = O ( n a ) O(n^a+n^b) = O(n^a) O(na+nb)=O(na), 其中 a > b > 0 a>b>0 a>b>0

在这里插入图片描述
常用的函数阶:

  • 常数阶复杂度 O ( 1 ) O(1) O(1):复杂度与输入规模无关,主要是一些不含有循环、分支及递归的顺序执行序列。
  • 对数阶复杂度 O ( l o g n ) O(log n) O(logn): 常底数 l o g a n log_a^n logan 可替换为任意值 l o g a b ⋅ l o g b n log_a^b · log_b^n logablogbn,常数次幂 l o g n c logn^c lognc 可转换为常系数 c ⋅ l o g n c·logn clogn,底数 a a a、常数次幂 c c c 和 对数多项式的低次项可忽略。
  • 多项式阶复杂度 O ( n c ) O(n^c) O(nc) 和 指数阶复杂度 O ( 2 n ) O(2^n) O(2n)

在这里插入图片描述

总结:1 < logn < n^c < 2^n。除此之外,由于循环结构的嵌套等,还有可能出现类似 O(nlogn)O(nloglogn)这样的组合型复杂度.

算法分析方法

算法分析主要基于时间复杂度的 “大O”记号,统计算法描述为RAM的基本指令的执行次数。高级语言的基本指令等效于常数条RAM基本指令,渐进分析下两者指令数量大体相当,因此基本指令执行次数的计数又可以转化为对于源代码语句块的计数。

  • 常 数 阶 O ( 1 ) : 顺 序 结 构 , 分 支 结 构 常数阶O(1):顺序结构,分支结构 O(1)
  • 对 数 阶 O ( l o g n ) : 二 分 查 找 对数阶O(logn): 二分查找 O(logn):
  • 线 性 阶 O ( n ) : 单 个 循 环 线性阶O(n): 单个循环 线O(n):
  • 平 方 阶 O ( n 2 ) : 循 环 嵌 套 平方阶O(n^2): 循环嵌套 O(n2):

实际应用过程中会出现更加复杂的情况,比如调用和递归(自我调用)就不能用以上复杂度计算。因此,我们针对常见迭代和递归有如下方法:

  • 迭 代 : 级 数 求 和 迭代:级数求和
  • 递 归 : 递 归 跟 踪 、 递 推 方 程 递归:递归跟踪、递推方程
1. 级数求和
  • 收 敛 级 数 : T ( n ) = 1 + 1 2 2 + 1 3 2 + . . . + 1 n 2 < 1 + 1 2 2 + 1 3 2 + . . . = π 2 6 = O ( 1 ) 收敛级数: T(n) = 1+\frac{1}{2^2}+\frac{1}{3^2}+...+\frac{1}{n^2} < 1 +\frac{1}{2^2}+\frac{1}{3^2}+... = \frac{π^2}{6} = O(1) :T(n)=1+221+321+...+n21<1+221+321+...=6π2=O(1)
  • 调 和 级 数 : T ( n ) = 1 + 1 2 + 1 3 + . . . + 1 n = O ( l o g n ) 调和级数: T(n) = 1+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n} = O(logn) :T(n)=1+21+31+...+n1=O(logn)
  • 对 数 级 数 : T ( n ) = l o g 1 + l o g 2 + l o g 3 + . . . + l o g n = O ( n l o g n ) 对数级数: T(n) = log1 + log2 + log3 +...+logn = O(nlogn) :T(n)=log1+log2+log3+...+logn=O(nlogn)
  • 算 术 级 数 : T ( n ) = 1 + 2 + . . . + n = n ( n + 1 ) 2 = O ( n 2 ) 算术级数: T(n) = 1 + 2 + ... + n = \frac{n(n+1)}{2} = O(n^2) :T(n)=1+2+...+n=2n(n+1)=O(n2)
  • 幂 方 级 数 : T ( n ) = 1 2 + 2 2 + 3 2 + . . . + n 2 = ∑ k = 0 n k 2 ≈ ∫ 0 n x 2 d x = 1 3 n 3 = O ( n 3 ) 幂方级数: T(n) = 1^2+2^2+3^2+...+n^2 = \sum_{k=0}^{n}k^2≈\int_{0}^{n}x^{2}dx=\frac{1}{3}n^{3}=O(n^{3}) :T(n)=12+22+32+...+n2=k=0nk20nx2dx=31n3=O(n3)
  • 几 何 级 数 : T 2 ( n ) = 1 + 2 + 4 + 8 + . . . + 2 n = ∑ k = 0 n 2 k = 2 n + 1 − 1 = O ( 2 n + 1 ) = O ( 2 n ) 几何级数:T_{2}(n) = 1+ 2 + 4+ 8+...+2^n =\sum_{k=0}^{n}2^k = 2^{n+1}-1 =O(2^{n+1})=O(2^n) :T2(n)=1+2+4+8+...+2n=k=0n2k=2n+11=O(2n+1)=O(2n)
2. 递归跟踪

递归跟踪,即把整个递归调用的过程用一张图表示出来。递归跟踪分析包含以下三个步骤:

  • 枚举(可以采用树状图的方法)所有递归实例
  • 归纳出每个递归实例时间开销的规律(注意:进入或者说调用下一层递归复杂度仅为O(1))
  • 累计

递归跟踪虽然直观形象,但仅适用于简明的递归模式。对于比较复杂的递归调用形式,很难使用图绘制出来。因此需要另外一种较为间接抽象,但更适用于复杂的递归模式的递归方程。

3. 递推方程

如果说递归跟踪分析是偏向“几何”的方法, 那递推方程就是纯正的“代数”方法了
类似于解数列通项表达式或者微分方程等从隐式表达解显式方程的过程,我们借助递推关系归纳出T(n)与T(n – 1)等的关系,联立base case(比如T(1) = c),解出T(n)的表达式再化简为O(f(n))

1、迭代法:不断用递推方程的右部替换左部、换元迭代
2、递归树:建立递归树,每次迭代将函数项作为儿子,非函数项作为根的值。以二分归并排序递归方程为例。
3、主定理

算法设计思想

算法设计思想实践中的总体趋向是缩减问题规模,根据划分的问题是否重叠可分为分治动态规划,而分治根据划分规模又可以分为减而治之分而治之

1. 减而治之

定 义 : 将 大 规 模 的 问 题 划 分 两 个 不 相 关 子 问 题 : 其 一 平 凡 , 另 一 规 模 缩 减 , 分 别 求 解 , 由 子 问 题 的 解 , 得 到 原 问 题 的 解 。 定义:将大规模的问题划分两个不相关子问题:其一平凡,另一规模缩减,分别求解,由子问题的解,\\得到原问题的解。

在这里插入图片描述
本程序采用减而治之的方法计算数组所有元素和,问题划分为子问题返回0,另一规模缩减为sum(n-1),再分别减治两个子问题,直至问题规模为O(1):

sum(int a[], int n) 
{ return n < 1 ? 0 : sum(A, n-1) + a[n - 1]; }

递 归 跟 踪 递归跟踪
( 1 ) 检 查 每 个 递 归 实 例 为 s u m ( ) , 总 计 n + 1 项 (1)检查每个递归实例为sum(),总计n+1项 (1)sum()n+1
( 2 ) 每 个 递 归 实 例 仅 执 行 一 条 语 句 , 实 例 时 间 开 销 为 O ( 1 ) (2)每个递归实例仅执行一条语句,实例时间开销为O(1) (2)O(1)
( 3 ) 累 计 时 间 复 杂 度 T ( n ) = O ( 1 ) ∗ ( n + 1 ) = O ( n ) (3)累计时间复杂度T(n) = O(1) * (n+1) = O(n) (3)T(n)=O(1)(n+1)=O(n)
在这里插入图片描述

递 归 方 程 : T ( n ) = T ( n − 1 ) + O ( 1 ) 递归方程:T(n) = T(n-1) + O(1) T(n)=T(n1)+O(1)
递 归 基 : T ( 0 ) = O ( 1 ) 递归基:T(0) = O(1) T(0)=O(1)
求 解 : T ( n ) = T ( n − 1 ) + O ( 1 ) = T ( n − 2 ) + O ( 1 ) + O ( 1 ) = . . . = T ( 0 ) + n = O ( 1 ) + n = O ( n ) 求解:T(n) = T(n-1)+ O(1) = T(n-2) + O(1) + O(1) = ... = T(0) + n = O(1) + n = O(n) T(n)=T(n1)+O(1)=T(n2)+O(1)+O(1)=...=T(0)+n=O(1)+n=O(n)

2. 分而治之

定 义 : 将 大 规 模 的 问 题 划 分 为 若 干 不 相 关 子 问 题 , 规 模 大 体 相 当 , 分 别 求 解 子 问 题 , 由 子 问 题 的 解 , 得 到 原 问 题 的 解 。 定义:将大规模的问题划分为若干不相关子问题,规模大体相当,分别求解子问题,由子问题的解,\\得到原问题的解。
在这里插入图片描述
本程序采用分而治之的方法计算数组所有元素和,问题划分为左子问题sum(a, lo, mi)和右子问题sum(a, mi+1, hi),再分别分治两个子问题,直至问题规模为O(1):

sum(int a[], int lo, int hi) 
{ 
	if (lo == hi) return a[lo];
	int mi = (lo + hi) >> 1;
	return sum(a, lo, mi) + sum(a, mi+1, hi);
}

递 归 跟 踪 递归跟踪
( 1 ) 检 查 每 层 递 归 实 例 s u m ( ) 数 量 , 总 计 ( 2 0 + 2 1 + 2 2 + . . . + 2 l o g n ) 项 (1)检查每层递归实例sum()数量,总计(2^0+2^1+2^2+...+2^{logn})项 (1)sum()(20+21+22+...+2logn)
( 2 ) 每 个 递 归 实 例 仅 执 行 3 条 语 句 , 实 例 时 间 开 销 为 O ( 1 ) (2)每个递归实例仅执行3条语句,实例时间开销为O(1) (2)3O(1)
( 3 ) 累 计 时 间 复 杂 度 T ( n ) = O ( 1 ) ∗ ( 2 0 + 2 1 + 2 2 + . . . + 2 l o g n ) = O ( 1 ) ∗ ( 2 1 + l o g n − 1 ) = O ( n ) (3)累计时间复杂度T(n) = O(1) * (2^0+2^1+2^2+...+2^{logn}) =O(1)*(2^{1+logn}-1)= O(n) (3)T(n)=O(1)(20+21+22+...+2logn)=O(1)(21+logn1)=O(n)
在这里插入图片描述
递 归 方 程 : T ( n ) = 2 ∗ T ( n / 2 ) + O ( 1 ) 递归方程:T(n) = 2*T(n/2) + O(1) T(n)=2T(n/2)+O(1)
递 归 基 : T ( 1 ) = O ( 1 ) 递归基:T(1) = O(1) T(1)=O(1)
求 解 : T ( n ) = 2 ∗ T ( n / 2 ) + O ( 1 ) = 4 ∗ T ( n − 2 ) + O ( 3 ) = . . . = n ∗ T ( 0 ) + O ( n − 1 ) = O ( 2 n − 1 ) + n = O ( n ) 求解:T(n) = 2*T(n/2) + O(1) = 4*T(n-2)+ O(3) = ... = n*T(0) + O(n-1) = O(2n-1) + n = O(n) T(n)=2T(n/2)+O(1)=4T(n2)+O(3)=...=nT(0)+O(n1)=O(2n1)+n=O(n)

3. 分治法缺陷

分治法将问题划分为互不相交的子问题,递归求解子问题,再将它们的解组合起来,求出原来问题的解。而当子问题出现重叠时,分治算法会做许多重复的工作,浪费大量资源和时间。下面以 f i b ( n ) = f i b ( n − 1 ) + f i b ( n − 2 ) fib(n)=fib(n-1)+fib(n-2) fib(n)=fib(n1)+fib(n2)为例:

int fib(n){
    if(n < 2){
        return n;
    }else{
        return fib(n - 1) + fib(n - 2);
    }
}

在这里插入图片描述
递 推 方 程 : T ( n ) = T ( n − 1 ) + T ( n − 2 ) + 1 , T ( 0 ) = T ( 1 ) = 1 , ∀ n > 1 递推方程:T(n) = T(n - 1) + T(n - 2) + 1,T(0) = T(1) = 1,\forall n> 1 T(n)=T(n1)+T(n2)+1T(0)=T(1)=1n>1

令 S ( n ) = [ T ( n ) + 1 ] / 2 令 \quad S(n) = [T(n)+1]/2 S(n)=[T(n)+1]/2
则 S ( 0 ) = 1 = f i b ( 1 ) , S ( 1 ) = 1 = f i b ( 2 ) 则 \quad S(0) = 1 = fib(1),S(1) = 1 = fib(2) S(0)=1=fib(1)S(1)=1=fib(2)
故 S ( n ) = S ( n − 1 ) + S ( n − 2 ) = f i b ( n + 1 ) 故 \quad S(n) = S(n - 1) + S(n - 2) = fib(n + 1) S(n)=S(n1)+S(n2)=fib(n+1)
T ( n ) = 2 ∗ S ( n ) + 1 = 2 ∗ f i b ( n + 1 ) − 1 = O ( f i b ( n + 1 ) ) = O ( φ n ) \qquad T(n) = 2*S(n)+1 = 2*fib(n+1)-1 = O(fib(n + 1)) = O(φ^n) T(n)=2S(n)+1=2fib(n+1)1=O(fib(n+1))=O(φn)
其 中 φ = ( 1 + √ 5 ) / 2 ≈ 1.618 , φ 43 ≈ 2 30 ≈ 1 0 9 f l o p s = 1 s , φ 67 ≈ 1 0 1 4 f l o p s = 1 0 5 s = 1 d a y 其中\quad φ = (1+√5)/2 ≈ 1.618,φ^{43} ≈ 2^{30} ≈ 10^9flops = 1s,φ^{67} ≈ 10^14flops = 10^5s = 1day φ=(1+5)/21.618φ43230109flops=1sφ671014flops=105s=1day

封底估算:介于猜测和铁证之间的一个概念,指的是一个很粗略的计算,不精确,但可以被用作对某个观点的支持或论据。本课程的封底估算是用flops(每秒所执行的浮点运算次数)估算算法的运行时间,1秒可执行10^9条浮点数指令。

4. 动态规划

定 义 : 动 态 规 划 和 分 治 法 相 似 , 都 是 通 过 组 合 子 问 题 的 解 来 求 解 原 问 题 。 不 同 的 是 , 动 态 规 划 颠 倒 计 算 方 向 , 由 自 顶 而 下 的 递 归 改 为 自 顶 而 上 迭 代 , 使 得 对 每 个 子 问 题 只 求 解 一 次 , 避 免 重 复 计 算 的 步 骤 。 定义:动态规划和分治法相似,都是通过组合子问题的解来求解原问题。不同的是,动态规划颠倒计算\\方向,由自顶而下的递归改为自顶而上迭代,使得对每个子问题只求解一次,避免重复计算的步骤。 使
在这里插入图片描述

int fib(int n)
{
	f = 1; g = 0;	// fib(-1), fib(0)
	while (0 < n--) 
	{
		g = g + f;
		f = g - f;
	}
	return g;
}

复 杂 度 : T ( n ) = O ( n ) , 而 且 仅 需 O ( 1 ) 空 间 复杂度:T(n) = O(n),而且仅需O(1)空间 T(n)=O(n)O(1)

*算法平台运行实例

1. TM运行实例

在这里插入图片描述
在这里插入图片描述

(<,1;0,L,<): 当前状态为<,且当前字符为1,则将当前字符修改为0,读写头转向左侧邻格,转入<状态,指令执行4(<,0;1,R,>): 当前状态为<,且当前字符为0,则将当前字符修改为1,读写头转向右侧邻格,转入>状态,指令执行1(<,#;1,R,>): 当前状态为>,且当前字符为0,指令不执行,可省略
(>,0;0,R,>): 当前状态为>,且当前字符为0,则将当前字符修改为0,读写头转向右侧邻格,转入>状态,指令执行4(>,#;#,L,h): 当前状态为>,且当前字符为#,则将当前字符修改为#,读写头转向左侧邻格,转入h状态并停机

2. RAM运行实例

在这里插入图片描述

RAM算法:
0	R[3] <- 1			
1	R[0] <- R[0] + R[3]	// c = 1 + c
2	R[0] <- R[0] - R[1]	// c -= d
3	R[2] <- R[2] + R[3]	// x++
4	IF R[0] > 0 GOTO 2	// c > 0 循环
5	R[0] <- R[2] - R[3]	// else x--
6	STOP				// return x  

c = 1 + c;
for (x = 0; c > 0; x++)
	c -= d;
return x;
StepR[0]R[1]R[2]R[3]指令(IR)含义
012500R[3] <- 1
1^^^1R[0] <- R[0] + R[3]c = 1 + c
213^^^R[0] <- R[0] - R[1]c-=d
38^^^R[2] <- R[2] + R[3]x++
4^^1^IF R[0] > 0 GOTO 2c > 0
5^^^^R[0] <- R[0] - R[1]c-=d
63^^^R[2] <- R[2] + R[3]x++
7^^2^IF R[0] > 0 GOTO 2c > 0
8^^^^R[0] <- R[0] - R[1]]c-=d
90^^^R[2] <- R[2] + R[3]x++
10^^3^IF R[0] > 0 GOTO 2c < 0
11^^^^R[2] <- R[2] + R[3]x–
122^^^R[2] <- R[2] + R[3]return x
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值