簡體   English   中英

如何從 Java 中的 HashMap 中選擇一個隨機鍵?

[英]How to select a random key from a HashMap in Java?

我正在使用一個大的ArrayList<HashMap<A,B>> ,我會反復需要從隨機 HashMap 中選擇一個隨機鍵(並用它做一些事情)。 選擇隨機 HashMap 是微不足道的,但我應該如何從這個 HashMap 中選擇一個隨機鍵?

速度很重要(因為我需要這樣做 10000 次並且哈希圖很大),所以只在 [0,9999] 中選擇一個隨機數 k 然后在迭代器上執行.next() k 次,真的不是一個選擇. 同樣,在每次隨機選擇時將 HashMap 轉換為數組或 ArrayList 也不是一種選擇。 請在回復之前閱讀此內容。

從技術上講,我覺得這應該是可能的,因為 HashMap 在內部將其鍵存儲在Entry[] ,並且從數組中隨機選擇很容易,但我不知道如何訪問這個Entry[] 因此,任何訪問內部Entry[]想法都非常受歡迎。 當然也歡迎其他解決方案(只要它們不消耗哈希圖大小的線性時間)。

注意:啟發式很好,所以如果有一種方法可以排除 1% 的元素(例如,因為多填充桶),那完全沒有問題。

從我的頭頂

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

那么就

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

我設法找到了一個沒有性能損失的解決方案。 我會把它貼在這里,因為它可能會幫助其他人——並且可能會回答關於這個主題的幾個懸而未決的問題(我稍后會搜索這些)。

您需要的是第二個類似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);
     }
}

您需要訪問基礎條目表。

// 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)。

聽起來您應該考慮將輔助鍵列表或真實對象而不是 Map 存儲在您的列表中。

正如@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);
     }
}

我假設您正在使用HashMap因為您需要在以后查找某些內容?

如果不是這種情況,那么只需將您的HashMap更改為Array / ArrayList

如果是這種情況,為什么不將您的對象存儲在MapArrayList以便您可以隨機或按鍵查找。

或者,您可以使用TreeMap而不是HashMap嗎? 我不知道您的密鑰是什么類型,但您將TreeMap.floorKey()與一些密鑰隨機化器結合使用。

花了一些時間后,我得出結論,您需要創建一個模型,該模型可以由List<Map<A, B>>List<A>來維護您的密鑰。 您需要保留對List<Map<A, B>>List<A>的訪問權限,只需向調用者提供操作/方法即可。 通過這種方式,您將完全控制實現,並且實際對象將更安全,不受外部更改的影響。

順便說一句,你的問題讓我想到,

這個例子, 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);

注意:我自己沒有嘗試過。 稍后會這樣做(趕回家)。

從 Java 8 開始,有一個 O(log(N)) 方法和 O(log(N)) 額外的內存:通過map.entrySet().spliterator()創建一個Spliterator , make log(map.size()) trySplit()調用並隨機選擇前半部分或后半部分。 Spliterator元素少於 10 個Spliterator ,將它們轉儲到列表中並隨機選擇。

如果絕對需要訪問 HashMap 中的 Entry 數組,則可以使用反射。 但是你的程序將依賴於 HashMap 的具體實現。

按照建議,您可以為每個地圖保留一個單獨的鍵列表。 您不會保留密鑰的深層副本,因此實際的內存非規范化不會那么大。

第三種方法是實現您自己的 Map 實現,該實現將鍵保存在列表中而不是集合中。

如何將 HashMap 包裝在 Map 的另一個實現中? 另一個映射維護一個列表,在 put() 上它會:

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

(我假設值的空值是不允許的,如果它們使用 containsKey,但速度較慢)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM