快速排序(Quicksort)是对冒泡排序的一种改进。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。1
算法原理
- 从数列中选取一个数作为基准(pivot)
- 将数列中比pivot小的数移动到pivot左边,比pivot大的数移动到pivot右边。
- 以pivot为界,将数列分成两个子列,这样前一个子列的数都比都一个子列的要小。
- 对子列重复1-3的操作,直到所有子列都有序。
步骤分析
我认为在这个算法中,前面提到的第二步是比较困难。这里采用“挖空填空”的思路。
这里的在某位置挖空意思是某数据可以被覆盖,例如一种常见交换变量的方法:
/**
array.length() = 2, 求array的倒序.
*/
public void swap(Object[] array) {
Object tmp = array[0]; // 在array[0]位置挖空
array[0] = array[1];
array[1] = tmp;
}
为了方便起见,我们选取数列最左端的数作为pivot,将待排列的数组记作array,同时定义两个int left,和int right记录数组下标。
首先因为我们选取了最左端的数作为pivot,就相当于在array[0]挖了一个空,所以我们先比较array[right]。
- 若array[right]比pivot大(或等于),则说明array[right]位置正确,right下标向左移动一位,重复比较操作。
- 若array[right]比pivot小,则说明array[right]应该在pivot的左边,之前我们已经在array[0]处挖了个空,所以我们将array[0]设置为array[right]。这时候我们在array[right]处挖了个空,结束第一次移动。
将left下标向右移动一位,比较array[left],下面的操作和上面的很相似。
- 若array[left]比pivot小(或等于),则说明array[left]位置正确,left下标向右移动一位,重复比较操作。
- 若array[left]比pivot大,则说明array[left]应该在pivot的右边,由于之前我们已经在array[right]处挖了个空,所以我们将array[right]设置为array[left]。这时候我们在array[left]处挖了个空,结束第二次移动。
经过上面两个操作交替进行若干次,最终left=right,第一次排序结束,我们得到了两个子列array[i](i=0,1,2···,left - 1),和array[I](i=right + 1, right + 2,······,n),对得到子列重复上述操作,直到子列长度为1,这样我们就完成了数组的排序。
示意图
下面我用图形表示一下这个过程,两个箭头表示下标的位置,括号表示挖空的数据。
- 0x01 选取&比较 选取4作为pivot 比较right和pivot
$$ \begin{matrix} ↓&&&&&&&\\ (4)&7&6&2&1&8&2&\\ &&&&&&↑&\\ \end{matrix} $$
- 0x02 赋值 4 > 2 -> 将left设置为2
$$ \begin{matrix} ↓\\ 2&7&6&2&1&8&(2)\\ &&&&&&↑ \end{matrix} $$
- 0x03 移动&比较 将left向右移动一位 比较left和pivot
$$ \begin{matrix} &↓&&&&&&\\ 2&7&6&2&1&8&(2)&\\ &&&&&&↑&\\ \end{matrix} $$
- 0x04 赋值 4 < 7 -> 将right设置为7
$$ \begin{matrix} &↓&&&&&&\\ 2&(7)&6&2&1&8&7&\\ &&&&&&↑\\ \end{matrix} $$
- 0x05 移动&比较 将right向左移动一位 比较right和pivot
$$ \begin{matrix} &↓&&&&&&\\ 2&(7)&6&2&1&8&7&\\ &&&&&↑\\ \end{matrix} $$
- 0x06 再次移动&比较 4 < 8 -> 将right再向左移动一位 比较right和pivot
$$ \begin{matrix} &↓&&&&&&\\ 2&(7)&6&2&1&8&7&\\ &&&&↑\\ \end{matrix} $$
- 0x07 赋值 4 > 1 将left设置为1
$$ \begin{matrix} &↓&&&&&&\\ 2&1&6&2&(1)&8&7&\\ &&&&↑\\ \end{matrix} $$
- 0x08 移动&比较 将left向右移动一位 比较left和pivot
$$ \begin{matrix} &&↓&&&&&\\ 2&1&6&2&(1)&8&7&\\ &&&&↑\\ \end{matrix} $$
- 0x09 赋值 4 < 6 将right设置为6
$$ \begin{matrix} &&↓&&&&&\\ 2&1&(6)&2&6&8&7&\\ &&&&↑\\ \end{matrix} $$
- 0x0A 移动&比较 将right向左移动一位 比较right和pivot
$$ \begin{matrix} &&↓&&&&&\\ 2&1&(6)&2&6&8&7&\\ &&&↑\\ \end{matrix} $$
- 0x0B 赋值 4 > 2 将left设置为2
$$ \begin{matrix} &&↓&&&&&\\ 2&1&2&(2)&6&8&7&\\ &&&↑\\ \end{matrix} $$
- 0x0C 移动&检测 将left向右移动一位
$$ \begin{matrix} &&&↓&&&&\\ 2&1&2&(2)&6&8&7&\\ &&&↑\\ \end{matrix} $$
- 0x0D 赋值 将left(right)设置为pivot
$$ \begin{matrix} &&&↓&&&&\\ 2&1&2&4&6&8&7&\\ &&&↑\\ \end{matrix} $$
至此,原数列被分成两个子列Arrays.copyOfRange(array, 0, left -1)和Arrays.copyOfRange(array, right + 1, array.length()),左边的子列中的数都比pivot小,右边的都比pivot大。第一次排序结束。
小结
从上面的示意图分析我们可以知道2
- 下标left和right的作用是从两个方向扫描数列,left寻找比pivot大的数,right寻找比pivot小的数。
- 当下标left和right发现“位置错误”的数时,会将这个数赋值给上一次挖空的位置,然后在这次寻找到的位置挖空。
- 下标left挖空后,right左移;下标right挖空后,left右移。
实例代码(Java)
直接按照上面示意图分析,不难写处这个代码。
/**
* 使用快速排列算法排序数组.
* @param array 待排序的数组
* @param start 数组起始下标
* @param end 数组结束下标
*/
public static void quickSort(int[] array, int start, int end) {
if (end - start + 1 <= 1) { // 数组长度小于等于1
return;
}
int pivot = array[start]; // 选取最左边的数作为基准
int left = start; // 下标 left
int right = end; // 下标 right
sort:while(true) {
while(true) {
if (left == right) {
break sort;
}
if (array[right] < pivot) { // 下标right的数小于pivot
array[left] = array[right];
left++;
break;
} else {
right--;
}
}
while(true) {
if (left == right) {
break sort;
}
if (array[left] > pivot) { // 下标left的数大于pivot
array[right] = array[left];
right--;
break;
} else {
left++;
}
}
}
array[left] = pivot;
quickSort(array, start, left -1); // 递归排序左子列
quickSort(array, right + 1, end); // 递归排序右子列
}
当然这段代码右很多地方可以优化,例如可以将比较语句放到循环条件处,将赋值语句提到循环结构外。
另外这个提供一个调试用的代码,在每次执行操作前调用,可以输出这样的信息:
count=1, pivot=4, start=0, end=6
↓
4 7 6 2 1 8 2↑
需要导入
import java.util.Arrays;
import java.util.stream.Stream;
代码如下
static int count = 0;
public static void debug(int[] array, int start, int end, int left, int right, int pivot) {
count++;
System.out.println("\n\ncount=" + count + ", pivot=" + pivot + ", start=" + start + ", end=" + end);
StringBuilder[] strs = Stream.generate(StringBuilder::new).limit(3).toArray(StringBuilder[]::new);
for(int i=0;i<array.length;i++) {
if (i == left) {
strs[0].append("↓\t");
} else {
strs[0].append("\t");
}
if (i == right) {
strs[2].append("↑\t");
} else {
strs[2].append("\t");
}
strs[1].append(array[i]).append("\t");
}
Arrays.stream(strs).forEach(System.out::println);
}
后记
这里给出的快速排序代码是比较粗糙的,实际使用过程中还有很多的地方可以优化,比如取数组中间的数作为pivot,优化控制代码,根据数组的大小调整排序的方案等,有兴趣可以参考java.util.Arrays#sort(int[])3,另外快速排序是一个分治算法,可以方便地进行并行处理,可参考java.util.Arrays#parallelSort(int[])4。