zoukankan      html  css  js  c++  java
  • 结合《剑指offer(第二版)》面试题51来谈谈归并排序

    一.题目大意

      给定一个数组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相比,也属于一种空间换时间的策略。

  • 相关阅读:
    Linux中查找当前目录下占用空间最大的前10个文件
    Redis的优势和特点
    java中final,finally,finalize三个关键字的区别
    消息队列介绍
    Redis的应用场景
    Spring中@Autowired注解与@Resource注解的区别
    多版本并发控制(MVCC)
    Linux查看CPU和内存使用情况
    进程调度算法
    一致性Hash算法
  • 原文地址:https://www.cnblogs.com/wangkundentisy/p/9042717.html
Copyright © 2011-2022 走看看