归并排序详解(包括递归和非递归的版本)

138 阅读1分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

归并排序介绍

在讲归并排序之前,我们先了解一下什么是归并。

我们先来看看这个题目:合并两个有序数组

这个题目是很简单的,我们只要创建一个数组ret【】,长度是两个数组长度的和。

然后创建两个变量,cur1 、cur2

这两个变量分别指向了两个数组的第一个元素,

然后比较他们所指向元素的大小,小的那个就添加到我们的ret里面,然后指向下一个地方。

就这样一直循环,直到有个变量指向了数组的最后一个。

这个时候跳出循环,看看是哪个数组没有被用完,然后弄个循环,把变量送到我们创建的那个数组里面去。

public static int[] merge(int[] arr1 , int[] arr2){
        int len = arr2.length + arr1.length;
        int[] ret = new int[len];
        int cur1 = 0;
        int cur2 = 0;
        int index = 0;
        while(cur1 < arr1.length || cur2 < arr2.length){
//            这边我这样子做是为了省去一个循环
            int num1 = cur1 < arr1.length ? arr1[cur1] : Integer.MAX_VALUE;
            int num2 = cur2 < arr2.length ? arr2[cur2] : Integer.MAX_VALUE;
            //这个是判断两个元素的大小,并让较小的cur往后走一步。
            if(num1 < num2){
                ret[index++] = num1;
                cur1++;
            }else{
                ret[index++] = num2;
                cur2++;
            }
        }
        return ret;
    }

上面的代码就是将两个有序的数组变成一个有序的数组。

那我们解决了上面的那个问题之后,对我们解决归并算法有什么帮助呢?

那我们想想,把一个数组分开来看,是不是一个又一个的小数组组成的。

我们看这个数组

1,5,9,8

我们可以看到,这个数组可以分为{1}、{5}、{9}、{8}

那我们让他们两两合并看看

{1,5}、{8,9}

我们继续合并

{1,5,8,9}

那么这个就是归并排序。

那我们怎么能做到把元素变成一个又一个单独的小个体呢?

变成小个体之后怎么能让他们再集合起来呢?

这个当然是很简单的。我们只要用到递归就可以了。之后再介绍不用递归的方法。

那我们是怎么借用递归来让他们变成一个又一个的单独体呢?

public static void process(int[] arr, int L, int R) {
    if (L == R) {
        System.out.println(arr[L]);
    }
    int mid = L + ((R - L) / 1);
    process(arr, L, mid);          //递归调用自己的左边
    process(arr, mid + 1, R);      //递归调用自己的右边,这边就是把一个数组分为了两半,然后继续分。
}

我们这样子就可以解决了,我们有一个左边的变量,有一个右边的变量,然后让他们相互靠近,之后l == r的时候就是单独的一个了。

当我们得到单独的一个又一个的时候,我们只要在里面添加merge就可以了。

public static void process(int[] arr , int L , int R){
    if(L == R){
        return;
    }
    int mid = L + ((R-L)/2);
    process(arr , L , mid);
    process(arr , mid + 1 , R);
    merge(arr , L , mid , R);
}
public static void merge(int[] arr , int l , int m , int r){
    int[] ret = new int[r - l + 1];
    int cur1 = l;
    int cur2 = m + 1;
    int index = 0;
    while(cur1 <= m || cur2 <= r){
        int num1 = cur1 <= m ? arr[cur1] : Integer.MAX_VALUE;
        int num2 = cur2 <= r ? arr[cur2] : Integer.MAX_VALUE;
        if(num1 <= num2){
            ret[index++] = num1;
            cur1++;
        }else{
            ret[index++] = num2;
            cur2++;
        }
    }
    for(int i = 0;i<ret.length;i++){
        arr[i + l] = ret[i];
    }
}

这边我把merge也顺带放出来了,其实就和最上面的差不多,那这个时候我们就可以写一个函数来调用这个process然后归并排序的非递归版本就完成了。

public static void mergeSort1(int[] arr){
    if(arr == null || arr.length < 2){
        return;
    }
    process(arr , 0 , arr.length - 1);
}

在我们实际开发的时候其实是很回避递归这个东西的,为什么呢?

因为这个会调用系统栈,在调用自己的时候,栈会单独开辟一个空间,那我们递归到最小的部分的时候,其实有可能开了好多的栈了,就导致了效率比较低,那我们现在来弄一个非递归版本

我们想想,上边的递归版本是为什么递归的,是不是因为切割最小部分的时候用到了递归,那我们能不能用循环来做到这个呢?

其实是可以的,你可以发现,我们在合并的时候其实是2的倍数。

原本都是一个单独的个体,然后两两在一起,这个时候个体集合里面有两个元素,这个时候再两两集合,这个时候一个集合里面有四个,这样持续下去就可以了,那我们也可以在循环里面弄个这样的变量,先是1,后面是2,后面是4,来控制哪些可以在一起。

我们先定义一个tep来记录应该有几个元素在一个单独的集合里面,

然后我们不能让这个tep过大,最大只能是数组长度,这个当做循环的结束条件

这个时候我们定义一个l来控制左边的内容,我们同时也要控制变量的大小,不能让数组溢出,l是一个集合的最左侧,那么我们得让中间部分不能溢出,那么我们怎么能控制右边呢?其实是有方法的,我们后面解答。

这个时候我们的l + tep是不能炒股数组的长度的,我们还要一个循环,然后在这个循环里面操作merge。

那有的人会问了,两个循环,那不是O(n^2)了吗?

其实不是的,因为我们的tep是每次乘2上升的,所以时间复杂度不是O(n^2)

我们上面已经解决了溢出的问题,那我们要确定中间的位置了,我们这边的mid = l + tep - 1,这个减一是为了遇到偶数的时候,我们选择的是上中位数。

那我们的右边怎么操作呢?我们上边只是保证了中间不会溢出,那右边怎么办呢?

咱们这样子解决

int r = Math.min(mid + tep , N - 1);

我们让r在mid + tep 和 N - 1中间选一个(N是数组的长度)。

当我们左中右都弄好的时候,我们就可以调用merge函数了。

当我们最里面的循环结束之后,tep就可以乘2

我们来看看代码是怎么样的。

public static void mergeSort2(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int tep = 1;
    int N = arr.length;
    while(tep < N){
        int l = 0;
        while(l + tep < N){
            int mid = l + tep - 1;
            int r = Math.min(mid + tep , N - 1);
            merge(arr , l , mid , r);
            l = r + 1;
        }
        if(tep > N / 2){
            break;
        }
        tep *= 2;
    }
}

里面有一段代码是要解读一下的

if(tep > N / 2){
        break;
}

这个代码是为了防止溢出Integer。

如果N是Integer最大限度的一半多的时候,那我们如果让tep乘2,就很有可能会溢出。