LeetCode第 315 题:计算右侧小于当前元素的个数(C++)

315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

暴力法,两层for循环,很简单,但是会超出时间限制。怎么优化呢?

看题目的直觉思路是逆序找,因为左边会包含右边,举个例子:
[ 6 , 4 , 5 , 2 , 3 , 1 ] [6,4,5,2,3,1] [6,4,5,2,3,1]
对应的count数组为:
[ 5 , 3 , 3 , 1 , 1 , 0 ] [5,3,3,1,1,0] [5,3,3,1,1,0]
元素5右边有3个元素符合要求,元素6在元素5的左边且6 >= 5,那么5对应的集合是6对应的集合的子集,逆序遍历的话就可以减少大量重复运算。
但是问题是怎么记录。
而且前面是7, 后面是5,虽然存在子集关系但也没用,因为万一后面有6呢?

只能换个思路,计算右侧比当前元素小的元素个数,那快速排序里面的划分思想不就可以用了吗?
当前值作为partition的key, 处理区间为当前元素之后的区间,划分之后自然就知道有多少个小于当前值的元素了。但是时间复杂度相比暴力法并没有变化,都是O(n^2)

借助辅助数组可以将右边的元素边处理边添加(添加的同时保持有序):

当前元素i, 先去辅助数组里查询i的右边比i小的元素个数(如果辅助数组有序这将很简单),然后将i插入辅助数组,并保持有序(排序?)。所以重点有两个:

  • 一是怎么进行查询,遍历的话复杂度O(n),因为辅助数组是有序的,如果要降低复杂度可以使用二分查找。
  • 二是怎么保证辅助数组有序,如果用vector作为辅助数组,插入元素时可能涉及到大量元素的搬运,很耗时,所以可以选择使用list容器,方便进行插入(后续发现list是不行的,因为该容器的二分查找很慢,二分查找需要随机访问)。
  • 所以vector的插入操作似乎不可避免。。
class Solution {
public:
    vector countSmaller(vector& nums) {
        int n = nums.size();
        if(n == 0) return vector();
        vector counts(n, 0);
        vector help; //辅助数组
        if(n == 1) return counts;
        for(int i = n-1; i >= 0; --i){
             int low = 0, high = help.size();
             while(low < high){
                 int mid = low + (high-low)/2;
                 if(nums[i] > help[mid])
                    low = mid + 1;
                else
                    high = mid;
            }
            help.insert(help.begin()+high, nums[i]);
            counts[i] = high;
        }
        return counts;
    }
};

性能比较一般,应该是vector插入操作比较耗时。
使用标准库函数lower_bound:

class Solution {
public:
    vector countSmaller(vector& nums) {
        int n = nums.size();
        if(n == 0) return vector();
        vector counts(n, 0);
        vector help; //辅助数组
        for(int i = n-1; i >= 0; --i){
            auto it = lower_bound(help.begin(), help.end(), nums[i]);
            int index = it - help.begin();
            help.insert(it, nums[i]);
            counts[i] = index;
        }
        return counts;
    }
};

归并排序

和这个类似
剑指 Offer 51. 数组中的逆序对_qq_32523711的博客-CSDN博客

左右两个都是有序的集合合并,所以可以在合并的过程中计算左边的每个元素在它的右边有多少小于它的元素,归并排序的过程和计算某个元素右边有多少个小于它的元素这个过程完美契合
参考题解思路:

class Solution {
public:
    vector> copy_nums;
    vector> tmp;//只申请一次tmp数组
    vector res;
    void merge_count(vector> &nums, int beg, int mid, int end){
        int i = beg, j = mid+1;
        int k = 0;
        while(i <= mid && j <= end){
            if(copy_nums[i].first <= copy_nums[j].first){
                tmp[k++] = copy_nums[i];
                res[copy_nums[i++].second] += j-mid-1;已经排在copy_nums[i]前面的个数
            }  
            else    tmp[k++] = copy_nums[j++];
        }
        while(i <= mid){
            tmp[k++] = copy_nums[i];
            res[copy_nums[i++].second] += end - mid;//说明右边所有的元素均比现在剩下的左边元素要小
        } 
        while(j <= end) tmp[k++] = copy_nums[j++];
        copy(tmp.begin(), tmp.begin() + k, copy_nums.begin() + beg);
    }
    void merge_sort(vector> ©_nums, int beg, int end){
        if(beg >= end) return;
        int mid = beg + (end - beg)/2;
        merge_sort(copy_nums, beg, mid);
        merge_sort(copy_nums, mid+1, end);
        if(copy_nums[mid].first <= copy_nums[mid+1].first)    return; //加一个判断,此时数组已经是有序的,不用进行后续操作了
        merge_count(copy_nums, beg, mid, end);
    }

    vector countSmaller(vector& nums) {
        int n = nums.size();
        if(n < 2) return vector(n, 0);
        //使用pair的原因是需要随时记录该值在nums中的索引,因为归并排序后值的位置就改变了
        //但是返回的res需要原始的索引信息
        for(int i = 0; i < n; ++i)  copy_nums.push_back({nums[i], i});
        tmp = vector>(n);
        res = vector(n);
        merge_sort(copy_nums, 0, n-1);
        return res;
    }
};

你可能感兴趣的:(leetcode,leetcode)