算法必知 --- 归并排序(优化与案例)

简介: 算法必知 --- 归并排序(优化与案例)

算法描述



使用归并排序进行升序排列。


示例

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]


算法设计



基本思路:借助额外空间,合并两个有序数组,得到更长的有序数组。例如:「力扣」第 88 题:合并两个有序数组


核心函数:mergeSort函数(分治思想)和merge函数(合并有序数组),分治过程一定情况下可以实现并行化。


算法优化



  • 优化1:小区间采用插入排序


Java 源码里面也有类似这种操作,「小区间」的长度是个超参数,需要测试决定,我这里参考了 JDK 源码;


  • 优化2:两个数组本身有序,无需合并


左边界大于右边界或者要归并的两个数组已经有序,无需进行merge过程。


  • 优化3:全程使用一份临时数组进行两个数组的合并操作


避免创建临时数组和销毁的消耗,避免计算下标偏移量。即使用System.arraycopy(nums, left, tmp, left, right - left + 1)拷贝到tmp临时数组。


  • 优化4:位运算求解中间值


对Java语言,left + right >>> 1 在可能存在溢出的情况下,结论也是正确的。


代码实现


class Solution {
    // 列表大小大于或等于该值,将优先使用归并排序,否则使用插入排序
    private static final int INSERTION_SORT_THRESHOLD = 7;
    public int[] sortArray(int[] nums) {
        int n = nums.length;
        int[] tmp = new int[n];
        mergeSort(nums, 0, n - 1, tmp);
        return nums;
    }
    private void mergeSort(int[] nums, int left, int right, int[] tmp) {
        if (right - left <= INSERTION_SORT_THRESHOLD) {
            insertionSort(nums, left, right);
            return;
        }
        // int mid = left + (right - left) / 2;
        int mid = left + right >>> 1;
        mergeSort(nums, left, mid, tmp);
        mergeSort(nums, mid + 1, right, tmp);
        // 如果子区间本身有序,则无需合并
        if (nums[mid] <= nums[mid + 1]) {
            return;
        }
        merge(nums, left, mid, right, tmp);
    }
    private void insertionSort(int[] nums, int left, int right) {
        for (int i = left + 1; i <= right; ++i) {
            int tmp = nums[i];
            int j = i;
            while (j > left && nums[j - 1] > tmp) {
                nums[j] = nums[j - 1];
                j--;
            }
            nums[j] = tmp;
        }
    }
    private void merge(int[] nums, int left, int mid, int right, int[] tmp) {
        System.arraycopy(nums, left, tmp, left, right - left + 1);
        int i = left;
        int j = mid + 1;
        for (int k = left; k <= right; k++) {
            if (i == mid + 1) {
                nums[k] = tmp[j];
                j++;
            } else if (j == right + 1) {
                nums[k] = tmp[i];
                i++;
            } else if (tmp[i] <= tmp[j]) {
                // 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
                nums[k] = tmp[i];
                i++;
            } else {
                // tmp[i] > tmp[j]:先放较小的元素
                nums[k] = tmp[j];
                j++;
            }
        }
    }
}


注意点(补充):


  • 实现归并排序的时候,要特别注意,不要把这个算法实现成非稳定排序,区别就在 <=< ,已在代码中注明。
  • 「归并排序」比「快速排序」好的一点是,它借助了额外空间,可以实现「稳定排序」,Java 里对于「对象数组」的排序任务,就是使用归并排序(的升级版 TimSort,在这里就不多做介绍)。
  • 时间复杂度:O(N log N),这里 N 是数组的长度;空间复杂度:O(N),辅助数组与输入数组规模相当。
  • 「归并排序」也有「原地归并排序」和「不使用递归」的归并排序,但是我个人觉得不常用,编码、调试都有一定难度。
  • 经典问题:


应用拓展



示例1:「力扣」第 88 题合并两个有序数组


注意:归并排序的merge过程,但是考虑不使用额外空间,技巧:倒序遍历两个数组(先添加较大的)


代码实现:

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = m - 1;
        int j = n - 1;
        int merge = m + n - 1;
        while (i > -1 || j > -1) {
            if (j < 0) {
                nums1[merge--] = nums1[i--];
            } else if (i < 0) {
                nums1[merge--] = nums2[j--];
            } else if (nums1[i] > nums2[j]) {
                nums1[merge--] = nums1[i--];
            } else {
                nums1[merge--] = nums2[j--];
            }
        }
    }
}


示例2:《剑指 Offer》第 51 题:数组中的逆序对


注意:暴力解超时!必然进行时间优化,这里利用归并排序分治思想(降低时间复杂度)+合并两个数组的有序性(统计逆序对的个数)


  • 排序的过程是必要的,正是因为排序才加速下一轮的统计逆序对的速度。
  • 分治先统计左右区间的逆序对,再计算跨区间的逆序对(这里我们采用在第2个区间归并时,即j指向元素归并回去的时候,统计逆序数 == 前面还没有归并回去的元素个数:mid - i + 1)即以左边的元素比当前大的元素个数,统计逆序对。


ps:这里不要求排序所以需要拷贝一份nums数组。


ps2:以右侧小于当前元素个数的方式统计逆序数的个数,类似下面的315题,只需要改成归并i,统计逆序数 == j - mid + 1,如果只是统计逆序数,左右无区别,如本题。


代码实现:

class Solution {
    public int reversePairs(int[] nums) {
        int n = nums.length;
        if (n < 2) {
            return 0;
        }
        int[] copy = new int[n];
        System.arraycopy(nums, 0, copy, 0, n);
        int[] tmp = new int[n];
        return reversePairs(copy, 0, n - 1, tmp);
    }
    private int reversePairs(int[] copy, int left, int right, int[] tmp) {
        if (left >= right) {
            return 0;
        }
        int mid = left + ((right - left) >> 1);
        int leftPairs = reversePairs(copy, left, mid, tmp);
        int rightPairs = reversePairs(copy, mid + 1, right, tmp);
        if (copy[mid] <= copy[mid + 1]) {
            return leftPairs + rightPairs;
        }
        int crossPairs = mergeAndCount(copy, left, mid, right, tmp);
        return leftPairs + rightPairs + crossPairs;
    }
    private int mergeAndCount(int[] copy, int left, int mid, int right, int[] tmp) {
        System.arraycopy(copy, left, tmp, left, right - left + 1);
        int i = left;
        int j = mid + 1;
        int count = 0;
        for (int k = left; k <= right; k++) {
            if (i == mid + 1) {
                copy[k] = tmp[j];
                j++;
            } else if (j == right + 1) {
                copy[k] = tmp[i];
                i++;
            } else if (tmp[i] <= tmp[j]) {
                copy[k] = tmp[i];
                i++;
            } else {
                copy[k] = tmp[j];
                j++;
                // 当j指向元素归并进去,计算逆序数
                count += (mid - i + 1); 
            }
        }
        return count;
    }
}


示例3:「力扣」第 315 题:计算右侧小于当前元素的个数


注意:数据量很大,暴力必然超时!与上题相同,即每个位置逆序数的个数。


  • 需要在「前有序数组」的元素归并的时候,数一数「后有序数组」已经归并回去的元素的个数,因为这些已经出列的元素都比当前出列的元素要(严格)小;
  • 一个元素在算法的执行过程中位置发生变化,我们还想定位它,可以使用「索引数组」(记录元素下标),技巧在于:「原始数组」不变,用于比较两个元素的大小,真正位置变化的是「索引数组」的位置;归并回去时,方便知道哪个下标的元素。
  • ps: res[indexes[k]] += (j - mid - 1); 理解:indexes[k] 表示当前 k 下标对应的原始数组的下标是多少,外面再套一层 res[indexes[k]] 记录了原始数组的对应下标右侧小于当前元素的个数,在排序的过程中,每一次计算一部分,所以是 += 。整个算法完成以后,indexes 映射成输入数组的元素的值以后,是有序的。


代码实现:核心:比较数值,操作下标

class Solution {
    public List<Integer> countSmaller(int[] nums) {
        List<Integer> result = new ArrayList<>();
        int n = nums.length;
        if (n < 1) {
            return result;
        }
        int[] ans = new int[n];
        int[] tmp = new int[n];
        int[] indexs = new int[n];
        for (int i = 0; i < n; i++) {
            indexs[i] = i;
        }
        countSmaller(nums, 0, n - 1, tmp, indexs, ans);
        for (int num : ans) {
            result.add(num);
        }
        return result;
    }
    private void countSmaller(int[] nums, int left, int right, int[] tmp, int[] indexs, int[] ans) {
        if (left == right) {
            return;
        }
        int mid = left + (right - left) / 2;
        countSmaller(nums, left, mid, tmp, indexs, ans);
        countSmaller(nums, mid + 1, right, tmp, indexs, ans);
        // 已经有序,不存在逆序对,不需要进行合并
        if (nums[indexs[mid]] <= nums[indexs[mid + 1]]) {
            return;
        }
        mergeAndCount(nums, left, mid, right, tmp, indexs, ans);
    }
    private void mergeAndCount(int[] nums, int left, int mid, int right, int[] tmp, int[] indexs, int[] ans) {
        System.arraycopy(indexs, left, tmp, left, right - left + 1);
        int i = left;
        int j = mid + 1;
        for (int k = left; k <= right; k++) {
            if (i == mid + 1) {
                indexs[k] = tmp[j];
                j++;
            } else if (j == right + 1) {
                indexs[k] = tmp[i];
                i++;
                // 后有序数组已经归并完成,一定比当前要归并的元素小
                ans[indexs[k]] += (right - mid);
            } else if (nums[tmp[i]] <= nums[tmp[j]]) {
                indexs[k] = tmp[i];
                i++;
                // 当前元素要归并,后有序数组已经归并的元素一定比当前元素小
                ans[indexs[k]] += (j - mid - 1);
            } else {
                indexs[k] = tmp[j];
                j++;
            }
        }
    }
}


示例4:计算数组的小和(计算左侧大于当前元素总和) 要求时间复杂度O(NlogN),空间复杂度O(N)

1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,1、3;
2左边比2小的数,1;
5左边比5小的数,1、3、4、2;
所以小和为1+1+3+1+1+3+4+2=16


代码实现:

import java.util.Scanner;
public class Main {
    // 运行超时
    public static long solution1(int[] nums) {
        long sum = 0;
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] <= nums[i]) {
                    sum += nums[j];
                }
            }
        }
        return sum;
    }
    public static long solution(int[] nums) {
        int n = nums.length;
        if (n < 1) {
            return 0;
        }
        int[] tmp = new int[n];
        return mergeSort(nums, 0, n - 1, tmp);
    }
    private static long mergeSort(int[] nums, int left, int right, int[] tmp) {
        if (left == right) {
            return 0;
        }
        int mid = left + (right - left) / 2;
        return mergeSort(nums, left, mid, tmp) +
               mergeSort(nums, mid + 1, right, tmp) + 
               mergeAndSum(nums, left, mid, right, tmp);
    }
    private static long mergeAndSum(int[] nums, int left, int mid, int right, int[] tmp) {
        System.arraycopy(nums, left, tmp, left, right - left + 1);
        int i = left;
        int j = mid + 1;
        long smallSum = 0;
        for (int k = left; k <= right; k++) {
            if (i == mid + 1) {
                nums[k] = tmp[j];
                j++;
            } else if (j == right + 1) {
                nums[k] = tmp[i];
                i++;
            } else if (tmp[i] <= tmp[j]) {
                // i进行归并时,左边小于右边产生小和(当前加入的一定比右边还未加入的元素小)
                smallSum += tmp[i] * (right - j + 1);
                nums[k] = tmp[i];
                i++;
            } else {
                nums[k] = tmp[j];
                j++;
            }
        }
        return smallSum;
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] nums = new int[n];
        int i = 0;
        while (n-- > 0) {
            nums[i++] = sc.nextInt();
        }
        System.out.println(solution(nums));
//         System.out.println(solution1(nums));
    }
}


目录
打赏
0
0
0
0
7
分享
相关文章
|
12天前
|
解析公司屏幕监控软件中 C# 字典算法的数据管理效能与优化策略
数字化办公的时代背景下,企业为维护信息安全并提升管理效能,公司屏幕监控软件的应用日益普及。此软件犹如企业网络的 “数字卫士”,持续记录员工电脑屏幕的操作动态。然而,伴随数据量的持续增长,如何高效管理这些监控数据成为关键议题。C# 中的字典(Dictionary)数据结构,以其独特的键值对存储模式和高效的操作性能,为公司屏幕监控软件的数据管理提供了有力支持。下文将深入探究其原理与应用。
31 4
基于和声搜索优化算法的机器工作调度matlab仿真,输出甘特图
本程序基于和声搜索优化算法(Harmony Search, HS),实现机器工作调度的MATLAB仿真,输出甘特图展示调度结果。算法通过模拟音乐家即兴演奏寻找最佳和声的过程,优化任务在不同机器上的执行顺序,以最小化完成时间和最大化资源利用率为目标。程序适用于MATLAB 2022A版本,运行后无水印。核心参数包括和声记忆大小(HMS)等,适应度函数用于建模优化目标。附带完整代码与运行结果展示。
基于GA遗传优化的最优阈值计算认知异构网络(CHN)能量检测算法matlab仿真
本内容介绍了一种基于GA遗传优化的阈值计算方法在认知异构网络(CHN)中的应用。通过Matlab2022a实现算法,完整代码含中文注释与操作视频。能量检测算法用于感知主用户信号,其性能依赖检测阈值。传统固定阈值方法易受噪声影响,而GA算法通过模拟生物进化,在复杂环境中自动优化阈值,提高频谱感知准确性,增强CHN的通信效率与资源利用率。预览效果无水印,核心程序部分展示,适合研究频谱感知与优化算法的学者参考。
基于GA遗传优化TCN-GRU时间卷积神经网络时间序列预测算法matlab仿真
本项目基于MATLAB2022a开发,提供无水印算法运行效果预览及核心程序(含详细中文注释与操作视频)。通过结合时间卷积神经网络(TCN)和遗传算法(GA),实现复杂非线性时间序列的高精度预测。TCN利用因果卷积层与残差连接提取时间特征,GA优化超参数(如卷积核大小、层数等),显著提升模型性能。项目涵盖理论概述、程序代码及完整实现流程,适用于金融、气象、工业等领域的时间序列预测任务。
基于遗传优化算法的多AGV栅格地图路径规划matlab仿真
本程序基于遗传优化算法实现多AGV栅格地图路径规划的MATLAB仿真(测试版本:MATLAB2022A)。支持单个及多个AGV路径规划,输出路径结果与收敛曲线。核心程序代码完整,无水印。算法适用于现代工业与物流场景,通过模拟自然进化机制(选择、交叉、变异)解决复杂环境下的路径优化问题,有效提升效率并避免碰撞。适合学习研究多AGV系统路径规划技术。
125 12
基于GA遗传优化TCN时间卷积神经网络时间序列预测算法matlab仿真
本内容介绍了一种基于遗传算法优化的时间卷积神经网络(TCN)用于时间序列预测的方法。算法运行于 Matlab2022a,完整程序无水印,附带核心代码、中文注释及操作视频。TCN通过因果卷积层与残差连接学习时间序列复杂特征,但其性能依赖超参数设置。遗传算法通过对种群迭代优化,确定最佳超参数组合,提升预测精度。此方法适用于金融、气象等领域,实现更准确可靠的未来趋势预测。
基于BBO生物地理优化的三维路径规划算法MATLAB仿真
本程序基于BBO生物地理优化算法,实现三维空间路径规划的MATLAB仿真(测试版本:MATLAB2022A)。通过起点与终点坐标输入,算法可生成避障最优路径,并输出优化收敛曲线。BBO算法将路径视为栖息地,利用迁移和变异操作迭代寻优。适应度函数综合路径长度与障碍物距离,确保路径最短且安全。程序运行结果完整、无水印,适用于科研与教学场景。
基于NSGAII的的柔性作业调度优化算法MATLAB仿真,仿真输出甘特图
本程序基于NSGA-II算法实现柔性作业调度优化,适用于多目标优化场景(如最小化完工时间、延期、机器负载及能耗)。核心代码完成任务分配与甘特图绘制,支持MATLAB 2022A运行。算法通过初始化种群、遗传操作和选择策略迭代优化调度方案,最终输出包含完工时间、延期、机器负载和能耗等关键指标的可视化结果,为制造业生产计划提供科学依据。
基于GA遗传优化TCN-LSTM时间卷积神经网络时间序列预测算法matlab仿真
本项目基于MATLAB 2022a实现了一种结合遗传算法(GA)优化的时间卷积神经网络(TCN)时间序列预测算法。通过GA全局搜索能力优化TCN超参数(如卷积核大小、层数等),显著提升模型性能,优于传统GA遗传优化TCN方法。项目提供完整代码(含详细中文注释)及操作视频,运行后无水印效果预览。 核心内容包括:1) 时间序列预测理论概述;2) TCN结构(因果卷积层与残差连接);3) GA优化流程(染色体编码、适应度评估等)。最终模型在金融、气象等领域具备广泛应用价值,可实现更精准可靠的预测结果。
基于WOA鲸鱼优化的CNN-LSTM-SAM网络时间序列回归预测算法matlab仿真
本内容介绍了一种基于CNN-LSTM-SAM网络与鲸鱼优化算法(WOA)的时间序列预测方法。算法运行于Matlab2022a,完整程序无水印并附带中文注释及操作视频。核心流程包括数据归一化、种群初始化、适应度计算及参数更新,最终输出最优网络参数完成预测。CNN层提取局部特征,LSTM层捕捉长期依赖关系,自注意力机制聚焦全局特性,全连接层整合特征输出结果,适用于复杂非线性时间序列预测任务。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等