刷到算法导论思考题
2-4 d
给出一个确定在n个元素的任何排列中逆序对数量的算法(提示:修改归并排序)
觉得有意思,而leetcode上又是困难题,所以记录下。因为用归并排序的思路做,不用暴力求解的方法,所以先从归并排序讲起。
一.归并排序
def merge_sort(nums, l, r):
if l>=r:
return
m = l + (r - l)//2 #数组取中点,没有(l+r)/2是为了防止两大数相加溢出
merge_sort(nums,l,m) # divide 分
merge_sort(nums, m +1, r) # divide 分
merge(nums,l,m,r) #conquer 治理
def merge(nums,l,m,r):
temp = [0 for i in range(0,r-l+1)] #初始化临时数组
temp[l:r+1] = nums[l:r+1] #复制原数组至临时数组
i= l
j= m + 1 #双指针
for k in range(l,r + 1):
if i == m + 1:
nums[k] = temp[j]
j += 1
elif (j == r + 1) or temp[i] <= temp[j]: #可以分开为两个elif
nums[k] = temp[i]
i += 1
else:
nums[k] = temp[j]
j += 1
标准的分治divide and conquer归并排序。
下面还提供一个放了哨兵的归并排序。好处在于不需要考虑这么多case,更容易想到,理解。
import math
def merge_sort(nums, l, r):
if l>=r:
return
m = l + (r - l)//2
merge_sort(nums,l,m)
merge_sort(nums, m +1, r)
merge_sentinal(nums,l,m,r)
def merge_sentinal(nums,l,m,r):
left = nums[l:m+1]
right = nums[m+1:r+1]
left.append(math.inf) #哨兵
right.append(math.inf)
i,j=0,0
for k in range(l,r+1):
if left[i] <= right[j]:
nums[k] = left[i]
i += 1
else:
nums[k] = right[j]
j += 1
二.数组中的逆序对
为什么用归并排序?
逆序对就是相对顺序逆的两个元素。对归并排序熟悉后会发现他是几个高级排序里,在排序时,对各元素相对位置要求最高且能清晰看见的那个。 归并排序会把数组对半分开,左右两边各自从头比较大小,然后依次排序放回数组。按照数组里相对元素位置,分开后的右数组元素理应比左边大,但实际上归并排序的“治”的时候,有些右边元素是小于左边的。这时,一个逆序对就被发现了。
下面上码,用的leetcode答案格式,即包了个class。
class Solution:
def reversePairs(self, nums):
tmp = [0 for i in range(0,len(nums))]
return self.mergePairs(nums, 0, len(nums) - 1, tmp)
def mergePairs(self, nums, l, r,tmp):
if l >= r:
return 0
m = l + (r - l) // 2
left = self.mergePairs(nums, l, m, tmp)
right = self.mergePairs(nums, m + 1, r,tmp)
middle = self.merge(nums, l, m, r, tmp)
inversion = right + middle + left
return inversion
def merge(self, nums, l, m, r, tmp):
tmp[l:r+1] = nums[l:r+1]
i, j = l, m+1
count = 0
for k in range(l, r + 1):
if (i == m + 1):
nums[k] = tmp[j]
j += 1
elif (j == r + 1) or (tmp[i] <= tmp[j]):
nums[k] = tmp[i]
i += 1
else: #右子数组比左子数组小,即逆序
nums[k] = tmp[j]
j += 1
count += m - i + 1 #累加逆序对的数量
return count
题目先call reversePairs, 多亏这看似多余的部分,能建造一个大的临时数组,这样递归的时候不用重复造浪费时间空间。
下面的mergepairs()是标准的归并排序起手。区别在于递归base case为0而不是null,并需统计求和各递归分支的逆序对数量作为返回值
conquer“治”部分merge()里,变化最大的是多了个逆序对计数 count += m - i + 1 我个人感觉是比较难想的一点。但如果用简单例子过一遍流程,并画图的话会较为直观的算出等式。
口头来讲就是:此时m为左数组的最后一个元素,m+1为右数组的元素。其中 i 这个计数器记录的是左数组i个已经排序并放回原数组。
进入最后一个else的条件是右数组未排序的第一个bj 小与 左数组未排序的第一个ai。根据递归,我们可以信任左右两个子数组(也就是子问题)已经排序完成,那么ai到左数组末尾元素am未止的所有元素均>bj,且这m-i+1个元素排序故递增,即都是右一bj的逆序对。