B站左神算法课学习笔记(P4):认识O(NlogN)的排序

递归

定义

举例:(在数组上使用递归求最大值)

我的理解:

master公式

用处:在等规模的子递归问题中,可以直接使用此公式求解时间复杂度。

T\left ( N \right )=a*T\left ( \frac{N}{b} \right )+O\left ( N^d \right )

其中,T( N ) 表示母问题的数据量为 N ,a 表示递归中对于母函数的调用次数,N / b 表示子递归中的规模,最后一项表示除了递归以外剩下的部分的时间复杂度。

对于上述的递归代码,易得其时间复杂度为T\left ( N \right )=2*T\left ( \frac{N}{2} \right )+O\left ( 1 \right )

在求出master公式中的 a、b、d 三个参数之后,我们根据下列条件即可求得时间复杂度:

  1. 当 log_ba < d 时,时间复杂度为 O\left ( N^d \right )
  2. 当 log_ba>d 时,时间复杂度为 O\left ( N^{log_ba} \right )
  3. 当 log_ba==d 时,时间复杂度为 O\left ( N^d*logN \right )

求中点

使用此方法可能在数组很大时溢出:

int mid = (L + R)/2

所以更推荐使用此写法来防止这种情况发生:

int mid = L + (R - L)/2

当然 “ / 2 ” 也可以使用移位操作来代替, “ >> 1 ” 右移一位即可。

而且移位操作比除法更快!!

归并排序

定义

归并排序(Merge Sort):先递归使得左右两侧有序,再将两侧结果整合到一起,并重复该过程。

归并排序时间复杂度O(NlogN)额外空间复杂度为O(N)。

            

代码实现

Step1:递归排序,使得左右两侧分别有序。

Step2:合并两侧

在 p1 或 p2 越界前,比大小并依次放入;当任一指针越界后,将另一侧剩下的数字放入。

    

对于归并排序使用master公式可得 T\left ( N \right )=2*T\left ( \frac{N}{2} \right )+O\left ( N \right )

其中 a = 2, b = 2,d = 1,故符合 log_ba==d 的情况,时间复杂度为 O\left ( N^d*logN \right ),代入数据可知,归并排序的时间复杂度为 O\left ( NlogN \right )

Q: 为什么归并排序的时间复杂度是O(NlogN)?

核心:没有浪费比较行为

对于冒泡排序和选择排序,经过多次比较才找到一个数的合适位置,大量浪费了比较行为。

而归并排序并未浪费!归并排序是将左右两部分进行排序后,利用带序特性进行合并,随后再将排好序的这部分数据,与其同等大小的较初始大小更大的数据进行排序,得到新的带序数据。

可以理解为:每次比较后的信息是往下传递的!!

拓展:

小和问题

在一个数组中,每一个数的左边比当前数小的所有数之和,叫做这个数组的小和

Q:求数组 [ 1, 3, 4, 2, 5 ] 的小和。

手算可知答案为:5+6+4+1=16

下面讨论如何借助归并排序的思想,在时间复杂度为 O(NlogN) 的限制下完成求小和:

核心思想:正难则反

已知小和的定义是求所有数的左侧比各自数值的数的和,不妨反过来想:只需找到每个数右侧有多少个比其数值更大的数,我们就可以确定每个数产生的小和数量,即可求出小和。

代码实现:

其中,process 函数和 merge 函数既对数组进行了排序,又在排序过程中实现了求小和。

process 函数 return :左侧求小和的值 + 右侧求小和的值 + 左右合并求小和的值。

再来看 merge 函数:

标绿的部分表示求小和的过程:如果p1指针位置的值小于p2指针位置的值,说明p1指针位置上的值会产生小和,我们可以通过指针之差直接求算p1位置所产生的小和的数量(即有( r - p2 + 1 )个arr[ p1 ]作为小和)。

绿色部分的下一行是拷贝过程,注意,当左右两侧所指的数相等时,必须先拷贝右侧的数。

因为上文所提到使用指针之差来计算产生此小和的个数,所以需要在相等时先移动右侧指针。其中,右侧不会产生小和(右侧数,一方面本身顺序就相对左组来说位于右侧;另一方面,我们写算法的时候取的是小和的“正难则反”的定义,即找右侧有多少比它大的数,而在内部排序的时候已经计算过内部的小和,所以这里说右侧不会产生小和),但是左侧所产生的小和需要借助右侧 p2 指针与 r 指针位置之差来确定,所以必须先合并右侧的数,使得能够用指针之差确定左侧产生小和的个数!!

补充:

Q:为什么保证了不重不漏

大概是讲清楚了,但是感觉理解还没有很通畅,得练两题感受一下。

逆序对问题

在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请找出数组中逆序对的数量。

与上题十分类似,排序+找逆序对即可。

快速排序

引入

Q1:给定一个数组 arr 和一个数 num ,请把小于等于 num 的数放在数组的左边,大于 num 的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。

举例说明:

划分一个 <= 区域,表示放置在数组左侧。

  1. 当 arr [ i ] <= num 时,arr [ i ] 上的数与 <= 区的下一个数做交换,<= 区右扩,i ++ ;
  2. 当 arr [ i ] > num 时,i ++ 。

Q2:(荷兰国旗问题)给定一个数组 arr 和一个数 num ,请把小于等于 num 的数放在数组的左边,等于 num 的数放在数组的中间,大于 num 的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。

可以看作:

实现条件:

  1. 当 arr [ i ] < num 时,arr [ i ] 上的数与 < 区的下一个数做交换,< 区右扩,i ++;
  2. 当 arr [ i ] == num 时,i ++ ;
  3. 当 arr [ i ] >= num 时,arr [ i ] 上的数与 > 区的前一个数做交换,> 区左扩,i 原地不动(交换后未验证当前位置)。

手算模拟:

快排1.0

每次取最后一个数字作为 num ,把每个数与之比较后放置在前半部分或后半部分,最后将大于 num 部分的第一个数与 num 交换位置。然后再各个小部分上重复该操作。

例:

快排2.0

改进:添加 = num 的区域

优化点:一次搞定一批值相等的数,比 1.0 一次搞定一个数字更快。

例:

到此为止,上方的两种快排时间复杂度都是O\left ( N^{2} \right )

因为我们很容易举出最坏情况的例子:[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

对于此数组运用上面两种快排,得到的时间复杂度都是等差数列 9 + 8 + ... + 1 ,即O\left ( N^{2} \right )级别。

快排3.0

改进:从数组中随机选择一个数与最后位置上的数进行交换,然后进行比较与分区操作。

根据开始提到的 master 公式我们可以估计其时间复杂度,每种情况的时间复杂度出现概率都是\frac{1}{N}.

对于所有可能的时间复杂度求数学期望,得到其时间复杂度的期望为O\left ( NlogN \right )

注意:这是概率事件!此处给出的时间复杂度是使用数学期望进行的估计!

代码实现:

先随机选数并交换位置,随后调用 partition 函数,即快排的实现函数,其实现了比较和划分的过程,返回一个含两位的数组,表示 “ = ” 区域的左右两侧边界,其中 p [ 0 ] 为  “ = ” 区域的左边界, p [ 1 ] 为  “ = ” 区域的右边界。

相应的, p [ 0 ] - 1 即为 < 区的右边界, p [ 0 ] + 1 即为 > 区的左边界。

完成一轮快排后,在左右的 < 区和 > 区上递归进行第二轮快排,直至完全有序。

注意:下图中,L 代表指针位置,第一个 swap() 函数 ++less 是先自增后传给函数,对应 “与 < 区右边界的下一位交换” ;第二个 swap() 函数 --more 是先自减后传给函数,对应 “与 > 区左边界的前一位交换”。

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值