基本思想
快速排序每次从当前需要排序的部分数组中,选择一个元素,以这个元素为基点。
比如像下面这个数组中,选择“4”这个元素作为基点,之后想办法把4这个元素,移动到它在排好序的时候应该所处的位置,“4”这个元素处在这个位置,使得整个数组就有了一个性质,“4”之前所有的元素都是小于“4”的,“4”之后所有的的元素都是大于“4”的。之后再对小于“4”的这部分子数组和大于“4”的这部分子数组,分别继续使用快速排序的思路进行排序,逐渐递归下去完成整个排序过程。
为此大家就可以想象了,对于快速排序算法来说,最重要的就是如何把一个选定的元素比如说这个“4”,移动到正确的位置上。而这个过程,也是快速排序的核心,通常我们管这个子过程叫Partition(划分),也就是把整个数组分成两部分的过程。
Partition过程
在这个过程中,通常使用需要排序部分的数组的第一个元素,来作为我们分界的基点标志点。对于这个数组来说,它的第一个元素叫l(left),之后我们逐渐遍历右边所有没有被访问过的元素,在遍历的过程中,我们将逐渐的整理数组,让整个数组一部分小余V元素,另外一部分大于V元素。
在这个过程中,我们需要记录"<V"和">V"的分界点,用索引“j”来记录这个位置,当前访问的元素用索引"i"记录。
这样一来 arr[l+1...j] < v和arr[j+1...i-1] > v。这两部分都是闭区间。
下面我们来讨论"i"这个位置,如何来决定当前的元素要怎样变化,才能使整个数组还保持这样的性质。我们来分两种情况讨论。
1.如果当前元素“e”大于元素“v”(e>v),这种情况非常简单,元素“e”直接就放到“>v”这部分后面,然后我们马上去一个元素“i++”
2.如果当前元素“e”小于元素“v”(e<v),这时候我们需要想办法把元素“e”放到“<v”这部分(黄色部分)。其实也非常简单,只需要把arr[i](元素e)和arr[j+1]两个元素交换。大家就可以看到,有一个“>v”的元素放到了现在“i”的位置,而我们当前考察的这个"<v"的元素“e”,放到了“j”的后面。这种情况下需要“j++”,这样相当于“<v”这部分(黄色部分)的元素多了一个,最后再进行“i++”,来考察下一个元素。
使用这种方式对整个要考察的数组进行遍历,在遍历完成后,数组就变成这个样子。整个数组被分成了三部分,第一个元素是“v”,橙色的部分“<v”,紫色的部分“>v”。
最后我们要做的事情,只需要把数组中“l”位置的元素和“j”位置的元素进行交换。交换完成之后,整个数组就变成我们设想的那样,被分成了“<v”和“>v”的两部分,而“v”这个元素则放到它应该在的位置,而此时指向“v”这个元素的索引就是“j”这个位置。
代码实现如下:
package com.zeng.sort;
import java.util.Arrays;
public class QuickSort {
public void quickSort(int[] arr){
quickSort(arr, 0, arr.length - 1);
}
/**
* 使用递归,对arr[left...right]部分进行快速排序,区间是前闭后闭的
* @param arr
* @param left
* @param right
*/
private void quickSort(int[] arr, int left, int right){
if(left >= right){
return;
}
//先规划数组
int p = partition(arr, left, right);
//再对两部分数组分别做快速排序
quickSort(arr, left, p - 1);
quickSort(arr, p + 1, right);
}
/**
* 对arr[left...right]部分进行partition操作
* @param arr
* @param left
* @param right
* @return 返回p,使得arr[left...p-1] < arr[p]; arr[p+1...right] > arr[p]
*/
private int partition(int[] arr, int left, int right){
//分成了两部分[left+1, j],[j+1, i)
int v = arr[left];
//arr[left+1...j] < v; arr[j+1...i) > v
//初始化两个为空的区间arr[left+1...j]和arr[j+1...i)
//使得整个程序从初始的情况下,都满足这个条件。
int j = left;
for(int i = left + 1; i <= right; i++){
if(arr[i] < v){
int temp = arr[j + 1];
arr[j + 1] = arr[i];
arr[i] = temp;
j++;
}
}
int temp = arr[left];
arr[left] = arr[j];
arr[j] = temp;
return j;
}
}
生成测试数组的代码如下:
/**
* 生成完全随机数组
* @param n 数组的规模
* @param rightL 数组中元素的最小值
* @param rightR 数组中元素的最大值
* @return
*/
public static int[] generateRandomArray(int n, int rightL, int rightR){
if(rightL > rightR || n < 0){
throw new IllegalArgumentException();
}
int[] arr = new int[n];
for(int i = 0; i < n; i ++){
arr[i] = (int)(Math.random()*(rightR - rightL + 1) + rightL);
}
return arr;
}
/**
* 生成一个近乎有序的数组
* @param n 数组的规模
* @param swapTimes 进行数据交换的对数
* @return
*/
public static int[] generateNearlyOrderedArray(int n, int swapTimes){
if(n < 0 || swapTimes < 0){
throw new IllegalArgumentException();
}
int[] arr = new int[n];
for(int i = 0; i < n; i ++){
arr[i] = i;
}
for(int i = 0; i < swapTimes; i++){
int posx = (int)(Math.random() * n);
int posy = (int)(Math.random() * n);
int temp = arr[posx];
arr[posx] = arr[posy];
arr[posy] = temp;
}
return arr;
}
这里对上面实现的快速排序和上篇文章的归并排序做对比测试
一个100万大小的完全随机的数组,测试结果:
QuickSort: 1000000 true 97ms
MergeSort: 1000000 true 197ms
从这个测试结果中,可以看出上面实现的快速排序有很大的缺点。对于完全随机的数组排序时,快速排序比归并排序的效率要高。
快速排序的简单优化
高级的排序算法递归到底层的时候,都可以采用插入排序算法来做优化。
package com.zeng.sort;
import java.util.Arrays;
public class QuickSort {
public void quickSort(int[] arr){
quickSort(arr, 0, arr.length - 1);
}
/**
* 使用递归,对arr[left...right]部分进行快速排序,区间是前闭后闭的
* @param arr
* @param left
* @param right
*/
private void quickSort(int[] arr, int left, int right){
// if(left >= right){
// return;
// }
//优化:对元素量比较少的部分,用插入排序法进行优化
if(right - left <= 15){
insertionSort(arr, left, right);
return;
}
//先添加一些规划数组
int p = partition(arr, left, right);
//再对两部分数组分别做快速排序
quickSort(arr, left, p - 1);
quickSort(arr, p + 1, right);
}
/**
* 插入排序算法,对数组中子数组[left, right]进行排序.
* @param arr
* @param left
* @param right
*/
private void insertionSort(int[] arr, int left, int right){
for(int i = left + 1; i <= right; i ++){
int e = arr[i];
int j = i;
for(; j > left && arr[j - 1] > e; j --){
arr[j] = arr[j - 1];
}
arr[j] = e;
}
}
/**
* 对arr[left...right]部分进行partition操作
* @param arr
* @param left
* @param right
* @return 返回p,使得arr[left...p-1] < arr[p]; arr[p+1...right] > arr[p]
*/
private int partition(int[] arr, int left, int right){
//分成了两部分[left+1, j],[j+1, i)
int v = arr[left];
//arr[left+1...j] < v; arr[j+1...i) > v
//初始化两个为空的区间arr[left+1...j]和arr[j+1...i)
//使得整个程序从初始的情况下,都满足这个条件。
int j = left;
for(int i = left + 1; i <= right; i++){
if(arr[i] < v){
swap(arr, ++j, i);
}
}
swap(arr, left, j);
return j;
}
/**
* 交换数组中两个元素的值
* @param arr
* @param i
* @param j
*/
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
这里对做了第一条优化的快速排序和上篇文章的归并排序做对比测试
一个100万大小的完全随机的数组,测试结果:
QuickSort: 1000000 true 88ms
MergeSort: 1000000 true 198ms
从测试结果可以看出,用插入排序算法对小规模的子数组进行排序,这个优化策略对提升快速排序法的效率是有帮助的。
下面我们来看另外一组测试结果,
一个100万大小的近乎有序的数组,测试结果:
QuickSort: 1000000 true 18892ms
MergeSort: 1000000 true 43ms
所以接下来我们将介绍快速排序法的一个优化策略 随机化快速排序法。