归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
归并排序是一种高效的排序算法,在任何情况下时间复杂度都nlogn 但是,它需要用额外的内存空间来暂时储存归并过程中的元素,因此我们可以认为归并排序是以牺牲一部分内存空间为代价来获得时间的高效性。
归并排序基本思想:
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
分解:将n个元素分成个含n/2个元素的子序列。
解决:用合并排序法对两个子序列递归的排序。
合并:合并两个已排序的子序列已得到排序结果。
归并排序可以使用不同方式进行切分:比如 以索引切分;以数组长度切分
以索引切分
解析:
1 对数组进行切分,mid = (left+right)/2;其中left初始化为0,right初始化为length-1
数组本分成[left--mid]=[8 4 5 7];[mid+1---right]=[ 1 3 6 2]俩部分
2 对left---mid部分进行递归,对mid+1---right部分进行递归
3 left---mid部分满足切分条件(left<right)继续被切分成[8 4] ;[5 7] 两部分
4 继续对3 的[8 4] 进行切分为[8] ;[4] .8 无法被切分了所以左部分递归结束;开始右半部分递归;右半部分为4无法被继续切分则让left=8;right=4 进行merge合并成[4 8]
5 当4中递归完成,右半部分[5 7] 开始执行递归 。重复3-4步骤合并后为[5 7]
6 步骤5递归结束开始合并[4 8] ; [5 7] 这两部分。合并后为[4 5 7 8]
7 [left--mid]=[8 4 5 7] 结束,[mid+1---right]=[ 1 3 6 2]开始递归与合并。重复2--6
得到最终的结果[1 2 3 4 5 6 7 8]。
8 每部分对应的数组索引 左半部分 [0 1] --[2 3] --[0 3] 右半部分[4 5]---[6 7]---[4 7]
最终合并[0 7] 可以看出8个数最终合并7次。则n个数合并n-1次;
分的详细流程
合并的详细流程
//以数组索引切分归并排序
public static int[] mergeSortByIndex(int[] arr){
var temp = new int[arr.length];
return mergeSort(arr,temp,0, arr.length-1);
}
private static int[] mergeSort(int[] arr,int[]temp, int left,int right){
if(left<right){
var mid = (left+right)/2;
if (mid<=10){// 优化
return insertSorts(arr,left,right);
}
mergeSort(arr,temp,left,mid);//左边归并排序,使得左子序列有序
mergeSort(arr,temp,mid+1,right);//右边归并排序,使得右子序列有序
if (arr[mid] > arr[mid+1]) {//优化:如果middle后面有序则无需再合并
return mergeByIndex(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
return arr;
}
private static int[] mergeByIndex(int[] arr,int left,int mid,int right,int[] temp){
var i = left;
var j = mid+1;
var tem= 0;
//left和right两边的有序序列,拷贝到temp[],直到一方处理完毕为止
while (i<=mid && j<=right){
temp[tem++] = arr[i] <= arr[j] ?arr[i++] : arr[j++];
}
if(i<=mid){//将左边left剩余元素填充进temp中
System.arraycopy(arr,i,temp,tem,mid-i+1);
}
if(j<=right){//将右序列right剩余元素填充进temp中
System.arraycopy(arr,j,temp,tem,right-j+1);
}
//将temp中的元素全部拷贝到原数组中
System.arraycopy(temp,0,arr,left,right-left+1);
return arr;
}
以数组长度切分
//以数组长度切分归并排序
public static int[] mergeSortByLength(int[] arr ){
if (arr.length<=2){//如果当前arr长度是2直接比较无需归并
if (arr.length==2&&arr[0]>arr[1]){
arr[0] = arr[0]^arr[1];
arr[1] = arr[0]^arr[1];
arr[0] = arr[0]^arr[1];
}
return arr;
}
var middle=arr.length>>1;
if (middle<=10){// 优化:如果不超过10启用插入排序
return insertSorts(arr,0,arr.length-1);
}
var leftArray= Arrays.copyOfRange(arr, 0, middle);
var rightArray= Arrays.copyOfRange(arr, middle, arr.length);
return merge(arr,mergeSortByLength(leftArray),mergeSortByLength(rightArray));
}
//将两个有序数组合并,合并后数组仍然有序
private static int[] merge(int[] arr, int[] left, int[] right ){
var i = 0;
var tem=0;
var j=0;
while (i< left.length &&j< right.length ) {
arr[tem++] = left[i] <= right[j] ?left[i++] : right[j++];
}
if(i== left.length){//说明left已经放完,right存在没有放完的元素
System.arraycopy(right,j,arr,tem,right.length-j);
}
if(j== right.length){//说明right已经放完,left存在没有放完的元素
System.arraycopy(left,i,arr,tem,left.length-i);
}
return arr;
}
归并排序非递归
正向切分,自顶向下,直到无法grep>=len无法切分位置
思想:
将数组中的相邻元素两两配对。将他们排序,构成n/2组长度为2的排序好的子数组段,然后再将他们合并成长度为4的子数组段,如此继续下去,直至整个数组排好序
//非递归归并排序-正向切分,自顶向下,直到无法grep>=len无法切分位置
public static int[] mergeSortByGroup(int[] arr){
var len= arr.length;
//将数组中的相邻元素两两配对。将他们排序,构成n/2组长度为2的排序好的子数组段,然后再将他们合并成长度为4的子数组段,如此继续下去,直至整个数组排好序
//步长的改变,步长grep=1, [left=0*grep,left+2*grep-1];[left=2*grep,left+2*grep-1];[left=4*grep,left+2*grep-1].....
//分成len组,每组1个数
//步长为grep=2 [left=0*grep,left+2*grep-1];[left=2*grep,left+2*grep-1];[left=4*grep,left+2*grep-1]
//分成len/2组 每组有2个数
//步长为grep=4 [left=0*grep,left+2*grep-1];[left=2*grep,left+2*grep-1];[left=4*grep,left+2*grep-1]
//分成len/4组 每组有4个数
//每次步长均为原来的2倍即 1 2 4 8 16....直到为length为止
var temp = new int[arr.length];
for (var grep = 1; grep < len; grep*=2) {
for (var left = 0; left<len; left+=grep*2 ) {
var middle = Math.min(left + grep - 1, arr.length-1);
var right = Math.min(left +2* grep -1, arr.length-1);
// 优化--如果中间值小于中间值的下一个,说明当前序列是从小到大有序的无需再归并
if (arr[middle] > arr[Math.min(middle+1,arr.length-1)]) mergeByIndex (arr,left, middle,right, temp );
}
}
return arr;
}
使用多线程的归并排序
package Algorithm.Sort;
import java.io.Serial;
import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
/**
* @author: Jerssy
* @create: 2021-03-17 17:44
* @version: V1.0
* @slogan: 业精于勤, 荒于嬉;行成于思,毁于随。
* @description: 多线程模式下归并排序
*/
public class ThreadMergeSort extends RecursiveAction {
public static void main(String[] args) {
int[] arr = new int[8000];
for (int i = 0; i <8000; i++) {
arr[i]= (int) (Math.random() * 8000);
}
long start=System.currentTimeMillis();
System.out.println(Arrays.toString(threadMergeSort(arr)));
long end= System.currentTimeMillis();
System.out.println("多线程归并排序消耗时间:"+(end-start));
}
@Serial
private static final long serialVersionUID = -4369231251235987766L;
private final int[] meargeArr;
public ThreadMergeSort(int[] meargeArr) {
this.meargeArr = meargeArr;
}
@Override
protected void compute() {
if (meargeArr.length<=2){
if (meargeArr.length==2&&meargeArr[0]>meargeArr[1]){
meargeArr[0] = meargeArr[0]^meargeArr[1];
meargeArr[1] = meargeArr[0]^meargeArr[1];
meargeArr[0] = meargeArr[0]^meargeArr[1];
}
return ;
}
int middle=meargeArr.length>>1;
int[] leftArray= Arrays.copyOfRange(meargeArr, 0, middle);
int[] rightArray= Arrays.copyOfRange(meargeArr, middle, meargeArr.length);
invokeAll(new ThreadMergeSort(leftArray), new ThreadMergeSort(rightArray));
merge(leftArray, rightArray,meargeArr);
}
public static int[] threadMergeSort(int[] arr){
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new ThreadMergeSort(arr));
return arr;
}
//将两个有序数组合并,合并后数组仍然有序
private static void merge(int[] left, int[] right,int[] temp){
int i = 0;
int tem=0;
int j=0;
while (i< left.length &&j< right.length ) {
if (left[i]<= right[j]){//比较left元素与right元素大小
temp[tem++] = left[i++];
}
else {
temp[tem++] = right[j++];
}
}
if(i== left.length){//说明left已经放完,right存在没有放完的元素
System.arraycopy(right,j,temp,tem,right.length-j);
}
if(j== right.length){//说明right已经放完,left存在没有放完的元素
System.arraycopy(left,i,temp,tem,left.length-i);
}
}
}
测试效率
同样8000个数:
可以看出非递归效率要高,毕竟频繁重复递归开辟栈空间需要消耗时间