快速排序及其优化——随机化标定点、双路快排、三路快排

快速排序基本概念

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序 n个项目要 O(nlog n)(大O符号)次比较。在最坏状况下则需要 $O(n^{2})$次比较,但这种状况并不常见。事实上,快速排序 O(nlog n)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。

算法步骤

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
步骤为:

  1. 从数列中挑出一个元素,称为”基准”(pivot),
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

图示

复杂度分析

数据结构 不定
最坏时间复杂度 O (n^{2})
最优时间复杂度 O(nlog n)
平均时间复杂度 O (nlog n)
空间复杂度 根据实现的方式不同而不同

普通版本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package SortingAdvance.QuickSort;


/**
* @ Description: 快速排序普通版
* @ Date: Created in 18:44 2018/7/29
* @ Author: Anthony_Duan
*/
public class QuickSort {

private QuickSort(){}

/**
* 对arr[l..r]部分进行partition操作,
* 返回p 是得arr[l..p-1]<=[p] arr[p+1...r]>arr[p]
* @param arr
* @param l
* @param r
* @return
*/
private static int partition(Comparable[] arr, int l, int r) {

//这里每次取第一个来比较
Comparable v = arr[l];

int j = l;
/**
* arr[l+1...j] <= v ; arr[j+1...i) > v
* l是数组的左端点 r是数组的右端点
* j是小于等于v部分的右端点
* i是遍历比较的索引
*
* 1. 当arr[i]大于v的时候,i++就好了
* 2. 当arr[i]小于v的时候,将arr[i]与当前arr[j+1]交换
*/
for (int i = l + 1; i <= r; i++) {
if (arr[i].compareTo(v) < 0) {
j++;
swap(arr, j, i);
}
}
//最后 将这一轮比较的元素与j进行交换
swap(arr, l, j);
return j;
}


/**
* 递归快速排序 对arr[l...r]的范围进行排序
* 每一轮确定一个元素的最终位置
* @param arr
* @param l
* @param r
*/
private static void sort(Comparable[] arr, int l, int r) {
if (l >= r) {
return;
}
int p = partition(arr, l, r);
sort(arr, l, p - 1);
sort(arr, p + 1, r);
}

private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}

public static void sort(Comparable[] arr) {
int n = arr.length;
sort(arr, 0, n - 1);
}


// 测试 QuickSort
public static void main(String[] args) {

// Quick Sort也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
SortTestHelper.testSort("SortingAdvance.QuickSort.QuickSort", arr);

return;
}
}

针对近乎有序的序列的优化——标定点随机化

对于近乎有序的序列,如果不进行优化,快速排序每次选择第一个元素作为标定点就会产生大于与小于标定点其中的一个部分,几乎没有数据,产生失衡。退化为O(n^2)的复杂度,类似于二叉树的失衡不平衡问题
解决这个问题的方法很简单,就是随机选择标定点,统计意义上这样平均每次都是平衡的(但是这里没有对等于标定点做处理,这是下一个优化点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package SortingAdvance.QuickSort;

/**
* @ Description:快速排序优化
* 对于近乎有序的序列,如果不进行优化,快速排序每次选择第一个元素作为标定点
* 就会产生大于与小于标定点的部分几乎没有数据,产生失衡。退化为O(n^2)的复杂度
* 类似于二叉树的失衡不平衡问题
* 解决这个问题的方法很简单,就是随机选择标定点,统计意义上这样平均每次都是平衡的
* (但是这里没有对等于标定点做处理,这是下一个优化点)
* @ Date: Created in 20:10 2018/7/29
* @ Author: Anthony_Duan
*/
public class QuickSortDealWithNearlyOrdered {


private QuickSortDealWithNearlyOrdered(){}

private static int partition(Comparable[] arr, int l, int r) {


// 随机在arr[l...r]的范围中, 选择一个数值作为标定点
swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
Comparable v = arr[l];

int j = l;
for (int i = l + 1; i <= r; i++) {
if (arr[i].compareTo(v) < 0) {
j++;
swap(arr, j, i);
}
}
swap(arr, j, l);
return j;
}

private static void swap(Object[] arr,int i,int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}

private static void sort(Comparable[] arr, int l, int r) {

/**
* 对于小规模的数组,使用插入排序效率更高
* 这个可以作为排序算法的通用优化策略
*/
if (r - l <= 15) {
InsertionSortAdvance.sort(arr, l, r);
return ;
}
int p = partition(arr, l, r);
sort(arr, l, p - 1);
sort(arr, p + 1, r);
}

public static void sort(Comparable[] arr) {
int n = arr.length;
sort(arr, 0, n - 1);
}
public static void main(String[] args) {

// Quick Sort也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
SortTestHelper.testSort("SortingAdvance.QuickSort.QuickSortDealWithNearlyOrdered", arr);

return;
}

}

针对含有大量相等元素序列排序的优化——双路快排与三路快排

双路快排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package SortingAdvance.QuickSort;


/**
* @ Description: 双路快排 解决的是相同数据过多导致失衡的问题
* @ Date: Created in 20:53 2018/7/29
* @ Author: Anthony_Duan
*/
public class QuickSort2Ways {


private QuickSort2Ways(){}

private static int partition(Comparable[] arr, int l, int r) {

//随机选定标定点 针对的是近乎有序序列的优化
swap( arr, l,(int)(Math.random()*(r-l+1))+l);

Comparable v = arr[l];

/**
* 双路排序的具体实现
* arr[l+1...i) <= v; arr(j...r] >= v
* 双路排序是从两端向中间排序,
* 如果arr[i]小于标定点就让控制小于等于标定点部分边界的i++,此时arr[i]中的元素一定大于等于v
* 如果arr[j]小于标定点就让控制大于等于标定点部分边界的j--,此时arr[i]中的元素一定小于等于v
* 当前两个条件都不满足并且i<j的时候,交换arr[i],arr[j]因为此时他们保留的分别是符合对方区间的元素
* 交换后i,j分别加一
* 应当注意的是即使 arr[i],arr[j]此时都等于v 他们也会交换元素
*/
int i = l + 1, j = r;
while (true) {
while (i <= r && arr[i].compareTo(v) < 0) {
i++;
}
while (j >= l + 1 && arr[j].compareTo(v) > 0) {
j--;
}
if (i > j) {
break;
}
swap(arr, i, j);
i++;
j--;
}
swap(arr, l, j);
return j;
}


private static void sort(Comparable[] arr, int l, int r){

// 对于小规模数组, 使用插入排序
if( r - l <= 15 ){
InsertionSortAdvance.sort(arr, l, r);
return;
}

int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}

private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
public static void sort(Comparable[] arr){

int n = arr.length;
sort(arr, 0, n-1);
}

public static void main(String[] args) {

// 双路快速排序算法也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
SortTestHelper.testSort("SortingAdvance.QuickSort.QuickSort2Ways", arr);

return;
}
}

三路快排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package SortingAdvance.QuickSort;

/**
* @ Description: 三路快排 解决的是数据相同导致不平衡的问题
* @ Date: Created in 21:40 2018/7/29
* @ Author: Anthony_Duan
*/
public class QuickSort3Ways {

private QuickSort3Ways(){}


/**
* 递归使用快速排序,对arr[l...r]的范围进行排序
* @param arr
* @param l
* @param r
*/
private static void sort(Comparable[] arr, int l, int r) {
// 对于小规模数组, 使用插入排序
if (r - l <= 15) {
InsertionSortAdvance.sort(arr, l, r);
return;
}

// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);


/**
* arr[l+1...lt] < v
* arr[gt...r] > v
* arr[lt+1...i) == v
*/
Comparable v = arr[l];
int lt = l ;
int gt = r + 1;
int i = l + 1;

//递归终止条件
while (i < gt) {
if (arr[i].compareTo(v) < 0) {
swap(arr, i, lt + 1);
i++;
lt++;
}
else if (arr[i].compareTo(v) > 0) {
swap(arr,i,gt-1);
gt--;
}else{ // arr[i] == v
i ++;
}



}

/**
* 每一轮交换后
* arr[l...lt-1] < v
* arr[gt...r] > v
* arr[lt...i) == v
*/
swap(arr, l, lt );

sort(arr, l, lt-1);
sort(arr, gt, r);
}


private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
public static void sort(Comparable[] arr) {
int n = arr.length;
sort(arr, 0, n - 1);
}

public static void main(String[] args) {

// 三路快速排序算法也是一个O(nlogn)复杂度的算法
// 可以在1秒之内轻松处理100万数量级的数据
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
SortTestHelper.testSort("SortingAdvance.QuickSort.QuickSort3Ways", arr);

return;
}
}

-------------End Of This ArticleThank You For Reading-------------