note-算法导论

第一部分 基础知识

第一章 算法在计算中的作用

算法:把输入转换成输出的计算步骤的一个序列。
算法问题的共有特征:存在多个候选解;存在实际应用。
数据结构:一种存储和组织数据的方式。

第二章 算法基础

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(n2n)/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)。

5
1 5
1
7
3 7
3
6
2 6
2
8
4 8
4
1 3 5 7
2 4 6 8
1 2 3 4 5 6 7 8
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 使nn 0 0c 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):cn 0 使nn 0 0f(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):cn 0 使nn 0 0cg(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):cn 0 使nn 0 0f(n)cg(n)}
    记号 oO 的区别:O(g(n)) 表示的上界只对某个常量 c >0 成立;o(g(n)) 表示的上界对所有 c >0 成立。
  • ω 记号
    ω 记号和 Ω 记号的关系与 oO 的关系类似。
    ω(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):cn 0 使nn 0 0cg(n)f(n)}
  • 渐近记号的性质
  1. 传递性(所有渐近记号均符合)
    若 f(n) = θ(g(n)) 且 g(n) = θ(h(n)),则 f(n) = θ(h(n))。
  2. 自反性(θ、Ω、O)
    f(n) = θ(f(n))
  3. 对称性(θ)
    f(n) = θ(g(n)) 当且仅当 g(n) = θ(f(n))
  4. 转置对称性
    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=blog⁡balog⁡ban=nlog⁡balog⁡ba=log⁡calog⁡cbalog⁡bc=clog⁡ba \\ 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(n1)! 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>1a1b1。函数 f(n)f(n)f(n) 包含了分解和合并的代价 (此处忽略舍入问题,n/b 为取整后的结果)。
只要记住 3 种情况,就能用主方法很容易地求解递归式。
主定理:常数 a≥1,b>1,f(n)a≥1,b>1,f(n)a1b1f(n) 为函数,定义在非负整数上的递归式 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)T(n)=aT(n/b)+f(n) 有如下渐近界:

  1. 若对某个常数 ϵ>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)
  2. 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)
  3. 若对某个常数 ϵ>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ϵ。平常遇到的多项式界的函数,多数都满足这个条件。但也有不满足的,而且不是所有函数都能渐近比较,这样的递归式不能用主方法求解。

第五章 概率分析和随机算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值