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

news2024/11/27 12:54:18

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

归并排序及其面试题

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

归并排序

归并排序是什么

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

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

归并排序的实际运用

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

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

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

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

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

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

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

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

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

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

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

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

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

相关文章

STM32F103 晶振问题详解

博主自制开发板&#xff0c;用的 STM32F103RCT6&#xff0c;设计时 8M 晶振并联了个 1M 电阻&#xff0c;实测发现&#xff1a; 1、软件延时 1s &#xff0c;实际延时 9s&#xff0c;拆掉 1M 电阻问题消失。 2、部分代码下载进去后单片机不工作。&#xff08;实测晶振不起振 o…

MySQL的高级语句

一、SQL高级语句 1、 SELECT 显示表格中一个或数个栏位的所有资料 语法&#xff1a;SELECT "字段" FROM "表名"; select * from test1; select name from test1; select name,sex from test1;2、DISTINCT 不显示重复的内容 语法&#xff1a;SELECT D…

win11安装java8后,jps、jvisualvm等jdk工具无法使用的问题

文章目录 基础环境1 找不到jps、jvisualvm等命令问题1.1 原因1.2 解决方案 2 jdk工具无法正常使用问题2.1 原因2.2 %TMP%\hsperfdata_username文件夹2.3 解决方案 基础环境 jdk-8u261-windows-x64&#xff0c;一直下一步&#xff0c;安装到d盘下 1 找不到jps、jvisualvm等命令…

华为基于dhcp snooping表的各种攻击防御

所有的前提是必须开启了dhcp snooping功能 一、dhcp 饿死攻击&#xff1a; 接口下或vlan下开启 dhcp snooping check dhcp-chaddr enable 开启二层源mac和chaddr一致性检测 dhcp snooping max-user-number 1 接口上手动配置的绑定成员数量&#xff08;可选择项&#xff09; …

C++常用函数语法

C常用函数详解 memset()函数字符串的插入和删除字符串替换解析字符串查询解析substr函数 memset()函数 memset 函数是内存赋值函数&#xff0c;用来给某一块内存空间进行赋值的。 其原型是&#xff1a;void* memset(void *_Dst, int _Val, size_t _Size) _Dst是目标起始地址&…

MySQL的日志管理,备份及恢复

一.MySQL 日志管理 MySQL 的日志默认保存位置为 /usr/local/mysql/data MySQL 的日志配置文件为/etc/my.cnf &#xff0c;里面有个[mysqld]项 修改配置文件&#xff1a; vim /etc/my.cnf [mysqld] 1、错误日志 错误日志&#xff0c;用来记录当MySQL启动、停止或运行时发生…

chatGPT免费站点分享

下面的应该都能用&#xff0c;试试吧... ChatGPT是一种人工智能聊天机器人&#xff0c;能够生成虚拟语言和交互回复。使用ChatGPT&#xff0c;您可以与机器人进行真实的交互&#xff0c;让机器人根据您提出的问题或请求来生成回复。但是&#xff0c;在使用ChatGPT时&#xff0…

IOS上传到App Store教程

引言 大家都知道开发APP包含安卓和IOS,而安卓申请商家需要在各大厂家的开发者中心进行上传,比如华为、小米、魅族等等开发者中心。 而苹果只有一个官网&#xff0c;但是苹果要上传IPA&#xff08;苹果的安装包&#xff09;需要使用mac电脑进行上传&#xff0c;而我平时用的win…

vue3(setup语法糖)+typescript+echarts5大屏可视化项目(底部附源码地址)

最近在学习echarts5想结合着自己所学的vue3ts结合起来做个demo, 效果图如下: 登录页 首页: 每个模块支持放大全屏的功能 踩坑: echarts实例化不建议使用ref,echarts内部机制导致的在main.ts中将echarts挂载到app的实例上 let app createApp(App) app.config.globalPrope…

企业战略管理:精要

一、企业战略管理的概念与流派 国内外学者对企业战略和企业战略管理的各种理解— 安索夫&#xff1a;企业在确定战略前&#xff0c;应该先确定自己的经营性质安德鲁斯&#xff1a;企业战略管理是一个决策模式&#xff0c;决定企业的目标&#xff0c;提出实现目标的方针和计划&…

新能源 石油化工 HJL-98/B数字式电流继电器 瞬时动作,过负荷、短路保护 JOSEF约瑟

名称&#xff1a;数字式交流电流继电器&#xff0c;品牌&#xff1a;JOSEF约瑟&#xff0c;型号&#xff1a;HJL-98/B&#xff0c;动作电流&#xff1a;30500mA&#xff0c;工作电压&#xff1a;AC220V/AC380V&#xff0c;安装方式&#xff1a;柜内导轨&#xff0c;零序孔径&am…

基于STM32F103C8T6的物联网温湿度光照烟雾监测系统

1、系统组成&#xff1a;STM32F103C8T6最小系统、S8050三极管、有源高电平触发蜂鸣器、ESP8266_01S模块、DHT11温湿度传感器、0.96OLED显示屏、BH1750光照度传感器、MQ2烟雾浓度传感器、LED灯、碳膜电阻&#xff08;300欧&#xff09;、独立按键、排针若干、杜邦线若干、微信小…

使用docker构建并部署MySQL5.7镜像

使用docker构建并部署MySQL5.7镜像 前言一、docker中部署MySQL主要有哪几种方式&#xff1f;二、CentOS 镜像中构建 MySQL 容器1.编写Dockerfile2.初始化MySQL 三、MySQL 官方镜像中构建容器1. 拉取官方镜像2. 运行镜像3. 配置镜像外网访问 四、MySQL 容器初始化脚本1. 将sql文…

一不小心,你就掉进了Spring延迟初始化的坑!

前言 书接上回&#xff0c;之前我们有聊到 Spring 的延迟初始化机制&#xff0c;是什么&#xff0c;有什么作用。今天跟各位大佬分享一下&#xff0c;我在使用 Spring 延迟初始化踩过的一些坑。 List<坑> 坑列表 new ArrayList<>(2); 首先&#xff0c;让我们回顾…

STM32-单通道ADC采集(DMA读取)实验

关于ADC的一些原理和实验我们已经有了2篇笔记&#xff0c;链接如下&#xff1a; 关于ADC的笔记1_Mr_rustylake的博客-CSDN博客 STM32-ADC单通道采集实验_Mr_rustylake的博客-CSDN博客 实验要求&#xff1a;通过ADC1通道1&#xff08;PA1&#xff09;采集电位器的电压&#x…

专升本资料怎么找?可以通过哪些渠道找到?

统招专升本专科生每个人只有一次考试的机会&#xff0c;近年来越来越多的人注意到学历的重要性&#xff0c;报考的人也越来越多&#xff0c;竞争激烈。 所以想要成功上岸&#xff0c;光靠靠努力可不行&#xff0c;拥有一个好的复习资料你就成功了一半。 随着粉丝量越来越大&am…

让思维结构化: 美团四大名著之 <金字塔原理>

目录 什么是金字塔原理 纵向结构 横向结构 如何构建金字塔模型 纵向结构 自上而下法 自下而上法 总结 自下而上思考 自上而下表达&#xff0c;结论先行 横向结构 时间顺序 空间顺序 重要性顺序 演绎推理 SCQA方法 序言&#xff08;前言/引言&#xff09;&#x…

如何降低OBS推流直播延迟的问题?

使用推流OBS工具进行直播推流操作时&#xff0c;默认的推流关键帧间隔是10秒&#xff0c;而客户端在播放时&#xff0c;通常需要3个关键帧的数据才会开始播放&#xff0c;为了实现更低的延迟&#xff0c;您需要在推流时将关键帧的间隔设置的小一些&#xff0c;您可以逐步调整这…

Netty(4)

Netty 文章目录 Netty6 Netty 核心模块6.1 EventLoopGroup 与 NioEventLoopGroup6.2 Bootstrap/ServerBootstrap6.3 ChannelPipline、ChannelHandler、ChannelHandlerContext6.3.1 三者的关系6.3.2 ChannelPipline6.3.3 ChannelHandler6.3.4 ChannelHandlerContext6.3.5 三者创…

微服务---Redis实用篇-黑马头条项目-优惠卷秒杀功能(使用java阻塞队列对秒杀进行异步优化)

Redis实用篇-黑马头条项目-优惠卷秒杀功能(使用java阻塞队列对秒杀进行异步优化) 1、秒杀优化 1.1 秒杀优化-异步秒杀思路 我们来回顾一下下单流程 当用户发起请求&#xff0c;此时会请求nginx&#xff0c;nginx会访问到tomcat&#xff0c;而tomcat中的程序&#xff0c;会进…