【问题标题】:Finding duplicate numbers in an array of numbers在数字数组中查找重复数字
【发布时间】:2019-10-11 20:06:46
【问题描述】:

我在一次采访中被问到这个问题,给定一个数字列表,只返回输入中存在的重复项作为排序输出。

示例:

Input = [6, 7, 5, 6, 1, 0, 1, 0, 5, 3, 2]
Output = [0, 1, 5, 6] - sorted unique numbers which are duplicates in input

我想出了以下解决方案:

方法1:

public static List<Integer> process(List<Integer> input) {
    List<Integer> result = new ArrayList<>();
    Map<Integer, Integer> map = new HashMap<>();
    for (int val : input) {
        map.put(val, map.getOrDefault(val, 0) + 1);
    }

    map.forEach((key, val) -> {
        if (val > 1) {
            result.add(key);
        }
    });
    result.sort(null);
    return result;
}

更新方法2:

public static List<Integer> process1(List<Integer> input) {
    Set<Integer> dups = new HashSet<>();
    Set<Integer> set = new HashSet<>();
    for (int val : input) {
        if (set.contains(val)) {
            dups.add(val);
        } else {
            set.add(val);
        }
    }
    List<Integer> result = new ArrayList<>(dups);
    result.sort(null);
    return result;
}

旧方法2

public static List<Integer> process1(List<Integer> input) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> set = new HashSet<>();
    for (int val : input) {
        if (set.contains(val)) {
            result.add(val);
        } else {
            set.add(val);
        }
    }
    result.sort(null);
    return result;
}

方法1的时间复杂度是(n)Log(n),因为java中的排序是nlogn,空间复杂度是n

方法 2 的时间复杂度再次为 (n)Log(n),因为在 java 中的排序是 nlogn,空间复杂度与方法 1 相比略低,因为我在我的集​​合中只保存一次元素。

如果我在找出时间和空间复杂性方面有误,请纠正我。

现在的问题是,如果输入包含数百万个数字,这个逻辑是否有效?如果输入是百万个数字,HashMap 是否有效?

根据我的一般理解,map 或 set 的时间复杂度较低,HashSet 内部实现也使用 HashMap。如何回答这个问题。

【问题讨论】:

  • 我运行了你的两种方法,看起来 Approach1 有效,但 Approach2 返回了不正确的结果(它在列表中有多个相同的数字)。请注意,我在大小为 10 的简单列表上运行了这些。
  • @Nexevis,你能分享一下输入数据吗
  • 当您有至少 3 个相同的输入时,它会中断,例如 {1,1,1} 将返回 1, 1 的列表
  • @Nexevis,谢谢,我已经更新了代码

标签: java


【解决方案1】:

如果一个数字出现 3 次或更多次,Approach2 将失败,因为它将多次将该数字添加到输出中。你说得对,空间复杂度较低,但你的推理有点奇怪——这是因为 HashSet 将在其底层 HashMap 内部使用相同的虚拟对象来指示存在一个值,而对于 Approach1,你正在分配一个 Integer 每个时间。

HashMap 内部包含 buckets 的列表,因此通常,如果您能够分配包含一百万个数字的列表,您也应该能够分配 HashMap 持有(最多)尽可能多的数字。

在构造 HashMap 时将其初始容量设置为列表的大小是一个好主意。这将使您的代码更快地处理大型列表,因为它避免了重新散列。

请注意,可能有一种更快的方法:对初始列表进行排序。在排序列表中,查找重复项是微不足道的,因为它们是相邻的,因此您不需要 HashMap。但是,如果不允许修改它,则需要为此复制初始列表,因此空间要求将相同。理论复杂度保持不变(排序是 O(nlogn),查找重复项是 O(n)),由于我们对大列表进行排序,实际排序时间会更多,但您将避免 HashMap 中的所有分配。这可能会也可能不会弥补对大列表进行排序所花费的额外时间。

【讨论】:

    【解决方案2】:

    我很好奇这个算法的不同实现在 JMH 性能测试下会如何表现,我想出的最快实现是:

    Set<Integer> all = new HashSet<>(input.size());
    Set<Integer> output = new TreeSet<>();
    
    for(Integer val : input) {
       if (!all.add(val)) {
          output.add(val);
       }
    }
    
    return new ArrayList<>(output);
    

    以下是上述实施 (algo2) 和您的方法 1 实施 (algo1) 的 JMH 结果:

    Benchmark                   (N)  Mode  Cnt    Score    Error  Units
    PerformanceTests.algo1  1000000  avgt    3  323.265 ± 33.919  ms/op
    PerformanceTests.algo2  1000000  avgt    3  285.505 ± 29.744  ms/op
    

    更新,@josejuan 你是对的,下面的算法比以前的算法快 6 倍:

    int[] input = new int[INPUT.size()];
    for (int i = 0; i < input.length; i++) {
        input[i] = INPUT.get(i);
    }
    Arrays.sort(input);
    
    List<Integer> output = new ArrayList<>(input.length);
    int prev = input[0];
    boolean added = false;
    for (int i = 1; i < input.length; i++) {
        if (prev == input[i]) {
            if (!added) {
                output.add(prev);
                added = true;
            }
        } else {
            added = false;
            prev = input[i];
        }
    }
    return output;
    

    【讨论】:

    • all.add(val) 添加到 all
    • 你是对的(对不起),使用两个哈希集(并在列表转换后排序)效果更好
    • @josejuan 不,我已经在 jmh 中检查过了
    • 然后更改随机范围
    • (更快的方式是转成int[],排序遍历)
    猜你喜欢
    • 2018-06-01
    • 2016-01-17
    • 2022-01-03
    • 1970-01-01
    • 1970-01-01
    • 2021-12-03
    • 2015-06-14
    • 2014-03-05
    • 2018-01-21
    相关资源
    最近更新 更多