《算法设计与分析--第二章》

第 2 章 递归与分治策略

2.1 递归的概念

2.2 分治法的基本思想

2.3 二分搜索技术

2.4 大整数的乘法

2.5 Strassen矩阵乘法

2.6 棋盘覆盖

2.7 合并排序

2.8 快速排序

2.9 线性时间选择

2.10 最接近点对问题

2.11 循环赛日程表

思维导图:

 

主定理:

T(n)=aT(n/b)+f(n)

1.如果a<b^d,T(n)=O(n^d)

2.如果a=b^d,T(n)=O(n^dlogn)

3.如果a>b^d,T(n)=O(n^logba)

2.1递归的思想:

直接或间接地调用自身的算法称为递归算法

用函数自身给出定义的函数称为递归函数。递归函数的二要素:边界条件、递归方程。

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

分治法特征:
该问题的规模缩小到一定的程度就可以容易地解决
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
利用该问题分解出的子问题的解可以合并为该问题的解;
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

2.3二分查找技术:

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。

使用二分查找的前置条件:待查表为有序表。

思想:

现有一含有N个元素的有序数组A(从小到大排列),查找key:

获取两个变量left、right,查找开始的时候:设置两个变量left=0,right=N-1;
计算数组中间元素下标mid,mid=(left+right)/2,比较A[mid]和key的大小;
A[mid]==key,查找成功
A[mid]>key,递归查找左半数组A[left,mid-1]
A[mid]<key,递归查找右半数组A[mid+1,right]
重复步骤2,直到查找成功或查找失败。

时间复杂度:

如果用顺序搜索(从头开始依次往后比较),最坏情况需要n次比较;最坏用 O(logn)时间完成搜索任务。

2.4大整数的乘法

常规计算方法:
有n位大整数X和Y,计算XY:将Y的每一位与X相乘再按位相加;
时间复杂度:我们需要进行一次n位的乘法,即T(n)=o(n^2)

分治法(理想状态下)
理想状态:假设有n位大整数X和Y(X和Y位数n相同且为偶数),计算XY。

分治:我们将X、Y分别拆分为a与b、c与d,X=a∗10^⌊n/2⌋b,Y=c∗10^ ⌊n/2⌋+d 
再计算:
XY=ac∗10 ^2⌊n/2⌋ +(ad+bc)∗10^ ⌊n/2⌋+bd

时间复杂度:一共需要进行4次n/2的乘法(ac,ad,bc、bd各一次)和3次加法,
因而T(n)=4T(n/2)+θ(n)=o(n^2)

改善:E=ac,F=bd,G=(a−b)∗(d−c)=ad−bd−ac+bc,则XY=E∗10 ^2⌊n/2⌋ +(G+E+F)∗10 ^⌊n/2⌋ +F

改善后的时间复杂度:一共需要进行3次n/2的乘法(E,F,G各一次)和4次加法,因而T(n)=3T(n/2)+θ(n)=o(n^ 1.59)
 

2.5Strassen矩阵乘法

基本思想:

使用分治法,将一个矩阵转换为子矩阵相乘的方式。矩阵乘法耗费时间要比矩阵加法耗费的时间多,想要改进矩阵乘法的计算时间复杂性,必须减少乘法运算。Strassen矩阵乘法用了7次对于n/2阶矩阵乘积的递归调用和18次n/2阶矩阵的加减运算。

时间复杂度:

T(n)=7T(n/2)+O(n^2) ,n>2

        O(1),n=2

2.6棋盘覆盖

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

时间复杂度:设T(k)为覆盖2^k*2^k棋盘的时间, (1)k=0 覆盖它需要常数时间O(1) (2)k>0
测试哪个子棋盘特殊以及形成3个特殊子棋盘需要O(1) 覆盖4个特殊子棋盘需四次递归调用,共需时间4T(k-1)。最后为O(4^k).

2.7合并排序

基本思想:

合并排序是用分治思想,首先将序列分为两部分,然后对每一部分进行递归的排序,最后将结果进行合并。

具体步骤:

(1)分解:将n个元素分成个含n/2个元素的子序列。
(2)解决:用合并排序法对两个子序列递归排序。
(3)合并:合并两个已排序的子序列已得到排序结果。

将待排序元素分成大小大致相同的两个子集合,分别对两个子集合进行排序,最终将排好序的子集合合并成要求的排好序的集合。

时间复杂度:
最坏时间复杂度:O(nlogn)
最好时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定

代码:归并递归

#include<iostream>
using namespace std;
int a[100];
 void Merge(int a[],int mid,int left,int right)
{
	int temp[1000]; 
    int i=left;
    int j=mid+1;
    int k=left;
while(i<=mid&&j<=right)
{
	if(a[i]<=a[j])
	temp[k++]=a[i++];
	else
	temp[k++]=a[j++];
	
}
while(i<=mid) temp[k++]=a[i++];
while(j<=right) temp[k++]=a[j++];

for(int i=left;i<=right;i++)
{
	a[i]=temp[i];
 } 
}
void MergeSort(int a[],int left,int right)
{
	if(left<right)
	{
		int mid=(left+right)/2;
	MergeSort(a,left,mid);
	MergeSort(a,mid+1,right);
	Merge(a,mid,left,right);
	}
	
}
int main()
{
	int n;
	cin>>n;
	for(int i=0;i<n;i++)
	{
		cin>>a[i];
	}
	MergeSort(a,0,n-1);

	for(int i=0;i<n;i++)
	{
		cout<<a[i]<<" ";
	}	
	
}

2.8快速排序

基本思想:
快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

具体步骤:
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)
(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大
(3)递归处理:递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。

选择基准的方式:
(1)固定位置
取序列的第一个或最后一个元素作为基准
(2)随机化选择
随机取待排序列中任意一个元素作为基准
快速排序算法的性能取决于划分的对称性。而划分基准的随机选择,可以使期望划分变得较为对称。

时间复杂度
最坏时间复杂度:O(n^2)
最好时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:根据实现方式的不同而不同
稳定性:不稳定


代码:随机化

#include <iostream>
#include <cstdlib>
using namespace std;
int a[20005];
void quick_sort(int l,int r){
    if(l>=r) return ;
    int x=rand()%(r-l+1)+l;
    swap(a[r],a[x]);
    int i=l-1;
    for(int j=l;j<=r-1;++j){
        if(a[j]<=a[r]){
            ++i;
            swap(a[i],a[j]);
        }
    }
    swap(a[i+1],a[r]);
    quick_sort(l,i);
    quick_sort(i+2,r);
}
 
int main(int argc, const char * argv[]) {
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;++i)
        scanf("%d",&a[i]);
    quick_sort(0,n-1);
    for(int i=0;i<n;++i){
        if(i!=n-1) printf("%d ",a[i]);
        else printf("%d\n",a[i]);
    }
    return 0;
}

代码:固定

#include<stdio.h>
int main() 
{
	void quicksort(int a[],int low,int high);
	int n,i,a[100];
	scanf("%d",&n);
	for(i=0;i<n;i++)
	{
		scanf("%d",&a[i]);
	}
	
	quicksort(a,0,n-1);
	for(i=0;i<n;i++)
		printf("%d ",a[i]);

	return 0;
}

void quicksort(int a[],int low,int high)
{
	int i=low;
	int j=high;
	int temp=a[i];
	if(low<high)
	{
		while(i<j)       //当i=j时结束循环
		{
		while((a[j]>=temp)&&(i<j))   //处理右边
		{
			j--;
		}
		a[i]=a[j];
		while((a[i]<=temp)&&(i<j))      //处理左边
		{
			i++;
		}
		a[j]=a[i];
		}
		a[i]=temp;
		//第一轮结束,递归调用。i-1和j+1是因为中间数已经排好,不用再次递归
		quicksort(a,low,i-1);
		quicksort(a,j+1,high);
	}	
	else
	return ;
}

2.9线性时间选择算法

算法步骤:
将n个元素划分成⌈n/5⌉组,每组5个元素,至多只有一组包含n mod 5个元素
通过每组排序,找出每组的中位数构成序列M
取序列M的中位数x(若序列有偶数,取两个中位数中较大者)
用x作为基准元素,对原n个元素进行划分,i为分裂点
若k<=j,则用前部分子问题递归求第k小元素;
若k>j,则用后部分子问题求第k-i小元素
j为前部分子问题元素的个数。
 

时间复杂度:

T(n)=T(n/5)+T(3n/4)+O(n),最后为O(n);

2.10最接近点对问题

基本思想:
将所给的平面上n个点的集合S分为两个子集S1和S2(可以按照x坐标排序中分),每个子集中约有n/2个点,然后在每个子集中递归地求其最接近的点对。最近点对可能单纯在S1或S2中,也可能分别在S1和S2中。对于这个问题,
一维:第三种情况只可能是最靠近中线的那两个点。
二维:取两个子集递归求解最小值为d,第三种情况只会发生在 ( mid - d , mid + d ) 内,这个范围,mid左边p1,mid右边p2,p1中每个点最多在p2中存在6个点会更新答案,即按照y坐标排序后,p1每点最多只要检查p2中排好序的相继6个点。
 

时间复杂度:O(nlogn)

2.11循环赛日程表

基本思想:

采用分治策略,将所有的选手分成两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定。递归地用对选手进行分割,直到只剩下两个选手时,比赛日程表的制定就变得很简单了。这时只需要让这两个选手进行比赛即可。

在每次迭代中,将问题划分为4部分:

左上角:左上角为2^{k-1}个选手在前半程的比赛日程;
左下角:左下角为另2^{k-1}个选手在前半程的比赛日程,由左上角加2^{k-1}得到,例如2^{2}个选        手比赛,左下角由左上角直接加2得到;2^{3}个选手比赛,左下角由左上角直接加4得到;
右上角:将左下角直接抄到右上角得到另2^{k-1}个选手在后半程的比赛日程;
右下角:将左上角直接抄到右下角得到2^{k-1}个选手在后半程的比赛日程;

时间复杂度:T(n)=O(n^2)

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值