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;
}
这里需要注意,我们在插入的时候,如果使用
O(N)
O(1)
,但是[]重载
插入位置确是O(N)
的时间复杂度所以我们使用deque双端队列,使得插入
和[]重载
的时间复杂度近似于O(1)
二分法的缺点在于,他插入的时候不能完美的做到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)
i = mid + 1
。那么右边区间就直接插入对应位置,不需要处理,因为右边区间剩余的元素都是比左边区间大的。i = ri + 1
。那么左边区间就需要插入对于位置,这个时候需要进行处理,因为左边区间的此时的元素都是比右边区间所有元素大的,所以需要加上右边区间的元素个数ri - mid
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];
}