5. 快速排序
5.1 概念
-
什么是快速排序(Quick Sort)、
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
-
实现步骤
-
每一轮排序选择一个基准点(point)进行分区,让小于基准点的元素进入一个分区,大于基准点的元素进入另一个分区;当分区完成时,基准点元素的位置就是其在整个数组排序好之后的位置。(即这一轮结果确定了基准点元素的最终位置)
-
在子分区中重复步骤1的过程,直至分区元素个数小于等于1,这体现了**分而治之(divide-and-conquer,简称D and C)**的思想。
-
5.2 单边循环快排
-
单边循环快排实现方式:
- 最右边的元素(数组末尾元素)作为point基准点元素
- j 指针负责找到比基准点小的元素,一旦找到则与i进行交换
- i 指针有两个作用:
- 维护小于基准点元素的边界
- 每次交换的目标索引
- 最后基准点与 i 交换,i 即为分区位置
-
代码实现
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将一直递归调用,直到每一个分区完毕。
-
5.3 双边循坏快排
-
实现方式:
- 选择最左边的元素(数组首元素)作为point基准点元素
- j 指针负责从右往左找比基准点小的元素,找到后等待 i 指针;
- 在步骤1的条件下,一定要让 j 指针先执行并等待 i 指针的执行完毕。
- i 指针负责从左往右找比基准点大的元素。
- 当两个指针都找到各自要找的元素后,二者交换
- 重复步骤4直到指针 i 与指针 j 相交。
- 最后基准点与 i(或 j,因为此时 i = j )交换,i 即为分区位置
-
代码实现
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 )的位置,得到的结果是不正确的,即不满足比基准点大的元素在右边,比基准点小的元素在左边。
-
5.4 快速排序的特点
- 平均复杂度是O(nlogn),最坏时间复杂度是O(n²)
- 数据量较大时,快排的优势比较明细
- 快排是不稳定排序