K:leetcode 5381.查询带键的排列 这题简单,但我还能优化。精益求精,才是算法的乐趣所在!

前言:

本题来自leetcode第184场周赛的第二小题。以前参加过周赛,觉得很有趣。苦于最近一段时间比较忙就没坚持参加了(实际上是借口来着....),由于昨晚思考一些事情,导致睡不着,所以起得有点早,就参加了本场周赛,然后就碰到了这道题。
这题本身并不难,但是在比赛结束后,参看了别人的题解。基本都是用暴力模拟的方式来解决的(虽然也能accept),但本人觉得有着改进空间。为此,特地整理了思路,并将思路整理成文,以期能够共同获得进步。
为循序渐进的讲解该题,按照以往的习惯,先从最简单的方式入手,再逐步考虑优化。

题目:

给你一个待查数组 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]

数据结构:

List,Array

结合题意,因为要获取元素的下标值,为此,我们可以生成一个数组P,其包含了元素1~m,每次执行一次查找操作就遍历数组P,并把该元素的下标作为结果记录下来,之后将该元素提取到数组的起始位置。

以题干案例为例:

输入:queries=[3,1,2,1],m=5;

执行步骤如下:

  1. 先生成数组P=[1,2,3,4,5]

  2. 查找queries第1个元素3,遍历数组P后得到结果result=[2],之后修改数组P,将数组P中的元素3提到数组起始位置,之后数组P=[3,1,2,4,5]

  3. 查找queries第2个元素1,遍历数组P后得到结果result=[2,1],之后修改数组P,将数组P中的元素1提到数组起始位置,之后数组P=[1,3,2,4,5]

  4. 查找queries第3个元素2,遍历数组P后得到结果result=[2,1,2],之后修改数组P,将数组P中的元素2提到数组起始位置,之后数组P=[2,1,3,4,5]

  5. 查找queries第4个元素1,遍历数组P后得到结果result=[2,1,2,1],之后修改数组P,将数组P中的元素1提取到数组起始位置,之后数组P=[1,2,3,4,5]

该过程的代码如下:

public int[] processQueries(int[] queries, int m) {
    //存放元素1~m的数组
    int[] P = new int[m];
    for(int i=0;i<m;i++){
        P[i] = i+1;
    }
    //用于存放结果
    int[] result = new int[queries.length];
    //遍历queries的元素
    for(int i=0;i<queries.length;i++){
        //查找元素在P中的下标
        for(int j =0;j<P.length;j++){
            if(P[j]==queries[i]){
                result[i] = j;
                System.arraycopy(P,0,P,1,j);
                P[0] = queries[i];
                break;
            }
        }
    }
    return result;
}

分析:

很容易就可以分析出来,该算法的时间复杂度为O(nm),空间复杂度为O(m)

那么,通过上面的分析过程,我们可以改进优化哪个点呢?很明显的,一个可以优化的地方是数组P。数组元素移动的次数与n成正比,每次将数组P中的第index个元素提取到起始位置,都需要将0~index-1的元素往后移动一位,并将第index元素插入到P[0]中。这种场景,采用链表的方式来解决,会更好,于是可以将代码改进为如下形式。

该过程的代码如下:

public int[] processQueries(int[] queries, int m) {
    int[] result = new int[queries.length];
    List<Integer> P = new LinkedList<Integer>();
    //生成数组P
    for(int i=1;i<=m;i++){
        P.add(i);
    }
    for(int i=0;i<queries.length;i++){
        int index = P.indexOf(queries[i]);
        result[i] = index;
        P.remove(index);
        P.add(0,queries[i]);
    }
    return result;
}

分析:

改进了数据结构之后,算法的时间复杂度依旧为O(nm),空间复杂度为O(m),改进只是改进了时间复杂度的常系数。那么是否还有改进的空间呢?显然有,否则也不会有这文章。

我们换个思路来思考在数组P中查找元素下标的过程。首先,我们可以将数组P划分为两部分,一部分是已经查找过的queries[0]queries[index-1]的元素,我们**称这部分元素为A**,其必定排序在P的前面,且为乱序的。另一部分由剩下的其它元素所组成,我们**称这部分元素为B,其可能乱序也可能有序,但是元素必定是严格按照升序排列的**,也就是未出现在queries[0]queries[index-1]中的元素。

我们还注意到几个情况

  1. 当数组P完全有序时,即P=[1,2,3,4,...,m],queries[index]在数组P中的下标是queries[index] -1。假设queries[index] == 2,则其结果应该返回1,所谓的元素在P中的下标,也就是元素queries[index]在P中前面有几个元素。

  2. 当查找的元素在A中时,由于A为乱序的,为此我们只能遍历A,得到元素queries[index]的下标值进行返回。

  3. 当要查找的元素在B中,且B为完全有序时,即P=[A,B],B=[k,k+1,k+2,.....m],则元素queries[index]在数组P中的下标为queries[index]-1。我们还可以知道,无论A的排列顺序为何种,均不影响结果。假设A=[2,1,4,3] ,B=[5,6,7,8,9],queries[index]==6,则其结果为5。当A=[1,2,4,3]时,该结果返回依旧为5不变。

  4. 当要查找的元素在B中,且B为部分有序(元素按照升序排列,但是会缺少部分值)时,即P=[A,B],B=[k,k+1,k+2,k+4,k+5,...,m],此时我们应该返回的元素queries[index]在数组P中的下标为queries[index]-1+maxThanOnA,其中maxThanOnA为A中大于元素queries[index]值的数目,也就是B中大于queries[index]的值被移动到A中的元素个数。假设queries[index]== k+5,则其应当返回k+4,也就是queries[index]-1的值,因为B中的k+3被移动到A中了,无论其在哪个位置,都不影响k+5前面的元素个数这个结果。为此,下标依旧为queries[index]-1。当queries[index]==k+2时,由于元素k+3被移动到了A中,其使得元素k+2前面的元素个数多了一个,为此,其结果应该返回k+2。

综上分析,我们可以用一个List记录A中的元素的情况。当元素queries[index]在A中时,遍历A获取结果,并将进行查询的那个元素移动到A的起始位置中,否则,统计A中大于queries[index]的元素个数,直接返回queries[index]-1+maxThanOnA,并将queries[index]放置到A的起始位置中。

该算法的代码如下:

public int[] processQueries(int[] queries, int m) {
    //用于存放结果
    int[] result = new int[queries.length];
    //用于放置乱序(A)的那些个元素
    List<Integer> list = new LinkedList<Integer>();
    for(int i=0;i<queries.length;i++){
        //遍历乱序的元素的索引
        int index = 0;
        //记录列表中比当前元素大的元素个数
        int maxThanOnA = 0;
        while(index<list.size()){
            int number = list.get(index);
            if(number == queries[i]){
                result[i] = index;
                list.remove(index);
                list.add(0,number);
                break;
            }else if(number>queries[i]){
                maxThanOnA++;
            }
            index++;
        }
        //在列表中找到了元素
        if(index<list.size()){
            continue;
        }
        result[i] = queries[i]-1+maxThanOnA;
        list.add(0,queries[i]);
    }
    return result;
}

分析:
该算法的时间复杂度为O(n^2),空间复杂度也为O(n)。

总结:
综上所述,当m>>>n时,时间复杂度为O(n)的算法更加有利。最坏情况下,也是m==n,此时,无论采取何种算法,时间复杂度为O(n^2),空间复杂度为O(n)。


这个是本人的公众号,致力于写出绝大部分人都能读懂的技术文章。欢迎相互交流,我们博采众长,共同进步。

公众号二维码

posted @ 2020-04-12 21:34  林学徒  阅读(208)  评论(0编辑  收藏  举报