- 分治算法由两部分组成:
分:递归解决较小的问题(基本情况除外)。
治:然后从子问题的解构建原问题的解。 - 传统上,在正文中至少含有两个递归调用的例程叫作分治算法,而正文中只含有一个递归调用的例程不是分治算法。一般坚持子问题是不相交的(即基本不重叠)。
- 分治算法的经典例子有归并排序和快速排序,它们分别有O(N logN)的最坏情形以及平均情形的时间界。
- 所有有效的分治算法都是把问题分成一些子问题,每个子问题都是原问题的一部分,然后进行某些附加的工作以算出最后的答案。这里我们讨论分治算法中的快速排序算法。
- 快速排序算法的平均运行时间是O(N logN),该算法之所以特别快,主要是由于非常精练和高度优化的内部循环。它的最坏情形性能为O(N^2),但经过稍许努力可使这种情况极难出现。通过将快速排序和堆排序结合,由于堆排序的O(N logN)最坏情形运行时间,我们可以对几乎所有的输入都能达到快速排序的快速运行时间。
- 简单的递归排序算法实现:
/**
1. @author zclong
*/
public class Quicksort {
static int count = 0; // 计数器
public static void main(String[] args) {
List<Integer> items = new ArrayList<>();
for (int i = 0; i < 10; i++) {
items.add((int) (Math.random() * 10));
}
System.out.println("排序前列表:" + items);
long startTime = System.currentTimeMillis();
sort(items); // 进行排序
long endTime = System.currentTimeMillis();
System.out.println("排序后列表:" + items + ",递归次数:" + count);
System.out.println("所用时间为:" + (endTime - startTime) + "毫秒");
}
/**
* 排序算法实现
*
* @param items
*/
public static void sort(List<Integer> items) {
if (items.size() > 1) {
count++; // 计数
List<Integer> smaller = new ArrayList<>(); // 存储小于被选项的一组
List<Integer> same = new ArrayList<>(); // 存储等于被选项的一组
List<Integer> large = new ArrayList<>(); // 存储大于被选项的一组
Integer chosenItem = items.get(items.size() / 2); // 获取列表中间项的元素(被选项)
// 进行对列表遍历,与被选项比较
for (Integer i : items) {
if (i < chosenItem)
smaller.add(i);
else if (i > chosenItem)
large.add(i);
else
same.add(i);
}
sort(smaller); // 递归调用排序方法
sort(large);
items.clear();
items.addAll(smaller);
items.addAll(same);
items.addAll(large);
}
}
}
- 上述排序算法实现原理:
随机选取任意一项,如何形成三组:小于被选项的一组,等于被选项的一组,大于被选项的一组。 递归地对第一和第三组排序,然后把三组接龙。根据递归的基本原理,结果保证是对原始列表的一个有序排列 (如果表中含有大量重复项,以及相对较少的不同项,其表现是非常好的) 但是该方法会产生额外的列表,并且还是递归的这么做,我们必须避免使用大量额外的内存,并且有干净的内循环。于是快速排序通常应避免建立第二组(包含等于项的)。 经典的快速排序:其中输入存放在数组里,且算法不产生额外的数组。
将数组S排序的基本算法有下列四步完成 :
a,如果S中元素个数是0或者1,则返回。
b,取S中任一元素v,称之为枢纽元(pivot)。
c,将S-{v}(S中其余元素)划分成两个不相交的集合:S1={x∈S-{v} | x<=v}和S2={x∈S-{v} | x>=v}.
d,返回{ quicksort(S1)后跟v,继而返回quicksort(S2) }。以图例说明快速排序的各步:
如同归并排序那样,快速排序递归的解决两个子问题并需要线性的附加工作(第三步),不过,与归并排序不同,这两个子问题并不保证具有相同的大小,这是一个潜在的隐患。快速排序更快地原因在于,第三步分割成两组实际上是在适当位置进行并且非常有效,他的高效不仅可以弥补大小不等的递归调用而且还能有所超出。
实现快速排序的关键是第二步和第三步:
一、选取枢纽元:
a,一种错误的做法是将第一个元素用作枢纽元:
如果输入是随机的,那么这是可以接受的,而如果输入是预排序的或者是反序的,那么这样的枢纽元就产生一个劣质的分割,因为所有的元素不是被划入S1就是被划入S2。更糟糕的是这种情况毫无例外的发生在所有的递归调用中。因此使用第一个元素作为枢纽元是绝对可怕的坏主意。另一个想法是选取前两个互异的关键字中的较大者作为枢纽元,不过这和只选取第一个元素作为枢纽元具有相同的害处。不要使用这两种选取枢纽元的策略。
b,一种安全的做法是随机选取枢纽元:
因为随机的枢纽元不可能总在接连不断的产生劣质的分割。另一方面,随机数的生成开销很大,根本减少不了算法其余部分的平均运行时间。
c,三数中值分割法:
一组N个数的中指(也叫作中位数)是第N/2(上取整)个最大的数。枢纽元的最好的选择是数组的中值。不幸的是,这很难算出并且会明显减慢快速排序的速度。一般的做法是使用左端、右端和中心位置上的三个元素的中值做为枢纽元。例如,输入8,1,4,9,6,3,5,2,7,0,他的左元素是8,右元素是0,中心位置((left+right)/2)(下取整)上的元素是6。于是枢纽元则是v=6。显然使用三数中值分割法消除了预排序输入的坏情况(在这种情况下,这些分割都是一样的)。
二、分割策略:
分割是一种很容易出错或低效的操作,但使用一种已知的方法是安全的。
1、该法的第一步是将枢纽元与最后的元素交换使得枢纽元离开要被分割的数据段。i从第一个元素开始而j从倒数第二个元素开始。设输入的数据为8,1,4,9,6,3,5,2,7,0,枢纽元为6。
2、在分割阶段要做的就是把所有小的元素移到数组的左边而把所有大元素移到数组的右边。“小”和“大”是相对于枢纽元而言的。
当i在j的左边时,我们将i右移,移过那些小于枢纽元的元素,并将j左移,移过那些大于枢纽元的元素。当i和j停止时,i指向一个大元素而j指向一个小元素。如果i在j的左边,那么将这两个元素互换,其效果就是把一个大元素推向右边而把一个小元素推向左边。
3、然后我们交换由i和j指向的元素,重复该过程直到i和j彼此交错为止。
第一次交换后
第二次交换前
第二次交换后
第三次交换前
此时,i和j已经交错,故不再交换。分割的最后一步是将枢纽元与i所指向的元素交换。
在与枢纽元交换后:
对于很小的数组(N<=20),快速排序不如插入排序。通常解决对于小的数组不使用递归的快速排序,而取而代之的是如插入排序这样的对小数组有效的排序算法。
执行三数中值分割法的程序:
/**
* 三数中值分割法
* 一组N个数的中值(也叫中位数)是第N/2(下取整)个最大的数
* @param a
* @param left
* @param right
* @return
*/
@SuppressWarnings("hiding")
private static <Integer extends Comparable<? super Object>> Object median3(java.lang.Integer[] a, int left, int right) {
int center = (left + right) / 2;
if(a[center].compareTo(a[left]) < 0) {
swapReferences(a, left, center); //交换引用
}
if(a[right].compareTo(a[left]) < 0) {
swapReferences(a, left, right);
}
if(a[right].compareTo(a[center]) < 0) {
swapReferences(a, center, right);
}
swapReferences(a, center, right-1);
return a[right-1];
}
/**
* 对列表的引用进行交换
* @param a
* @param arg0
* @param arg1
*/
private static void swapReferences(Object[] a, int arg0, int arg1) {
Object temp;
temp = a[arg0];
a[arg0] = a[arg1];
a[arg1] = temp;
}
快速排序的主程序:
/**
* 快速排序的主方法
* @param a
* @param left
* @param right
*/
private static <Integer extends Comparable<? super Object>> void quicksort(java.lang.Integer[] a, int left, int right) {
// 当i和j已经交错,故不再交换
if(left>=right) {
return;
}
int pivot = (int) median3(a, left, right); // 获取枢纽元
System.out.println("枢纽元为:" + pivot);
/*System.out.println("快速排序后列表为:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}*/
int i = left, j = right-1;
while(i != j) {
while(a[++i].compareTo(pivot) < 0) {}
while(a[--j].compareTo(pivot) > 0) {}
if(i < j) {
swapReferences(a, i, j);
}else {
break;
}
}
swapReferences(a, i, right-1);
quicksort(a, left, i-1);
quicksort(a, i+1, right);
}
快速排序的最好的情况分析:当枢纽元正好位于中间。
T(N) = 2T(N/2) + cN 可推出 T(N) = cN logN + N = O(N logN)快速排序的最坏的情况分析:当枢纽元始终是最小元素。
T(N) = T(N - 1) + cN 可推出 T(N) = O(N^2)- 快速排序的平均时间:对于平均情况,假设对于S1,每个大小都是等可能的,因此每个大小均有概率1/N。
推得: T(N) = O(N logN)