
[TOC]
转载: 于风‘s blog
算法之排序 排序算法基本上是我们无论是在项目中还是在面试中都会遇到的问题,加上最近在看《算法》这本书,所以就准备好好的将排序算法整理一下。
所有排序算法都是基于 Java 实现,为了简单,只使用了int类型,从小到大排序
基本排序
高效的排序
各大排序的时间测试
如何选择排序
排序之基本排序算法 准备阶段:有一个交换位置的函数exc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void exc (int a[],int i,int j) { if (a[i] != a[j]){ a[i] ^= a[j]; a[j] ^= a[i]; a[i] ^= a[j]; } }
基本排序算法主要是分为插入排序,选择排序,冒泡排序和梳排序。
选择排序 原理: 选择排序的原理很简单,就是从需要排序的数据中选择最小的(从小到大排序),然后放在第一个,选择第二小的放在第二个……
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static int [] selectionSort(int a[]){ int min; for (int i=0 ;i<a.length;i++){ min = i; for (int j = i+1 ; j < a.length; j++) { if (a[min]>a[j]){ min = j; } } if (min != i){ exc(a, i, min); } } return a; }
选择排序的动画演示
插入排序 原理: 如果数组进行循环得到a,若a比a前面的一个数小,则a就与前面数交换位置(相当于a向前面移动一位),若移动后a任然比前面一个数小,则再向前移动一位……
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static int [] insertSort(int a[]) { int N = a.length; for (int i = 0 ; i < N; i++) { for (int j = i; j >0 && (a[j-1 ]>a[j]); j--) { exc(a, j, j-1 ); } } return a; }
动画演示:
若数组的长度是N(不重复 ),则时间复杂度:
平均:NN/4 次比较,N N/4次交换
最好:N-1次比较,0次交换
最坏:NN/2次比较, N N/2次交换
特点:
若数据倒置的数量很少时,速度快。
冒泡排序 原理: 冒泡排序的原理就是小的数字慢慢的往上浮。从数组最后面开始循环,如果一个数比它前面数小,则交换两者位置。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static int [] bubbleSort(int [] a) { int N = a.length; for (int i = 0 ; i < N - 1 ; i++) { for (int j= N-1 ; j > i; j--) { if (a[j-1 ]>a[j]){ exc(a, j-1 , j); } } } return a; }
冒泡排序的动画示意图:
这个示意图和代码刚好相反,这个是将大的向后下沉
时间复杂度:
平均情况下:冒泡比较的次数约是插入排序的两倍,移动次数一致。
平均情况下:冒泡与选择排序的比较此时是一样的,移动比选择排序多出n次
冒泡算法的改进:
改进部分就是,如果在第二层for循环中,如果不发生交换,则代表数据已经排好序了,不需要继续排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static int [] bubbleSort2(int [] a) { int N = a.length; boolean flag = true ; for (int i = 0 ; i < N - 1 && flag; i++) { int j = N-1 ; for (flag = false ; j > i; j--) { if (a[j-1 ]>a[j]){ flag = true ; exc(a, j-1 , j); } } } return a; }
bubbleSort2()并不是一个多么令人欣喜的改进,但是基于bubbleSort2()的梳排序,却值得研究一下
——《C++数据结构与算法》
排序之高效排序算法 梳排序 原理: 梳排序分为两部分,第一部分通过步长stepn进行简单的排序,将大的数据集中到后面。第二部分是使用bubbleSort2()进行排序。
通过第一部分step的比较,我们能够有效的消除数组中的乌龟(即在数组尾部的较小的数值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static int [] combSort(int [] a) { int N = a.length; int step = N; int k; while ((step /= 1.3 ) > 1 ) { for (int i = N-1 ; i >= step; i--) { k = i -step; if (a[k]>a[i]){ exc(a, k, i); } } } a= bubbleSort2(a); return a; }
梳排序动画示意图:
在梳排序中,原作者用随机数做实验,得到了最有效的递减效率是1.3。也就是step/=1.3,同样也可以写成step *= 0.8,因为编程语言乘法比除法快。
希尔排序 希尔排序是基于插入排序进行改进,又称之为递减增量排序。在前面中我们知道,插入排序是将小的元素往前挪动位置,并且每次只移动一个位置。那么希尔排序是怎么解决这个问题的呢?
原理 :希尔排序的理念和梳排序的理念有点类似。在梳排序中,我们比较距离相差为step的两个元素来完成交换。在希尔排序中,我们的做法也是类似。我们在数组中每隔h取出数组中的元素,然后进行插入排序。当h=1时,则就是前面所写的插入排序了。
代码实现:
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 public static int [] shellSort(int [] a){ int N = a.length; int h = 1 ; while (h < N/3 ){ h = h*3 + 1 ; } while (h>=1 ){ for (int i = h; i < N; i++) { for (int j = i; j >= h && a[j-h]>a[j]; j -= h) { exc(a, j, j-h); } } h /= 3 ; } return a; }
快速排序 原理: 快速排序使用分治法(Divide and conquer)策略来把一个序列分为较小和较大的2个子序列,然后递归地排序两个子序列。
步骤为:
挑选基准值:从数列中挑出一个元素,称为“基准”(pivot),
分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
选取基准值有数种具体方法,此选取方法对排序的时间性能有决定性影响。
快速排序的实现代码:
在前面我们知道,选取正确的基准值对排序的性能有着决定性的影响,在这里我们选择序列中间的值作为基准值。
代码主要分为两个部分:
进行切分的代码
进行递归调用的代码
第一部分
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 public static int partition (int [] a,int lo,int h) { int middle = (lo+h+1 )/2 ; int v = a[middle]; exc(a, lo, middle); int i = lo; int j = h+1 ; while (true ){ while (a[++i] < v){ if (i == h){ break ; } } while (a[--j]>v){ if (j == lo){ break ; } } if (i>=j){ break ; } exc(a, i,j); } exc(a, lo, j); return j; }
第二部分:
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 public static void quickSort (int [] a) { quickSort(a,0 ,a.length-1 ); } public static void quickSort (int [] a,int lo,int h) { if (h <= lo) { return ; } int j = partition(a, lo, h); quickSort(a,lo,j-1 ); quickSort(a,j+1 ,h); }
快速排序动画示意图 :
特点:
快速排序在最坏的情况下时间复杂度是O(n**2),平均时间复杂度是O(nlogn)。快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。
堆排序 原理:堆排序是利用堆这个数据结构而设计的一种排序算法。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
接下来我们将使用大顶堆来进行从小到大的排序。图源 这位大佬讲的不错!!
在一个堆中,位置k的结点的父元素的位置是(k+1)/2-1,而它的两个子节点的位置分别是2k+1和2k+2,这样我们就可以通过计算数组的索引在树中上下移动。
那么我们 进行堆排序, 应该怎么做呢?首先,我们得构建一个堆(大顶堆)。构建的思路就是:我们将小的元素下沉(sink())即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void sink (int [] a,int k,int N) { while (2 *k+1 <= N){ int j = 2 *k + 1 ; if (j < N -1 && a[j+1 ] > a[j]){ j ++; } if (a[j] < a[k]){ break ; } exc(a, k, j); k = j; } }
我们通过调用sink()函数和一些逻辑就可以得到一个大顶堆了。【注意:在大顶堆中,可以很简单的知道堆顶的元素是最大值】那么我们如何进行堆排序呢?这时候我们可以将对顶的元素移动到最后使得末尾的元素最大,然后我们继续调用sink函数,又可以使得堆顶的元素最大(实则为总的第二大),然后继续重复以前的操作即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void heepSort (int [] a) { int N = a.length; for (int i = N/2 ; i >= 0 ; i--) { sink(a, i,N - 1 ); } N = N -1 ; while (N>0 ){ exc(a, 0 , N--); sink(a, 0 ,N); } }
动画演示:
堆排序的特点:
最好、最坏、平均的时间复杂都为O(nlogn),空间复杂度为O(1)。
是一种不稳定的排序。
牺牲空间节约时间的高效排序 归并排序(Merge Sort) 归并排序的核心思想是分治法,是创建在归并操作上面的一种有效的排序算法。
原理:
采用分治法:
代码实现:
首先我们来实现数组之间的归并操作:
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 public static int [] aux;public static void merge (int [] a,int lo,int middle,int hi) { int i = lo; int j = middle +1 ; for (int k = lo; k <= hi; k++) { aux[k] = a[k]; } for (int z = lo; z <= hi; z++) { if (i > middle){ a[z] = aux[j++]; }else if (j > hi){ a[z] = aux[i++]; }else if (aux[i] > aux[j]){ a[z] = aux[j++]; }else { a[z] = aux[i++]; } } }
MergeSort算法调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void mergeSort (int [] a) { aux = new int [a.length]; mergeSort(a, 0 , a.length-1 ); } public static void mergeSort (int [] a, int lo, int hi ) { if (lo >= hi){ return ; } int middle = (lo + hi)/2 ; mergeSort(a,lo,middle); mergeSort(a,middle+1 ,hi); merge(a, lo, middle, hi); }
特点:
归并排序是一种稳定的并且十分高效的排序。在时间复杂度方面,mergeSort的时间复杂度是O(nlogn)【无论是最好还是最坏的情况】,空间复杂度是O(n)。
基数排序(非比较排序)
实例分析
基数排序的方式有 LSD (Least sgnificant digital) 和 MSD (Most sgnificant digital)两种方式。LSD 的排序方式由键值的最右边开始,而 MSD 则相反,由键值的最左边开始。 以 LSD 为例
1 data = [10 123 732 67 5 918 7 ]
首先根据个位数的数值,j将数据分配到不同的桶中
编号
0
1
2
3
4
5
6
7
8
9
10
732
123
5
67
918
7
99
然后,将这些数字按照桶以及桶内部的排序连接起来:
1 data = [10 732 123 5 67 7 918]
接着按照十位的数值,放入不同的桶中(ps:5的十位是0 )
编号
0
1
2
3
4
5
6
7
8
9
5
10
123
732
67
918
7
重复连接操作,完成排序:
1 data = [5 7 10 123 732 67 918]
最后根据百位的数值,放入不同的桶中(ps:5的十位是0 )
编号
0
1
2
3
4
5
6
7
8
9
5
123
732
918
7
10
67
最后重复连接操作,完成排序:
1 data = [5 7 10 67 123 732 918]
代码实现
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 public static void radixSort (int [] a) { int max = a[0 ]; for (int value : a) { if (max < value){ max = value; } } int N = 0 ; if (max == 0 ){ N = 1 ; }else { N = (int ) (Math.log10(max) + 1 ); } radixSort(a,N); } public static void radixSort (int [] a, int N) { int radix = 10 ; int length = a.length; int factor = 1 ; int [][] bucket = new int [radix][length]; int [] order = new int [radix]; for (int i =0 ;i<N;i++,factor *= 10 ){ for (int v : a) { int digit = (v/factor)%10 ; bucket[digit][order[digit]] = v; order[digit] ++; } int position = 0 ; for (int j =0 ;j<radix;j++ ){ if (order[j] != 0 ){ for (int k = 0 ; k < order[j]; k++) { a[position++] = bucket[j][k]; } order[j] = 0 ; } } } }
特点:
不依赖于数据比较。
时间复杂度为O(k*n);空间复杂度为O(n)
计数排序(非比较排序) 原理:
计数排序使用一个额外的数组C,其中C中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
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 public static void countSort (int [] a) { int max = a[0 ]; for (int v : a) { if (v > max){ max = v; } } int [] count = new int [max+1 ]; for (int v : a) { count[v] ++; } int indexArray = 0 ; for (int i = 0 ; i < count.length; i++) { while (count[i] > 0 ){ a[indexArray++]=i; count[i] --; } } }
当然,如果数据比较集中的话,我们大可不必创建那么大的数组,我们找出最小和最大的元素,以最小的元素作为基底以减小数组的大小。
动画演示:
特点:
计数排序是一种稳定的线性时间排序算法。
时间复杂度为O(n+k),空间复杂度为O(n+k)