【问题标题】:A fast way to find unique values in the list在列表中查找唯一值的快速方法
【发布时间】:2012-09-25 14:36:13
【问题描述】:

给定一个 KeyValuePairs 列表,其中每对都有一个 getValue() 方法,获得唯一值的 List(或 Set)最快的方法是什么?

以下所有都产生可接受的结果。 u1 似乎比预期的列表大小(大约 1000-2000 KVP)最快

我们可以做得更好(更快)吗?

private static Set<String> u1(List<_KVPair> pairs) {
    Set<String> undefined = new HashSet<String>();

    for (_KVPair pair : pairs) {
        undefined.add(pair.getValue());
    }

    if (undefined.size() == 1) {
        return new HashSet<String>();
    }
    return undefined;
}

private static List<String> u2(List<_KVPair> pairs) {

    List<String> undefined = new ArrayList<String>();
    for (_KVPair pair : pairs) {
        if (!undefined.contains(pair.getValue())) {
            undefined.add(pair.getValue());
        }
    }

    return undefined;
}

private static List<String> u3(List<_KVPair> pairs) {

    List<String> undefined = new LinkedList<String>();

    Iterator<_KVPair> it = pairs.iterator();
    while (it.hasNext()) {
        String value = it.next().getValue();
        if (!undefined.contains(value)) {
            undefined.add(value);
        }
    }
    return undefined;
}

在大约 3600 对时,“u3”获胜。在大约 1500 对时,'u1' 获胜

【问题讨论】:

  • 当你尝试它时会发生什么?哪个时间复杂度最低?
  • 好像第一个最快的。
  • .. 它的时间复杂度最低,O(N) vs O(N^2)
  • 我会确保每个测试至少运行 2-5 秒,否则您会得到无法重现的结果。
  • @NimChimpsky 集合是只包含唯一值的集合。通常,从大量值集合中找到唯一值的最快方法是将所有值添加到集合中,从而导致所有重复项消失,因为如果输入已经存在于集合中,则集合的 add 方法将简单地忽略输入。

标签: java performance collections


【解决方案1】:

第一个选项应该更快。您可以通过在使用之前调整集合大小来使其更快。通常,如果您预计会有少量重复:

Set<String> undefined = new HashSet<String>(pairs.size(), 1);

请注意,我使用 1 作为负载因子以防止任何调整大小。

出于好奇,我进行了测试(代码如下) - 结果是(编译后):

测试 1(注意:预热需要几分钟)

原始列表的大小 = 3,000,没有重复:
套装:8
数组列表:668
链表:1166

测试 2

原始列表的大小 = 30,000 - 所有字符串都相同:
设置:25
数组列表:11
链接列表:13

这样说的有道理:

  • 当有很多重复项时,List#contains 将运行得相当快,因为​​会更快地找到重复项,并且分配大集合的成本 + 散列算法是不利的
  • 当没有重复或重复很少时,该组以较大优势获胜。
public class TestPerf {

    private static int NUM_RUN;
    private static Random r = new Random(System.currentTimeMillis());
    private static boolean random = false; //toggle to false for no duplicates in original list


    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

        for (int i = 0; i < 30_000; i++) {
            list.add(getRandomString());
        }

        //warm up
        for (int i = 0; i < 10_000; i++) {
            method1(list);
            method2(list);
            method3(list);
        }

        NUM_RUN = 100;
        long sum = 0;
        long start = System.nanoTime();
        for (int i = 0; i < NUM_RUN; i++) {
            sum += method1(list);
        }
        long end = System.nanoTime();
        System.out.println("set: " + (end - start) / 1000000);

        sum = 0;
        start = System.nanoTime();
        for (int i = 0; i < NUM_RUN; i++) {
            sum += method2(list);
        }
        end = System.nanoTime();
        System.out.println("arraylist: " + (end - start) / 1000000);

        sum = 0;
        start = System.nanoTime();
        for (int i = 0; i < NUM_RUN; i++) {
            sum += method3(list);
        }
        end = System.nanoTime();
        System.out.println("linkelist: " + (end - start) / 1000000);

        System.out.println(sum);
    }

    private static int method1(final List<String> list) {
        Set<String> set = new HashSet<>(list.size(), 1);
        for (String s : list) {
            set.add(s);
        }
        return set.size();
    }

    private static int method2(final List<String> list) {
        List<String> undefined = new ArrayList<>();
        for (String s : list) {
            if (!undefined.contains(s)) {
                undefined.add(s);
            }
        }
        return undefined.size();
    }

    private static int method3(final List<String> list) {
        List<String> undefined = new LinkedList<>();

        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String value = it.next();
            if (!undefined.contains(value)) {
                undefined.add(value);
            }
        }
        return undefined.size();
    }

    private static String getRandomString() {
        if (!random) {
            return "skdjhflkjrglajhsdkhkjqwhkdjahkshd";
        }
        int size = r.nextInt(100);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < size; i++) {
            char c = (char) ('a' + r.nextInt(27));
            sb.append(c);
        }
        System.out.println(sb);
        return sb.toString();
    }
}

【讨论】:

  • 实际上这在我的测试中会减慢大约 30% 的速度(大约 3000 kvp)
  • @Jam 你如何运行你的测试?你跑了多少次?有多少重复?同样对于 3000 个值,该方法可能会在几毫秒内运行,因此您的时钟精度可能会影响某些结果。
  • u1 只是创建一个集合,它不会删除在原始列表中重复的元素。只需删除重复项。
  • @Jam 听起来您的代码基准测试可能很差。您需要运行这些方法来测试数十次或数百次,以避免测量任何“预热”成本。见stackoverflow.com/questions/504103/…
  • @Jam 还要确保 KVPair 实现了 equals() 和 hashCode()
【解决方案2】:

您可以通过将第一行更改为:加快u1

Set<String> undefined = new HashSet<String>(pairs.size());

否则,当您添加值时,该集合将在内部进行大量调整。

【讨论】:

  • 这将在集满 75% 后调整大小。
  • 好点,虽然这与我的相关程度取决于列表中有多少唯一值...
  • 嗯...这总是一个折衷方案...如果您有一百万个键值对只有少数几个唯一值,那么这个解决方案将浪费大量空间。由于偶尔需要调整缓冲区大小只会向 add 方法添加一个摊销的常量运行时,所以缓冲区调整大小应该不是太大的问题。但是,如果操作对性能非常关键,我同意猜测所需容量可能是一个好主意(只要猜测基于对预期输入的合理假设)。
  • 也许将大小加倍可以完全避免重新散列,但肯定会浪费内存。
  • @Alderath 为什么这看起来是个坏主意?如果您知道该集合只需要包含 N 个项目,我认为您不需要真正花费空间来确保它可以有效地确定第 N+1 个项目
【解决方案3】:

更新:见下方编辑

当你可以做的时候迭代列表是没有意义的

return new HashSet<_KVPair>(pairs)

最糟糕的选择是 u2 和 u3,您将第一个列表中的项目添加到第二个列表中,并在循环的每次迭代中调用 List.contains(item)。此操作接近O(n^2) - List.contains(item) 需要将项目与可能的整个列表进行比较。避免需要迭代列表的算法调用一些也迭代列表的操作。

如果您想要独特的物品,请使用Set。如果您需要按排序顺序排列这些项目,请使用 TreeSet,否则 99% 的时间您需要 HashSet

编辑:我错过了你想得到一组pair.getValue();但无论如何建议都是一样的 - 使用 Set,不要在循环中使用 List.contains()

【讨论】:

  • 没有找到 unqiue 值,只是从列表中创建一个哈希集
  • 你知道pairs列表不包含String对象吧?
  • 我只对配对的不同值感兴趣,而不是配对本身。键也可以相同或不同,都没有关系。
  • @Jam 添加了编辑,但建议保持不变 - 使用 Set。在循环中将一堆项目添加到 Set 与使用 new HashSet(collection) 构造集合具有相同的复杂性。
  • 通常,键值对的散列就是键的散列,因此建议的解决方案将生成具有唯一键但不一定是唯一值的集合。
【解决方案4】:

我敢说选项 1 是最快和最干净的。在检查值是否已经包含在那里方面,很难击败哈希集。

基于列表的解决方案不像之前的回答所说的那样扩展

【讨论】:

    【解决方案5】:

    另一种方法可能是Sort list,然后在一个循环中,如果引用相等,您可以通过保留添加的最后一个元素的引用来消除重复项,否则不要添加到新列表中添加

    Collections.sort(pairs)//O(n log n)
    
    Loop
    if(!lastAdded.equals(pairs.get(i)))
     {
       //Add to list 
       //change lastAdded
     }
    

    【讨论】:

    • 试过了。根据大小,这会大大减慢速度。对大列表进行排序需要时间
    • 然后您将必须实现自定义列表,该列表还包含哈希映射和实现方法,该方法将通过仅从每个存储桶返回元素来避免重复。
    【解决方案6】:

    给出的答案都没有从最终结果中删除重复项,它们只是删除了重复项。因此,如果一个字符串出现两次,它仍然会出现在最终结果中,但只会出现一次。如果那不是必需的,那么是的,我只是浪费了五分钟......

     public Map<String, String> countOccurences(List<String> source){
           Map<String, Integer> result =   new HashMap<>(source.size());
            int temp =0;
            for (String value : source) {
                if(result.containsKey(value)){
                    temp = result.get(value);
                    temp++;
                    result.put(value, temp);
                    temp = 0;
                }
                else {
                    result.put(value, 1);
                }
            }
        }
        public List<String> sublistSingles(Map<String, Integer> results){
            List<String> duplicatesRemoved = new ArrayList<>(results.size());
            for(Map.Entry<String, Integer> result:results.entrySet()){
                if(result.getValue().equals(1)){
                  duplicatesRemoved.add(result.getKey());
                }
            }
            return duplicatesRemoved;
        }
    

    【讨论】:

    • 我不认为原始代码是这样做的。您返回一个在原始列表中只出现一次的所有值的列表,而 OP 的代码返回一个唯一值的列表,包括那些出现多次的值(换句话说,它会删除重复项)。
    • @assylias 正是我的观点,代码不会“在列表中找到唯一值”。它只是制作了一组独特的元素。
    • 我同意,但看起来 op 想要的是一组独特的元素。诚然,这还不清楚。
    • 我想这是一个糟糕和模棱两可的要求/规范的问题。或者,您可以将问题解释为“获取原始列表中仅在原始列表中出现一次的所有值的列表”,或者您可以将其解释为“获取原始列表中出现的所有值的列表,并确保每个值在此输出列表中仅出现一次”。尚不清楚唯一性是仅指输出还是指输入。
    • @NimChimpsky 但这不是问题的表述方式。问题是“......获得唯一值列表(或集合)的最快方法是什么?”。严格来说,我什至认为return Arrays.asList(1, 2); 可能是该问题的答案,因为那是唯一值的列表。最初的问题甚至没有提到获得的唯一值需要是原始键值对列表的一部分(这个约束只是隐含的)。因此,就像我说的,规格不明确......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-14
    • 1970-01-01
    • 1970-01-01
    • 2010-09-07
    • 1970-01-01
    • 2020-11-23
    相关资源
    最近更新 更多