面试题51 数组中的逆序对 分治算法 归并排序

题目

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

示例 1:

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

限制:

0 <= 数组长度 <= 50000

思路1 暴力求解

对数组中的每一个数Xi,它的后面有ni个比它小的数,则有多少个以数Xi开头的逆序对。

所以,总对数为:

\Sigma_{i=1}^{N}(n_i)

其中N为数组长度。

得到如下代码:

class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        N = len(nums)
        ans = []
        for i in range(N):
            num = 0
            for j in range(i+1, N):
                if nums[i] > nums[j]:
                    num = num + 1
            ans.append(num)
        return sum(ans)

超出时间限制(时间复杂度O(N^2))

思路2 分治算法 + 归并排序

原理

分治思想即分而治之。

归并排序的含义如下:

归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

归并操作,也叫归并算法,指的是将两个顺序序列合并成一个顺序序列的方法。

如 设有数列{6,202,100,301,38,8,1}

初始状态:6,202,100,301,38,8,1

第一次归并后:{6,202},{100,301},{8,38},{1};

第二次归并后:{6,100,202,301},{1,8,38};

第三次归并后:{1,6,8,38,100,202,301};

这就是归并排序

归并排序使用的是分治算法的思想,它和逆序对的关系如下图所示:

而[2,3,5,8,1,3,2,1]的逆序对的确为16.

具体地,以[2,3,5,8]和[1,1,2,3]为例,

使用双指针进行归并排序时:

  • step1:lp指向2, rp指向1, 1<2, 故将rp指向的1添加到结果list中,res = [1], rp右移1;
  • step2: lp指向2, rp指向1, 1<2, 故将rp指向的1添加到结果list中,res = [1, 1], rp右移1;
  • step3: lp指向2, rp指向2, 2 不小于 2, 故将lp指向的2添加到结果list中,res = [1, 1, 2], lp右移1; 一旦lp右移,就记录下该lp指向的值对逆序对的贡献。它的贡献就是移动lp时rp移动的次数。例如,此时因为rp移动了两次,故2对逆序对的贡献为2;
  • step4: lp指向3, rp指向2, 2 < 3, 故将rp指向的2添加到结果list中,res = [1, 1, 2, 2], rp右移1;
  • step5: lp指向3, rp指向3, 3 不小于 3, 故将lp指向的3添加到结果list中,res = [1, 1, 2, 2, 3], lp右移1;3对逆序对的贡献为3;
  • step6: lp指向5, rp指向3, 3 小于 5, 故将rp指向的3添加到结果list中,res = [1, 1, 2, 2, 3, 3], rp右移1;
  • step7: 因为rp已经是最后一个,故左侧剩余的5和8可直接加入到res中, res = [1, 1, 2, 2, 3, 3, 5, 8]. 5和8对逆序对的贡献均为4.

因此,归并[2,3,5,8]和[1,1,2,3]的过程中,逆序对共有2+3+4+4=13组。

以此类推,就可以得到上图中的结果,也能够理解为什么可以通过归并排序来计算逆序对。

代码

使用递归法实现上述思想,此处使用递归的基本原理如下图:

代码如下

class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        n = len(nums)
        temp = [0] * n  # 最终为归并排序好的list
        count = merge(nums, temp, 0, n-1)
        return count
    
def merge(nums, temp, l, r):
    if l>=r:
        return 0
    
    mid = (l + r) // 2
    count = merge(nums, temp, l, mid) + merge(nums, temp, mid+1, r)
    i = l       # 左指针
    j = mid + 1 # 右指针
    pos = l     # 此次递归temp的初始位置
    while i <= mid and j <= r:
        if nums[i] <= nums[j]:
            temp[pos] = nums[i]
            i += 1
            count += (j - (mid + 1))
        else:
            temp[pos] = nums[j]
            j += 1
        pos += 1
    
    # 多余的部分
    for k in range(i, mid + 1):
        temp[pos] = nums[k]
        count += (j - (mid + 1))
        pos += 1
    for k in range(j, r + 1):
        temp[pos] = nums[k]
        pos += 1

    nums[l:r+1] = temp[l:r+1]

    return count

复杂度

时间复杂度O(NlogN)

空间复杂度O(N)

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

R.X. NLOS

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

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

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

打赏作者

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

抵扣说明:

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

余额充值