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

归并排序

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

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

归并操作

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

两个有序子串为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)

稳定性

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

相关推荐
野渡拾光1 小时前
【考研408数据结构-05】 串与KMP算法:模式匹配的艺术
数据结构·考研·算法
tainshuai3 小时前
用 KNN 算法解锁分类的奥秘:从电影类型到鸢尾花开
算法·分类·数据挖掘
Coovally AI模型快速验证9 小时前
农田扫描提速37%!基于检测置信度的无人机“智能抽查”路径规划,Coovally一键加速模型落地
深度学习·算法·yolo·计算机视觉·transformer·无人机
pusue_the_sun9 小时前
数据结构:二叉树oj练习
c语言·数据结构·算法·二叉树
RaymondZhao349 小时前
【全面推导】策略梯度算法:公式、偏差方差与进化
人工智能·深度学习·算法·机器学习·chatgpt
zhangfeng113310 小时前
DBSCAN算法详解和参数优化,基于密度的空间聚类算法,特别擅长处理不规则形状的聚类和噪声数据
算法·机器学习·聚类
啊阿狸不会拉杆10 小时前
《算法导论》第 32 章 - 字符串匹配
开发语言·c++·算法
小学生的信奥之路11 小时前
洛谷P3817题解:贪心算法解决糖果分配问题
c++·算法·贪心算法
你知道网上冲浪吗12 小时前
【原创理论】Stochastic Coupled Dyadic System (SCDS):一个用于两性关系动力学建模的随机耦合系统框架
python·算法·数学建模·数值分析
地平线开发者13 小时前
征程 6 | PTQ 精度调优辅助代码,总有你用得上的
算法·自动驾驶