【问题标题】:How to select a random key from a HashMap in Java?如何从 Java 中的 HashMap 中选择一个随机键?
【发布时间】:2012-09-05 07:15:56
【问题描述】:

我正在使用一个大的ArrayList<HashMap<A,B>>,我会反复需要从随机 HashMap 中选择一个随机键(并用它做一些事情)。选择随机 HashMap 很简单,但是我应该如何从这个 HashMap 中选择一个随机键呢?

速度很重要(因为我需要这样做 10000 次并且哈希图很大),所以只需在 [0,9999] 中选择一个随机数 k,然后在迭代器上执行 k 次 .next(),真的不是一个选项。 同样,在每次随机选择时将 HashMap 转换为数组或 ArrayList 确实不是一种选择。请在回复之前阅读此内容。

从技术上讲,我觉得这应该是可能的,因为 HashMap 在内部将其键存储在 Entry[] 中,并且从数组中随机选择很容易,但我不知道如何访问这个 Entry[]。因此,任何访问内部Entry[] 的想法都非常受欢迎。当然也欢迎其他解决方案(只要它们不消耗散列图大小的线性时间)。

注意:启发式方法很好,所以如果有一种方法可以排除 1% 的元素(例如,由于多个填充的桶),那根本没有问题。

【问题讨论】:

  • 当您在同一个索引中有多个条目时,条目将被链接。所以这不会那么简单。
  • 如果将 entrySet 转换为 List 的速度不够快(您进行了分析吗?),那么您需要另一个数据结构。
  • @dystroy Pseudorandom 很好,如果有 1% 的条目从未被选中,这没什么大不了的。这是否提供了额外的选择?所以,如果一个元素被链接了,不用担心,然后选择另一个元素。

标签: java random hashmap


【解决方案1】:

从我的头顶

List<A> keysAsArray = new ArrayList<A>(map.keySet())
Random r = new Random()

那么就

map.get(keysAsArray.get(r.nextInt(keysAsArray.size()))

【讨论】:

  • keySet 的大小仍然具有线性复杂性,不是吗? ://
  • @user1111929 这取决于您的密钥集是否经常更改。只有在地图中添加或删除某些内容时,您才可以只更新列表。然后获取本身将是恒定的时间。
  • 唉,每个随机选择都会修改其中一个哈希图。如果我可以访问Entry[],我当然可以在数组中做一个简单的修改,但似乎这个Entry[] 是不可访问的(除非我复制粘贴整个源代码)。
  • 在不需要反射的意义上更好,即使剩下的条目很少也能有效地工作。
【解决方案2】:

我设法找到了一个没有性能损失的解决方案。我会把它贴在这里,因为它可能会帮助其他人——并且可能会回答关于这个主题的几个未解决的问题(我稍后会搜索这些)。

您需要第二个自定义的Set 类数据结构来存储密钥——而不是这里建议的列表。类似列表的数据结构要从中删除项目的成本很高。所需的操作是在恒定时间内添加/删除元素(以使其与 HashMap 保持同步)以及选择随机元素的过程。以下类MySet 正是这样做的

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 time
     A randomKey() {
         return contents.get(R.nextInt(contents.size()));
     }

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

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

【讨论】:

  • 添加操作是 O(n),因为您使用的是 ArrayList。
  • 为什么加法运算会是O(n)?我在 ArrayList 的末尾附加了一个,即 O(1)。
  • 是的,但是 ArrayList 的大小不是无限的,是吗?那么如果你在已经完整的 ArrayList 中插入另一个元素会发生什么?它声明了一个新的更大的数组,并将该数组复制到更大的数组,需要 O(n)。但这发生在后面,所以从技术上讲,你可以依赖 O(1)。严格来说,是 O(n)。
  • 并且删除操作也是 O(n) (这一次甚至在技术上)。 ArrayList 说:“size、isEmpty、get、set、iterator 和 listIterator 操作在常数时间内运行。add 操作在 amortized 常数时间内运行,即添加 n 个元素需要 O(n) 时间。所有其他操作以线性时间运行(粗略地说)。”
  • 不错的解决方案,但有一点问题:在 remove 方法中,您从内容中删除最后一个项目,然后使用 (new last element, index in which you put the previous last element) 更新索引映射)。您应该存储从内容中删除的最后一个元素并将其用作索引键。
【解决方案3】:

您需要访问底层条目表。

// defined staticly
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
Random rand = new Random();

public Entry randomEntry(HashMap map) {
    Entry[] entries = (Entry[]) table.get(map);
    int start = rand.nextInt(entries.length);
    for(int i=0;i<entries.length;i++) {
       int idx = (start + i) % entries.length;
       Entry entry = entries[idx];
       if (entry != null) return entry;
    }
    return null;
}

这仍然需要遍历条目以找到其中的条目,因此最坏的情况是 O(n),但典型的行为是 O(1)。

【讨论】:

  • HashMap.class.getDeclaredField("table");,太棒了,谢谢!我现在想知道的是为什么他们默认没有把它放在 HashMap 和 HashSet 中。 :-)
  • @user1111929 为这种目的使用泛型是有问题的——如果实现发生变化,程序就会被破坏。应该针对接口而不是实现进行编程。
  • @JakubZaverka 我认为你的意思是使用反射有点可疑而且很脆弱。我认为使用泛型没有问题。 ;)
  • 这个解决方案怎么样stackoverflow.com/questions/12385284/…你觉得更好吗?
【解决方案4】:

听起来您应该考虑将辅助键列表或真实对象(而不是地图)存储在您的列表中。

【讨论】:

  • 不幸的是,HashMap 不提供将键存储在简单列表中的能力,并且没有其他结构可以使用恒定时间 get() 将任意对象映射到任意对象。
  • 因此出现了“辅助”一词。它是您将与地图列表一起维护的单独数据结构。你的想法太低了。
  • 确实如此,但是考虑到我的 HashMap 的大小,任何辅助结构都会显着增加内存使用量(因为对象很小但数量很多)。我仍然希望以某种方式访问​​Entry[]。我可以将整个源代码复制粘贴到一个新文件中并在那里使用它,但这并不是很好的编码风格。 ://
【解决方案5】:

正如@Alberto Di Gioacchino 所指出的,在已接受的解决方案中存在与删除操作相关的错误。我就是这样解决的。

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 time
     A randomKey() {
         return contents.get(R.nextInt(contents.size()));
     }

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

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

【讨论】:

  • 啊,我发誓我以前修过这个,看来我没有。但确实是正确的代码!现在我自己也修复了它。 (顺便说一句,你有 item 而不是 a,这不会编译。)
  • 啊,是的!谢谢指出,刚刚编辑。还要感谢您的实施,它对我来说非常完美。
【解决方案6】:

我假设您正在使用HashMap,因为您需要在以后查找某些内容?

如果不是这样,那么只需将您的 HashMap 更改为 Array/ArrayList

如果是这种情况,为什么不将您的对象存储在 MapArrayList 中,以便您可以随机或按键查找。

或者,您可以使用TreeMap 代替HashMap 吗?我不知道您的密钥是什么类型,但您将TreeMap.floorKey() 与一些密钥随机器结合使用。

【讨论】:

  • Treemap 插入和搜索是 log(n) 而不是 log(1)
【解决方案7】:

花了一些时间后,我得出的结论是,您需要创建一个可以由List&lt;Map&lt;A, B&gt;&gt;List&lt;A&gt; 支持的模型来维护您的密钥。您需要保留List&lt;Map&lt;A, B&gt;&gt;List&lt;A&gt; 的访问权限,只需将操作/方法提供给调用者即可。通过这种方式,您将拥有对实现的完全控制权,并且实际对象将不会受到外部更改的影响。

顺便说一句,你的问题引导我,

这个例子,IndexedSet,可能会让你知道如何做。

[编辑]

如果您决定创建自己的模型,该课程SetUniqueList 可能会对您有所帮助。它明确声明它包装了list,而不是副本。所以,我认为,我们可以做类似的事情,

List<A> list = new ArrayList(map.keySet());
SetUniqueList unikList = new SetUniqueList(list, map.keySet);
// Now unikList should reflect all the changes to the map keys
...
// Then you can do
unikList.get(i);

注意: 我自己没有尝试过。稍后会这样做(赶回家)。

【讨论】:

    【解决方案8】:

    从 Java 8 开始,有一个 O(log(N)) 方法,增加 O(log(N)) 内存:通过 map.entrySet().spliterator() 创建一个 Spliterator,make log(map.size()) @987654323 @ 调用并随机选择前半部分或后半部分。当Spliterator 中剩余的元素少于 10 个时,将它们转储到一个列表中并随机选择。

    【讨论】:

    • 这会产生(或多或少)hashmap 的均匀随机元素吗? trySplit() 是否总是将它们分成两半或什么?我对这个新程序的内部运作感到困惑。
    • 将剩余元素转储到列表并进行随机选择时,截断尺寸越大,整体随机键的选择就越统一。 10 个元素是测试(均匀性)的起点。 I think that the actual value may be between 8 and 32 elements when the choice becomes very uniform.
    • 然而,这可能取决于密钥散列码的分布质量。如果质量不错,这不应该是一个问题,但是如果有很多精确的哈希码冲突或所有键都集中在表的某些部分,因为某些位在大多数或所有哈希码中是 0 或 1,随机密钥选择的一致性可能会受到影响。
    【解决方案9】:

    如果你绝对需要访问HashMap中的Entry数组,你可以使用反射。但是你的程序将依赖于 HashMap 的具体实现。

    按照建议,您可以为每个地图保留一个单独的键列表。您不会保留密钥的深层副本,因此实际的内存非规范化不会那么大。

    第三种方法是实现您自己的 Map 实现,将键保存在列表中而不是集合中。

    【讨论】:

      【解决方案10】:

      在 Map 的另一个实现中包装 HashMap 怎么样?另一张地图维护一个列表,并在 put() 上进行维护:

      if (inner.put(key, value) == null) listOfKeys.add(key);
      

      (我假设值的空值是不允许的,如果它们使用 containsKey,但这会更慢)

      【讨论】:

        猜你喜欢
        • 2016-09-06
        • 1970-01-01
        • 1970-01-01
        • 2019-07-30
        • 2015-05-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-25
        相关资源
        最近更新 更多