归并排序:从二路到多路

news2024/9/24 6:29:20

前言

我们所熟知的快速排序归并排序都是非常优秀的排序算法。

但是快速排序和归并排序的一个区别就是:快速排序是一种内部排序,而归并排序是一种外部排序

简单理解归并排序:递归地拆分,回溯过程中,将排序结果进行合并。

大致流程示意图:

 

假设递归的终止条件就是剩下三个以下的元素就可以排序了。

注意:这步合并的过程用到了额外的存储空间。完成了排序之后再复制回去。

二路归并演示代码

#include <iostream>
using namespace std;

void merge_sort(int *arr, int l, int r) {
    // 递归终止条件:只有一个元素或者没有元素的时候,不需要排序。
    if (l >= r) return ;

    // 打印输出排序之前的情况
    cout << endl;
    cout << "sort " << l << "<-->" << r << " : ";
    for (int i = l; i <= r; i++) cout << arr[i] << " ";
    cout << endl;

    int mid = (l + r) >> 1;
    merge_sort(arr, l, mid); // left sort
    merge_sort(arr, mid + 1, r); // right sort
    // 写递归代码,一定不要展开地看,上面两行代码就当做左右子区间已经排序好了。
    
    // 下面将对两个区间进行合并,需要开辟新的空间将元素存到temp数组中。
    int *temp = (int *)malloc(sizeof(int) * (r - l + 1));
    int k = 0, p1 = l, p2 = mid + 1;
    while (p1 <= mid || p2 <= r) {
        if ((p2 > r) || (p1 <= mid && arr[p1] <= arr[p2])) {
            // 只有当右边为空,或者左边不为空并且左边比右边小,才将左边的元素放入
            temp[k++] = arr[p1++];
        } else {
            temp[k++] = arr[p2++];
        }
    }
    // 最后再拷贝回去即可
    for (int i = l; i <= r; i++) arr[i] = temp[i - l];
    // 打印输出排序之后的情况
    for (int i = l; i <= r; i++) cout << arr[i] << " ";
    cout << endl;
    free(temp);
    return ;
}

int main() {
    int n;
    int arr[100];
    cin >> n;
    for (int i = 0; i < n; i++) cin >> arr[i];
    merge_sort(arr, 0, n - 1);
    for (int i = 0; i < n; i++) cout << arr[i] << " ";

    return 0;
}

输入数据:

10 
7 9 0 8 6 4 5 3 1 2

输出:

sort 0<-->9 : 7 9 0 8 6 4 5 3 1 2 

sort 0<-->4 : 7 9 0 8 6 

sort 0<-->2 : 7 9 0 

sort 0<-->1 : 7 9 
7 9 
0 7 9 

sort 3<-->4 : 8 6
6 8
0 6 7 8 9

sort 5<-->9 : 4 5 3 1 2

sort 5<-->7 : 4 5 3

sort 5<-->6 : 4 5
4 5
3 4 5

sort 8<-->9 : 1 2
1 2
1 2 3 4 5
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9

多路归并

上述演示代码的归并排序只是二路归并。将两个有序数组合并为一个有序数组。

那么多路归并就很好理解了,就是将多个有序数组合并为一个有序数组。

比如三路归并:

 关于多路归并排序的应用,有一道很经典的面试题:

意思就是:我的内存太小了,无法通过诸如快速排序这样的内部排序算法,进行数据的直接整体排序。那么为什么这个问题可以由归并算法来解决呢?

归并的时候,外存可以作为归并排序中的那片关键的额外空间,数据是可以直接写回外存的,所以不需要内存有40GB的额外空间来先存放排序完的数据,再写回外存。

其实这40GB的文件可以被拆分成20份2GB的小文件,我们只要分别对20份小文件进行排序之后,进行20路归并操作就可以了

注意:程序执行一定是在内存当中,所有的数据也都需要从辅存或者外存当中调入内存当中,才可以进行CPU的运算。一个2GB大小的内存当然无法调入40GB的数据。

还需注意的是:我们在程序中只存储了相应的文件指针,并没有将文件中的内容一次性全部读满内存,而是需要一个数据就从文件中读一个数据。

读取文件演示代码:

*************************************************************************
        > File Name: merge_file.cpp
        > Author: jby
        > Mail: 
        > Created Time: Sat 12 Aug 2023 11:39:20 PM CST
 ************************************************************************/

#include<iostream>
using namespace std;

int main(int argc, char *argv[]) {
    int n = argc - 1; // 读取文件数量
    FILE **farr = (FILE **)malloc(sizeof(FILE *) * n);
    for (int i = 1; i <= n; i++) {
        farr[i - 1] = fopen(argv[i], "r");
    }
    for (int i = 0; i < n; i++) {
        int a;
        while (~fscanf(farr[i], "%d", &a)) {
            printf("%d\n", a);
        }
        printf("======\n");
    }
    return 0;
}

生成俩数据文件:file1、file2

# file1
1
34
56
78

# file2:
3
45
89
100
执行命令行:$./a.out file1 file2

输出结果:

1
34
56
78
======
3
45
89
100
======

这样我们就依次读取了存放在两个文件中的数据。

文件排序演示代码(简单实现,不用归并)

/*************************************************************************
        > File Name: merge_file.cpp
        > Author: jby
        > Mail: 
        > Created Time: Sat 12 Aug 2023 11:39:20 PM CST
 ************************************************************************/

#include<iostream>
using namespace std;


struct Data {
    FILE *fin;     // fin: 当前文件指针
    int val, flag; // val: 当前读取的值;flag: 当前文件是否为空
};

int main(int argc, char *argv[]) {
    int n = argc - 1; // 读取文件数量
    Data *data = (Data *)malloc(sizeof(Data) * n);
    for (int i = 1; i <= n; i++) {
        data[i - 1].fin = fopen(argv[i], "r");
        if (fscanf(data[i - 1].fin, "%d", &data[i - 1].val) == EOF) {
            data[i - 1].flag = 1;
        } else {
            data[i - 1].flag = 0;
        }
    }
    FILE *fout = fopen("output", "w");
    while (1) {
        int flag = 0;
        int ind = -1;
        int minVal = 0x3f3f3f3f;
        for (int i = 0; i < n; i++) {
            if (data[i].flag) continue; // 当前文件为空
            if (ind == -1 || data[i].val < data[ind].val) {
                ind = i;
            }
        }
        if (ind != -1) {
            fprintf(fout, "%d\n", data[ind].val); // 向结果文件中输出内容
            if (fscanf(data[ind].fin, "%d", &data[ind].val) == EOF) {
                data[ind].flag = 1;
            } else {
                data[ind].flag = 0;
            }
            flag = 1;
        }
        if (flag == 0) break;
    }

    return 0;
}
执行命令行:$./a.out file1 file2

输出结果,保存在output文件中:

1
3
34
45
56
78
89
100

归并排序的算法思想

我们不妨把思维从排序问题当中延展出来,归并排序的算法思想可以看成是以下三个步骤:

  1. 左边处理一下,得到左边的信息
  2. 右边处理一下,得到右边的信息
  3. 最后再处理,横跨左右两边的信息

这就是分而治之的思想。

LeetCode刷题实战

剑指 Offer 51. 数组中的逆序对

在归并排序的过程中,当右边区间的元素放进额外空间的时候,左边剩下的元素个数就是该元素所对应的逆序对个数。所以可以在归并的过程中不断累加。

class Solution {
public:
    vector<int> temp;
    int countResult(vector<int>& nums, int l, int r) {
        if (l >= r) return 0; // 如果只有一个元素,逆序数为0
        int ans = 0, mid = (l + r) >> 1;
        ans += countResult(nums, l, mid);
        ans += countResult(nums, mid + 1, r);
        int k = l, p1 = l, p2 = mid + 1;
        while (p1 <= mid || p2 <= r) {
            if ((p2 > r) || (p1 <= mid && nums[p1] <= nums[p2])) {
                temp[k++] = nums[p1++];
            } else {
                temp[k++] = nums[p2++];
                ans += (mid - p1 + 1);
            }
        }
        for (int i = l; i <= r; i++) nums[i] = temp[i];
        return ans;
    }
    int reversePairs(vector<int>& nums) {
        while (temp.size() < nums.size()) temp.push_back(0);
        return countResult(nums, 0, nums.size() - 1);       
    }
};

23. 合并 K 个升序链表 - 力扣(LeetCode)

这道题其实跟之前的文件排序演示代码的逻辑没有本质区别,只不过这道题可以用到堆来加速。

class Solution {
public:
    struct CMP {
        bool operator()(ListNode *p, ListNode *q) {
            return p->val > q->val;
        }
    };
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        priority_queue<ListNode *, vector<ListNode *>, CMP> q;
        for (auto x : lists) {
            if (x == nullptr) continue;
            q.push(x);
        }
        ListNode ret, *p = &ret;
        while (!q.empty()) {
            ListNode *cur = q.top();
            q.pop();
            p->next = cur;
            p = cur;
            if (cur->next) q.push(cur->next);
        }
        return ret.next;
    }
};

148. 排序链表 - 力扣(LeetCode)


 

如何用归并排序实现链表的排序呢?下面这段代码还是很具有典型意义的用链表来实现过程。

lass Solution {
public:
    ListNode *mergeSort(ListNode *head, int n) {
        if (head == nullptr || head->next == nullptr) return head;
        int l = n / 2, r = n - l;
        ListNode *lp = head, *rp = lp, *p;
        for (int i = 1; i < l; i++, rp = rp->next);
        p = rp, rp = rp->next;
        p->next = nullptr;
        lp = mergeSort(lp, l); // left Sort
        rp = mergeSort(rp, r); // right Sort
        ListNode ret;
        p = &ret;
        while (lp || rp) {
            if (rp == nullptr || (lp && lp->val < rp->val)) {
                p->next = lp;
                lp = lp->next;
                p = p->next;
            } else {
                p->next = rp;
                rp = rp->next;
                p = p->next;
            }
        }
        return ret.next;
    }
    ListNode* sortList(ListNode* head) {
        int n = 0;
        ListNode *p = head;
        while (p) p = p->next, n += 1;
        return mergeSort(head, n);
    }
};

1305. 两棵二叉搜索树中的所有元素 - 力扣(LeetCode)

用中序遍历,归并两颗子树,也是具有一定综合性的题。(怎么说的跟考研数学似的。。。)

class Solution {
public:
    void getResult(TreeNode *root, vector<int> &arr) {
        if (root == nullptr) return ;
        getResult(root->left, arr);
        arr.push_back(root->val);
        getResult(root->right, arr);
        return ;
    }
    vector<int> getAllElements(TreeNode* root1, TreeNode* root2) {
        vector<int> lnums, rnums;
        getResult(root1, lnums);
        getResult(root2, rnums);
        vector<int> ret;
        int p1 = 0, p2 = 0;
        while (p1 < lnums.size() || p2 < rnums.size()) {
            if (p2 >= rnums.size() || (p1 < lnums.size() && lnums[p1] < rnums[p2])) {
                ret.push_back(lnums[p1++]);
            } else {
                ret.push_back(rnums[p2++]);
            }
        }
        return ret;
    }
};

327. 区间和的个数 - 力扣(LeetCode)

一说到区间和值,就能想到前缀和。区间和等于前缀和数组中两项相减的值。问题就变成了,前缀和数组中,有多少对 lower <= sun[i]-sum[j] <= upper (i>j)

利用左右区间的有序性,加速查找的过程。

算法解题过程的封装思维:当我们将问题转化成另一个问题的时候,我们就忘掉前面的问题到底是什么,只需专注解决当前这个独立的问题。而不是脑子里一团乱麻。

class Solution {
public:
    int countTwoPart(vector<long long> &sum, int l1, int r1, int l2, int r2, int lower, int upper) {
        int ans = 0, k1 = l1, k2 = l1;
        for (int j = l2; j <= r2; j++) {
            long long a = sum[j] - upper;
            long long b = sum[j] - lower;
            while (k1 <= r1 && sum[k1] < a) k1++;
            while (k2 <= r1 && sum[k2] <= b)k2++;
            ans += k2 - k1;
        }
        return ans;
    }
    int mergeSort(vector<long long> &sum, int l, int r, int lower, int upper) {
        if (l >= r) return 0; // 只有一个元素的话,根本找不到数值对。
        int mid = (l + r) >> 1, ans = 0;
        ans += mergeSort(sum, l, mid, lower, upper);
        ans += mergeSort(sum, mid + 1, r, lower, upper);
        ans += countTwoPart(sum, l, mid, mid + 1, r, lower, upper);
        int k = l, p1 = l, p2 = mid + 1;
        while (p1 <= mid || p2 <= r) {
            if (p2 > r || (p1 <= mid && sum[p1] < sum[p2])) {
                temp[k++] = sum[p1++];
            } else {
                temp[k++] = sum[p2++];
            }
        }
        for (int i = l; i <= r; i++) sum[i] = temp[i];
        return ans; 
    }
    vector<long long> temp;
    int countRangeSum(vector<int>& nums, int lower, int upper) {
       vector<long long> sum(nums.size() + 1);
       while (temp.size() < sum.size()) temp.push_back(0);
       sum[0] = 0;
       for (int i = 0; i < nums.size(); i++) sum[i + 1] = sum[i] + nums[i];
       return mergeSort(sum, 0, sum.size() - 1, lower, upper);

    }
};

本质上还是利用了分治的思想。核心的过程就是如何计算跨左右两半部分的过程

315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

已经求得左右半边各自比它小的元素。两个区间合并。

class Solution {
public:
    // 归并排序的思想:分两个区间,统计两个区间的性质。
    // 在归并的过程中,左右两个有序区间,合并的时候,从大到小的顺序排,左边区间内,如果元素大于右边,则左边的元素比他小的个数应当加上右边r-p2+1

    struct Data {
        Data(int val, int ind) : val(val), ind(ind), cnt(0) {}
        bool operator > (const Data &a) {
            return val > a.val;
        }
        int val, ind, cnt;
    };
    void mergeSort(vector<Data> &arr, int l, int r) {
        if (l >= r) return ; // 如果只剩下一个元素,那就计算不了左右两边的统计性质
        int mid = (l + r) >> 1;
        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);
        int k = l, p1 = l, p2 = mid + 1;
        while (p1 <= mid || p2 <= r) {
            if (p2 > r || (p1 <= mid && arr[p1] > arr[p2])) {
                arr[p1].cnt += r - p2 + 1;
                temp[k++] = arr[p1++];
            } else {
                temp[k++] = arr[p2++];
            }
        }
        for (int i = l; i <= r; i++) arr[i] = temp[i];
        return ;
    }
    vector<Data> temp;
    vector<int> countSmaller(vector<int>& nums) {
        vector<Data> arr;
        for (int i = 0; i < nums.size(); i++) arr.push_back(Data{nums[i], i});
        while (temp.size() < nums.size()) temp.push_back(Data{0, 0});
        mergeSort(arr, 0, arr.size() - 1);
        vector<int> ret(nums.size());
        for (auto x : arr) ret[x.ind] = x.cnt;
        return ret;
    }
};

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

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

相关文章

Servlet 初步学习

文章目录 Servlet1 简介2 快速入门3 执行流程4 生命周期5 方法介绍6 体系结构7 urlPattern配置8 XML配置 Servlet 1 简介 Servlet是JavaWeb最为核心的内容&#xff0c;它是Java提供的一门 动态 web资源开发技术。 使用Servlet就可以实现&#xff0c;根据不同的登录用户在页面…

基于IMX6ULLmini的linux裸机开发系列八:按键处理实验

目录 GIC相关寄存器 GPIO中断相关寄存器 中断服务函数表 中断向量表偏移位置 make有报错 解决方法&#xff1a;error: for loop initial declarations are only allowed in C99 mode_‘for’ loop initial declarations are only allowed i_Young_2717的博客-CSDN博客 GIC…

19-普通组件的注册使用

普通组件的注册使用-局部注册 一. 组件注册的两种方式:1.局部注册:只能在注册的组件内使用 (1) 创建 vue 文件(单文件组件) (2) 在使用的组件内导入,并注册 components:{ 组件名: 组件对象 } // 导入需要注册的组件 import 组件对象 from.vue文件路径 import HmHeader from ./…

element表格多选实现

表格实现多选 实现表格多选很简单&#xff0c;只需要在表格里加上一列即可&#xff0c;加完之后就会在表格里出现一列白色的四方块按钮&#xff0c;可以多选&#xff0c;也可以单选 <el-table-columntype"selection"width"55"align"center"&…

第17集丨Vue中的render函数

目录 一、脚手架中不能使用template配置二、基本使用三、关于不同版本的Vue 一、脚手架中不能使用template配置 // 引入vue import Vue from vue import App from ./Appnew Vue({el:#app,template:<h1>hhh</h1>,comments:{App},})上面案例中&#xff0c;配置了temp…

Python标准库-追踪异常,定位问题-traceback

在日常的编程过程中&#xff0c;我们经常会遇到各种错误和异常。而当程序发生异常时&#xff0c;了解如何有效地追踪异常信息并定位问题&#xff0c;是每个开发者必备的技能之一。 Python 提供了一个强大的工具&#xff0c;称为 Traceback&#xff0c;它可以帮助我们跟踪异常的…

通过 OpenAI 引入superalignment

推荐&#xff1a;使用 NSDT场景编辑器助你快速搭建可二次编辑的3D应用场景 首席执行官Sam Altman曾多次谈到AI的安全性&#xff0c;例如在美国参议院委员会上&#xff0c;他说&#xff1a; “我认为如果这项技术出错&#xff0c;它可能会出错......我们想对此直言不讳。我们希…

Vue 2 处理边界情况

访问元素和组件 通过Vue 2 组件基础一文的学习&#xff0c;我们知道组件之间可以通过传递props或事件来进行通信。 但在一些情况下&#xff0c;我们使用下面的方法将更有用。 1.访问根实例 根实例可通过this.$root获取。 我们在所有子组件中都可以像上面那样访问根实例&…

使用 umap 图形化展示原文在嵌入后的位置情况

使用 umap_plot 图形化展示原文在嵌入后的位置情况 1. 效果展示2. 工具函数3. 示例代码14. 示例代码2 1. 效果展示 2. 工具函数 import umap import altair as altfrom numba.core.errors import NumbaDeprecationWarning, NumbaPendingDeprecationWarning import warningswar…

对前端PWA应用的部分理解和基础Demo

一、什么是PWA应用&#xff1f; 1、PWA简介 ​ 渐进式Web应用&#xff08;Progressive Web App&#xff09;&#xff0c;简称PWA&#xff0c;是 Google 在 2015 年提出的一种使用web平台技术构建的应用程序&#xff0c;官方认为其核心在于Reliable&#xff08;可靠的&#xf…

kubernetes(namespace、pod、deployment、service、ingress)

NameSpace NameSpace名称空间 用来隔离资源&#xff0c;但是不隔离网络 使用命令行&#xff1a; kubectl create ns hello #创建 kubectl delete ns hello #删除 kubectl get ns #查看使用配置文件&#xff1a; vi hello.yamlapiVersion: v1 kind: Namespace metadata:name…

Transformer 相关模型的参数量计算

如何计算Transformer 相关模型的参数量呢&#xff1f; 先回忆一下Transformer模型论文《Attention is all your need》中的两个图。 设Transformer模型的层数为N&#xff0c;每个Transformer层主要由self-attention 和 Feed Forward组成。设self-attention模块的head个数为 …

回归预测 | MATLAB实现CSO-SVM布谷鸟优化算法优化支持向量机多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现CSO-SVM布谷鸟优化算法优化支持向量机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现CSO-SVM布谷鸟优化算法优化支持向量机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09;效果一…

移植PeerTalk开源库IOS的USB通信监听服务到QT生成的FFmpeg工程

1.添加生成的PeerTalk库 下图选中部分为FFmpeg依赖库 将USB通信服务的m与h文件添加到工程 因为OC文件使用了弱指针,所以要启用弱指针支持 因为FFmpeg拉流动用到本地网络,所以要在plist文件中启动本地网络使用 设置PeerTalk为嵌入模式 设置Runpath Search Paths为@executable_p…

FPGA:uart原理+tx发送模块+rx接收模块

文章目录 一、串口通信二、UART通信三、tx发送模块四、rx模块接收 一、串口通信 处理器与外部设备通信的两种方式&#xff1a; 串行通信&#xff1a; 指数据的各个位使用多条数据线同时进行传输。 并行通信&#xff1a; 将数据分成一位一位的形式在一条数据线上逐个传输。 串…

1.flink快速入门

前言 下图表示的是一个简单的flink-job的计算图&#xff0c;这种图被称为DAG(有向无环图)&#xff0c;表示的这个任务的计算逻辑&#xff0c;无论是spark、hive、还是flink都会把用户的计算逻辑转换为这样的DAG&#xff0c;数据的计算按照DAG触发&#xff0c;理论上只要构建出…

spring源码分析bean的生命周期(下)

doGetBean()执行过程 createBean()执行过程 一、DependsOn注解 spring创建对象之前会判断类上是否加了DependsOn注解&#xff0c;加了会遍历然后会添加到一个map中&#xff0c;spring会先创建DependsOn注解指定的类 二、spring类加载器 在合并BeanDefinition&#xff0c;确定…

centos7.9和redhat6.9 离线升级OpenSSH和openssl (2023年的版本)

升级注意事项&#xff01; 1、多开几个连接窗口&#xff08;xshell&#xff09;&#xff0c;避免升级openssh失败无法再次连接终端&#xff0c;否则要跑机房了。 2、可开启telnet服务、vnc服务、打快照。多几个“保命”的路数。一、centos7.9的信息 [rootnode2 ~]# openssl v…

1391. 检查网格中是否存在有效路径;2502. 设计内存分配器;1638. 统计只差一个字符的子串数目

核心思想&#xff1a;并查集。枚举网格中的块&#xff0c;把能连通的连通在一起&#xff0c;最后看&#xff08;0&#xff0c;0&#xff09;和&#xff08;m-1,n-1&#xff09;是否连通&#xff0c;然后网格中的每个点坐标是二维的&#xff0c;然后通过x*ny转换为一维&#xff…

大数据课程K2——Spark的RDD弹性分布式数据集

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解Spark的RDD结构; ⚪ 掌握Spark的RDD操作方法; ⚪ 掌握Spark的RDD常用变换方法、常用执行方法; 一、Spark最核心的数据结构——RDD弹性分布式数据集 1. 概述 初学Spark时,把RDD看…