daily存在重复元素 II,优势洗牌

144 阅读1分钟

这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

题目1 - 存在重复元素

leetcode-cn.com/problems/co…

分析

首先是范围:

  • 1 <= nums.length <= 10^5^
  • -10^9^ <= nums[i] <= 10^9^
  • 0 <= k <= 10^5^

既然nums[i]范围这么大,就不好用桶排序的方式来确定元素分布顺序了。

一个简单的方式是:维护一个长度为k的set,

  • 遇到一个数存一个数,发现原来有了那就返回true;
  • 如果达到k了还没有遇到,那就把第一个加入的元素移除,这个可以通过k以及当前索引确定。

那么先试试这个方案,代码如下:

public static boolean containsNearbyDuplicate(int[] nums, int k) {
    Set<Integer> set = new HashSet<>();
    for (int i = 0; i < nums.length; i++) {
        if(set.size()>k){
            set.remove(nums[i-k-1]);
        }
        if(set.contains(nums[i])){
            return true;
        }
        set.add(nums[i]);
    }
    return false;
}

可以AC,结果如下:

执行用时:20 ms, 在所有 Java 提交中击败了44.02%的用户

内存消耗:53.2 MB, 在所有 Java 提交中击败了5.10%的用户

题目2 - 优势洗牌

题目:leetcode-cn.com/problems/ad…

分析

所求的是:

  • 令A中对应位置的元素比B大的次数最多的排列。

那么做以下分析:

  • 对于B中任意位置而言,取A中按顺序排列下恰好比B该位置的数大的数,留着更大的数来让B后面的数再次匹配;
  • 如果不存在,那么就取最小的数和这个B对应,让后面能尽可能地比B对应位置的数大。

那么就需要做:

  • 对A做排序,方便每一次取恰好大的数和最小的数。

根据这个思路拟定一个基础的版本:

public static int[] advantageCount(int[] nums1, int[] nums2) {
        Set<Integer> select = new HashSet<>();
        Arrays.sort(nums1);
        int[] res = new int[nums1.length];
        int bottom = 0;
        for (int i = 0; i < nums2.length; i++) {
            boolean flag = false;
            for (int j = 0 ;j < nums1.length;j++){
                if(select.contains(j)) continue;
                if(nums2[i]<nums1[j]){
                    res[i] = nums1[j];
                    select.add(j);
                    flag = true;
                    break;
                }
            }
            if(!flag){
                while(select.contains(bottom)) bottom++;
                select.add(bottom);
                res[i] = nums1[bottom];
                bottom++;
            }
        }
        return res;
}

结果直接OT了。

考虑一下,这样有效率上的问题:

  • B中的数并不是有序的,每次找数都需要对A进行一次从头的遍历。从效率上来说,就是O(n^2^)。

那么我们试着将查找的逻辑改为二分查找,这样子时间复杂度就会变成O(nlogn);然而这样子编码会比较麻烦,因为此种方法还得考虑已经选择的值,边界不好确定。

那么我们考虑自己做对应的idMapping,将b同时做排序并让之与原本位置的索引位置做对应:

    /**
     * int[0]:整理好的,int[1]:原本索引位置
     */
    public static int[][] sortWithIdx(int[] num){
        int[][] res = new int[2][num.length];
        res[0] = num;
        for (int i = 0; i < res[1].length; i++) res[1][i] = i;
        sortR(res[0],res[1],0,num.length-1);
        return res;
    }

    public static void sortR(int[] num,int[] idx,int start,int end){
        if(start>=end) return;
        int val = num[start];
        int tmpL = start,tmpR = end;
        while(tmpL<tmpR){
            while(tmpR>=tmpL&&num[tmpR]>=val) tmpR--;
            while(tmpL<tmpR&&num[tmpL]<=val) tmpL++;
            if(tmpL<tmpR){
                swap(num,tmpL,tmpR);
                swap(idx,tmpL,tmpR);
            }
        }
        swap(num,tmpL,start);
        swap(idx,tmpL,start);
        sortR(num,idx,tmpL+1,end);
        sortR(num,idx,start,tmpL-1);
    }

    public static void swap(int[] num,int tmpL,int tmpR){
        int tmp = num[tmpR];
        num[tmpR] = num[tmpL];
        num[tmpL] = tmp;
    }

这里提供的方法是做一个自己的排序;同时考虑到最低值和往前计算填入的值可能会撞的问题,丢到一个set里做记录。

public static int[] advantageCount2(int[] nums1, int[] nums2){
    int n = nums2.length;
    Arrays.sort(nums1);
    int[][] sortRes = sortWithIdx(nums2);
    int j = 0,bottom = 0;
    int[] res = new int[n];
    Set<Integer> bottomSkip = new HashSet<>(n);
    for (int i = 0; i < n; i++) {
        while(j<n && nums1[j]<=sortRes[0][i])j++;
        if(j==n){
            while(bottomSkip.contains(bottom)) bottom++;
            res[sortRes[1][i]] = nums1[bottom];
            bottomSkip.add(bottom);
        }else{
            res[sortRes[1][i]] = nums1[j];
            bottomSkip.add(j);
            j++;

        }
    }
    return res;
}

这样时间复杂度就是2个二分排序加上对于两个数组的单次顺序查询,复杂度为O(nlogn) (排序A)+ O(nlogn)(排序B) + O(n)(读取B) + O(n) (读取A),最终为O(nlogn)。

结果:

执行用时:38 ms, 在所有 Java 提交中击败了99.69%的用户

内存消耗:54.6 MB, 在所有 Java 提交中击败了57.44%的用户