承接上文归并排序及小和问题
归并排序扩展-逆序对问题
在一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对,找到逆序对的数量
比如 3,2,4,5,0
所有的逆序对是 3,2; 3,0; 2,0; 4,0; 5,0
上文的小和问题是 求右边有多少个数比左边的数大
这个题目是求右边有多少个数比左边的数小
所以逆序对的问题和小和问题是等效的
归并排序为什么不会重算和漏算或归并排序的实质
一个数组[a,b,c,d,e,f,g,h,i]
就看数字c 看它经历什么样的心路历程
一开始数组是这么被分的
![](https://i-blog.csdnimg.cn/blog_migrate/c72c24f2db48324d9e4b62c82a8fb06e.png)
当a和b整体产生小和变成有序之后 跟c合并的过程
c不产生小和
因为右组所有的数都不会产生小和
然后a,b,c就变成了一个有序的部分
c在哪里不重要
接下来 a,b,c这个整体和d,e所在的组合并
这样合并的时候c产生的小和数量它的范围被扩到右侧的d,e范围上
d,e这个范围上有可能有比c大的(有0个比c大的,有1个比c大的,有2个比c大的)
具体哪种情况不重要,重要的是c已经把自己求有多少个数比自己大的范围扩到d,e上
然后a,b,c,d,e共同构成一个有序的部分,c在哪里不重要
重要的是下面会和f,g,h,i所在的数组
去求这个范围上有多少个数比c大
c先去求de范围有多少个数比c大
再去求f,g,h,i范围有多少个数比c大
a,b,c,d,e,f,g,h,i 这些数成为一个整体
c在哪个位置不重要
如果右侧还有一个范围 则c所在的这个整体会继续求那个右侧的范围有多少个数比c大
c在求有多少个数比它大的范围是右侧依次往外扩的
所以说它不会漏算
为什么说不会重算?
因为c一旦跟某一个右组合并了之后
它们就会变成一个组
而一个组内部是不会产生小和的
只有左组和右组之间才会产生小和
c只是一个普通的一员而已 对于所有的数来说都是这个过程
所以求每一个数的小和都不会漏算 都不会重算
荷兰国旗问题
怎么把一组数做划分 左边都小于它 右边都大于它 中间都等于它
1、问题1
左边都小于等于number,右边都大于number
不要求有序 只要求对数组进行划分
![](https://i-blog.csdnimg.cn/blog_migrate/2c3afc74fa32ee0ef106268dbaaf8240.png)
比如 一个数组是 3,5,6,7,4,3,5,8
number是5
所有小于等于5的放左边,大于5的放右边
准备一个变量表示小于等于区域的右边界
一开始在数组的最左侧
![](https://i-blog.csdnimg.cn/blog_migrate/35c9a7a0114e074f3d0cc7835620e991.png)
当前来到的位置是i位置
当来到i位置就会有两种情况
第一种是小于等于number的
第二种是大于number
1)i所在当前位置的数<=number
把当前数[i]和<=区的下一个数做交换
然后<=区往右扩一个位置
当前数也跳下一个i++
如图第一个数是3
3<5
3和<=区的下一个数也是3 即自己和自己交换
交换完之后 <=区往后扩
当前数跳下一个
![](https://i-blog.csdnimg.cn/blog_migrate/22850566b6835976b8b5c9cdf85850ca.png)
当前数是5 也满足 <=number
5和5 自己和自己交换 <=区向右扩
当前数下移
![](https://i-blog.csdnimg.cn/blog_migrate/6d187c03858ef806b49e1cf71baf0835.png)
此时当前数6大于5 命中第二种情况
i直接跳下一个
![](https://i-blog.csdnimg.cn/blog_migrate/5c898b3c0059dabb04dd681d827bb7cf.png)
当前数是7 又中了情况2 还继续往下走
![](https://i-blog.csdnimg.cn/blog_migrate/320f03b35c8e2bd2189fc0eda6d896c5.png)
当前数是4 ,<=number的
把它跟<=区的下个数做交换
4和6做交换
<=区往右扩
当前数也跳下一个
![](https://i-blog.csdnimg.cn/blog_migrate/1c754808340a09640a5e06ab1aa5fc26.png)
有中了情况1
把当前数和下个数做交换
<=区往右扩
当前数跳下一个
![](https://i-blog.csdnimg.cn/blog_migrate/054de7e3c137a2731214090f73522abd.png)
又中第一种情况 结果是
![](https://i-blog.csdnimg.cn/blog_migrate/79ca110263ebc4cee4eeef11c5053466.png)
当前数是8 ,i++ 越界了
此时已经做到了 <=区都是<=5的 ,右侧都是>5的
![](https://i-blog.csdnimg.cn/blog_migrate/6ed0fd346a66fc65bede4eb6982df011.png)
i是当前来到的位置
右侧是还未看过的位置 待定区域
<=区域里面都是<=已经做到的位置
<=区域和i中间的位置大于区域
荷兰国旗问题🇳🇱
小于区域放左边 中间都是等于number的 右边都是大于number的
![](https://i-blog.csdnimg.cn/blog_migrate/96c7203f85aeba291218384b01298583.png)
小于和大于区域都不要求有序
当然可以没有小于区域或等于区域或大于区域 如果有的话 严格分开
时间复杂度是O(N) 用有限几个变量
定义两个变量 一个是小于区域的左边界
一个是大于区域的右边界
![](https://i-blog.csdnimg.cn/blog_migrate/cd4220551cc0ff56a4c07109fe66dd7e.png)
每来一个位置都有三种情况
1)第一种情况
i当前所在的数[i]<num,当前数和小于区域下一个做交换,<区域右扩,i跳下一个
2)第二种情况
[i]=num,i直接跳下一个
3)第三种情况
[i]>num
当前数i大于num
[i]当前数i和大于区域的前一个做交换
大于区域左扩
i原地不变
如图分析
i 位置上3 ,3<5 命中第一条逻辑
![](https://i-blog.csdnimg.cn/blog_migrate/101b3afd51bb6f81542859238a6508e7.png)
i位置上是5 命中第二个规则
直接跳下一个
![](https://i-blog.csdnimg.cn/blog_migrate/6629e5557f3b196c419755cc11366a86.png)
然后中了情况3,6大于5
i和大于区域的前一个做交换
6和0交换
大于区域向左扩
i原地不动
![](https://i-blog.csdnimg.cn/blog_migrate/e4d286b106cb70e13ee604af317bd7e4.png)
因为这个0是交换过来的 还没有看过它 所以原地不动
0是小于num的 中了逻辑1
所以和小于区域的下一个数做交换
0和5交换
小于区域向右扩一个位置
i跳下一个
![](https://i-blog.csdnimg.cn/blog_migrate/0bded91f870c26ebb0bd035a7fcebbc0.png)
3<5 名中第一个逻辑
![](https://i-blog.csdnimg.cn/blog_migrate/6cbe9a8182bff7a84ce34e04a30932fd.png)
4<5 名中第一个逻辑
![](https://i-blog.csdnimg.cn/blog_migrate/07eeb435a14a8fe39ab1727b0c8ca2e6.png)
5=5 直接往下走
![](https://i-blog.csdnimg.cn/blog_migrate/33363b615fa3c0248240f170f5e83ac9.png)
2<5 名中第一个逻辑
2和第一个5交换
![](https://i-blog.csdnimg.cn/blog_migrate/eecc2720c3f08287a73558d29fb34cdf.png)
6>5 名中第三个逻辑
和大于区域的前一个做交换
6和9换
大于区域往左扩一个位置
i原地不动
![](https://i-blog.csdnimg.cn/blog_migrate/43a9c33211d1c82052f1c5779f956274.png)
9>5
名中第三个逻辑
和大于区域的前一个做交换
9和9自己换
大于区域往左扩一个位置
i原地不动
![](https://i-blog.csdnimg.cn/blog_migrate/832eabc3b58ff3f7f6b6e01b6471f2c7.png)
当大于区域和i撞上的时候 交换停止
此时已经做到了 左边都是小于5的,中间都是等于5的,右边都是大于5的
![](https://i-blog.csdnimg.cn/blog_migrate/34954de12f749aba406da093cf8448c6.png)
i根据自己现在来到的数
如果是等于num的 就在等于区域直接扩充
然后跳下一个
![](https://i-blog.csdnimg.cn/blog_migrate/56a431161de583beb4c9c68b9d95f605.png)
如果i是小于区域的 就把i放到小于区域的下一个
![](https://i-blog.csdnimg.cn/blog_migrate/2a1b15e4ab45287958268d2c33dc509d.png)
相当于小于区域推着等于区域往前走
如果i是大于区域的
![](https://i-blog.csdnimg.cn/blog_migrate/7c2bcb26994306fee90472b0413d0cab.png)
和大于区域的下一个交换并左扩
要么i往右走压缩待定区域
让小于区域推着等于区域奔向大于区域
要么i位置直接发货到大于区域
让大于区域往左扩 压缩待定位置
当小于区域推着等于区域和大于区域撞上的时候 就结束了
快排1.0版本
![](https://i-blog.csdnimg.cn/blog_migrate/a9648f90c1e5278d87c8c65eb38c67f8.png)
在整个数组中 拿最后一个数做划分值
最后一个数认为是num
让最后一个位置前面的这一段 小于等于num的放到左边
大于num的放在右边
![](https://i-blog.csdnimg.cn/blog_migrate/02ca4c0e4ce9a3ff365fd80d4f358870.png)
这个数和大于区域的第一个数做交换
就可以做到小于等于区域被扩充了 而且最后一个数一定是num
然后剩下的都是大于区域的
让num的左侧和右侧重复这个行为
比如
最后一个数是5
大于5的区域的第一个数是6
6和5交换
![](https://i-blog.csdnimg.cn/blog_migrate/331a1310e881aafdcc976156ae69c082.png)
5就不用动了 在数组中固定下来
这一个过程就是partition分层过程
再在小于等于5的区域上拿最后一个数重复此过程
大于5区域上拿最后一个数重复此过程
左侧递归下去
右侧递归下去
总能让左右2个区域都有序
在一个范围上总拿这个范围的最后一个数做划分
然后把最后一个数放到小于等于区域的最后一个位置
然后让小于等于区域做递归
让大于区域也做递归
因为每次都能排好一个数
所以总有整体都有序的时候
每个区域的最后一个值可能都不一样 上面是以5为例做的说明
快排2.0基于荷兰国旗
![](https://i-blog.csdnimg.cn/blog_migrate/8740aa8f33d480606aaa08802f3a8ccf.png)
拿最后一个数做划分 比如是5
让前面的分三个区域 小于5 等于5 大于5区域
把5和大于5区域的第一个数做交换
等于5的区域就靠在一起了
整个数组就变成 等于5的区域在中间
大于5的区域在右边
等于5的区域就不用动了
在小于5的区域上做递归
在大于5的区域上做递归
每一次递归搞定的是一批等于划分值的数
所以总有有序的时候
快排2.0版本比1.0版本快一些
因为它一次搞定一批数
举例说明
原数组
![](https://i-blog.csdnimg.cn/blog_migrate/a41584aa55a4c6c77091a6ccf8b69af8.png)
最后一个是5
将前面的数据分为3个区域
![](https://i-blog.csdnimg.cn/blog_migrate/a09257046aba7781726cf912d67e530e.png)
小于5区域
等于5区域
大于5区域
最后一个元素5和大于区域的第一个元素6做交换
![](https://i-blog.csdnimg.cn/blog_migrate/e4c8ce632c0f92d46d064a4c15e4a65d.png)
等于5的区域在整个数组中的位置固定了
接下来在左侧区域 4301上以最后一个元素1做划分重复该行为
![](https://i-blog.csdnimg.cn/blog_migrate/3612dc3a5fe8fcd12341589b17d55f60.png)
0继续做递归是0
4 3区域 以3做划分
总有都变成有序的时候
在右侧区域 786上 以6做划分重复该行为
![](https://i-blog.csdnimg.cn/blog_migrate/fba3efb2fc1f0827f2a5e2f064f7c88c.png)
左侧和右侧做递归总有都排好的时候
快排1.0和2.0时间复杂度都是O(N^2)
拿最差的例子说明
1,2,3,4,5,6,7,8,9
以9做划分 没有右侧区域
只有左侧区域
划分的过程就是partition
partition过了9个数
在左侧区域拿8做划分 没有右侧区域
只有左侧区域
partion过了8个数
每次只搞定一个
所以等差数列
所以是O(N^2)的算法
最差情况原因只有一个 划分值打的很偏
先看什么时候是好情况
![](https://i-blog.csdnimg.cn/blog_migrate/c9b5626d5dd4eca1b87a89ef28bed9c4.png)
好情况是划分值打在几乎中间的位置
左侧递归的规模和右侧递归的规模都是差不多的
此时的master公式是
T(N)=2 T(N/2) + O(N)
除了调用递归之外(就是partition过程)的时间复杂度是O(N)
整体的时间复杂度是 O(N*LogN)
这种是最好的情况
划分值打偏就会逐渐退化成N^2的算法
![](https://i-blog.csdnimg.cn/blog_migrate/3dcadeafb00dc2bb6120b41dbcbe0f39.png)
左侧很小 右侧规模很大 最差情况就是没有左侧部分 只有右侧部分
或者 右侧很小 左侧规模很大 最差情况就是没有右侧部分 只有左侧部分
不管哪一种都是O(N^2)的算法
因为总是拿数组的最后一个位置做划分
所以差情况没法避免
可以人为构建差的例子
快排3.0
在数组L~R范围上
拿谁做划分
随机选择一个数
随机选了一个数之后 拿它和最后位置上的数做交换
然后拿这个新的最后位置的数做划分
既然是随机选的
好情况和差情况就是概率事件
随机选的可能是最坏的情况 事件复杂度是O(N^2)
也可能是1/5处
![](https://i-blog.csdnimg.cn/blog_migrate/af83273e66bdfd1a7115b6824d814e34.png)
master公式是
T(N)=T(N/5)+T((4/5) * N)+O(N)
可能是1/3处
![](https://i-blog.csdnimg.cn/blog_migrate/70965f6889c8d218cc0ce9a2861facc3.png)
master公式是
T(N)=T(N/3)+T((2/3) * N)+O(N)
可能是1/2处
![](https://i-blog.csdnimg.cn/blog_migrate/e62787336d1b46689f636f6407274d2f.png)
master公式是
T(N)=2*T(N/2)+O(N)
可能是4/5
![](https://i-blog.csdnimg.cn/blog_migrate/b073f4bf03e5f9ecacd82480b47adf5d.png)
master公式是
T(N)=T(4/5)+T((1/5) * N)+O(N)
每一位置都是等概率事件
每一种情况都是1/N的权重
把所有的master公式做概率累加
再求数学期望 得到的结果是O(N logN)的算法
代码
![](https://i-blog.csdnimg.cn/blog_migrate/7f3792e679642184128461faf5e11365.png)
第一步等概率随机选择一个位置
把它跟最右侧位置做交换
在L~R这个范围上 拿最后位置的数即选出来的这个随机数
做partition
返回一个数组 长度一定为2
指的是划分值等于区域的左右边界
![](https://i-blog.csdnimg.cn/blog_migrate/fe22a953ace0ed52a54608f49623378e.png)
原数组最后一个数是5
按照5进行partition划分
等于5的区域是下标12和13的位置
返回的这个数组就是[12,13]
表示划分值等于区域的左右边界
p[0]就是等于区域的左边界
p[0]-1 为小于区域的右边界
然后在小于区域上递归
p[1]是等于区域的右边界
p[1]+1是大于区域的第一个数的位置
在右侧范围上递归
![](https://i-blog.csdnimg.cn/blog_migrate/1aaff794559319c0e86fe78df4fadf32.png)
partition就是荷兰国旗问题