Algorithm——快速排序
对于包含n个数的输入数组来说,快速排序是一种最坏情况实际复杂度为O(n^2)的排序算法。虽然最坏情况时间复杂度很差,但是快速排序通常是实际排序应用中最好的选择,因为它的平均性能非常好:它的期望时间复杂度是O(nlgn)。
(原址排序:在排序算法中,如果输入数组中仅有常数个元素需要在排序过程中存储在数组之外,则称排序算法是原址的)
与归并排序一样,快速排序也使用了分治思想。《算法导论》中给出了一个典型子数组A[p...r]进行快速排序的三步分治过程:
- 分解:数组A[p...r]被划分成两个(可能为空)子数组A[p...q-1]和A[q+1...r],使得A[p...q-1]中的每一个元素都小于等于A[q],而A[q]也小于等于A[q+1...r]中的每个元素。其中,计算下标q也是划分过程的一部分。
- 解决:通过递归调用快速排序,对子数组A[p...q-1]和A[q+1...r]进行排序。
- 合并:因为子数组都是原址排序的,所以不需要合并操作:数组A[p...r]已经有序。
下面是快速排序的伪代码实现:
QUICKSORT(A, p, r)
if p < r
q = PARTITION(A, p, r)
QUICKSORT(A, p, q - 1)
QUICKSORT(A, q +1, r)
为了排序一个数组A的全部元素,初始调用是QUICKSORT(A, 1, A.length)
其中,算法的关键部分是PARTITION(A, p, r)过程,它实现了对子数组A[p...r]的原址排序:
PARTITION(A, p, r)
x = A[r]
i = p - 1
for j = p to r - 1
if A[j] = < x
i = i + 1
exchange A[i] with A[j]
exchange A[i+1] with A[r]
return i + 1
这部分是《算法导论》中给出的子数组划分的伪代码;该方法的主要思想就是:
- 先从数组中取出一个数最为基准数(pivot element),围绕它划分数组
- 划分过程,将比这个数大的元素都放到它的右边;将比它小于等于的元素的全部放到它的左边
- 在对第二部划分的子数组重复进行第二不,直到各区间只有一个数
为了更进一步了解快速排序的本质,我们再看一下百度百科对排序算法实现思路的解释:
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
一趟快速排序的算法是:
- 设置两个变量i、j,排序开始的时候:i=0,j=N-1;
- 以第一个数组元素作为关键数据,赋值给key,即key=A[0];
- 从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
- 从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
- 重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+1或j-1完成的时候,此时令循环结束)。
这是一个示例,来自百度百科。假设用户输入了如下数组:
下标
|
0
|
1
|
2
|
3
|
4
|
5
|
数据
|
6
|
2
|
7
|
3
|
8
|
9
|
下标
|
0
|
1
|
2
|
3 |
4
|
5
|
数据
|
3
|
2
|
7
|
6
|
8
|
9
|
下标
|
0
|
1
|
2
|
3
|
4
|
5
|
数据
|
3
|
2
|
6
|
7
|
8
|
9
|
下标
|
0
|
1
|
2
|
3
|
4
|
5
|
数据
|
3
|
2
|
6
|
7
|
8
|
9
|
对排序算法有了一定的认识后,下面就参照《算法导论》实现普通版的快速排序算法:
/**
*
* 快速排序算法
*
* @param A
* 待排序的主数组
* @param p
* 要排序的子数组的下标下边界
* @param r
* 要排序的子数组的下标上边界
*/
public void quickSort(int[] A, int p, int r) {
if (p < r) {
int q = partition(A, p, r);
quickSort(A, p, q - 1);
quickSort(A, q + 1, r);
}
}
/**
*
* 在给定的数组下标区间范围内,根据选定的主元划分子数组
*
* @param A
* 待排序的主数组
* @param p
* 子数组下标的左边界
* @param r
* 子数组下标的右边界
* @return 得到的下标边界点
*/
private int partition(int[] A, int p, int r) {
int pivot = A[r];// pivot element
int i = p - 1;
for (int j = p; j < r; j++) {
// 当pivot >= A[j]时,下标为j的元素就是需要与下标为i的元素进行交换的元素;下表为i的元素是前面那些比pivot大的元素的下标;即,在发现一个元素比pivot小时,就将此元素与之前的比pivot元素大的元素进行交换;这样比pivot小的元素总会出现在数组左边,并连续出现.
if (A[j] <= pivot) {
i++;// 下标自加1;每次自加1后,当前下标i指向的元素都比pivot大
exchange(A, i, j);
}
}
// for循环结束后此时数组A[p...r]的状态是:
// 对于任意下标k,如果k在[p,i]区间内,总有A[k] <= pivot;
// 对于任意下标k,如果k在[i+1,j]区间内,总有A[k] >= pivot;
// 若下标k = r,则A[r] = pivot;
exchange(A, i + 1, r);// 将A[i+1]与A[r]交换,此时会有小远(i+1)下标的元素的值都小于等于pivot;大于(i+1)下标的元素值都大于pivot
return i + 1;
}
public void exchange(int[] A, int idex, int idex2) {
int temp = A[idex];
A[idex] = A[idex2];
A[idex2] = temp;
}
有时候我们可以通过在算法中加入随机性,从而使得算法对于所有的输入都能获得较好的期望性能。很多人都选择随机化版本的快速排序作为大数据输入情况下的排序算法。《算法导论》就介绍了快速排序的一个随机化版本,即数组划分函数partition()中使用了随机抽样技术,它的伪代码实现如下:
RANDOMIZED-PARTITION(A, p, r)
i = RANDOM(p, r)
exchange A[r] with A[i]
return PARTITION(A, p, r)
随机抽样是从子数组A[p...r]中随机选择一个元素作为pivot element。为达到这一目的,首先将A[r]与从A[p...r]中随机选出的一个元素交换。通过对p,...,r的随机抽样,我们可以保证pivot elemen(主元)x=A[r]是等概率地从子数组的r-p+1个元素中选取的。因为主元元素时随机抽取的,我们期望在平均情况下,对输入数组的划分是比较均衡的。
新的快速排序不再使用PARTITION,而是调用RANDOMIZED-PARTITION:
RANDOMIZED-QUICKSORT(A, p, r)
if p < r
q = RANDOMIZED-PARTITION(A, p, r)
RANDOMIZED-QUICKSORT(A, p, q-1)
RANDOMIZED-QUICKSORT(A, q+1, r)
根据随机版本的伪代码实现,我们可以得出快速排序的随机版本是:
/**
*
* 在给定的数组下标区间范围内,根据选定的主元划分子数组
*
* @param A
* 待排序的主数组
* @param p
* 子数组下标的左边界
* @param r
* 子数组下标的右边界
* @return 得到的子数组划分分界下标值
*/
private int partition(int[] A, int p, int r) {
int pivot = A[r];// pivot element
int i = p - 1;
for (int j = p; j < r; j++) {
// 当pivot >= A[j]时,下标为j的元素就是需要与下标为i的元素进行交换的元素;下表为i的元素是前面那些比pivot大的元素的下标;即,在发现一个元素比pivot小时,就将此元素与之前的比pivot元素大的元素进行交换;这样比pivot小的元素总会出现在数组左边,并连续出现.
if (A[j] <= pivot) {
i++;// 下标自加1;每次自加1后,当前下标i指向的元素都比pivot大
exchange(A, i, j);
}
}
// for循环结束后此时数组A[p...r]的状态是:
// 对于任意下标k,如果k在[p,i]区间内,总有A[k] <= pivot;
// 对于任意下标k,如果k在[i+1,j]区间内,总有A[k] > pivot;
// 若下标k = r,则A[r] = pivot;
exchange(A, i + 1, r);// 将A[i+1]与A[r]交换,此时会有小远(i+1)下标的元素的值都小于等于pivot;大于(i+1)下标的元素值都大于pivot
return i + 1;
}
/**
* 加入随机抽样的快速排序算法
*
* @param A
* 待排序的数组A
* @param p
* 待排序的子数组下标左边界
* @param r
* 带排序的子数组下标右边界
*/
public void randomizedQuickSort(int[] A, int p, int r) {
if (p < r) {
int q = randomizedPartition(A, p, r);
randomizedQuickSort(A, p, q - 1);
randomizedQuickSort(A, q + 1, r);
}
}
// 用于随机抽样
private Random util = new Random();
/**
*
* 加入随机抽样的数组划分算法
*
* @param A
* 待排序的数组A
* @param p
* 待排序的子数组下标左边界
* @param r
* 带排序的子数组下标右边界
* @return 得到的子数组划分分界下标值
*/
public int randomizedPartition(int[] A, int p, int r) {
int i = util.nextInt(r - p + 1)/* 区间[0,r-p+1) */ + p;// 随机生成一个在区间[p,r]之间的下标值
exchange(A, i, r);// 保证子数组划分所需要的pivot element是随机从A[p...r]中选取的
return partition(A, p, r);// 调用常规子数组划分函数,进行子数组划分
}
public void exchange(int[] A, int idex, int idex2) {
int temp = A[idex];
A[idex] = A[idex2];
A[idex2] = temp;
}
本文中讲述快速排序设计到的完整测试代码如下:
public class SortAlgor {
public static void main(String[] args) {
int[] A = { 2, 5, 10, 8, 7, 12, 15, 4 };
SortAlgor algorithm = new SortAlgor();
algorithm.randomizedQuickSort(A, 0, A.length - 1);
algorithm.quickSort(A, 0, A.length - 1);
//algorithm.arrayPrint(A);
}
/**
*
* 快速排序算法
*
* @param A
* 待排序的主数组
* @param p
* 要排序的子数组的下标下边界
* @param r
* 要排序的子数组的下标上边界
*/
public void quickSort(int[] A, int p, int r) {
if (p < r) {
int q = partition(A, p, r);
quickSort(A, p, q - 1);
quickSort(A, q + 1, r);
}
}
/**
*
* 在给定的数组下标区间范围内,根据选定的主元划分子数组
*
* @param A
* 待排序的主数组
* @param p
* 子数组下标的左边界
* @param r
* 子数组下标的右边界
* @return 得到的子数组划分分界下标值
*/
private int partition(int[] A, int p, int r) {
int pivot = A[r];// pivot element
int i = p - 1;
for (int j = p; j < r; j++) {
// 当pivot >= A[j]时,下标为j的元素就是需要与下标为i的元素进行交换的元素;下表为i的元素是前面那些比pivot大的元素的下标;即,在发现一个元素比pivot小时,就将此元素与之前的比pivot元素大的元素进行交换;这样比pivot小的元素总会出现在数组左边,并连续出现.
if (A[j] <= pivot) {
i++;// 下标自加1;每次自加1后,当前下标i指向的元素都比pivot大
exchange(A, i, j);
}
}
// for循环结束后此时数组A[p...r]的状态是:
// 对于任意下标k,如果k在[p,i]区间内,总有A[k] <= pivot;
// 对于任意下标k,如果k在[i+1,j]区间内,总有A[k] > pivot;
// 若下标k = r,则A[r] = pivot;
exchange(A, i + 1, r);// 将A[i+1]与A[r]交换,此时会有小远(i+1)下标的元素的值都小于等于pivot;大于(i+1)下标的元素值都大于pivot
return i + 1;
}
/**
* 加入随机抽样的快速排序算法
*
* @param A
* 待排序的数组A
* @param p
* 待排序的子数组下标左边界
* @param r
* 带排序的子数组下标右边界
*/
public void randomizedQuickSort(int[] A, int p, int r) {
if (p < r) {
int q = randomizedPartition(A, p, r);
randomizedQuickSort(A, p, q - 1);
randomizedQuickSort(A, q + 1, r);
}
}
// 用于随机抽样
private Random util = new Random();
/**
*
* 加入随机抽样的数组划分算法
*
* @param A
* 待排序的数组A
* @param p
* 待排序的子数组下标左边界
* @param r
* 带排序的子数组下标右边界
* @return 得到的子数组划分分界下标值
*/
public int randomizedPartition(int[] A, int p, int r) {
int i = util.nextInt(r - p + 1)/* 区间[0,r-p+1) */ + p;// 随机生成一个在区间[p,r]之间的下标值
exchange(A, i, r);// 保证子数组划分所需要的pivot element是随机从A[p...r]中选取的
return partition(A, p, r);// 调用常规子数组划分函数,进行子数组划分
}
public void exchange(int[] A, int idex, int idex2) {
int temp = A[idex];
A[idex] = A[idex2];
A[idex2] = temp;
}
public void arrayPrint(int[] A) {
System.out.print("[");
for (int i = 0; i < A.length; i++) {
System.out.print(A[i] + ", ");
}
System.out.println("]");
}
}