1、基本原理
1.1 快排基本思想:
Step 1: 对输入的数组进行shuffle,防止对输入产生依赖性,以保证随机性
Step 2:对数组进行切分,对于某个j,
- a[j]已经排定;
- a[lo]到a[j-1]中的所有元素都 不大于 a[j];
- a[j+1]到a[hi]中的所有元素都 不小于 a[j]。
Step 3:然后再分别对左子数组和右子数组分别递归排序
1.2 切分算法伪代码:
private static int partition(Comparable[] a, int lo, int hi) { // 将数组切分成a[lo .. i-1]、a[i]、a[i+1 .. hi]三个部分 int i = lo,j = hi + 1; // 左右扫描指针 Comparable v = a[lo]; // 切分元素 while(true){ // 扫描左右,检查扫描是否结束并交换元素 while(less(a[++i], v)) if(i == hi) break; // 还需要检查指针i是否越界,less是a[++i] < v,即遇到 大于等于 切分元素值的元素时停下 while(less(v, a[--j])) if(j == lo) break; // 同样需要检查指针j是否越界 if(i >= j) break; // 循环终止条件 exch(a, i, j); // 交换下标分别i、j的元素位置 } exch(a, lo, j); // 将v = a[j]放入正确的位置,注意是与j交换而不是i return j; //a[lo .. j-1] <= a[j] <= a[j+1 .. hi] 达成 }
1.3 快排伪代码:
public static void sort(Comparable[] a) { shuffle(a); sort(a, 0, a.length - 1); } public static void sort(Comparable[] a, int lo, int hi){ if(lo >= hi) return ; int j = partition(a, lo, hi); //切分 sort(a, lo, j - 1); // 对左半部分a[lo .. j-1]排序 sort(a, j + 1, hi); //对右半部分a[j+1 .. hi]排序 }
2、算法分析
算法的注意事项:
- 原地切分。若使用辅助数组,可以很容易实现切分。但是切分后的数组复制回去 和 数组额外空间开销 都会使我们得不偿失,因此直接对原数组进行切分是更好的选择。
- 别越界。如果切分元素v是最小或最大的那个元素,则会越界,详看切分的伪代码。
- 保证随机性。两种策略
a.对初始数组进行shuffle;
b.选择切分元素时,从数组中随机选取一个。 - 终止循环。快排的切分内的循环需要注意,正确地检查数组越界 和 考虑数组中可能存在元素值相同的情况。
- 处理切分元素值有重复的情况。左侧扫描 最好是遇到 大于等于 切分元素值的元素时停下,右侧扫描 最好是遇到 小于等于 切分元素值的元素时停下,虽然会造成一些没必要的等值元素交换,但是可以避免一些典型情况下运行时间变成平方级别。
典型情况:假设遇到和切分元素相同值的元素时继续扫描而不是停下来,那么可证明:处理只有若干种元素值的数组时的运行时间是平方级别的。 - 终止递归。快排的递归终止条件。
算法复杂度分析:
- 时间复杂度
最好、平均:O(nlogn)
最坏:O(n^2),与划分算法有关
- 空间复杂度
由于使用了递归,空间复杂度为O(logn)
3、Java实现
/** * 是一个整型数组的快速排序的简单实现,未优化的地方有: * 1、未对输入数组shuffle,且选取数组第一个作为主元,因此没有消除输入的依赖性 * 2、左侧扫描 是遇到 大于等于 切分元素值的元素停下,这样避免“处理只有若干种元素值的数组时的运行时间是平方级别的”,但是会 * 造成不必要的相同元素值进行交换,如:所有元素值都是5 */ public class QuickSort{ /** * 此函数用于交换数组任意两个元素的位置 * @param arr * @param i * @param j */ private static void swap(int[] arr, int i, int j) { //健壮性判断 if(arr == null || arr.length <= 0) { System.out.println("数组为空"); return; } //交换下标分别为i和j的元素值 int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } /** * 此函数是快排中的划分算法,实现了一趟快速排序,以第一个元素为主元, * 本函数运行结束后使得主元左侧元素均小于主元,主元右侧元素大于主元。 * @param arr 待排序的数组 * @param start * @param end * @return 返回一趟排序后主元的下标 */ private static int partition(int[] arr, int start, int end) { int i = start,j = end + 1; //选择第一个元素为主元 int key = arr[start]; while(true) { // 扫描左右,检查扫描是否结束并交换元素 while(arr[++i] < key) if(i == end) break; // 还需要检查指针i是否越界,且遇到 大于等于 切分元素值的元素时停下 while(key < arr[--j]) if(j == start) break; // 同样需要检查指针j是否越界 if(i >= j) break; // 循环终止条件 swap(arr, i, j); // 交换下标分别i、j的元素位置 } swap(arr, start, j); // 将v = a[j]放入正确的位置,注意是与j交换而不是i System.out.println("某一趟排序结果:"+printArray(arr)); return j; // a[lo .. j-1] <= a[j] <= a[j+1 .. hi] 达成 } /** * 快速排序的递归函数 * @param arr 待排序的数组 * @param start 数组起始下标 * @param end 数组结束下标 */ private static void QuickSort(int[] arr, int start, int end) { if(start >= end) return; int j = partition(arr, start, end); // 切分 QuickSort(arr, start, j - 1); // 对左半部分a[start .. j-1]排序 QuickSort(arr, j + 1, end); // 对右半部分a[j+1 .. end]排序 } /** * 此函数为快排的入口函数 * @param arr */ public static void QuickSort(int[] arr) { // 健壮性判断 if(arr == null || arr.length <= 0) { System.out.println("数组为空"); return; } // 通过递归进行快排 QuickSort(arr, 0, arr.length - 1); } public static String printArray(int[] arr) { // 健壮性判断 if(arr == null) { System.out.println("数组为空"); return null; } StringBuffer sb = new StringBuffer(); for(int i = 0; i < arr.length; i++){ sb.append(arr[i] + " "); } return sb.toString(); } public static void main(String[] args) { //测试案例 int[] arr = {2,12,34,34,56,623,21}; System.out.println("before sort:" + printArray(arr)); QuickSort(arr); System.out.println("after sort:" + printArray(arr)); } }