递归
递归概念
直接或间接地调用自身的算法成为递归算法。用函数自身给出定义的函数成为递归函数
递归实例
- 阶乘函数
- 斐波那契数列
- Ackerman函数
- 排列问题
- 证书划分问题
- 汉诺塔问题
分治
分治法的基本思想
将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立而且与原问题相同,递归地求解后,将各个子问题合并得到原问题的解。
分治法的特征
- 该问题所分解出的各个子问题是相互独立的,子问题之间不包含公共的子问题
- 利用该问题分解出的子问题的解可以合并为该问题的解
- 该问题的规模缩小到一定的程度就可以容易地解决
- 该问题可以分解为若干个规模较小的相同问题
主定理计算时间复杂度
用T(n)表示该分治法解规模为|p|=n的问题所需的计算时间,
设 d>=0 ,则
分治法实例
1.二分搜索
基本思想
将n个元素分成个数大致相同的两半,取a[n/2]与x比较,如果x<a[n/2],在数组a的左半部分继续搜索;如果X>a[n/2],在数组a的右半部分继续搜索,如果x==a[n/2],找到x程序终止。
代码实现
#include<iostream>
using namespace std;
int BinarySearch(int a[], int aSize, int key)
{
if ( aSize == 0 )
return -1;
int low = 0;
int high = aSize - 1;
int mid = 0;
while ( low <= high )
{
mid = (low + high )/2;
if ( a[mid] < key)
low = mid + 1;
else if ( a[mid] > key )
high = mid - 1;
else
return mid;
}
return -1;
}
int main()
{
int a[10];
for (int i=0; i<10; i++)
a[i] = i;
cout<<BinarySearch(a, 10, 6)<<endl;
return 0;
}
时间复杂度
时间复杂度为O(logn)
2.大整数乘法
基本思想
将n位整数X和Y分为2段,每段为n/2位,X=A*10^(n/2)+B,Y=C*10^(n/2)+D;XY=AC*10^n+((A-B)(D-C)+AC+BD)*10^n+BD
代码实现
#include<iostream>
#include<cmath>
using namespace std;
long long mul(long long a,long long b,int len){
if(len==0)return 0;
else if(len==1){
return a*b;
}
else{
long long A=a/(int)(pow(10,len/2));
long long B=a%(int)(pow(10,len/2));
long long C=b/(int)(pow(10,len/2));
long long D=b%(int)(pow(10,len/2));
long long AC=mul(A,C,len/2);
long long BD=mul(B,D,len/2);
long long ABCD=mul((A-B),(D-C),len/2)+AC+BD;
return pow(10,len/2+len/2)*AC+ABCD*pow(10,len/2)+BD;
}
}
int main(){
long long a,b,temp;
cin>>a>>b;
temp=a;
int len=0;
while(temp){
temp/=10;
len++;
}
long long c=mul(a,b,len);
cout<<c;
}
时间复杂度
3.Strassen矩阵乘法
基本思想
时间复杂度
4.合并排序
基本思想
递归实现:使用分治法,将待排序的数组分为两份,分别递归排序,然后合并两个各自有序的数组。
非递归实现:先将数组中的相邻两元素两两配对,再用合并算法将它们排序,构成n/2组长度为2的排好序的子数组段,再将它们排序成长度为4的排好序的字数组段。如此下去,直至整个数组排好序。
代码实现
递归
#include<iostream>
using namespace std;
void merge(int a[],int l,int m,int h){
int i=l,j=m+1,k=0;
int b[h-l+1];
while(i<=m&&j<=h){
if(a[i]>a[j]){
b[k++]=a[j++];
}
else b[k++]=a[i++];
}
while(i<=m){
b[k++]=a[i++];
}
while(j<=h){
b[k++]=a[j++];
}
for(i=l,k=0;i<=h;i++,k++){
a[i]=b[k];
}
}
void merge_sort(int a[],int low,int high){
if(low<high){
int mid=(low+high)/2;
merge_sort(a,low,mid);
merge_sort(a,mid+1,high);
merge(a,low,mid,high);
}
}
int main (){
int n;
cin>>n;
int a[n];
for(int i=0;i<n;i++){
cin>>a[i];
}
merge_sort(a,0,n-1);
cout<<count;
}
非递归
#include<iostream>
using namespace std;
void merge_sort(int a[],int l,int r,int n){
int gap=1;
int temp[n];
while(gap<n){
for(int i=0;i<=r;i=i+2*gap){
int begin1=i,end1=i+gap-1,mid=i+gap-1;
int begin2=mid+1,end2=i+2*gap-1;
if(end2>r)end2=r;
if(begin2>r)break;
int j=i;
while(begin1<=end1&&begin2<=end2){
if(a[begin1]>a[begin2]){
temp[j++]=a[begin2++];
}
else temp[j++]=a[begin1++];
}
while(begin1<=end1)temp[j++]=a[begin1++];
while(begin2<=end2)temp[j++]=a[begin2++];
for(j=i;j<=end2;j++)a[j]=temp[j];
}
gap*=2;
}
}
int main(){
int n;
cin>>n;
int a[n];
for(int i=0;i<n;i++){
cin>>a[i];
}
merge_sort(a,0,n-1,n);
for(int i=0;i<n;i++)cout<<a[i]<<' ';
}
时间复杂度
时间复杂度为O(nlogn)
5.棋盘覆盖
基本思想
当k>0时,将2 k ∗ 2 k 2^k*2^k2 k∗2 k 棋盘分割为4个2 k − 1 ∗ 2 k − 1 2^{k-1}*2^{k-1}2 k−1∗2 k−1子棋盘。特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,这3个子棋盘上被L型骨牌覆盖的方格就成为该棋盘上的特殊方格,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为1 ∗ 1 1*11∗1棋盘。
时间复杂度
时间复杂度为O(4^k)
6.快速排序
基本思想
基本思想:使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
代码实现
递归代码
#include<iostream>
using namespace std;
void quick_sort(int a[],int l,int r){
if(l>=r)return;
int i=l-1,j=r+1,x=a[(l+r)/2];
while(i<j){
do i++;while(a[i]<x);
do j--;while(a[j]>x);
if(i<j)swap(a[i],a[j]);
}
quick_sort(a,l,j);
quick_sort(a,j+1,r);
}
int main(){
int n;
cin>>n;
int a[n];
for(int i=0;i<n;i++)cin>>a[i];
quick_sort(a,0,n-1);
for(int i=0;i<n;i++)cout<<a[i]<<' ';
}
随机代码
#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;
void quick_sort(int a[],int l,int r){
srand(unsigned(time(NULL)));
int y=rand()%(r-l+1)+l;
swap(a[(l+r)/2],a[y]);
if(l>=r)return;
int i=l-1,j=r+1,x=a[(l+r)/2];
while(i<j){
do i++; while(a[i]<x);
do j--;while(a[j]>x);
if(i<j)swap(a[i],a[j]);
}
quick_sort(a,l,j);
quick_sort(a,j+1,r);
}
int main(){
int n;
cin>>n;
int a[n];
for(int i=0;i<n;i++)cin>>a[i];
quick_sort(a,0,n-1);
for(int i=0;i<n;i++)cout<<a[i]<<' ';
}
时间复杂度
最坏情况下,标志点一侧没有元素,时间复杂度为O(n^2),最好情况下,两侧元素数相同,时间复杂度为O(nlogn),快速排序不稳定。
7.最近点对问题
基本思想
将所给的平面上n个点的集合S分为两个子集S1和S2,每个子集中约有n/2个点,然后在每个子集中递归地求其最接近的点对。最近点对可能单纯在S1或S2中,也可能分别在S1和S2中。取两个子集递归求解最小值为d,第三种情况只会发生在 ( mid - d , mid + d ) 内,这个范围,mid左边p1,mid右边p2,p1中每个点最多在p2中存在6个点会更新答案,即按照y坐标排序后,p1每点最多只要检查p2中排好序的相继6个点。
时间复杂度
时间复杂度为 O(nlogn)
8.求第k小问题
基本思想
快排改进算法:在快速排序的过程中,基准点在经历一次排序后会排在应在的位置上,这时只需判断第k个点与该基准点位置的距离,递归求解即可。
线性时间选择法:找到一个划分基准,使得这个基准的划分的两个子数组的长度都至少是原来的ε 倍(0 < ε &&ε < 1),那么最坏情况也能O( n )解决问题。
随机选择算法:在快排改进算法的基础上改进,随机取基准点,将快排的不稳定性降低为概率事件
代码实现
快排改进算法
#include<iostream>
#include<algorithm>
using namespace std;
const int N=10000000;
int x[N],y[N];
int k_sort(int a[],int l,int r,int k){
if(l>=r)return a[l];
int i=l-1,j=r+1,x=a[(l+r)/2];
while(i<j){
do i++;while(a[i]<x);
do j--;while(a[j]>x);
if(i<j)swap(a[i],a[j]);
}
if(k<=j-l+1)return k_sort(a,l,j,k);//k在左侧
else return k_sort(a,j+1,r,k-(j-l+1));//k在右侧,k变成右侧的第k-(j-l+1)小
}
int main(){
int n;cin>>n;
for(int i=0;i<n;i++){
scanf("%d %d",&x[i],&y[i]);
}
cout<<"("<<k_sort(x,0,n-1,(n+1)/2)<<","<<k_sort(y,0,n-1,(n+1)/2)<<")";
}
线性时间选择
#include <cstdio>
#include <cstdlib>
int num[2000001];
int select(int low, int high, int top);
int partition(int low, int high, int median);
void selectSort(int low, int high);
void swap(int &a, int &b);
int main()
{
int n, m, i;
while (~scanf("%d%d", &n, &m))
{
for (i = 0; i < n; i++)
scanf("%d", &num[i]);
printf("%d\n", select(0, n - 1, m - 1));
/*
for (i = 0; i < n; i++)
printf("%d%c", num[i], i < n - 1 ? ' ' : '\n');
*/
}
return 0;
}
// 中位数法线性时间选择
int select(int low, int high, int top)
{
// 小于75个数据随便用一个排序方法
if (high - low < 74)
{
selectSort(low, high); // 选择排序
return num[low + top]; // 排完序直接返回第low + top的数
}
int groupNum = (high - low - 4) / 5; // 每组5个数, 计算多少个组, 从0开始计数
for (int i = 0; i <= groupNum; i++)
{
int start = low + 5 * i; // 每组的起始位置
int end = start + 4; // 每组的结束位置
for (int j = 0; j < 3; j++) // 从小到大冒3个泡
for (int k = start; k < end - j; k++)
if (num[k] > num[k + 1])
swap(num[k], num[k+1]);
swap(num[low + i], num[start + 2]); // 每组的中位数交换到前面第low + i的位置
}
// 上面排完后, 数组low + 0 到 low + groupNum都是每一组的中位数
int median = select(low, low + groupNum, (groupNum + 1) / 2); // 找中位数的中位数
int p = partition(low, high, median); // 将数组分为两段, 左边的小于中位数的中位数, 右边的大于中位数的中位数
int n = p - low; // 计算p到low之间有多少个数, 后面得减掉
if (n == top)
return num[p]; // 如果运气好, 刚好要找的就是中位数
if (n > top)
return select(low, p - 1, top); // n比top大就在左边找
if (n < top)
return select(p + 1, high, top - n - 1); // n比top小就在右边找, 并且top要减去已经大的个数
}
// 以中位数进行分割, 分成两半
int partition(int low, int high, int median)
{
int p;
for (int i = low; i <= high; i++)
if (num[i] == median)
{
p = i;
break;
}
// 将中位数交换到最前面
swap(num[p], num[low]);
// 记下最前面的数
int key = num[low];
// 把小于key的放前面, 大于key的放后面
while (low < high)
{
while (num[high] >= key && low < high)
high--;
if (low < high)
num[low] = num[high];
while (num[low] <= key && low < high)
low++;
if (low < high)
num[high] = num[low];
}
// 分别从两头开始, 找到中间时, 把key存回
num[low] = key;
return low;
}
// 选择排序
void selectSort(int low, int high)
{
for (int i = low; i <= high; i++)
{
int MIN = i;
for (int j = i + 1; j <= high; j++)
if (num[MIN] > num[j])
MIN = j;
swap(num[i], num[MIN]);
}
}
// 交换两个元素
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
时间复杂度
快排改进:最坏O(n^2),时间复杂度为O (N*K)
随机选择:最坏O(n^2)
线性时间:O(n)