【趣学算法】分治算法

14天阅读挑战赛
努力是为了不平庸~
算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法!欢迎记录下你的那些努力时刻(算法学习知识点/算法题解/遇到的算法bug/等等),在分享的同时加深对于算法的理解,同时吸收他人的奇思妙想,一起见证技术er的成长~

一、分治算法

分治算法采用分而治之的策略,将一个大规模的问题分解为若干个规模较小的相同子问题。

1.使用分治算法的条件

(1)原问题可分解为若干个规模较小的相同子问题。
(2)子问题相互独立。
(3)子问题的解可以合并为原问题的解。

2.分治法解题步骤

(1)分解:将要解决的问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题。
(2)治理:求解各个子问题。由于各个子问题与原问题形式相同,只是规模较小而已,而当子问题划分得足够小时,就可以用较简单的方法解决。
(3)合并:按原问题的要求,将子问题的解合并为原问题的解。

二、分治算法实例——二分搜索
1.问题描述

主持人在女嘉宾的手心上写一个10以内的整数,让女嘉宾的老公猜是多少,而女嘉宾只能提示大了,还是小了,并且只有3次机会。
主持人悄悄地在女嘉宾手心写了一个8。
老公:“2。”
老婆:“小了。”
老公:“3。”
老婆:“小了。”
老公:“10。”
老婆:“晕了!”

2.问题分析

猜数游戏是一个简单的二分搜索问题。
在有序序列中查找,每次和中间的元素比较,如果比中间元素小,则在前半部分查找,如果比中间元素大,则去后半部分查找。这种方法称为二分查找或折半查找,也称为二分搜索技术。

3.算法设计
  1. 初始化。令low=0,即指向有序数组S[]的第一个元素;high=n−1。
  2. 判定low≤high是否成立,如果成立,转向第3步,否则,算法结束。
  3. middle=(low+high)/2,即指向查找范围的中间元素。
  4. 判断x与S[middle]的关系。如果x=S[middle],则搜索成功,算法结束;如果x>S[middle],则令low=middle+1;否则令high=middle−1,转向第2步。
4.程序代码
//二分搜索,非递归算法
#include<iostream>
#include<algorithm>
using namespace std;
const int M=10005;
int s[M];

int BinarySearch(int s[],int n,int x){
	int low=0,high=n-1;  //low指向有序数组的第一个元素,high指向有序数组的最后一个元素
	while(low<=high){
		int middle=(low+high)/2; //middle为查找范围的中间值
		if(x==s[middle])       //x等于中间元素,查找成功
			return middle;
		else if(x>s[middle])  //x大于中间元素,则在前半部分查找
		    	low=middle+1;
		    else             //x小于中间元素,则在后半部分查找
		    	high=middle-1;
	}
    return -1;
}

int main(){
	int t,n,x; //测试用例数,元素个数,待查找元素 
	cin>>t;
    while(t--){
        cin>>n;
        for(int i=0;i<n;i++)
        	cin>>s[i];
		sort(s,s+n); //升序排序 
		for(int i=0;i<n;i++) //输出有序序列 
        	cout<<s[i]<<" ";
        cout<<endl;
        cin>>x; //输入要查找的元素
        int k=BinarySearch(s,n,x);
        if(k==-1)
        	cout<<"-1"<<endl;
        else
        	cout<<k+1<<endl;
    }
    return 0;
}
5.算法分析

当n>1时,待查找元素和中间位置元素比较,需要O(1)时间,如果比较不成功,那么需要在前半部分或后半部分搜索,问题的规模缩小了一半,时间复杂度变为T(n/2)。
T ( n ) = { O ( 1 ) ,n=1 T ( n / 2 ) + O ( 1 ) ,n>1 T(n)= \begin{cases} O(1)& \text{,n=1}\\ T(n/2)+O(1)& \text{,n>1} \end{cases} T(n)={O(1)T(n/2)+O(1),n=1,n1
T ( n ) = T ( n / 2 ) + O ( 1 ) = T ( n / 2 2 ) + 2 O ( 1 ) = T ( n / 2 3 ) + 3 O ( 1 ) = . . . . . . = T ( n / 2 x ) + x O ( 1 ) T(n)=T(n/2)+O(1)=T(n/2^2)+2O(1)=T(n/2^3)+3O(1)=......=T(n/2^x)+xO(1) T(n)=T(n/2)+O(1)=T(n/22)+2O(1)=T(n/23)+3O(1)=......=T(n/2x)+xO(1)
n = 2 x , n=2^x, n=2x x = l o g n 。 T ( n ) = T ( 1 ) + l o g n O ( 1 ) = O ( 1 ) + l o g n O ( 1 ) = O ( l o g n ) x=logn。T(n)=T(1)+lognO(1)=O(1)+lognO(1)=O(logn) x=lognT(n)=T(1)+lognO(1)=O(1)+lognO(1)=O(logn)

二分查找的时间复杂度的为O(logn)。
二分查找的空间复杂度的为O(1)。

三、分治算法实例——合并排序
1.基本思想

合并排序是采用分治的策略,将一个大的问题分成很多小问题,再通过小问题解决大问题。
把待排序元素分解成两个规模大致相等的子序列,如果不易解决,再将得到的子序列继续分解,直到子序列中包含1个元素。然后自底向上进行合并,得到一个有序序列。

2.排序步骤

1)分解—将待排序元素分成大小相当的两个子序列。
2)治理—对两个子序列进行合并排序。
3)合并—将排好序的有序子序列合并,得到有序序列。

3.程序代码
//合并排序
#include<iostream>
using namespace std;
const int M=10005;
int A[M];

void Merge(int A[],int low,int mid,int high){ //合并函数,合并有序序列A[low:mid]和A[mid+1:high]
    int *B=new int[high-low+1];//申请一个辅助数组
    int i=low,j=mid+1,k=0;
    while(i<=mid&&j<=high) {
        if(A[i]<=A[j]) //将较小元素存放到B[]中
            B[k++]=A[i++];
        else
            B[k++]=A[j++];
    }
    while(i<=mid) B[k++]=A[i++]; //如果前半部分有剩余,将剩余元素放置到B[]中
    while(j<=high) B[k++]=A[j++]; //如果后半部分有剩余,将剩余元素放置到B[]中
    for(i=low,k=0;i<=high;i++) //将有序序列放回A[]数组
        A[i]=B[k++];
    delete []B;
}

void MergeSort(int A[],int low,int high){//合并排序 
    if(low<high){
        int mid=(low+high)/2; //取中点
        MergeSort(A,low,mid); //对A[low:mid]中的元素合并排序
        MergeSort(A,mid+1,high); //对A[mid+1:high]中的元素合并排序
        Merge(A,low,mid,high); //将两个有序序列A[low:mid]和A[mid+1:high]合并
    }
}

int main(){
    int t,n; //测试用例数,元素个数
    cin>>t;
    while(t--){
	    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]<<" ";
	    cout<<endl;
    }
    return 0;
}
4.算法分析

时间复杂度:
• 分解:这一步仅仅计算出子序列的中间位置,需要O(1)时间。
• 解决:递归求解两个规模为n/2的子问题,所需时间为2T(n/2)。
• 合并:Merge算法可以在O(n)的时间内完成。
T ( n ) = { O ( 1 ) ,n=1 2 T ( n / 2 ) + O ( n ) ,n>1 T(n)= \begin{cases} O(1)& \text{,n=1}\\ 2T(n/2)+O(n)& \text{,n>1} \end{cases} T(n)={O(1)2T(n/2)+O(n),n=1,n1
时间复杂度:O(nlogn)
空间复杂度:O(n)

四、分治算法实例——快速排序
1.基本思想

通过一组排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,最终使所有数据变成有序序列。

2.排序步骤

1)分解:先从数列中取出一个元素作为基准元素。以基准元素为标准,将问题分解为两个子序列,使小于或等于基准元素的子序列在左侧,使大于基准元素的子序列在右侧。
2)治理:对两个子序列进行快速排序。
3)合并:将排好序的两个子序列合并在一起,得到原问题的解。

2.1.基准元素选取有以下几种方法:

• 取第一个元素。
• 取最后一个元素。
• 取中间位置元素。
• 取第一个、最后一个、中间位置元素三者之中位数。
• 取第一个和最后一个之间位置的随机数k(low≤k≤high),选R[k]做基准元素。

3.算法步骤

1)取第一个元素作为基准元素pivot=R[low],i=low,j=high。
2)从右向左扫描,找小于等于pivot的数,则R[i]和R[j]交换,i++。
3)从左向右扫描,找大于pivot的数,则R[i]和R[j]交换,j− −。
4)重复2)和3),直到i和j重合,返回该位置mid=i,该位置的数正好是pivot元素。
5)至此完成一趟排序。此时以mid为界,将原数据分为两个子序列,左侧子序列元素都比pivot小,右侧子序列元素都比pivot大,再分别对这两个子序列进行快速排序。

4.程序代码
//快速排序
#include<iostream>
using namespace std;
const int M=10005;
int R[M];

int Partition(int R[],int low,int high){//划分函数
    int i=low,j=high,pivot=R[low];//基准元素
    while(i<j){
        while(i<j&&R[j]>pivot) j--;//向左扫描
        if(i<j)
            swap(R[i++],R[j]);//R[i]和R[j]交换后,i+1右移一位
        while(i<j&&R[i]<=pivot) i++;//向右扫描
        if(i<j)
            swap(R[i],R[j--]);//R[i]和R[j]交换后,j-1左移一位
    }
    return i;//返回划分完成后基准元素位置
}

int Partition2(int R[],int low,int high){//划分函数优化 
    int i=low,j=high,pivot=R[low];//基准元素
    while(i<j){
        while(i<j&&R[j]>pivot) j--;//向左扫描
        while(i<j&&R[i]<=pivot) i++;//向右扫描
        if(i<j)
            swap(R[i++],R[j--]);//R[i]和R[j]交换
    }
    if(R[i]>pivot){
        swap(R[i-1],R[low]);//R[i-1]和R[low]交换
        return i-1;//返回划分完成后基准元素位置
    }
    swap(R[i],R[low]);//R[i]和R[low]交换
    return i;//返回划分完成后基准元素位置
}

void QuickSort(int R[],int low,int high){//实现快排算法
    if(low<high){
        int mid=Partition(R,low,high); //基准位置
        QuickSort(R,low,mid-1);//左区间递归快排
        QuickSort(R,mid+1,high);//右区间递归快排
    }
}

int main(){
    int t,n; //测试用例数,元素个数
    cin>>t;
    while(t--){
		cin>>n;
    	for(int i=0;i<n;i++)
        	cin>>R[i];
    	QuickSort(R,0,n-1);
	    for(int i=0;i<n;i++){
	        if(i!=0)
	            cout<<" ";
	        cout<<R[i];
	    }
	    cout<<endl;
    }
    return 0;
}
5.算法分析
5.1时间复杂度分析

1)分解:划分函数Partition需要扫描每个元素,每次扫描的元素个数不超过n,因此时间复杂度为O(n)。
2)治理:最好情况下,每次划分将问题分解为两个n/2的子问题,递归求解两个子问题,所需时间为2T(n/2) 。最坏情况下,每次划分将问题分解为1和n-1的子问题,所需时间为T(n-1) 。
3)合并:原地排序,合并操作不需要时间。

a.最好情况:
T ( n ) = { O ( 1 ) ,n=1 2 T ( n / 2 ) + O ( n ) ,n>1 T(n)= \begin{cases} O(1)& \text{,n=1}\\ 2T(n/2)+O(n)& \text{,n>1} \end{cases} T(n)={O(1)2T(n/2)+O(n),n=1,n1
T ( n ) = 2 T ( n / 2 ) + O ( n ) = 2 ( 2 T ( n / 2 2 ) + O ( n / 2 ) ) + O ( n ) = 2 2 T ( n / 2 2 ) + 2 O ( n ) = 2 3 T ( n / 2 3 ) + 3 O ( n ) = . . . . . . = 2 x T ( n / 2 x ) + x O ( n ) T(n)=2T(n/2)+O(n)=2(2T(n/2^2)+O(n/2))+O(n)=2^2T(n/2^2)+2O(n)=2^3T(n/2^3)+3O(n)=......=2^xT(n/2^x)+xO(n) T(n)=2T(n/2)+O(n)=2(2T(n/22)+O(n/2))+O(n)=22T(n/22)+2O(n)=23T(n/23)+3O(n)=......=2xT(n/2x)+xO(n)
n = 2 x , n=2^x, n=2x x = l o g n 。 T ( n ) = n T ( 1 ) + l o g n O ( n ) = n + l o g n O ( n ) = O ( n l o g n ) x=logn。T(n)=nT(1)+lognO(n)=n+lognO(n)=O(nlogn) x=lognT(n)=nT(1)+lognO(n)=n+lognO(n)=O(nlogn)

b.最坏情况:
T ( n ) = { O ( 1 ) ,n=1 T ( n − 1 ) + O ( n ) ,n>1 T(n)= \begin{cases} O(1)& \text{,n=1}\\ T(n-1)+O(n)& \text{,n>1} \end{cases} T(n)={O(1)T(n1)+O(n),n=1,n1
T ( n ) = T ( n − 1 ) + O ( n ) = T ( n − 2 ) + O ( n − 1 ) + O ( n ) = T ( n − 3 ) + O ( n − 2 ) + O ( n − 1 ) + O ( n ) = . . . . . . = T ( 1 ) + O ( 2 ) + . . . + O ( n − 1 ) + O ( n ) = O ( 1 ) + O ( 2 ) + . . . + O ( n − 1 ) + O ( n ) = O ( n ( n + 1 ) / 2 ) T(n)=T(n-1)+O(n)=T(n-2)+O(n-1)+O(n)=T(n-3)+O(n-2)+O(n-1)+O(n)=......=T(1)+O(2)+...+O(n-1)+O(n)=O(1)+O(2)+...+O(n-1)+O(n)=O(n(n+1)/2) T(n)=T(n1)+O(n)=T(n2)+O(n1)+O(n)=T(n3)+O(n2)+O(n1)+O(n)=......=T(1)+O(2)+...+O(n1)+O(n)=O(1)+O(2)+...+O(n1)+O(n)=O(n(n+1)/2)

c.平均情况:
T ( n ) = 1 n ∑ k = 1 n ( T ( n − k ) + T ( k − 1 ) ) + O ( n ) = 1 n ( T ( n − 1 ) + T ( 0 ) + T ( n − 2 ) + T ( 1 ) + . . . + T ( 1 ) + T ( n − 2 ) + T ( 0 ) + T ( n − 1 ) ) + O ( n ) = 2 n ∑ k = 1 n − 1 T ( k ) + O ( n ) T(n)=\frac{1}{n}\sum_{k=1}^{n}(T(n-k)+T(k-1))+O(n)=\frac{1}{n}(T(n-1)+T(0)+T(n-2)+T(1)+...+T(1)+T(n-2)+T(0)+T(n-1))+O(n)=\frac{2}{n}\sum_{k=1}^{n-1}T(k)+O(n) T(n)=n1k=1n(T(nk)+T(k1))+O(n)=n1(T(n1)+T(0)+T(n2)+T(1)+...+T(1)+T(n2)+T(0)+T(n1))+O(n)=n2k=1n1T(k)+O(n)

5.2空间复杂度分析:

在这里插入图片描述

最好(平均)情况:

  • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
  • 空间复杂度: O ( l o g n ) O(logn) O(logn)

最坏情况:

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CS_木成河

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值