归并排序:从二路到多路

前言

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

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

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

大致流程示意图:

 

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

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

二路归并演示代码

#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.mfbz.cn/a/80966.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android12之com.android.media.swcodec无法生成apex问题(一百六十三)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

源于传承,擎领未来,新架构、新工艺下的“换心工程”——金融电子化访中电金信副总经理、研究院院长况文川

当前&#xff0c;商业银行的经营环境正在发生着深刻而复杂的变化&#xff0c;在深化改革主旋律的指引下&#xff0c;数字化转型已成为我国商业银行普遍认同、广泛采用的战略性举措。核心系统作为承载银行业务的关键支柱系统&#xff0c;一直是各银行在金融科技建设中重点关注和…

多种方法实现 Nginx 隐藏式跳转(隐式URL,即浏览器 URL 跳转后保持不变)

多种方法实现 Nginx 隐藏式跳转(隐式URL,即浏览器 URL 跳转后保持不变)。 一个新项目,后端使用 PHP 实现,前端不做路由,提供一个模板,由后端路由控制。 Route::get(pages/{name}, [\App\Http\Controllers\ResourceController::class, getResourceVersion])

Redis数据结构——快速列表quicklist、快表

定义 Redis中的数据结构&#xff0c;链表和压缩列表这两种数据结构是列表对象的底层实现方式。 当时考虑到链表的附加空间太大&#xff0c;节点的内存都是单独分配的&#xff0c;还会导致内存碎片化问题严重。 因此从Redis3.2开始&#xff0c;对列表的底层数据结构进行了改造&…

css学习1

1、样式定义如何显示元素。 2、样式通常保存至外部的css文件中。 3、样式可以使内容与表现分离。 4、css主要有两部分组成&#xff1a;选择器与一条或多条声明。 选择器通常为要改变的html元素&#xff0c;每条声明由一个属性和一个值组成。每个属性有一个值&#xff0c;属性…

C语言编程:最小二乘法拟合直线

本文研究通过C语言实现最小二乘法拟合直线。 文章目录 1 引入2 公式推导3 C语言代码实现4 测试验证5 总结 1 引入 最小二乘法&#xff0c;简单来说就是根据一组观测得到的数值&#xff0c;寻找一个函数&#xff0c;使得函数与观测点的误差的平方和达到最小。在工程实践中&…

leetcode15. 三数之和

这里保证1.元素a不会重复。2.所有解都是有序的。3.b和c元素不重复。所以解不会重复。 class Solution { public:vector<vector<int>> threeSum(vector<int>& nums) {std::vector<std::vector<int>> result;if (nums.size() < 3) return …

提升大数据技能,不再颓废!这6家学习网站是你的利器!

随着国家数字化转型&#xff0c;大数据领域对人才的需求越来越多。大数据主要研究计算机科学和大数据处理技术等相关的知识和技能&#xff0c;从大数据应用的三个主要层面&#xff08;即数据管理、系统开发、海量数据分析与挖掘&#xff09;出发&#xff0c;对实际问题进行分析…

更多openEuler镜像加入AWS Marketplace!

自2023年7月openEuler 22.03 LTS SP1正式登陆AWS Marketplace后&#xff0c;openEuler社区一直持续于在AWS上提供更多版本。 目前&#xff0c;openEuler22.03 LTS SP1 ,SP2两个版本及 x86 arm64两种架构的四个镜像均可通过AWS对外提供&#xff0c;且在亚太及欧洲15个Region开放…

UML图绘制 -- 类图

1.类图的画法 类 整体是个矩形&#xff0c;第一层类名&#xff0c;第二层属性&#xff0c;第三层方法。 &#xff1a;public- : private# : protected空格: 默认的default 对应的类写法。 public class Student {public String name;public Integer age;protected I…

深入理解【二叉树】

&#x1f4d9;作者简介&#xff1a; 清水加冰&#xff0c;目前大二在读&#xff0c;正在学习C/C、Python、操作系统、数据库等。 &#x1f4d8;相关专栏&#xff1a;C语言初阶、C语言进阶、C语言刷题训练营、数据结构刷题训练营、有感兴趣的可以看一看。 欢迎点赞 &#x1f44d…

java+springboot+mysql银行管理系统

项目介绍&#xff1a; 使用javaspringbootmysql开发的银行管理系统&#xff0c;系统包含超级管理员、管理员、客户角色&#xff0c;功能如下&#xff1a; 超级管理员&#xff1a;管理员管理&#xff1b;客户管理&#xff1b;卡号管理&#xff08;存款、取款、转账&#xff09…

C++系列-内存模型

内存模型 内存模型四个区代码区全局区栈区堆区内存开辟和释放在堆区开辟数组 内存模型四个区 不同区域存放的数据生命周期是不同的&#xff0c;更为灵活。 代码区&#xff1a;存放函数体的二进制代码&#xff0c;操作系统管理。全局区&#xff1a;存放全局变量&#xff0c;常…

【ES5和ES6】数组遍历的各种方法集合

一、ES5的方法 1.for循环 let arr [1, 2, 3] for (let i 0; i < arr.length; i) {console.log(arr[i]) } // 1 // 2 // 32.forEach() 特点&#xff1a; 没有返回值&#xff0c;只是针对每个元素调用func三个参数&#xff1a;item, index, arr &#xff1b;当前项&#…

Gradio部署应用到服务器不能正常访问

用Gradio部署一个基于ChatGLM-6B的应用&#xff0c;发布到团队的服务器上&#xff08;局域网&#xff0c;公网不能访问&#xff09;&#xff0c;我将gradio应用发布到服务器的9001端口 import gradio as gr with gr.Blocks() as demo:......demo.queue().launch(server_port90…

RocketMQ、Dashboard部署以及安全设置

RocketMQ、dashboard部署以及安全设置 一、启动RocketMQ1.1 下载RocketMQ1.2 修改配置文件1.2.1 修改nameServer Jvm内存配置1.2.2 修改broker参数 1.3 启动1.3.1 启动NameServer1.3.2 启动Broker1.3.3 测试是否启动成功1.3.3.1 测试消息发送1.3.3.2 测试消息接收1.3.3.3 Java程…

SparkSQL源码分析系列03-Antlr4分析测试

SparkSQL主要通过Antlr4定义SQL的语法规则&#xff0c;完成SQL词法&#xff0c;语法解析&#xff0c;最后将SQL转化为抽象语法树。所以有必要先了解下Antlr4的工作流程。 ANTLR4是什么&#xff1f; ANTLR 是 ANother Tool for Language Recognition 的缩写&#xff0c;官网&a…

3D沉浸式旅游网站开发案例复盘【Three.js】

Plongez dans Lyon网站终于上线了。 我们与 Danka 团队和 Nico Icecream 共同努力&#xff0c;打造了一个令我们特别自豪的流畅的沉浸式网站。 这个网站是专为 ONLYON Tourism 和会议而建&#xff0c;旨在展示里昂最具标志性的活动场所。观看简短的介绍视频后&#xff0c;用户…

数据结构基础

将节点构建成树 数据的结构逻辑结构集合线性结构树形结构图状结构 存储结构合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如…

vue2.0/vue3.0学习笔记——2022.08.16

vue2&#xff08;查漏补缺&#xff09; 一、vue基础 内置指令&#xff08;查漏补缺&#xff09; 1、v-text 更新元素的textContent 2、v-html 更新元素的innerHtml 3、v-cloak 防止闪现&#xff0c;与css配合: [v-cloak] {dispaly: none} 4、v-once 在初次动态渲染厚&#x…
最新文章