注意:2.1节copy于:https://www.cnblogs.com/chengxiao/p/6194356.html
1. O(n^2)的排序算法
1.1 冒泡排序(Bubble Sort)
假如我们要排序这个数组:【8,6,2,3,1】
第一次(0-4):【6,8,2,3,1】-->【6,2,8,3,1】--> 【6,2,3,8,1】-->【6,2,3,1,8】
第二次(0-3):【2,6,3,1,8】-->【2,3,6,1,8】-->【2,3,1,6,8】
第三次(0-2):【2,3,1,6,8】-->【2,1,3,6,8】
第四次(0-1):【1,2,3,6,8】
public class _01_BubbleSort {
// 我们的算法类不允许产生任何实例
private _01_BubbleSort(){}
public static void sort(Comparable[] arr){
for(int i=0 ; i<arr.length ; i++){
for(int j=0 ; j<arr.length-i-1; j++){
if(arr[j].compareTo(arr[j+1])>0){
swap(arr,j,j+1);
}
}
}
}
}
1.2 选择排序(Selection Sort)
假如我们要排序这个数组:【8,6,2,3,1,5,7,4】
我们首先找到序号(0-7)中最小值1,让1与序号为0的值交换:【1,6,2,3,8,5,7,4】
我们再找到序号(1-7)中最小值2,让2与序号为1的值交换:【1,2,6,3,8,5,7,4】
我们再找到序号(2-7)中最小值3,让3与序号为2的值交换:【1,2,3,6,8,5,7,4】
继续。。。。。。。
我们来看下代码
//选择排序
public class _02_SelectionSort {
// 我们的算法类不允许产生任何实例
private _02_SelectionSort(){}
public static void sort(Comparable[] arr){
int n = arr.length;
for( int i = 0 ; i < n ; i ++ ){
int minIndex = i; // 寻找[i, n)区间里的最小值的索引
for( int j = i + 1 ; j < n ; j ++ )
if( arr[j].compareTo( arr[minIndex] ) < 0 ) // 使用compareTo方法比较两个Comparable对象的大小
minIndex = j;
swap( arr , i , minIndex);
}
}
}
1.3 插入排序(Insertion Sort)
假如我们要排序这个数组:【8,6,2,3,1,5,7,4】
首先我们考虑序号为0的元素,因为序号0是第一个元素 ,所以不用动:【8,6,2,3,1,5,7,4】
我们考虑序号为1的元素,我们把元素6放在序号(0-1)中合适的位置:【6,8,2,3,1,5,7,4】
我们考虑序号为2的元素,我们把元素2放在序号(0-2)中合适的位置:【2,6,8,3,1,5,7,4】
我们考虑序号为3的元素,我们把元素3放在序号(0-3)中合适的位置:【2,3,6,8,1,5,7,4】
继续。。。。。。
//相比于选择排序,插入排序的一个优点是提前终止,不用遍历整个数组,因此,插入排序应该要比选择排序的效率更加的高效,
//数组与接近有序,插入排序越快,当数组完全有序时,插入排序为O(n)级别的排序
public class _03_InsertionSort {
// 我们的算法类不允许产生任何实例
private _03_InsertionSort(){}
public static void sort(Comparable[] arr){
int n = arr.length;
for (int i = 0; i < n; 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 && arr[j].compareTo(arr[j-1]) < 0 ; j--){
// swap(arr, j, j-1);
// }
// 写法3----速度更快,因为方法一二交换一次要赋值三次,而本方法只要赋值一次
Comparable e = arr[i];
int j = i;
for( ; j > 0 && arr[j-1].compareTo(e) > 0 ; j--)
arr[j] = arr[j-1];
arr[j] = e;
}
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
相比选择排序,插入排序的一个优点是提前终止,不用遍历整个数组,因此,插入排序应该要比选择排序的效率更加高效,但是如果测试你会发现,选择排序会快些,为什么呢,因为插入排序每交换个位置,就要赋值三次,在这里浪费了大量时间,所以当我们用上面代码方法3就能避免这个问题,时间就比选择排序快了
1.4 希尔排序(Shell Sort)
2. O(nlogn)的排序算法
2.1 归并排序
1) 自顶向下—递归
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
可以看到这种结构很像一棵完全二叉树。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
最后我们来看看代码实现
import java.util.Arrays;
//缺点:要开辟临时空间(O(n)的额外空间),但是时间的复杂度相对于空间的复杂更重要
public class _05_MergeSort {
// 我们的算法类不允许产生任何实例
private _05_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 lp = l, rp = mid+1;
for( int i = l ; i <= r; i ++ ){
if( lp > mid ){ // 如果左半部分元素已经全部处理完毕
arr[i] = aux[rp-l]; rp ++;
}
else if( rp > r ){ // 如果右半部分元素已经全部处理完毕
arr[i] = aux[lp-l]; lp ++;
}
else if( aux[lp-l].compareTo(aux[rp-l]) < 0 ){ // 左半部分所指元素 < 右半部分所指元素
arr[i] = aux[lp-l]; lp ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[i] = aux[rp-l]; rp ++;
}
}
}
// 递归使用归并排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r) {
if (l >= r)
return;
int mid = (l+r)/2; //其实要注意l+r溢出
sort(arr, l, mid);
sort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
}
归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
2) 自底向上—非递归
import java.util.Arrays;
public class _05_MergeSort_02_DB {
// 我们的算法类不允许产生任何实例
private _05_MergeSort_02_DB(){}
// 将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 lp = l, rp = mid+1;
for( int i = l ; i <= r; i ++ ){
if( lp > mid ){ // 如果左半部分元素已经全部处理完毕
arr[i] = aux[rp-l]; rp ++;
}
else if( rp > r ){ // 如果右半部分元素已经全部处理完毕
arr[i] = aux[lp-l]; lp ++;
}
else if( aux[lp-l].compareTo(aux[rp-l]) < 0 ){ // 左半部分所指元素 < 右半部分所指元素
arr[i] = aux[lp-l]; lp ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[i] = aux[rp-l]; rp ++;
}
}
}
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));
}
}
2.2 快速排序
1)两路快速排序
快速排序是非常常用的排序方法, 采用分治法的策略将数组分成两个子数组, 基本 思路是:
- 从数组中取一个元素作为基准元素, 通常取数组的第一个或者最后一个元素;
- 分区, 即将数组中比基准元素小的元素放到基准元素的左边, 比基准元素大的元素放 到右边;
- 递归, 分别对比基准元素小的部分和比基准元素大的子数组用相同的方式进行排序;
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归 下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素 摆到它最后的位置去。
优点与缺点
快速排序最大的优点速度快, 通常能够达到 O(NlogN)
的速度, 原地排序, 不需要额 外的空间, 是非常优秀的算法, 在不考虑稳定性的情况下, 通常会考虑使用快速排序。
不过, 快速排序的缺点也是很明显的:
- 首先就是不稳定, 会打乱数组中相同元素的相对位置;
- 算法的速度严重依赖分区操作, 如果不能很好的分区, 比如数组中有重复元素的情况, 最坏情况下(对于已经排序的数组), 速度有可能会降到
O(N^2)
。
通常在快排实现中, 会对数组进行一次随机排序, 防止最坏的情况出现。
两路排序的算法在我github上:https://github.com/yangbishang/Algorithm/blob/master/01-Sort/_06_QuickSort_02_2Ways.java
2)三路快速排序
三路快速排序是快速排序的的一个优化版本, 将数组分成三段, 即小于基准元素、 等于 基准元素和大于基准元素, 这样可以比较高效的处理数组中存在相同元素的情况, 其它特 征与快速排序基本相同。
我们对整个数组进行处理,处理到一般的样子就如下图:
现在我们要处理i位置的元素,就需要分情况讨论:
1)当前要处理的元素e等于v,很简单,e纳入等于v的部分,i++就行了
2)当前要处理的元素e小于v时,就将元素e与等于v的第一个元素作交换,然后lt++,i++
3)当前要处理的元素e大于v时,就将元素e与gt-1位置的元素作交换,gt--
最后排列完后,就是下面这个样子,然后我们需要将第一个等于v的元素与lt位置的元素作交换就ok了!注意,现在小于v的元素范围就变成了【l ,lt-1】
注意:一定要注意边界问题!
排序过程: ● <v:【 L+1 , Lt 】 ● =v :【 Lt+1 , gt-1 】 ● >v:【 gt , r 】
排完序后: ● <v:【 L , Lt-1 】 ● =v :【 Lt , gt-1 】 ● >v:【 gt , r 】
//相较于2路快速排序,三路快速排序不需要再次递归排列等于v的数,
public class _06_QuickSort_03_3Ways {
// 我们的算法类不允许产生任何实例
private _06_QuickSort_03_3Ways(){}
// 递归使用快速排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r){
// 对于小规模数组, 使用插入排序
if( r - l <= 15 ){
_03_InsertionSort.sort(arr, l, r);
return;
}
// 随机在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 --;
i ++;
}
else{ // arr[i] == v
i ++;
}
}
swap( arr, l, lt );
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(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}