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));
}
}