一、概述
作为一个合格的程序员,算法是必备技能,特此总结十大基础排序算法。java版源码实现,强烈推荐《算法第四版》非常适合入手,所有算法网上可以找到源码下载。
PS:本文讲解算法分三步:1.思想2.图示3.源码4.性能分析
1.1 时间复杂度
算法的运行时间,在这里主要考量:比较和交换的成本。
1.2 空间复杂度
使用的内存,是否需要额外的存储空间。
1.3 稳定性
相等元素排序前后相对位置保持不变,就认为是稳定的。
稳定性何时有意义?
1)排序对象包含多个属性
2) 原始序列其它属性有序
3)期望最终排序后原来有序的属性还有序,原来没序的字段有序了。
二、常见排序算法
通用函数封装
由于排序算法都用到一些相同源码,所以提炼出来作为一个父类,算法只需要继承这个类即可使用通用方法:
1.less()比较元素大小
2.exch()交换元素
3.show()打印排序后数组
4.isSorted()校验排序是否正确
1 package algorithm;
2
3 /**
4 *
5 * @ClassName:MySort
6 * @Description:自定义排序基类,封装常用方法
7 * @author denny.zhang
8 * @date 2018年2月5日下午3:12:03
9 */
10 public class MySort {
11 protected static String[] a = new String[]{"a","d","c","b"};
12
13 /**
14 *
15 * @Description 打印排序后结果
16 * @param a
17 * @author denny.zhang
18 * @date 2018年2月5日下午3:11:46
19 * @since JDK1.8
20 */
21 protected static void show(Comparable[] a){
22 for(int i=0;i<a.length;i++){
23 System.out.print(a[i]+" ");
24 }
25 System.out.println();
26 }
27
28 /**
29 *
30 * @Description 比较是否v小于w
31 * @param v
32 * @param w
33 * @return
34 * @author denny.zhang
35 * @date 2018年2月5日下午3:11:14
36 * @since JDK1.8
37 */
38 protected static boolean less(Comparable v,Comparable w){
39 return v.compareTo(w)<0;
40 }
41
42 /**
43 *
44 * @Description 对于数组a,交换a[i]和a[j]。
45 * @param a
46 * @param i
47 * @param j
48 * @author denny.zhang
49 * @date 2018年2月5日下午3:09:41
50 * @since JDK1.8
51 */
52 protected static void exch(Comparable[] a,int i,int j){
53 Comparable temp= a[i];
54 a[i]=a[j];
55 a[j]=temp;
56 //System.out.println("交换a["+i+"]"+",a["+j+"]");
57 }
58
59 /**
60 *
61 * @Description 用于校验是否升序
62 * @param a
63 * @return
64 * @author denny.zhang
65 * @date 2018年2月5日下午3:10:57
66 * @since JDK1.8
67 */
68 protected static boolean isSorted(Comparable[] a){
69 //只要有一个后数<前数,返回失败
70 for(int i=0;i<a.length;i++){
71 if(less(a[i], a[i-1])) return false;
72 }
73 //都没问题,返回成功
74 return true;
75 }
76
77 /**
78 *
79 * @Description 用于校验是否升序
80 * @param a
81 * @param lo 起始下标
82 * @param hi 结束下标
83 * @return
84 * @author denny.zhang
85 * @date 2018年2月6日下午5:19:43
86 * @since JDK1.8
87 */
88 protected static boolean isSorted(Comparable[] a, int lo, int hi) {
89 for (int i = lo + 1; i <= hi; i++)
90 if (less(a[i], a[i-1])) return false;
91 return true;
92 }
93 }
2.1 冒泡排序
思想:
最简单的排序,内外两遍循环。外循环简单遍历a[n-1]~a[1],内循环一遍确定一个最大值,类似"冒泡"。
1.外循环遍历a[n-1]~a[1]。例如选定a[n-1]
2.内循环,从a[0]-a[n-1] 从左往右,比较相邻的元素对。如果第一个比第二个大,就交换它们两个,一直到最后一对,这样在最后的元素a[n-1]是最大的数;
2.重复以上的步骤,一直到外循环遍历到a[1],内循环会比较a[0],a[1],至此排序完毕。
优化点:添加一个标记,当内循环一遍不交换,排序完毕。
动态图:
源码:
1 /**
2 * @author denny
3 * @Description 冒泡排序
4 * @date 2019/7/2 下午6:19
5 */
6 public class Bubble extends MySort {
7
8 public static void sort(Comparable[] a) {
9 int n = a.length;
10 // 标记内层遍历是否交换过
11 int flag = 0;
12 // 外层遍历n-1次,每次确定一个最大值 a[i]
13 for (int i = n - 1; i > 0; i--) {
14 // 内层遍历:比较a[0]a[1]...a[n-1-1]a[n-1]
15 for (int j = 0; j < i; j++) {
16 // 相邻元素两两对比,如果后<前,交换
17 if (less(a[j + 1], a[j])) {
18 // 元素交换
19 exch(a, j + 1, j);
20 flag = 1;
21 }
22 }
23 // flag=0,说明数列已有序不需要再交换了!!!
24 if(flag==0){
25 break;
26 }
27 }
28 }
29
30 public static void main(String[] args) {
31 Bubble.sort(a);
32 show(a);
33 }
34 }
分析:
时间复杂度:外部循环O(n) * 内部循环O(n) -->O(n²)
空间复杂度:不需要借助外部空间-->O(1)
是否稳定:相等元素不交换-->是。
2.2 选择排序
思想:
最简单的排序,内外两遍循环。外循环简单遍历a[0]~a[n-1],内循环每次确定一个最小值
1.外循环假设第一个数是最小值
2.内循环拿后面所有元素和第一个元素比较,找到最小值,放在第一位(如果第一位不是最小值就交换),第一小的元素确定。
3.外循环从第一个元素遍历到最后,重复1,2操作,排序完毕。
图示:
动态图:
源码:
1 package algorithm;
2
3 /**
4 *
5 * @ClassName:Selection
6 * @Description:选择排序:对于数组长度为N的数组<br>
7 * <ul>
8 * <li>比较次数:(n-1)+(n-2)+...2+1=n(n-1)/2,大约N²/2次</li>
9 * <li>交换次数:N次</li>
10 * </ul>
11 * @author denny.zhang
12 * @date 2018年2月5日上午11:13:26
13 */
14 public class Selection extends MySort{
15
16 @SuppressWarnings("rawtypes")
17 public static void sort(Comparable[] a){
18 int n = a.length;
19 //遍历n次,每次确定一个最小值
20 for(int i=0 ; i<n ; i++){
21 int min=i;//初始化最小值下标为i
22 //把i之后的每一项都和a[min]比较,求最小项
23 for(int j=i+1;j<n;j++){
24 if (less(a[j], a[min])) min = j;
25 }
26 //a[i]和a[min]交换,第i位排序完毕
27 exch(a, i, min);
28 }
29 }
30
31 public static void main(String[] args) {
32 Selection.sort(a);
33 show(a);
34 }
35 }
分析:
时间复杂度:外部循环O(n) * 内部循环O(n) = O(n²)
空间复杂度:O(1)不需要借助外部空间
是否稳定:否。只要遇到小的就交换,被交换对象如果有等值元素存在且在交换元素之前,打破相对顺序了。列子:58529-》第一个5和2交换,2个5的相对顺序变了。
2.3 插入排序
思想:
类似扑克牌抓牌一样,一次插入一张牌保证当前牌有序。
从第二张牌开始,插入第一张牌;第三张牌插入前两张牌...一直到最后一张牌。插入牌时,两两比较,遇到逆序交换。
外循环i++,内循环a[j]和a[j-1]比较,逆序交换。j--往左移动,两两比较。
图示:
动态图:
源码:
1 package algorithm;
2
3 /**
4 *
5 * @ClassName:Insertion
6 * @Description:插入排序:对于数组长度为N的数组<br>就像扑克牌插纸牌一样
7 * <ul>
8 * <li>比较次数:N-1~N²/2 平均N²/4 </li>
9 * <li>交换次数:0~N²/2 平均N²/4</li>
10 * </ul>
11 * @author denny.zhang
12 * @date 2018年2月5日上午11:13:26
13 */
14 public class Insertion extends MySort{
15
16 @SuppressWarnings("rawtypes")
17 public static void sort(Comparable[] a){
18 int n = a.length;
19 //遍历n-1次,a[i]插入前i个数
20 for(int i=1 ; i<n ; i++){
21 //System.out.println("i="+i);
22 //i之前的每两项比较,出现逆序,立即交换
23 for(int j=i;j>0 && less(a[j], a[j-1]);j--){
24 exch(a, j, j-1);
25 }
26 }
27 }
28
29 public static void main(String[] args) {
30 Insertion.sort(a);
31 show(a);
32 }
33 }
分析:
时间复杂度:O(n~n²)如果元素有序就是n, 元素逆序就是n²
空间复杂度:O(1)
是否稳定:是
2.4 希尔排序
思想:
希尔排序又称缩小增量排序,是插入排序的升级版。核心思想是使数组中任意间隔为h的元素有序,针对每个h,取出间隔h的子数组并插入排序,h越来越小一直到1,即排序完成。这个间隔h的选择有考究。
图示:
动态图:
步长:3-2-1
源码:
递增序列有很多,只要满足最后一个数为1即可。即最少有间隔为1的插入排序即可保证排序。至于这个数列具体最优不在本文讨论范围内。
下图展示了2种序列,sort()方法是复杂序列,sort2()是简单序列。如果只是理解算法的话sort2()足矣。
1 package algorithm;
2
3 /**
4 *
5 * @ClassName:Shell
6 * @Description:希尔排序:改进自插入排序,交换不相邻元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序<br>
7 *
8 * @author denny.zhang
9 * @date 2018年2月5日上午11:13:26
10 */
11 public class Shell extends MySort{
12
13 /**
14 *
15 * @Description 更加符合要求的算法
16 * @param a
17 * @author denny.zhang
18 * @date 2018年3月7日下午5:35:07
19 * @since JDK1.8
20 */
21 @SuppressWarnings("rawtypes")
22 public static void sort(Comparable[] a){
23 int n = a.length;
24
25 // 3x+1 increment sequence: 1, 4, 13, 40, 121, 364, 1093, ...
26 int h = 1;
27 while (h < n/3) {//这里确保了每个子数组有>=3个元素,h间隔过大,每个子数组元素太少就没有排序的意义了
28 h = 3*h + 1; //1.如果小于n/3,h变大,直到h>=n/3为止
29 }
30 while (h >= 1) {
31 // h-sort the array 2.遍历从h->n
32 for (int i = h; i < n; i++) {
33 //3.从j开始往左每h间隔比较一次,逆序就交换
34 for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) {
35 exch(a, j, j-h);
36 }
37 }
38 assert isHsorted(a, h);
39 h /= 3;//排完序再把h降低回来
40 }
41 assert isSorted(a);
42 }
43
44 /**
45 *
46 * @Description d=n/2序列(向上取整)简单算法
47 * @param a
48 * @author denny.zhang
49 * @date 2018年3月7日下午5:33:38
50 * @since JDK1.8
51 */
52 @SuppressWarnings("rawtypes")
53 public static void sort2(Comparable[] a){
54 int n = a.length;
55 int h = n;
56 //1.第一层循环 是h值计算
57 while (h >= 1) {
58 h=(int) Math.ceil(h/2);//向上取整
59 //2.第二层循环i++ 从h->n-1
60 for (int i = h; i < n; i++) {
61 //3.第三层循环 从j开始往左每h间隔比较一次,逆序就交换,做到局部有序
62 for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) {
63 exch(a, j, j-h);
64 }
65 }
66 assert isHsorted(a, h);
67 }
68 assert isSorted(a);
69 }
70
71 // is the array h-sorted?
72 private static boolean isHsorted(Comparable[] a, int h) {
73 for (int i = h; i < a.length; i++)
74 if (less(a[i], a[i-h])) return false;
75 return true;
76 }
77
78 public static void main(String[] args) {
79 Shell.sort(a);
80 show(a);
81 }
82 }
分析:
时间复杂度:O(n的1~2次方,暂无法证明最优递增数列)
空间复杂度:O(1)
是否稳定:否
2.5 归并排序
思想:
将数组拆分为两部分(递归)分别排序,再将结果归并排序。
图示:
动态图:
源码:
1 package algorithm;
2
3 /**
4 *
5 * @ClassName:Merge
6 * @Description:归并排序:自顶向下+自底向上。性能差不多:比较次数:1/2NlgN-NlgN 访问次数6NlgN
7 * @author denny.zhang
8 * @date 2018年2月6日下午5:09:57
9 */
10 public class Merge extends MySort{
11
12
13 /**
14 *
15 * @Description 归并
16 * @param a
17 * @param aux
18 * @param lo
19 * @param mid
20 * @param hi
21 * @author denny.zhang
22 * @date 2018年2月6日下午4:55:25
23 * @since JDK1.8
24 */
25 private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
26
27 // 复制数组a->aux
28 for (int k = lo; k <= hi; k++) {
29 aux[k] = a[k];
30 }
31
32 // 归并进a数组
33 int i = lo, j = mid+1;
34 //从aux数组找到小值并赋值给数组a
35 for (int k = lo; k <= hi; k++) {
36 if (i > mid) a[k] = aux[j++];//左半边用尽,取右半边元素aux[j]->赋值给a[k],并j++
37 else if (j > hi) a[k] = aux[i++];//右半边用尽,取左半边元素aux[i]->赋值给a[k],并i++
38 else if (less(aux[j], aux[i])) a[k] = aux[j++];//aux[j]小->赋值给a[k],并j++
39 else a[k] = aux[i++];//aux[i]小->赋值给a[k],并i++
40 }
41 }
42
43 // 自顶向下归并排序 a[lo..hi] 使用辅助数组 aux[lo..hi]
44 private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
45 if (hi <= lo) return;
46 int mid = lo + (hi - lo) / 2;
47 sort(a, aux, lo, mid);//左半边排序
48 sort(a, aux, mid + 1, hi);//右半边排序
49 merge(a, aux, lo, mid, hi);//归并结果
50 }
51
52 /**
53 *
54 * @Description 自底向上归并排序
55 * @param a
56 * @author denny.zhang
57 * @date 2018年2月6日下午6:46:01
58 * @since JDK1.8
59 */
60 public static void sortBU(Comparable[] a,Comparable[] aux) {
61 int n = a.length;
62 for (int len = 1; len < n; len *= 2) {//len子数组大小,有多少个子数组遍历多少回
63 //子数组从2个元素开始自己归并:22归并->44归并->88归并...最后一个大归并
64 for (int lo = 0; lo < n-len; lo += len+len) {//lo子数组起始下标
65 int mid = lo+len-1;
66 int hi = Math.min(lo+len+len-1, n-1);//最后一个数组可能小于len,即不能按照2的倍数分小数组,最后一个取最小值
67 merge(a, aux, lo, mid, hi);
68 }
69 }
70 }
71
72 /**
73 *
74 * @Description 归并排序
75 * @param a
76 * @author denny.zhang
77 * @date 2018年2月6日下午4:52:45
78 * @since JDK1.8
79 */
80 public static void sort(Comparable[] a) {
81 //定义一个辅助数组
82 Comparable[] aux = new Comparable[a.length];
83 //自顶向下归并
84 sort(a, aux, 0, a.length-1);
85 //自底向上归并
86 //sortBU(a,aux);
87 //校验是否排序成功
88 assert isSorted(a);
89 }
90
91 public static void main(String[] args) {
92 Merge.sort(a);
93 show(a);
94 }
95 }
分析:
时间复杂度:O(NlogN)简单理解:看上图源码44行sort(),把数组拆分2个有序数组,然后再归并2个小数组。长度为N的数组递归“折半拆分成2个有序数组”需要logN步(二叉排序树的树高),每一步的归并耗时N,相乘得到NlogN即是时间复杂度。
空间复杂度:O(N)不用多说使用了额外的辅助数组aux[N]
是否稳定:是
2.6 快速排序
思想:
两向切分:取数组第一个元素作为切分元素,左边子数组全部小于等于切分元素,右边子数组全部大于等于切分元素,这样每切分一次就可以确定一个元素的位置。左右子数组再分别递归排序。
三向切分:对于数组中重复元素多的数组,更适合三向切分。也是第一个元素作为切分元素值=v,三向切分:第一段:lo~lt元素<v,第二段:lt~gt元素=v,第三段:gt~hi元素>v。递归给第一段和第三段排序。
注意:对于小数组(<=15),插入排序更适合。
图示:
下图是一个两向切分
动态图
源码:
1 package algorithm;
2
3 import edu.princeton.cs.algs4.StdRandom;
4
5 /**
6 *
7 * @ClassName:Quick
8 * @Description:快速排序
9 * <ul>
10 * <li>两向切分</li>
11 * <li>三向切分:重复元素多的时候</li>
12 * </ul>
13 * @author denny.zhang
14 * @date 2018年2月7日下午5:50:56
15 */
16 @SuppressWarnings("rawtypes")
17 public class Quick extends MySort{
18
19
20 public static void sort(Comparable[] a) {
21 StdRandom.shuffle(a);//随机排列,避免数组有序出现最差排序
22 show(a);
23 sort(a, 0, a.length - 1);//两向切分
24 //sort3Way(a, 0, a.length - 1);//三向切分
25 assert isSorted(a);
26 }
27
28 /**
29 *
30 * @Description 两向切分快排
31 * @param a
32 * @param lo
33 * @param hi
34 * @author denny.zhang
35 * @date 2018年2月7日下午4:36:48
36 * @since JDK1.8
37 */
38 private static void sort(Comparable[] a, int lo, int hi) {
39 if (hi <= lo) return;//
40 int j = partition(a, lo, hi);//拆分,找到拆分下标
41 System.out.println("j="+j);
42 show(a);
43 sort(a, lo, j-1);//左边排序,递归
44 sort(a, j+1, hi);//右边排序,递归
45 assert isSorted(a, lo, hi);
46 }
47
48 // 两向切分数组,找到拆分下标并返回,保证a[lo..j-1] <= a[j] <= a[j+1..hi]
49 private static int partition(Comparable[] a, int lo, int hi) {
50 System.out.println("lo="+lo+",hi="+hi);
51 int i = lo;//左指针
52 int j = hi + 1;//右指针
53 Comparable v = a[lo];//初始化一个切分元素
54 //一个大的自循环
55 while (true) {
56
57 // 分支1:如果左指针元素小于v,++i,一直到右边界退出,或者左边有不小于v的元素停下,执行分支2
58 while (less(a[++i], v))
59 if (i == hi) break;
60
61 // 分支2:如果右指针元素大于v,j--,一直到到左边界退出,或者右边有不大于v的元素为止,执行分支3
62 while (less(v, a[--j]))
63 if (j == lo) break;
64
65 // 分支3:如果左右指针碰撞,甚至左指针大于右指针,退出
66 if (i >= j) break;
67 // 交换需要交换的a[i]>=v>=a[j],使得a[i]<a[j]
68 System.out.println("交换 a[i] a[j] i="+i+",a[i]="+a[i]+",j="+j+",a[j]="+a[j]);
69 exch(a, i, j);
70
71 }
72 System.out.println("i="+i+",j="+j+",设置 a[j]="+a[j]+",替换为a[lo]="+a[lo]);
73 // a[j]=a[lo],即v
74 exch(a, lo, j);
75
76 // 返回下标j,a[j]就是中轴
77 return j;
78 }
79
80 //三向切分快排
81 @SuppressWarnings("unchecked")
82 private static void sort3Way(Comparable[] a, int lo, int hi) {
83 if (hi <= lo) return;
84 int lt = lo, gt = hi;
85 Comparable v = a[lo];
86 int i = lo + 1;
87 while (i <= gt) {
88 int cmp = a[i].compareTo(v);
89 if (cmp < 0) exch(a, lt++, i++);
90 else if (cmp > 0) exch(a, i, gt--);
91 else i++;
92 }
93
94 // a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi].
95 sort(a, lo, lt-1);
96 sort(a, gt+1, hi);
97 assert isSorted(a, lo, hi);
98 }
99
100 public static void main(String[] args) {
101 Quick.sort(a);
102 show(a);
103 }
104 }
分析:
时间复杂度:O(NlogN):长度与为N的数组,一颗二叉排序树左节点<根节点<右节点,一层一层拆分下去,层数=logN+1,每一层的时间复杂度都是o(N)所以NlogN+N,N>2时logN>1,所以时间复杂度为O(NlogN)
空间复杂度:O(1)
是否稳定:否
2.7 堆排序
思想:
1.构造一个有序堆(每个节点都大于等于2个子节点),2.下沉排序销毁堆:交换a[1]和a[n],即每次确认一个最大元素,后移除a[n],这样数组越来越小一直到元素为0.
图示:
动态图
源码:
1 package algorithm;
2
3 /**
4 *
5 * @ClassName:Heap
6 * @Description:堆排序:对于数组长度为N的数组<br>
7 * <ul>
8 * <li>比较次数:2NlgN+2N</li>
9 * <li>交换次数:NlgN+N</li>
10 * </ul>
11 * @author denny.zhang
12 * @date 2018年2月5日上午11:13:26
13 */
14 public class Heap extends MySort{
15
16 /**
17 *
18 * @Description 排序
19 * @param pq
20 * @author denny.zhang
21 * @date 2018年3月2日下午2:28:38
22 * @since JDK1.8
23 */
24 @SuppressWarnings("rawtypes")
25 public static void sort(Comparable[] pq){
26 int n = pq.length-1;//这样数组下标好算一点。原来下标0~6==>1~6
27 //1.构造有序堆(父节点大于等于子节点),从k=n/2--》1
28 for (int k = n/2; k >= 1; k--)
29 sink(pq, k, n);
30 //2.下沉排序,升序排序
31 while (n > 1) {
32 exch(pq, 1, n--);//交换根节点和首节点,后n--交换完一次就剔除最后那个已排序的元素
33 sink(pq, 1, n);//修复有序堆
34 }
35 }
36
37 /**
38 *
39 * @Description 从上至下的堆有序化的实现
40 * @param pq
41 * @param k 根节点
42 * @param n 堆长度
43 * @author denny.zhang
44 * @date 2018年3月2日上午10:28:36
45 * @since JDK1.8
46 */
47 @SuppressWarnings("rawtypes")
48 private static void sink(Comparable[] pq, int k, int n) {
49 while (2*k <= n) {
50 int j = 2*k;//左子节点下标
51 if (j < n && less(pq[j], pq[j+1])) j++;//找到子节点大值(pq[j]如果小于pq[j+1]就j++一直找,一直到大于等于的j),即pq[j]为左右子节点最大值
52 if (!less(pq[k], pq[j])) break;//如果父节点大于等于子节点大值,则堆有序,退出当前循环
53 exch(pq, k, j);//否则交换根节点和子节点大值
54 k = j;//跟节点下标变为j,即下沉到j
55 }
56 }
57
58 public static void main(String[] args) {
59 String[] a = new String[]{"0","d","c","b","e","a"};//第一个元素空着不用排序,这样数组下标好算一点。排序结果:0 a b c d e
60 Heap.sort(a);
61 show(a);
62 }
63 }
分析:
时间复杂度:O(NlogN):1.恢复堆:由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作;2.建堆操作:从len/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2) 其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程(具体需要公式推导这里不再拓展),结果是O(n)。二次操作时间相加=(N-1)logN+N=O(NlogN)。
空间复杂度:O(1)
是否稳定:否
2.8 计数排序
思想:
利用数组下标有序,新构造一个bucket[max-min+1],计数后,把新数组的下标(与min的差值)反向赋值给原数组。
为什么要求min? 避免不必要的空间浪费。比如90~99,如果开辟99+1=100,浪费太多空间了。现在只需要99-90+1=10的空间
注:升级版可以保证稳定性(相同元素值排序前后相对位置保持不变),这里就不再多写。
动态图:
源码:
1 package study.algorithm.sorting;
2
3 import study.algorithm.sorting.base.MySort;
4
5 /**
6 * @author denny
7 * @Description 计数排序,利用数组下标有序,新构造一个bucket[max-min+1],计数后,把新数组的下标反向赋值给原数组。
8 * 为什么要求min? 避免不必要的空间浪费。比如90~99,如果开辟99+1=100,浪费太多空间了。现在只需要99-90+1=10的空间
9 * 时间复杂度:O[n+k],k = max-min+1
10 * 空间复杂度:O[n+k]
11 * @date 2019/7/8 上午10:21
12 */
13 public class CountingSort extends MySort {
14
15 public static void sort(Integer[] b) {
16 // 1.遍历一遍,求最大值,最小值
17 int max = b[0], min = b[0];
18 for (int i : b) {
19 if (i > max) {
20 max = i;
21 }
22 if (i < min) {
23 min = i;
24 }
25 }
26 int k = max - min + 1;
27 // 构造长度为max+1的数组bucket
28 int[] bucket = new int[k];
29
30 // 2.遍历一遍b[], 以b元素-min值作为下标,统计出现次数赋值给bucket[]
31 for (int value : b) {
32 bucket[value-min]++;
33 }
34
35 int sortedIndex = 0;
36 // 3.遍历一遍bucket[],反过来给b赋值,j=元素与最小值差值
37 for (int j = 0; j < k; j++) {
38 // 重复元素赋值,bucket[j]元素重复出现次数,bucket的下标是b的元素值
39 while (bucket[j] > 0) {
40 // bucket的下标是b的元素值,j+min=元素值
41 b[sortedIndex++] = j+min;
42 bucket[j]--;
43 }
44 }
45
46 }
47
48 public static void main(String[] args) {
49 CountingSort.sort(b);
50 show(b);
51 }
52 }
分析:
计数排序是一种牺牲空间换取时间的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序表现良好。
时间复杂度:1.遍历一遍b[n],求最大值最小值=O(n) 2.遍历一遍b[n],给新数组赋值=O(n) 3.遍历一遍bucket(k=max-min+1)=O(k) O(2n+k),去掉系数=O(n+k)
空间复杂度:构造了额外空间int[] bucket = new int[k], 所以为O(k)。其中k=max-min+1
2.9 桶排序
思想:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
- 1.求原数组max、min。
- 2.把原数组元素分发到有限数量个桶中。(桶个数可自定义,映射关系例如:Math.floor(arr[i] - min)/ bucketSize)
- 3.桶内元素排序(可自选排序算法)。
- 4.遍历所有桶,把桶内元素顺序赋值给原数组。
图示:
源码:
1 package study.algorithm.sorting.intSort;
2
3 import study.algorithm.sorting.base.MyIntSort;
4
5 import java.util.Arrays;
6
7 /**
8 * @Description 桶排序
9 * @author denny
10 * @date 2019/7/9 下午4:03
11 */
12 public class BucketSort extends MyIntSort {
13
14 public static void sort(int[] arr) {
15 // 每个桶能存储5个元素
16 bucketSort(arr, 5);
17 }
18
19 /**
20 * 桶排序
21 * @param arr 需要排序的数组
22 * @param bucketSize 每个桶的容量
23 */
24 public static void bucketSort(int[] arr, Integer bucketSize){
25 // 1.遍历一遍,求最大值,最小值
26 int max = arr[0], min = arr[0];
27 for (int i : arr) {
28 if (i > max) {
29 max = i;
30 }
31 if (i < min) {
32 min = i;
33 }
34 }
35 // 桶个数
36 int bucketCount = (int) Math.floor((max - min) / bucketSize) + 1;
37 // 构造一个二维数组,行=桶个数,列=桶容量,初始为0
38 int[][] buckets = new int[bucketCount][0];
39
40 // 2.利用映射函数将数据分配到各个桶中
41 for (int i = 0; i < arr.length; i++) {
42 // arr[i]在哪个桶中
43 int index = (int) Math.floor((arr[i] - min) / bucketSize);
44 // 把arr[i]追加进桶 buckets[index]
45 buckets[index] = arrAppend(buckets[index], arr[i]);
46 }
47
48 int arrIndex = 0;
49 // 3.遍历所有桶,赋值
50 for (int[] bucket : buckets) {
51 if (bucket.length <= 0) {
52 continue;
53 }
54 // 对每个桶内部元素排序,这里使用了插入排序
55 InsertionIntSort.sort(bucket);
56 // 遍历每个桶中的元素,赋值给原始数组
57 for (int value : bucket) {
58 arr[arrIndex++] = value;
59 }
60 }
61
62
63 }
64
65 /**
66 * 自动扩容1位,并保存数据
67 *
68 * @param arr
69 * @param value
70 */
71 public static int[] arrAppend(int[] arr, int value) {
72 // 扩容1
73 arr = Arrays.copyOf(arr, arr.length + 1);
74 // 最后一位赋值
75 arr[arr.length - 1] = value;
76 return arr;
77 }
78
79 public static void main(String[] args) {
80 // 桶排序
81 BucketSort.sort(a);
82 // 查看排序结果
83 show(a);
84 }
85
86 }
分析:
时间复杂度:1.桶数量足够大时,一次分配完事,不需要桶内部排序O(n)。2.桶数量只有一个,都在一个桶内部排序,排序算法最差是O(n^2)
空间复杂度:使用了N个桶,桶初始容量固定。每个桶容量自动扩容,所以空间没浪费,但是有可能存在空桶。假设空桶有k个:O(n+k)
2.10 基数排序
思想:
1.求最高位
2.从低位开始排序,一直到高位。每一次排序都是稳定的。
动态图:
源码:
1 package study.algorithm.sorting.intSort;
2
3 import study.algorithm.sorting.base.MyIntSort;
4
5 /**
6 * @Description 基数排序:从低位到高位,一遍一遍排序。
7 * @author denny
8 * @date 2019/7/10 上午11:40
9 */
10 public class RadixIntSort extends MyIntSort {
11
12 public static void sort(int[] arr) {
13 // 求最高位
14 int maxDigit = getMaxDigit(arr);
15 // 基数排序
16 radixSort(arr,maxDigit);
17
18 }
19
20 /**
21 * 基数排序核心方法
22 * @param arr
23 * @param maxDigit
24 */
25 private static void radixSort(int[] arr, int maxDigit) {
26 // 取模数
27 int mod = 10;
28 // 除数
29 int dev = 1;
30 // 依次是:个位有序、十位有序(十位相同的个位有序)、百位有序(百位相同的个位、十位先后有序)...
31 for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
32 // 考虑负数的情况,这里扩展一倍队列数,其中 [0~(mod-1)]对应负数,[mod~(2mod-1)]对应正数 (bucket + 10),int[行][列] 行=桶 ,列=元素
33 int[][] buckets = new int[mod * 2][0];
34 // 遍历数组arr
35 for (int j = 0; j < arr.length; j++) {
36 // 求arr[j]在counter中的index,这里+mod,就是为了处理负数,例如mod=10,-1+10=9,在负数里面是最大的。
37 int index = ((arr[j] % mod) / dev) + mod;
38 // 把arr[j]追加进桶buckets[index]
39 buckets[index] = arrAppend(buckets[index], arr[j]);
40 }
41 int pos = 0;
42 // 遍历所有桶
43 for (int[] bucket : buckets) {
44 // 每个桶元素赋值给arr
45 for (int value : bucket) {
46 arr[pos++] = value;
47 }
48 }
49 }
50 }
51
52 /**
53 * 求最高位数
54 * @param arr
55 * @return
56 */
57 protected static int getMaxDigit(int[] arr) {
58 // 最大值
59 int maxValue = getMaxValue(arr);
60 // 求长
61 return getNumLenght(maxValue);
62 }
63
64 /**
65 * 求数长度
66 * @param num
67 * @return
68 */
69 protected static int getNumLenght(long num) {
70 if (num == 0) {
71 return 1;
72 }
73 int lenght = 0;
74 for (long temp = num; temp != 0; temp /= 10) {
75 lenght++;
76 }
77 return lenght;
78 }
79
80 public static void main(String[] args) {
81 int[] a = new int[] {321,60,1,-1,21,577,-123,11,10,743,127};
82 RadixIntSort.sort(a);
83 show(a);
84 }
85 }
分析:
时间复杂度:假设d是最高位,每一位排序都需要O(n),赋值给数组要O(n),所以总=O(d*2n),当然一般来说d<<n,所以整体来说也是O(n)线性阶。
空间复杂度:1.每次遍历都构造了一个int[k][0]的二维数组,k代表桶的个数 2.每个元素都会全部插入桶中,即O(n), 所以空间复杂度=O(n+k)。
三、总结
要想吃透这些算法,特别是时间空间的复杂度。这些算法很多都有改进版,但是是否真实有效,是否值得改进,又是另一说(例如现代计算机的cache命中机制、算法优化后难以理解且提升可能也只是适合特定的特征入参)。看完了算法,有一个例子可以来看看前辈们是如何使用算法的:jdk源码Arrays.sort(),后续会写一篇。总体就一句话,没有最优的算法,只有最适合特定场景的算法。 最后一张表来总结下:
各种常用排序算法 | |||||||
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 特点 | ||
最好 | 平均 | 最坏 | 辅助存储 | ||||
插入 排序 | 直接插入 | O(N) | O(N2) | O(N2) | O(1) | 稳定 | |
希尔排序 | O(N) | O(Ns) | O(N2) | O(1) | 不稳定 | 希尔排序的平均时间复杂度并未证明,和递增序列有关 | |
选择 排序 | 直接选择 | O(N) | O(N2) | O(N2) | O(1) | 不稳定 | |
堆排序 | O(N*log2N) | O(N*log2N) | O(N*log2N) | O(1) | 不稳定 | 实际效果并不如快排好!!! | |
交换 排序 | 冒泡排序 | O(N) | O(N2) | O(N2) | O(1) | 稳定 | 1、冒泡排序是一种用时间换空间的排序方法,n小时好2、最坏情况是把顺序的排列变成逆序,或者把逆序的数列变成顺序,最差时间复杂度O(N^2)只是表示其操作次数的数量级3、最好的情况是数据本来就有序,复杂度为O(n) |
快速排序 | O(N*log2N) | O(N*log2N) | O(N2) | O(log2n)~O(n) | 不稳定 | 1、n大时好,快速排序比较占用内存,内存随n的增大而增大,但却是效率高不稳定的排序算法。2、划分之后一边是一个,一边是n-1个,这种极端情况的时间复杂度就是O(N^2),在排序前手动随机排序即可避免。3、最好的情况是每次都能均匀的划分序列,O(N*log2N) 比较排序中实际最快的排序了,原因: 1.内循环中指令很少,且能利用缓存(具有更好的引用局部性:下一个要访问的对象通常与您刚刚查看的对象在内存中很接近。) 2.有优化方案:小数组切换为插入排序效果更好;大量重复元素时使用三向排序可将排序时间从现行对数级别降低到线性级别。 | |
归并排序 | O(N*log2N) | O(N*log2N) | O(N*log2N) | O(n) | 稳定 | 1、n大时好,归并比较占用内存,内存随n的增大而增大,但却是效率高且稳定的排序算法。 | |
基数排序 | O(2n) | O(d*2n) | O(d*2n) | O(n+k) | 稳定 | 当d=1,即都是个位数,最快O(2n) | |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 | k=max-min+1 | |
桶排序 | O(n) | O(n+k) | O(n^2) | O(n+k) | 稳定 | 时间复杂度: 1.桶数量足够大时,一次分配完事,不需要桶内部排序O(n)。 2.桶数量只有一个,都在一个桶内部排序,排序算法最差是O(n^2) |
=============
《算法第四版》