簡體   English   中英

Java hashmap 搜索真的是 O(1) 嗎?

[英]Is a Java hashmap search really O(1)?

我已經看到了一些關於 SO re Java 哈希圖及其O(1)查找時間的有趣聲明。 有人可以解釋為什么會這樣嗎? 除非這些哈希圖與我購買的任何哈希算法有很大不同,否則必須始終存在包含沖突的數據集。

在這種情況下,查找將是O(n)而不是O(1)

有人可以解釋它們是否O(1),如果是,它們是如何實現的?

HashMap 的一個特殊功能是,與平衡樹不同,它的行為是概率性的。 在這些情況下,根據最壞情況發生的概率來討論復雜性通常是最有幫助的。 對於散列映射,這當然是與映射恰好有多滿有關的沖突的情況。 碰撞很容易估計。

p碰撞= n / 容量

因此,即使元素數量很少的哈希映射也很可能會遇到至少一次沖突。 大 O 符號使我們能夠做一些更引人注目的事情。 觀察任何任意的固定常數 k。

O(n) = O(k * n)

我們可以利用這個特性來提高哈希映射的性能。 相反,我們可以考慮最多發生 2 次碰撞的概率。

p碰撞 x 2 = (n / 容量) 2

這要低得多。 由於處理一次額外碰撞的成本與 Big O 性能無關,因此我們找到了一種無需實際更改算法即可提高性能的方法! 我們可以將其概括為

p碰撞 xk = (n / 容量) k

現在我們可以忽略一些任意數量的碰撞,最終得到比我們所考慮的更多碰撞的可能性微乎其微。 您可以通過選擇正確的 k 將概率提高到任意微小的水平,而這一切都不會改變算法的實際實現。

我們通過說哈希映射具有 O(1) 訪問的高概率來談論這個

您似乎將最壞情況的行為與平均情況(預期的)運行時間混為一談。 對於一般的哈希表,前者確實是 O(n)(即不使用完美的哈希),但這在實踐中很少相關。

任何可靠的哈希表實現,加上半體面的哈希,在預期的情況下具有 O(1) 的檢索性能,在非常小的方差范圍內具有非常小的因子(實際上為 2)。

在 Java 中,HashMap 是如何工作的?

  • 使用hashCode定位對應的bucket【inside buckets container model】。
  • 每個存儲桶都是駐留在該存儲桶中的項目的列表(或從 Java 8 開始的樹)。
  • 項目被一項一項掃描,使用equals進行比較。
  • 添加更多項目時,一旦達到特定負載百分比,HashMap 就會調整大小。

因此,有時它必須與幾個項目進行比較,但一般來說,它比O(n)更接近O(1 )
出於實際目的,這就是您需要知道的全部內容。

請記住,o(1) 並不意味着每次查找僅檢查單個項目 - 它意味着檢查的項目的平均數量與容器中的項目數量保持恆定。 因此,如果平均需要 4 次比較才能在具有 100 個物品的容器中找到一個物品,那么在具有 10000 個物品的容器中找到一個物品也應該平均需要 4 次比較,並且對於任何其他數量的物品(總是有一個有點差異,特別是在哈希表重新散列的點附近,以及當項目數量非常少時)。

所以沖突不會阻止容器進行 o(1) 操作,只要每個桶的平均鍵數保持在固定范圍內。

我知道這是一個老問題,但實際上有一個新的答案。

嚴格來說,哈希映射並不是真正的O(1)是對的,因為隨着元素的數量變得任意大,最終您將無法在恆定時間內進行搜索(並且 O 符號是用術語定義的)可以變得任意大的數字)。

但這並不意味着實時復雜度是O(n) ——因為沒有規則說桶必須作為線性列表來實現。

事實上,一旦超過閾值,Java 8 就會將桶實現為TreeMaps ,這使得實際時間為O(log n)

如果桶的數量(稱為 b)保持不變(通常情況下),那么查找實際上是 O(n)。
隨着 n 變大,每個桶中的元素數量平均為 n/b。 如果沖突解決以一種常用方式(例如鏈表)完成,則查找為 O(n/b) = O(n)。

O 表示法是關於當 n 越來越大時會發生什么。 當應用於某些算法時,它可能會產生誤導,哈希表就是一個很好的例子。 我們根據我們期望處理的元素數量來選擇桶的數量。 當 n 與 b 的大小大致相同時,查找時間大致為常數,但我們不能稱其為 O(1),因為 O 是根據 n → ∞ 的極限定義的。

O(1+n/k)其中k是桶的數量。

如果實現設置k = n/alpha那么它是O(1+alpha) = O(1)因為alpha是一個常數。

我們已經確定哈希表查找的標准描述為 O(1) 指的是平均情況的預期時間,而不是嚴格的最壞情況性能。 對於使用鏈式解決沖突的哈希表(如 Java 的哈希圖),這在技術上是 O(1+α) , 具有良好的哈希函數,其中 α 是表的加載因子。 只要您存儲的對象數量不超過表大小的常數因子,它就保持不變。

也有人解釋說,嚴格來說,可以構造需要 O( n ) 查找任何確定性散列函數的輸入。 但考慮最壞情況的預期時間也很有趣,這與平均搜索時間不同。 使用鏈接是 O(1 + 最長鏈的長度),例如 Θ(log n / log log n ) 當 α=1 時。

如果您對實現恆定時間預期最壞情況查找的理論方法感興趣,您可以閱讀動態完美散列,它以遞歸方式解決與另一個散列表的沖突!

僅當您的散列函數非常好時才為 O(1)。 Java 哈希表實現不能防止錯誤的哈希函數。

添加項目時是否需要擴大表格與問題無關,因為它與查找時間有關。

HashMap 內部的元素存儲為一個鏈表(節點)數組,數組中的每個鏈表代表一個桶,用於存儲一個或多個鍵的唯一哈希值。
在 HashMap 中添加條目時,使用鍵的哈希碼來確定桶在數組中的位置,例如:

location = (arraylength - 1) & keyhashcode

這里 & 代表按位 AND 運算符。

例如: 100 & "ABC".hashCode() = 64 (location of the bucket for the key "ABC")

在 get 操作期間,它使用相同的方式來確定 key 的桶的位置。 在最好的情況下,每個鍵都有唯一的哈希碼,並為每個鍵生成一個唯一的桶,在這種情況下,get 方法只花時間來確定桶的位置並檢索常數 O(1) 的值。

在最壞的情況下,所有鍵都具有相同的哈希碼並存儲在同一個桶中,這導致遍歷整個列表,從而導致 O(n)。

在 java 8 的情況下,如果大小增長到 8 以上,則鏈表存儲桶將替換為 TreeMap,這將最壞情況下的搜索效率降低到 O(log n)。

這基本上適用於大多數編程語言中的大多數哈希表實現,因為算法本身並沒有真正改變。

如果表中不存在沖突,則只需進行一次查找,因此運行時間為 O(1)。 如果存在沖突,則必須進行多次查找,這將性能降低到 O(n)。

這取決於您選擇避免沖突的算法。 如果您的實現使用單獨的鏈接,那么最壞的情況就是每個數據元素都被散列到相同的值(例如散列函數的選擇不當)。 在這種情況下,數據查找與鏈表上的線性搜索沒有什么不同,即 O(n)。 但是,這種情況發生的概率可以忽略不計,並且查找最佳和平均情況保持不變,即 O(1)。

只有在理論情況下,當hashcode總是不同並且每個hashcode的bucket也不同時,O(1)才會存在。 否則,它的順序是不變的,即在 hashmap 的增量上,它的搜索順序保持不變。

當然,hashmap 的性能將取決於給定對象的 hashCode() 函數的質量。 但是,如果函數的實現使得沖突的可能性非常低,那么它將具有非常好的性能(這不是在所有可能的情況下嚴格為 O(1),但在大多數情況下是)。

例如,Oracle JRE 中的默認實現是使用一個隨機數(它存儲在對象實例中,因此它不會改變 - 但它也禁用了偏向鎖定,但這是另一個討論)所以碰撞的機會是非常低。

除了學術界,從實踐的角度來看,HashMaps 應該被接受為對性能的影響無關緊要(除非您的分析器另有說明。)

暫無
暫無

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

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