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

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

归并排序及其面试题

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

归并排序

归并排序是什么

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

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

归并排序的实际运用

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

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

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

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

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

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

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

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

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

#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 sum
            SUM += (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必须要使用 “ * ” 运算符 不能使用位运算 否则负数就有可能出问题
  • 在进行乘法的时候要进行类型转换 不然可能会有数据溢出的问题

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

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

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

相关文章

(十一)地理数据库创建——创建新的地理数据库

地理数据库创建——创建新的地理数据库 目录 地理数据库创建——创建新的地理数据库 1.地理数据库概述2.地理数据库建立一般过程2.1地理数据库设计2.2地理数据库建立2.2.1从头开始建立一个新的地理数据库2.2.2移植已经存在数据到地理数据库2.2.3用CASE工具建立地理数据库 2.3建…

Python 科研绘图可视化(后处理)Matplotlib - 2D彩图

Introduction 科研可视化是将数据和信息转化为可视化形式的过程&#xff0c;旨在通过图形化展示数据和信息&#xff0c;使得科研工作者能够更好地理解和分析数据&#xff0c;并从中发现新的知识和洞见。科研可视化可以应用于各种领域&#xff0c;如生物学、物理学、计算机科学…

C++类和对象再探

文章目录 const成员再谈构造函数成员变量的定义函数体内赋值初始化列表 隐式类型转换explicitstatic成员 const成员 我们知道在调用类的成员函数时,会有一个默认的this指针且这个this指针时不可以被修改的,例如在日期类中,会有隐式的Date * const this;注意这里默认会在this前…

一五一、web+小程序骨架屏整理

骨架屏介绍 请点击查看智能小程序骨架屏 车载小程序骨架屏 车载小程序为方便开发者设置骨架屏&#xff0c;在智能小程序的基础上抽取出骨架屏模板&#xff0c;开发者只需要在 skeleton 文件夹下配置config.json&#xff08;page 和骨架屏的映射关系文件&#xff09;即可生效骨…

第十四届蓝桥杯青少组模拟赛Python真题 (2022年11月8日)

第十四届蓝桥杯青少组模拟赛Python真题 (2022年11月8日) 编程题 第 1 题 问答题 二进制位数 十进制整数2在十进制中是1位数,在二进制中对应10,是2位数。 十进制整数22在十进制中是2位数,在二进制中对应10110,是5位数。 请问十进制整数2022在二进制中是几位数? 第2题问…

Pr 拍立得风格图片展示

哈喽&#xff0c;各位小伙伴&#xff01;今天我们来学习一下如何制作拍立得风格的照片展示效果&#xff1f; 新建三个序列 在开始之前&#xff0c;我们需要新建三个序列 序列1&#xff1a;总合成-尺寸1902*1080序列2&#xff1a;照片合成-尺寸1920*1080序列3&#xff1a;照片…

自动驾驶TPM技术杂谈 ———— I-vista验收标准(试验规程)

文章目录 术语介绍试验准备场地要求环境要求精度要求边界车辆&路沿石 试验方法能力试验双边界车辆平行车位白色标线平行车位双边界车辆垂直车位白色标线垂直车位方柱垂直车位双边界车辆斜向车位白色标线斜向车位 新功能评价平行车位远程操控泊入泊出试验垂直车位远程操控泊…

能伸展脖子的机器人?东京大学最新研究成果:基于鸵鸟肌肉骨骼结构和行为,具有高度灵活性的新型机械臂—RobOstrich(附论文)

原创 | 文 BFT机器人 得益于高度灵活的颈部&#xff0c;鸟类可以做很多事情&#xff0c;无论是转过头梳理自己的后背&#xff0c;在飞行过程中“眼观六路”&#xff0c;还是在地面或树上难以触及的角落和缝隙寻找食物。而在所有鸟类中&#xff0c;鸵鸟以其结实灵巧的颈部脱颖而…

​ NISP一级备考知识总结之信息安全概述、信息安全基础

参加每年的大学生网络安全精英赛通过初赛就可以嫖一张 nisp&#xff08;国家信息安全水平考试&#xff09; 一级证书&#xff0c;nisp 一级本身没啥考的价值&#xff0c;能白嫖自然很香 1.信息安全概述 信息与信息技术 信息概述 信息奠基人香农认为&#xff1a;信息是用来消…

【Linux】如何实现单机版QQ,来看进程间通信之管道

学会了管道&#xff0c;就可以实现简单的qq哦~ 文章目录 前言一、匿名管道总结 前言 为什么要进行进程间通信呢&#xff1f;因为需要以下这些事&#xff1a; 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程 资源共享&#xff1a;多个进程之间共享同样的资源。 …

ChatGPT实现旅行安排

工作之余&#xff0c;出门旅行一趟放松放松身心&#xff0c;是对自己辛勤工作最好的犒劳方式之一。旅行可以近郊游、可以远游&#xff0c;可以穷游&#xff0c;可以自驾游&#xff0c;可以一言不合打飞的喂鸽子&#xff0c;方式多种多样。但是多数情况&#xff0c;我们是到一个…

论文解析-基于 Unity3D 游戏人工智能的研究与应用

1.重写 AgentAction 方法 1.1 重写 AgentAction 方法 这段代码是一个重写了 AgentAction 方法的方法。以下是对每行代码解释&#xff1a; ①public override void AgentAction(float[] vectorAction) 这行代码声明了一个公共的、重写了父类的 AgentAction 方法的方法。它接受…

Java版本工程管理系统源码企业工程项目管理系统简介

一、立项管理 1、招标立项申请 功能点&#xff1a;招标类项目立项申请入口&#xff0c;用户可以保存为草稿&#xff0c;提交。 2、非招标立项申请 功能点&#xff1a;非招标立项申请入口、用户可以保存为草稿、提交。 3、采购立项列表 功能点&#xff1a;对草稿进行编辑&#x…

Vue收集表单数据和过滤器

目录 收集表单数据 收集表单数据总结 过滤器 过滤器小结 收集表单数据 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><!--vue--><script src"https://cdn.sta…

C++ ---- 类和对象(下)

目录 初始化列表 初始化列表的语法 初始化列表的特性 explicit关键字 构造函数的隐式转换 explicit的作用 static修饰成员变量和成员函数 static修饰成员变量 static修饰成员函数 友元 友元函数 友元类 内部类 匿名对象 拷贝对象时的一些编译器优化 初始化列表 …

Kibana 的安装

1. 简介 Kibana 是一个开源的分析与可视化平台&#xff0c;可以用 Kibana 搜索、查看存放在 Elasticsearch 中的数据&#xff0c;就跟谷歌的 elasticsearch head 插件类似&#xff0c;但 Kibana 与 Elasticsearch 的交互方式是各种不同的图表、表格、地图等&#xff0c;直观的…

超稳定ChatGPT镜像网站,小白适用,赶紧收藏【持续更新中】

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计学专业大二本科在读&#xff0c;同时任汉硕云&#xff08;广东&#xff09;科技有限公司ABAP开发顾问。在学习工作中&#xff0c;我通常使用偏后…

Redis修炼 (15. redis的持久化-RDB)

RDB 就是 redis database backup file 数据备份文件 就是把内存中的所有数据都保存在磁盘中。 save 注意这个保存过程是主进程在做 因为redis 是单线程 所有其他所有请求都会被卡死。 bgsave 这个稍微友好一点 是子进程 执行&#xff0c;避免主进程收到影响。 redis在服务停机…

母亲节快到了,祝所有母亲节日快乐!Happy Mother‘s Day

《游子吟》唐孟郊 慈母手中线&#xff0c;游子身上衣。 临行密密缝&#xff0c;意恐迟迟归。 谁言寸草心&#xff0c;报得三春晖。 My kind mother has a needle and thread in her hand,Making new clothes for her son who is to travel far away. She is busy sewing c…

【Pandas与SQL系列】Pandas实现分布函数percent_rank、cume_dist

目录 1&#xff0c;分布函数,1.1&#xff0c;percent_rank()1.2&#xff0c;cume_dist()1.3 SQL例子 2&#xff0c;Pandas 实现3&#xff0c;补充Pandas实现排序 1&#xff0c;分布函数, 应用场景&#xff1a;快速查看某个记录所归属的组内的比例 分布函数分类及基础语法&…