排序题目:计算右侧小于当前元素的个数

题目

标题和出处

标题:计算右侧小于当前元素的个数

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

难度

8 级

题目描述

要求

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

示例

示例 1:

输入: nums   =   [5,2,6,1] \texttt{nums = [5,2,6,1]} nums = [5,2,6,1]
输出: [2,1,1,0] \texttt{[2,1,1,0]} [2,1,1,0]
解释:
5 \texttt{5} 5 的右侧有 2 \texttt{2} 2 个更小的元素( 2 \texttt{2} 2 1 \texttt{1} 1)。
2 \texttt{2} 2 的右侧有 1 \texttt{1} 1 个更小的元素( 1 \texttt{1} 1)。
6 \texttt{6} 6 的右侧有 1 \texttt{1} 1 个更小的元素( 1 \texttt{1} 1)。
1 \texttt{1} 1 的右侧有 0 \texttt{0} 0 个更小的元素。

示例 2:

输入: nums   =   [-1] \texttt{nums = [-1]} nums = [-1]
输出: [0] \texttt{[0]} [0]

示例 3:

输入: nums   =   [-1,-1] \texttt{nums = [-1,-1]} nums = [-1,-1]
输出: [0,0] \texttt{[0,0]} [0,0]

数据范围

  • 1 ≤ nums.length ≤ 10 5 \texttt{1} \le \texttt{nums.length} \le \texttt{10}^\texttt{5} 1nums.length105
  • -10 4 ≤ nums[i] ≤ 10 4 \texttt{-10}^\texttt{4} \le \texttt{nums[i]} \le \texttt{10}^\texttt{4} -104nums[i]104

前言

定义「逆序对」的概念如下:对于下标 i i i j j j,如果 i < j i < j i<j nums [ i ] > nums [ j ] \textit{nums}[i] > \textit{nums}[j] nums[i]>nums[j],则下标对 ( i , j ) (i, j) (i,j) 是一个逆序对。

这道题要求计算数组 nums \textit{nums} nums 中的每个元素右侧小于当前元素的个数,实质是计算每个元素右侧的元素中与当前元素形成逆序对的元素个数。

对于长度为 n n n 的数组,最朴素的计算逆序对的数量的方法是对于每个元素遍历其右边的元素并统计可以形成逆序对的元素个数,每个元素需要 O ( n ) O(n) O(n) 的时间计算逆序对的个数,总时间复杂度是 O ( n 2 ) O(n^2) O(n2)。由于数组 nums \textit{nums} nums 的长度最大为 1 0 5 10^5 105,因此 O ( n 2 ) O(n^2) O(n2) 的时间复杂度过高,必须使用时间复杂度更低的方法。

以下介绍三种时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的计算逆序对数量的方法,分别是归并排序、线段树和树状数组。

解法一

思路和算法

由于逆序对和排序相关,因此可以使用排序的思想计算逆序对的数量。归并排序的过程中,每次将两个升序子数组合并,合并的过程中即可发现这两个升序子数组中的逆序对。因此可以在归并排序的过程中计算每个元素右侧的元素中与当前元素形成逆序对的元素个数。

考虑下标范围 [ 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. 将下标范围 [ low , mid ] [\textit{low}, \textit{mid}] [low,mid] 和下标范围 [ mid + 1 , high ] [\textit{mid} + 1, \textit{high}] [mid+1,high] 的两个子数组分别排序,得到两个升序子数组。

  3. i i i j j j 分别表示下标范围 [ low , mid ] [\textit{low}, \textit{mid}] [low,mid] 和下标范围 [ mid + 1 , high ] [\textit{mid} + 1, \textit{high}] [mid+1,high] 的两个升序子数组中的待合并元素下标,初始时 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 时,按照如下操作合并两个子数组,合并过程中需要保持排序的稳定性。

    • 如果 nums [ i ] ≤ nums [ j ] \textit{nums}[i] \le \textit{nums}[j] nums[i]nums[j],则将 nums [ i ] \textit{nums}[i] nums[i] 添加到合并后的结果中,然后将 i i i 1 1 1

    • 如果 nums [ i ] > nums [ j ] \textit{nums}[i] > \textit{nums}[j] nums[i]>nums[j],则将 nums [ j ] \textit{nums}[j] nums[j] 添加到合并后的结果中,然后将 j j j 1 1 1

  4. 当其中一个升序子数组的元素全部合并结束之后,将另一个升序子数组的元素依次添加到合并后的结果中。

由于合并过程保持排序的稳定性,因此当 nums [ i ] ≤ nums [ j ] \textit{nums}[i] \le \textit{nums}[j] nums[i]nums[j] 或者 j = high + 1 j = \textit{high} + 1 j=high+1 时,一定有 nums [ i ] > nums [ j − 1 ] \textit{nums}[i] > \textit{nums}[j - 1] nums[i]>nums[j1](如果 j = high + 1 j = \textit{high} + 1 j=high+1,则有 j − 1 = high j - 1 = \textit{high} j1=high),否则 nums [ i ] = nums [ j − 1 ] \textit{nums}[i] = \textit{nums}[j - 1] nums[i]=nums[j1],在合并 nums [ i ] \textit{nums}[i] nums[i] 之前合并 nums [ j − 1 ] \textit{nums}[j - 1] nums[j1] 与归并排序的稳定性矛盾。因此对于任意 mid + 1 ≤ k ≤ j − 1 \textit{mid} + 1 \le k \le j - 1 mid+1kj1 都有 nums [ i ] > nums [ k ] \textit{nums}[i] > \textit{nums}[k] nums[i]>nums[k],即在下标范围 [ mid + 1 , high ] [\textit{mid} + 1, \textit{high}] [mid+1,high] 的升序子数组中有 j − mid − 1 j - \textit{mid} - 1 jmid1 个数小于 nums [ i ] \textit{nums}[i] nums[i],将 j − mid − 1 j - \textit{mid} - 1 jmid1 加到 nums [ i ] \textit{nums}[i] nums[i] 对应的计数中。

为了记录每个元素右侧小于当前元素的个数,需要维护下标数组和计数数组,在归并排序计算逆序对的过程中同时对原数组和下标数组执行归并操作,确保原数组和下标数组中的每个元素分别对应。当更新一个元素对应的计数时,从下标数组得到该元素的下标,更新计数数组中该下标处的计数。

当整个数组排序结束时,计数数组即为每个元素右侧小于当前元素的个数。

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

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

代码

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

class Solution {
    public List<Integer> countSmaller(int[] nums) {
        int length = nums.length;
        int[] indices = new int[length];
        for (int i = 0; i < length; i++) {
            indices[i] = i;
        }
        List<Integer> counts = new ArrayList<Integer>();
        for (int i = 0; i < length; i++) {
            counts.add(0);
        }
        mergeSortAndCount(nums, indices, counts, 0, length - 1);
        return counts;
    }

    public void mergeSortAndCount(int[] nums, int[] indices, List<Integer> counts, int low, int high) {
        if (low >= high) {
            return;
        }
        int mid = low + (high - low) / 2;
        mergeSortAndCount(nums, indices, counts, low, mid);
        mergeSortAndCount(nums, indices, counts, mid + 1, high);
        merge(nums, indices, counts, low, mid, high);
    }

    public void merge(int[] nums, int[] indices, List<Integer> counts, int low, int mid, int high) {
        int currLength = high - low + 1;
        int[] tempNums = new int[currLength];
        int[] tempIndices = new int[currLength];
        int i = low, j = mid + 1, k = 0;
        while (i <= mid && j <= high) {
            if (nums[i] <= nums[j]) {
                tempNums[k] = nums[i];
                tempIndices[k] = indices[i];
                counts.set(indices[i], counts.get(indices[i]) + (j - mid - 1));
                i++;
            } else {
                tempNums[k] = nums[j];
                tempIndices[k] = indices[j];
                j++;
            }
            k++;
        }
        while (i <= mid) {
            tempNums[k] = nums[i];
            tempIndices[k] = indices[i];
            counts.set(indices[i], counts.get(indices[i]) + (j - mid - 1));
            i++;
            k++;
        }
        while (j <= high) {
            tempNums[k] = nums[j];
            tempIndices[k] = indices[j];
            j++;
            k++;
        }
        System.arraycopy(tempNums, 0, nums, low, currLength);
        System.arraycopy(tempIndices, 0, indices, low, currLength);
    }
}

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

class Solution {
    public List<Integer> countSmaller(int[] nums) {
        int length = nums.length;
        int[] indices = new int[length];
        for (int i = 0; i < length; i++) {
            indices[i] = i;
        }
        List<Integer> counts = new ArrayList<Integer>();
        for (int i = 0; i < length; i++) {
            counts.add(0);
        }
        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);
                merge(nums, indices, counts, low, mid, high);
            }
        }
        return counts;
    }

    public void merge(int[] nums, int[] indices, List<Integer> counts, int low, int mid, int high) {
        int currLength = high - low + 1;
        int[] tempNums = new int[currLength];
        int[] tempIndices = new int[currLength];
        int i = low, j = mid + 1, k = 0;
        while (i <= mid && j <= high) {
            if (nums[i] <= nums[j]) {
                tempNums[k] = nums[i];
                tempIndices[k] = indices[i];
                counts.set(indices[i], counts.get(indices[i]) + (j - mid - 1));
                i++;
            } else {
                tempNums[k] = nums[j];
                tempIndices[k] = indices[j];
                j++;
            }
            k++;
        }
        while (i <= mid) {
            tempNums[k] = nums[i];
            tempIndices[k] = indices[i];
            counts.set(indices[i], counts.get(indices[i]) + (j - mid - 1));
            i++;
            k++;
        }
        while (j <= high) {
            tempNums[k] = nums[j];
            tempIndices[k] = indices[j];
            j++;
            k++;
        }
        System.arraycopy(tempNums, 0, nums, low, currLength);
        System.arraycopy(tempIndices, 0, indices, 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 ( n ) O(n) O(n) 的空间,自顶向下实现时需要递归调用栈的空间是 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 计算逆序对的数量。

创建线段树,用于存储数组 nums \textit{nums} nums 中的每个元素的名次。从右到遍历数组 nums \textit{nums} nums,对于每个元素 num \textit{num} num,其右侧的元素已经遍历过,其右侧的元素中的每个小于 num \textit{num} num 的元素都与 num \textit{num} num 组成一个逆序对,因此得到 num \textit{num} num 的名次 rank \textit{rank} rank,计算线段树的子区间 [ 0 , rank − 1 ] [0, \textit{rank} - 1] [0,rank1] 中的元素个数,该元素个数即为当前元素 num \textit{num} num 元素右侧小于 num \textit{num} num 的元素个数。得到当前元素右侧小于当前元素的个数之后,将 rank \textit{rank} rank 添加到线段树中,继续向左遍历元素并计算每个元素右侧小于当前元素的个数。遍历结束之后,即可得到数组 nums \textit{nums} nums 中的每个元素右侧小于当前元素的个数。

代码

class Solution {
    public List<Integer> countSmaller(int[] nums) {
        List<Integer> counts = new ArrayList<Integer>();
        int length = nums.length;
        for (int i = 0; i < length; i++) {
            counts.add(0);
        }
        Map<Integer, Integer> ranks = getRanks(nums);
        SegmentTree st = new SegmentTree(length);
        for (int i = length - 1; i >= 0; i--) {
            int rank = ranks.get(nums[i]);
            counts.set(i, st.getCount(0, rank - 1));
            st.add(rank);
        }
        return counts;
    }

    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;
    }
}

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,计算树状数组的子区间 [ 0 , rank + 1 ] [0, \textit{rank} + 1] [0,rank+1] 中的元素个数,该元素个数即为即为当前元素 num \textit{num} num 元素右侧小于 num \textit{num} num 的元素个数。得到当前元素右侧小于当前元素的个数之后,将 rank \textit{rank} rank 添加到树状数组中,继续向左遍历元素并计算每个元素右侧小于当前元素的个数。遍历结束之后,即可得到数组 nums \textit{nums} nums 中的每个元素右侧小于当前元素的个数。

代码

class Solution {
    public List<Integer> countSmaller(int[] nums) {
        List<Integer> counts = new ArrayList<Integer>();
        int length = nums.length;
        for (int i = 0; i < length; i++) {
            counts.add(0);
        }
        Map<Integer, Integer> ranks = getRanks(nums);
        BinaryIndexedTree bit = new BinaryIndexedTree(length);
        for (int i = length - 1; i >= 0; i--) {
            int rank = ranks.get(nums[i]);
            counts.set(i, bit.getCount(0, rank - 1));
            bit.add(rank);
        }
        return counts;
    }

    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;
    }
}

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、付费专栏及课程。

余额充值