01、经典排序算法
学习资源:慕课网liuyubobobo老师的《算法与数据结构精解》
0、排序算法说明
0.1、排序的定义
对一序列对象根据某个关键字进行排序。排序对象可以是基本数据类型,也可以是复杂数据类型,但是类必须具有可比较性。
0.2、 术语说明
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
- n: 数据规模
- k: “桶”的个数
- In-place: 占用常数内存,不占用额外内存
- Out-place: 占用额外内存
0.3、排序算法分类
1、冒泡排序 O(n2)
1.1、简介
冒泡排序(Bubble Sort)又称为泡式排序,是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
1.2、代码
public class MergeSort{
/**
* Description:冒泡排序
*
* @param array 需要排序的数组
* @author JourWon
* @date 2019/7/11 9:54
*/
public static void bubbleSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
int length = array.length;
// 外层循环控制比较轮数i
for (int i = 0; i < length; i++) {
// 内层循环控制每一轮比较次数,每进行一轮排序都会找出一个较大值
// (array.length - 1)防止索引越界,(array.length - 1 - i)减少比较次数
for (int j = 0; j < length - 1 - i; j++) {
// 前面的数大于后面的数就进行交换
if (array[j] > array[j + 1]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
}
}
1.3、复杂度分析
最佳情况:T(n) = O(n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
2、选择排序 O(n2)
2.1、简介
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序毕。
2.2、代码
public class SelectionSort {
public static void sort(Comparable[] arr){
int n = arr.length;
for(int i = 0; i < arr.length; i++){
int minIndex = i;
for(int j = i+1; j < n; j++)
if(less(arr[j], arr[minIndex]))
minIndex = j;
swap(arr, i, minIndex);
}
}
private static boolean less(Comparable a, Comparable b){
return a.compareTo(b) < 0;
}
private static void swap(Comparable[] arr, int a, int b){
Comparable temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
2.3、测试
public static void main(String[] args) {
// 测试Integer
int n = 10000;
Integer[] arr = SortTestHelper.generateRandomArray(n, 0, 10000);
SelectionSort.sort(arr);
SortTestHelper.printArray(arr);
System.out.println();
SortTestHelper.testSort("Ⅰ_sorting_basic.Ⅰ_selection_sort.SelectionSort", arr);
// 测试自定义类型,类需要实现Comparable接口
}
2.4、复杂度分析
选择排序的交换操作介于 0 和 (n - 1) 次之间。选择排序的比较操作为 n(n - 1) / 2 次。选择排序的赋值操作介于 0 与 3(n - 1) 次之间。所以总体上来说,时间复杂度为 O(n2) 。
比较次数 O(n2),比较次数与关键字的初始状态无关,总的比较次数 N = (n -1) + (n - 2) + ... + 1。
交换次数 O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换 n - 1 次。交换次数比冒泡排序较少,由于交换所需CPU时间比比较所需的CPU时间多,n 值较小时,选择排序比冒泡排序快。
原地操作几乎是选择排序的唯一优点,当空间复杂度要求较高时,可以考虑选择排序;实际适用的场合非常罕见。
复杂度 | |
---|---|
平均时间复杂度 | О(n²) |
最坏时间复杂度 | О(n²) |
最优时间复杂度 | О(n²) |
空间复杂度 | 总共О(n),需要辅助空间O(1) |
最佳解 | 偶尔出现 |
3、插入排序 O(n2)
3.1、简介
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
一般来说,插入排序都采用 in-place 在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素(新元素),在已经排序的元素序列中从后向前扫描
- 如果扫描到的元素(已排序)大于新元素,将扫描到的元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
(由Swfung8 - 自己的作品,CC BY-SA 3.0,链接)
3.2、代码
public class InsertionSort{
// 算法类不允许产生任何实例
private InsertionSort(){}
public static void sort(Comparable[] arr){
int n = arr.length;
for (int i = 1; i < n; i++) {
// 寻找元素arr[i]合适的插入位置
// 写法1
// for( int j = i ; j > 0 ; j -- )
// if( arr[j].compareTo( arr[j-1] ) < 0 )
// swap( arr, j , j-1 );
// else
// break;
// 写法2
for( int j = i; j > 0 && more(arr[j-1], arr[j]); j--)
swap(arr, j, j-1);
}
}
private static boolean more(Comparable a, Comparable b){
return a.compareTo(b) > 0;
}
private static void swap(Comparable[] arr, int i, int j) {
Comparable t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
3.3、算法优化
由于插入排序会有更多的交换操作,所以会导致其性能比选择排序还要差。
优化思路(减少交换操作):
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素(新元素),将其提取到一个临时变量中;定义一个"指针",初始指向新元素,
- 在已经排序的元素序列中从后向前扫描,如果"指针"前的元素大于"指针"元素,将"指针"元素的值更改为前一个元素的值,并且"指针"前移
- 重复步骤3,直到不满足步骤3,即说明当前"指针"处为新元素的插入位置,将"指针"元素的值更改为新元素的值
public class InsertionSort{
// 算法类不允许产生任何实例
private InsertionSort(){}
public static void sort(Comparable[] arr){
int n = arr.length;
for (int i = 1; i < n; i++) {
// 寻找元素arr[i]合适的插入位置
Comparable e = arr[i];
// j 即是"指针"
int j = i;
for( ; j > 0 && more(arr[j-1], e); j--)
arr[j] = arr[j-1];
arr[j] = e;
}
}
private static boolean more(Comparable a, Comparable b){
return a.compareTo(b) > 0;
}
}
3.4、测试
// 测试InsertionSort
public static void main(String[] args) {
int N = 10000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
SortTestHelper.printArray(arr);
InsertionSort.sort(arr);
SortTestHelper.printArray(arr);
SortTestHelper.testSort("Ⅰ_sorting_basic.Ⅱ_insertion_sort.InsertionSort", arr);
}
3.5、复杂度分析
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需 n-1 次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有 1/2 n(n - 1) 次。插入排序的赋值操作是比较操作的次数减去 n-1 次,(因为 n-1 次循环中,每一次循环的比较都比赋值多一个,多在最后那一次比较并不带来赋值)。平均来说插入排序算法复杂度为 O(n2) 。
因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千;或者若已知输入元素大致上按照顺序排列,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
复杂度 | |
---|---|
平均时间复杂度 | О(n²) |
最坏时间复杂度 | О(n²) |
最优时间复杂度 | О(n²) |
空间复杂度 | 总共О(n),需要辅助空间O(1) |
最佳解 | No |
4、归并排序 O(nlogn)
4.1、简介
归并排序(英语:Merge sort,或mergesort),是创建在归并操作上的一种有效的排序算法,效率为 O(n logn)。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
由Swfung8 - 自己的作品,CC BY-SA 3.0,链接
归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。
4.2、递归法(Top-down)
采用分治法:
- 分割:递归地把当前序列平均分割成两半。
- 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)。
- 申请空间,使其大小为两个已经排序序列之和(最后一层的2个序列皆只有一个元素,即是排好序的),该空间保存的是两个已排序序列的值,用于合并后序列值的更新
- 设定3个指针 i 、j 、k ,其中 i ,j 最初位置分别为两个已经排序序列的起始位置,k 指向合并序列中元素待放入的位置
- 比较 i 、j 所指向的元素,选择相对小的元素放入到合并空间,并移动 i 或 j 到下一位置;同时 k 也要移动到下一位置
- 重复步骤3直到某一指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
4.3、代码
public class MergeSort{
// 算法类不允许产生任何实例
private MergeSort(){}
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
private static void merge(Comparable[] arr, int l, int mid, int r) {
// 临时空间
Comparable[] aux = Arrays.copyOfRange(arr, l, r+1);
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid+1;
for( int k = l ; k <= r; k ++ ){
if( i > mid ){ // 如果左半部分元素已经全部处理完毕
arr[k] = aux[j-l]; j ++;
}
else if( j > r ){ // 如果右半部分元素已经全部处理完毕
arr[k] = aux[i-l]; i ++;
}
else if( aux[i-l].compareTo(aux[j-l]) < 0 ){ // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i-l]; i ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j-l]; j ++;
}
}
}
// 递归使用归并排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r) {
// 递归终止条件
if (l >= r)
return;
// int mid = l + (r-l) / 2;
int mid = (l+r)/ 2;
sort(arr, l, mid);
sort(arr, mid + 1, r);
// 算法优化:如果两个序列已经有序,则不需要进行归并
if(arr[mid].compareTo(arr[mid+1]) > 0)
merge(arr, l, mid, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
}
4.4、迭代法(Bottom-up)
原理如下(假设序列共有 n 个元素):
- 将序列每相邻两个数字进行归并操作,形成 ceil(n/2) 个序列,排序后每个序列包含 两/一 个元素
- 若此时序列数不是1个则将上述序列再次归并,形成 ceil(n/4) 个序列,每个序列包含 四/三 个元素
- 重复步骤2,直到所有元素排序完毕,即序列数为1
4.5、代码
public class MergeSortBU{
// 我们的算法类不允许产生任何实例
private MergeSortBU(){}
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
private static void merge(Comparable[] arr, int l, int mid, int r) {
Comparable[] aux = Arrays.copyOfRange(arr, l, r+1);
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid+1;
for( int k = l ; k <= r; k ++ ){
if( i > mid ){ // 如果左半部分元素已经全部处理完毕
arr[k] = aux[j-l]; j ++;
}
else if( j > r ){ // 如果右半部分元素已经全部处理完毕
arr[k] = aux[i-l]; i ++;
}
else if( aux[i-l].compareTo(aux[j-l]) < 0 ){ // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i-l]; i ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j-l]; j ++;
}
}
}
public static void sort(Comparable[] arr){
int n = arr.length;
// Merge Sort Bottom Up 无优化版本
for (int sz = 1; sz < n; sz *= 2)
for (int i = 0; i < n - sz; i += sz+sz)
// 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并
merge(arr, i, i+sz-1, Math.min(i+sz+sz-1,n-1));
// Merge Sort Bottom Up 优化
// 对于规模较小的数组, 使用插入排序优化
// for( int i = 0 ; i < n ; i += 16 )
// InsertionSort.sort(arr, i, Math.min(i+15, n-1) );
//
// for( int sz = 16; sz < n ; sz += sz )
// for( int i = 0 ; i < n - sz ; i += sz+sz )
// // 对于arr[mid] <= arr[mid+1]的情况,不进行merge
// if( arr[i+sz-1].compareTo(arr[i+sz]) > 0 )
// merge(arr, i, i+sz-1, Math.min(i+sz+sz-1,n-1) );
}
}
4.6、复杂度分析
比较操作的次数介于 (n log n)/2 和 n log n - n + 1 。 赋值操作的次数是 2n log n。归并算法的空间复杂度为:O(n)
复杂度 | |
---|---|
平均时间复杂度 | O(n logn) |
最坏时间复杂度 | O(n logn) |
最优时间复杂度 | O(n logn) |
空间复杂度 | O(n) |
最佳解 | 有时是 |
5、快速排序 O(nlogn)
5.1、简介
快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序 n 个项目要 O(nlog n) 次比较。在最坏状况下则需要 O(n2) 次比较,但这种状况并不常见。事实上,快速排序O(nlog n)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。
快速排序使用分治法策略来把一个序列分为较小和较大的2个子序列,然后递归地排序两个子序列。
步骤为:
- 挑选基准值:从数列中挑出一个元素(可以选取第一个),称为“基准”(pivot);,
- 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
-
递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
-
递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
选取基准值有数种具体方法,此选取方法对排序的时间性能有决定性影响。
5.2、代码
public class QuickSort {
// 我们的算法类不允许产生任何实例
private QuickSort(){}
// 对arr[l...r]部分进行partition操作
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
private static int partition(Comparable[] arr, int l, int r){
// 选择当前序列的第一个元素作为基准值
Comparable v = arr[l];
// arr[l+1...j] < v ; arr[j+1...i) > v
// j初始等于l,则arr[l+1...j]不存在
int j = l;
// i初始等于 l+1,arr[j+1...i]也不存在
// 从当前序列的第二个元素遍历
for( int i = l + 1 ; i <= r ; i ++ ) {
// 当前元素小于基准值,交换当前元素与arr[j+1]。
// 这样arr[l+1...j]区间开始扩张,遍历到最后剩余的arr[j+1...i]即为大于基准值的区间
if (arr[i].compareTo(v) < 0) {
j++;
swap(arr, j, i);
}
}
// 最后交换 v 与 arr[l+1...j]区间的最后一个元素,使得基准值移动到正确的位置
swap(arr, l, j);
return j;
}
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r){
// 递归终止条件:序列长度为1或0
if( l >= r )
return;
// 在arr[l...r]中挑取基准值,并根据基准值进行分割
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);
}
}
5.3、算法优化
5.3.1、基准值选取随机化
如果快速排序处理的序列为近乎有序的,选取当前序列固定位置的元素作为基准值,会导致一次分割操作生成的两个序列极度不均衡的:
最差的情况,会退化为 O(n2):
public class QuickSort2Ways {
// 我们的算法类不允许产生任何实例
private QuickSort2Ways(){}
// 双路快速排序的partition
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
private static int partition(Comparable[] arr, int l, int r){
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= v
int i = l+1, j = r;
while( true ){
// 注意这里的边界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0
// 思考一下为什么?
while( i <= r && arr[i].compareTo(v) < 0 )
i ++;
// 注意这里的边界, arr[j].compareTo(v) > 0, 不能是arr[j].compareTo(v) >= 0
// 思考一下为什么?
while( j >= l+1 && arr[j].compareTo(v) > 0 )
j --;
// 对于上面的两个边界的设定, 有的同学在课程的问答区有很好的回答:)
// 大家可以参考: http://coding.imooc.com/learn/questiondetail/4920.html
// 遍历完成,退出遍历
if( i > j )
break;
// 交换左边 >=v 和右边 <=v 的元素
swap( arr, i, j );
// 交换完成,继续遍历
i ++;
j --;
}
// 最后让基准值处于正确的位置
swap(arr, l, j);
return j;
}
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r){
// 递归终止条件:序列长度为1或0
if( 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);
}
private static void swap(Comparable[] arr, int i, int j) {
Comparable t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
5.3.2、双路快排
如果当前序列含有大量重复的元素,即使基准值的选取实现了随机化,但是在进行分割后,也有可能形成最差的情况,退化为 O(n2)。
优化思路:
- 挑选基准值 v 之后,设定两个指针 i , j , i 指向当前遍历到的元素(初始指向当前序列的第二个元素), j 指向最后一个元素
- i 不断向后移动,遇到小于 v 的元素,继续向后移动,遇到大于等于 v 的元素,则停止; j 不断向前移动,遇到大于 v 的元素,继续向前移动,遇到小于等于 v 的元素,则停止;此时交换 i 和 j 所指向的元素
- 重复步骤2,直到两个指针遍历完序列中的所有元素,即 i 大于 j
注意:此时分割出来两个序列中均可能含有等于 v 的元素,并不是以 v 为分界点的两个绝对有序的队列 - 最后交换基准值 v 和 arr[ j ] ,完成这轮的快速排序
这样进行递归分割,由于基准值 v 是随机选取的,又因为等于 v 的其他元素被分散到两个序列,不会造成等于 v 的元素集中分布在一侧,从一定程度上保证了两个序列是均衡的。
public class QuickSort2Ways {
// 我们的算法类不允许产生任何实例
private QuickSort2Ways(){}
// 双路快速排序的partition
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
private static int partition(Comparable[] arr, int l, int r){
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= 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;
// 交换左边 >=v 和右边 <=v 的元素
swap( arr, i, j );
// 交换完成,继续遍历
i ++;
j --;
}
// 最后让基准值处于正确的位置
swap(arr, l, j);
return j;
}
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r){
// 递归终止条件:序列长度为1或0
if( 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);
}
private static void swap(Comparable[] arr, int i, int j) {
Comparable t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
5.3.3、三路快排
双路快排是为了解决序列中含有大量重复的元素,优化思路是将和基准值 v 重复的元素分散到基准值 v 左右两边的序列,避免集中。
而我们可以想到一个更高效的方式:将当前区间分割为三个区间:< v,= v,> v
优化思路:
- 挑选基准值 v 之后,设定三个指针 i, lt , gt , i 指向当前遍历到的元素, lt 指向小于 v的最后一个元素, gt 指向大于 v 的第一个元素(从左看)
- i 不断向后移动,遇到等于 v 的元素,继续向后移动;遇到小于 v 的元素,交换 arr[lt] 和 arr[i] , lt 向后移动一个位置;遇到大于 v 的元素,交换 arr[gt] 和 arr[i] ,gt 向前移动一个位置
- 当 i = gt 时,遍历完成
- 最后交换 arr[l] 和 arr[lt] ,使得基准值处于正确的位置
public class QuickSort3Ways {
// 我们的算法类不允许产生任何实例
private QuickSort3Ways(){}
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r){
// 递归终止条件:序列长度为1或0
if(l > r)
return;
// partition
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr, l, (int)(Math.random()*(r-l+1)) + l );
Comparable v = arr[l];
// 定义三个指针
int lt = l; // arr[l+1...lt] < v
int gt = r + 1; // arr[gt...r] > v
int i = l+1; // arr[lt+1...i) == v
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 ++;
}
}
swap( arr, l, lt );
// 最后基准值 v 归位后, 三个区间更新为 arr[l+1...lt-1] < v, arr[gt...r] > v, arr[lt...gt-1] == v
// 递归
sort(arr, l, lt-1);
sort(arr, gt, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Comparable[] arr, int i, int j) {
Comparable t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
5.4、测试
测试比对归并排序、双路快排、三路快排的性能
public class Main {
public static void main(String[] args) {
int N = 1000000;
// 测试1 一般性测试
System.out.println("Test for random array, size = " + N + " , random range [0, " + N + "]");
Integer[] arr1 = SortTestHelper.generateRandomArray(N, 0, N);
Integer[] arr2 = Arrays.copyOf(arr1, arr1.length);
Integer[] arr3 = Arrays.copyOf(arr1, arr1.length);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅰ_merge_sort.MergeSort", arr1);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅳ_quick_sort_2ways.QuickSort2Ways", arr2);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅴ_quick_sort_3ways.QuickSort3Ways", arr3);
System.out.println();
// 测试2 测试近乎有序的数组
int swapTimes = 100;
assert swapTimes >= 0;
System.out.println("Test for nearly ordered array, size = " + N + " , swap time = " + swapTimes);
arr1 = SortTestHelper.generateNearlyOrderedArray(N, swapTimes);
arr2 = Arrays.copyOf(arr1, arr1.length);
arr3 = Arrays.copyOf(arr1, arr1.length);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅰ_merge_sort.MergeSort", arr1);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅳ_quick_sort_2ways.QuickSort2Ways", arr2);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅴ_quick_sort_3ways.QuickSort3Ways", arr3);
System.out.println();
// 测试3 测试存在包含大量相同元素的数组
System.out.println("Test for random array, size = " + N + " , random range [0,10]");
arr1 = SortTestHelper.generateRandomArray(N, 0, 10);
arr2 = Arrays.copyOf(arr1, arr1.length);
arr3 = Arrays.copyOf(arr1, arr1.length);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅰ_merge_sort.MergeSort", arr1);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅳ_quick_sort_2ways.QuickSort2Ways", arr2);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅴ_quick_sort_3ways.QuickSort3Ways", arr3);
}
}
总结如下:
- 对于包含有大量重复数据的数组, 三路快排有巨大的优势
- 对于一般性的随机数组和近乎有序的数组, 三路快排的效率虽然不是最优的, 但是是在非常可以接受的范围里。因此, 在一些语言中, 三路快排是默认的语言库函数中使用的排序算法,比如Java
6、堆排序 O(nlogn)
6.1、简介
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值或索引总是不小于(或者不大于)它的父节点。
6.2、最大堆
此处贴出liyubobobo老师这版的最大堆代码
// 在堆的有关操作中,需要比较堆中元素的大小,所以Item需要extends Comparable
public class MaxHeap<Item extends Comparable> {
protected Item[] data;
protected int count;
protected int capacity;
// 构造函数, 构造一个空堆, 可容纳capacity个元素
public MaxHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
count = 0;
this.capacity = capacity;
}
// 返回堆中的元素个数
public int size(){
return count;
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return count == 0;
}
// 像最大堆中插入一个新的元素 item
public void insert(Item item){
assert count + 1 <= capacity;
data[count+1] = item;
count ++;
siftUp(count);
}
// 从最大堆中取出堆顶元素, 即堆中所存储的最大数据
public Item extractMax(){
assert count > 0;
Item ret = data[1];
swap( 1 , count );
count --;
siftDown(1);
return ret;
}
// 获取最大堆中的堆顶元素
public Item getMax(){
assert( count > 0 );
return data[1];
}
// 交换堆中索引为i和j的两个元素
private void swap(int i, int j){
Item t = data[i];
data[i] = data[j];
data[j] = t;
}
//********************
//* 最大堆核心辅助函数
//********************
private void siftUp(int k){
while( k > 1 && data[k/2].compareTo(data[k]) < 0 ){
swap(k, k/2);
k /= 2;
}
}
private void siftDown(int k){
while( 2*k <= count ){
int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置
if( j+1 <= count && data[j+1].compareTo(data[j]) > 0 )
j ++;
// data[j] 是 data[2*k]和data[2*k+1]中的最大值
if( data[k].compareTo(data[j]) >= 0 ) break;
swap(k, j);
k = j;
}
}
// 测试 MaxHeap
public static void main(String[] args) {
MaxHeap<Integer> maxHeap = new MaxHeap<Integer>(100);
int N = 100; // 堆中元素个数
int M = 100; // 堆中元素取值范围[0, M)
for( int i = 0 ; i < N ; i ++ )
maxHeap.insert( new Integer((int)(Math.random() * M)) );
Integer[] arr = new Integer[N];
// 将maxheap中的数据逐渐使用extractMax取出来
// 取出来的顺序应该是按照从大到小的顺序取出来的
for( int i = 0 ; i < N ; i ++ ){
arr[i] = maxHeap.extractMax();
System.out.print(arr[i] + " ");
}
System.out.println();
// 确保arr数组是从大到小排列的
for( int i = 1 ; i < N ; i ++ )
assert arr[i-1] >= arr[i];
}
}
也可以使用我之前《数据结构》博文中实现的最大堆,二者无本质区别。
6.3、代码
实现思路:
- 将所有的元素依次添加到堆中
- 再将所有元素从堆中依次取出来,即完成了排序(升序还是降序,由取出时遍历的方向决定)
无论是创建堆的过程,还是从堆中依次取出元素的过程,时间复杂度均为O(nlogn),整个堆排序的整体时间复杂度为O(nlogn)
public class HeapSort1 {
//算法类不允许产生任何实例
private HeapSort1(){}
public static void sort(Comparable[] arr){
int n = arr.length;
MaxHeap<Comparable> maxHeap = new MaxHeap<Comparable>(n);
for( int i = 0 ; i < n ; i ++ )
maxHeap.insert(arr[i]);
for( int i = n-1 ; i >= 0 ; i -- )
arr[i] = maxHeap.extractMax();
}
}
上一版的堆排序,是将数组中的元素逐个的添加进最大堆中,即创建待排序数组对应的最大堆,复杂度为 O(nlogn) ,这一过程可以优化
6.4、heapify
定义:将任意数组整理成堆的形状
实现思路:
-
通过循环将数组中的元素逐个添加进堆对象中。复杂度为O(nlogn)
-
从最后一个非叶子结点(最后一个结点的父结点),从下之上、从右至左(对应到数组中也就是从后向前),逐个地对每一个元素元素进行Sift Down。复杂度为O(n)
-
直到对根结点完成 Sift Down
定义:将任意数组整理成堆的形状
实现思路(这里选择思路2):
-
通过循环将数组中的元素逐个添加进堆对象中。复杂度为O(nlogn)
-
从最后一个非叶子结点(最后一个结点的父结点),从下之上、从右至左(对应到数组中也就是从后向前),逐个地对每一个元素元素进行Sift Down。复杂度为O(n)
-
直到对根结点完成 Sift Down
// 新的构造函数, 通过一个给定数组创建一个最大堆
// 该构造堆的过程, 时间复杂度为O(n)
public MaxHeap(Item arr[]){
int n = arr.length;
data = (Item[])new Comparable[n+1];
capacity = n;
for( int i = 0 ; i < n ; i ++ )
data[i+1] = arr[i];
count = n;
for( int i = count/2 ; i >= 1 ; i -- )
siftDown(i);
}
6.5、算法优化
6.5.1、使用heapify创建堆
HeapSort2,借助我们的 heapify
过程创建堆。
此时, 创建堆的过程时间复杂度为O(n),将所有元素依次从堆中取出来, 实践复杂度为 O(nlogn) ,堆排序的总体时间复杂度依然是 O(nlogn) , 但是比HeapSort1性能更优, ,因为创建堆的性能更优。
public class HeapSort2 {
// 我们的算法类不允许产生任何实例
private HeapSort2(){}
public static void sort(Comparable[] arr){
int n = arr.length;
MaxHeap<Comparable> maxHeap = new MaxHeap<Comparable>(arr);
for( int i = n-1 ; i >= 0 ; i -- )
arr[i] = maxHeap.extractMax();
}
}
6.5.2、原地堆排序
之前实现的堆排序,还需要使用 O(n) 的辅助空间来创建堆。
- 现在的创建最大堆的操作只是将待排序的数组拷贝到堆类的内部进行
heapify
,但我们可以直接对待排序的数组进行heapify
操作使其成为一个形式意义上的最大堆,(满足最大堆的性质而没有堆的API)
- 交换当前最大堆的首个元素 v 和当前最大堆中的最后一个元素 w
因为是最大堆,所以首个元素 v 即是最大值,交换之后 v 就排好序了 - 经过步骤2后,除去 v 后的剩余序列已经不再满足最大堆的性质了, 再对交换后排在首位的 w 进行 Sift Down 操作,使除去 v 后的剩余序列继续是一个最大堆
- 重复步骤2,直到当前最大堆只剩一个元素,排序完成
// 不使用一个额外的最大堆, 直接在原数组上进行原地的堆排序
public class HeapSort3 {
// 我们的算法类不允许产生任何实例
private HeapSort3(){}
public static void sort(Comparable[] arr){
int n = arr.length;
// 将 arr 数组构建成为一个最大堆(从形式上看是这样的)
// 注意,此时我们的堆是从0开始索引的
// 从(最后一个元素的索引-1)/2开始
// 最后一个元素的索引 = n-1
for( int i = (n-1-1)/2 ; i >= 0 ; i -- )
siftDown2(arr, n, i);
for( int i = n-1; i > 0 ; i-- ){
swap( arr, 0, i);
siftDown2(arr, i, 0);
}
}
// 交换堆中索引为i和j的两个元素
private static void swap(Object[] arr, int i, int j){
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 原始的siftDown过程
private static void siftDown(Comparable[] arr, int n, int k){
while( 2*k+1 < n ){
int j = 2*k+1;
if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 )
j += 1;
if( arr[k].compareTo(arr[j]) >= 0 )break;
swap( arr, k, j);
k = j;
}
}
// 优化的siftDown过程, 使用赋值的方式取代不断的swap,
// 该优化思想和我们之前对插入排序进行优化的思路是一致的
private static void siftDown2(Comparable[] arr, int n, int k){
Comparable e = arr[k];
while( 2*k+1 < n ){
int j = 2*k+1;
if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 )
j += 1;
if( e.compareTo(arr[j]) >= 0 )
break;
arr[k] = arr[j];
k = j;
}
arr[k] = e;
}
}
7、排序算法总结
8、工具类SortTestHelper
(我迷了,反射都快忘完了)
package Ⅰ_sorting_basic.util;
import java.lang.reflect.Method;
import java.lang.Class;
import java.util.Random;
public class SortTestHelper {
// SortTestHelper不允许产生任何实例
private SortTestHelper(){}
// 生成有n个元素的随机数组,每个元素的随机范围为[rangeL, rangeR]
public static Integer[] generateRandomArray(int n, int rangeL, int rangeR) {
assert rangeL <= rangeR;
Integer[] arr = new Integer[n];
for (int i = 0; i < n; i++)
arr[i] = new Integer((int)(Math.random() * (rangeR - rangeL + 1) + rangeL));
return arr;
}
// 生成一个近乎有序的数组
// 首先生成一个含有[0...n-1]的完全有序数组, 之后随机交换swapTimes对数据
// swapTimes定义了数组的无序程度:
// swapTimes == 0 时, 数组完全有序
// swapTimes 越大, 数组越趋向于无序
public static Integer[] generateNearlyOrderedArray(int n, int swapTimes){
Integer[] arr = new Integer[n];
for( int i = 0 ; i < n ; i ++ )
arr[i] = new Integer(i);
for( int i = 0 ; i < swapTimes ; i ++ ){
int a = (int)(Math.random() * n);
int b = (int)(Math.random() * n);
int t = arr[a];
arr[a] = arr[b];
arr[b] = t;
}
return arr;
}
// 打印arr数组的所有内容
public static void printArray(Object[] arr) {
for (int i = 0; i < arr.length; i++){
System.out.print( arr[i] );
System.out.print( ' ' );
}
System.out.println();
return;
}
// 判断arr数组是否有序
public static boolean isSorted(Comparable[] arr){
for( int i = 0 ; i < arr.length - 1 ; i ++ )
if( arr[i].compareTo(arr[i+1]) > 0 )
return false;
return true;
}
// 测试sortClassName所对应的排序算法排序arr数组所得到结果的正确性和算法运行时间
public static void testSort(String sortClassName, Comparable[] arr){
// 通过Java的反射机制,通过排序的类名,运行排序函数
try{
// 通过sortClassName获得排序函数的Class对象
Class sortClass = Class.forName(sortClassName);
// 通过排序函数的Class对象获得排序方法
Method sortMethod = sortClass.getMethod("sort",new Class[]{Comparable[].class});
// 排序参数只有一个,是可比较数组arr
Object[] params = new Object[]{arr};
long startTime = System.currentTimeMillis();
// 调用排序函数
sortMethod.invoke(null,params);
long endTime = System.currentTimeMillis();
assert isSorted( arr );
System.out.println( sortClass.getSimpleName()+ " : " + (endTime-startTime) + "ms" );
}
catch(Exception e){
e.printStackTrace();
}
}
}