归并排序(mergeSort)介绍
归并排序是一种高级排序算法,速度仅次于快速排序,为稳定排序算法,没有对应的简单排序算法。
思路推演:
1、想法应该是从发现两个有序数组合成一个有序数组是O(n)开始的,如何将两个有序数据合成一个有序数组?依次从两个数组中取数进行比较,将较小的放入新数组。
准备两个索引i=0和j=0,逐个比较arrA[i]和arrB[j],将较小值给新数组,并将较小值所在数据的索引++,一旦某个索引++之后超出了数组的长度,说明这个数组全部放过去了,这时就可以将另一个数组余下值按次序放入新数组。
一次循环比较即可完成,上面的逻辑的时间复杂度是O(n)
2、假设将一个大数组,一分为2,二分为4,,,最终拆分成最多只有两个元素的数组,将这两个元素的数组比较一次,排个序。然后逐个顺序合并起来,就是归并排序。
时间复杂度:
O(log(n))
拆开数组,O(logn),2个元素的数组每个比较一次O(1)*n/2 = O(n/2),合并的时候会每一次合并是n,一共logn层,所以合并是O(nlogn),总时间是O(nlogn) + O(n/2) + O(logn),去除低次的就是O(nlogn)的时间复杂度,空间复杂度看怎么用,可以在每一次合并的时候创建一个temp数组,这样需要从2+ 4+ 8+ …+ n个长度的数组,也就是O(n^2)的空间。也可以从头到尾使用一个temp数组,在一开始的时候根据arr的length创建,带着走完整个算法,根据起止位置,分段取用。
稳定性:
稳定排序算法
属于相邻比较,交换,所以可以实现为稳定的。是否稳定只是说能不能实现稳定,假设对于两个元素的数组排序时,采用前面大等于后面则交换,那就是不稳定的,采用前面大于后面才交换,就是稳定的。
高级算法唯一能实现为稳定算法的。
代码思路:
这个代码生生去想,很难直接写出来,掉了不少次坑才写出来。
1、前面说的拆分成最多2个长度的数组和后续的比较合并,其实都可以在一个数组内完成,借助递归,一分为二、二分为四,也就是每次要从中间分开(当然也可以不从中间分开,哪怕我们强行分成前面2个元素和后面所有元素也可以,但是二分能达到logn。所以要理解算法,而不是死记硬背)。
2、传入开始index和结束index
3、判断startIndex是否小于endIndex,递归应该结束的情况是endIndex - startIndex == 1或者等于0,(最后发现这个其实也可以放到递归里,但是那是优化之后的,一开始按照最原始的算法思路来实现)
4、如果endIndex-startIndex > 1(比如0 1 2),那么数组就是可以继续拆分的,继续拆成前半部分和后半部分,将前半部分和后半部分分好后,merge成一个大数组。
5、伪代码
// 为了方便运算,endIndex第一次传入为(数组长度-1)。
mergeSort(arr, startIndex, endIndex){
if(startIndex != endIndex){
if(endIndex - startIndex == 1){
// 将这个最小数组交换
}else{
int midIndex = (startIndex + endIndex) >> 1;
//前半部分startIndex, midIndex
//后半部分midIndex + 1, endIndex
//合并
}
}
}
//
mergeArray(arr, startIndex, midIndex, endIndex){
// 根据这三个确定两个相邻的数据段,认为是两个数组,进行数据整合
// 这个时候每次需要新建一个长度为(endIndex - startIndex + 1)的temp数组,才方便合并,或者最开始新建一个大的长度为原始数组长度的temp数组,然后一直带着走。理论上后者效率更高一些。
}
Java代码实现:
public static void main(String[] args){
int[] arr = new int[]{10, 1, 4, 2, 8, 3, 5};
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 作为一个引子,因为下面的方法需要用到递归,这样写好看一写,也可以直接写到main方法中
* @param arr
*/
private static void mergeSort(int[] arr){
doMergeSort(arr, 0, arr.length - 1);
}
/**
* 递归 1 3 2 4
* @param arr
* @param startIndex
* @param endIndex
* @return
*/
private static void doMergeSort(int[] arr, int startIndex,int endIndex){
// 等于当前数组的前半部分和后半部分先mergeSort再mergeArray
// 前半部分和后半部分长度小于等于2(差小等于1)的时候,分拆的递归结束,开始往回收拢,逐步合成一个大数组
if(startIndex < endIndex){
// 找到中间位置
int midIndex = (startIndex + endIndex) >> 1;
doMergeSort(arr, startIndex, midIndex);
doMergeSort(arr, midIndex + 1, endIndex);
mergeArray(arr, startIndex ,midIndex,endIndex);
}
/*if(startIndex != endIndex){
// 如果间隔是1,进行排序,如果间隔是0,不排序,如果间隔大于1继续分拆
if(endIndex - startIndex == 1){
if(arr[startIndex] > arr[endIndex]){
int tmp = arr[startIndex];
arr[startIndex] = arr[endIndex];
arr[endIndex] = tmp;
}
}else{
int midIndex = (startIndex + endIndex) >> 1;
doMergeSort(arr, startIndex, midIndex);
doMergeSort(arr, midIndex + 1, endIndex);
mergeArray(arr, startIndex ,midIndex,endIndex);
}
}*/
}
/**
* 假设数组为1 3 2 4或者 1 2 4
* 代码难以直接写出来的时候,举几个简单的例子激发一下思路
* @param arr
* @param startIndex
* @param midIndex
* @param endIndex
* @return
*/
private static void mergeArray(int[] arr, int startIndex, int midIndex,int endIndex){
//
int len = endIndex - startIndex + 1;
int[] temp = new int[len];
int i = startIndex;
int j = midIndex + 1;
int tempIndex = 0;
// temp用来简化操作,先放到temp中,再全部丢到arr中
// 直到一方走完
while(true){
if(arr[i] <= arr[j]){
temp[tempIndex] = arr[i];
i++;
}else{
temp[tempIndex] = arr[j];
j++;
}
tempIndex++;
if(i > midIndex){
// j中剩下的不动,arr中的也不用动
break;
}
if(j > endIndex){
// i中剩下的需要全部移动到temp中国
// 剩下的个数为m = midIndex - i + 1
// 剩下的全部填满(也就是填满是len - m)
for(int z = i; z <= midIndex;z++){
temp[len - (midIndex - z + 1)] = arr[z];
tempIndex++;
}
break;
}
}
// merge过来,一直merge到startIndex + tempIndex,因为temp的有效数据到tempIndex(不包括tempIndex)
for(i = startIndex; i < startIndex + tempIndex; i++){
arr[i] = temp[i - startIndex];
}
}
使用一个temp数组的写法
// 使用一个temp的归并排序
public static void main(String[] args) {
int[] arr = new int[]{10, 8, 5, 1, 4, 2, 3};
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void mergeSort(int[] arr) {
int[] temp = new int[arr.length];
doMergerSort(arr, 0, arr.length - 1, temp);
}
private static void doMergerSort(int[] arr, int startIndex, int endIndex, int[] temp) {
if (startIndex < endIndex) {
// 如果等于1,递归结束
// 比较最小的元素,但是比较最小的元素,也可以看成两个长度为1的数组的合并,所以逻辑可以写到最后,直到startIndex == endIndex
// if(endIndex - startIndex == 1){
// }else{
// }
// 将左边进行mergeSort
// 将右边进行mergeSort
// 合并左右两边
int midIndex = (startIndex + endIndex) >> 1;
doMergerSort(arr, startIndex, midIndex, temp);
doMergerSort(arr, midIndex + 1, endIndex, temp);
mergeArray(arr, startIndex, midIndex, endIndex, temp);
}
}
/**
* 合并数组
*
* @param arr
* @param startIndex
* @param midIndex
* @param endIndex
* @param temp
* @return
*/
private static void mergeArray(int[] arr, int startIndex, int midIndex, int endIndex, int[] temp) {
// temp数组的索引,从startIndex到最终停止的位置的数据将会被复制到arr
int tempIndex = startIndex;
// 左边数组起始位置
int i = startIndex;
// 右边数组起始位置
int j = midIndex + 1;
// 从两个数组中逐个取第一个未被选定的数进行比较,取较小值放入temp中
while (true) {
// 为保证稳定性,前面小于等于后面
if (arr[i] <= arr[j]) {
temp[tempIndex] = arr[i];
i++;
} else {
temp[tempIndex] = arr[j];
j++;
}
tempIndex++;
// 如果i超出了边界,tempIndex会停留在j中的某个位置,不需要复制,循环停止
if (i > midIndex) {
break;
}
// 如果j超出了边界,需要将i中剩余的数据,复制到对应的位置
if (j > endIndex) {
// 复制数据
for (; i <= midIndex; tempIndex++, i++) {
temp[tempIndex] = arr[i];
}
break;
}
}
// 将temp中的数据复制到arr中,上面每次都是tempIndex++然后再退出循环,也就是tempIndex多加了1,需要注意边界
for (; startIndex < tempIndex; startIndex++) {
arr[startIndex] = temp[startIndex];
}
}