目录
正文
一、概述
作为一个合格的程序员,算法是必备技能,特此总结6大基础算法。java版强烈推荐《算法第四版》非常适合入手,所有算法网上可以找到源码下载。
PS:本文讲解算法分三步:1.思想2.图示3.源码4.性能分析
1.1 时间复杂度
算法的运行时间,在这里主要考量:比较和交换的成本。
1.2 空间复杂度
使用的内存,是否需要额外的存储空间,如果递归注意栈内存。
1.3 稳定性
相等元素排序前后相对位置保持不变,就认为是稳定的。
二、常见6种基础排序算法
2.1 通用函数准备
由于6种排序算法都用到一些相同源码,所以提炼出来作为一个父类,6种排序算法只需要集成这个类即可使用通用方法:
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.2 基础排序算法
目的:给长度为n的数组排序,从小到大排序。
2.2.1选择排序
思想:
最简单的排序,内外两遍循环。外循环简单遍历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(1)
是否稳定:否
2.2.2插入排序
思想:
类似扑克牌抓牌一样,一次插入一张牌保证当前牌有序。
从第二张牌开始,插入第一张牌;第三张牌插入前两张牌...一直到最后一张牌。插入牌时,两两比较,遇到逆序交换。
外循环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.2.3希尔排序
思想:
相较于插入排序,相邻交换次数较多。希尔排序的思想是使数组中任意间隔为h的元素有序。(h有序数组)这个h间隔就是“增量序列”,并且从最大的h开始排序然后h减小一直到1,数组间隔越来于小一直到1(即最少有间隔为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.2.4归并排序
思想:
将数组拆分为两部分(递归)分别排序,再将结果归并排序。
图示:
源码:
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.2.5快速排序
思想:
两向切分:取数组第一个元素作为切分元素,左边子数组全部小于等于切分元素,右边子数组全部大于等于切分元素,这样每切分一次就可以确定一个元素的位置。左右子数组再分别递归排序。
三向切分:对于数组中重复元素多的数组,更适合三向切分。也是第一个元素作为切分元素值=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 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.2.6堆排序
思想:
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)
是否稳定:否
三、总结
本文讲解六种基本排序算法,配以思想、图示、源码、分析,从4个角度来理解。要想吃透这些算法,特别是时间空间的复杂度,还得复习《算法导论》。这些算法很多都有改进版,但是是否真实有效,是否值得改进,又是另一说(例如现代计算机的cache命中机制、算法优化后难以理解且提升可能也只是适合特定的特征入参)。当然像还有什么冒泡排序、基数排序等等,这里未做分析。看完了算法,有一个例子可以来看看前辈们是如何使用算法的:jdk源码Arrays.sort(),后续会写一篇。总体就一句话,没有最优的算法,只有最适合特定场景的算法。 最后摘自网络上的一张表来总结下:
各种常用排序算法 | ||||||||
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | 特点 | ||
最好 | 平均 | 最坏 | 辅助存储 |
| 简单 |
| ||
插入 排序 | 直接插入 | O(N) | O(N2) | O(N2) | O(1) | 稳定 | 简单 |
|
希尔排序 | O(N) | O(N1.3) | 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小时好 |
快速排序 | O(N*log2N) | O(N*log2N) | O(N2) | O(log2n)~O(n) | 不稳定 | 复杂 | 1、n大时好,快速排序比较占用内存,内存随n的增大而增大,但却是效率高不稳定的排序算法。 实际最快的排序,原因: 1.内循环中指令很少,且能利用缓存(顺序地访问数据) 2.有优化方案:小数组切换为插入排序效果更好;大量重复元素时使用三向排序可将排序时间从现行对数级别降低到线性级别。 | |
归并排序 | O(N*log2N) | O(N*log2N) | O(N*log2N) | O(n) | 稳定 | 复杂 | 1、n大时好,归并比较占用内存,内存随n的增大而增大,但却是效率高且稳定的排序算法。 | |
基数排序 | O(d(r+n)) | O(d(r+n)) | O(d(r+n)) | O(rd+n) | 稳定 | 复杂 |
|
=============
《算法第四版》