剑指 Offer 51 数组中的逆序对(树状数组、归并排序思想)

1. 问题描述:

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5

限制:

0 <= 数组长度 <= 50000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof

2. 思路分析:

① 这道题目类似于力扣的693题,两道题目都属于逆序对的相关问题,求解逆序对的个数属于经典的问题,一般有两种比较常见的解决方法,分别是树状数组归并排序的思想。树状数组可以解决区间单点修改(给某个位置加上一个数,变成某一个数)和区间和查询的相关问题,对于这道题目来说每增加一个数字那么我们可以在树状数组对应位置上的增加1即可(表示当前位置多一个数),我们在求解逆序对的时候实际上是找到比当前数字小的数字的个数,所以我们在调用树状数组的query方法求解区间和的时候实际上是求解当前[1: x]范围的和(求解[1: x]中1的个数),而题目中没有给出数据范围,所以x可能是非常大的,所以我们不能够直接开这么大的数组,我们需要先对nums进行离散化(离散化包括对nums中的数字去重和排序),通过离散化将nums中的数字映射到一段连续的区间,这样可以大大节省存储空间,并且能够在查询的时候降低时间复杂度(离散化之后数组的长度减小了所以查询的范围也缩短了);离散化的时候我们可以使用set集合对nums中的数字进行去重,然后对nums中的元素由小到大进行排序,这样nums中的每一个数字都对应着离散化之后的_nums中的一个数字,也即nums中的每一个数字对应_nums中唯一确定的位置;因为求解的是逆序对的数目所以我们需要逆序遍历nums,在查询区间和的时候我们需要找到当前的数字x在离散化之后的位置k,查询[1: k - 1]的区间中1的个数,1的个数就是当前x对应的逆序对的数目,然后我们需要在树状数组k这个位置上加上1即可。

② 归并排序的思想来解决逆序对的问题的过程其实也很好理解,我们其实是借助归并排序之后来求解逆序对的个数,归并排序其实是利用了分治的思想,分别对[l,mid]和[mid + 1,r]的区间进行排序,最终使得区间[l,r]是有序的,当我们递归求解区间[l,mid]和[mid + 1,r]之后然后求解[l,mid]中的每一个数与区间[mid + 1,r]的逆序对的个数即可。

3. 代码如下:

树状数组:

from typing import List

# 树状数组
class Solution:
     tr = None
     
     # lowbit函数求解二进制中最低位的1对应的十进制数字
     def lowbit(self, x: int): return x & -x

     # 在x的位置上加上v, add和query函数都需要借助于lowbit函数进行求解
     def add(self, x: int, v: int):
          tr = self.tr
          n = len(tr) - 1
          i = x
          while i <= n:
               tr[i] += v
               i += self.lowbit(i)
     # 查询区间[1: x]之前的区间和
     def query(self, x: int):
          i = x
          tr = self.tr
          res = 0
          while i > 0:
               res += tr[i]
               i -= self.lowbit(i)
          return res

     # 二分查找x的在nums中的位置, x在nums中一定存在, 而且这个位置是唯一的
     def binarySearch(self, nums: List[int], x: int):
          l, r = 0, len(nums) - 1
          while l < r:
               mid = l + r >> 1
               if nums[mid] >= x: r = mid
               else: l = mid + 1
          # 这里需要注意的一个问题是树状数组的下标是从1开始的所以这里返回x位置的时候需要加上1
          return r + 1
                    
     def reversePairs(self, nums: List[int]):
          # 离散化: 先去重然后排序
          _nums = list(set(nums))
          # 注意是先需要进行排序
          _nums.sort()
          n = len(_nums)
          # 树状数组的下标从1开始所以要声明多一个长度
          self.tr = [0] * (n + 1)
          res = 0
          # 因为求解的是逆序对所以需要逆序遍历nums, 先查询再更新
          for i in range(len(nums) - 1, -1, -1):
               x = nums[i]
               # 在_nums中查找离散化之后的位置k
               k = self.binarySearch(_nums, x)
               # [1: k - 1]就是以当前x为第一个数字的逆序对的个数
               res += self.query(k - 1)
               # 在k这个位置上加1
               self.add(k, 1)
          return res

归并排序思想:

from typing import List


# 归并排序思路

class Solution:
    res = 0

    def mergeSort(self, nums: List[int], l: int, r: int):
        if l >= r: return 0
        mid = l + r >> 1
        res = self.mergeSort(nums, l, mid) + self.mergeSort(nums, mid + 1, r)
        i, j = l, mid + 1
        # 计算递归之后左半部分区间的与右半部分区间的逆序对个数
        while i <= mid:
            while j <= r and nums[i] > nums[j]: j += 1
            self.res += j - mid - 1
            i += 1
        # h为辅助列表后面可以用来更新[l, r]区间, 使得[l, r]是有序的
        h = list()
        i, j = l, mid + 1
        while i <= mid and j <= r:
            if nums[i] <= nums[j]:
                h.append(nums[i])
                i += 1
            else:
                h.append(nums[j])
                j += 1
        # 左半部分区间还剩下字
        while i <= mid:
            h.append(nums[i])
            i += 1
        # 右半部分区间还剩下数字
        while j <= r:
            h.append(nums[j])
            j += 1
        k = 0
        # 令nums区间[l, r]是有序的
        for i in range(l, r + 1):
            nums[i] = h[k]
            k += 1
        return self.res

    def reversePairs(self, nums: List[int]) -> int:
        return self.mergeSort(nums, 0, len(nums) - 1)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值