一、递归
递归函数的定义:函数直接或间接调用函数本身,形如:
Func(Type a, ….) {
……; Func(Type a1, …) ; Func(Type a2, …) ; ……
}
重要特性:
(1)函数是“执行特定任务”的一种封装,独立实现某项功能。递归函数中的同名自我调用,决定了函数本身和被调用函数是在处理同类的事务、实现同类的功能。
(2)通过函数参数(代码中的a, a1, a2)的变化,可以动态实现同样一个事务、功能在不同规模下的问题求解过程
递归算法特点:
优点:结构清晰,可读性强,容易用数学归纳法来证明算法的正确性,为设计算法、调试程序带来很大方便。
缺点:运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。
递归的思考模板:
(1)欲知__A__如何,只需求出___a____
(2)a 求出后,进一步求 A
二、分治
分治策略:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
分治法应用
合并排序
基本思想:将待排序元素分成大小大致相同的2个子集合,分别对2个子集合进行排序,最终将排好序的子集合合并成为所要求的排好序的集合
void MergeSort(Type a[], int left, int right)
{
if (left<right)
{ //至少有2个元素
int i=(left+right)/2; //取中点
mergeSort(a, left, i);
mergeSort(a, i+1, right);
merge(a, b, left, i, right); //合并到数组b
copy(a, b, left, right); //复制回数组a
}
}
快速排序
快速排序是基于分治策略的一个排序算法。
其基本思想是,对于输入的子数组a[p:r],
按以下三个步骤进行排序。
(1)分解(Divide);
(2)递归求解(Conquer);
(3)合并(Merge)。
template<class Type>
void QuickSort (Type a[], int low, int high)
{
if (low < high) {
int q=Partition(a, low, high);
QuickSort (a, low,q-1); //对左半段排序
QuickSort (a,q+1, high); //对右半段排序
}
}
int Partition(SqList &L,int low,int high)
{ //交换L中子表L.r[low..high]记录,枢轴到位,返回其位置 L.r[0]=L.r[low]; pivotkey=L.r[low].key; //用第一个记录作枢轴
while(low<high) //从表的两端交替地向中间扫描
{ while(low<high&&L.r[high].key>=pivotkey) --high;
L.r[low]=L.r[high]; // 将比枢轴记录小的记录交换到低端
while(low<high&&L.r[low].key<=pivotkey) ++low;
L.r[high]=L.r[low]; // 将比枢轴记录大的记录交换到高端
}//while
L.r[low]=L.r[0];
return low; // 返回枢轴所在位置
}
快速排序算法的性能取决于划分的对称性。通过修改算法partition,可以设计出采用随机选择策略的快速排序算法。在快速排序算法的每一步中,当数组还没有被划分时,可以在a[low:high]中随机选出一个元素作为划分基准,这样可以使划分基准的选择是随机的,从而可以期望划分是较对称的。
最坏时间复杂度:O(n2)
平均时间复杂度:O(nlogn)
template<class Type>
int RandomizedPartition (Type a[], int low, int high)
{
int i = Random(low, high);
Swap(a[i], a[low]);
return Partition (a, low, high);
}
线性时间选择
给定线性序集中n个元素和一个整数k,1≤k≤n,要求找出这n个元素中第k小的元素。
template<class Type>
Type RandomizedSelect(Type a[],int p,int r,int k)
{
if (p==r) return a[p];
int i=RandomizedPartition(a,p,r),
j=i-p+1;
if (k<=j) return RandomizedSelect(a,p,i,k);
else return RandomizedSelect(a,i+1,r,k-j);
}
如果能在线性时间内找到一个划分基准,使得按这个基准所划分出的2个子数组的长度都至少为原数组长度的ε倍(0<ε<1是某个正常数),那么就可以在最坏情况下用O(n)时间完成选择任务。
例如,若ε=9/10,算法递归调用所产生的子数组的长度至少缩短1/10。则在最坏情况下,算法所需的计算时间T(n)满足递归式T(n)≤T(9n/10)+O(n) 。由此可得T(n)=O(n)。
最接近点问题
给定平面S上n个点,找其中的一对点,使得在n(n-1)/2 个点对中, 该点对的距离最小。
- n较小时直接求 (n=2);
n较大时,将S上的n个点分成大致相等的2个子集S1和S2,分别求S1
和S2中的最接近点对; - 求一点在S1、另一点在S2中的最近点对;
- 从上述三对点中找距离最近的一对。
double Cpair1(S)
{
n=|S|;
if (n < 2) {d = ∞; return false;}
m=S中各点坐标的中位数; //时间O(n)
构造S1和S2;//S1={x∈S|x<=m}, S2={x∈S|x>m}
d1 = Cpair1(S1);
d2 = Cpair1(S2);
p = max(S1);
q = min(S2);
d=min(d1,d2, q-p);
return d;
}
二维的情形
证明:将矩形R的长为2d的边3等分,将它的长为d的边2等分,由此导出6个(d/2)×(2d/3)的矩形。若矩形R中有多于6个S中的点,则由鸽舍原理易知至少有一个(d/2)×(2d/3)的小矩形中有2个以上S中的点。设u,v是位于同一小矩形中的2个点,则
即,distance(u,v)<d,这与d的意义相矛盾。
double cpair2(S)
{
n=|S|;
if (n < 2) return ;
1、m=S中各点x间坐标的中位数;
构造S1和S2;//S1={p∈S|x(p)<=m}, S2={p∈S|x(p)>m}
2、d1=cpair2(S1); //递归调用
d2=cpair2(S2); //递归调用
3、dm=min(d1,d2);
4、设P1是S1中距垂直分割线l的距离在dm之内的所有点组成的集合;
P2是S2中距分割线l的距离在dm之内所有点组成的集合;
将P1和P2中点依其y坐标值排序;
设X和Y是相应的已排好序的点列;
5、通过扫描X以及对于X中每个点检查Y中与其距离在dm之内的所有点
(最多6个)可以完成合并;
当X中的扫描指针逐次向上移动时,Y中的扫描指针可在宽为2dm的
区间内移动;
设dl是按这种扫描方式找到的点对间的最小距离;
6、d=min(dm,dl);
return d;
}
double cpair2(S)
{
n=|S|;
if (n < 2) return ;
循环赛日程表
设有n=2k个运动员要进行网球循环赛,请设计一个满足以下要求的比赛日程表:
(1)每个选手必须与其他n-1个选
手各赛一次;
(2)每个选手一天只能赛一次;
(3)循环赛一共进行n-1天。
算法思路:在每次迭代求解的过程中,可以看作4部分。
1)求左上角子表:左上角子表是前2^(k-1)个选手的比赛前半程的比赛日程。
2)求左下角子表:左下角子表是剩余的2^(k-1)个选手的比赛前半程比赛日程。这个子表和左上角子表的对应关系为对应元素等于左上角子表对应元素加2 ^(k-1)。
3)求右上角子表:等于左下角子表的对应元素。
4)求右下角子表:等于左上角子表的对应元素。
邮局选址问题