第一部分 基础知识
第一章 算法在计算中的作用
算法:把输入转换成输出的计算步骤的一个序列。
算法问题的共有特征:存在多个候选解;存在实际应用。
数据结构:一种存储和组织数据的方式。
第二章 算法基础
2.1 插入排序 insertion-sort
输入:含有 n 个数的序列。
输出:排序好的序列
特点:对于少量元素的排序,是一个有效的算法。
def insertion_sort(a): # a为要排序的数组或列表
for i in range(0, len(a)-1):
j = i # 从第i位开始比较
while j >= 0 and a[j] > a[j+1]: # 若下一个数较小,就交换值,让小的数前移一位
a[j], a[j+1] = a[j+1], a[j]
j -= 1 # 前移一位后,这个数需要再和前面的数比较,故j减1
return a
在如上代码的循环中加上 print 语句,观察其输出。对于最差的情况(完全反向),第一个 for 循环把前两位排对,while 循环执行 1 次;第二个 for 循环把前 3 位排对,while 执行 2 次;第三个 for 排对前 4 位,while 执行 3 次。可以发现其中规律是,排序 n 个数,最多会需要执行 (n2−n)/2(n^2-n)/2(n2−n)/2 次 while 循环。
def insertion_sort(a):
for i in range(0, len(a)-1):
j = i
while j >= 0 and a[j] > a[j+1]:
a[j], a[j+1] = a[j+1], a[j]
j -= 1
print('w-'+str(a), end=', ')
print('f-'+str(a))
insertion_sort([4, 3, 2, 1]) # 调用函数
w-[3, 4, 2, 1], f-[3, 4, 2, 1]
w-[3, 2, 4, 1], w-[2, 3, 4, 1], f-[2, 3, 4, 1]
w-[2, 3, 1, 4], w-[2, 1, 3, 4], w-[1, 2, 3, 4], f-[1, 2, 3, 4]
2.2 分析算法
算法需要的时间一般与输入的规模同步增长。
输入规模:其量度视所研究的问题而定。
运行时间:是指执行的基本操作数。从插入排序可以看出,相同规模的输入因初始排序状况不同,算法运行的步数也不同。分析时一般计算的是最大运行时间。
增长量级:运行时间算出后,做一些简化会使分析更容易。把每条语句的运行代价简化为1,能得到关于输入规模 n 的一个函数;当 n 的值很大时,函数内低阶项的影响相应会小,也可忽略,最后得到的结果就是该算法的增长量级,如 an2+bn+can^2+bn+can2+bn+c 会简化为 θ(n2)θ(n^2)θ(n2)。一般认为最坏运行时间的增长量级小的算法更好。
2.3 设计算法
算法的设计方法有很多,插入排序使用增量法,这一节介绍分治法。
2.3.1 分治法
分治思想:把原问题分解为几个规模小但类似原问题的子问题,递归地求解再合并这些解建立原问题的解。分治模式在每层递归都有三个步骤,分解原问题、解决子问题、合并解。
归并排序完全遵循分治思想,python 代码编写如下:
def merge_sort(a):
half = round(len(a)/2)
if half == 0: # 子组只含一个数就直接返回
return a
elif half == 1: # 子组有两个数就从小到大返回
return [a[0],a[1]] if a[0]<a[1] else [a[1],a[0]]
else: # 有多个数就分子组,递归
L = merge_sort(a[0:half])
R = merge_sort(a[half:])
# 这里只需要合并已排好序的两个子组L和R
result = []
i, j = 0, 0
lenL, lenR = len(L), len(R)
while i != lenl and j != lenr: # 设条件避免下标溢出上限
# 比较两列表的数,依次放到结果中,每次比较完下标加1
if left[i] < right[j]:
result.append(L[i])
i += 1
else:
result.append(R[j])
j +=1
# 加1后下标等于数组长度,说明某个子组已经比较完,
# 只需把另一子组的剩余数据加到结果中
if i == lenL:
result += R[j:]
break
elif j == lenR:
result += L[i:]
break
return result
代码中的 half 不使用 len(a)//2,而是用 round(len(a)/2),方便接下来的判断,因为2//2 和 3//2 都等于 1。
用流程图画出归并操作,如果有 2 个数,归并操作只有一层;有 4 个数,归并会有两层;8 个数归并三层;以此类推,n 个数会归并 log2n 层。每一层的每一个数都要比较大小并转移到新数组,这样的操作总共 n 次。这样能比较直观地理解为什么归并排序的增长量级为 θ(nlgn)。
2.3.2 分析分治算法
归并算法对规模为 n 的数组进行排序的操作,相当于对规模 n/2 的两个子组排序再把结果合并的操作。设归并排序的运行时间为 T(n)T(n)T(n),合并操作需要时间 θ(n)θ(n)θ(n),则:T(n)={θ(1)当 n=12T(n/2)+θ(n)当 n>1T(n) = \begin{cases} θ(1) & \text{当 } n=1 \\ 2T(n/2)+θ(n) & \text{当 } n>1 \end{cases}T(n)={θ(1)2T(n/2)+θ(n)当 n=1当 n>1 。
第三章 函数的增长
算法的渐近效率:输入规模无限大时算法的效率。
3.1 渐近记号
- θ 记号
θ(g(n)) 表示以下集合:
θ(g(n))={f(n):存在正常量c 1 ,c 2 和n 0 ,使对所有n≥n 0 ,有0≤c 1 g(n)≤f(n)≤c 2 g(n)}\qquad θ(g(n)) = \{ f(n): 存在正常量c~1~,c~2~ 和 n~0~,使对所有 n ≥ n~0~,有 0 ≤ c~1~g(n) ≤ f(n) ≤ c~2~g(n) \}θ(g(n))={f(n):存在正常量c 1 ,c 2 和n 0 ,使对所有n≥n 0 ,有0≤c 1 g(n)≤f(n)≤c 2 g(n)}
把 g(n) 称为 f(n) 的一个渐近紧确界。
一般地对于任意多项式 p(n) = a0n0 + a1n1 + a2n2 + … + adnd,有 p(n) = θ(nd)。这里的 = 号是属于该集合的意思。 - O 记号
θ 记号渐近地给出了函数的上界和下界。当只有一个上界时,使用 O 记号。O(g(n))表示以下函数的集合:
O(g(n))={f(n):存在正常量c和n 0 ,使对所有n≥n 0 ,有0≤f(n)≤cg(n)}\qquad O(g(n)) = \{ f(n): 存在正常量 c 和 n~0~,使对所有 n ≥ n~0~,有 0 ≤ f(n) ≤ cg(n) \}O(g(n))={f(n):存在正常量c和n 0 ,使对所有n≥n 0 ,有0≤f(n)≤cg(n)}
对于给定的 n,算法的实际运行时间是变化的。通常说“运行时间为 O(n2)” 时,意思是存在一个属于 O(n2) 的函数 f(n),使得对于 n 的任意值,不管选择什么规模为 n 的输入,运行时间的上界都是 f(n)。 - Ω 记号
Ω 记号提供了函数的渐进下界。
Ω(g(n))={f(n):存在正常量c和n 0 ,使对所有n≥n 0 ,有0≤cg(n)≤f(n)}\qquad Ω(g(n)) = \{ f(n): 存在正常量 c 和 n~0~,使对所有 n ≥ n~0~,有 0 ≤ cg(n) ≤ f(n) \}Ω(g(n))={f(n):存在正常量c和n 0 ,使对所有n≥n 0 ,有0≤cg(n)≤f(n)} - 等式和不等式中的渐近记号
渐进记号独立出现在等式右端的情况如 f(n) = O(g(n)),视作 f(n) ∈ O(g(n))。
而当渐近号出现在公式中,如 T(n) = T(n/2) + θ(g(n)),表示只对 T(n) 的渐进行为感兴趣,使用渐近号表示的匿名函数消除无关紧要的细节。
注意:∑θ(i) 不同于 θ(1) + θ(2) + θ(3) + … + θ(n),后者的每个渐近记号都包含不同的细节。
渐近号出现在等式左边,2n2 + θ(n) = θ(n2),是说无论怎样的 f(n) = θ(n),总存在 g(n) = θ(n2) 使等式成立。意指等式右边的细节比左边粗糙。 - o 记号
O 提供的渐进上界不一定是渐近紧确的,记号o 表示一个非渐近紧确的上界。
o(g(n))={f(n):对任意正常量c和n 0 ,使对所有n≥n 0 ,有0≤f(n)<cg(n)}\qquad o(g(n)) = \{ f(n): 对任意正常量 c 和 n~0~,使对所有 n ≥ n~0~,有 0 ≤ f(n) < cg(n) \}o(g(n))={f(n):对任意正常量c和n 0 ,使对所有n≥n 0 ,有0≤f(n)<cg(n)}
记号 o 和 O 的区别:O(g(n)) 表示的上界只对某个常量 c >0 成立;o(g(n)) 表示的上界对所有 c >0 成立。 - ω 记号
ω 记号和 Ω 记号的关系与 o 和 O 的关系类似。
ω(g(n))={f(n):对任意正常量c和n 0 ,使对所有n≥n 0 ,有0≤cg(n)<f(n)}\qquad ω(g(n)) = \{ f(n): 对任意正常量 c 和 n~0~,使对所有 n ≥ n~0~,有 0 ≤ cg(n) < f(n) \}ω(g(n))={f(n):对任意正常量c和n 0 ,使对所有n≥n 0 ,有0≤cg(n)<f(n)} - 渐近记号的性质
- 传递性(所有渐近记号均符合)
若 f(n) = θ(g(n)) 且 g(n) = θ(h(n)),则 f(n) = θ(h(n))。 - 自反性(θ、Ω、O)
f(n) = θ(f(n)) - 对称性(θ)
f(n) = θ(g(n)) 当且仅当 g(n) = θ(f(n)) - 转置对称性
f(n) = O(g(n)) 当且仅当 g(n) = Ω(f(n))
f(n) = o(g(n)) 当且仅当 g(n) = ω(f(n))
对于两个实数 a,b,一定有 a>b,a=b,a<b 中的一种,即所有实数都能做比较。但不是所有函数都能渐进比较,可能 f(n) = O(g(n)) 和 g(n) = Ω(f(n)) 都不成立。
3.2 标准记号和常用函数
回顾了一下常用的数学函数的性质。
单调性:单调递增、单调递减、严格递增、严格递减。
模运算:若 a 与 b 除以 n 后有相同的余数,就称模 n 时 a 等价于 b,记为 a ≡ b(mod n)。
一些对数公式:
a=blogbalogban=nlogbalogba=logcalogcbalogbc=clogba
\\ a = b ^ {{ \log_b } ^a}
\\ \log_b a^n = n\log_b a
\\ \log_b a = \frac{\log_c a} {\log_c b}
\\ a^{\log_b c} = c^{\log_b a}
a=blogbalogban=nlogbalogba=logcblogcaalogbc=clogba
阶乘公式:n!={1若 n=0n∗(n−1)!若 n>0n! = \begin{cases} 1 &\text{若 } n=0 \\ n*(n-1)! &\text{若 } n>0 \end{cases}n!={1n∗(n−1)!若 n=0若 n>0
多重函数、多重对数函数、斐波那契函数
第四章 分治策略
递归情况:子问题够大,需要递归求解。
基本情况:子问题够小,不需要递归。
递归式的三种求解方法:代入法、递归树法、主方法。
递归式的技术细节:实际应用中有时忽略一些声明和求解的细节,还有一些边界条件。
4.1 最大子组问题
问题:给定数组 a,寻找 a 的和最大的非空连续子数组 (a 中含有负数时问题才有意义)。
- 用分治策略求解
分解:最终得到的目标子数组在 a 中的位置有三种,在数组 a 中点的左侧、在中点右侧、跨过 a 的中点。
解决子问题:左侧、右侧的情况相当于找 a左、a右的最大子组(递归);跨中点的情况则直接从中点开始向左、向右求和就能得出。
合并解:三种情况求出的三个子组,选择和最大的子组,返回其位置。
以下为代码,函数 find_cross_mid 寻找跨中点的最大子组;find_max 递归求得数组 a 的最大子组。
def find_cross_mid(a): # 寻找跨过中点的最大子组
leng = round(len(a)/2)
lsum = rsum = -float('inf') # 初始化和为负无穷大
sum = 0
for i in a[leng::-1]: # 从中点向左侧寻找
sum += i
if lsum < sum:
lsum = sum
lmax = a.index(i) # 得到最大子组的左端索引值
sum = 0
for j in a[leng:]: # 从中点向右侧寻找
sum += j
if rsum < sum:
rsum = sum
rmax = a.index(j) # 最大子组的右端索引值
maxsum = lsum + rsum - a[leng] # 得到跨中点的最大子组的和
return lmax, rmax+1, maxsum # 注意右端索引值要加1
def find_max(a):
leng = round(len(a)/2)
if leng == 0: # 长度为1直接返回
return (0, 0, a[0])
else: # 长度大就分三种情况,分别求解再比较
left = find_max(a[0: leng]) # 求左侧存在的最大子组
right = find_max(a[leng:]) # 求右侧的最大子组
mid = find_cross_mid(a) # 求跨中点的最大子组
if left[2] >= right[2] >= mid[2]:
return left
elif right[2] >= left[2] >= mid[2]:
return right
else:
return mid
- 分治算法的分析
函数 find_max 中,前面判断是否长度为 1 的部分以及后面对三种情况的比较判断的运行时间都是常量,可忽略。find_max 函数对规模为 n 的数组运行时间设为 T(n)T(n)T(n),find_cross_mid 函数对规模为 n 的数组,运行时间为 θ(n)θ(n)θ(n),则有:T(n)={θ(1)当 n=12T(n/2)+θ(n)当 n>1T(n) = \begin{cases} θ(1) & \text{当 } n=1 \\ 2T(n/2)+θ(n) & \text{当 } n>1 \end{cases}T(n)={θ(1)2T(n/2)+θ(n)当 n=1当 n>1 。这和归并排序的递归式一样。
4.2 矩阵乘法的 Strassen 算法
4.3 用代入法求解递归式
代入法求解递归式分为两步:1. 猜测解的形式;2. 用数学归纳法求出解中的常数并证明解是正确的。
数学归纳法要求证明这个解在边界条件下也成立,有时这并不容易。这时可以把这个麻烦的边界条件从归纳证明中移除。
做出好的猜测需要经验。画递归树能对猜测有所启发。也能先证明一个较松的上界下界,再逐渐缩小范围。有时猜测到了正确的渐近界,却无法归纳证明,这时修改猜测,将其减去一个低阶项可能会有用。通过代数运算改变递归式的变量,把递归式变成熟悉的形式更易于猜测。
4.4 用递归树法求解递归式
画递归树是设计好的猜测的简单直接的方法。
递归树中,每个节点表示单一子问题的代价,把每层的代价求出,再算出所有层的代价就是递归地总代价。
4.5 用主方法求解递归式
主方法适合用来求解形如 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)T(n)=aT(n/b)+f(n) 的递归式。
这个形式意思是:把规模为 nnn 的问题分为 aaa 个子问题,每个子问题规模是 n/bn/bn/b,其中 a≥1,b>1a ≥ 1,b>1a≥1,b>1。函数 f(n)f(n)f(n) 包含了分解和合并的代价 (此处忽略舍入问题,n/b 为取整后的结果)。
只要记住 3 种情况,就能用主方法很容易地求解递归式。
主定理:常数 a≥1,b>1,f(n)a≥1,b>1,f(n)a≥1,b>1,f(n) 为函数,定义在非负整数上的递归式 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)T(n)=aT(n/b)+f(n) 有如下渐近界:
- 若对某个常数 ϵ>0\epsilon \gt 0ϵ>0,有 f(n)=O(nlogba−ε)f(n) = O(n^{log_b a - ε})f(n)=O(nlogba−ε),则 T(n)=θ(nlogba)T(n) = θ(n^{log_b a})T(n)=θ(nlogba);
- 若 f(n)=θ(nlogba)f(n) = θ(n^{log_b a})f(n)=θ(nlogba),则 T(n)=θ(nlogbalgn)T(n) = θ(n^{log_b a}lgn)T(n)=θ(nlogbalgn);
- 若对某个常数 ϵ>0\epsilon \gt 0ϵ>0,有 f(n)=Ω(nlogba+ε)f(n) = Ω(n^{log_b a + ε})f(n)=Ω(nlogba+ε),且对某个常数 c<1 和足够大的 n 有 af(n/b)≤cf(n)af(n/b) ≤ cf(n)af(n/b)≤cf(n),则 T(n)=θ(f(n))T(n) = θ(f(n))T(n)=θ(f(n))。
主定理把 f(n)f(n)f(n) 和 nlogban^{log_b a}nlogba 作比较,两个函数较大的就是递归式的解,大小相当时要乘上一个对数因子。这里的比较是多项式意义上的比较,即二者必须相差一个因子 nϵn^\epsilonnϵ。平常遇到的多项式界的函数,多数都满足这个条件。但也有不满足的,而且不是所有函数都能渐近比较,这样的递归式不能用主方法求解。
1083

被折叠的 条评论
为什么被折叠?



