需要两个唯一元素和索引访问时设置vs列表

时间:2016-08-05 11:27:01

标签: java performance list set

我需要保留一个独特的元素列表,我还需要随时从中随机选择一个元素。我有两种简单的方法可以做到这一点。

  1. 保持在集合中看到的元素 - 这给了我元素的唯一性。如果需要随机选择一个,请执行以下操作:

    elementsSeen.toArray()[random.nextInt(elementsSeen.size())]
    
  2. 保持List中的元素 - 这种方式不需要转换为数组,因为当我需要随机的时候有get()函数。但是在这里我需要在添加时执行此操作。

    if (elementsSeen.indexOf(element)==-1) {elementsSeen.add(element);}
    
  3. 所以我的问题是哪种方式会更有效率?转换为数组更多消耗还是indexOf更糟?如果尝试添加元素的次数经常增加10或100或1000倍会怎么样?

    我感兴趣的是如何以最高效的方式将列表的功能(按索引访问)与集合的功能(唯一添加)相结合。

6 个答案:

答案 0 :(得分:25)

如果使用更多内存不是问题,那么你可以通过使用list和set in wrapper来充分利用它们:

public class MyContainer<T> {
    private final Set<T> set = new HashSet<>();
    private final List<T> list = new ArrayList<>();

    public void add(T e) {
        if (set.add(e)) {
            list.add(e);
        }
    }

    public T getRandomElement() {
        return list.get(ThreadLocalRandom.current().nextInt(list.size()));
    }
    // other methods as needed ...
}

答案 1 :(得分:11)

HashSet和TreeSet都扩展了AbstractCollection,其中包含toArray()实现,如下所示:

public Object[] toArray() {
    // Estimate size of array; be prepared to see more or fewer elements
    Object[] r = new Object[size()];
    Iterator<E> it = iterator();
    for (int i = 0; i < r.length; i++) {
        if (! it.hasNext()) // fewer elements than expected
            return Arrays.copyOf(r, i);
        r[i] = it.next();
    }
    return it.hasNext() ? finishToArray(r, it) : r;
}

如您所见,它负责为数组分配空间,以及创建用于复制的Iterator对象。因此,对于Set,添加为O(1),但由于元素复制操作,检索随机元素将为O(N)。

另一方面,List允许您快速访问支持数组中的特定索引,但不保证唯一性。您必须重新实现addremove和相关方法,以保证插入时的唯一性。添加唯一元素将是O(N),但检索将是O(1)。

所以,这实际上取决于哪个区域是您潜在的高使用点。是否会大量使用添加/删除方法,并且谨慎使用随机访问?或者这将是一个最重要的检索容器,因为在程序的生命周期内将添加或删除很少的元素?

如果是前者,我建议将SettoArray()一起使用。如果是后者,那么实现一个唯一的List以利用快速检索可能是有益的。明显的缺点是add包含许多边缘情况,标准Java库需要非常谨慎地以高效的方式工作。您的实施是否符合相同的标准?

答案 2 :(得分:3)

编写一些测试代码并为您的用例添加一些实际值。如果性能对你来说是一个真正的问题,那么这两种方法都不是那么复杂,不值得付出努力。

我基于您描述的两种方法快速尝试了这一点,并且由于indexOf方法的缓慢,如果您添加的内容比检索的要多得多,则Set实现会更快。但我真的建议你自己做测试 - 你是唯一知道细节可能是什么的人。

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

public class SetVsListTest<E> {
    private static Random random = new Random();
    private Set<E> elementSet;
    private List<E> elementList;

    public SetVsListTest() {
        elementSet = new HashSet<>();
        elementList = new ArrayList<>();
    }

    private void listAdd(E element) {
        if (elementList.indexOf(element) == -1) {
            elementList.add(element);
        }
    }

    private void setAdd(E element) {
        elementSet.add(element);
    }

    private E listGetRandom() {
        return elementList.get(random.nextInt(elementList.size()));
    }

    @SuppressWarnings("unchecked")
    private E setGetRandom() {
        return (E) elementSet.toArray()[random.nextInt(elementSet.size())];
    }

    public static void main(String[] args) {
        SetVsListTest<Integer> test;
        List<Integer> testData = new ArrayList<>();
        int testDataSize = 100_000;
        int[] addToRetrieveRatios = new int[] { 10, 100, 1000, 10000 };

        for (int i = 0; i < testDataSize; i++) {
            /*
             * Add 1/5 of the total possible number of elements so that we will
             * have (on average) 5 duplicates of each number. Adjust this to
             * whatever is most realistic
             */
            testData.add(random.nextInt(testDataSize / 5));
        }

        for (int addToRetrieveRatio : addToRetrieveRatios) {
            /*
             * Test the list method
             */
            test = new SetVsListTest<>();
            long t1 = System.nanoTime();
            for(int i=0;i<testDataSize; i++) {
                // Use == 1 here because we don't want to get from an empty collection
                if(i%addToRetrieveRatio == 1) {
                    test.listGetRandom();
                } else {
                    test.listAdd(testData.get(i));
                }
            }
            long t2 = System.nanoTime();
            System.out.println(((t2-t1)/1000000L)+" ms for list method with add/retrieve ratio "+addToRetrieveRatio);

            /*
             * Test the set method
             */
            test = new SetVsListTest<>();
            t1 = System.nanoTime();
            for(int i=0;i<testDataSize; i++) {
                // Use == 1 here because we don't want to get from an empty collection
                if(i%addToRetrieveRatio == 1) {
                    test.setGetRandom();
                } else {
                    test.setAdd(testData.get(i));
                }
            }
            t2 = System.nanoTime();
            System.out.println(((t2-t1)/1000000L)+" ms for set method with add/retrieve ratio "+addToRetrieveRatio);
        }
    }
}

我机器上的输出是:

819 ms for list method with add/retrieve ratio 10
1204 ms for set method with add/retrieve ratio 10
1547 ms for list method with add/retrieve ratio 100
133 ms for set method with add/retrieve ratio 100
1571 ms for list method with add/retrieve ratio 1000
23 ms for set method with add/retrieve ratio 1000
1542 ms for list method with add/retrieve ratio 10000
5 ms for set method with add/retrieve ratio 10000

答案 3 :(得分:2)

您可以扩展HashSet并跟踪对其的更改,维护所有条目的当前数组。

这里我保留一个数组副本,并在每次更改时调整它。要获得更强大(但成本更高)的解决方案,您可以在toArray方法中使用pick

class PickableSet<T> extends HashSet<T> {
    private T[] asArray = (T[]) this.toArray();

    private void dirty() {
        asArray = (T[]) this.toArray();
    }

    public T pick(int which) {
        return asArray[which];
    }

    @Override
    public boolean add(T t) {
        boolean added = super.add(t);
        dirty();
        return added;
    }

    @Override
    public boolean remove(Object o) {
        boolean removed = super.remove(o);
        dirty();
        return removed;
    }
}

请注意,如果由Iterator删除,则无法识别对该集的更改 - 您需要以其他方式处理。

答案 4 :(得分:1)

  

所以我的问题是哪种方式会更有效率?

要回答一个很难回答的问题,取决于一个人做了什么,插入或随意选择?

我们需要查看每个操作的Big O。在this case(最好的情况):

  • 设置:插入O(1)
  • 设置:toArray O(n)(我假设)
  • 数组:访问O(1)

VS

  • 列表:包含O(n)
  • 列表:插入O(1)
  • 列表:访问O(1)

所以:

  • 设置:插入:O(1),访问O(n)
  • 列表:插入:O(n),访问O(1)

所以在最好的情况下,如果插入的数量多于你选择的数量,那么它们与Set获胜相当多,如果相反,则为List。

现在是邪恶的答案 - 选择一个(最能代表问题的那个(所以设置IMO)),将它包好并运行它。如果它太慢,那么以后处理它,当你处理它时,看看问题空间。您的数据经常更改吗?不,缓存数组。

答案 5 :(得分:0)

这取决于你更重视的东西。

Java中的

List实现通常使用数组或链表。这意味着插入和搜索索引很快,但是搜索特定元素将需要循环思考列表并比较每个元素直到找到元素。

Java中的

Set实现主要使用数组,hashCode方法和equals方法。因此,当您想要插入时,一个集合会更加繁重,但是在寻找元素时会胜过列表。由于集合不保证结构中元素的顺序,因此您将无法通过索引获取元素。您可以使用有序集,但由于排序,这会在插入上带来延迟。

如果您要直接使用索引,那么您可能必须使用List,因为当您向{{1}添加元素时,元素将放入Set.toArray()的顺序会发生变化}}。

希望这会有所帮助:)