【Hello Algorithm】归并排序及其面试题

news/2024/12/29 5:40:59/

作者:@小萌新
专栏:@算法
作者简介:大二学生 希望能和大家一起进步
本篇博客简介:介绍归并排序和几道面试题

归并排序及其面试题

  • 归并排序
    • 归并排序是什么
    • 归并排序的实际运用
    • 归并排序的迭代写法
    • 归并排序的时间复杂度
  • 归并排序算法题
    • 小和问题
    • 翻转对问题

归并排序

归并排序是什么

归并排序是一种基于归并操作的有效的排序算法
它是采用分治法的一个典型的应用 将一个大的数组分成两个或多个小的数组 对每个小的数组进行排 然后再将排好序的小数组合并成一个有序的大数组

其实它的中心思想和递归差不多 ---- 分治

归并排序的实际运用

我们下面会用图解和代码的方式来详细介绍下归并排序

现在给我们一个大的无序数组

在这里插入图片描述
要求我们使用归并排序的思路来将这个数组进行排序

首先第一步我们要将这个数组分为左右两个部分 并且保证这个数组的左右两个部分是有序的

我们分隔之后能看到这样子的结果

在这里插入图片描述
但是我们发现这两个数组依然不是有序的 不满足进行合并的条件

所以说我们就要继续分割 直到有序为止

那么什么时候我们可以说一个数组是有序的呢? 当这个数组只有一个元素的时候

在这里插入图片描述
所以说该无序数组会被分隔成八个有序的数组(单个元素肯定是有序的)

之后我们开始对这八个有序的数组两两开始合并

在这里插入图片描述

合并成四个有序的数组之后继续开始合并

在这里插入图片描述

合并成两个有序的数组之后继续开始合并

在这里插入图片描述

最终我们就能得到一个有序的数组了 以上就是归并排序的思路 下面我们来看代码

#include <iostream>    
using namespace std;    void Merge(int arr[], int left , int mid , int right)    
{    int temp[right - left + 1];    int i = left;    int j = mid + 1;    int k = 0;    while(i <= mid && j <= right)    {    if (arr[i] <= arr[j])    {    temp[k] = arr[i];    i++;    }    else    {    temp[k] = arr[j];    j++;    }    k++;    }    while(i <= mid)    {    temp[k++] = arr[i++];    }    while(j <= right)    {    temp[k++] = arr[j++];    }    for (int i = left ; i <= right ; i++)    {    arr[i] = temp[i - left];    }    
}    void MergeSort(int arr[],int left , int right)    
{    if (left >= right)    {    return;                                                                                                                                                                                                                                                                                                                                                                                                                                                  }    int mid = left + (right - left) / 2;    MergeSort(arr , left , mid);    MergeSort(arr , mid+1 , right);    Merge(arr , left , mid , right);    // sorted    
}    

解释下上面这段代码

首先这段代码分为MergeSort和Merge两个函数

其中MergeSort函数是我们暴露给外面的接口函数 Merge函数是为了实现归并封装的一个子函数

MergeSort函数中有两次递归 这两次递归的作用是将数组两端变为有序状态 最后我们调用Merge将这两端有序的数组进行排序

我们可以写出一段代码将其验证

int main()
{int arr[] = {10 , 6, 7, 1, 3, 9, 4, 2};                                                               MergeSort(arr , 0 , 7);for (int i = 0; i < 8; i++){cout << arr[i] << "  " ;}cout << endl;return 0;
}

运行结果如下

在这里插入图片描述
我们可以发现运行后的结果是有序的

归并排序的迭代写法

我们可以省略递归的步骤 使用迭代的方式来写出归并排序

我们能够保证单个元素肯定是有序的 (其实递归写法最后也是从单个元素开始)

那么我们就可以将单个元素看作是一个有序的数组 直接开始归并 之后我们就能得到若干个两个元素的有序数组了 将这些两个元素的有序数组再次进行归并 我们就能得到若干个有四个元素的有序数组 依次类推

我们将两个有序数组进行归并 首先要做的就是找到这两个要进行归并的数组 前面在数量充足的时候我们可以保证没问题 但是到了后面我们可能会发现剩余的元素不能完美凑成两个相同元素的数组了

于是在我们分组的过程中会遇到一些问题

  1. 左数组的右边界越界了
  2. 右数组的左边界越界了
  3. 右数组的右边界越界了

至于为什么不存在左数组的左边界越界的情况 因为此时说明前面刚好排序完毕 后面不属于我们要排序的部分了

下面我们分别讨论这三种问题的解法

  1. 左数组的右边界越界 此时我们将剩余的部分全部纳入左数组 无需排序 因为给我们的左右数组是排好序的 如果说只存在左数组的情况就说明该段是有序的 我们就无序进行下面的操作了
  2. 右数组的左边界越界 此时和情况一相同 大家可以画图理解下
  3. 右数组的右边界越界 此时我们不管越界的部分 将右数组的右边界设置为数组末尾的位置 之后进行归并排序

代码表示如下

void MergeSort(int arr[] , int len)    
{    if (len < 2)    {    return;    }    int mergesize = 1;    while(mergesize < len)    {    int L = 0;                                                                                                                           while(L < len)    {    int M = L + mergesize -1;    if (M >= len)    {    break;    }    int R = min(len - 1 ,M + mergesize);    Merge(arr , L , M , R);    L = R + 1;    }    mergesize <<= 1;    }     
} 

替换递归部分的代码一共就这么多 Merge代码可以复用上面的

逻辑很简单 从数组长度为一开始(一定有序)归并 分别找出两个数组的左右边界 再考虑上前面的三种特殊情况就可以了

再每次归并完毕之后我们更新左数组的左下标

数组长度为一归并排序完毕之后我们开始归并数组长度为2的部分 (每次扩大两倍)

最后我们测试下代码运行结果

int main()
{int arr[] = {10 , 6, 7, 1, 3, 9, 4, 2};                                                               MergeSort(arr , 0 , 7);for (int i = 0; i < 8; i++){cout << arr[i] << "  " ;}cout << endl;return 0;
}

运行结果如下

在这里插入图片描述

归并排序的时间复杂度

归并排序的时间复杂度是N*logN 这是一个很优秀的时间复杂度

那么相比起时间复杂度为N平方的排序它优在哪里呢?

实际上它优秀的地方在于 每两个数之间只比较了一次

拿选择排序来具体 它要比较几乎整个数组才能够选择出一个最大或者最小的数 这是极其浪费的做法

我们可以利用归并排序每两个数之间只比较一次这个特性去解决很多问题

下面是一些面试算法题

归并排序算法题

小和问题

题目要求我们计算数组的小和 完整的题目和示例如下

在这里插入图片描述

该题目出自于牛客网编程题 – 数组的小和

做这道题目最笨的方法就是多次遍历整个数组 每次找当前位置的小和 很显然这样子做的时间复杂度是N的平方 使用这种解法在面试的过程中是没有分数的!

所以我们必须要想出一个更加优秀的解法出来 实际上我们可以利用归并排序每两个数只比较一次的特点来解决

首先我们回答下下面这个问题

找出数组中所有的小和 是不是等价于将数组从左到右的比较一遍找出小于数字出现的次数啊

如果说一个数字在比较的时候小于另外一个数字 我们就叫它小于数字

在这里插入图片描述

比如说数组中只有两个元素3和4 在比较的时候我们即可以说有一个数字3也可以说3出现了一次 最后得到的结果是等价的

而我们使用归并排序的过程就是这个从左到右比较的过程

代码运行如下

#include <iostream>
using namespace std;
#include <vector>long long Merge(vector<int>& nums , int left , int mid , int right)
{vector<int> temp(right-left+1, 0);int i = left;int j = mid + 1;int k = 0;long long SUM = 0;while(i <= mid && j <= right){if (nums[i] <= nums[j]){// small sumSUM += (right - j + 1) * nums[i];temp[k] = nums[i];i++;}else{temp[k] = nums[j];j++;}k++;}while(i <= mid){temp[k++] = nums[i++];}while(j <= right){temp[k++] = nums[j++];}for (i = left ; i <= right ; i++){nums[i] = temp[i - left];}return SUM;
}long long SmallNums(vector<int>& nums , int left , int right)
{if (left >= right){return 0;}int mid = left + (right - left) / 2; return SmallNums(nums, left, mid)+SmallNums(nums, mid + 1, right)+Merge(nums,  left, mid,  right);
}int main() 
{long n = 0;cin >> n;vector<int> nums(n , 0);for (int i = 0; i < n ; i++){cin >> nums[i];}long long sum = SmallNums(nums, 0, n-1);cout << sum;return 0;
}

我们可以发现 实际上这段代码对比归并排序只多出了一行代码

  • 一行代码
   SUM += (right - j + 1) * nums[i];

这就是计算小和的步骤了

运行结果如下
在这里插入图片描述

翻转对问题

让你计算一个数组中的翻转对 题目和示例出现在lc493题

在这里插入图片描述

值得我们注意的是 该翻转对的下标必须要是左下标小于右下标 也就是说该翻转对必须要按照从左到右的顺序 并且只比较一遍 这里我们就应该想到使用我们的归并排序了

但是题目中的要求并不是单纯的大于小于 而是一个大于两倍的关系

这其实就是这道题目相比较于其他考查归并排序题目的一个难点 解决方案是这样子的

我们首先找出符合题目要求的数据 最后再进行归并排序

使用这个思路去解决问题就好了

也就是说归并之前我们先用一段代码来找到答案 代码表示如下

    int MergeAndFind(vector<int>& nums , int left , int mid , int right){int ans = 0;int windowr = mid + 1; for (int i = left ; i <= mid ; i++){while(windowr <= right && nums[i] > (long)(nums[windowr] << 1)){windowr++;}ans += windowr - mid - 1;}vector<int> help(right - left + 1 , 0);int i = left;int j = mid + 1;int k = 0;while(i <= mid && j <= right){if (nums[i] <= nums[j]){help[k] = nums[i];i++;}else {help[k] = nums[j];j++;}k++;}while(i <= mid){help[k++] = nums[i++];}while (j <= right){help[k++] = nums[j++];}for (i = left ; i <= right ; i++){nums[i] = help[i - left];}return ans;}

剩下的代码就是单纯的归并排序了 没有什么好讲解的 完整代码如下

class Solution {
public:int MergeAndFind(vector<int>& nums , int left , int mid , int right){int ans = 0;int windowr = mid + 1; for (int i = left ; i <= mid ; i++){while(windowr <= right && (long long)nums[i] >((long long)nums[windowr] * 2)){windowr++;}ans += windowr - mid - 1;ii}vector<int> help(right - left + 1 , 0);int i = left;int j = mid + 1;int k = 0;while(i <= mid && j <= right){if (nums[i] <= nums[j]){help[k] = nums[i];i++;}else {help[k] = nums[j];j++;}k++;}while(i <= mid){help[k++] = nums[i++];}while (j <= right){help[k++] = nums[j++];}for (i = left ; i <= right ; i++){nums[i] = help[i - left];}return ans;}int _reversPairs(vector<int>& nums , int left , int right){if (left >= right){return 0;}int mid = left + (right - left) / 2;return _reversPairs(nums , left , mid)+_reversPairs(nums , mid + 1 , right)+MergeAndFind(nums , left , mid , right); }int reversePairs(vector<int>& nums) {return  _reversPairs(nums , 0 , nums.size() -1);}
};

这道题目注意的有两点

  • 我们乘2必须要使用 “ * ” 运算符 不能使用位运算 否则负数就有可能出问题
  • 在进行乘法的时候要进行类型转换 不然可能会有数据溢出的问题

以上就是归并排序的写法以及归并排序思想可以解决的一些面试题


http://www.ppmy.cn/news/69499.html

相关文章

2023-04-30:用go语言重写ffmpeg的resampling_audio.c示例,它实现了音频重采样的功能。

2023-04-30&#xff1a;用go语言重写ffmpeg的resampling_audio.c示例&#xff0c;它实现了音频重采样的功能。 答案2023-04-30&#xff1a; resampling_audio.c 是 FFmpeg 中的一个源文件&#xff0c;其主要功能是实现音频重采样。 音频重采样是指将一段音频数据从一个采样率…

mysql事务及搜索引擎

mysql事务后半部分 加快查询速度索引会自动排序&#xff0c;&#xff08;升序&#xff09; select * from t1&#xff1b;全盘扫描 where可以索引查找show create table 索引是一个排序的列表&#xff0c;包含字段值和相应行数据的物理地址 事务是一种机制&#xff0c;一个…

Spark大数据处理讲课笔记3.5 RDD持久化机制

文章目录 零、本讲学习目标一、RDD持久化&#xff08;一&#xff09;引入持久化的必要性&#xff08;二&#xff09;案例演示持久化操作1、RDD的依赖关系图2、不采用持久化操作3、采用持久化操作 二、存储级别&#xff08;一&#xff09;持久化方法的参数&#xff08;二&#x…

android log的使用

现在在分析一个android netd的问题&#xff0c;只要一开启热点&#xff0c; for (String ifname : added) {try {Log.d(TAG, "TetheredState, processMessage CMD_TETHER_CONNECTION_CHANGED, add mIfaceName " mIfaceName " ifname " ifname );mNetd.…

etcd的Watch原理

在 Kubernetes 中&#xff0c;各种各样的控制器实现了 Deployment、StatefulSet、Job 等功能强大的 Workload。控制器的核心思想是监听、比较资源实际状态与期望状态是否一致&#xff0c;若不一致则进行协调工作&#xff0c;使其最终一致。 那么当你修改一个 Deployment 的镜像…

蛋糕烘焙店小程序开发 让生活多点甜

蛋糕甜品因为较高的颜值、香甜的口感深受大众喜欢&#xff0c;当我们路过一家蛋糕烘焙店的时候&#xff0c;飘香的味道让我们流连忘返。但是互联网时代&#xff0c;各个行业都在转型&#xff0c;蛋糕烘焙店也需要由传统线下店面向线上线下结合的方式转变&#xff0c;以求摆脱区…

LeetCode 63 不同路径 II

题目&#xff1a; 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。现在考虑网格中有障碍物。那么从左…

Golang笔记:使用os.Args和flag包编写命令行界面(CLIs)

文章目录 目的os.ArgsflagFlagSet总结 目的 命令行界面&#xff08;Command-line Interfaces&#xff09;是比较常用的一种软件形式。对于大部分开发运维人员来说很多时候CLIs可能比图形界面更加方便。软件开发时也经常会有需要开发命令行界面形式软件的情况&#xff0c;使用G…