[leetcode 315] 计算右侧小于当前元素的个数

题目链接与描述

https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/
在这里插入图片描述

暴力解法

如果按照暴力方法的思维,我们可以从后向前遍历,每一次找到当前位置右侧比他小的数有多少个就可以了,那么这样写的时间复杂度为O(N ^ 2),应该是过不了的

    vector<int> countSmaller(vector<int>& nums)
    {
        int len = nums.size();
        vector<int> ans(len,0);
        for(int i = len-1; i >= 0; i--)
            for(int j = i + 1; j < len; j++)
                ans[i] += (nums[j] < nums[i]) ? 1 : 0;
        return ans;
    }

显然,这样做是会超时的
在这里插入图片描述

二分法

暴力解法的缺点在于,我们每次查找的时候,可以说是把后面的数据都遍历了一次。每次遍历的时间复杂度都是O(N),我们是否可以将这个时间复杂度降为O(log N)

因为原数组的数据我们不保证一定是有序的,所以我们不能使用二分查找的方法。但是如果我们在从后向前查找的时候,再建立一个数组,在这个新的数组中,他的元素都是有序的(升序)排列。

这样我们每次只需要在这个数组中找到第一个比他大的数据的位置,那么这个位置的就是结果了,最后再在这个位置插入当前数据就可以了。

在这里插入图片描述

    vector<int> countSmaller(vector<int>& nums)
    {
        int len = nums.size();
        vector<int> ans(len,0);
        deque<int> tmp;//使用双端队列
        for(int i = nums.size()-1; i >= 0; i--)
        {
            ans[i] = BinaryFind(tmp,nums[i]);
            tmp.insert(tmp.begin() + ans[i],nums[i]);
        }
        return ans;
    }

    int BinaryFind(deque<int>& arr,int num)
    {
        if(arr.size() == 0)
            return 0;
        int le = 0;
        int ri = arr.size();
        while(le < ri)
        {
            int mid = (le + ri) / 2;
            if(arr[mid] < num) le = mid + 1;
            else if(arr[mid] >= num) ri = mid;
        }
        return le;
    }

这里需要注意,我们在插入的时候,如果使用

  • vector数组,那么我们插入的时间复杂度为O(N)
  • list链表,那么我们插入的时间复杂度为O(1),但是[]重载插入位置确是O(N)的时间复杂度

所以我们使用deque双端队列,使得插入[]重载的时间复杂度近似于O(1)

二分法的优化,二叉搜索树型dp

二分法的缺点在于,他插入的时候不能完美的做到O(1),为了克服这一问题,我们使用二叉搜素树来解决。

对于二叉搜索树的每一个结点,除了左右子树节点与值之外,还多了一个计数器,用来记录左子树节点的数量,也就是在他的子树中,比自己小的节点的数量。

还是从后向前遍历,当遇到一个节点

  • 如果是nullptr节点,那么就给nullptr节点赋值
  • 如果比当前节点数值。记录比他小的节点数量(计数器 + 1),再次遍历他的右子树
  • 如果比当前节点数值小或者等于。意味着需要在他的左子树进行插入,所以说给当前位置的计数器+1,在遍历他的左子树

在这里插入图片描述

    struct TreeNode
    {
        TreeNode* _left;
        TreeNode* _right;
        int _val;
        int _count;//左子树节点个数
        TreeNode(int _v = 0)
            :_val(_v),_left(nullptr),_right(nullptr),_count(0)
        {}
    };
    //树状 二分
    vector<int> countSmaller(vector<int>& nums)
    {
        TreeNode* root = nullptr;
        int len = nums.size();
        vector<int> ans(len,0);
        for(int i = len-1; i >= 0; i--)
            dfs(root,nums[i],ans,i);
        return ans;
    }

    void dfs(TreeNode*& root,int val,vector<int>& ans,int p)
    {
        if(root == nullptr)
        {
            root = new TreeNode(val);
            return;
        }
		//比当前节点数值`小或者等于`
        if(val <= root->_val)
        {
            root->_count++;
            dfs(root->_right,val,ans,p);
        }
        else//比当前节点数值`大`
        {
            ans[p] += root->_count + 1;//+1表示当前节点本身
            dfs(root->_left,val,ans,p);
        }
    }

归并排序

我们可以把最后的问题进行分解,采用归并排序(升序排列)的思想。其中为了找到每一个数据对于的下标位置,所有我们数组的元素类型为一个pair类型的结构体,第一个元素是数据,第二个元素是数据对应的下标

而对于归并排序而言,我们需要做手脚的地方有以下四点:(已知,le,mid,ri)

  1. 当左边区间遍历完毕,i = mid + 1。那么右边区间就直接插入对应位置,不需要处理,因为右边区间剩余的元素都是比左边区间大的。
  2. 当右边区间遍历完毕,i = ri + 1。那么左边区间就需要插入对于位置,这个时候需要进行处理,因为左边区间的此时的元素都是比右边区间所有元素大的,所以需要加上右边区间的元素个数ri - mid
  3. 比较小的元素在右边区间的位置。也是不需要进行处理的,因为他右边比他小的数在上一层的归并中已经处理了。
  4. 比较小的元素在左边区间的位置。在插入的同时,记录右边区间已经使用的元素个数,pr - mid - 1
	//归并
    vector<int> countSmaller1(vector<int>& nums) {
        vector<pair<int,int> > copy;
        for(int i = 0; i < nums.size(); i++)
            copy.push_back({nums[i],i});
        vector<pair<int,int> > tmp = copy;
        vector<int> ans(nums.size(),0);

        MergerSort(copy,tmp,0,nums.size()-1,ans);
        return ans;
    }

    void MergerSort(vector<pair<int,int> >& nums,vector<pair<int,int> >& tmp,int le,int ri,vector<int>& ans)
    {
        if(le >= ri)    return;
        int mid = (le + ri) / 2;
        MergerSort(nums,tmp,le,mid,ans);
        MergerSort(nums,tmp,mid+1,ri,ans);
        //防止本身就是有序的数组
        if(nums[mid].first <= nums[mid+1].first)
            return;

        int pl = le;
        int pr = mid+1;
        for(int i = le; i <= ri; i++)
        {
            if(pl > mid)//左区间元素排列完毕
            {
                tmp[i] = nums[pr++];
            }
            else if(pr > ri)//右区间元素排列完毕
            {
                tmp[i] = nums[pl++];
                ans[tmp[i].second] += ri - mid;//此时右区间所有元素都比这个数小
            }
            else if(nums[pl].first <= nums[pr].first)//比较小的元素在左区间
            {
                tmp[i] = nums[pl++];
                ans[tmp[i].second] += pr - mid - 1;
            }
            else if(nums[pl].first > nums[pr].first)//比较小的元素在右区间
            {
                tmp[i] = nums[pr++];
            }
        }

        for(int i = le; i <= ri; i++)
            nums[i] = tmp[i];
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值