到目前看完了选择排序, 插入排序(希尔排序), 冒泡排序, 这些都是遍历加遍历, 复杂度都是n平方级别的, 希尔排序稍微好一些.
现在来看看更快的排序, 这些排序或多或少都采取了不断二分的分治法策略, 所以时间复杂度都要比之前平方级别的
来看看这些更快的方法, 首先是归并排序.
归并排序的思想
归并排序的核心思想是:
- 将要排序的数组分成两部分, 对每一部分按相同的顺序排序
- 创建一个等于原来数组的总长度的新数组
- 对已经有序的两个子数组, 从两个子数组的开头比较两个子数组的所有元素, 哪个符合顺序(大于或者小于另外一个), 就把哪个元素放入到新的数组中. 直到一个数组的元素全部放入新数组中.
- 再把另外一个数组剩余的元素全部放入新数组中. 得到的新数组就是有序的.
很显然, 这个用递归的思路非常容易编写. 用迭代倒不是那么容易. 递归的思路很简单, 主要要写一个将两个有序数组合并成一个的方法.
不过这里还有一个问题要解决, 就是如果每次都创建一个新数组并且返回, 就会反复创建很多数组, 由于只需要创建一个新数组, 所以就创建一个长度和原来数组相同的数组, 然后每次都用这个数组的一部分做临时数组即可.
再考虑到以面向对象的方法编写, 因此还需要一个辅助方法, 即创建一个新数组供其他方法使用. 这样一共需要两个工具方法和一个排序方法:
private static <T extends Comparable<? super T>> void sort(T[] array, int startIndex, int endIndex, boolean reverse)
, 这个是和之前的排序类一样的核心私有排序方法, 内部使用其他方法.private static <T extends Comparable<? super T>> void sort(T[] array, int startIndex, int endIndex, T[] tempArray, boolean reverse)
, 这个方法是实际执行排序的方法, 多一个临时数组参数, 方便反复使用同一个临时数组.private static <T extends Comparable<? super T>> T[] merge(T[] array1, int array1StartIndex, int array1EndIndex, T[] array2, int array2StartIndex, int array2EndIndex, T[] tempArray)
, 这是将两个有序数组归并排序到一个临时数组上的对应位置的方法.private static <T extends Comparable<? super T>> T[] getTempArray()
, 这是新创建临时数组的方法.
由于需要在方法中间共用同一个临时数组, 而对外暴露的核心私有方法并没有临时数组参数, 所以需要在核心私有方法中创建一个临时数组变量, 然后套一层上边的第二个方法, 来让其中的各个递归使用相同的临时数组.
核心方法编写
按照方法逻辑来看一层一层看, 首先是最外层的私有核心方法:
private static <T extends Comparable<? super T>> void sort(T[] array, int startIndex, int endIndex, boolean reverse) { //先创建一个临时的数组 T[] tempArray = getTempArray(array.length); //交给其他私有方法进行使用, 保证只使用同一个临时数组 sort(array, startIndex, endIndex, tempArray, reverse); }
然后看这个使用临时数组的sort()方法, 这个方法是递归的思路所在:
private static <T extends Comparable<? super T>> void sort(T[] array, int startIndex, int endIndex, T[] tempArray, boolean reverse) {
checkArguments(array, startIndex, endIndex);
//停机条件是数组只有一项, 直接停止
//两个索引不相等, 就归并
if (startIndex != endIndex) {
sort(array, startIndex, (startIndex + endIndex) / 2, reverse);
sort(array, (startIndex + endIndex) / 2 + 1, endIndex, reverse);
merge(array, startIndex, (startIndex + endIndex) / 2, (startIndex + endIndex) / 2 + 1, endIndex, tempArray, reverse);
}
}
如果数字只有一项, 就停机. 如果数组超过一项, 用中间索引将其分成两半, 对每一半进行排序, 然后按照索引把排好序的两半数组归并起来.
之后看归并方法merge(), 这个方法是核心:
private static <T extends Comparable<? super T>> void merge(T[] array, int array1StartIndex, int array1EndIndex, int array2StartIndex, int array2EndIndex, T[] tempArray, boolean reverse) { //记录临时数组的起始索引, 由于后边归并的时候会更新两个数组各自的startIndex, 所以先要保存一个最小的索引 int tempArrayRangeStart = array1StartIndex; //获取临时数组的当前索引, 一开始等于第一个数组的起始索引, 后边会变动. int startIndex = array1StartIndex; //先遍历数组1的元素, 只要小于, 根据reverse判断是否升序 //只要两个数组有任意一个被遍历完就结束循环, 否则一直循环 while (array1StartIndex <= array1EndIndex && array2StartIndex <= array2EndIndex) { //在每次循环中不断比较两个数组的第一个元素, 哪个数组的第一个元素小, 就将那个元素放入临时数组, 然后将那个数组的当前索引和临时数组的当前索引都更新 //升序排列, 谁小先把谁放进temp中 if (!reverse) { //假如第一个数组的第一个元素小于第二个数组的第一个元素 if (array[array1StartIndex].compareTo(array[array2StartIndex]) < 0) { //将其放入临时数组中 tempArray[startIndex] = array[array1StartIndex]; //自增临时数组的当前索引和第一个数组的当前索引 startIndex++; array1StartIndex++; //如果不大于, 就把数组二的第一个元素放入临时数组, 自增数组二的当前索引和临时数组的当前索引 } else { tempArray[startIndex] = array[array2StartIndex]; startIndex++; array2StartIndex++; } //降序的思路和前边完全一样 } else { if (array[array1StartIndex].compareTo(array[array2StartIndex]) > 0) { tempArray[startIndex] = array[array1StartIndex]; startIndex++; array1StartIndex++; } else { tempArray[startIndex] = array[array2StartIndex]; startIndex++; array2StartIndex++; } } } //执行到这里, 两个数组中一定有一个数组已经被遍历完, 把没有遍历完的数组中剩余元素挨个放入临时数组中 //如果数组一没有被遍历完, 将剩余部分复制到临时数组中 if (array1StartIndex <= array1EndIndex) { for (int i = array1StartIndex; i <= array1EndIndex; i++) { tempArray[startIndex] = array[i]; startIndex++; } } //如果数组二没有被遍历完, 将剩余部分复制到临时数组中 if (array2StartIndex <= array2EndIndex) { for (int i = array2StartIndex; i <= array2EndIndex; i++) { tempArray[startIndex] = array[i]; startIndex++; } } //最后将临时数组的部分复制到原始数组中, 数组末尾一定是array2EndIndex, 而起始索引我们保存好了, 这样array的对应索引的部分就被归并排序好了. for (int j = tempArrayRangeStart; j <= array2EndIndex; j++) { array[j] = tempArray[j]; } }
最后是创建临时数组的方法, 这个方法有点意思, 由于数组协变, 不能直接创建泛型类型的数组, 这里有个小技巧, 由于T必定实现Comparable<T的父类>, 所以可以用一个套路:
@SuppressWarnings("unchecked")
private static <T extends Comparable<? super T>> T[] getTempArray(int length) {
return (T[]) new Comparable<?>[length];
}
如果用Object[], 是无法直接转型的, 因为Object[]并没有实现Comparable接口, 但是用Comparable是可以的, 因为T实现了这个接口.
关于泛型数组, 还真的要好好看看.
迭代版本的归并排序, 思路是从数组的开头, 先两两归并, 再四四归并, 再这样一路下去. 书上的练习还要求不能使用额外的临时数组, 待我仔细想想怎么做.