LeetCode-315,这可能是你能找到的注释最丰富的Python解答——分治+索引数组

题目及链接

315. 计算右侧小于当前元素的个数

给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例:

输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
 

提示:

0 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4

思路

这道题和剑指offer的51题,也是分治的经典问题“逆序对问题”何其相似?不了解的同学可以先去了解一下这道题,很简单,但思想很深刻,和本题待解决思路是一致的。

不过本题作为一道hard,显然不是一个简单的逆序对就完事了的。逆序对问题,只是返回“逆序对的数量”,而本题则多了个“定位”的需求——要把这些逆序对到底来源于哪个数字搞清楚,这也是本题最艰难的地方,也是最坑爹的地方——因为你一不小心,就会写出一个正确但超时的答案。。。

有关逆序对问题的前序知识:

这里要简述一下逆序对问题的两种解法。首先,这两种解法均出自分治,二者在分的过程是一致的,唯一区别在“治”的过程。假设我们现在已经做完了“分”操作,得到前、后两个有序的数组:[2,5,9]和[1,6,7],我们现在要往辅助数组里填它:

<思路1>(简单,但此思路复杂度比思路2高,用在此题上会导致超时错误):
对前后数组的数值进行大小比较(前数组:2,后数组:1),当后数组的数小时(注意,是小于,而非小于等于),说明出现了逆序,那么现在这个后数组的较小的数进入辅助数组,然后由于前后数组均有序,所以该数一定小于前数组所有数,所以逆序对的数量一下子新增3个(这个3来自于前数组的元素个数,因为它们都能和这个入辅助数组的1组成逆序对),以此类推,当前数组或后数组迭代完毕后,全部的逆序对数量也统计完毕。

<思路2>(稍微复杂,但比思路1复杂度稍低,用在此题可通过):
同样对前后数组的数值进行大小比较,假设现在辅助数组已有1个元素[1,2],那么目前(前数组:5,后数组:6),当前数组小于等于后数组时(注意,这里有等号了,可以想想为什么要加等号,要是一时糊涂没想到,我在后面会解惑),前数组的数当然要照常进入辅助数组进行排序,但我们在前数组的数进入辅助数组时,计算逆序对的数量。这样我们可以一次性算出当前入辅助数组的前数组的数“5”所有组成逆序对的数量——因为比5小的位置还在它后面的数都已经入辅助数组了(比5小,还在他后面,那就是后数组已经入辅助数组的数),所以前数组的5此时计算它身上背负的逆序对的总数,那就是1个,即后数组最初进辅助数组的那个“1”。以此类推,当后数组全迭代完毕后,如果前数组还有剩余(此时辅助数组=[1,2,5,6,7]),那么接着让前数组剩余的数"9"(or more)进入辅助数组,同时继续在前数组的数入辅助数组时计算逆序对的数量,只不过此时由于所有后数组数都已经进去了,所以它们的逆序对的数量,就都是后数组的元素总个数——3。

正文开始:

该题目用到了两个关键技术,一个是分治(或曰之归并排序进化版),一个是索引数组。分治及在逆序对问题上的做法,前文已述,现在要说的是索引数组的玄机。

索引数组,即用一个list记录原始数组里每个数的位置。还是以上面的例子来说,假设这个数组一开始就是nums=[2,5,9,1,6,7]这个样子,那么它对应的索引数组就是indexes=[0,1,2,3,4,5],该索引数组的脚标代表原始数组原本的位置(永远是不变的,索引数组的脚标也是不变的),而变化的是索引数组每个脚标对应的值。我们在分治过程中,若用原始的归并排序思路,会打乱数组nums顺序,让记录逆序对数量这件事变得非常复杂;但我们可借用索引数组,每次拷贝一个原始数组nums的脚标过来,然后记录脚标的位置变化,而非原数值的位置变化。

下面我会直接上代码。由于注释都在code里了,所以我也不废话了。但有一点需要说的是,我下面所提的辅助数组,其实都是“虚拟的辅助数组”,即它是不存在的。只会有记录脚标的temp数组。这点和归并排序是不同的,也是索引数组indexes存在的意义:

思路1对应的代码(正确,但超时)

class Solution1:
    def countSmaller(self, nums):
        size = len(nums)
        if size == 0:
            return []
        if size == 1:
            return [0]
        temp = [-1 for _ in range(size)]    # 拷贝上一轮脚标位置的数组。我们称之为“老脚标数组”
        res = [0 for _ in range(size)]      # 真正记录结果的数组
        indexes = [i for i in range(size)]  # 索引数组
        self.merge_and_count_smaller(nums, 0, size - 1, temp, indexes, res)
        return res

    def merge_and_count_smaller(self, nums, left, right, temp, indexes, res):
        if left == right:
            return
        mid = left + (right - left) // 2
        self.merge_and_count_smaller(nums, left, mid, temp, indexes, res)
        self.merge_and_count_smaller(nums, mid + 1, right, temp, indexes, res)

        # mid指向目前左边数组最后一个值,mid+1是右边数组的第一个值,因为左右数组内部有序,所以若这俩再有序,那左右就不用排了,算是归并的优化
        if nums[indexes[mid]] <= nums[indexes[mid + 1]]:# mid指向前数组的一个位置,indexes负责给它对回到原始位置,nums原始数组未改动,可以直接作大小比较
            return
        self.sort_and_count_smaller(nums, left, mid, right, temp, indexes, res)
    
    def sort_and_count_smaller(self, nums, left, mid, right, temp, indexes, res):
        # [left,mid] 前有序数组, [mid+1,right] 后有序数组
        # temp作用:记录上一轮归并之后的脚标位置
        for i in range(left, right + 1):
            temp[i] = indexes[i]   # temp相当于在存储上一轮归并后的脚标位置,而indexes一会儿用于存储当前这一轮最新的脚标位置
        p = left       # 前数组指针
        q = mid + 1    # 后数组指针
        assist_arr_num = 0    # 虚拟的辅助数组中,进入了几个元素。注,这个辅助数组最终显然应该有(right - left + 1)个元素
        
        while p <= mid and q <= right:
            # 后数组的值小于前数组,那么这个后数组的值小于前数组中的每个元素
            # 注意这里用的是temp数组而非indexes数组,因为我们现在比的是未更新的“[left,mid] 前数组, [mid+1,right] 后数组”
            # 而indexes指向的脚标是将要存入的**虚拟的**辅助数组、即新的顺序。temp指向的,才是旧顺序,即前后有序数组内部有序、但二者之间未归并的顺序
            if nums[temp[p]] > nums[temp[q]]:
                # 与mergeSort一致,谁小谁进辅助数组,这里则是谁小,谁就将脚标填入对应位置,相当于做了入辅助数组的操作
                indexes[left + assist_arr_num] = temp[q]
                # 后数组的数进了辅助数组,说明逆序对出现,前有序数组在当前位置p之后的所有元素的逆序值都该+1
                for i in range(p, mid+1, 1):   # 迭代更新前数组的逆序值,挨个+1
                    res[temp[i]] += 1
                q += 1    # 后数组走了一个,所以后数组指针步进
            else:    # 前数组的值小于后数组,不算逆序,不需更新res
                indexes[left + assist_arr_num] = temp[p]
                p += 1
            assist_arr_num += 1  # 虚拟存在的辅助数组元素个数+1
        if p == mid + 1:   # 前数组越界,就处理后数组。且此时说明后数组的数都大于前数组,不是逆序,不需要更新res
            for i in range(q, right+1, 1):
                indexes[left + assist_arr_num] = temp[i]
                assist_arr_num += 1
        if q == right + 1:  # 后数组越界,就处理前数组。且此时说明前数组剩余数值都大于后数组,都不构成逆序,不需要更新res
            for i in range(p, mid+1, 1):
                indexes[left + assist_arr_num] = temp[i]
                assist_arr_num += 1
        assert assist_arr_num == right - left + 1      # 这句话可不加,就是验证一下,最终虚拟的辅助数组的元素个数,一定是和前数组+后数组的元素总个数是一致的

思路2对应的代码

注意,思路2和思路1之间,只有sort_and_count_smaller()函数有区别

class Solution2:
    def countSmaller(self, nums):
        size = len(nums)
        if size == 0:
            return []
        if size == 1:
            return [0]
        temp = [-1 for _ in range(size)]    # 拷贝上一轮脚标位置的数组。我们称之为“老脚标数组”
        res = [0 for _ in range(size)]      # 真正记录结果的数组
        indexes = [i for i in range(size)]  # 索引数组
        self.merge_and_count_smaller(nums, 0, size - 1, temp, indexes, res)
        return res

    def merge_and_count_smaller(self, nums, left, right, temp, indexes, res):
        if left == right:
            return
        mid = left + (right - left) // 2
        self.merge_and_count_smaller(nums, left, mid, temp, indexes, res)
        self.merge_and_count_smaller(nums, mid + 1, right, temp, indexes, res)

        # mid指向目前左边数组最后一个值,mid+1是右边数组的第一个值,因为左右数组内部有序,所以若这俩再有序,那左右就不用排了,算是归并的优化
        if nums[indexes[mid]] <= nums[indexes[mid + 1]]:# mid指向前数组的一个位置,indexes负责给它对回到原始位置,nums原始数组未改动,可以直接作大小比较
            return
        self.sort_and_count_smaller(nums, left, mid, right, temp, indexes, res)
    
    def sort_and_count_smaller(self, nums, left, mid, right, temp, indexes, res):
        # [left,mid] 前有序数组, [mid+1,right] 后有序数组
        # temp作用:记录上一轮归并之后的脚标位置
        for i in range(left, right + 1):
            temp[i] = indexes[i]   # temp相当于在存储上一轮归并后的脚标位置,而indexes一会儿用于存储当前这一轮最新的脚标位置
        p = left       # 前数组指针
        q = mid + 1    # 后数组指针
        assist_arr_num = 0    # 虚拟的辅助数组中,进入了几个元素。注,这个辅助数组最终显然应该有(right - left + 1)个元素
        
        while p <= mid and q <= right:
            # 后数组的值小于前数组,那么这个后数组的值小于前数组中的每个元素
            # 注意这里用的是temp数组而非indexes数组,因为我们现在比的是未更新的“[left,mid] 前数组, [mid+1,right] 后数组”
            # 而indexes指向的脚标是原mergeSort要存入辅助数组、即新的顺序。temp指向的,才是旧顺序,即前后有序数组内部有序、但二者之间未归并的顺序
            if nums[temp[p]] <= nums[temp[q]]:   # 注意,这儿有等号!因为前数组的值入虚拟辅助数组,就要对res进行累加,而前后数组值相等时,显然不该对res更新(即计算新的逆序对),所以要让入辅助数组时不操作的后数后入,防止算累加时将这个相等的后数累加进去了
                # 与mergeSort一致,谁小谁进辅助数组,这里则是:谁小,谁就将脚标填入对应位置,相当于做了入辅助数组的操作
                indexes[left + assist_arr_num] = temp[p]
                # 前数组的数进了辅助数组,开始对逆序对计数,当前在辅助数组里的后数组里的数都是当前入辅助数组的这个数的逆序数。
                # 如何计算当前辅助数组里有几个后数组的数?那就要看后数组指针q相对于后数组左端`mid+1`走了几步了
                res[indexes[left + assist_arr_num]] += (q - (mid + 1))
                p += 1
            else:    # 前数组的值小于后数组,不算逆序,不需更新res
                indexes[left + assist_arr_num] = temp[q]
                q += 1
            assist_arr_num += 1  # 虚拟存在的辅助数组元素个数+1
        if p == mid + 1:   # 前数组越界,就处理后数组。且此时说明后数组的数都大于前数组,不是逆序,不需要更新res
            for i in range(q, right+1, 1):
                indexes[left + assist_arr_num] = temp[i]
                assist_arr_num += 1
        if q == right + 1:  # 后数组越界,就处理前数组。且此时说明前数组剩余数值都大于后数组,都可以对逆序计数,且计的个数为后数组全长,需更新res
            for i in range(p, mid+1, 1):
                indexes[left + assist_arr_num] = temp[i]
                res[indexes[left + assist_arr_num]] += (right - mid)   # 这是后数组全长
                assist_arr_num += 1
        assert assist_arr_num == right - left + 1

前面说过的,取等号问题的后面解惑在这里:详见上面的code line39

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_illusion_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值