1. 问题描述:
给你一个待查数组 queries ,数组中的元素为 1 到 m 之间的正整数。 请你根据以下规则处理所有待查项 queries[i](从 i=0 到 i=queries.length-1):
一开始,排列 P=[1,2,3,...,m]。
对于当前的 i ,请你找出待查项 queries[i] 在排列 P 中的位置(下标从 0 开始),然后将其从原位置移动到排列 P 的起始位置(即下标为 0 处)。注意, queries[i] 在 P 中的位置就是 queries[i] 的查询结果。
请你以数组形式返回待查数组 queries 的查询结果。
示例 1:
输入:queries = [3,1,2,1], m = 5
输出:[2,1,2,1]
解释:待查数组 queries 处理如下:
对于 i=0: queries[i]=3, P=[1,2,3,4,5], 3 在 P 中的位置是 2,接着我们把 3 移动到 P 的起始位置,得到 P=[3,1,2,4,5] 。
对于 i=1: queries[i]=1, P=[3,1,2,4,5], 1 在 P 中的位置是 1,接着我们把 1 移动到 P 的起始位置,得到 P=[1,3,2,4,5] 。
对于 i=2: queries[i]=2, P=[1,3,2,4,5], 2 在 P 中的位置是 2,接着我们把 2 移动到 P 的起始位置,得到 P=[2,1,3,4,5] 。
对于 i=3: queries[i]=1, P=[2,1,3,4,5], 1 在 P 中的位置是 1,接着我们把 1 移动到 P 的起始位置,得到 P=[1,2,3,4,5] 。
因此,返回的结果数组为 [2,1,2,1] 。
示例 2:
输入:queries = [4,1,2,2], m = 4
输出:[3,1,2,0]
示例 3:
输入:queries = [7,5,5,8,3], m = 8
输出:[6,5,0,7,5]
提示:
1 <= m <= 10^3
1 <= queries.length <= m
1 <= queries[i] <= m
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/queries-on-a-permutation-with-key
2. 思路分析:
① 一开始的时候看了一下评论区的解题思路,其中我感觉比较好的解决办法是使用树状数组来解决,因为使用树状数组的话可以使得计算与移动数组的效率比较高,这个思路主要是参照了领扣中的一位大佬题解,https://leetcode-cn.com/problems/queries-on-a-permutation-with-key/solution/java-shu-zhuang-shu-zu-1ms-by-cjjohn/,里面有大佬对于题目的看法与相关代码,就我个人的感觉来说,这个代码真的写得非常好,真的非常值得学习其中的思路,一开始的时候我也不怎么理解这个树状数组所以查了一下资料,我感觉比较好理解树状数组的的博客:
(1) https://blog.csdn.net/bestsort/article/details/80796531
(2) https://www.cnblogs.com/circlegg/p/7189676.html
(3) https://blog.csdn.net/weixin_30951231/article/details/96959684
不懂树状数组的需要了解一下其中的概念以及大概的实现,其实认真看还是可以理解树状数组大部分的概念的,我感觉树状数组的话比较关键的有三个函数:
(1) lowbit函数:计算最低位是1对应的数字是几,比如1100,最低位1对应的数字就是100,也就是4,11最低位对应的数字是1
(2) getsum函数:获取当前位置的总和(相当于是求和函数)
(3) update函数:从某个位置开始更新为某个值(这个函数与上面的两个函数都是密切相关的)
下面是我对于题解中的自己的一些理解:
② 由题目可以知道,我们是找到P数组中的元素的对应元素的位置在哪里,然后移动这个元素到数组一开始的位置,假如模拟整个过程的话可能效率太低了,而树状数组从某个位置开始更新元素与统计某个区间的总和是非常高效的,利用这个特点可以使用树状数组来解决
③ 可以声明一个c数组(这个是树状数组本身就有的)不过这里需要声明一个长度为queries数组的长度与P数组的长度 + 1的c数组,为什么要这么长呢?c数组可以看成是前半部分由queries的长度与后面P数组的长度组成,c数组的后面主要是为了放置P数组的值,我们不是真的放置,而是将这些具有元素的位置都标为1,在使用lowbit函数的时候可以在更新的时候可以知道到达当前位置那么有多少个元素了,因为题目正是要求的是要查找元素的下标,所以我们使用lowbit函数与update函数进行更新的时候可标为1的过程在使用getsum函数的时候就可以统计出当前索引之前有多少个元素,也就是存在多少个1,所以更新为1这个想法很巧妙,那么就可以计算出要找的元素的下表了
④ 并且声明这样的长度的c数组关键是可以进行移动,因为要查找的次数为queries数组的长度,所以我们移动数组的次数也是这么多次,一开始的时候c数组的后半部分可以看成是P数组,需要使用一个额外的数组来将当前需要查找的值转化为c数组中对应的下表,所以需要声明一个额外的数组valuetoindex,这个数组其实与P数组进行对应,当前需要查找的值可以通过这个数组的下表来进行反应,而这个数组下表对应的元素就是c数组的索引,这一点也很巧妙,其实也就是将查找元素映射成了c数组中对应的下表在哪里
⑤ 上面的工作做好了其实剩下来的就比较简单了,通过valuetoindex找到c数组中对应的索引,由于要求是移除掉当前位置的元素,所以我们使用update函数更新-1的值的操作即可模拟删除的过程,将这个c数组对应的索引位置以及关联位置使用update函数一并更新模拟删除过程,并且将删除元素放到一开始的位置,这个也是我们声明这么长的c数组的原因,可以模拟移动过程,对应的操作是:c数组中后半部分的P数组的前一个位置
比如一开始的测试用例:3 1 2 1 5,因为需要移动四次,所以一开始的时候删除的元素放置到开头是放到c中P数组的前一个位置,使用update函数更新当前位置以及与之关联的之后位置,之后我们使用update函数统计c数组对应位置对应的1的总和是多少就知道查询元素的索引在哪里了,第二次循环是将元素放到3这个位置依次类推一直到1这个位置,此外还需要更新valuetoindex数组,更新当前删除元素的最新索引在哪里,上面的例子中删除的元素索引在4这个位置
⑥ 一开始的时候感觉理解还是有点困难,反复使用debug理解后面的话就思路比较清楚了,非常感谢这位大佬的题解
3. 代码如下:(按照大佬的代码重写写了一遍)
class Solution {
public int c[];
public int lowbit(int x){
return x & (-x);
}
public int getSum(int pos){
int sum = 0;
for (int i = pos; i > 0; i -= lowbit(i)){
sum += c[i];
}
return sum;
}
/*实际上是上面的逆操作*/
public void update(int pos, int val){
for (int i = pos; i < c.length; i += lowbit(i)){
c[i] += val;
}
}
public int[] processQueries(int[] queries, int m) {
int querylen = queries.length;
int res[] = new int[querylen];
/*因为最多移动query的次数所以只需要创建querylen + m + 1的长度即可的数组,每一次更新的时候都是往*/
c = new int[querylen + m + 1];
/*下面这个数组是用来建立对应的值与索引之前的关系, 因为query[i]表示的是某个值*/
int valuetoindex[] = new int[m + 1];
for (int i = 1; i <= m ; ++i){
valuetoindex[i] = querylen + i;
update(querylen + i, 1);
}
for (int i = 0; i < querylen; ++i){
int index = valuetoindex[queries[i]];
/*前面存在有多少个的数字说明索引就是多少了*/
res[i] = getSum(index) - 1;
/*将当前位置的数字清除掉表示移除掉当前位置的数字*/
update(index, -1);
/*更新删除元素的索引表示的那个钱要查询的元素最新所以在c数组的位置在哪里*/
valuetoindex[queries[i]] = querylen - i;
/*从移动到的最开始的位置开始更新为1*/
update(querylen - i, 1);
}
return res;
}
}