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