【初阶算法4】——归并排序的详解,及其归并排序的扩展

简介: 【初阶算法4】——归并排序的详解,及其归并排序的扩展

学习内容:

通过上面的学习目标,我们可以列出要学习的内容:

  1. 学习归并排序的思想
  2. 使用代码进行编写归并排序
  3. AC两道经典的OJ题目
  4. 练习几道LeetCode的题目
  5. 总结在什么情况下使用归并排序的算法

一、介绍归并排序

      在之前的学习中,我们可能或多或少的接触过排序算法,也知道一些排序算法:冒泡排序选择排序插入排序。不过这些排序有共同点——时间复杂度为O(N * N),时间上不是那么有效,我们需要进行进一步的优化,从而就有了我们这一篇博客讲述的归并排序,其时间复杂度为:O(N * logN)空间复杂度为:O(N)

1.1 归并排序的思路

      归并排序,顾名思义,“归并”的含义是将两个两个以上的有序表合并一个新的有序表。假设待排序数表有N个记录,则可将其视为N个有序的子表,每一个子表的长度是1,然后两两归并,得到[ N / 2 ]个长度为2或1的有序表;继续两两归并……如此重复,直到合并成一个长度为N有序表为止。下面小编将画图带大家理解:

      而这个排序刚开始有点像这个相反的做法,其思路是:先将这一大长串数组分割,因为其是一个递归的过程,就是将数组一直分割,直到每一个数组长度为1,此时每一个数组都是有序的;之后要开始合并数组,合并数组时将进行比较,看需要来判断是升序或降序,合并的时候就如同上方的合并一样,最后能够得出一个有序的数组。

1.2 归并排序的代码

这个算法有三个部分组成,请看下面我一一为大家进行讲解:

1.2.1 mergesort函数部分

void mergesort(int arr[], int left, int right)
{
    int sz = right - left + 1; //计算数组的长度
    if(arr == NULL || sz < 2)  //如果数组的首元素不存在,则不用排序;
        return ;               //如果数组的长度只有一个,则也不用排序
    process(arr, left, right); //进入process函数部分
}

1.2.2 process函数部分

void process(int arr[], int left, int right)
{
    int sz = right - left + 1;   //与mergesort函数的部分作用是一样的
    if(arr == NULL || sz < 2)
        return ;
//分割数组
    int mid = left + ((right - left) >> 1);  //计算出这段数组中正中心的位置坐标
    process(arr, left, mid);     //递归过程中,将数组分为左右部分,这是左部分
    process(arr, mid + 1, right);//这是右部分
    merge(arr, left, mid, right);
}

1.2.3 merge函数部分

void merge(int arr[], int left, int mid, int right)
{
    int sz = right - left + 1;
    int* help = (int*)malloc(sizeof(int) * sz); //构造辅助数组
    int i = 0;
    int p1 = left;  //建立指针
    int p2 = mid + 1;
    while(p1 <= mid && p2 <= right) //如果左指针与右指针都不越界,则进入循环
    {
        help[i++] = arr[p1] > arr[p2] ? arr[p1++] : arr[p2++]; //进行比较,交换数据
    }
    while(p1 <= mid) //将左边剩余的数据拷贝到辅助数组中
    {
        help[i++] = arr[p1++];
    }
    while(p2 <= right) //将右边剩余的数据拷贝到辅助数组中
    {
        help[i++] = arr[p2++];
    }
    for(int i = 0; i < sz; i++) //最后将辅助数组中的数据转移到原数组中
    {
        arr[left + i] = help[i];
    }
}

二、AC两道经典的OJ题目

题目一:逆序对问题

题目:

      在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

思路:

      在这道题中,我们要注意题目中标红的描述,这个逆序对永远都是前一个数字的坐标位置永远小于后一个数字的坐标位置。这一点就和归并排序不谋而合,归并排序算法总是将左边的数字与右边的数字进行比较,然后进行排序。而这道题要求的是逆序对的个数,我们可以在进行比较的时候进行计数,这就是大致思路。

      将这个数组进行降序排序还是逆序排序呢?如果是降序排序时,我们将左边有右边进行比较,如果左边大于右边,则大于右边所有的数字,进行计数即可;如果是升序排序可能会出现漏项的情况,自然排除升序,采用降序。

代码:

int merge(int arr[], int left, int mid, int right)
{
    int sz = right - left + 1;
    int* help = (int*)malloc(sizeof(int) * sz);
    int p1 = left;
    int p2 = mid + 1;
    int i = 0;
    int ret = 0;
    while(p1 <= mid && p2 <= right)
    {
        ret += arr[p1]>arr[p2]?(right - p2 + 1):0; //如果左边的数字大于右边的数字,则计算右边
//一共有多少数字
        help[i++] = arr[p1] > arr[p2]?arr[p1++]:arr[p2++];
    }
    while(p1 <= mid)
    {
        help[i++] = arr[p1++];
    }
    while(p2 <= right)
    {
        help[i++] = arr[p2++];
    }
    for(int i = 0; i < sz; i++)
    {
        arr[left + i] = help[i];
    }
    return ret;
}
 
int process(int arr[], int left, int right)
{
    int sz = right - left + 1;
    if(arr == NULL || sz < 2)
        return 0;
    int mid = left + ((right - left) >> 1);
    return process(arr, left, mid) + process(arr, mid + 1, right) + merge(arr, left, mid, right);
}
 
int revOrder(int arr[], int left, int right)
{
    int sz = right - left + 1;
    if(arr == NULL || sz < 2)
        return 0;
    return process(arr, left, right);
}
 
int reversePairs(int* nums, int numsSize){
    int left = 0;
    int right = numsSize - 1;
    int ans = revOrder(nums, left, right);
    return ans;
}

题目二:小和问题

题目:

      在一个数组中,每一个数左边比当前的数小进行累加起来,叫做这个数组的小和,求一个数组的小和。

思路:

      同理,看题目中被标红的描述,进行降序排序,如果左边的数字小于右边的数字,则将左边的数字乘右边有多少个大于他的个数,进行累加即可。

代码:

int merge(int arr[], int left, int mid, int right)
{
  int sz = right - left + 1;
  int* help = (int*)malloc(sizeof(int) * sz);
  int i = 0;
  int p1 = left;
  int p2 = mid + 1;
  int count = 0;
  while (p1 <= mid && p2 <= right)
  {
    count += arr[p1] < arr[p2] ? (right - p2 + 1)*arr[p1] : 0;
    help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
  }
  while (p1 <= mid)
  {
    help[i++] = arr[p1++];
  }
  while (p2 <= right)
  {
    help[i++] = arr[p2++];
  }
  for (int i = 0; i < sz; i++)
  {
    arr[left + i] = help[i];
  }
  return count;
}
 
int process(int arr[], int left, int right)
{
  int sz = right - left + 1;
  if (arr == NULL || sz < 2)
    return 0;
  int mid = left + ((right - left) >> 1);
  return process(arr, left, mid) + process(arr, mid + 1, right) + merge(arr, left, mid, right);
}
int reverseOrder(int arr[], int left, int right)
{
  int sz = right - left + 1;
  if (arr == NULL || sz < 2)
    return 0;
  return process(arr, left, right);
}
 
int main()
{
  int arr[] = { 1,3,4,2,5 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  int left = 0;
  int right = sz - 1;
  int ans = reverseOrder(arr, left, right);
  printf("%d\n", ans);
  return 0;
}

三、练习一道LeetCode的题目

题目:

      给定一个数组 nums ,如果 i < jnums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对。你需要返回给定数组中的重要翻转对的数量。

思路:

      前面的操作基本一样,只需将merge函数部分进行修改即可,将左边的指针不动,一一右边的数字进行比较,如果条件成立,就将指针向右移动一位,如果不成立跳出,结果加上右指针减去初始位置的个数

代码:

int process(int arr[], int left, int right)
{
    int sz = right - left + 1;
    if(arr == NULL || sz < 2)
        return 0;
    int mid = left + ((right - left) >> 1);
    int n1 = process(arr, left, mid);
    int n2 = process(arr, mid + 1, right);
    int ret = n1 + n2;
    int p1 = left;
    int p2 = mid + 1;
    int i = 0;
    while(p1 <= mid)
    {
        while(p2 <= right && (long long)arr[p1]>2*(long long)arr[p2])
        p2++;
        ret += (p2 - mid - 1);
        p1++;
    }
    p1 = left;
    p2 = mid + 1;
    int* help = (int*)malloc(sizeof(int) * sz);
    while(p1 <= mid && p2 <= right)
    {
        help[i++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
    }
    while(p1 <= mid)
    {
        help[i++] = arr[p1++];
    }
    while(p2 <= right)
    {
        help[i++] = arr[p2++];
    }
    for(int i = 0; i < sz; ++i)
    {
        arr[left + i] = help[i];
    }
    return ret;
}
 
int mergeSort(int arr[], int left, int right)
{
    int sz = right - left + 1;
    if(arr == NULL || sz < 2)
        return 0;
    return process(arr, left, right);
}
 
int reversePairs(int* nums, int numsSize){
    int left = 0;
    int right = numsSize - 1;
    return mergeSort(nums, left, right);
}

四、总结在什么情况下使用归并排序的算法

      小编觉得在数组对问题可能使用归并排序,尤其是一个数组对中满足一定条件,左边的左边小于右边的坐标时,可以考虑考虑归并排序算法的思想。


学习产出:

  1. 学习归并排序的思想
  2. 使用代码进行编写归并排序
  3. AC两道经典的OJ题目
  4. 练习几道LeetCode的题目
  5. 总结在什么情况下使用归并排序的算法
相关文章
|
1月前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
20 1
|
1月前
|
存储 搜索推荐 算法
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
|
1月前
|
存储 算法 搜索推荐
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
68 0
|
1月前
|
搜索推荐 Java Go
深入了解归并排序算法
深入了解归并排序算法
18 0
|
2月前
|
资源调度 算法
基于迭代扩展卡尔曼滤波算法的倒立摆控制系统matlab仿真
本课题研究基于迭代扩展卡尔曼滤波算法的倒立摆控制系统,并对比UKF、EKF、迭代UKF和迭代EKF的控制效果。倒立摆作为典型的非线性系统,适用于评估不同滤波方法的性能。UKF采用无迹变换逼近非线性函数,避免了EKF中的截断误差;EKF则通过泰勒级数展开近似非线性函数;迭代EKF和迭代UKF通过多次迭代提高状态估计精度。系统使用MATLAB 2022a进行仿真和分析,结果显示UKF和迭代UKF在非线性强的系统中表现更佳,但计算复杂度较高;EKF和迭代EKF则更适合维数较高或计算受限的场景。
|
3月前
|
算法 搜索推荐 Java
算法实战:手写归并排序,让复杂排序变简单!
归并排序是一种基于“分治法”的经典算法,通过递归分割和合并数组,实现O(n log n)的高效排序。本文将通过Java手写代码,详细讲解归并排序的原理及实现,帮助你快速掌握这一实用算法。
41 0
|
3月前
|
数据采集 搜索推荐 算法
【高手进阶】Java排序算法:从零到精通——揭秘冒泡、快速、归并排序的原理与实战应用,让你的代码效率飙升!
【8月更文挑战第21天】Java排序算法是编程基础的重要部分,在算法设计与分析及实际开发中不可或缺。本文介绍内部排序算法,包括简单的冒泡排序及其逐步优化至高效的快速排序和稳定的归并排序,并提供了每种算法的Java实现示例。此外,还探讨了排序算法在电子商务、搜索引擎和数据分析等领域的广泛应用,帮助读者更好地理解和应用这些算法。
40 0
|
4月前
|
存储 算法 搜索推荐
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
【7月更文挑战第12天】归并排序是高效稳定的排序算法,采用分治策略。Python 实现包括递归地分割数组及合并已排序部分。示例代码展示了如何将 `[12, 11, 13, 5, 6]` 分割并归并成有序数组 `[5, 6, 11, 12, 13]`。虽然 $O(n log n)$ 时间复杂度优秀,但需额外空间,适合大规模数据排序。对于小规模数据,可考虑其他算法。**
76 4
|
4月前
|
算法 搜索推荐 C#
|
5月前
|
搜索推荐 算法 Java
Java中的快速排序、归并排序和堆排序是常见的排序算法。
【6月更文挑战第21天】Java中的快速排序、归并排序和堆排序是常见的排序算法。快速排序采用分治,以基准元素划分数组并递归排序;归并排序同样分治,先分割再合并有序子数组;堆排序通过构建堆来排序,保持堆性质并交换堆顶元素。每种算法各有优劣:快排平均高效,最坏O(n²);归并稳定O(n log n)但需额外空间;堆排序O(n log n)且原地排序,但不稳定。
46 3