第三章 分治法
1. 分治法概述
1.1 分治法的设计思想
- 分治法:对于一个规模为n的问题:若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解
- 分治法所能解决的问题的特征
- 该问题的规模缩小到一定的程度就可以容易地解决
- 该问题可以分解为若干个规模较小的相同问题
- 利用该问题分解出的子问题的解可以合并为该问题的解
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题
1.2 分治法的求解过程
-
分治法通常采用递归算法设计技术,在每一层递归上都有3个步骤:
- 分解:将原问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题
- 求解子问题:若子问题规模较小而容易被解决则直接求解,否则递归地求解各个子问题
- 合并:将各个子问题的解合并为原问题的解
-
分治法的一般的算法设计框架
divide-and-conquer(P){ if(|P| ≤ n0) return adhoc(P); // 将P分解为较小的子问题 P1,P2,…,Pk; for(i=1;i<=k;i++){ //循环处理k次 yi=divide-and-conquer(Pi); //递归解决Pi return merge(y1,y2,…,yk); //合并子问题 } }
-
人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同,即,将一个问题分成大小相等的k个子问题的处理方法是行之有效的;当k=1时称为减治法
-
许多问题可以取 k=2,称为二分法,如下图示,这种使子问题规模大致相等的做法是出自一种平衡子问题的思想,它几乎总是比子问题规模不等的做法要好
2. 求解排序问题
2.1 快速排序
-
基本思想:在待排序的n个元素中任取一个元素(通常取第一个元素)作为基准,把该元素放入最终位置后,整个数据序列被基准分割成两个子序列,所有小于基准的元素放置在前子序列中,所有大于基准的元素放置在后子序列中,并把基准排在这两个子序列的中间,这个过程称作划分。然后对两个子序列分别重复上述过程,直至每个子序列内只有一个记录或空为止
-
分治策略:
- 分解:将原序列a[s…t]分解成两个子序列a[s…i-1]和a[i+1…t],其中i为划分的基准位置
- 求解子问题:若子序列的长度为0或为1,则它是有序的,直接返回;否则递归地求解各个子问题
- 合并:由于整个序列存放在数组中a中,排序过程是就地进行的,合并步骤不需要执行任何操作
-
示例:对于{2,5,1,7,10,6,9,4,3,8}序列,其快速排序过程如下
-
快速排序算法代码:
// 划分算法 int Partition(int a[], int s, int t){ int i=s, j=t; int tmp=a[s]; // 用序列的第1个记录作为基准 while(i != j){ // 从序列两端交替向中间扫描,直至i=j为止 while (j>i && a[j]>=tmp) j--; // 从右向左扫描,找第1个关键字小于tmp的a[j] a[i] = a[j]; // 将a[j]前移到a[i]的位置 while (i<j && a[i]<=tmp) i++; //从左向右扫描,找第1个关键字大于tmp的a[i] a[j] = a[i]; //将a[i]后移到a[j]的位置 } a[i]=tmp; return i; } //对a[s..t]元素序列进行递增排序 void QuickSort(int a[], int s, int t){ if(s < t){ //序列内至少存在2个元素的情况 int i=Partition(a, s, t); QuickSort(a, s, i-1); //对左子序列递归排序 QuickSort(a, i+1, t); //对右子序列递归排序 } }
-
算法分析:
快速排序的时间主要耗费在划分操作上,对长度为n的区间进行划分,共需n-1次关键字的比较,时间复杂度为O(n)
对n个记录进行快速排序的过程构成一棵递归树,在这样的递归树中,每一层至多对n个记录进行划分,所花时间为O(n)
当初始排序数据正序或反序时,此时的递归树高度为n,快速排序呈现最坏情况,即最坏情况下的时间复杂度为O(n2);当初始排序数据随机分布,使每次分成的两个子区间中的记录个数大致相等,此时的递归树高度为log2n,快速排序呈现最好情况,即最好情况下的时间复杂度为O(nlog2n);快速排序算法的平均时间复杂度也是O(nlog2n)
2.2 归并排序
基本思想:
首先将a[0…n-1]看成是n个长度为1的有序表,将相邻的k(k≥2)个有序子表成对归并,得到n/k个长度为k的有序子表;然后再将这些有序子表继续归并,得到n/k2个长度为k2的有序子表,如此反复进行下去,最后得到一个长度为n的有序表
若k=2,即归并在相邻的两个有序子表中进行的,称为二路归并排序;若k>2,即归并操作在相邻的多个有序子表中进行,则叫多路归并排序
2.2.1 自底向上的二路归并排序算法
-
示例:对于{2,5,1,7,10,6,9,4,3,8}序列
-
分支策略:循环log2n次,length依次取1、2、…、log2n。每次执行以下步骤:
- 分解:将原序列分解成length长度的若干子序列
- 求解子问题:将相邻的两个子序列调用Merge算法合并成一个有序子序列
- 合并:由于整个序列存放在数组中a中,排序过程是就地进行的,合并步骤不需要执行任何操作
-
算法代码:
// a[low..mid]和a[mid+1..high]→a[low..high] void Merge(int a[], int low, int mid, int high){ int *tmpa; int i=low, j=mid+1, k=0; tmpa=(int *)malloc((high-low+1)*sizeof(int)); while (i<=mid && j<=high){ if (a[i]<=a[j]){ // 将第1子表中的元素放入tmpa中 tmpa[k]=a[i]; i++; k++; }else{ // 将第2子表中的元素放入tmpa中 tmpa[k]=a[j]; j++; k++; } } while (i<=mid){ // 将第1子表余下部分复制到tmpa tmpa[k]=a[i]; i++; k++; } while (j<=high){ // 将第2子表余下部分复制到tmpa tmpa[k]=a[j]; j++; k++; } for (k=0, i=low; i<=high; k++, i++) a[i]=tmpa[k];// 将tmpa复制回a中 free(tmpa); // 释放tmpa所占内存空间 } // 一趟二路归并排序 void MergePass(int a[], int length, int n){ int i; for (i=0; i+2*length-1<n; i=i+2*length){ //归并length长的两相邻子表 Merge(a,i,i+length-1,i+2*length-1); } if (i+length-1<n){ //余下两个子表,后者长度小于length Merge(a, i, i+length-1, n-1); //归并这两个子表 } } //二路归并算法 void MergeSort(int a[], int n){ int length; for (length=1; length<n; length=2*length) MergePass(a, length, n