第二章 递归与分治策略知识总结

目录

2.1递归

2.2分治

2.3重要代码及经典例题

二分查找

合并排序(归并排序) 

快速排序

选择第k小

大整数乘法 

 棋盘覆盖

最近点对问题 

循环赛日程表 


2.1递归

  • 递归的概念

        直接或间接的调用自身的算法成为递归算法。用函数自身给出定义的函数称为递归函数。例如我们首先接触的汉诺塔问题,以及我们很早接触的未使用递归解Fibonacci数问题。

2.2分治

  • 分支的基本思想

分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同。递归的解这些子问题,然后将各个子问题的解合并得到原问题的解。

  • 适用条件
  1. 该问题的规模缩小到一定程度就可以很容易的解决。
  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构。
  3. 利用该问题分解出的子问题的解可以合并为该问题的解。
  4. 原问题分解出的各个子问题是相互独立的,即子问题直接不包含公共的子问题。
  • 求解过程
  1. 分解:将原问题分解为若干个规模较小的子问题。
  2. 求解:直接解决子问题或递归求解。
  3. 合并:将各个子问题的解合并及为原问题的解。
  • 主定理计算分治算法的时间复杂性

如果在递推关系式中T(n) = a*T(n/b)+f(n),f(n)∈Θ(n^d)(d>=0)

则当a<b^d时,T(n) =Θ(n^d) ,当a = b^d时,T(n) = Θn^d*logn,当a<b^d时,T(n) =Θ(n^logba)(以b为低a的对数) 。

2.3重要代码及经典例题

  • 二分查找

基本思路:将n个元素一分为二,取a[n/2]与x作比,如果相等,终止,如若x<a[n/2],则在数组a的左半部分继续搜索,相反,则在右半部分继续搜索。

/**
    二分查找
 */
int BinarySearch(int a[], const int& x,int left, int right){
    int left = 0;int right = n-1;
    while(left<=right)
    {                      
        int middle = (left + right) / 2;                            
        if (x == a[middle])
            return middle;        
        if (x > a[middle])
            left = middle + 1;
        else 
            right = middle - 1;
    }
    return -1;                                         
}
  • 合并排序(归并排序) 

基本思路:合并排序的基本思路是用分治策略实现对n个元素进行排序的算法,其基本思想是:将待排序的元素分成大小大致相同的两个子集合,分别对两个子集合进行排序,最终将排好序的子集合合并成要求的排好序的集合。代码如下:

#include<stdio.h>

void merge(int arr[],int temparr[],int left,int mid,int right) 
{
	int l_pos = left;
	int r_pos = mid+1;
	
	//临时元素下标
	int pos = left;
	while(l_pos <=mid&&r_pos<=right)
	{
		if(arr[l_pos]<arr[r_pos])
		temparr[pos++] = arr[l_pos++];
		else
		temparr[pos++] = arr[r_pos++]; 	
	 }
	 //合并左边剩余 
	 while(l_pos<=mid)
	 temparr[pos++] = arr[l_pos++];
	 //合并右边剩余
	 while(r_pos<=right) 
	 temparr[pos++] = arr[r_pos++];
	 
	 while(left <= right)
	 {
	 	arr[left] = temparr[left];
	 	left++;
	 }
}

//归并排序
void msort(int arr[],int temparr[],int left,int right)
{
	//只有一个数字只需要归并
	if(left<right)//left小于right肯定有数
	{
		int mid = (left+right)/2;
		//递归划分左半区 
		msort(arr,temparr,left,mid);
		//划分右半区 
		msort(arr,temparr,mid+1,right);
		//合并已经排序的部分
		merge(arr,temparr,left,mid,right); 
		
	 } 
 }
	
//归并排序入口 
void merg_sort(int arr[],int n)
{
	int temarr[1000];
	msort(arr,temarr,0,n-1);
}

int main()
{
	
	int a[10] = {3,6,7,10,8,1,5,4,9,2};
	int n = 10;
	merg_sort(a,n);
	for(int i = 0;i<10;i++)
	{
		printf("%d ",a[i]);
	 } 
}
	

算法分析:

归并排序算法的时间复杂度为O(nlogn)。当n=1时:T(n)=O(1)
当n>1时:T(n)=2T(n/2)+O(n)。

  • 快速排序

基本思路:1分解(Divide):以a[p]为基准元素将a[p:r]划分成3段alp:q-1], a[q]和 a[q+1:r],使a[p:q-1]中任何一个元素小于等于a[q],而a[q+1:r]中任何一个元素大于等于 a[q]。下标q在划分过程中确定。

2递归求解(Conquer):通过递归调用快速排序算法,分别对a[p:q-1]和a[q+1:r]进行排序。

3合并(Merge): 由于对a[p:q-1]和al[q+1:r]的排序是就地进行的,因此在a[p:q-1]和a[q+ 1:r]都已排好的序后,不需要执行任何计算,a[p:r]则已排好序。

给出的代码以第一个元素为pivot(注释掉的为最后一个元素为pivot):

#include<stdio.h>
void quick_sort(int a[],int len);
void qsort(int a[],int low,int high);
int partition(int a[],int low,int high);

int main(){	
	int a[10] = {3,6,7,10,8,1,5,4,9,2};
	int n = 10;
	quick_sort(a,10);
	for(int i = 0;i<10;i++)
	{
		printf("%d ",a[i]);
	 }
}

void quick_sort(int a[],int len)
{
	
	qsort(a,0,len-1);
}
void qsort(int a[],int low,int high)
{
	if(low<high)
	{
		int mid = partition(a,low,high);
		qsort(a,low,mid-1);
		qsort(a,mid+1,high);
	}
}

//int partition(int a[],int low,int high)
//{
//	
//	int pivot = a[high];
//	int i = low;
//	for(int j  = low;j<high;j++)
//	{
//		if(a[j]<pivot)
//		{
//			int temp;
//			temp = a[j];
//			a[j] = a[i];
//			a[i] = temp;
//			i++;
//		}
//	}
//	int temp;
//	temp = a[high];
//	a[high] = a[i];
//	a[i] = temp;
//	return i; 
//}





int partition(int a[],int low,int high)
{
	
	int pivot = a[low];
	int i = high;
	for(int j  = high-1;j>=low;j--)
	{
		if(a[j]>pivot)
		{
			int temp;
			temp = a[j];
			a[j] = a[i];
			a[i] = temp;
			i--;
		}
	}
	int temp;
	temp = a[low];
	a[low] = a[i];
	a[i] = temp;
	return i; 
}
//void quick_sort(int a[],int len)
//{
//	q_sort(a,0,len-1);
//}
//void q_sort(int a[],int low,int high)
//{
//	if(low<high)
//	{
//	int mid = partition(a,low,high);
//	q_sort(a,low,mid-1);
//	q_sort(a,mid+1,high); 		
//	}
//
//}
//
//int partion(int a[],int low,int high)
//{
//	int pivot = a[high];
//	int i = low;
//	for(int j = low;j<high;j++)
//	{
//		if(a[j]<pivot)
//		swap(a[i++],a[j]);
//	}
//	swap(a[i],a[high]);
//	return i;
//}

算法分析:

快速排序是在冒泡排序的基础上改进而来的,冒泡排序每次只能交换相邻的两个元素,而快速排序是跳跃式的交换,交换的距离很大,因此总的比较和交换次数少了很多,速度也快了不少。

但是快速排序在最坏情况下的时间复杂度和冒泡排序一样,是 O(n2),实际上每次比较都需要交换,但是这种情况并不常见。我们可以思考一下如果每次比较都需要交换,那么数列的平均时间复杂度是 O(nlogn),事实上在大多数时候,排序的速度要快于这个平均时间复杂度。这种算法实际上是一种分治法思想,也就是分而治之,把问题分为一个个的小部分来分别解决,再把结果组合起来。

快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为 O(logn),在最差的情况下,若每次只完成了一个元素,那么空间复杂度为 O(n)。所以我们一般认为快速排序的空间复杂度为 O(logn)。

快速排序是一个不稳定的算法,在经过排序之后,可能会对相同值的元素的相对位置造成改变。

快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。

下面给出快排随机标杆算法:

#include<stdio.h>
#include<time.h>
#include<stdlib.h>
int Partition(int a[],int p,int r)
{
	int i = 0,j = 0,temp = 0,x = 0;
	x = a[r];
	i = p-1;
	for(j = p;j<r;j++)
	{
		if(a[j]<=x)
		{
			i++;
			temp = a[i];
			a[i] = a[j];
			a[j] = temp;
		}
	}
	temp = a[i+1];
	a[i+1] = a[r];
	a[r] = temp;
	
	return i+1;
}

int RandomPartition(int a[],int p,int r)
{
	int i = 0;
	int temp = 0;
	srand((unsigned)time(NULL));
	i = rand()%(r-p+1)+p;
	
	temp = a[i];
	a[i] = a[r];
	a[r] = temp;
	
	return Partition(a,p,r);
 } 
void RandomQuickSort(int a[],int p,int r)
{
	int q = 0;
	if(p<r)
	{
		
		q = RandomPartition(a,p,r);
		RandomQuickSort(a,p,q-1);
		RandomQuickSort(a,q+1,r);
	}
 } 
int main(){
	
	int a[10] = {3,6,7,10,8,1,5,4,9,2};
	int n = 10;
	RandomQuickSort(a,0,n-1);
	for(int i = 0;i<10;i++)
	{
		printf("%d ",a[i]);
	}
	return 0; 	
}
  • 选择第k小

基本思路:本算法时在快速排序的基础上做的修改而得到的。基本思路是因为在快速排序时每进行一次快排,都有一个点的位置被确定了下来,利用这个被确定下来的位置判断k点的位置,如果小了,就往左找,大了向右找。

给个例题:邮局选址问题:

问题描述:
在一个按照东西和南北方向划分成规整街区的城市里,n个居民点散乱地分布在不同的街区中。用x 坐标表示东西向,用y坐标表示南北向。各居民点的位置可以由坐标(x,y)表示。街区中任意2 点(x1,y1)和(x2,y2)之间的距离可以用数值∣x1−x2∣+∣y1−y2∣度量。
居民们希望在城市中选择建立邮局的最佳位置,使n个居民点到邮局的距离总和最小。
编程任务:
给定n 个居民点的位置,编程计算邮局的最佳位置。

输入格式:

第1 行是居民点数n,1≤n≤5000000。接下来的n行是居民点的位置,每行两个整数x和y,分别为居民点位置的x坐标和y坐标,−109≤x,y≤109

输出格式:

输出只有一行,表示邮局的最佳位置,形如:(x,y)

输入样例:

在这里给出一组输入。例如:

5
1 2
2 2
1 3
3 -2
3 3

输出样例:

在这里给出相应的输出。例如:

(2,2)

解决代码:

#include<iostream>
using namespace std;
const int N = 1e7+5;
int x[N];
int y[N];
int qsort(int q[],int l,int r,int k)
{
	if(l == r) return q[l];
    int m=( l + r ) /2;
	int x = q[m],i,j;
	i = l ,j = r + 1;
    swap(q[m],q[l]);
	while(1)
	{
		while(q[++i] < x&&i<r);
		while(q[--j] > x);
        if(i>=j)break;
		swap(q[i],q[j]);
	}
    q[l]=q[j];
    q[j]=x;
	int sl = j - l + 1;
    if(sl==k)return q[j];
	if(k < sl) qsort(q,l,j-1,k);
	else qsort(q,j + 1,r,k - sl);
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i = 1;i<=n;i++)
	{
		scanf("%d%d",&x[i],&y[i]);
	}
	int xmid = qsort(x,1,n,(n+1)/2);
	int ymid = qsort(y,1,n,(n+1)/2); 	
 	printf("(%d,%d)",xmid,ymid);
	return 0;
} 
  • 大整数乘法 

令X = A*2^n/2+B,Y = C*2^n/2+D

可将X*Y写成X*Y = AC*2^n+((A-B)*(D-C)+A*C+B*D)*2^n/2+B*D;

时间复杂度分析:

  • Strassen矩阵乘法 

七次乘法:

 时间复杂度分析:

  •  棋盘覆盖

在一个2^k×2^k个方格组成的棋盘中,若恰有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一特殊棋盘。显然,特殊方格在棋盘上出现的位置有4^k种情形。因而对任何k≥0,有4^k种特殊棋盘。图2-4中的特殊棋盘是k=2时16个特殊棋盘中的一个。

在棋盘覆盖问题中,要用图2-5所示的4种不同形态的L型骨牌覆盖一个给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。易知,在任何一个2^k×2^k的棋盘覆盖中,用到的L型骨牌个数恰为(4^k-1)/3

用分治策略,可以设计解棋盘覆盖问题的一个简捷的算法。当k>0时,将2^k×2^k棋盘分割为4个2^k-1×2^k-1子棋盘,如图2-6(a)所示。特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,如图2-6(b)所示,这3个子棋盘上被L型骨牌覆盖的方格就成为该棋盘上的特殊方格,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为1×1棋盘。

代码

 

 

时间复杂度分析:

 

  • 最近点对问题 

分治法:1.将点集分成大致相等的两个部分A和B

                2.分别递归求解A和B中的最近点对Da和Db。

                3.再求出一点在A中?零一点在B中的最近点对Dab。

                4.合并得到原问题的解d = min{Da,Db,Dab}

步骤:

 

 

 

 

 

  • 循环赛日程表 

 

思路:观察此表,我们发现在比赛还有两个人时比赛顺序是固定的,所以 按分治的策略,将所有选手分为两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定。递归的执行选手的分割,知道剩下两个选手时,只要这两个选手进行比赛

#include<stdio.h>
int a[10000][10000];
void Table(int n);
void copyToLB(int k);
void copyToRB(int k);
void copyToRT(int k);
int main()
{
	int n;
	scanf("%d",&n);
	Table(n);
	for(int i = 0;i<n;i++)
	{
		for(int j = 0;j<n;j++)
		{
			printf("%d ",a[i][j]);
		}
		printf("\n");
	 } 
 } 
void Table(int n)
{
	if(n == 2)
	{
		a[0][0] = 1;
		a[1][1] = 1;
		a[0][1] = 2;
		a[1][0] = 2;
	}
	else
	{
		Table(n/2);
		copyToLB(n/2);
		copyToRT(n/2);
		copyToRB(n/2);
	}
}

void copyToLB(int k)
{
	for(int i = 0;i<k;++i)
	{
		for(int j = 0;j<k;++j)
			a[i+k][j] = a[i][j]+k;
	}
}
void copyToRT(int k)
{
	for(int i = 0;i<k;++i)
	{
		for(int j = 0;j<k;++j)
			a[i][j+k] = a[i][j]+k;
	}
}
void copyToRB(int k)
{
	for(int i = 0;i<k;++i)
	{
		for(int j = 0;j<k;++j)
			a[i+k][j+k] = a[i][j];
	}
}

 

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值