【问题标题】:Writing a Set implementation with a randomElement() method使用 randomElement() 方法编写 Set 实现
【发布时间】:2015-09-13 09:51:15
【问题描述】:

我正在尝试编写Set 的实现,它有一个附加方法randomElement(),它从Set 中随机返回一个元素。我基于HashSet 实现了快速contains() 方法。我还使用了ArrayList,因此randomElement() 方法也是O(1) - 使用ArrayList,您所要做的就是选择一个随机索引。

这是我的代码。

public final class RandomChoiceSet<E> extends AbstractSet<E> {

    private final List<E> list = new ArrayList<>();
    private final Set<E> set = new HashSet<>();
    private final Random random = new Random();

    public RandomChoiceSet() {}

    public RandomChoiceSet(Collection<? extends E> collection) {
        addAll(collection);
    }

    public E randomElement() {
        return list.get(random.nextInt(list.size()));
    }

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public boolean contains(Object o) {
        return set.contains(o);
    }

    @Override
    public void clear() {
        list.clear();
        set.clear();
    }

    @Override
    public boolean add(E e) {
        boolean result = set.add(e);
        if (result)
            list.add(e);
        return result;
    }

    @Override
    public boolean remove(Object o) {
        boolean result = set.remove(o);
        if (result)
            list.remove(o);
        return result;
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {

            private final Iterator<E> iterator = list.iterator();
            private E e;

            @Override
            public boolean hasNext() {
                return iterator.hasNext();
            }

            @Override
            public E next() {
                return e = iterator.next();
            }

            @Override
            public void remove() {
                iterator.remove();
                set.remove(e);
            }
        };
    }
}

维护ListSet 的缺点是它使remove() 方法成为O(n),因为必须首先在ArrayList 中找到该元素,然后所有其他元素都有沿一个地方移动。所以我想知道是否可以写这样一个Set,其中所有五个操作size()contains()add()remove()randomElement() 都是O(1)

【问题讨论】:

  • 我不确定复制数据是否是个好主意。 HashSetArrayList 已经引入了足够的内存开销
  • @Dici 是的。我主要想知道这在理论上是否可行。
  • 我明白了。我也想知道,看来您正在寻找无法以有效方式同时实现的属性

标签: java collections set time-complexity


【解决方案1】:

我唯一能想到的就是用一个 HashMap 替换你的集合,它从你的元素映射到它在 arrayList 中的位置。大小、包含、添加和随机将是相同的。对于删除,您将执行以下操作:

Find the element from the HashMap
Retrieve it's position in the arrayList
Remove the element from the HashMap
Swap the deleted element with the last element in the array
Modify the swapped element position in the HashMap
Delete the last element from the array //Now this is O(1)

这项工作的原因在于您不需要阵列中的任何特定顺序,您只需要一个随机访问存储,因此只要您保持与您的同步,更改顺序或数据就不会造成任何问题哈希图。

【讨论】:

  • ArrayList 中删除项目将是O(n),无论您是否知道要删除的项目在哪里。这就是为什么你不应该有一个包含所有项目的列表,而是每个存储桶一个列表
  • 哇。我认为这行得通。我需要考虑一下,看看我是否能想到任何问题。
  • @Dici 如果 index 是最后一个元素,我真的不知道 Java 如何在内部处理 remove(int index)。但是,如果您实现自己的 arrayList 并添加一个 remove last 方法,它只是减小数组大小的大小,这很容易实现。
  • @Amr 我认为删除ArrayList 的最后一个元素是O(1)。反正也没关系。您可以将最后一个元素留在那里。只要相关的Es占据ArrayList开头的连续槽,还有什么都没关系。
  • @Dici 没问题很高兴澄清。只需从 arrayList 中选择一个随机元素。这是它的唯一目的。从 [0,arrayList size[ 生成一个随机数并从数组中返回该元素。
【解决方案2】:

这是一个功能实现,遵循 Amr 的解决方案。甚至 Iteratorremove() 方法也有效,因为元素总是从后面的位置换入。

public final class RandomChoiceSet<E> extends AbstractSet<E> {

    private final List<E> list = new ArrayList<>();
    private final Map<E, Integer> map = new HashMap<>();
    private final Random random = new Random();

    public RandomChoiceSet() {}

    public RandomChoiceSet(Collection<? extends E> collection) {
        addAll(collection);
    }

    public E randomElement() {
        return list.get(random.nextInt(list.size()));
    }

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public boolean contains(Object o) {
        return map.containsKey(o);
    }

    @Override
    public void clear() {
        list.clear();
        map.clear();
    }

    @Override
    public boolean add(E e) {
        if (map.containsKey(e))
            return false;
        map.put(e, list.size());
        list.add(e);
        return true;
    }

    @Override
    public boolean remove(Object o) {
        Integer currentIndex = map.get(o);
        if (currentIndex == null)
            return false;
        int size = list.size();
        E lastE = list.get(size - 1);
        list.set(currentIndex, lastE);
        list.remove(size - 1);
        map.put(lastE, currentIndex);
        map.remove(o);
        return true;
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {

            private int index = 0;

            @Override
            public boolean hasNext() {
                return index < list.size();
            }

            @Override
            public E next() {
                return list.get(index++);
            }

            @Override
            public void remove() {
                RandomChoiceSet.this.remove(list.get(--index));
            }
        };
    }
}

编辑

我想得越多,这个Set 的实现似乎就越有用。虽然我在上面的代码中没有这样做,但您可以包含一个get() 方法来按索引获取元素。这不仅比从Set(例如set.iterator().next()set.stream().findAny().get())获取任何元素的其他方法更好,而且它使您能够使用显式索引遍历集合。

int n = set.size();
for (int i = 0; i < n; i++) {
    Object o = set.get(i);
    // do something with o.
}

我没有进行适当的基准测试,但是像这样迭代 Set 似乎比使用 for each 循环迭代 HashSet 快几倍。显然没有检查并发修改,这是一个Set 实现,它比HashSet 更需要内存(虽然不像LinkedHashSet 那样需要内存),但总的来说我认为它非常有趣。

【讨论】:

    【解决方案3】:

    我认为您需要HashMap 的自定义实现,因为它需要细粒度控制。随机选择一个存储桶很容易,您可以在存储桶中使用ArrayLists 进行随机访问。

    为了清楚起见,您将实现一个经典的HashMap,但不是在每个存储桶中使用LinkedList,而是使用ArrayList。选择一个随机元素就像这样简单:

    • 在 0 和 nbBuckets 之间随机选择一个索引 rd0
    • 在 0 和 buckets[rd0].size() 之间随机选择索引 rd1

    【讨论】:

      【解决方案4】:

      这是一个完美的例子,说明为什么 Collections 接口应该有一个 getRandom() 方法,以及为什么它是 API 中一个基本的缺失功能。 HashSet 的 getRandom() 实现只会调用它的私有 HashMap getRandom() 方法等等,直到数据表示是可索引的或可迭代的,此时应该实现 getRandom() 逻辑。

      基本上,这个getRandom() 方法会根据底层实现的不同而改变复杂性。但由于在底层,所有数据最终都必须以数组或链表的形式存储,因此由于没有能够识别集合的 getRandom(),许多优化都被搁置了。

      想一想,什么是集合?认为我可以从现实世界的集合中检索随机元素吗?是的,所以它应该在正确的 OO 代码中。

      但事实并非如此,因此如果您负担不起构建自己的类的费用,您将不得不遍历 hashSet 中的项目并返回 random.nextInt(size()) 元素。

      如果您有能力构建自己的实现,这似乎是您的情况,那么您的建议是一种公平的方法,但是我不明白为什么您要实现自己的匿名迭代器,这应该就可以了。

      @Override public Iterator<E> iterator() { return set.iterator(); }

      【讨论】:

      • 你必须重写iterator的原因是Iterator有一个remove方法。如果您只是执行return set.iterator();,您将返回一个可用于修改set 的对象,而无需进行其他所需的更改。在我回答的代码中(不是问题),我真的为 iterator 方法感到自豪。使用Iterator,您可以在迭代集合时从集合中过滤元素,它仍然可以继续工作。
      猜你喜欢
      • 2019-07-11
      • 1970-01-01
      • 2020-06-25
      • 2021-06-18
      • 1970-01-01
      • 2016-10-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多