常见排序算法总结 (三) - 归并排序与归并分治

news2024/12/26 23:54:29

归并排序

算法思想

将数组元素不断地拆分,直到每一组中只包含一个元素,单个元素天然有序。之后用归并的方式收集跨组的元素,最终形成整个区间上有序的序列。

稳定性分析

归并排序是稳定的,拆分数组时会自然地将元素分成有先后顺序的子数组,在归并的过程中如果遇到相等的值,优先收集早出现的子数组中的那个即可。

具体实现

递归

// 注意预先定义辅助数组,防止递归层数深的情况下花大量时间空间去开数组
public static int MAX_N = 50010;
public static int[] temp = new int[MAX_N];

// 归并,用于将已经有序的两个数组合并成更大的有序数组
private void merge(int[] arr, int left, int mid,  int right) {
    int index1 = left, index2 = mid + 1, index = left;
    // 两个数组都有剩余元素,每次收集值较小的元素
    while(index1 <= mid && index2 <= right) {
        temp[index++] = arr[index1] <= arr[index2] ? arr[index1++] : arr[index2++];
    }
    // 其中一个数组为空,将另一个数组剩余的所有元素都添加到结果数组的末尾
    while(index1 <= mid) {
        temp[index++] = arr[index1++];
    }
    while(index2 <= right) {
        temp[index++] = arr[index2++];
    }
    // 结果回写到原数组
    System.arraycopy(temp, left, arr, left, right - left + 1);
}

// 归并排序
private void mergeSort(int[] arr, int left, int right) {
    // 左右端点相同,数组中只有单个元素认为天然有序
    if (left == right) {
        return;
    }
    int mid = left + ((right - left) >>> 1); // 计算区间中点作为划分数组的依据
    // 递归地对左半边数组进行排序
    mergeSort(arr, left, mid);
    // 递归地对右半边数组进行排序
    mergeSort(arr, mid + 1, right);
    // 归并收集结果
    merge(arr, left, mid, right);
}

非递归

// 注意预先定义辅助数组,防止递归层数深的情况下花大量时间空间去开数组
public static int MAX_N = 50010;
public static int[] temp = new int[MAX_N];

// 归并方法,用于将已经有序的两个数组合并成更大的有序数组
private void merge(int[] arr, int left, int mid,  int right) {
    int index1 = left, index2 = mid + 1, index = left;
    // 两个数组都有剩余元素,每次收集值较小的元素
    while(index1 <= mid && index2 <= right) {
        temp[index++] = arr[index1] <= arr[index2] ? arr[index1++] : arr[index2++];
    }
    // 其中一个数组为空,将另一个数组剩余的所有元素都添加到结果数组的末尾
    while(index1 <= mid) {
        temp[index++] = arr[index1++];
    }
    while(index2 <= right) {
        temp[index++] = arr[index2++];
    }
    // 结果回写到原数组
    System.arraycopy(temp, left, arr, left, right - left + 1);
}

// 归并排序
private void mergeSort(int[] arr) {
    int n = arr.length;
    // 根据步长来迭代,每一轮扩大一倍
    for (int left, mid, right, step = 1; step < n; step <<= 1) {
        left = 0; // 左端点从 0 开始
        while (left < n) {
            mid = left + step - 1; // 计算区间中点的位置
            // 另一半区间无法构成,直接进行下一轮
            if (mid >= n - 1) {
                break;
            }
            // 数组内剩余元素不一定够填满右半边数组,右端点要根据情况来取值
            right = Math.min(left + (step << 1) - 1, n - 1);
            merge(arr, left, mid, right);
            // 移动指针,准备归并右半边数组
            left = right + 1;
        }
    }
}

归并分治

分治是一种算法思想,归并分治顾名思义就是涉及到了归并的分治策略。这部分内容其实和排序关系不大,但是作为归并应用的扩展放在一起整理比较合适的。

算法思想

如果一个问题满足两个条件,那么大概率可以使用归并分治解决:

  • 全局的结果相当于划分成两部分之后,左半边、右半边与跨左右三部分的结果的并集,也就是这个问题可以总中间拆分成子问题。
  • 如果拆分之后小范围上有序,能够使得计算跨左右的答案时更方便。

实践案例:Leetcode 493. 翻转对

递归

class Solution {
    // 注意预先定义辅助数组,防止递归层数深的情况下花大量时间空间去开数组
    public static int MAX_N = 50010;
    public static int[] temp = new int[MAX_N];

    public int reversePairs(int[] nums) {
        return reversePairs(nums, 0, nums.length - 1);
    }

    // 重载一个同名方法,将它改造成方便递归的形式
    private int reversePairs(int[] nums, int left, int right) {
        // 只有一个元素的情况下没有答案
        if(left == right) {
            return 0;
        }
        int mid = left + ((right - left) >>> 1);
        // 结果等于左半边、右半边与跨左右的结果的并集
        return reversePairs(nums, left, mid) + reversePairs(nums, mid + 1, right) + merge(nums, left, mid, right);
    }

    private int merge(int[] nums, int left, int mid,  int right) {
        int res = 0;
        // 当前跨小范围的情况下,更小范围内已经有序
        for(int i = left, j = mid + 1; i <= mid; i++) {
            // 确定了当前轮 j 的位置之后,这个指针不会回退,这也是提升性能的根本原因
            while(j <= right && (long) nums[i] > (long) 2 * nums[j]) {
                j++;
            }
            res += j - mid - 1;
        }

        // 常规归并流程
        int index1 = left, index2 = mid + 1, index = left;
        while(index1 <= mid && index2 <= right) {
            temp[index++] = nums[index1] <= nums[index2] ? nums[index1++] : nums[index2++];
        }
        while(index1 <= mid) {
            temp[index++] = nums[index1++];
        }
        while(index2 <= right) {
            temp[index++] = nums[index2++];
        }
        System.arraycopy(temp, left, nums, left, right - left + 1);

        return res;
    }
}

非递归

class Solution {
    // 注意预先定义辅助数组,防止递归层数深的情况下花大量时间空间去开数组
    public static int MAX_N = 50010;
    public static int[] temp = new int[MAX_N];

    public int reversePairs(int[] nums) {
        int res = 0;
        int n = nums.length;
        for (int left, mid, right, step = 1; step < n; step <<= 1) {
            left = 0;
            while (left < n) {
                mid = left + step - 1;
                if (mid >= n - 1) {
                    break;
                }
                right = Math.min(left + (step << 1) - 1, n - 1);
                res += merge(nums, left, mid, right); // 累计每次归并的结果
                left = right + 1;
            }
        }
        return res;
    }

    private int merge(int[] nums, int left, int mid,  int right) {
        int res = 0;
        // 当前跨小范围的情况下,更小范围内已经有序
        for(int i = left, j = mid + 1; i <= mid; i++) {
            // 确定了当前轮 j 的位置之后,这个指针不会回退,这也是提升性能的根本原因
            while(j <= right && (long) nums[i] > (long) 2 * nums[j]) {
                j++;
            }
            res += j - mid - 1;
        }

        // 常规归并流程
        int index1 = left, index2 = mid + 1, index = left;
        while(index1 <= mid && index2 <= right) {
            temp[index++] = nums[index1] <= nums[index2] ? nums[index1++] : nums[index2++];
        }
        while(index1 <= mid) {
            temp[index++] = nums[index1++];
        }
        while(index2 <= right) {
            temp[index++] = nums[index2++];
        }
        System.arraycopy(temp, left, nums, left, right - left + 1);

        return res;
    }
}

梳理总结

分治是一种通过不断划分来减小问题规模,而归并是用来收集得到全局结果的方法。上面的例子所使用的都是一分为二的做法,其实每一轮可以划分成更多子问题,演变成多路归并。
正是上面这种不停划分的过程,使得无论是归并排序还是归并分治,都能有效地将暴力搜索需要 O ( N 2 ) O(N ^ 2) O(N2) 量级的方法,优化到 O ( N l o g N ) O(NlogN) O(NlogN) 这个级别。
当然,本质上来说还是空间换时间,这样的操作很明显需要 O ( N ) O(N) O(N) 量级的额外空间。

从使用上来说,归并排序一般不会成为手写排序的选择。但是归并分治则是很多问题的优秀解决方案,需要注意它的使用前提和具体实现。

后记

使用 Leetcode 912. 排序数组 进行测试,归并排序能够比较高高效地完成任务,略逊于计数排序和基数排序(不过其实题目要求使用尽可能少的额外空间,归并排序肯定不属于首选的方案)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2253218.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[C++设计模式] 为什么需要设计模式?

文章目录 什么是设计模式&#xff1f;为什么需要设计模式&#xff1f;GOF 设计模式再次理解面向对象软件设计固有的复杂性软件设计复杂性的根本原因如何解决复杂性&#xff1f;分解抽象 结构化 VS 面向对象(封装)结构化设计代码示例&#xff1a;面向对象设计代码示例&#xff1…

级联树结构TreeSelect和上级反查

接口返回结构 前端展示格式 前端组件 <template><div ><el-scrollbar height"70vh"><el-tree :data"deptOptions" :props"{ label: label, children: children }" :expand-on-click-node"false":filter-node-me…

Figma入门-自动布局

Figma入门-自动布局 前言 在之前的工作中&#xff0c;大家的原型图都是使用 Axure 制作的&#xff0c;印象中 Figma 一直是个专业设计软件。 最近&#xff0c;很多产品朋友告诉我&#xff0c;很多原型图都开始用Figma制作了&#xff0c;并且很多组件都是内置的&#xff0c;对…

【Unity基础】使用InputSystem实现物体跳跃

要在Unity中使用 InputSystem 实现小球按空格键跳起的效果&#xff0c;可以按照以下步骤进行&#xff1a; 1. 安装 InputSystem 包 首先&#xff0c;确保你已经安装了 Input System 包。你可以通过以下步骤安装&#xff1a; 打开 Unity 编辑器&#xff0c;点击菜单 Window -…

【ArkTS】使用AVRecorder录制音频 --内附录音机开发详细代码

系列文章目录 【ArkTS】关于ForEach的第三个参数键值 【ArkTS】“一篇带你读懂ForEach和LazyForEach” 【小白拓展】 【ArkTS】“一篇带你掌握TaskPool与Worker两种多线程并发方案” 【ArkTS】 一篇带你掌握“语音转文字技术” --内附详细代码 【ArkTS】技能提高–“用户授权”…

一种多功能调试工具设计方案开源

一种多功能调试工具设计方案开源 设计初衷设计方案具体实现HUB芯片采用沁恒微CH339W。TF卡功能网口功能SPI功能IIC功能JTAG功能下行USB接口 安路FPGA烧录器功能Xilinx FPGA烧录器功能Jlink OB功能串口功能RS232串口RS485和RS422串口自适应接口 CAN功能烧录器功能 目前进度后续计…

浏览器的事件循环机制

浏览器和Node的事件循环机制 引言浏览器的事件循环机制 引言 由于JS是单线程的脚本语言&#xff0c;所以在同一时间只能做一件事情&#xff0c;当遇到多个任务时&#xff0c;我们不可能一直等待任务完成&#xff0c;这会造成巨大的资源浪费。为了协调时间&#xff0c;用户交互…

Zabbix添加防火墙温度监控值实战

我们在Zabbix监控系统会监控诸如Server、network device、application等实例&#xff0c;通常我们在监控某个具体产品时&#xff0c;我们会找到具体的监控模板&#xff0c;在设备添加到平台以后&#xff0c;将模板链接到该设备&#xff0c;但很多时候我们企业内部的设备是没有标…

【k8s】创建基于sa的token的kubeconfig

需求 创建一个基于sa的token的kubeconfig文件&#xff0c;并用这个文件来访问集群。 具体创建sa 和sa的token请参考文章: 【k8s】给ServiceAccount 创建关联的 Secrets-CSDN博客 创建sa apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata:namespace: jtkjdevnam…

Fastapi + vue3 自动化测试平台---移动端App自动化篇

概述 好久写文章了&#xff0c;专注于新框架&#xff0c;新UI界面的实践&#xff0c;废话不多说&#xff0c;开搞 技术架构 后端&#xff1a; Fastapi Airtest multiprocessing 前端&#xff1a; 基于 Vue3、Vite、TypeScript、Pinia、Pinia持久化插件、Unocss 和 Elemen…

Apache Doris 现行版本 Docker-Compose 运行教程

特别注意&#xff01;Doris On Docker 部署方式仅限于开发环境或者功能测试环境&#xff0c;不建议生产环境部署&#xff01; 如有生产环境或性能测试集群部署诉求&#xff0c;请使用裸机/虚机部署或K8S Operator部署方案&#xff01; 原文阅读&#xff1a;Apache Doris 现行版…

Docker的彻底删除与重新安装(ubuntu22.04)

Docker的彻底删除与重新安装&#xff08;ubuntu22.04&#xff09; 一、首先我们彻底删除Docker1、删除docker及安装时自动安装的所有包2、删除无用的相关的配置文件3、删除相关插件4、删除docker的相关配置和目录 二、重新安装1、添加 Docker 的官方 GPG 密钥&#xff1a;2、将…

Nginx学习-安装以及基本的使用

一、背景 Nginx是一个很强大的高性能Web和反向代理服务&#xff0c;也是一种轻量级的Web服务器&#xff0c;可以作为独立的服务器部署网站&#xff0c;应用非常广泛&#xff0c;特别是现在前后端分离的情况下。而在开发过程中&#xff0c;我们常常需要在window系统下使用Nginx…

力扣hot100道【贪心算法后续解题方法心得】(三)

力扣hot100道【贪心算法后续解题方法心得】 十四、贪心算法关键解题思路1、买卖股票的最佳时机2、跳跃游戏3、跳跃游戏 | |4、划分字母区间 十五、动态规划什么是动态规划&#xff1f;关键解题思路和步骤1、打家劫舍2、01背包问题3、完全平方式4、零钱兑换5、单词拆分6、最长递…

系统--线程互斥

1、相关背景知识 临界资源多线程、多执行流共享的资源,就叫做临界资源临界区每个线程内部,访问临界资源的代码互斥在任何时刻,保证有且只有一个执行流进入临界区,访问临界资源,对临界资源起到保护作用原子性不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么…

Qt桌面应用开发 第十天(综合项目二 翻金币)

目录 1.主场景搭建 1.1重载绘制事件&#xff0c;绘制背景图和标题图片 1.2设置窗口标题&#xff0c;大小&#xff0c;图片 1.3退出按钮对应关闭窗口&#xff0c;连接信号 2.开始按钮创建 2.1封装MyPushButton类 2.2加载按钮上的图片 3.开始按钮跳跃效果 3.1按钮向上跳…

getchar()

getchar():从计算机终端&#xff08;一般是键盘&#xff09;输入一个字符 1、getchar返回的是字符的ASCII码值&#xff08;整数&#xff09;。 2、getchar在读取结束或者失败的时候&#xff0c;会返回EOF 输入密码并确认&#xff1a; scanf读取\n之前的内容即12345678 回车符…

linux 获取公网流量 tcpdump + python + C++

前言 需求为&#xff0c;统计linux上得上下行公网流量&#xff0c;常规得命令如iftop 、sar、ifstat、nload等只能获取流量得大小&#xff0c;不能区分公私网&#xff0c;所以需要通过抓取网络包并排除私网段才能拿到公网流量。下面提供了一些有效得解决思路&#xff0c;提供了…

Node.js:开发和生产之间的区别

Node.js 中的开发和生产没有区别&#xff0c;即&#xff0c;你无需应用任何特定设置即可使 Node.js 在生产配置中工作。但是&#xff0c;npm 注册表中的一些库会识别使用 NODE_ENV 变量并将其默认为 development 设置。始终在设置了 NODE_ENVproduction 的情况下运行 Node.js。…

KAN-Transfomer——基于新型神经网络KAN的时间序列预测

1.数据集介绍 ETT(电变压器温度)&#xff1a;由两个小时级数据集&#xff08;ETTh&#xff09;和两个 15 分钟级数据集&#xff08;ETTm&#xff09;组成。它们中的每一个都包含 2016 年 7 月至 2018 年 7 月的七种石油和电力变压器的负载特征。 traffic(交通) &#xff1a;描…