算法基础
著名的图灵奖获得者Donald E. Knuth曾经说:"Computer science is the study of algorithm",也就是计算科学就是研究算法的科学。这门学科主要的学习方法是通过经典算法的学习,积累经验,触类旁通,举一反三。
算法设计这门课学的是发现解决问题的算法,同时证明算法是正确、有效和能被接受的。不仅仅是找到答案,更要证明答案。通过例子来学习算法的思想,然后搬到其他问题中去解决其他问题。
什么是算法?(基本概念)
算法是解决某个问题的一系列运算或操作。编译器和操作系统就是典型的例子。算法问题主要包括输入和输出
- 输入:明确了算法能够接受的合法输入
- 输出:明确对于一组合法的输入,相应所得到的结果是什么
数学公式:output=f(input),其中f(⋅)就表示算法。
伪代码
伪代码是一种能够将标准运行的程序通过简介明了的语言进行描述,其特点是保持了程序原本主要的结构,类似C语言等。语法规则如下:
- 赋值:←
- 分支:if...then...else
- 循环:while,for,repeat...until
- 输出:return
- 调用:写函数名
- 注释: //...
需要特别注意的是,伪代码不能照搬原代码,需要进行部分精简,不需要具体的实现流程,只需要写出算法思路流程,分为哪几步,不用详细写这几步的实现过程,只需要使用自然语言或者相应的函数替代就行。忽略数据结构、模块、异常处理等。忽略变量的说明。
Insert_Sort:
输入: n个元素的数组A;
输出: 按非降序排列的数组A;
for i <- 2 to n
x <- A[i]
j <- i - 1
while (j > 0) and (x < A[j])
A[j + 1] <- A[j]
j <- j - 1
A[j + 1] <- x
return A
算法复杂度
复杂度定义
算法的复杂度实际上是对算法执行次数的范围做一个估计。描述了一个算法执行次数至多和至少有多少,衡量一个算法的效率。描述的时候用到了高等数学中界的概念,一般有两种描述方式:
先设有任意正数K1,使得有f(n)≤K1,则称为f(n)的上界,记作f(n)=O(K1),其中K1=cg(n)且存在正数c和n0,对一切的n≥n0都成立。
同理,若任意正数K2,使得有f(n)≥K2,则称为f(n)的下界,记作f(n)=Ω(K2),其中K2=cg(n)且存在正数c和n0,对一切的n≥n0都成立。
如果,f(n)=O(g(n))且f(n)=Ω(g(n)),则记作f(n)=Θ(g(n)).
::: note O(g(n))与Ω(g(n))的含义
- O(g(n))表示该算法至多执行g(n)次(最多)
- Ω(g(n))表示该算法至少执行g(n)次(最少)
本质上算法复杂度会这样表示是因为简化表示,比如现在有一个程序的复杂度为nlogn+n−2,如果直接表示的话就是f(n)=nlogn+n−2,但这样表示不够方便,而且有很多不必要的元素,因为算法复杂度其实不需要一个很精确的表示,因为就算你表示得很精确又不能说明什么,何必多此一举;而且在某个区间上来说,nlogn≥n那我既然都不用表示那么完整了,何必又要再写n和2?直接用最大的那个表示出来就可以了。所以就可以写成O(nlogn).
- 不需要精确表示
- 不需要精确表示就可以对式子进行化简,留下最大那个
- 为什么留下最大那个,因为他起主导作用
:::
函数的渐近的界
设f1(n)=O(g1(n))和f2(n)=O(g2(n)),则
- f1(n)+f2(n)=O(max{g1(n),g2(n)})
- f1(n)⋅f2(n)=O(g1(n)⋅g2(n))
两种情况下的时间复杂度
- 最坏情况下时间复杂度: 算法求解输入规模为n的算法所需要的最长时间W(n)
- 平均情况下时间复杂度: 算法求解输入规模为n的算法所需要的平均时间A(n)
A(n)=I∈S∑tIpI 其中,S:实例集, tI:实例I基本操作次数, pI: I的概率
复杂度表示的是针对问题选择基本运算,将基本运算的次数(输入规模)为自变量的函数。
- 也就是说,复杂度中的n表示算法的输入规模,经过f(n)次运算之后消耗的时间。本质上时间复杂度就是一个表示操作次数的边界的函数。
复杂度计算
例1
7n2+6n+1=O(n2)
7n2=O(n)
logn!=O(nlogn)(logn=log2n)
logan=O(logn)
2100=O(1)
例2
3nlog(n!)+(n2+3)nlogn
3n=O(n),log(n!)=O(nlogn)⇒3nlog(n!)=O(n2logn)(n2+3)n=O(n3),logn=O(logn)⇒(n2+3)nlogn=O(n3logn)
所以,3nlog(n!)+(n2+3)nlogn=O(max{n2logn,n3logn})=O(n3logn)
例3
根据伪代码计算算法时间复杂度,每一行执行的操作的时间复杂度都要写上,有循环就用乘法,没有循环就用加法,将所有操作的时间复杂度加起来之后进行简化得到最终的时间复杂度。
递推方程求解
换元迭代
使用换元的方式对n进行处理。一般来说会用n=2k进行换元。通过多此迭代套娃直到计算到n=1
- 例
{W(n)=2W(2n)+n−1W(1)=0 令n=2k,则有W(n)=2W(2k−1)+2k−1=2[2W(2k−2)+2k−1−1]+2k−1=22W(2k−2)+2k−2+2k−1=22[2W(2k−3)+2k−2−1]+2k−2+2k−1]=...=2kW(1)+k2k−(2k−1+2k−2+...+2+1)=k2k−2k+1=nlogn−n+1
差消,化简迭代法
该法一般用于求解有比较复杂的式子的时候,通过作差消掉这些复杂项(复杂的项指的是那些求和符号之类的一些式子,跟正常纯字母的项不一样的)
- 例
{T(n)=n2∑i=1n−1T(i)+O(n),n≥2T(1)=0 T(n)=n2i=1∑n−1T(i)+cn2,n≥2nT(n)=2i=1∑n−1T(i)+cn2(1)(n−1)T(n−1)=2i=1∑n−2T(i)+c(n−1)2(2)nT(n)=(n+1)T(n−1)+O(n)(3)n+1T(n)=nT(n−1)+n+1c1=...=c1[n+11+n1+...+31]+2T(1)=c1[n+11+n1+...+31]=Θ(logn)T(n)=Θ(nlogn) (1)(2)两式作差得(3)式
递归树
将子问题进行b等分,
主定理
设a≥1,b>1为常数,f(n)为函数,T(n)为非负整数,且
T(n)=aT(n/b)+f(n)=aT(n/b)+O(nd)
则有以下结果:
- 若f(n)=O(nlogba−ϵ),ϵ>0,那么T(n)=Θ(nlogba)(a>bd)
- 若f(n)=Θ(nlogba),那么T(n)=Θ(nlogbalogn)(a=bd)
- 若f(n)=Ω(nlogba+ϵ),ϵ>0,且对于某个常数c<1和充分大的n有af(bn)≤cf(n), 那么T(n)=Θ(f(n))(a<bd)
::: danger 特别重要!
从本质上来说,上面几条规则都是原式的f(n)与nlogba比大小。
- f(n)<nlogba时,满足第一种情况
- f(n)=nlogba时,满足第二种情况
- f(n)>nlogba时,满足第三种情况
:::
- 例1:求解递推方程T(n)=9T(3n)+n
上述方程中a=9,b=3,f(n)=n, 那么nlog39=n2f(n)=n=O(nlog39−1)=O(n)(ϵ=1), 故满足第一种情况, T(n)=Θ(n2)
- 例2:求解递推方程T(n)=T(32n)+1
上述方程中a=1,b=23,f(n)=1, 那么nlog231=n0=1f(n)=1=O(nlog231)=O(1), 故满足第二种情况, T(n)=Θ(n0logn)=Θ(nlogn)
- 例3:求解递推方程T(n)=3T(4n)+nlogn
上述方程中a=3,b=4,f(n)=nlogn, 那么nlogn=Ω(nlog43+ϵ)=Ω(n0.793+ϵ)(ϵ≈0.2), 那么要使得af(bn)≤cf(n)成立,代入f(n)=nlogn, 得到43nlog4n≤cnlogn, 不等式解得c≥43,就可成立,满足第三种情况, 故得T(n)=Θ(f(n))=Θ(nlogn).