zoukankan      html  css  js  c++  java
  • 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)。


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

    公众号二维码

  • 相关阅读:
    MYSQL学习笔记——数据类型
    MYSQL学习笔记——常用语句
    MYSQL学习笔记——基本语法
    Java虚拟机——类加载机制
    Java虚拟机——Class类文件结构
    Tmux
    nginx 更新提示端口占用的解决办法
    fcitx 无法启动
    E:无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系
    清除浮动
  • 原文地址:https://www.cnblogs.com/MyStringIsNotNull/p/12687832.html
Copyright © 2011-2022 走看看