❶ 散列查找堆積現象為什麼不影響存儲效率
一般的線性表、樹中,記錄在結構中的相對位置是隨機的即和記錄的關鍵字之間不存在確定的關系,在結構中查找記錄時需進行一系列和關鍵字的比較。這一類查找方法建立在「比較」的基礎上,查找的效率與比較次數密切相關。理想的情況是能直接找到需要的記錄,因此必須在記錄的存儲位置和它的關鍵字之間建立一確定的對應關系f,使每個關鍵字和結構中一個唯一的存儲位置相對應。因而查找時,只需根據這個對應關系f找到給定值K的像f(K)。若結構中存在關鍵字和K相等的記錄,則必定在f(K)的存儲位置上,由此不需要進行比較便可直接取得所查記錄。在此,稱這個對應關系f為哈希函數,按這個思想建立的表為哈希表(又稱為雜湊法或散列表)。
哈希表不可避免沖突(collision)現象:對不同的關鍵字可能得到同一哈希地址 即key1≠key2,而f(key1)=f(key2)。具有相同函數值的關鍵字對該哈希函數來說稱為同義詞(synonym)。 因此,在建造哈希表時不僅要設定一個好的哈希函數,而且要設定一種處理沖突的方法。可如下描述哈希表:根據設定的哈希函數H(key)和所選中的處理沖突的方法,將一組關鍵字映象到一個有限的、地址連續的地址集(區間)上並以關鍵字在地址集中的「象」作為相應記錄在表中的存儲位置,這種表被稱為哈希表。
註:這個函數f(key)為哈希函數。(注意:這個函數並不一定是數學函數) 哈希函數是一個映象,即:將關鍵字的集合映射到某個地址集合上,它的設置很靈活,只要這個地址集合的大小不超出允許范圍即可。 現實中哈希函數是需要構造的,並且構造的好才能使用的好。
對於動態查找表而言,1) 表長不確定;2)在設計查找表時,只知道關鍵字所屬范圍,而不知道確切的關鍵字。因此,一般情況需建立一個函數關系,以f(key)作為關鍵字為key的錄在表中的位置,通常稱這個函數f(key)為哈希函數。(注意:這個函數並不一定是數學函數)
哈希函數是一個映象,即:將關鍵字的集合映射到某個地址集合上,它的設置很靈活,只要這個地址集合的大小不超出允許范圍即可。
現實中哈希函數是需要構造的,並且構造的好才能使用的好。
用途:加密,解決沖突問題。。。。
用途很廣,比特精靈中就使用了哈希函數,你可 以自己看看。
具體可以學習一下數據結構和演算法的書。
字元串哈希函數(著名的ELFhash演算法)
int ELFhash(char *key)
{ unsigned long h=0;
while(*key)
{ h=(h<<4)+*key++;
unsigned long g=h&0Xf0000000L;
if(g) h^=g>>24;
h&=~g;
}
return h%MOD;
}
❷ 隊列和散列表跟數據的存儲結構有關還是無關
隊列和散列表的實現方式和存儲結構有關,不同的存儲結構會影響它們的實現方式和性能表現。
隊列是一種先進先出(FIFO)的數據結構,主要有兩種實現方式:數組和鏈表。用數組實現的隊列叫做順序隊列,用鏈表實現的隊列虛大叫做鏈式隊列。對於順序隊列,隊列元素是順序存儲在連續的內存空間中的,隊列的頭部和尾部分別指向隊列的第一個元素和最後一個元素。對於鏈式隊列,隊列元素是通過指針連接起來的,隊列的頭部指向鏈表的頭結點,尾部指向鏈表的尾結點。
散列表是一種根據關鍵字直接訪問數據的數據結構,主要有兩種實現方式:開放地址法和鏈表法。開放地差橘豎址法使用數組來存儲數據,通過哈希函數將數據的關鍵字映射到數組的索引上,如果這個位置已經有數據了,就根據一定的規則在數組中尋找下一個空閑位置。鏈表法使用鏈表來存儲數據,如果哈希函數將多個數據的關鍵字映射到了同一個索引上,那麼就將它們存儲在同一個鏈表伍察中。
因此,隊列和散列表的實現方式和性能表現與數據的存儲結構密切相關。選擇合適的實現方式和存儲結構可以提高數據結構的性能和效率。
❸ 第十九節-散列表(中)
散列函數的設計的好壞,決定了散列沖突的概率大小,也直接決定了散列表的性能。
過於復雜的散列函數,會消耗過多的計算時間,也就間接影響到散列表的性能。
這樣才能避免或者最小化散列沖突。即便是出現沖突,散列到每個槽的數據也會分布得比較均勻,不會出現某個槽內的數據特別多的情況。
這些因素有關鍵字的長度、特點、分布、還有散列表的大小等。
通過分析傳入散列函數的 key 的特徵規律,設計相應的專用散列函數。比如處理使用散列函數處理手悉派機號碼,考慮到手機號碼前幾位重復性很大,後幾位比較隨機,我們就可以取手機號碼後幾位作為散列值。
比如對字元串進行散列,可以將字元串中每個字母的 ASCII 值進行「進位」相加,然後再跟散列表的大小求余、取模,作為散列值。比如,對因為單詞 nice 進行散列:
hash("nice")=(("n" - "a") * 26*26*26 + ("i" - "a")*26*26 + ("c" - "a")*26+ ("e"-"a")) / 78978
裝載睜搏賀因子的定義為 散列表的裝載因子 = 填入表中的元素個數 / 散列表的長度 ,根據這個公式,可以推出,裝載因子越大且散列表的長度不變,則散列表中的元素越多,空閑位置越少,散列沖突的概率會變大。不僅插入數據的過程要多次定址或者拉很長的鏈,查找的過程也會因此變得很慢。
對沒有頻繁插入和刪除的靜態數據集合來說,我們很容易根據數據的特點、分布等,設計出完美的、極少沖突的散列函數。
對於動態散列表來說,數據集合頻繁變動,負載因子可能隨著時間不斷變大。這時就要動態擴容。
動態擴容的過程,簡單來說,就是申請一個新的散列表,然後把原來的數據搬運到新的散列表中,但是不是簡單的搬運,而是每個元素都要根據新的散列表重新存儲位置。
這個過程,運用平攤分析法,每個插入操作的時間復雜度仍是 O(1)。
擴展一下,當散列表的裝載因子小於某個閾值時,我們也可以依樣畫葫蘆,進行動態縮容。
大部分情況下,動態擴容的散列表插入一個數據都很快,但在特殊情況下,裝載因子達到擴容的閾值,此時,再插入數據,就會觸發漫長的銀喚擴容過程,在特定的場合,這樣漫長的等待過程是不可接受的。
舉個直觀的例子,假設原來散列表有 1GB 的數據,現在進行擴容,就需要對這 1GB 的數據進行再散列,這個擴容的時間,看起來就很耗時。
在這種情況下,「一次性」擴容的機制就不適合了。此時,我們可以將擴容操作穿插在插入操作的過程中,分批完成。當裝載因子達到擴容的閾值後,我們只申請新空間,但不立即將老的數據全部搬移到新的散列表中。
當有新的數據插入時,我們就將新數據插入新散列表中,同時從舊散列表中拿出一個數據放入到新的散列表。每次插入一個數據到散列表,我們都重復以上的過程。經過多次操作後,老的散列表中的數據就一點一點全部搬移到新的散列表中了。沒有了一次性全部搬移,插入操作就不會出現一次很慢的情況了。
這期間的插入操作,可以先從新的散列表中查,查不到再去舊的散列表中查。甚至,可以在每次查詢操作中也穿插一條數據搬移。
上節講了兩種主要的散列沖突的解決辦法,開放定址法和鏈表法。這兩種沖突的解決辦法在實際的軟體開發中很常用。比如, Java 中的 LinkedHashMap 就採用了鏈表法解決沖突,ThreadLocalMap 是通過線性探測的開放定址法來解決沖突。
這種實現方式,散列表中的數據都存放在數組中,可以有效利用 CPU 的緩存加快查詢速度。序列化過程比較簡單。鏈表法包含指針,序列化就比較麻煩。
缺點是,刪除數據比較麻煩,需要使用特殊標記。所有數據都存在一個數組中,沖突的代價很大。
總結,數據量小、裝載因子小,適合開放定址法。這也是 ThreadLocalMap 採用開放定址法解決散列沖突的原因。
鏈表法的優點是內存利用率高。鏈表節點可以在需要的時候再創建,並不需要想開放定址法那樣實現申請好。
鏈表法對沖突的容忍性更高,裝在因子可以大於 1,甚至再散列沖突很平均的情況下,即使裝載因子達到 10,查找效率也不會大幅度衰退。更進一步,對鏈表法進行改造,使用紅黑樹或者跳錶解決散列沖突,那即使是極端情況下,所有數據都存放在一個槽內,查詢時間也是衰退到 O(logn) 的數量級。
缺點是,需要維持鏈表的指針引用,若是存放小對象,那麼指針佔用的內存就會對比起來挺多,而且鏈表法也容易造成內存碎片。
總結起來就是,基於鏈表法的散列沖突方法適合存儲大對象、大數據量的散列表,而且比起開放定址法,它更加靈活,支持更多的優化策略。
分析 HashMap
HashMap 的初始大小是 16,當然這個默認值是可以設置的,如果事先知道大概的數據量多少,就可以依此設置合適的初始容量。
最大裝載因子默認是 0.75。當 HashMap 中元素個數超過 0.75 * capacity 時,就會啟動擴容,每次擴容後的容量是原來的兩倍。
關於 0.75 這個數字的由來,可以查看這篇文章 https://blog.csdn.net/reliveIT/article/details/82960063 。
HashMap 底層採用鏈表法解決沖突。在 JDK 1.8 之前,沖突的元素插入鏈表首端,在 JDK 1.8 之後,插入尾端。
另外,在 JDK 1.8 之後,當鏈表長度超過 8 時,會啟動樹化,當樹中元素少於 6 個時,會退化回鏈表。
HashMap 的二次哈希,使用的是除留余數法。
因為 A % B = A & (B - 1),所以,(h ^ (h >>> 16)) & (capitity -1) = (h ^ (h >>> 16)) % capitity。
設計這樣的散列表應該從三個方面考慮
❹ 數據結構與演算法之美筆記——散列表(上)
摘要:
我們已經知道隨機訪問數組元素時間復雜度只有 ,效率極高,當我們想利用數組的這個特性時就需要將元素下標與存儲信息對應。例如,一個商店只有四件商品,依次編號 0 至 3,這樣就可以將四件商品信息按照編號對應下標的方式存儲到數組中,依據編號就可以快速從數組中找到相應商品信息。
如果一段時間之後,商店盈利並且重新進貨 100 件商品,商家想對大量商品在編號上區分類別,這時陵鋒頌候需要使用類別編號加順序編號的方式標識每件商品,這種編號變得復雜,並不能直接對應數組下標,此時的商品編號又該如何對應數組下標以實現快速查找商品的功能?這時候我們可以將類別編號去除之後按照順序編號對應數組下標,同樣也能享受數組高效率隨機訪問的福利。這個例子中,商品編號稱為「 鍵 」或「 關鍵字 」,將鍵轉化為數組對應下標的方法就是「 散列函數 」或「 Hash 函數 」,由散列函數生成的值叫做「 散列值 」或「 Hash 值 」,而這樣的數組就是散列表。
從散列表的原理來看,數據通過散列函數計算得到散列值是關鍵,這個步驟中散列函數又是其中的核心,一個散列函數需要遵守以下三個原則。
因為散列函數生成的散列值對應數組下標,而數組下標就是非負整數,所以需要滿足第一個原則;兩個相等的數據經過散列演算法得到的散列值肯定相等,否則利用散列值在散列表中查找數據就無從談起;至於第三個原則雖然在情理之中,卻不那麼容易做到,即使是被廣泛運用的散列演算法也會出現散列值沖突的情況,導致無法滿足第三個原則。
散列函數作為散列表的核心部分,尺鄭必然不能拖散列表的執行效率後腿,畢竟散列表的查詢、插入和刪除操作都需要經過基告散列函數,所以散列函數不能太復雜,執行效率不能太低。由於散列函數不可避免地都會出現散列沖突情況,散列函數要盡量降低散列沖突,使散列值能夠均勻地分布在散列表中。
解決散列沖突主要有「 開放定址 」(open addressing)和「 鏈表法 」(chaining)兩類方法。
開放定址法是指插入操作時,當生成的散列值對應槽位已經被其他數據佔用,就探測空閑位置供插入使用,其中探測方法又分為「 線性探測 」(Linear Probing)、「 二次探測 」(Quadratic Probing)和「 雙重散列 」(Double hashing)三種。
線性探測是其中較為簡單的一種,這種探測方式是當遇到散列沖突的情況就順序查找(查找到數組尾部時轉向數組頭部繼續查找),直到查找到空槽將數據插入。當進行查找操作時,也是同樣的操作,利用散列值從散列表中取出對應元素,與目標數據比對,如果不相等就繼續順序查找,直到查找到對應元素或遇到空槽為止,最壞情況下查找操作的時間復雜度可能會下降為 。
散列表除了支持插入和查找操作外,當然也支持刪除操作,不過並不能將需刪除的元素置為空。如果刪除操作是將元素置為空的話,查找操作遇到空槽就會結束,存儲在被刪除元素之後的數據就可能無法正確查找到,這時的刪除操作應該使用標記的方式,而不是使用將元素置空,當查找到被標識已刪除的元素將繼續查找,而不是就此停止。
線性探測是一次一個元素的探測,二次探測就是使用都是線性探測的二次方步長探測。例如線性探測是 ,那二次探測對應的就是 。
雙重探測是當第一個散列函數沖突時使用第二個散列函數運算散列值,利用這種方式探測。例如,當 沖突時,就使用 計算散列值,如果再沖突就使用 計算散列值,依此類推。
關於散列表的空位多少使用「 裝載因子 」(load factor)表示,裝載因子滿足數學關系 ,也就是說裝載因子越大,散列表的空閑空間越小,散列沖突的可能性也就越大,一般我們會保持散列表有一定比例的空閑空間。
為了保持散列表一定比例的空閑空間,在裝載因子到達一定閾值時需要對散列表數據進行搬移,但散列表搬移比較耗時。你可以試想下這樣的步驟,在申請一個新的更大的散列表空間後,需要將舊散列表的數據重新通過散列函數生成散列值,再存儲到新散列表中,想想都覺得麻煩。
散列表搬移的操作肯定會降低散列表的操作效率,那能不能對這一過程進行改進?其實可以將低效的擴容操作分攤至插入操作,當裝載因子達到閾值時不一次性進行散列表搬移,而是在每次插入操作時將一個舊散列表數據搬移至新散列表,這樣搬移操作的執行效率得到了提高,插入操作的時間復雜度也依然能保持 的高效。當新舊兩個散列表同時存在時查詢操作就要略作修改,需先在新散列表中查詢,如果沒有查找到目標數據再到舊散列表中查找。
當然,如果你對內存有更高效的利用要求,可以在裝載因子降低至某一閾值時對散列表進行縮容處理。
除了開放定址之外,還可以使用鏈表法解決散列沖突的問題。散列值對應的槽位並不直接存儲數據,而是將數據存儲在槽位對應的鏈表上,當進行查找操作時,根據散列函數計算的散列值找到對應槽位,再在槽位對應的鏈表上查找對應數據。
鏈表法操作的時間復雜度與散列表槽位和數據在槽位上的分布情況有關,假設有 n 個數據均勻分布在 m 個槽位的散列表上,那鏈表法的時間復雜度為 。鏈表法可以不用像開放定址一樣關心裝載因子,但需要注意散列函數對散列值的計算,使鏈表結點能夠盡可能均勻地分布在散列表槽位上,避免散列表退化為鏈表。有時黑客甚至會精心製造數據,利用散列函數製造散列沖突,使數據集中某些槽位上,造成散列表性能的極度退化。
面對這樣的惡意行為散列表只能坐以待斃嗎?其實不然,當槽位上的鏈表過長時,可以將其改造成之前學習過的跳錶等,鏈表改造為跳錶後查詢的時間復雜度也只是退化為 ,依然是可以接受的范圍。
鏈表法在存儲利用上比開放定址更加高效,不用提前申請存儲空間,當有新數據時申請一個新的結點就行。而且鏈表法對裝載因子也不那麼敏感,裝載因子的增高也只是意味著槽位對應的鏈表更長而已,鏈表增長也有將鏈表改造為跳錶等結構的應對策略,所以鏈表法在裝載因子超過 1 的情況下都可保持高效。
開放定址不存在像鏈表法一樣有鏈表過長而導致效率降低的煩惱,不過裝載因子是開放定址的晴雨表,裝載因子過高會造成散列沖突機率的上升,開放定址就需要不斷探測空閑位置,演算法的執行成本會不斷被提高。而且在刪除操作時只能將數據先標記為刪除,對於頻繁增刪的數據效率會受到影響。
當然也可以在這種風險出現前進行散列表的動態擴容,不過這樣就會出現大量空閑的存儲空間,導致存儲的利用效率過低,這種現象在數據量越大的情況下越明顯。所以開放定址比較適用於數據量較小的情況。
鏈表法對於散列沖突的處理更加靈活,同時對存儲空間的利用效率也更高,但鏈表結點除了存儲數據外還需要存儲指針,如果存儲數據較小指針佔用的存儲甚至會導致整體存儲翻倍的情況,但存儲數據較大時指針佔用的存儲也就可以忽略不計,所以鏈表法較適合存儲數據對象較大,但頻繁的增刪操作不會對鏈表法造成明顯的影響。因為這樣的特點,鏈表法更加適合大數據量,或者數據對象較大的時候,如果數據操作頻繁,那鏈表法更是不二之選。
散列表由數組擴展而來,使用散列函數將鍵計算為散列值,散列值對應數據存儲的數組下標。雖然散列表的執行效率較高,但會有散列沖突的問題,可以通過開放定址法和鏈表法解決此問題。
開放定址存儲利用效率較低,適用數據量較小並且增刪不頻繁的情況,如果數據量較大,增刪頻繁的情況更加適用鏈表法,相對之下鏈表法更加普適。
❺ 用哈希(散列)方法處理沖突(碰撞)可能出現堆積(聚集)現象,為什麼「存儲效率」會受堆積現象直接影響
因為可能數據會重復
簡單的例子,網路網盤檢查你上傳的文件是否違規,就會將你的文件哈希值與違規文件的進行比對,假如你的文件或數據與違規庫中的文件的哈希值沖突(碰撞),那麼激動文件就會被誤封。
❻ r為什麼redis散列表內存小
Redis散列表內存小是因為Redis散列表是使用哈希表實現的,而哈希表是一種用空間換時間的數據結構。在哈希表中,元素的存儲位置是通過將元素的鍵轉換成哈希值,並對哈希值取模得到的索引位置進行匹配,因此哈希表可以實現O(1)時間復雜度的查找、插入和刪除操作。在Redis中,通過對哈希表進行優化,可以更加高效地利用內存空間,從而實現散列表內存小的優勢。
具體來說,Redis哈希表的優化包括:
1. 壓縮列表:當散列表中的元素數量較少時,Redis會使用壓縮列表(ziplist)來存儲散列表埋笑橘節點,這樣可以減少內存的使用。
2. 散列表節點:Redis使用指針數組來存儲哈彎團希表節點,而不是使用指針結構體升咐來存儲,這樣可以節省一些內存空間。
3. 漸進式rehash:當散列表需要擴容時,Redis會採用漸進式rehash的方式進行,將哈希表的鍵值對從舊表復制到新表上,並逐漸釋放舊表佔用的內存,這樣可以在不阻塞讀寫操作的情況下完成散列表的擴容。
綜上所述,Redis散列表通過利用哈希表的優勢和進行優化,可以實現較小的內存佔用,從而達到高效的存儲和操作數據的目的。
❼ 散列表和線性存儲的區別,在線等!!急!!
(慎派猜1)順序存儲是把邏輯上相鄰的節點存儲在一羨高組連續的存儲單元寬型之中,節點間的邏輯關系由存儲單元地址關系隱含表示;
散列存儲是根據節點的直確定節點的存儲地址。
(2)順序存儲優點是節省存儲空間,缺點是不便於修改;
散列存儲的優點是查找速度快,但存儲空間利用效率低。
(3)順序存儲處理簡單,散列存儲處理方法復雜。
❽ 數據結構 散列表
散列表是一種數據結構,通過散列函數(也就是 hash 函數)將輸入映射到一個數字,一般用映射出的數字作為存儲位置的索引。數組在查找時效率很高,但是插入和刪除卻很低。而鏈表剛好反過來。設計合理的散列函數可以集成鏈表和數組的優點,在查找、插入、刪除時實現 O(1) 的效率。散列表的存儲結構使用的也是數組加鏈表。執行效率對比可以看下圖 1.3:
散列表的主要特點:1.將輸入映射到數字2.不同的輸入產生不同的輸出3.相同的輸入產生相同的輸出4. 當填裝因子超過閾值時,能自動擴展。填裝因子 = 散列表包含的元素數 / 位置總數,當填裝因子 =1,即散列表滿的時候,就需要調整散列表的長度,自動擴展的方式是:申請一塊舊存儲容量 X 擴容系數的新內存地址,然後把原內存地址的值通過其中的 key 再次使用 hash 函數計算存儲位置,拷貝到新申請的地址。5. 值呈均勻分布。這里的均勻指水平方向的,即數組維度的。如果多個值被映射到同一個位置,就產生了沖突,需要用鏈表來存儲多個沖突的鍵值。極端情況是極限沖突,這與一開始就將所有元素存儲到一個鏈表中一樣。這時候查找性能將變為最差的 O(n),如果水平方向填充因子很小,但某些節點下的鏈表又很長,那值的均勻性就比較差。