[英]Why does hashmap does not have ensureCapacity() method like ArrayList?
[英]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;
}
有時可能存在Hashcode沖突的可能性,並且為了解決此沖突,Hashmap使用equals(),然后將該元素存儲到同一存儲桶中的LinkedList中。
獲取密鑰vaibahv的數據:map.get(new Key(“vaibhav”));
腳步:
計算Key {“vaibhav”}的哈希碼。它將生成為118。
使用索引方法計算索引將為6。
轉到數組的索引6並將第一個元素的鍵與給定鍵進行比較。 如果兩者都是等於則返回值,否則檢查下一個元素是否存在。
在我們的例子中,它不是第一個元素,節點對象的下一個不是null。
如果node的下一個為null,則返回null。
如果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
計算的,並且它是為了減少編寫糟糕的哈希函數的風險)。 當一個情況發生時,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]
。
map.put("A", value);
假設“A”的哈希值為5.哈希映射現在可以執行bucket[5] = new Entry("A", value);
map.put("B", value);
假設散列(“B”)= 3.因此, bucket[3] = new Entry("B", value);
map.put("C"), value);
- hash(“C”)= 19 - bucket[19] = new Entry("C", value);
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);
,而是讓我們的bucket
有Bucket[]
而不是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
大小設置為比您猜測其中包含的條目數更多的桶,那么您將擁有一個優化的哈希映射,這將是插入的最快數據結構和復雜數據的查詢。
因為你說你經常看到這個for循環在你的調試中用多個元素迭代,這意味着你的HashMap
可能太小了。 如果您對可能放入的內容有合理的猜測,請嘗試將大小設置為大於此值。 請注意,在上面的示例中,我插入了10個字符串但是有一個帶有20個桶的哈希映射。 使用良好的哈希函數,這將產生非常少的沖突。
注意:上面的例子是對問題的簡化,並且為了簡潔起見確實采取了一些捷徑。 完整的解釋甚至會稍微復雜一些,但是您回答所提問題時需要知道的一切都在這里。
散列表具有存儲桶,因為對象的散列不必是唯一的。 如果對象的散列相等,則平均值,對象可能是相等的。 如果對象的散列不同,則對象完全不同。 因此,具有相同散列的對象被分組為桶。 for循環用於迭代此類存儲桶中包含的對象。
實際上,這意味着在這樣的哈希表中查找對象的算法復雜度不是恆定的(雖然非常接近它),但是在對數和線性之間。
我想用簡單的話說。 put
方法有一個FOR循環來迭代屬於hashCode的同一個桶的密鑰列表。
put
key-value
對放入hashmap時會發生什么:
HashMap
每個key
,它將為它計算hashCode。 keys
可以歸入同一個hashCode
存儲桶。 現在,HashMap將檢查同一個桶中是否存在相同的key
。 因此,在平均情況下,其時間復雜度為: O(1)
,在最壞的情況下,其時間復雜度為O(N)
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.