排序题目:翻转对

题目

标题和出处

标题:翻转对

出处:493. 翻转对

难度

8 级

题目描述

要求

给定一个整数数组 nums \texttt{nums} nums,返回数组中的翻转对的数量。

如果下标对 (i,   j) \texttt{(i, j)} (i, j) 满足 0 ≤ i < j < nums.length \texttt{0} \le \texttt{i} < \texttt{j} < \texttt{nums.length} 0i<j<nums.length nums[i] > 2 × nums[j] \texttt{nums[i]} > \texttt{2} \times \texttt{nums[j]} nums[i]>2×nums[j],则称为翻转对。

示例

示例 1:

输入: nums   =   [1,3,2,3,1] \texttt{nums = [1,3,2,3,1]} nums = [1,3,2,3,1]
输出: 2 \texttt{2} 2

示例 2:

输入: nums   =   [2,4,3,5,1] \texttt{nums = [2,4,3,5,1]} nums = [2,4,3,5,1]
输出: 3 \texttt{3} 3

数据范围

  • 1 ≤ nums.length ≤ 5 × 10 4 \texttt{1} \le \texttt{nums.length} \le \texttt{5} \times \texttt{10}^\texttt{4} 1nums.length5×104
  • -2 31 ≤ nums[i] ≤ 2 31 − 1 \texttt{-2}^\texttt{31} \le \texttt{nums[i]} \le \texttt{2}^\texttt{31} - \texttt{1} -231nums[i]2311

解法一

思路和算法

对于长度为 n n n 的数组,最朴素的计算翻转对的数量的方法的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。由于数组 nums \textit{nums} nums 的长度最大为 5 × 1 0 4 5 \times 10^4 5×104,因此 O ( n 2 ) O(n^2) O(n2) 的时间复杂度过高,必须使用时间复杂度更低的方法。

由于翻转对和排序相关,因此可以使用排序的思想计算翻转对的数量。归并排序的过程中,每次将两个升序子数组合并,合并的过程中即可发现这两个升序子数组中的翻转对。因此可以在归并排序的过程中完成翻转对数量的计算。

考虑下标范围 [ low , high ] [\textit{low}, \textit{high}] [low,high] 的子数组。当 low ≥ high \textit{low} \ge \textit{high} lowhigh 时,子数组的长度是 0 0 0 1 1 1,此时子数组中不存在翻转对。当 low < high \textit{low} < \textit{high} low<high 时,子数组的长度大于等于 2 2 2,将子数组分成两个更短的子数组,两个更短的子数组排序之后合并,同时计算翻转对。具体做法如下。

  1. low \textit{low} low high \textit{high} high 的平均值 mid \textit{mid} mid,将下标范围 [ low , high ] [\textit{low}, \textit{high}] [low,high] 的子数组分成下标范围 [ low , mid ] [\textit{low}, \textit{mid}] [low,mid] 和下标范围 [ mid + 1 , high ] [\textit{mid} + 1, \textit{high}] [mid+1,high] 的两个子数组。

  2. 对两个子数组分别排序并计算翻转对的数量,得到两个升序的子数组和两个子数组分别包含的翻转对数量。

  3. 计算两个升序子数组之间的翻转对数量。为了计算两个升序子数组之间的翻转对数量,需要定义两个指针 i i i j j j,初始时分别指向两个升序子数组的开始位置,即 i = low i = \textit{low} i=low j = mid + 1 j = \textit{mid} + 1 j=mid+1。当 i ≤ mid i \le \textit{mid} imid j ≤ high j \le \textit{high} jhigh 时,根据指针 i i i j j j 指向的元素计算翻转对数量。

    • 如果 nums [ i ] > 2 × nums [ j ] \textit{nums}[i] > 2 \times \textit{nums}[j] nums[i]>2×nums[j],则 ( i , j ) (i, j) (i,j) 是翻转对,由于对于任意 i ≤ k ≤ mid i \le k \le \textit{mid} ikmid 都有 nums [ i ] ≤ nums [ k ] \textit{nums}[i] \le \textit{nums}[k] nums[i]nums[k],因此 nums [ k ] > 2 × nums [ j ] \textit{nums}[k] > 2 \times \textit{nums}[j] nums[k]>2×nums[j],即 ( k , j ) (k, j) (k,j) 是翻转对,因此以 j j j 作为右侧下标的翻转对数量是 mid − i + 1 \textit{mid} - i + 1 midi+1。更新翻转对数量之后,将 j j j 指向的下标加 1 1 1

    • 如果 nums [ i ] ≤ 2 × nums [ j ] \textit{nums}[i] \le 2 \times \textit{nums}[j] nums[i]2×nums[j],则 ( i , j ) (i, j) (i,j) 不是翻转对,将 i i i 指向的下标加 1 1 1

  4. 计算两个升序子数组之间的翻转对数量之后,合并两个升序子数组。

当整个数组排序结束时即可得到整个数组的翻转对数量。

由于计算两个升序子数组之间的翻转对数量以及合并两个升序子数组都需要遍历两个升序子数组,因此计算翻转对数量并没有提升总时间复杂度,总时间复杂度和原始归并排序一样是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

归并排序可以使用自顶向下的方式递归实现,也可以使用自底向上的方式迭代实现。对于同一个数组,使用自顶向下和自底向上两种方式实现的中间过程可能有所区别,但是都能得到正确的结果。

代码

以下代码为归并排序的自顶向下实现。

class Solution {
    public int reversePairs(int[] nums) {
        return mergeSortAndCount(nums, 0, nums.length - 1);
    }

    public int mergeSortAndCount(int[] nums, int low, int high) {
        if (low >= high) {
            return 0;
        }
        int mid = low + (high - low) / 2;
        int count = mergeSortAndCount(nums, low, mid) + mergeSortAndCount(nums, mid + 1, high);
        int i = low, j = mid + 1;
        while (i <= mid && j <= high) {
            if ((long) nums[i] > 2 * (long) nums[j]) {
                count += mid - i + 1;
                j++;
            } else {
                i++;
            }
        }
        merge(nums, low, mid, high);
        return count;
    }

    public void merge(int[] nums, int low, int mid, int high) {
        int currLength = high - low + 1;
        int[] temp = new int[currLength];
        int i = low, j = mid + 1, k = 0;
        while (i <= mid && j <= high) {
            if (nums[i] <= nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++] = nums[j++];
            }
        }
        while (i <= mid) {
            temp[k++] = nums[i++];
        }
        while (j <= high) {
            temp[k++] = nums[j++];
        }
        System.arraycopy(temp, 0, nums, low, currLength);
    }
}

以下代码为归并排序的自底向上实现。

class Solution {
    public int reversePairs(int[] nums) {
        int count = 0;
        int length = nums.length;
        for (int halfLength = 1, currLength = 2; halfLength < length; halfLength *= 2, currLength *= 2) {
            for (int low = 0; low < length - halfLength; low += currLength) {
                int mid = low + halfLength - 1;
                int high = Math.min(low + currLength - 1, length - 1);
                int i = low, j = mid + 1;
                while (i <= mid && j <= high) {
                    if ((long) nums[i] > 2 * (long) nums[j]) {
                        count += mid - i + 1;
                        j++;
                    } else {
                        i++;
                    }
                }
                merge(nums, low, mid, high);
            }
        }
        return count;
    }

    public void merge(int[] nums, int low, int mid, int high) {
        int currLength = high - low + 1;
        int[] temp = new int[currLength];
        int i = low, j = mid + 1, k = 0;
        while (i <= mid && j <= high) {
            if (nums[i] <= nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++] = nums[j++];
            }
        }
        while (i <= mid) {
            temp[k++] = nums[i++];
        }
        while (j <= high) {
            temp[k++] = nums[j++];
        }
        System.arraycopy(temp, 0, nums, low, currLength);
    }
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。归并排序的时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。自顶向下实现时需要递归调用栈的空间是 O ( log ⁡ n ) O(\log n) O(logn),自底向上实现时可以省略递归调用栈的空间。无论是自顶向下实现还是自底向上实现,归并过程需要 O ( n ) O(n) O(n) 的辅助空间。

解法二

预备知识

该解法涉及到二分查找和线段树。

二分查找的作用是在有序序列中查找目标值,或者在特定范围中查找符合要求的最大值或最小值。

线段树是一种二叉搜索树,将一个区间划分成两个更短的区间。线段树中的每个叶结点都是长度为 1 1 1 的区间,称为单元区间。

线段树支持区间的快速查询和修改。对于长度为 n n n 的区间,使用线段树查询特定子区间的元素个数以及修改特定子区间内的元素个数的时间是 O ( log ⁡ n ) O(\log n) O(logn)

有时,为了降低线段树的空间复杂度,需要使用离散化。

思路和算法

对于长度为 n n n 的数组,其中不同元素的个数最多有 n n n 个。由于计算翻转对数量只需要考虑元素的相对大小,因此可以使用离散化将区间长度限制在 n n n 以内。离散化的方法是计算数组 nums \textit{nums} nums 中的每个元素的名次,计算名次的方法是:新建数组 sorted \textit{sorted} sorted 并将数组 nums \textit{nums} nums 中的所有元素复制到数组 sorted \textit{sorted} sorted 中,然后对数组 sorted \textit{sorted} sorted 排序,排序之后, sorted [ i ] \textit{sorted}[i] sorted[i] 的名次为 i i i,如果 i > 0 i > 0 i>0 sorted [ i ] = sorted [ i − 1 ] \textit{sorted}[i] = \textit{sorted}[i - 1] sorted[i]=sorted[i1] sorted [ i ] \textit{sorted}[i] sorted[i] 的名次与 sorted [ i − 1 ] \textit{sorted}[i - 1] sorted[i1] 的名次相同。每个元素的名次都在范围 [ 0 , n − 1 ] [0, n - 1] [0,n1] 中,表示数组中的小于该元素的元素个数。

创建线段树,用于存储数组 nums \textit{nums} nums 中的每个元素的名次。从左到右遍历数组 nums \textit{nums} nums,对于每个元素 num \textit{num} num,其左边的元素已经遍历过,其左边的元素中的每个大于 2 × num 2 \times \textit{num} 2×num 的元素都与 num \textit{num} num 组成一个翻转对,因此得到 num \textit{num} num 的名次 rank \textit{rank} rank,并使用二分查找得到大于等于 2 × num + 1 2 \times \textit{num} + 1 2×num+1 的元素的最小名次 minRank \textit{minRank} minRank,计算线段树的子区间 [ minRank , n − 1 ] [\textit{minRank}, n - 1] [minRank,n1] 中的元素个数,该元素个数即为已经遍历过的元素中的大于 2 × num 2 \times \textit{num} 2×num 的元素个数,将翻转对数量增加该元素个数。更新翻转对数量之后,将 rank \textit{rank} rank 添加到线段树中,继续遍历后面的元素并更新翻转对数量。遍历结束之后,即可得到数组 nums \textit{nums} nums 中的翻转对数量。

代码

class Solution {
    public int reversePairs(int[] nums) {
        int count = 0;
        Map<Integer, Integer> ranks = getRanks(nums);
        int length = nums.length;
        int[] sorted = new int[length];
        for (int i = 0; i < length; i++) {
            sorted[i] = nums[i];
        }
        Arrays.sort(sorted);
        SegmentTree st = new SegmentTree(length);
        for (int i = 0; i < length; i++) {
            int rank = ranks.get(nums[i]);
            int minRank = binarySearch(sorted, (long) 2 * nums[i] + 1);
            count += st.getCount(minRank, length - 1);
            st.add(rank);
        }
        return count;
    }

    public Map<Integer, Integer> getRanks(int[] nums) {
        int length = nums.length;
        int[] sorted = new int[length];
        System.arraycopy(nums, 0, sorted, 0, length);
        Arrays.sort(sorted);
        Map<Integer, Integer> ranks = new HashMap<Integer, Integer>();
        for (int i = 0; i < length; i++) {
            int num = sorted[i];
            if (i == 0 || num > sorted[i - 1]) {
                ranks.put(num, i);
            }
        }
        return ranks;
    }

    public int binarySearch(int[] nums, long target) {
        int low = 0, high = nums.length;
        while (low < high) {
            int mid = low + (high - low) / 2;
            if (nums[mid] >= target) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return low;
    }
}

class SegmentTree {
    int length;
    int[] tree;

    public SegmentTree(int length) {
        this.length = length;
        this.tree = new int[length * 4];
    }

    public int getCount(int start, int end) {
        return getCount(start, end, 0, 0, length - 1);
    }

    public void add(int rank) {
        add(rank, 0, 0, length - 1);
    }

    private int getCount(int rangeStart, int rangeEnd, int index, int treeStart, int treeEnd) {
        if (rangeStart > rangeEnd) {
            return 0;
        }
        if (rangeStart == treeStart && rangeEnd == treeEnd) {
            return tree[index];
        }
        int mid = treeStart + (treeEnd - treeStart) / 2;
        if (rangeEnd <= mid) {
            return getCount(rangeStart, rangeEnd, index * 2 + 1, treeStart, mid);
        } else if (rangeStart > mid) {
            return getCount(rangeStart, rangeEnd, index * 2 + 2, mid + 1, treeEnd);
        } else {
            return getCount(rangeStart, mid, index * 2 + 1, treeStart, mid) + getCount(mid + 1, rangeEnd, index * 2 + 2, mid + 1, treeEnd);
        }
    }

    private void add(int rank, int index, int start, int end) {
        if (start == end) {
            tree[index]++;
            return;
        }
        int mid = start + (end - start) / 2;
        if (rank <= mid) {
            add(rank, index * 2 + 1, start, mid);
        } else {
            add(rank, index * 2 + 2, mid + 1, end);
        }
        tree[index] = tree[index * 2 + 1] + tree[index * 2 + 2];
    }
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。每个元素在线段树中的查询和更新操作都需要 O ( log ⁡ n ) O(\log n) O(logn) 的时间,时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。创建用于排序的数组和线段树需要 O ( n ) O(n) O(n) 的空间。

解法三

预备知识

该解法涉及到二分查找和树状数组。

二分查找的作用是在有序序列中查找目标值,或者在特定范围中查找符合要求的最大值或最小值。

树状数组也称二叉索引树,由 Peter M. Fenwick 发明,因此又称 Fenwick 树。树状数组支持快速计算数组的前缀和与区间和,以及快速修改。对于长度为 n n n 的区间,使用树状数组查询特定子区间的区间和以及修改特定子区间内的元素值的时间是 O ( log ⁡ n ) O(\log n) O(logn)

有时,为了降低树状数组的空间复杂度,需要使用离散化。

思路和算法

计算翻转对数量也可以使用树状数组实现。

首先使用离散化将区间长度限制在 n n n 以内,然后创建树状数组,用于存储数组 nums \textit{nums} nums 中的每个元素的名次。从左到右遍历数组 nums \textit{nums} nums,对于每个元素 num \textit{num} num,得到 num \textit{num} num 的名次 rank \textit{rank} rank,并使用二分查找得到大于等于 2 × num + 1 2 \times \textit{num} + 1 2×num+1 的元素的最小名次 minRank \textit{minRank} minRank,计算树状数组的子区间 [ minRank , n − 1 ] [\textit{minRank}, n - 1] [minRank,n1] 中的元素个数,该元素个数即为已经遍历过的元素中的大于 2 × num 2 \times \textit{num} 2×num 的元素个数,将翻转对数量增加该元素个数。更新翻转对数量之后,将 rank \textit{rank} rank 添加到树状数组中,继续遍历后面的元素并更新翻转对数量。遍历结束之后,即可得到数组 nums \textit{nums} nums 中的翻转对数量。

代码

class Solution {
    public int reversePairs(int[] nums) {
        int count = 0;
        Map<Integer, Integer> ranks = getRanks(nums);
        int length = nums.length;
        int[] sorted = new int[length];
        for (int i = 0; i < length; i++) {
            sorted[i] = nums[i];
        }
        Arrays.sort(sorted);
        BinaryIndexedTree bit = new BinaryIndexedTree(length);
        for (int i = 0; i < length; i++) {
            int rank = ranks.get(nums[i]);
            int minRank = binarySearch(sorted, (long) 2 * nums[i] + 1);
            count += bit.getCount(minRank, length - 1);
            bit.add(rank);
        }
        return count;
    }

    public Map<Integer, Integer> getRanks(int[] nums) {
        int length = nums.length;
        int[] sorted = new int[length];
        System.arraycopy(nums, 0, sorted, 0, length);
        Arrays.sort(sorted);
        Map<Integer, Integer> ranks = new HashMap<Integer, Integer>();
        for (int i = 0; i < length; i++) {
            int num = sorted[i];
            if (i == 0 || num > sorted[i - 1]) {
                ranks.put(num, i);
            }
        }
        return ranks;
    }

    public int binarySearch(int[] nums, long target) {
        int low = 0, high = nums.length;
        while (low < high) {
            int mid = low + (high - low) / 2;
            if (nums[mid] >= target) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return low;
    }
}

class BinaryIndexedTree {
    int length;
    int[] tree;

    public BinaryIndexedTree(int length) {
        this.length = length;
        this.tree = new int[length + 1];
    }

    public int getCount(int start, int end) {
        return getPrefixSum(end + 1) - getPrefixSum(start);
    }

    public void add(int index) {
        index++;
        while (index <= length) {
            tree[index]++;
            index += lowbit(index);
        }
    }

    private int getPrefixSum(int index) {
        int sum = 0;
        while (index > 0) {
            sum += tree[index];
            index -= lowbit(index);
        }
        return sum;
    }

    private static int lowbit(int x) {
        return x & (-x);
    }
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。每个元素在树状数组中的查询和更新操作都需要 O ( log ⁡ n ) O(\log n) O(logn) 的时间,时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。创建用于排序的数组和树状数组需要 O ( n ) O(n) O(n) 的空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

伟大的车尔尼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值