簡體   English   中英

為什么HashMap的get方法有一個FOR循環?

[英]Why does the get method of HashMap have a FOR loop?

我正在查看Java 7中HashMap的源代碼,我看到put方法將檢查是否已經存在任何條目,如果它存在,那么它將用新值替換舊值。

    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

所以,基本上它意味着給定密鑰總是只有一個條目,我也通過調試看到了這一點,但如果我錯了,那么請糾正我。

現在,由於給定鍵只有一個條目,為什么get方法有一個FOR循環,因為它可以簡單地直接返回值?

    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }

我覺得上面的循環是不必要的。 如果我錯了,請幫助我理解。

table[indexFor(hash, table.length)]HashMap一個桶,它可能包含我們正在尋找的密鑰(如果它存在於Map )。

但是,每個桶可能包含多個條目(具有相同hashCode()不同鍵,或者具有不同hashCode()不同鍵仍然映射到同一個桶),因此您必須迭代這些條目,直到找到密鑰為止正在找。

由於每個桶中的預期條目數應該非常小,因此該循環仍然在預期的O(1)時間內執行。

如果你看到HashMap的get方法的內部工作。

public V get(Object key)  {
        if (key == null)
           return getForNullKey();
         int hash = hash(key.hashCode());
         for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) 
         {
             Object k;
             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                 return e.value;
         }
             return null;
}
  • 首先,它獲取傳遞的密鑰對象的哈希碼,並找到存儲桶位置。
  • 如果找到正確的存儲桶,則返回值(e.value)
  • 如果未找到匹配項,則返回null。

有時可能存在Hashcode沖突的可能性,並且為了解決此沖突,Hashmap使用equals(),然后將該元素存儲到同一存儲桶中的LinkedList中。

讓我們舉個例子: 在此輸入圖像描述

獲取密鑰vaibahv的數據:map.get(new Key(“vaibhav”));

腳步:

  1. 計算Key {“vaibhav”}的哈希碼。它將生成為118。

  2. 使用索引方法計算索引將為6。

  3. 轉到數組的索引6並將第一個元素的鍵與給定鍵進行比較。 如果兩者都是等於則返回值,否則檢查下一個元素是否存在。

  4. 在我們的例子中,它不是第一個元素,節點對象的下一個不是null。

  5. 如果node的下一個為null,則返回null。

  6. 如果node的下一個非空遍歷到第二個元素並重復進程3,直到找不到key或next不為null。

對於此檢索過程,將使用循環。 有關更多參考,請參閱此內容

對於記錄,在java-8中,這也存在(有點,因為還有TreeNode ):

if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }

基本上(對於bin不是Tree ),迭代整個bin,直到找到我們要查找的條目。

看看這個實現,你可能會理解為什么提供一個好的哈希是好的 - 所以不是所有的條目最終都在同一個桶中,因此需要更長的時間來搜索它。

我認為@Eran已經很好地回答了你的問題,並且@Prashant也和其他已經回答的人一起做了很好的嘗試, 所以讓我用一個例子來解釋它,這樣概念就變得非常明確了

概念

基本上@Eran試圖在給定的桶中(基本上在給定的數組索引處)說有可能存在多個條目(只有Entry對象),當2個或更多個鍵給出不同的哈希時,這是可能的但是給出相同的索引/桶位置。

現在,為了將條目放在hashmap中,這就是在高級別發生的事情( 請仔細閱讀,因為我已經花了很多時間來解釋一些好東西,否則這些東西不是你問題的一部分 ):

  • 獲取哈希:這里發生的是為給定密鑰計算第一個哈希值(請注意,這不是hashCode ,哈希是使用hashCode計算的,並且它是為了減少編寫糟糕的哈希函數的風險)。
  • 獲取索引:這基本上是數組的索引,換句話說就是桶。 現在,為什么計算此索引而不是直接使用散列作為索引,是因為為了降低散列可能超過散列映射大小的風險,因此此索引計算步驟確保索引始終小於散列的大小HashMap中。

當一個情況發生時,2個密鑰給出不同的散列但是相同的索引,那么這兩個密鑰將進入同一個桶,這就是FOR循環很重要的原因。

下面是我創建的一個簡單示例,用於向您演示這個概念:

public class Person {
    private int id;

    Person(int _id){
        id = _id;
    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return id;
    }
}

測試類:

import java.util.Map;

public class HashMapHashingTest {
    public static void main(String[] args) {
        Person p1 = new Person(129);
        Person p2 = new Person(133);

        Map<Person, String> hashMap = new MyHashMap<>(2);
        hashMap.put(p1, "p1");
        hashMap.put(p2, "p2");
        System.out.println(hashMap);
    }
}

調試截圖(請點擊並縮放,因為它看起來很小):

在此輸入圖像描述

請注意,在上面的示例中,兩個Person對象都給出了不同的哈希值(分別為136和140)但是給出了相同的0索引,因此兩個對象都在同一個桶中。 在屏幕截圖中,您可以看到兩個對象都在索引0並且您有一個next也填充,它基本上指向第二個對象。


更新: 另一個最容易看到多個密鑰進入同一個存儲桶的方法是創建一個類並重寫hashCode方法以始終返回相同的int值,現在會發生的是該類的所有對象都會給出相同的索引/存儲區位置,但由於您沒有覆蓋equals方法,因此它們不會被視為相同,因此將在該索引/存儲區位置形成一個列表。

這里的另一個轉折是假設你也覆蓋了equals方法,並且比較所有相等的對象,那么只有一個對象將出現在索引/桶位置,因為所有對象都是相等的。

雖然其他答案解釋了正在發生的事情,OP對這些答案的評論使我認為需要一個不同的解釋角度。

簡化示例

假設您要將10個字符串放入哈希映射:“A”,“B”,“C”,“Hi”,“Bye”,“Yo”,“Yo-yo”,“Z”,“1 “,”2“

您正在使用HashMap作為哈希映射,而不是使用自己的哈希映射(不錯的選擇)。 下面的一些內容不會直接使用HashMap實現,但會從更理論和抽象的角度來看待它。

HashMap並不會神奇地知道你要為它添加10個字符串,也不知道稍后會添加哪些字符串。 它必須提供放置任何你可能給它的東西的地方...因為它知道你將要放入100,000個字符串 - 也許是字典中的每個字。

讓我們說,因為你在創建new HashMap(n)時選擇的構造函數參數,你的哈希映射有20個 我們將它們稱為bucket[0]bucket[19]

  1. map.put("A", value); 假設“A”的哈希值為5.哈希映射現在可以執行bucket[5] = new Entry("A", value);

  2. map.put("B", value); 假設散列(“B”)= 3.因此, bucket[3] = new Entry("B", value);

  3. map.put("C"), value); - hash(“C”)= 19 - bucket[19] = new Entry("C", value);

  4. map.put("Hi", value); 現在這里有趣的地方。 假設您的哈希函數是哈希(“Hi”)= 3.所以現在哈希映射想要做bucket[3] = new Entry("Hi", value); 我們出現了問題! bucket[3]是我們放置鍵“B”的地方,而“Hi”肯定是與“B”不同的鍵...但是它們具有相同的散列值 我們碰撞了

由於這種可能性, HashMap實際上並沒有以這種方式實現。 哈希映射需要具有可以在其中包含多於1個條目的存儲桶。 注:沒有說超過1項使用相同的密鑰 ,因為我們不能有一點 ,但它需要有一個能容納不同的鍵超過1項桶。 我們需要一個可以同時保持“B” “Hi”的鏟斗。

所以,我們不要做bucket[n] = new Entry(key, value); ,而是讓我們的bucketBucket[]而不是Entry[] 所以現在我們做bucket[n].add( new Entry(key, value) );

那么讓我們改變......

bucket[3].add("B", value);

bucket[3].add("Hi", value);

如您所見,我們現在在同一個桶中有“B”和“Hi”的條目。 現在, 當我們想讓它們退出時,我們需要遍歷存儲桶中的所有內容, 例如,使用for循環

因此,由於碰撞而存在循環。 不是 key沖突,而是hash(key)沖突。

為什么我們使用這種瘋狂的數據結構?

你可能會在這一點上問, “等等,什么!?!為什么我們會這樣做一個奇怪的事情???為什么我們使用這樣一個人為的,錯綜復雜的數據結構?” 這個問題答案是...

哈希映射的工作方式與此類似,因為由於數學運算的方式,這種特殊的設置為我們提供了這些屬性。 如果您使用一個良好的哈希函數來最小化沖突,並且如果您將HashMap大小設置為比您猜測其中包含的條目數更多的桶,那么您將擁有一個優化的哈希映射,這將是插入的最快數據結構和復雜數據的查詢。

你的HashMap可能太小了

因為你說你經常看到這個for循環在你的調試中用多個元素迭代,這意味着你的HashMap可能太小了。 如果您對可能放入的內容有合理的猜測,請嘗試將大小設置為大於此值。 請注意,在上面的示例中,我插入了10個字符串但是有一個帶有20個桶的哈希映射。 使用良好的哈希函數,這將產生非常少的沖突。

注意:

注意:上面的例子是對問題的簡化,並且為了簡潔起見確實采取了一些捷徑。 完整的解釋甚至會稍微復雜一些,但是您回答所提問題時需要知道的一切都在這里。

散列表具有存儲桶,因為對象的散列不必是唯一的。 如果對象的散列相等,則平均值,對象可能是相等的。 如果對象的散列不同,則對象完全不同。 因此,具有相同散列的對象被分組為桶。 for循環用於迭代此類存儲桶中包含的對象。

實際上,這意味着在這樣的哈希表中查找對象的算法復雜度不是恆定的(雖然非常接近它),但是在對數和線性之間。

我想用簡單的話說。 put方法有一個FOR循環來迭代屬於hashCode的同一個桶的密鑰列表。

put key-value對放入hashmap時會發生什么:

  1. 因此,對於傳遞給HashMap每個key ,它將為它計算hashCode。
  2. 這么多keys可以歸入同一個hashCode存儲桶。 現在,HashMap將檢查同一個桶中是否存在相同的key
  3. 在Java 7中,HashMap在列表中維護同一個存儲桶的所有密鑰。 因此,在插入密鑰之前,它將遍歷列表以檢查是否存在相同的密鑰。 這就是FOR循環的原因。

因此,在平均情況下,其時間復雜度為: O(1) ,在最壞的情況下,其時間復雜度為O(N)

暫無
暫無

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

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