一.题目大意

  给定一个数组A,对于数组A中的两个数字,如果排在前面的一个数字大于(必须大于,等于不算)后面的数字,则这两个数字组成一个逆序对。要求输出数组A中的逆序对的总数。例如,对于数组{7,5,6,4},一共存在5个逆序对,分别是(7,5)、(7,6)、(7,4)、(5,4)、(6,4)。

注:根据题意可知,必须根据原数组中元素的相对顺序来统计,给定的数组时怎样,那就按照怎样的顺序。

二.思路分析

  方法1:暴力破解。双重循环来判断出所有的逆序对数,时间复杂度为O(N^2),空间复杂度为O(1)。数据量大的话肯定超时。

  方法2:利用归并排序的思想,具体思路介绍参考《剑指offer》。由于这里参考了归并排序的思想,所以此处先讲一下经典的归并排序,然后在此基础上再给出本题目的实现。

1. 2-路归并排序的实现:

关于归并排序的思想,此处就不多说了(书上或者网上资料一大把),这里只给出具体的实现,算作是一个模板吧,代码如下:

#include<iostream>
#include<unordered_map>
#include<queue>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<sstream>
using namespace std;
#define MAX_LEN 1000
int temp[MAX_LEN] = {0};
void Merge(int a[],int low, int middle,int high)
{
int i = 0 , j = 0, k = 0;
for(k = low; k <= high; k++)
temp[k] = a[k];
for(i = low, j = middle + 1, k = i; i <= middle && j <= high; k++)//注意i和j的终止条件
{
if(temp[i] <= temp[j])
a[k] = temp[i++];
else
a[k] = temp[j++]; }
while(i <= middle)
a[k++] = temp[i++];
while(j <= high)
a[k++] = temp[j++]; }
void MergeSort(int a[], int low, int high)
{
if(a == nullptr || low < 0 || high <= 0)//特殊输入和边界条件的判断与处理
return;
if(low < high)
{
int middle = (high - low) / 2 + low; //这样求中位数能够防止溢出
MergeSort(a,low,middle); //将数组进行拆分
MergeSort(a,middle + 1,high);
Merge(a,low,middle,high); //将拆分的数组进行归并 }
} int main()
{
int a[] = {2,1,3,4,6,5};
MergeSort(a,0,5);
for(int i = 0 ;i < 6; i++)
cout<<a[i];
cout<<endl;
}

运行结果如下:

其中,有几个需要注意的点:

(1).temp数组是用于辅助数组a进行排序的,这个数组的定义最好写在Merge函数之外(因为程序会多次调用Merge函数,如果每次都定义在Merge函数内的话,有可能会造成内存溢出),定义在MergeSort函数之中(这时需要把它作为参数传进Merge函数)或者作为全局变量都是可以的。

(2).low和high是指的数组的下标索引,2-路归并的话,就是将数组分成[low,middle]和[middle + 1,high]两个部分。

(3).Merge函数中的最后两个while循环,是为了处理两个数组长度不相同的情况,剩下多出的部分直接赋值到数组a的剩下部分即可。

2.本题的实现

  根据以上的2-路归并的实现,我们很容易能得到本题解,代码如下:

#include<iostream>
#include<unordered_map>
#include<queue>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<sstream>
using namespace std;
#define MAX_LEN 1000
int temp[MAX_LEN] = {0};
static int cnt = 0;
void Merge(int a[],int low, int middle,int high)
{
int i = 0 , j = 0, k = 0;
for(k = low; k <= high; k++)
temp[k] = a[k];
for(i = low, j = middle + 1, k = i; i <= middle && j <= high; k++)
{
if(temp[i] <= temp[j])
a[k] = temp[i++];
else
{
a[k] = temp[j++];
cnt = (cnt + middle - i + 1) % 1000000007;
} }
while(i <= middle)
a[k++] = temp[i++];
while(j <= high)
a[k++] = temp[j++]; }
void MergeSort(int a[], int low, int high)
{
if(a == nullptr || low < 0 || high <= 0)
return;
if(low < high)
{
int middle = (high - low) / 2 + low;
MergeSort(a,low,middle);
MergeSort(a,middle + 1,high);
Merge(a,low,middle,high); }
} int main()
{
int a[] = {2,1,3,4,6,5};
MergeSort(a,0,5);
for(int i = 0 ;i < 6; i++)
cout<<a[i];
cout<<endl;
cout<<cnt<<endl;
}

实际上就是在2-路归并排序的基础上增加了一步:cnt = (cnt + mid - i + 1) % 1000000007

相当于在归并的过程中就完成了逆序对数的统计,此处由于按照的是牛客上练习题的要求,所以cnt在计算过程中%了1000000007,但是需要注意的点是:

(1)此处用的是

cnt = (cnt + mid - i + 1) % 1000000007

而不是

cnt += (mid - i + 1) % 1000000007

要意识到这两种写法的实现过程是不同的,前者是把cnt整体取余,往往不会发生溢出;而后者只是对增加的部分取余,可能会发生溢出的。

(2)那么,此处为什么是mid - i + 1呢?这是因为在归并的过程中,实际统计的是不同数组之间的(或者说是同一数组的不同部分之间的)逆序对数。(而关于数组的内部的逆序对数,是该问题的子问题;只要解出了不同数组之间的逆序对数,就能够解出数组内部的逆序对数,更详细的介绍见《剑指offer》);而2-路归并排序,在对这两个数组刚开始进行归并时,这两个数组就已经是有序(此处是升序排序)的了,所以说如果temp[i]>temp[j]的话,说明从位置i到位置middle之间的所有元素都是大于temp[j]的了,所以说总数和增加了middle - i +1个。

(3)除此之外,还有一个需要注意的点,就是如果数组中存在相等的元素,也是按照小于的情况处理的,所以当temp[i] <= temp[j]时并不算作逆序对。

该方法的时间复杂度为O(N*logN),空间复杂度为O(N),与方法1相比,也属于一种空间换时间的策略。

结合《剑指offer(第二版)》面试题51来谈谈归并排序的更多相关文章

  1. 《剑指offer(第二版)》面试题55——判断是否为平衡二叉树

    一.题目大意 输入一颗二叉树,判断该二叉树是否为平衡二叉树(AVL树). 二.题解 <剑指offer>上给出了两种解决方式: 1.第一种是从根节点开始,从上往下遍历每个子节点并计算以子节点 ...

  2. 经典面试题目——找到第n个丑数(参考《剑指offer(第二版)》面试题49)

    一.题目大意 给你一个数n,要求返回第n个丑数.其中,丑数的定义如下: 丑数是指只包含因子2.3和5的数.(数字1也是丑数,不过是个特例)引用<剑指offer>上的话来说,对于一个数M,如 ...

  3. 《剑指offer(第二版)》——面试题36:二叉搜索树与双向链表

    具体的题目大意和参考思路在此处不详述(见<剑指offer>),实质就是在中序遍历的过程中调整指针的指向,关于中序遍历有递归和非递归两种操作,所以此处也用了两种方法. 方法1(递归法): 代 ...

  4. 《剑指offer(第二版)》面试题60——n个骰子的点数

    一.题目描述 把n个骰子仍在地上,所有的骰子朝上的一面的点数之和为s,输入n,打印出s所有可能的值出现的概率. 二.题解 <剑指offer>上给出的两种方法,尤其是代码,晦涩难懂且没有注释 ...

  5. 《剑指offer(第二版)》面试题64——求1+2+...+n

    一.题目描述 求1+2+3+...+n,要求不能使用乘除法.for.while.if.else.switch.case等关键字以及条件判断语句 (即三元运算符,A? B : C) 二.题解 虽然求和问 ...

  6. 《剑指offer 第二版》题解

    剑指Offer 按题号排序 面试题 3:数组中重复的数字 面试题 4:二维数组中的查找 面试题 5:替换空格 面试题 6:从头到尾打印链表 面试题 7:重建二叉树 面试题 8:二叉树的下一个节点 面试 ...

  7. 剑指offer第二版-10.斐波那契数列

    面试题10:斐波那契数列 题目要求: 求斐波那契数列的第n项的值.f(0)=0, f(1)=1, f(n)=f(n-1)+f(n-2) n>1 思路:使用循环从下往上计算数列. 考点:考察对递归 ...

  8. 剑指offer第二版-5.替换空格

    面试题5:替换空格 题目要求: 实现一个函数,把字符串中的每个空格都替换成“%20”,已知原位置后面有足够的空余位置,要求改替换过程发生在原来的位置上. 思路: 首先遍历字符串求出串中空格的数量,求出 ...

  9. 剑指offer第二版-3.数组中重复的数

    面试题3:数组中重复的数 题目要求: 在一个长度为n的数组中,所有数字的取值范围都在[0,n-1],但不知道有几个数字重复或重复几次,找出其中任意一个重复的数字. 解法比较: /** * Copyri ...

随机推荐

  1. 卸载Mariadb-报错

    1. rpm -qa|grep aria MariaDB-client-10.1.22-1.x86_64MariaDB-devel-10.1.22-1.x86_64MariaDB-shared-10. ...

  2. [MarkDown] markdown语法小结

    目录 写在前面 目录 特殊字符自动转换 段落和换行 第一阶标题 第二阶标题显示效果有下划线 H1 H2有下划线 H3 区块引用 列表 代码区块 分割线 链接 强调 删除线 代码 图片 反斜杠 表格 g ...

  3. C#读取文件夹下所有指定类型,并返回相应类型数据

    C#读取文件夹下所有文件 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享.心 ...

  4. 对于vs出现“This function or variable may be unsafe”

    1.项目上右击选择“属性” 2.选择C/C++ ->预处理器 ->预处理器定义 3.添加一行  _CRT_SECURE_NO_WARNINGS 4.点击确定,重新编译成功.

  5. NYOJ 737:石子合并(一)(区间dp)

    737-石子合并(一) 内存限制:64MB 时间限制:1000ms 特判: No 通过数:30 提交数:37 难度:3 题目描述: 有N堆石子排成一排,每堆石子有一定的数量.现要将N堆石子并成为一堆. ...

  6. Sublime Text3:插件+快捷键+环境变量设置+C/C++编译环境

    环境变量配置 如果电脑里之前下载过Codeblocks或者Dev,找到文件根目录,打开MinGw(或者MinGw64),点开bin目录,将bin文件夹的目录复制下来(以Codeblocks为例,将文件 ...

  7. 2017.7.11 linux 挂载

    挂载:Liunx采用树形的文件管理系统,也就是在Linux系统中,可以说已经没有分区的概念了.分区在Linux和其他设备一样都只是一个文件.要使用一个分区必须把它加载到文件系统中.这可能难于理解,继续 ...

  8. URL编码表(收集到的,为了方便查看)

    URL编码表

  9. Python——psutil的使用(获取系统性能信息)

    >>> import psutil #导入psutil >>> a=psutil.virtual_memory() >>> a.total #总虚 ...

  10. IO流的序列化和反序列化

    序列化和反序列化的概念: 序列化:把对象转换为字节序列的过程称为对象的序列化.(常见的就是存文件) 反序列化:把字节序列恢复为对象的过程称为对象阿德反序列化. 序列化和反序列化的使用: java.io ...