從兩個數組中查找相同的數字談Hashtable


問題的起因

假設數組A有n個元素,數組B有n個元素。

看到這種題的時候,我們最直觀的就是通過兩層for循環來對比每個數組中的數字。因此A數組中的每個元素都會和B數組中的每個元素對比過一次,所以總共要對比的次數是n個n相加(或者是n個m相加),也就是n2(或者為n x m).

因此我們想能不能有更快的方法呢?讓其中一個數組的查找的時間復雜度不再是O(n)就可以了。也就是我們在這個數組中查找一個數,不是通過遍歷的方式。

但是不是通過遍歷的方式能在一個數組中找到一個自己想要的數嗎?看起來必須有什么特殊的方法才行。

我們再回過頭來看數組是什么組成的:

1.下標

2.下標所代表的元素

 

我們按位置查找時,數組的速度是O(1)的,因為我們只需要通過下標就可以定位那個元素。否則需要采用遍歷的O(n)的方式。

O(n)看起來好像速度還可以,其實想想看還真沒有比它更慢的了,你都把遍歷完一遍了當然知道有沒有所需要的元素了。

這時,我們就想到,如果用下標位置來標示所需的數字呢,而元素用來標記是否出現,出現1次就加1。比如說數組A中存在元素12,就在由數組B轉換后的數組C中檢查C[12]是否等於1.

我們把數組B轉換成數組C,所需要的時間也為O(n)

那就使得對A數組的遍歷仍為O(n),但檢查數組A中的元素是否存在於數組C中則由原來的O(n)轉換為O(1).

所以總的時間復雜度為O(n+n)。

 

看起來不錯,但是問題來了,如果A中的數字很大並且很分散時,會造成數組C中大量的空間被浪費。比如說數組A是由{1,333,666}這三個元素,那么輔助數組C就需要667的大小,但是只用到了其中的3個,也就是664個空間是白浪費的。

這種直接通過數組下標尋址的方法,在數組元素非連續的情況下,會造成大量空間浪費。

 

 

那什么情況下數組中的數字會比較連續呢?還記得這張圖吧,不記得的點這里

 

也就是ASCII碼。

所以這種方式除了用來查找數字,也可以用來查找字母

代碼如下:(趕時髦用C來寫,寫的過程中感覺各種不適應啊。注:"abc"是作為指針傳遞,'abc'是作為整數傳遞,數組在作為參數時會退化為指針。)

 

#include <stdio.h>
#include <stdlib.h>


char * FindSameChar(char *stringA, char *stringB){
if(stringA == NULL || stringB == NULL ){
return '\0';
}

const int containerSize = 128;
unsigned int container[containerSize];
for (unsigned int i=0; i<containerSize; i++)
{
container[i] = 0;
}

char *result = (char *)malloc(sizeof(stringA));
char *key = stringA;
while(*key != '\0')
container[*(key++)]++;

key = stringB;
char *findResult = result;
while(*key != '\0')
{

if(container[*key] >= 1){
*findResult = *key;
findResult++;
}

key++;
}

if (result != NULL)
return result;
else
return '\0';
}

 空間復雜度為O(1),因為這里我開的輔助內存大小為128是個常數。

 

這種下標和數字緊耦合的方式使得我們這種方法受到了極大的限制,那么我們有沒有辦法“解耦”呢?

也就是說,讓1,和666不要離開這么遠,中間空了665個空位。讓他們盡可能的靠近些。這時候就需要通過一種方法來實現這種解耦,也就是說通過某個函數算出相應的位置,使得1和666之間的空格數盡量更小。

這就是hash函數。

 

取模算法

 hash技術很好的解決了直接尋址遇到的問題,但是這么做同時也引入了新的問題,通過hash函數算出來的位置可能會相同,一般就稱為發生了“碰撞"。也就是雖然我們希望通過hash函數使得1和666的位置盡量能靠近些,但是可能某些hash函數算出來的結果就是1和666位置相同。一般發生這種情況有幾種方式解決,二次hash和鏈接法。鏈接法用的是最多的,就是在同一個位置上創建一個鏈表。

當然如果碰撞多了,hash表也變成了鏈結構,會極大的影響效率。當初某個BUG就是因為采用了某種hash算法導致產生大量的碰撞,使得服務器性能下降。

 

hash表用的地方非常多,比如數據庫中就是用了哈希表,通常采用的是除法hash方式。

在用來設計哈希函數的除法散列法中,通過取k除以m的余數,來將關鍵字K映射到m個位置中的某個位置,也就是:

  hash(k) = k mod m;

順便值得一提的是,獲取余數是個經常用到的方法,比如說兩人跑步套圈,迷宮中的右手扶牆算法等。也就是說能讓一個數不停的在一個指定的范圍內打轉。

.NET中相應的實現是HashSet<T>,Dictionary<TKey,TValue>,Hashtable。

當然這幾種結構肯定有區別,下篇還要談談Equals和GetHashcode再說吧。

 

一致性Hash算法

如上面所說,最常規的方式莫過於hash取模的方式。比如集群中可用機器適量為N,那么key值為K的的數據請求很簡單的應該路由到hash(K) mod N對應的機器。的確,這種結構是簡單的,也是實用的。但是在一些高速發展的web系統中,這樣的解決方案仍有些缺陷。隨着系統訪問壓力的增長,緩存系統不 得不通過增加機器節點的方式提高集群的相應速度和數據承載量。增加機器意味着按照hash取模的方式,在增加機器節點的這一時刻,大量的緩存命不中,緩存 數據需要重新建立,甚至是進行整體的緩存數據遷移,瞬間會給DB帶來極高的系統負載,設置導致DB服務器宕機。
 

分布式緩存設計核心點:在設計分布式cache系統的時候,我們需要讓key的分布均衡,並且在增加cache server后,cache的遷移做到最少。這里提到的一致性hash算法ketama的做法是:選擇具體的機器節點不在只依賴需要緩存數據的key的hash本身了,而是機器節點本身也進行了hash運算。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM