分治算法,“分而治之”

分治

算法思想

把一个任务,分成形式和原任务相同,但规模更小的几个部分任务(通常是两个部分),分别完成,或只需要选一部分完成。然后再处理完成后的这一部分或几个部分的结果,实现整个任务的完成。

基本例题

case 1:归并排序

**排序思想:**数组排序任务分为以下三步完成

  • 把前一半排序
    • 把前一半的前一半排序
    • 把前一半的后一半排序
  • 把后一半排序
    • 把后一半的前一半排序
    • 把后一半的后一半排序
  • 把两半归并到一个新的有序数组,然后在拷贝回原数组,排序完成

示例代码

#include <iostream>
using namespace std;

// 将数组a的局部a[s,m]和a[m+1,e]合并到tmp,并保证tmp有序,然后再拷贝回a[s,e] 
void Merge(int a[], int s, int m, int e, int tmp[]) {
	int pb = 0;
	int p1 = s, p2 = m+1;
	while(p1 <= m && p2 <= e) {
		if(a[p1] < a[p2]) {
			tmp[pb++] = a[p1++];
		} else {
			tmp[pb++] = a[p2++];
		}
	}
	while(p1 <= m) {
		tmp[pb++] = a[p1++];
	}
	while(p2 <= e) {
		tmp[pb++] = a[p2++];
	}
	for(int i=0; i<e-s+1; i++) {
		a[s+i] = tmp[i];
	}
}

void MergeSort(int a[], int s, int e, int tmp[]) {
	if(s < e) {
		int m = s + (e-s)/2;
		MergeSort(a,s,m,tmp);
		MergeSort(a,m+1,e,tmp);
		Merge(a,s,m,e,tmp);
	}
}

int a[10] = { 13,27,19,2,8,12,2,8,30,89 };
int b[10];
int main() {
	int size = sizeof(a)/sizeof(int);
	MergeSort(a,0,size-1,b);
	for(int i=0; i<size; i++) {
		cout<<a[i]<<" ";
	} 
	cout<<endl;
	return 0;
} 

注意

  • 要注意拆分和合并的顺序,先对数组进行不断拆分的操作,直到拆分后的数组中只有1个元素,然后在进行合并。拆分数组容量从大到小,合并数组容量从小到大,两者是一个互逆的过程。
  • 在合并时要确保a[s,m]a[m+1,e]是有序的

时间复杂度分析
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ T(n) &= 2*T…

  • n / 2 k = 1 n/2^k=1 n/2k=1时, k = l o g 2 n k=log_2n k=log2n
  • T ( n ) = 2 k ∗ T ( n / 2 k ) + k ∗ a ∗ n = 2 k ∗ T ( 1 ) + k ∗ a ∗ n = 2 k + k ∗ a ∗ n = n + a ∗ l o g 2 n ∗ n T(n)=2^k*T(n/2^k)+k*a*n=2^k*T(1)+k*a*n=2^k+k*a*n=n+a*log_2n*n T(n)=2kT(n/2k)+kan=2kT(1)+kan=2k+kan=n+alog2nn
  • 故复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

case 2 :快速排序

**排序思想:**数组排序按以下步骤完成

  • k=a[0],将k移动到适当的位置,使得比k小的元素都位于k的左边,比k大的元素都位于k的右边,和k相等的,无所谓。 O ( n ) O(n) O(n)时间完成
  • k左边的部分快速排序
  • k右边的部分快速排序

示例代码

#include <iostream>
using namespace std;

// 交换变量a,b的值 
void swap(int &a, int &b) {
	int tmp =a;
	a = b;
	b = tmp; 
} 

void QuickSort(int a[], int s, int e) {
	if(s >= e) {
		return ;
	}
	int k = a[s];
	int i = s, j = e;
	while(i != j) {
		while(i < j && a[j] > k) {
			j--;
		}
		swap(a[i],a[j]);
		while(i < j && a[i] <= k) {
			i++;
		}
		swap(a[i],a[j]);
	}// 处理完后,a[i]=k
	QuickSort(a,s,i-1);
	QuickSort(a,i+1,e);
} 

int a[10] = { 13,27,19,2,8,12,2,8,30,89 };
int main() {
	
	// sizeof(int) 计算一个 int 型变量占内存多少单元。
	// sizeof(a) 计算整型数组里元素占用内存多少单元。
	int size = sizeof(a)/sizeof(int);	// 计算数组中的元素数量的个数 
	QuickSort(a,0,size-1);
	for(int i=0; i<size; i++) {
		cout<<a[i]<<" ";
	} 
	cout<<endl;
	return 0;
} 
  • while循环中采用两个指针ij分别指向当前元素,并和k比较,知道确定k的位置
  • 时间复杂度和归并排序相同: O ( n l o g n ) O(nlogn) O(nlogn)

case 3:输出前m大的数

**问题描述:**给定一个数组包含 n n n个元素,统计前 m m m大的数并且把其输出

**解题思路:**基本的解法就是将数组先排序再输出,这种做法的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),采用分治思想处理,可降低复杂度为 O ( n + m l o g ( m ) ) O(n+mlog(m)) O(n+mlog(m)),思路及关键步骤如下:

  • 思路:把前 m m m大的都弄到数组最右边,然后对这最右边的 m m m个元素排序,再输出
  • 关键: O ( n ) O(n) O(n)时间内实现把前 m m m大的都弄到数组最右边
    • 引入操作arrangeRight(k):把数组(或数组的一部分)前 k k k大的都弄到最右边
      • 设置key=a[0],将key挪到适当的位置,使得比key小的元素都在key左边,比key大的元素都在key的右边(线性时间内完成:快排)
      • 选择数组的前部或后部再进行arrangeRight操作,分为以下三种情况(假定key左端有元素b个,右端有元素a个)
        • a=m:完成
        • a>m:对右端a个元素再进行arrangeRight(m)操作
        • a<m:对左端b个元素进行arrangeRight(m-a-1)

示例代码

#include <iostream>
using namespace std;

// 交换变量a,b的值 
void swap(int &a, int &b) {
	int tmp =a;
	a = b;
	b = tmp; 
} 

void Print(int a[], int s, int e) {
	for(int i=s; i<=e; i++) {
		cout<<a[i]<<" ";
	}
}

void arrangeRight(int a[],int s,int e, int m) {
	if(s >= e) {
		return ;
	}
	int k = a[0];
	int i = s, j = e;
	while(i != j) {
		while(i < j && a[j] > k) {
			j--;
		}
		swap(a[i],a[j]);
		while(i < j && a[i] <= k) {
			i++;
		}
		swap(a[i],a[j]);
	}
	if(e - i == m) {
		Print(a,i+1,e);
	} else if(e - i > m) {
		arrangeRight(a,i+1,e,m);
	} else {
		Print(a,i,e);
		arrangeRight(a,s,i-1,m-e+i-1);
	}
} 

int a[10] = { 13,27,19,2,8,12,2,8,30,89 };
int main() {
	int size = sizeof(a)/sizeof(int);
	arrangeRight(a,0,size-1,8);
	return 0;
}

case 4:求排列的逆序数

**题目描述:**考虑 1 , 2 , 3 , . . . , n ( n ≤ 100000 ) 1,2,3,...,n(n\leq100000) 1,2,3,...,n(n100000)的排列 i 1 , i 2 , . . . , i n i_1,i_2,...,i_n i1,i2,...,in,如果其中存在 j , k j,k j,k,满足 j < k j<k j<k i j > i k i_j>i_k ij>ik,那么就称 ( i j , i k ) (i_j,i_k) (ij,ik)是这个排列的一个逆序。一个排列含有逆序的个数称为这个排列的逆序数。例如排列 263451 263451 263451含有 8 8 8个逆序 ( 2 , 1 ) , ( 6 , 3 ) , ( 6 , 4 ) , ( 6 , 5 ) , ( 6 , 1 ) , ( 3 , 1 ) , ( 4 , 1 ) , ( 5 , 1 ) (2,1),(6,3),(6,4),(6,5),(6,1),(3,1),(4,1),(5,1) (2,1),(6,3),(6,4),(6,5),(6,1),(3,1),(4,1),(5,1),因此该排列的逆序数就是 8 8 8。现在给定一个排列,求它的逆序数。

**解题思路:**基本想法是双重循环遍历,但是在 n n n足够大时,该方法会超时。现采用分治思想,结合归并排序求解。具体方法如下

  • 将数组分成两半,分别求出左半边的逆序数和右半边的逆序数
  • 再算有多少逆序是由左半边取一个数和右半边取一个数构成(要求 O ( n ) O(n) O(n)实现)。由于左右半边都是排好序的,因此左右半边只需从头到尾各扫一遍,就可以找出由两边各取一个数构成的逆序个数

示例代码

#include <iostream>
using namespace std;

void Merge(int a[], int s, int m, int e, int tmp[], int &count) {
	int pb = 0;
	int p1 = s, p2 = m+1;
	while(p1 <= m && p2 <= e) {
		if(a[p1] <= a[p2]) {
			tmp[pb++] = a[p1++];
		} else {
			count += (e-p2+1);
			tmp[pb++] = a[p2++];
		}
	}
	while(p1 <= m) {
		tmp[pb++] = a[p1++];
	}
	while(p2 <= e) {
		tmp[pb++] = a[p2++];
	}
	for(int i=0; i<e-s+1; i++) {
		a[s+i] = tmp[i];
	}
}

void MergeSortAndCount(int a[], int s, int e, int tmp[], int &count) {
	if(s < e) {
		int m = s + (e-s)/2;
		MergeSortAndCount(a,s,m,tmp,count);
		MergeSortAndCount(a,m+1,e,tmp,count);
		Merge(a,s,m,e,tmp,count);
	}
}

int count = 0;
int a[6] = {2,6,3,4,5,1};
int b[6];
int main() {
	int size = sizeof(a)/sizeof(int);
	MergeSortAndCount(a,0,size-1,b,count);
	cout<<"该排列的逆序数为 : "<<count<<endl;
	return 0;
} 

最后有需要工程文件的朋友可以在评论里说明(记得指明邮箱),小编看到后会第一时间发送到指定邮箱。文章如有不严谨之处,欢迎大家指正!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

steven_moyu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值