前置:
-
为了后续测试方便,定义了
IMutableSorter
和IImutableSorter
两种接口IMutableSorter
:不改变原来集合IImutableSorter
:返回一个新的集合
-
测试类,可通过反射测试对应的排序
public class MySortTest { public void sortTest(Class clazz, int N) { try { Constructor constructor = clazz.getConstructor(); Object rawInst = constructor.newInstance(); long start = System.currentTimeMillis(); //如果该该算法是可变对象(返回一个新的集合) if (rawInst instanceof IImutableSorter) { List<Integer> A = gen(N); if (N < 100) { System.out.println("排序前:" + A.toString()); } IImutableSorter inst = (IImutableSorter) rawInst; A = inst.sort(A); assertSorted(A); if (N < 100) { System.out.println("排序后:" + A.toString()); } System.out.println("用时:" + (System.currentTimeMillis() - start) + "ms"); } //不可变对象(不改变原来的集合) else if (rawInst instanceof IMutableSorter) { int[] ints = gen(N).stream().mapToInt(x -> x).toArray(); if (N < 100) { System.out.println("排序前:" + Arrays.toString(ints)); } IMutableSorter inst = (IMutableSorter) rawInst; inst.sort(ints); assertSorted(Arrays.stream(ints).boxed().collect(Collectors.toList())); if (N < 100) { System.out.println("排序后:" + Arrays.toString(ints)); } System.out.println("用时:" + (System.currentTimeMillis() - start) + "ms"); } } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } } /** * 校验集合是否有序 * * @param A */ private void assertSorted(List<Integer> A) { Integer o = Integer.MIN_VALUE; for (Integer i : A) { if (o > i) { System.out.println(A.toString()); Assert.fail("Array not in sorted order"); } o = i; } } /** * 生成一个数量为N的随机集合 * * @param n * @return */ private List<Integer> gen(int n) { List<Integer> A = new ArrayList<>(); Random random = new Random(); for (int i = 0; i < n; i++) { A.add((random.nextInt(50))); } return A; } }
-
交换类,在排序算法中会用到不少交换的地方,因此抽取出一个工具类:
//在集合A中,将i与j下标的元素互换 public class Helper { static void swap(int[] A, int i, int j){ int temp = A[i]; A[i] = A[j]; A[j] = temp; } }
-
知识回顾:流式计算
新时代的程序员(Java1.8+):lambda表达式、链式编程、函数式接口、Stream流式计算
详见文章:JAVA流式计算
在
MySortTest
中的流式计算:-
将
List<Integer>
转换成int[]
int[] ints = gen(N).stream().mapToInt(x -> x).toArray();
-
将
int[]
转换成List<Integer>
int[] ints = gen(N).stream().mapToInt(x -> x).toArray();
-
-
后续有涉及流式计算再做总结
循环不变式
- 最直观的排序,可以看到:有序的集合+1,无序的集合-1
插入排序
-
类似扑克牌中的抓牌码牌,将桌面的牌逐一放到手上码齐
-
程序框架
-
实现:
public class InsertionSort implements IMutableSorter { @Override public void sort(int[] A){ for(int i = 1;i<A.lengrh;i++){ //获取对应的牌 int tmpCard = A[i]; //记录当前位置 int j = i; //进行比较交换,得从第二张牌开始比较 for(;j>0 && A[j-1] > tmpCard;j--){ //前一张牌后移至后一张,依次比较 A[j] = A[j-1]; } //A[j-1] > tmpCard == false || j == 0,比到头了,进行临时牌交换 A[j] = tmpCard; } } }
-
手动Debug示意图:以集合
[10,13,9,5,11]
为例子: -
验证
@Test public void testInsertSort() { sortTest(InsertionSort.class, 10); }
-
小节:
- 在算法中进行了两轮的for循环,因此平均的时间复杂度为
O(n²)
- 同时时两两比较,也就是说原来集合的元素相对位置并不会改变,因此是稳定的算法
- 在算法中进行了两轮的for循环,因此平均的时间复杂度为
选择排序
-
选择和冒泡排序有些异曲同工之妙:都是在集合的0~i之间找出最大的元素放在最后,并不断重复遍历,完成排序
-
程序框架
达到每完成一次遍历达到【有序+1,无序-1】的效果
-
实现:
public class SelectionSort implements IMutableSorter { @Override public void sort(int[] A) { //个人习惯:左闭右开 for (int i = A.length - 1; i >= 0; i--) { //找出当前集合最大的元素的下标 //进行交换 int j = maxIndex(A, 0, i); Helper.swap(A, i, j); } } private int maxIndex(int[] A, int i, int j) { //用minValue 假定一个最大值 int max = Integer.MIN_VALUE; int maxIndex = j; //集合最后面开始 for (int k = j; k >= i; k--) { if (max < A[k]) { max = A[k]; maxIndex = k; } } return maxIndex; } }
-
手动Debug
A: [ 11 , 15 , 12 , 3 ,4 ] STEP1: 找出··[ 11 , 15 , 12 , 3 ,4 ]··的最大值与最后交换 [ 11 , 4 , 12 , 3 ,15 ] STEP2: 找出··[ 11 , 4 , 12 , 3··的最大值与最后交换 [ 11 , 4 , 3 , 12 ,15 ] STEP3: 找出··[ 11 , 4 , 3 ··的最大值与最后交换 [ 3 , 4 , 11 , 12 ,15 ] STEP4: 找出··[ 3 , 4 ··的最大值与最后交换 [ 3 , 4 , 11 , 12 ,15 ]
-
验证
-
小节:
-
在算法中进行了两轮的for循环,因此平均的时间复杂度为
O(n²)
-
在交换位置的时候会找到最大的maxIndex与末位/首位 角标的元素进行互换,所以是一个不稳定的排序算法
-
冒泡排序
-
简单来说:
- 选择排序就是在一次遍历中:选择到某一范围内具体的最大值,与集合末尾/首位元素进行交换
- 而冒泡排序在一次遍历中就是不断地进行着两两比较,满足则交换,直至遍历完成为止
-
框架同选择排序函数一致
-
实现:
public class BubbleSort implements IMutableSorter { @Override public void sort(int[] A) { for(int i = 0; i < A.length -1 ; i++){ for(int k = 0; i < A.length -1 -i; k++){ if(A[k] > A[k +1 ]){ Helper.swap(A,j,j+1); } } } } }
-
由于与选择排序过于相似,因此就不进行手动debug,读者们有兴趣可自行debug
-
验证
当然验证也是没问题的,不过值得一提的是,当N=100000时,选择排序和冒泡排序的执行效率相差较远:
-
选择:
-
冒泡:
-
其中最主要的差别还是在swap的时候进行了内存读写的操作。
-
-
小节:
- 在算法中进行了两轮的for循环,因此平均的时间复杂度为
O(n²)
- 同时时两两比较,也就是说原来集合的元素相对位置并不会改变,因此是稳定的算法
- 在算法中进行了两轮的for循环,因此平均的时间复杂度为