快速搞定基础排序算法 —— 归并排序

归并排序

今天开始搞归并排序。归并排序是借助于归并操作来实现快速排序的算法。但归并操作有个前提,即合并的两个子序列本身必须是有序的。所以实现归并排序时的第一步就是对无序数组做切割,切成一个个的有序子序列;第二步再对这些生成的有序子序列两两进行合并,即归并操作。第三步,重复第二步。针对合并过的序列继续归并,直到全部合并完成。至此就完成了归并排序操作。

归并排序可以说是分治法的典型应用。先切割数组,再对切割后的子串层层递归合并,每个子串合并可以同时进行而不互相影响。并且排序速度非常快,仅次于快排。

归并操作

归并即合并两个有序子串。这里用两个有序数组举例,并按从小到大排列。

两个有序子串为nums1,[0,5,8];nums2, [1,2,7,21,34]。合并后的数组为temp,长度为nums1.length + nums2.length。初始时指针i1指向数组nums1的0号位,i2指向nums2的0号位,指针t指向temp的0号位置。

第一次,比较i1和i2指向的数值,i1值0小于i2值1,将数值0复制到t指针指向的位子。i1和t分别向右挪动一位。结果如下图所示。

第二次,继续比较i1和i2指向的值,5大于1,所以将i2指向的值1复制到t指针位置。i2和t右移一位。结果如下图所示。

重复上面的步骤,直到i1指向nums1的末尾时结束。结果如下:

这时发现无需再次比较,将nums2从i2指向的节点开始到末尾全数复制到temp数组中即可。完成后的合并数组为:

用代码实现一遍:

java 复制代码
private static int[] mergeArray(int[] nums1, int[] nums2){
    if(null == nums1 || 0 == nums1.length) return nums2;
    if(null == nums2 || 0 == nums2.length) return nums1;
    int[] temp = new int[nums1.length + nums2.length];
    int t = 0;
    int i1 = 0;
    int i2 = 0;
    while (i1< nums1.length && i2 < nums2.length){
        if(nums1[i1] <= nums2[i2]){
            temp[t++] = nums1[i1++];
        }else {
            temp[t++] = nums2[i2++];
        }
    }
    //nums1数组有剩余
    while (i1< nums1.length){
        temp[t++] = nums1[i1++];
    }
    //nums2数组有剩余
    while (i2 < nums2.length){
        temp[t++] = nums2[i2++];
    }
    return temp;
}

一般进行归并排序的时候我们都是在同一个数组中进行操作的,所以需要将上面提到的nums1和nums2两个数组放置在同一个数组nums中然后进行归并操作。最终的结果也会返回到输入的nums数组中。

因此我们对上面的归并函数入参做出调整。设计三个指针,头中尾。start指向nums1的开始,mid指向nums1的末尾,end指向nums2的末尾。

调整后的代码为:

java 复制代码
private static void mergeArray2(int[] nums, int start, int mid, int end) {
        if (null == nums || 0 == nums.length) return;
        int[] temp = new int[nums.length];
        int t = start;
        int i1 = start;
        int i2 = mid + 1;
        //这里的mid和end是数组的下标,不同于刚才的数组长度length,所以要加等号
        while (i1 <= mid && i2 <= end) {
            if (nums[i1] <= nums[i2]) {
                temp[t++] = nums[i1++];
            } else {
                temp[t++] = nums[i2++];
            }
        }
        //拷贝剩余的数据
        while (i1 <= mid) {
            temp[t++] = nums[i1++];
        }
        //拷贝剩余的数据
        while (i2 <= end) {
            temp[t++] = nums[i2++];
        }
    //t重新指向temp数组的开头位置
        t = start;
      //将temp中的数值拷贝回nums中
        while (t <= end) {
            nums[t] = temp[t];
            t++;
        }
    }

数组的切割

开头的时候说过归并排序首先要做的就是数组的切割。

切割动作可以用递归动作来简单实现。如上图所示,start指向0号位,end指向7号为。计算mid=(start + end) / 2 = 3。递归调用,start到mid的分一组,mid+1到end的分一组。

java 复制代码
private static void mergeSort(int[] nums, int start, int end){
    int mid = (start + end) / 2;
    mergeSort(nums, start, mid); //继续切割左边的。
    mergeSort(nums, mid +1, end); //继续切割右边的。
}

我们默认认为只有一个数组元素的子串即为有序数组。因此添加一个递归的调用结束条件 start >= end时结束。

java 复制代码
private static void mergeSort(int[] nums, int start, int end) {
    if (start >= end) {
        return;
    }
    int mid = (start + end) / 2;
    mergeSort(nums, start, mid); //继续切割左边的。
    mergeSort(nums, mid + 1, end); //继续切割右边的。
}

切割+归并操作=归并排序

至此我们已经完成了归并排序的归并动作和切割动作。将这两者合在一起就是完整的归并排序。

java 复制代码
private static void mergeSort(int[] nums, int start, int end) {
    if (start >= end) {
        return;
    }
    int mid = (start + end) / 2;
    mergeSort(nums, start, mid); //继续切割左边的。
    mergeSort(nums, mid + 1, end); //继续切割右边的。
    
    mergeArray2(nums, start, mid, end); //合并两个有序子串
}

算法空间的优化

观察上面的归并操作,发现每次都要new一个临时数组,这里有浪费空间的嫌疑。而且每次归并都是在一个和nums同等大小的数组的不同位置进行操作。所以可以提前创建好一个temp数组传入即可,避免了浪费空间。

java 复制代码
private static void mergeSort(int[] nums, int start, int end, int[] temp) {
    if (start >= end) {
        return;
    }
    int mid = (start + end) / 2;
    mergeSort(nums, start, mid, temp); //继续切割左边的。
    mergeSort(nums, mid + 1, end, temp); //继续切割右边的。
​
    mergeArray2(nums, start, mid, end, temp); //合并两个有序子串
}
private static void mergeArray2(int[] nums, int start, int mid, int end, int[] temp) {
        if (null == nums || 0 == nums.length) return;
        int t = start;
        int i1 = start;
        int i2 = mid + 1;
        //这里的mid和end是数组的下标,不同于刚才的数组长度length,所以要加等号
        while (i1 <= mid && i2 <= end) {
            if (nums[i1] <= nums[i2]) {
                temp[t++] = nums[i1++];
            } else {
                temp[t++] = nums[i2++];
            }
        }
        //拷贝剩余的数据
        while (i1 <= mid) {
            temp[t++] = nums[i1++];
        }
        //拷贝剩余的数据
        while (i2 <= end) {
            temp[t++] = nums[i2++];
        }
​
        //t重新指向temp数组的开头位置
        t = start;
        //将temp中的数值拷贝回nums中
        while (t <= end) {
            nums[t] = temp[t];
            t++;
        }
    }

算法分析

时间复杂度

根据以上的切割和合并两个操作发现,在进行归并排序时无论数组里的元素是什么情况这两步是少不了的。所以说归并排序的最好、最坏、平均时间复杂度都是一样的。

同样记录结果:O( nlogn )

详细的推导过程可以参考:排序算法之 快速排序 及其时间复杂度和空间复杂度_快去排序算法的评论时间复杂度为-CSDN博客

空间复杂度

归并排序的空间分为两部分,1递归栈,O(logn);2临时数组temp,O(n)

O(logn) + O(n) = O(n)

稳定性

归并操作时,元素值如果相同,位置不会发生变化。因此归并排序是一个稳定的排序算法

相关推荐
滨HI021 分钟前
P8692 [蓝桥杯 2019 国 C] 数正方形--输出取模余数
c语言·c++·算法·职场和发展·蓝桥杯
h^hh1 小时前
洛谷 P2142 高精度减法(详解)c++
开发语言·c++·算法
代码骑士1 小时前
决策树(Decision Tree)案例分析
算法·决策树·机器学习
猫头鹰~4 小时前
数据结构——布隆过滤器
数据结构
lucky_syq4 小时前
Flink 窗口:流处理的核心利器
大数据·算法·flink
星空露珠4 小时前
迷你世界脚本背包接口:Backpack
数据结构·游戏·lua
星空露珠4 小时前
迷你世界脚本实体接口:Actor
数据结构·游戏·lua
绛洞花主敏明4 小时前
go语言for循环中嵌套defer的执行顺序
开发语言·算法·golang
好易学·数据结构5 小时前
为什么要学习数据结构与算法
数据结构·算法·leetcode·面试·力扣·笔试·牛客网
#看心情5 小时前
算法思想-贪心算法
算法·贪心算法