排序篇 - leetcode LCR 170.交易逆序对的总数(hard)

1. 题目描述

跳转至 leetcode 作答界面

在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。

示例 1:

输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。

限制:

0 <= record.length <= 50000

2. 思路/题解

2.1 思考核心

思考的角度可以转换成 => 对于数组中的每个元素,如何找出左侧比自己大的元素的数量,能否在归并排序的合并过程中做到这一点呢?

2.2 方法步骤

概述

在归并排序的左部分与右部分的合并过程中,累加逆序对的统计结果

为什么会使用到归并排序?

直观来看,使用暴力统计法即可,即遍历数组的所有数字对并统计逆序对数量。此方法时间复杂度为 O(N²),观察题目给定的数组长度范围:0 < len(record) < 50000,可知此复杂度不可接受

「归并排序」与「逆序对」是息息相关的,主要有以下两个原因:

  1. 在归并排序的合并过程会大量涉及到左子数组的元素与右子数组中元素之间大小比较
  2. 每一轮合并期间,「右子数组中所有元素在原数组的位置」都位于「左子数组中所有元素在原数组的位置」的右侧

以上两点为统计逆序对提供了充足的可能

复习归并排序(分治思想)

归并排序使用分治的思想,先分成左子集和右子集分别排序,将排完后的左子集和右子集合并。 合并过程中需开辟一个额外空间为 O(n)的临时变量

归并排序基本步骤示意图
排序篇 - leetcode LCR 170.交易逆序对的总数(hard)_第1张图片

归并排序代码(归并排序类似二叉树后序遍历的思想)
代码框架

def mergesort(nums: List[int], lo: int, hi: int) -> None:
    if lo == hi:
        return
    mid = (lo + hi) // 2
    # 利用定义,排序 nums[lo..mid]
    mergesort(nums, lo, mid)
    # 利用定义,排序 nums[mid+1..hi]
    mergesort(nums, mid + 1, hi)

    # 后序位置,左子数组和右子数组已经被排好序
    # 合并两个有序数组,使 nums[lo..hi] 有序
    merge(nums, lo, mid, hi)  # merge() 中需要利用 tmp 临时数组

完整代码

class Solution:
    def __init__(self):
        self.tmp = []
    def sortArray(self, nums):
        self.tmp = [0] * len(nums)
        self.mergeSort(nums, 0, len(nums)-1)
        return nums

    def mergeSort(self, nums, lo, hi):
        if lo == hi :
            return 

        mid = (lo + hi) // 2
        # 后序遍历框架
        self.mergeSort(nums, lo, mid)
        self.mergeSort(nums, mid+1, hi)

        self.tmp[lo:hi+1] = nums[lo: hi+1]

        i, j, k = lo , mid + 1, lo
        while k <= hi:
            while i <= mid and j <= hi:
                if self.tmp[i] <= self.tmp[j]:
                    nums[k] = self.tmp[i]
                    i += 1
                else:
                    nums[k] = self.tmp[j]
                    j += 1
                k += 1
            # 左子数组走完了
            if i > mid:
                nums[k:hi+1] = self.tmp[j:hi+1]
                k = hi + 1
            else:
                nums[k:hi+1] = self.tmp[i:mid+1]
                k = hi + 1

本题关键:如何通过归并排序找出每个元素左侧比自己大的元素的数量

举个例子来找规律…

假设归并排序来到要合并「左子数组(已排好序):[2,3,6,7]」和「右子数组(已排好序):[0,1,4,5]」的阶段,如下图所示…

排序篇 - leetcode LCR 170.交易逆序对的总数(hard)_第2张图片

当左子数组与右子数组开始合并时,指针 i ,j 分别指向两者的起始点,如上图所示,此时我们发现, nums[j](0) 是小于 nums[i](2)的,又由于左子数组和右子数组都已经是升序排列好的,所以左子数组中 nums[i](2)右侧的所有元素肯定都是大于nums[j](0)的,此时此刻,对于 nums[j](0) 这个元素,就找到了 4 个逆序对,分别为 「2-0」「3-0」「6-0」「7-0」,然后将这个 4 加给结果变量,然后继续归并排序的常规流程…

于是我们发现规律:

归并排序在合并两个有序的数组时,指针 i ,j 分别在左子数组和右子数组上游走,当遇到「左子数组当前元素 > 右子数组当前元素」 时,就意味着 「左子数组从当前元素至末尾元素」 与 「右子数组当前元素」 构成了若干 「逆序对」,而这若干 「逆序对」的个数也很容易算出:Δ = mid - i + 1(mid 是左子数组最右侧元素的索引)

2.3 具体代码

其实对照归并排序的代码,会发现就多了两行统计 res 的值

class Solution:
    def __init__(self):
        self.tmp = []
    def reversePairs(self, record: List[int]) -> int:
        self.tmp = [0] * len(record)
        return self.merge_sort(record, 0 , len(record)-1)
    
    # 归并排序, 返回当前数组的逆序对
    def merge_sort(self, record, lo, hi):
        mid = (lo + hi) //2
        if lo >= hi:
            return 0
        res = self.merge_sort(record, lo, mid) + self.merge_sort(record, mid+1, hi)
        # 将 record 移动到 tmp 数组
        self.tmp[lo: hi+1] = record[lo: hi+1]
        i,j,k = lo, mid + 1,lo
        while k <= hi:
            while i <= mid and j <= hi:
                if self.tmp[i] > self.tmp[j]:
                    record[k] = self.tmp[j]
                    res += mid - i + 1
                    j += 1
                else:
                    record[k] = self.tmp[i]
                    i += 1
                k += 1
            # i 走完了
            if i > mid:
                record[k:hi+1] = self.tmp[j:hi+1]
                k = hi + 1
            else:
                record[k:hi+1] = self.tmp[i:mid+1]
                k = hi + 1
            
        return res

附-归并排序复杂度 & 稳定性

递归算法的复杂度:子问题个数 × 解决一个子问题的复杂度
对归并排序而言,时间复杂度集中在mergeSort(..)的执行次数,可以将归并排序联想成二叉树后序遍历的场景

树高:logN
每一层子问题累加的复杂度:O(N) 【 N 为原数组的长度 N】
总复杂度:O(NlogN)
归并排序是一种稳定的排序算法,相同的元素排序后相对位置不变

你可能感兴趣的:(算法刷题,leetcode,python,排序算法)