题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字构成逆序对。输入一个数组,求该数组的逆序对总数。题目链接
解题思路:
1.最直观暴力的解法:
直接遍历,用多层循环去寻找逆序对。这样的方法会在LeetCode上超时(x)
2.套用归并排序思想,采用分解——归并的步骤解决。将数组一步步对半拆开到每个数组中只有一个元素,只有一个元素的数组一定是有序的。 过程如图所示:(图源自LeetCode解析)
当把数组拆分为只有一个元素之后,再把一个元素归并成包含两个元素的列表,归并的过程中既需要判断列表内是否存在逆序对,也需要判断列表之间是否存在逆序对,然后将列表按从小到大排序好;再往下将包含两个元素的列表归并为包含四个元素的列表,以此类推……
3.逆序对数的求解方法:
如图所示,逆序对总数等于左边列表内的逆序对总数+右边列表内的逆序对总数+左、右列表之间的逆序对总数 ,而列表内的逆序对总数=将列表拆分成两个包含两个元素的列表之间的逆序对总数。因此只需要写出求左右列表之间的逆序对数的代码,其余均可通过递归求得。
我们通过两个下标移动遍历两个列表来比较两个列表之间的数字大小,如图所示。
4.整体代码实现:
class Solution:
def reversePairs(self, nums: List[int]) -> int:
def merge_sort(nums, low, high):
if low >= high:
return 0
mid = (low + high) // 2
count = merge_sort(nums, low, mid) + merge_sort(nums, mid+1, high) + merge(nums, low, mid, high)
return count
def merge(nums, low, mid, high):
tmp = []
count = 0
i = low
j = mid + 1
while i <= mid and j <= high:
if nums[i] <= nums[j]:
tmp.append(nums[i])
i += 1
else:
tmp.append(nums[j])
j += 1
count += (mid -i + 1)
if i <= mid:
tmp.extend(nums[i:mid+1])
if j <= high:
tmp.extend(nums[j:high+1])
nums[low:high+1] = tmp[:]
return count
n = len(nums)
return merge_sort(nums, 0, n-1)
5.代码内注意点:
①逆序对数count的计算:count = merge_sort(nums, low, mid) + merge_sort(nums, mid+1, high) + merge(nums, low, mid, high)。merge_sort(nums,low,mid)为数组nums左边部分内部的逆序对总数,merge_sort(nums, mid+1, high)为数组nums右边部分内部的逆序对总数,merge(nums, low, high)为左右部分之间的逆序对总数。
②merge函数实现两个数组的一次归并,其中临时列表tmp的作用是存储排序后的部分结果,最后一定要记得将排序好的tmp替换nums部分。这样才能保证已经计算完的逆序对数不会被重复计算。
③merge函数比较下标i和下标j所指的数组数字大小时,当i>j时,从i开始左列表所有的元素都能和nums[j]构成逆序对(左列表从i开始往后的所有元素都比下标i所指数值大)。因此,当i>j时计算逆序对总数应该为(mid-i+1)。
p.s.LeetCode官网上的精选方法我测试发现消耗时间和内存都远远大于我写的这个代码。分析看来,精选方法将两个函数合并为一个函数,思想方法是一致,不同的是对临时列表tmp的处理。我的代码处理是在进行一次归并时,生成一个临时列表来存储需要处理的部分。而精选方法是在一开始便将tmp设置成长度大小与数组nums一致为n的列表,因此每次归并处理都是对长度为n的列表进行处理,列表长度远远大于每次归并时临时设置的列表。(如有理解错误,欢迎指正)
精选方法耗时:
我的方法耗时: