递归与分治策略
第五周第一节跳过了
递归的概念
递归算法的执行过程分递推和回归两个阶段
在【栈区】
long f(int n)
{
if(n==0) return 1;
return n*f(n-1);
}
Fibonacci数列
int f(int n)
{
if (n <= 1) return 1; //0 和 1 都是 1
return f(n-1)+f(n-2);
}
去递归:
1、前2例中的函数都可以找到相应的非递归方式定义,一个公式
(Ackerman函数却无法找到非递归的定义,结论:并非一切递归函数都能用非递归方式定义)
📕例题1:(全排列)
//按字典序排列
#include<iostream>
#include<algorithm>
using namespace std;
int A[11];
void perm(int list[],int k,int m) //从k到 m 全排列
{
if(k==m)
{
for(int i=0;i<=m;i++) cout<<list[i];
cout<<endl;
}
else
{
for(int i=k;i<=m;i++)
{
sort(list+k,list+m+1); //加一个sort就可以字典序了
swap(list[k],list[i]);
perm(list,k+1,m);
swap(list[k],list[i]);
}
}
}
void swap(int &a,int &b)
{
int temp=a;a=b;b=temp;
}
int main()
{
int n;cin>>n;
for(int i=0;i<n;i++){
A[i]=i+1;
}
perm(A,0,n-1);
}
非字典序
#include<iostream>
using namespace std;
int A[11];
void perm(int list[],int k,int m) //从k到 m 全排列
{
if(k==m)
{
for(int i=0;i<=m;i++) cout<<list[i];
cout<<endl;
}
else
{
for(int i=k;i<=m;i++)
{
swap(list[k],list[i]);
perm(list,k+1,m);
swap(list[k],list[i]);
}
}
}
void swap(int &a,int &b)
{
int temp=a;a=b;b=temp;
}
int main()
{
int n;cin>>n;
for(int i=0;i<n;i++){
A[i]=i+1;
}
perm(A,0,n-1);
}
分治法的基本思想
对这k个子问题分别求解。如果子问题的规模仍然不够小,则再划分为k个子问题,如此递归的进行下去,直到问题规模足够小,很容易求出其解为止
将求出的小规模的问题的解合并为一个更大规模的问题的解,【自底向上】逐步求出原来问题的解
分治法所能解决的问题一般具有以下几个特征:
1、该问题的规模缩小到一定的程度就可以容易地解决;
2、问题的最优解包含其子问题的最优解,即最优子结构性质
3、利用问题分解出的子问题的解可以合并为原问题的解;
4、该问题所分解出的各个子问题是【相互独立】的,即子问题之间不包含公共的子问题。
特征4涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的重复工作,重复地解公共的子问题,此时虽然也可用分治法,但一般用动态规划较好
人们从大量实践中发现,在用分治法设计算法时,最好【使子问题的规模大致相同】。
即将一个问题分成大小相等的k个子问题的处理方法是行之有效的。-- 平衡子问题
一个分治法将规模为n的问题分成规模为n/m的子问题,其中k个子问题需要求解(k>=1, m>1) 。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
通过迭代法求得方程的解:
看这里的博文
主定理(求分治法的时间复杂度)
T(n)=kT(n / m)+nd
练习:T(n)=16T(n/4)+n
k=16,m=4,d=1,有 k>md , T(n)=θ(n2)
练习:T(n)=T(3n/7)+1
k=1,m=7/3,d=0,有 k=md , T(n)=θ(logn)
并非所有递推式都可用主定理求解
如:T(n)=2T(n/2)+nlogn
T(n)=T(n-1)+1
📕例题2:(二分搜索)
最坏情况下时间复杂度:O(logn) 以二为底
#include<iostream>
using namespace std;
int BinarySearch(int arr[],const int& x,int left,int right)
{
while(left<=right)
{
int m=(left+right)/2;
if(x==arr[m]) return m;
if(x<arr[m]) right=m-1;
else left=m+1;
}
return -1;
}
int main()
{
int A[11];
int n;cin>>n;
for(int i=0;i<n;i++){
A[i]=i+1; //1 2 3
}
cout<<BinarySearch(A,2,0,n-1); //返回数字2的下标 1
}
#include<iostream>
using namespace std;
//递归法
int BinarySearch(int arr[],const int& x,int left,int right)
{
if(left>right) return -1;
int m=(left+right)/2;
if(x==arr[m]) return m;
if(x<arr[m]) return BinarySearch(arr,x,left,m-1);
else return BinarySearch(arr,x,m+1,right);
}
int main()
{
int A[11];
int n;cin>>n;
for(int i=0;i<n;i++){
A[i]=i+1; //1 2 3
}
cout<<BinarySearch(A,2,0,n-1); //返回数字2的下标 1
}
大整数的乘法
设计有效的算法,进行两个n位二进制大整数的乘法
运算
二进制乘法 : 0×0=0 1×0=0 0×1=0 1×1=1
传统的方法:O(n^2^) 效率太低
【例】求 (1110)2 乘(101)2 之积
解: 1 1 1 0 × 1 0 1 ----------------- 1 1 1 0 0 0 0 0 1 1 1 0 ------------------ 1 0 0 0 1 1 0
问题规模缩小了一半
ac、ad、bc、bd 四个子问题
Strassen(斯特拉森)矩阵乘法
几种排序方法的时间复杂度比较
排序方法 | 平均时间 | 最坏情况 |
---|---|---|
简单排序 | O(n2) | O(n2) |
快速排序 | O(nlogn) | O(n2) |
堆排序 | O(nlogn) | O(nlogn) |
合并排序 | O(nlogn) | O(nlogn) |
合并排序
基本思想:当n=1时终止排序,否则将待排序元素分为大小大致相同的2个子集合,分别对2个子集合进行排序,最终将拍好序的子集合合并称为所要求的排好序的集合。 O(n)
void MergeSort(int a[],int left,int right)
{
if(left<right) //至少有 2个元素
{
int mid=(left+right)/2; //取中点
mergeSort(a,left,mid); //递归调用自身
mergeSort(a,mid+1,right);
merge(a,b,left,mid,right);//合并到数组 b
copy(a,b,left,right); //复制回数组 a
}
}
■从分治的策略入手,容易消除算法中的递归。
■事实上,算法mergesort的递归过程只是将【待排序集合一分为二,直至集合中只剩下一个元素为止】。按此机制,将数组a中相邻元素两两配对,用合并算法排序,构成n/2组长度为2的排好序的子数组段,然后再将他们排序成n/4组长度为4的排好序的子数组段,如此继续下去直到整个数组排好序
MergePass
Merge
另一个方法:自然合并排序
📕合并排序代码
#include<iostream>
using namespace std;
void MergeSort(int a[],int n);
void MergePass(int a[],int b[],int s,int n);
void Merge(int c[],int d[],int l,int m,int r);
//自顶向下逐步细分
//消去递归后的合并排序算法
void MergeSort(int a[],int n)//n个元素
{
int *b=new int[n];
int s=1;
while(s<n)
{
MergePass(a,b,s,n); //每s一小段合并到数组 b
s+=s; //s要翻倍
if(s>=n){
for(int i=0;i<n;i++) a[i]=b[i];
break;
}
MergePass(b,a,s,n); //合并到数组 a
s+=s;
}
}
//MergePass函数用于合并排好序的相邻数组段
void MergePass(int a[],int b[],int s,int n)
{
int i=0;
while(i+2*s-1<=n-1) //最后一个值还在数组下标最大值内,没有越界
{
Merge(a,b,i,i+s-1,i+2*s-1);//合并到数组 b
i=i+2*s;
} ///合并大小为 s 的相邻 2 段子数组
//剩下元素小于 2s
if(i+s-1<n-1)Merge(a,b,i,i+s-1,n-1); //处理零头 1-3个元素
else for(int j=i;j<=n-1;j++) b[j]=a[j]; //只有一段时1-2个元素
}
void Merge(int c[],int d[],int l,int m,int r)
{
//合并 c[l:m] 和 c[m+1:r] 到 d[l:r]
int i=l,j=m+1,k=l;
while(i<=m&&j<=r)
{
if(c[i]<=c[j]) d[k++]=c[i++];
else d[k++]=c[j++];
}
if(i>m) for(int q=j;q<=r;q++) d[k++]=c[q]; //左边这一半填完了,就把右边全放进去就行了
else for(int q=i;q<=m;q++) d[k++]=c[q];
}
int main()
{
int A[11]={0};
int n;cin>>n;
for(int i=0;i<n;i++){
cin>>A[i];
}
MergeSort(A,n);
for(int i=0;i<n;i++){
cout<<A[i]<<" ";
}
}
📕快速排序(代码)
劈成两半,左边比右边的小
…
在快速排序中,元素的 比较和交换是从两端向 中间进行的,值较大的 元素一次就能交换到后 面单元,值较小的记录 一次就能交换到前面单
元,元素每次移动的距 离较大,而总的比较和 移动次数较少
若左边特别大,是9,则最后i 和 j 都指向 8
快速排序的运行时间与划分是否对称有关
#include<iostream>
using namespace std;
void QuickSort(int a[],int p,int r);
int Partition(int a[],int p,int r);
void QuickSort(int a[],int p,int r) //p左边 r右边
{
if(p<r) //至少有两个数待排序
{
int q=Partition(a,p,r); //把p到r分割成两半 q是中点
QuickSort(a,p,q-1); //对左半段排序
QuickSort(a,q+1,r); //对右半段排序
}
}
int Partition(int a[],int p,int r)
{
int i=p,j=r+1;
int x=a[p]; //6
//将 <x 的元素交换到左边区域
//将 >x 的元素交换到右边区域
while(true)
{ //i一上来就要加 j一上来就要减
while(a[++i]<x && i<r); //++i就是 7 a[i]=7 比 6 小的话 继续循环
while(a[--j]>x); //上面的找到 7 这个找到 5
if(i >= j) break; //遇到 2 7 不需要交换 ,且 退出
swap(a[i],a[j]); //交换 7 和 5
}
a[p]=a[j]; //2 和 6 交换
a[j]=x;
return j;
}
int main()
{
//6 7 5 2 5 8
int arr[]={6,7,5,2,5,8};
QuickSort(arr,0,5);
for(int i=0;i<6;i++)
{
cout<<arr[i]<<" ";
}
}
最坏情况下的快速排序
条件1:输入是有序
的(升序或降序) :
条件2:总以最大值或最小值为划分基准。
后果:划分的结果总是有一-边没有元素。
如:432 1, 以4为基准划分结果: {1 3 2} 4,
4的右方为空,则时间复杂度为T(0),左方为T(n-1)
最好情况下的快速排序
■每次以中值
为基准划分数组
■T(n)= 2T(n/2) +日(n) =日(r^log n)
■这种情况非常理想化,难以实现。
若随机数