从HashSet中提取任意元素的性能(运行时)

时间:2012-03-15 23:43:35

标签: java hashset

我在实现中使用HashSets来快速添加,删除和元素测试(摊销的常量时间)。 但是,我还想要一种从该集合中获取 arbitraty 元素的方法。我所知道的唯一方法是

Object arbitraryElement = set.iterator.next();

我的问题是 - 这有多快(渐近地说)?这是否在(不是摊销的)常量时间内以集合的大小工作,或者iterator().next()方法是否执行一些较慢的操作?我问,因为我似乎在实验中失去了一个对数因子,这是受影响的几条线之一。

非常感谢!

7 个答案:

答案 0 :(得分:4)

HashSet.iterator().next()线性扫描表格以查找下一个包含的项目。

对于.75的默认加载因子,每个空的加载因子将有三个完整的插槽。

当然,不能保证后备阵列中对象的分布是什么&该集合永远不会满满,因此扫描需要更长的时间。

我认为你会得到不变的摊销时间。

编辑:迭代器不会创建集合中任何内容的深层副本。它仅引用HashSet中的数组。你的例子创建了一些对象,但仅此而已。没有大的副本。

答案 1 :(得分:2)

平均而言,我不希望这是一个对数因子,但在极少数情况下可能会很慢。如果您关心这一点,请使用LinkedHashSet,这将保证不变的时间。

答案 2 :(得分:1)

我会维护一个ArrayList个密钥,当你需要一个随机对象时,只需生成一个索引,抓住密钥,然后将其拉出集合。 O(1)宝贝......

答案 3 :(得分:1)

使用迭代器从HashSet中获取第一个元素非常快:在大多数情况下,我认为它是摊销的O(1)。这假设HashSet因其给定的容量而相当充分地填充 - 如果容量与元素数量相比非常大,那么它将更像O(容量/ n),这是迭代器需要扫描的平均桶数在找到价值之前。

使用迭代器扫描整个HashSet只是O(n +容量),如果适当缩放容量,则有效O(n)。所以它仍然不是特别昂贵(除非你的HashSet非常大)

如果你想要更好,你需要一个不同的数据结构。

如果你确实需要通过索引快速访问任意元素,那么我个人只需将对象放在一个ArrayList中,它将通过索引为您提供非常快速的O(1)访问。如果要选择具有相同概率的任意元素,则可以将索引生成为随机数。

或者,如果您想获得一个任意元素但不关心索引访问,那么LinkedHashSet可能是一个不错的选择。

答案 4 :(得分:0)

这是来自HashSet的JDK 7 JavaDoc:

迭代此集合需要的时间与HashSet实例的大小(元素数量)加上后备HashMap实例的“容量”(桶数)之和成比例。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载因子太低)非常重要。

我查看了HashSet和LinkedHashSet的JDK 7实现。对于前者,下一个操作是在一个存储桶内的链表遍历,在存储桶之间是一个数组遍历,其中数组的大小由capacity()给出。后者严格来说是一个链表遍历。

答案 5 :(得分:0)

如果您在概率意义上需要任意元素,则可以使用以下方法。

class MySet<A> {
     ArrayList<A> contents = new ArrayList();
     HashMap<A,Integer> indices = new HashMap<A,Integer>();
     Random R = new Random();

     //selects random element in constant O(1) time
     A randomKey() {
         return contents.get(R.nextInt(contents.size()));
     }

     //adds new element in constant O(1) time
     void add(A a) {
         indices.put(a,contents.size());
         contents.add(a);
     }

     //removes element in constant O(1) time
     void remove(A a) {
         int index = indices.get(a);
         contents.set(index,contents.get(contents.size()-1));
         contents.remove(contents.size()-1);
         indices.set(contents.get(contents.size()-1),index);
         indices.remove(a);
     }

     //all other operations (contains(), ...) are those from indices.keySet()
}

答案 6 :(得分:0)

如果您反复使用迭代器选择任意集合元素并经常删除该元素,则可能导致内部表示不平衡并且找到第一个元素会降低线性时间复杂度的情况。

在实现涉及图遍历的算法时,这实际上是很常见的情况。

使用LinkedHashSet可以避免此问题。

演示:

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Random;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class SetPeek {

    private static final Random rng = new Random();

    private static <T> T peek(final Iterable<T> i) {
        return i.iterator().next();
    }

    private static long testPeek(Set<Integer> items) {
        final long t0 = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            peek(items);
        }
        final long t1 = System.currentTimeMillis();
        return t1 - t0;
    }

    private static <S extends Set<Integer>> S createSet(Supplier<S> factory) {
        final S set = new Random().ints(100000).boxed()
            .collect(Collectors.toCollection(factory));

        // Remove first half of elements according to internal iteration
        // order.  With the default load factor of 0.75 this will not trigger
        // a rebalancing.
        final Iterator<Integer> it = set.iterator();
        for (int k = 0; k < 50000; k++) {
            it.next();
            it.remove();
        }

        return set;
    }

    public static void main(String[] args) {
        final long hs = testPeek(createSet(HashSet::new));
        System.err.println("HashSet: " + hs + " ms");
        final long lhs = testPeek(createSet(LinkedHashSet::new));
        System.err.println("LinkedHashSet: " + lhs + " ms");
    }
}

结果:

HashSet: 6893 ms
LinkedHashSet: 8 ms