数据结构,邓俊辉老师
平台——学堂在线 https://www.xuetangx.com/course/THU08091000384/5883586?channel=learn_title
第一章 绪论
迭代与递归
迭代乃人工,递归方神通
To iterate is human, to recurse, divine
学习内容上,螺旋式的上升
从递归程序转到——> 高效的迭代,重点
凡治众如治寡,分数是也 —— 《孙子兵法》
空间复杂度 = 累加器 + 中间循环的控制变量,即除输入本身占的空间以外的,用于计算的必须的空间,即不考虑输入占用的空间
关于问题,计算任意n个整数之和,时间复杂度O(n),空间复杂度O(2),空间上只需要一个累加器和一个循环体的控制变量
+理解,剩余的有效的问题规模,随着已经参与统计的元素个数的增加而降低
通过逐步蚕食,不断削减问题有效规模的策略
—— 本节课重点
减而治之
法一,递归跟踪分析
1、原问题—— 转化为线性递归
代码:
sum(int A[], int n){
return (n < 1)? 0 : sum(A, n-1) + A[n-1];
}
线性思考思路:略
时间复杂度:
The complexity is O(n) because there are n instances each requiring O(1) execution time.
由于单个递归实例需要O(1)时间完成,共有n个实例,所以整个算法的复杂度是O(n)
局限:仅适用于简明的递归模式
法二, 递推分析方法
如果递推跟踪是几何,递推分析就是代数
递推方程:T(n) = T(n-1) + O(1)
边界条件:T(0) = O(1)
理解:
首先花T(n-1)的时间去求解了规模为n-1的问题,再花上O(1)的时间将它的解和平凡问题的解合并
求解方法:
对递推方程左右各减去一个n——> T(n) - n = T(n - 1) -(n - 1) =… = T(1) - 1 = T(0) ——> 挪动n到方程右边,得到T(n) = O(1) + n = O(n)
结果,与刚才的递归跟踪分析方法,殊途同归
减而治之实例,数组倒置,递归版
任给数组A[0, n) (此处采用Python语法,左闭右开),就像翻烙饼一样,前后颠倒
统一接口: void reverse(int* A, int lo, int hi)
思维上直观表示:
+代码
if(lo < hi)
{swap(A[lo], A[hi]); reverse(A, lo+1, hi-1); }
// 隐含 else return;
如果lo和hi之间含有足够多的元素,还没有到退化的情况,我们就将当前的lo和当前的hi,两个元素彼此互换
接下来,递归地去求解一个更小的问题,具体来说从lo+1,到hi-1,问题规模减少为原规模-2个元素
+问题 我们的递归基足以应用在所有情况吗?
自己验证:区间宽度偶数时,符合前序思路;区间宽度奇数时,假设-2次递归传入(lo,hi)为(1,3),-1次递归为(2,2),
当(2,2)时,不符合lo<hi, 因此不互换,总结,在该逻辑下运行程序,区间奇偶性递归基相同;但是,要注意到:程序隐含的else,对应了两个递归基,原因是,在问题规模缩减为0或1时,都需要停止递归;而非一种情况下就停止
+迭代法精简代码
while(lo < hi) swap(A[lo++], A[hi--]);
参照上述所给的两种方法,分析递归法的时间复杂度,两种方法相互佐证
+自己推导过程截图:
pad导入
法三,大师定理(原排序在本节最后部分)
决定因素:a, b, O(f(n))
如下方例子,二分递归
a, b = 2,2, logb(a) = 1
O(n) 与 O (1)的关系符合截图中情况1,因而T(n) = O(n)
分而治之Divide and Conquer
数组求和,二分递归
区间范围A[lo, hi]
统一接口: void 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);
+思路
- 思路代入具体数值
时间复杂度T(n)= 各层递归实例所需时间之和
= O(1) * (2^0 + 2^1 +2^2 +… + 2 ^log(n))
= O(1) * (2^(log(n)+1) - 1) = n*2 - 1 = O(n)
+思维方法,
进一步,如何快速分析?
所有递归实例,在每一层构成以2为倍数的几何级数,而几何级数的总和与末项同阶
问题,底层有多少递归实例?n个,底层递归实例,对原始区间中每个元素有且仅有唯一的对应
——》 直接得出O(n)
从另一递归分析角度,也可得到相同的结论,此处略
概念建立—— 减而治之和分而治之,是强有力的方法
实例,在一个区间中找出两个最大数
区间范围A[lo, hi)
统一接口: void max2(int* A, int lo, int hi, int & x1, int & x2)
1)无分治的普通迭代法:
- 局限,并未改善时间效率
(2)分治思想下:
-
取lo和hi 的中点(1)
-
令x1L,x2L指向左区间最大的前两个元素;右区间则为x1R,x2R(2)
-
接下来,对子区间的最大者进行比较,接着比较被淘汰下来的数值和另一区间第二大的值(3)
-
无论如何,在每一个递归实例中,至多进行一次或两次比较
+思路,注意分析步骤(3)两种情况,此处略
+代码,注意边界条件(有两个递归基)
# 边界条件
if (lo+2 == hi) {if(A[x1=lo]<A[x2=lo+1]{swap(x1,x2)};return;}// 区间范围[low,hi), 此时区间中仅剩两个元素,直接比较,T(2) = 1
if (lo+3 == hi) {max2(A,lo,hi,x1,x2); return;}// 此时区间包含三个元素,调用max2()比较,T(3) <= 3
# 分治下的递归
int mi = (lo+hi) // 2
int x1L, x2L; max2(A, lo, mi, x1L, x2L);
int x1R, x2R; max2(A, mi, hi, x1R, x2R);
if(A[x1L)] > A[x1R]{
x1 = x1L; x2 = (A[x2L] > A[x1R]) ? x2L : x1R;
} else{
x1 = x1R; x2 = (A[x2R] > A[x1L]) ? x2R : x1L;
}
T(n) = 2 * T(n/2) + 2 <= 5n/3 - 2
n的常系数从2下降到5/3
自己计算:最坏情况下——T(n) = 2*T(n/2) + 2
= 2*(2*T(n/4) + 2)=…
= 2^(log2(n/3)) * T(3) +2^(log2(n/6)) *2^2
= 5n/3 - 2
迭代递归算法
两种策略,减而治之和分而治之
两种递归常用分析方法,后面看到更多更复杂的例子
+总结
To apply divide-and-conquer, we divide the original problem into two degenerated sub-problems, solve them, and merge their solutions.
分而治之的思想是:将问题划分为两个平凡的子问题,分别求解子问题,来得到原问题的解。
+The question: we see a comment in the code: “Two base cases are required”. What does it refer to? 需要两个递归基的含义是?
The sequence of recursive callls is terminated when the problem size is reduced to either 0 or 1. 在问题规模缩减为两种不同的情况时,停止递归
用分而治之的思想来解决长度为n的数组的求和问题(n足够大),递归实例的数目会比用减而治之的方法少。
-错,在这个问题上,分而治之和减而治之的效率相同,时间复杂度均为O(n).
分而治之求和的思维底层有n个递归实例且其层级间为几何级数关系,几何级数的复杂度与末项同阶;而减而治之求和的思维共有n个递归实例线性排列,递归到底端后,回溯执行,每次执行复杂度为O(1),故也是O(n)
+自己总结
细节上,注意递归基的个数和边界条件
思维上,灵活运用减而治之和分而治之,特别是分而治之
掌握不同分析复杂度的方法,包括直观的递归跟踪和抽象的递推分析,第三种大师法,在后续课程还会深入