归并排序(逆序数问题)详解

微信公众号:bigsai

前言

在排序中,我们可能大部分更熟悉冒泡排序、快排之类。对归并排序可能比较陌生。然而事实上归并排序也是一种稳定的排序,时间复杂度为O(nlogn).

归并排序是基于分治进行归并的,有二路归并和多路归并.我们这里只讲二路归并并且日常用的基本是二路归并。并且归并排序的实现方式递归形式非递归形式。要注意其中的区分(思想上没有大的区别,只是划分上会有区分后面会对比)。

并且归并排序很重要的一个应用是求序列中的逆序数个数。当然逆序数也可以用树状数组完成,这里就不介绍了。

归并排序(merge sort)

归并和快排都是基于分治算法的。分治算法其实应用挺多的,很多分治会用到递归,也有很多递归实现的算法是分治,但事实上分治和递归是两把事。分治就是分而治之。因为面对排序,如果不采用合理策略。每多一个数就会对整个整体带来巨大的影响。而分治就是将整个问题可以分解成相似的子问题。子问题的解决要远远高效于整个问题的解决,并且子问题的合并并不占用太大资源。

至于归并的思想是这样的:

  • 第一次:整串先进行划分成1个一个单独,第一次是一一(12 34 56---)归并成若干对,分成若干2个区间.归并完(xx xx xx xx----)这样局部有序的序列。
  • 第二次就是两两归并成若干四个(1234 5678 ----)每个小局部是有序的
  • 就这样一直到最后这个串串只剩一个,然而这个耗费的总次数logn。每次操作的时间复杂的又是O(n)。所以总共的时间复杂度为O(nlogn).

对于分治过程你可能了解了,但是这个两两merge的过程其实是很重要的。首先我们直到的两个序列都是有序的。其实思想也很简单,假设两个串串为 3 5 7 82 6 9 10进行归并操作。我们需要借助一个额外的数组team[8]将两个串串有序存进去就行。而流程是这样的:

在这里插入图片描述

非递归的归并
正常归并的代码实现都是借助递归的。但是也有不借助递归的。大部分课本或者考试如果让你列归并的序列,那么默认就是非递归的,比如一个序列9,2,6,3,8,1,7,4,10,5序列的划分也是这样的。

第一次结束: {2,9}{3,6}{1,8}{4,7}{5,10}第二次结束:{2,3,6,9}{1,4,7,8}{5,10}第三次结束:{1,2,3,4,6,7,8,9}{5,10}第四次结束:{1,2,3,4,5,6,7,8,9,10}

递归的归并
在代码实现上的归并可能大部分都是递归的归并。并且递归和分治整在一起真的是很容易理解。递归可以将问题分解成子问题,而这恰恰是分治所需要的手段。而递归的一来一回过程的来(分治)回(归并),一切都刚刚好。

而递归的思想和上面非递归肯定不同的,你可以想想非递归:我要考虑当前几个进行归并,每个开始的头坐标该怎么表示,还要考虑是否越界等等问题哈,写起来略麻烦

而非递归它的过程就是局部—>整体的过程,而递归是整体—>局部—>整体的过程。
而递归实现的归并的思想:

 void mergesort(int[] array, int left, int right) {        int mid=(left+right)/2;//找到中间节点        if(left<right)//如果不是一个节点就往下递归分治        {            mergesort(array, left, mid);//左区间(包过mid)进行归并排序            mergesort(array, mid+1, right);//右区间进行归并排序            merge(array, left,mid, right);//左右已经有序了,进行合并        }    }

同样是9,2,6,3,8,1,7,4,10,5这么一串序列,它的递归实现的顺序是这样的(可能部分有点问题,但是还是有助于理解的):

在这里插入图片描述

所以实现一个归并排序的代码为:

private static void mergesort(int[] array, int left, int right) {        int mid=(left+right)/2;        if(left<right)        {            mergesort(array, left, mid);            mergesort(array, mid+1, right);            merge(array, left,mid, right);        }    }

    private static void merge(int[] array, int l, int mid, int r) {        int lindex=l;int rindex=mid+1;        int team[]=new int[r-l+1];        int teamindex=0;        while (lindex<=mid&&rindex<=r) {//先左右比较合并            if(array[lindex]<=array[rindex])            {                team[teamindex++]=array[lindex++];            }            else {                              team[teamindex++]=array[rindex++];            }        }        while(lindex<=mid)//当一个越界后剩余按序列添加即可          {              team[teamindex++]=array[lindex++];

          }        while(rindex<=r)          {              team[teamindex++]=array[rindex++];          }         for(int i=0;i<teamindex;i++)        {            array[l+i]=team[i];        }

    }

逆序数

首先得了解什么是逆序数:

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对

也就是比如3 2 1.看3 ,有2 1在后面,看2 有1在后面有3个逆序数。
而比如1 2 3的逆序数为0.

在数组中,暴力确实可以求出逆序数,但是暴力之法太复杂,不可取!而有什么好的方法能解决这个问题呢? 当前序列我可能不知道有多少序列。但是我们直到如果这个序列如果有序那么逆序数就为0.

在看个序列 abcd 3 2 1 efg编程abcd 1 2 3 efg整个序列逆序数减少3个。因为如果不管abcd还是efg和123三个数相对位置没有变。所以我们是可以通过某种方法确定逆序数对的。

我们就希望能不能有个过程,动态改变如果逆序数发生变化能够记录下来?!比如动那么一下能够知道有没有改变的。并且这个动不能瞎动,最好是局部的,有序的动。归并排序就是很适合的一个结构。因为肯定要选个小于O(n^2^)的复杂度算法,而归并排序满足,并且每次只和邻居进行归并,归并后该部分有序。

纵观归并的每个单过程例如两个有序序列:假设序列2 3 6 8 9和序列1 4 7 10 50这个相邻区域进行归并。

在这里插入图片描述
而纵观整个归并排序。变化过程只需要注意一些相对变化即可也就是把每个归并的过程逆序数发生变化进行累加,那么最终有序的那个序列为止得到的就是整个序列的逆序数!
在这里插入图片描述

至于规律,你可以发现每次归并过程中,当且仅当右侧的数提前放到左侧,而左侧还未放置的个数就是该元素减少的逆序个数! 这个需要消化一下,而在代码实现中,需要这样进行即可!

int value;-----------------private static void merge(int[] array, int l, int mid, int r) {        int lindex=l;int rindex=mid+1;        int team[]=new int[r-l+1];        int teamindex=0;        while (lindex<=mid&&rindex<=r) {            if(array[lindex]<=array[rindex])            {                team[teamindex++]=array[lindex++];            }            else {                              team[teamindex++]=array[rindex++];                value+=mid-lindex+1;//加上左侧还剩余的            }        }        while(lindex<=mid)          {              team[teamindex++]=array[lindex++];

          }        while(rindex<=r)          {              team[teamindex++]=array[rindex++];          }         for(int i=0;i<teamindex;i++)        {            array[l+i]=team[i];        }

    }

结语

至于归并排序和逆序数就讲这么多了!个人感觉已经尽力讲了,如果有错误或者不好的地方还请各位指正。如果感觉可以,还请点赞,关注一波哈。
欢迎关注公众号:bigsai 长期奋战输出!

原文地址:https://www.cnblogs.com/bigsai/p/12253254.html

时间: 2024-08-29 00:22:48

归并排序(逆序数问题)详解的相关文章

AcWing:108. 奇数码问题(归并排序 + 逆序数)

你一定玩过八数码游戏,它实际上是在一个3×3的网格中进行的,1个空格和1~8这8个数字恰好不重不漏地分布在这3×3的网格中. 例如: 5 2 8 1 3 _ 4 6 7 在游戏过程中,可以把空格与其上.下.左.右四个方向之一的数字交换(如果存在). 例如在上例中,空格可与左.上.下面的数字交换,分别变成: 5 2 8 5 2 _ 5 2 8 1 _ 3 1 3 8 1 3 7 4 6 7 4 6 7 4 6 _ 奇数码游戏是它的一个扩展,在一个nn×nn的网格中进行,其中nn为奇数,1个空格和1

Inversion(HDU_4911) 归并排序+逆序数对

Inversion Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 131072/131072 K (Java/Others) Total Submission(s): 3171    Accepted Submission(s): 1154 Problem Description bobo has a sequence a1,a2,-,an. He is allowed to swap two adjacent numbers f

数据结构 归并排序-逆序数对

逆序对是指数列a[1],a[2],a[3]-中的任意两个数a[i],a[j] (i<j),如果a[i]>a[j],那么我们就说这两个数构成了一个逆序对. 而归并排序的合并两个排列的过程中 会将右边的有序序列的元素依次插入前面的 有序序列 如(3 7 12)   ( 5 6 8) 将5 插入  (3 7 12) 中 因为后面有序 所以 假设   5和左边全部元素构成逆序对  所以有mid+1 而左边序列又有start1 个数比5 小  所以 逆序数就是  mid+1-start1 这样就能大大缩

Frosh Week(HDU_3743)归并排序+逆序数对

Frosh Week Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 2772    Accepted Submission(s): 923 Problem Description During Frosh Week, students play various fun games to get to know each other a

归并排序 逆序数

很好理解: int n,a[500010],c[500010]; long long ans; void msort(int L,int R) { if(L==R)return ; int mid=(L+R)>>1; msort(L,mid); msort(mid+1,R); int i=L,k=L,j=mid+1; while(i<=mid&&j<=R) if(a[i]<=a[j]) c[k++]=a[i++]; else c[k++]=a[j++],ans

求逆序数

第一部分:题目 描述 在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序.一个排列中逆序的总数就称为这个排列的逆序数. 现在,给你一个N个元素的序列,请你判断出它的逆序数是多少. 比如 1 3 2 的逆序数就是1. 输入 第一行输入一个整数T表示测试数据的组数(1<=T<=5)每组测试数据的每一行是一个整数N表示数列中共有N个元素(2〈=N〈=1000000)随后的一行共有N个整数Ai(0<=Ai<1000000000),表示数列中的所

归并排序_逆序数

归并排序求逆序数 在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序.一个排列中逆序的总数就称为这个排列的逆序数.一个排列中所有逆序总数叫做这个排列的逆序数.也就是说,对于n个不同的元素,先规定各元素之间有一个标准次序(例如n个 不同的自然数,可规定从小到大为标准次序),于是在这n个元素的任一排列中,当某两个元素的先后次序与标准次序不同时,就说有1个逆序.一个排列中所有逆序总数叫做这个排列的逆序数. 1 #include<cstdio> 2 #in

《TCP/IP详解卷1:协议》第5章 RARP:逆地址解析协议-读书笔记

章节回顾: <TCP/IP详解卷1:协议>第1章 概述-读书笔记 <TCP/IP详解卷1:协议>第2章 链路层-读书笔记 <TCP/IP详解卷1:协议>第3章 IP:网际协议(1)-读书笔记 <TCP/IP详解卷1:协议>第3章 IP:网际协议(2)-读书笔记 <TCP/IP详解卷1:协议>第4章 ARP:地址解析协议-读书笔记 <TCP/IP详解卷1:协议>第5章 RARP:逆地址解析协议-读书笔记 1.引言 具有本地磁盘的系统引导

求逆序数(归并排序)

求逆序数 时间限制:2000 ms  |  内存限制:65535 KB 难度:5 描述 在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序.一个排列中逆序的总数就称为这个排列的逆序数. 现在,给你一个N个元素的序列,请你判断出它的逆序数是多少. 比如 1 3 2 的逆序数就是1. 输入 第一行输入一个整数T表示测试数据的组数(1<=T<=5) 每组测试数据的每一行是一个整数N表示数列中共有N个元素(2〈=N〈=1000000) 随后的一行共有N个整