归并排序
前言
本篇文章是排序算法系列的第四篇,学习归并排序
后面这段话将作为排序算法系列博客每一篇的开头:
为避免文中过多赘述,写在最前面:
- 接下来所有的排序算法讲解中,无论是思路梳理,还是代码实现,都是最终实现从小到大排序,从大到小可以学会后自行类推。
- 都是使用int数组进行排序,数据总量为n
归并排序
核心理念
归并排序核心思想是分治,分治法顾名思义,分开来治理,首先将一个大问题拆分成小问题,小问题逐个治理解决,最终再合起来完整的解决大问题
这里就直接说了,将一整个数组排序的问题,归并排序的思想是拆分成两个有序数组的合并问题。
我当初看到这里就马上暂停了一下,自己去想,怎么才能把一个数组排序问题,拆分成两个有序数组的合并,想了很久也没想到,感觉怎么拆也不能保证都是有序的,接着往下看了,然后晕倒!当持续的拆下去,拆成左右两个数组都只剩一个数据,一个数据当然是有序的,它们就全是有序的了......
到这里一下就豁然开朗了,先一步步拆到最底层再退回来合成到最顶层,思路就来了,首先将数组一分为二,此时的左右两边数组还不是有序的,我们要把他们都变成有序的,再重新将两个数组当做新数组继续一分为二,直到数组中只有一个数字为止,再分别将两个一个数据的数组合成一个两个数据的有序数组,再将两个两个数据的有序数组合成一个四个数据的有序数组,一直到合成回顶层。
实现思路
每一次拆分其实都是四个步骤:
- 判断当前数组长度是否大于1,不大于1就结束
- 大于1就将数组对半拆成左右两个数组
- 左右数组也从第一步开始执行这四个步骤
- 将左右数组执行有序数组合并(此时的左右数组,因为第三步的存在,一层一层进去又出来,已经是有序的了)
到这里,分治中,分的过程就通过递归达成了目的,但这个第四步还需要进一步去深入一下,将有两个序数组合并成一个有序数组,步骤如下:
- 准备一个长度是两数组长度和的数组,临时存放合并后的结果,用一个指针执行数组第一个位置;
- 用两个指针分别指向两个数组的第一个元素;
- 如果两个指针都没有越界就进入循环,比较两个指针处数据的大小,将小的一个放入临时数组的指针位置,临时数组指针后移,小的一个指针后移,然后再次判断是否两个指针都没有越界,进入下一次循环
- 直到其中一个指针越界了,说明其中一个有序数组全部放到临时数组中了,因为两个数组都是从小到大排列的有序数组,此时只需要将另一个数组中所有剩余的元素依次添加到临时数组中即可
- 完成上述四步,临时数组中就是合并后的结果,将它存回原来拆开的两个数组中就完成了一次合并
思路有了,大家可以试着自己写完再来看代码:
package com.zwx.sort;
/**
* 归并排序
*
* @author coderZWX
* @date 2022-06-13 16:37
*/
public class MergeSort {
public static void mergeSort(int[] arr){
//由于最后一次合并,需要的临时数组长度就是原数组的总长度,所以直接只创建一个最大的数组作为临时数组全程使用
MergeSort.mergeSort(arr,0,arr.length-1,new int[arr.length]);
}
private static void mergeSort(int[] arr, int start, int end, int[] temp){
//如果数组段中的数据大于等于2个,就分割成左右两边
if (start < end){
//中间数索引(左边的终点)
int mid = (start + end) / 2;
mergeSort(arr, start, mid, temp);
mergeSort(arr, mid + 1, end, temp);
//无论是否需要再次分割,分割完毕或不分割,都执行一次合并操作
merge(arr,start,mid,end,temp);
}
}
/**
* 将一个数组段,按(start+end)/2为左边终点索引,分开为左右两个数组,已知左右两边分别有序(从小到大),合并为全部有序(从小到大)
* @param arr 大数组
* @param start 起点索引
* @param end 终点索引
* @param temp 临时数组
*/
private static void merge(int[] arr, int start, int mid, int end, int[] temp) {
//左起点为start,左终点为mid,右起点为mid+1,右终点为end
//左指针
int lPointer = start;
//右指针
int rPointer = mid + 1;
//临时数组指针
int t = 0;
//当左右指针都没越界比较左右两个指针指向数据的大小,谁小就把谁放在临时数组指针位置,并将指针后移一个
while (lPointer<= mid && rPointer<= end){
if (arr[lPointer]<arr[rPointer]) {
temp[t++] = arr[lPointer++];
}else {
temp[t++] = arr[rPointer++];
}
}
//当某个指针没越界,剩余的都放入临时数组
while (lPointer<= mid){
temp[t++] = arr[lPointer++];
}
while (rPointer<= end){
temp[t++] = arr[rPointer++];
}
//将排好序的temp放回arr的start到end
int index = start;
t = 0;
while (index<=end){
arr[index++] = temp[t++];
}
}
}
测试对比
写这篇文章的时候,和之前三篇不在同一个电脑环境测试,但我们知道之前篇章中,最快的是快速排序,这里先运行了最快的交换法快速排序,随机散列的800w数据排序,大约1100ms左右,这里的归并排序在1300ms左右,还是路逊与快速排序
但是在选择排序算法的时候,并不能无脑快速排序,各个排序都有优缺点,日常中有排序需求的时候,800w随机分散在0-8000w整数的非常散列的数据排序是很少见的,重点是理解这些排序的思想,扩宽思路,真正排序还需要具体情况具体分析。
jdk源码中,Arrays.sort()方法,也使用到了归并排序,但暂时不去看,后续介绍完其他排序算法,再来开章节阅读jdk源码中的排序