什么是快速排序(Quick Sort)、
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
实现步骤
每一轮排序选择一个基准点(point)进行分区,让小于基准点的元素进入一个分区,大于基准点的元素进入另一个分区;当分区完成时,基准点元素的位置就是其在整个数组排序好之后的位置。(即这一轮结果确定了基准点元素的最终位置)
在子分区中重复步骤1的过程,直至分区元素个数小于等于1,这体现了**分而治之(divide-and-conquer,简称D and C)**的思想。
单边循环快排实现方式:
代码实现
public class QuickSort01 {
public static void main(String[] args) {
int[] arr = {5, 3, 6, 1, 2, 4, 9, 8, 7};
quick(arr, 0, arr.length - 1);
}
public static void quick(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int p = partition(arr, left, right);
quick(arr, left, p - 1);
quick(arr, p + 1, right);
}
public static int partition(int[] arr, int left, int right) {
//定义一个基准点元素,通常为数组的最后一个元素
int point = arr[right];
//定义一个元素待交换的位置索引
int i = left;
for (int j = left; j < right; j++) {
if (arr[j] < point) {
if (i != j){
swap(arr, i, j);
}
//每次交换后,待交换元素索引位置右移一位
i++;
}
}
if (i != right){
swap(arr, right, i);
}
System.out.println(Arrays.toString(arr) + " i=" + i);
return i;
}
public static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
输出结果

进一步解析
事实上,每轮partition的最后结果都是能确定该轮基准点的最终位置。
调用partition方法,传入的参数为
- arr:要排序的数组
- left:元素初始待交换的位置索引,数组从左往右与基准点比较。(第一轮比较为整个数组,故第一个元素索引为0)
- right:定义基准点元素的索引,默认数组最后一个元素为基准点。(第一轮比较为整个数组,故第一轮的基准点为arr.length - 1 )
- 我们可以看到,quick方法是一个递归方法,而退出循环的条件是 left >= right ,即初始待交换的位置索引与基准点元素的索引重合(此时无需比较,该位置就是基准点的正确位置)或基准点元素索引在初始待交换位置索引的左边,而我们的比较时从左往右比的,此时比较无意义,两种情况都可以说明该分区已经排序完毕。在满足退出循环的条件前,quick将一直递归调用,直到每一个分区完毕。
实现方式:
代码实现
public class QuickSort02 {
public static void main(String[] args) {
int[] arr = {5, 3, 6, 1, 2, 4, 9, 8, 7};
quick(arr, 0, arr.length - 1);
}
public static void quick(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int p = partition(arr, left, right);
quick(arr, left, p - 1);
quick(arr, p + 1, right);
}
public static int partition(int[] arr, int left, int right) {
//定义基准点元素,双边循环中一般取数组首元素
int point = arr[left];
int i = left;
int j = right;
while (i < j) {
//j从右往左找比基准点小的元素
while (i < j && arr[j] > point) {
j--;
}
//i从左往右找比基准点大的元素
while (i < j && arr[i] <= point) {
i++;
}
swap(arr, i, j);
}
swap(arr, left, i);
System.out.println(Arrays.toString(arr) + " i=" + i);
return i;
}
public static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
输出结果

进一步解析
递归方法quick与单边循环快排原理基本一致。
与单边循环快排不同的是,双边循坏快排定义的基准点为该分区的第一个元素。(int point = arr[left])
定义一个变量 i,初始值为该分区的首元素位置索引,用来从左往右找比基准点大的元素。
定义一个变量 j,初始值为该分区尾元素的位置索引,用来从右往左找比基准点小的元素。
注意点一:while (
i < j&& arr[j] > point)与while (i < j&& arr[i] <= point)中 i < j 的意义是保证变量 i 的索引不会比 j 大。
注意点二:while (i < j && arr[i]
<=point)中<=而不是<的原因是,如果是<,第一次while循环arr[ i ]的值是等于point基准点的,所以不满足<的条件,直接退出循环,导致 i 指向的是基准点元素,为了让 i++正常执行,需要设置为<=。注意点三:在2的前提下,一定要
先执行 j,再执行 i。即让 j 先在找到比基准点小的元素,然后再让 i 去找比基准点大的元素。如果先执行 i,再执行 j,会导致这一轮循环完毕后(即 i = j )时,i 与 j 所指向的元素是比基准点大的,此时交换基准点与 i (或 j )的位置,得到的结果是不正确的,即不满足比基准点大的元素在右边,比基准点小的元素在左边。