javascript 哈希表

其實javascript的對象就是一個哈希表,爲了學習真正的數據結構,咱們仍是有必要本身從新實現一下。javascript

基本概念

哈希表(hash table )是一種根據關鍵字直接訪問內存存儲位置的數據結構,經過哈希表,數據元素的存放位置和數據元素的關鍵字之間創建起某種對應關係,創建這種對應關係的函數稱爲哈希函數html

clipboard.png

哈希表的構造方法

假設要存儲的數據元素個數是n,設置一個長度爲m(m > n)的連續存儲單元,分別以每一個數據元素的關鍵字Ki(0<=i<=n-1)爲自變量,經過哈希函數hash(Ki),把Ki映射爲內存單元的某個地址hash(Ki),並將數據元素存儲在內存單元中java

從數學的角度看,哈希函數其實是關鍵字到內存單元的映射,所以咱們但願經過哈希函數經過儘可能簡單的運算使得哈希函數計算出的花溪地址儘可能均勻的背影射到一系列的內存單元中,構造哈希函數有三個要點:(1)運算過程要儘可能簡單高效,以提升哈希表的插入和檢索效率;(2)哈希函數應該具備較好的散列型,以下降哈希衝突的機率;第三,哈希函數應具備較大的壓縮性,以節省內存。算法

如下有三種經常使用方法:編程

  • 直接地址法:以關鍵字的某個線性函數值爲哈希地址,能夠表示爲hash(K)=aK+C;優勢是不會產生衝突,缺點是空間複雜度可能會較高,適用於元素較少的狀況
  • 除留餘數法:它是由數據元素關鍵字除以某個常數所留的餘數爲哈希地址,該方法計算簡單,適用範圍廣,是常用的一種哈希函數,能夠表示爲:
hash(K=K mod C;該方法的關鍵是常數的選取,通常要求是接近或等於哈希表自己的長度,研究理論代表,該常數選素數時效果最好
  • 數字分析法:該方法是取數據元素關鍵字中某些取值較均勻的數字來做爲哈希地址的方法,這樣能夠儘可能避免衝突,可是該方法只適合於全部關鍵字已知的狀況,對於想要設計出更加通用的哈希表並不適用
  • 平方求和法:對當前字串轉化爲Unicode值,並求出這個值的平方,去平方值中間的幾位爲當前數字的hash值,具體取幾位要取決於當前哈希表的大小。
  • 分段求和法:根據當前哈希表的位數把所要插入的數值分紅若干段,把若干段進行相加,捨去調最高位結果就是這個值的哈希值。

哈希衝突的解決方案

在構造哈希表時,存在這樣的問題:對於兩個不一樣的關鍵字,經過咱們的哈希函數計算哈希地址時卻獲得了相同的哈希地址,咱們將這種現象稱爲哈希衝突數組

clipboard.png

哈希衝突主要與兩個因素有關,(1)填裝因子,填裝因子是指哈希表中已存入的數據元素個數與哈希地址空間的大小的比值,a=n/m ; a越小,衝突的可能性就越小,相反則衝突可能性較大;可是a越小空間利用率也就越小,a越大,空間利用率越高,爲了兼顧哈希衝突和存儲空間利用率,一般將a控制在0.6-0.9之間,而.net中的HashTable則直接將a的最大值定義爲0.72 (雖然微軟官方MSDN中聲明HashTable默認填裝因子爲1.0,但實際上都是0.72的倍數),(2)與所用的哈希函數有關,若是哈希函數得當,就可使哈希地址儘量的均勻分佈在哈希地址空間上,從而減小衝突的產生,但一個良好的哈希函數的得來很大程度上取決於大量的實踐,不過幸虧前人已經總結實踐了不少高效的哈希函數,能夠參考大神Lucifer文章:數據結構:HahTable: http://www.cnblogs.com/lucife...數據結構

1)開放定址法函數

Hi=(H(key) + di) MOD m i=1,2,...k(k<=m-1)
其中H(key)爲哈希函數;m爲哈希表表長;di爲增量序列。有3中增量序列:
1)線性探測再散列:di=1,2,3,...,m-1
2)二次探測再散列:di=1^2,-1^2,2^2,-2^2,....+-k^2(k<=m/2)
3)僞隨機探測再散列:di=僞隨機數序列

缺點:工具

咱們能夠看到一個現象:當表中i,i+1,i+2位置上已經填有記錄時,下一個哈希地址爲i,i+1,i+2和i+3的記錄都將填入i+3的位置,這種在處理衝突過程當中發生的兩個第一個哈希地址不一樣的記錄爭奪同一個後繼哈希地址的現象稱爲「二次彙集」,即在處理同義詞的衝突過程當中又添加了非同義詞的衝突。但另外一方面,用線性探測再散列處理衝突能夠保證作到:只要哈希表未填滿,總能找到一個不發生衝突的地址Hk。而二次探測再散列只有在哈希表長m爲形如4j+3(j爲整數)的素數時纔可能。即開放定址法會形成二次彙集的現象,對查找不利。性能

clipboard.png

2)再哈希法
Hi = RHi(key),i=1,2,...k RHi均是不一樣的哈希函數,即在同義詞產生地址衝突時計算另外一個哈希函數地址,直到不發生衝突爲止。這種方法不易產生彙集,可是增長了計算時間。

缺點:增長了計算時間。

3)鏈地址法(拉鍊法)

將全部關鍵字爲同義詞的記錄存儲在同一線性鏈表中。

優勢:

①拉鍊法處理衝突簡單,且無堆積現象,即非同義詞決不會發生衝突,所以平均查找長度較短;
②因爲拉鍊法中各鏈表上的結點空間是動態申請的,故它更適合於造表前沒法肯定表長的狀況;
③開放定址法爲減小衝突,要求裝填因子α較小,故當結點規模較大時會浪費不少空間。而拉鍊法中可取α≥1,且結點較大時,拉鍊法中增長的指針域可忽略不計,所以節省空間;
④在用拉鍊法構造的散列表中,刪除結點的操做易於實現。只要簡單地刪去鏈表上相應的結點便可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結 點的空間置爲空,不然將截斷在它以後填人散列表的同義詞結點的查找路徑。這是由於各類開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。所以在 用開放地址法處理衝突的散列表上執行刪除操做,只能在被刪結點上作刪除標記,而不能真正刪除結點

缺點:
拉鍊法的缺點是:指針須要額外的空間,故當結點規模較小時,開放定址法較爲節省空間,而若將節省的指針空間用來擴大散列表的規模,可以使裝填因子變小,這又減小了開放定址法中的衝突,從而提升平均查找速度

clipboard.png

4)創建一個公共溢出區
假設哈希函數的值域爲[0,m-1],則設向量HashTable[0...m-1]爲基本表,每一個份量存放一個記錄,另設立向量OverTable[0....v]爲溢出表。全部關鍵字和基本表中關鍵字爲同義詞的記錄,無論他們由哈希函數獲得的哈希地址是什麼,一旦發生衝突,都填入溢出表。

一個簡單哈希函數不作衝突處理的哈希表實現

// by 司徒正美
class Hash{
    constructor(){
        this.table = new Array(1024);
    }
    hash(data) {
    //就將字符串中的每一個字符的ASCLL碼值相加起來,再對數組的長度取餘
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
        console.log("Hash Value: " +data+ " -> " +total);
        return total % this.table.length;
    }
    insert(key, val){
        var pos = this.hash(key);
        this.table[pos] = val;
    }
    get(key){
        var pos = this.hash(key);
        return this.table[pos] 
    }
    show(){
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    }
    }
    var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];
    var hash = new Hash();
    for(var i = 0; i < someNames.length; ++i) {
    hash.insert(someNames[i],someNames[i]);
    }
    
    hash.show();

clipboard.png

採用的是平方取中法構建哈希函數,開放地址法線性探測法進行解決衝突。

class Hash{
    constructor(){
        this.table = new Array(1000);
    }
    hash(data) {
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
            //把字符串轉化爲字符用來求和以後進行平方運算
        var s = total * total + ""
            //保留中間2位
        var index = s.charAt( s.length/2 -1) *10 + s.charAt( s.length/2  ) * 1
        console.log("Hash Value: " +data+ " -> " +index);
        return index;
    }
    solveClash(index, value){
        var table = this.table
        //進行線性開放地址法解決衝突
        for(var i=0; index+i<1000;i++){
            if(table[index+i] == null){
                table[index+i] = value;
                break;
            }
        }
    }
    insert(key, val){
        var index = this.hash(key);
        //把取中當作哈希表中索引
        if(this.table[index] == null){
            this.table[index] = val;
        }else{
            this.solveClash(index, val);
        }
    }
    get(key){
        var pos = this.hash(key);
        return this.table[pos] 
    }
    show(){
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    }
}
var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];
var hash = new Hash();
for(var i = 0; i < someNames.length; ++i) {
    hash.insert(someNames[i],someNames[i]);
}

hash.show();

clipboard.png

幾種常見的hash函數

DJBHash

unsigned int DJBHash(char *str)    
{    
    unsigned int hash = 5381;    
     
    while (*str){    
        hash = ((hash << 5) + hash) + (*str++); /* times 33 */    
    }    
    hash &= ~(1 << 31); /* strip the highest bit */    
    return hash;    
}

javascript版

function DJBHash(str)    {    
    var hash = 5381;   
    var len = str.length , i = 0
     
    while (len--){    
        hash = (hash << 5) + hash + str.charCodeAt(i++); /* times 33 */    
    }    
    hash &= ~(1 << 31); /* strip the highest bit */    
    return hash;    
}

JS

Justin Sobel寫的一個位操做的哈希函數。
原版

public long JSHash(String str)  
   {  
      long hash = 1315423911;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash ^= ((hash << 5) + str.charAt(i) + (hash >> 2));  
      }  
      return hash;  
   }

javascript版

function JSHash(str)  {  
      var hash = 1315423911;  
      for(var i = 0; i < str.length; i++)  {  
         hash ^= ((hash << 5) + str.charCodeAt(i) + (hash >> 2));  
      }  
      return hash;  
}

PJW

該散列算法是基於貝爾實驗室的彼得J溫伯格的的研究。在Compilers一書中(原則,技術和工具),建議採用這個算法的散列函數的哈希方法。

public long PJWHash(String str)  
   {  
      long BitsInUnsignedInt = (long)(4 * 8);  
      long ThreeQuarters     = (long)((BitsInUnsignedInt  * 3) / 4);  
      long OneEighth         = (long)(BitsInUnsignedInt / 8);  
      long HighBits          = (long)(0xFFFFFFFF) << (BitsInUnsignedInt - OneEighth);  
      long hash              = 0;  
      long test              = 0;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = (hash << OneEighth) + str.charAt(i);  
         if((test = hash & HighBits)  != 0)  
         {  
            hash = (( hash ^ (test >> ThreeQuarters)) & (~HighBits));  
         }  
      }  
      return hash;  
   }

javascript版

function PJWHash( str)  {  
      var BitsInUnsignedInt = 4 * 8;  
      var ThreeQuarters     =  (BitsInUnsignedInt  * 3) / 4;  
      var OneEighth         = (BitsInUnsignedInt / 8);  
      var HighBits          = (0xFFFFFFFF) << (BitsInUnsignedInt - OneEighth);  
      var hash              = 0;  
      var test              = 0;  
      for(var i = 0; i < str.length; i++)  {  
         hash = (hash << OneEighth) + str.charCodeAt(i);  
         if((test = hash & HighBits)  != 0)  
         {  
            hash = (( hash ^ (test >> ThreeQuarters)) & (~HighBits));  
         }  
      }  
      return hash;  
}

若是將上面的哈表的hash函數改爲這個,打印以下:

clipboard.png

性能會大幅下隆,由於這讓咱們的table數組表得很是龐大。

ELF

和PJW很類似,在Unix系統中使用的較多。

public long ELFHash(String str)  
   {  
      long hash = 0;  
      long x    = 0;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = (hash << 4) + str.charAt(i);  
         if((x = hash & 0xF0000000L) != 0)  
         {  
            hash ^= (x >> 24);  
         }  
         hash &= ~x;  
      }  
      return hash;  
   }

BKDR

這個算法來自Brian Kernighan 和 Dennis Ritchie的 The C Programming Language。這是一個很簡單的哈希算法,使用了一系列奇怪的數字,形式如31,3131,31...31,看上去和DJB算法很類似。

public long BKDRHash(String str)  
   {  
      long seed = 131; // 31 131 1313 13131 131313 etc..  
      long hash = 0;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = (hash * seed) + str.charAt(i);  
      }  
      return hash;  
   }

SDBM

這個算法在開源的SDBM中使用,彷佛對不少不一樣類型的數據都能獲得不錯的分佈。

public long SDBMHash(String str)  
   {  
      long hash = 0;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = str.charAt(i) + (hash << 6) + (hash << 16) - hash;  
      }  
      return hash;  
   }

DJB

這個算法是Daniel J.Bernstein 教授發明的,是目前公佈的最有效的哈希函數。

public long DJBHash(String str)  
   {  
      long hash = 5381;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = ((hash << 5) + hash) + str.charAt(i);  
      }  
      return hash;  
   }

DEK

由偉大的Knuth在《編程的藝術 第三卷》的第六章排序和搜索中給出。

public long DEKHash(String str)  
   {  
      long hash = str.length();  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = ((hash << 5) ^ (hash >> 27)) ^ str.charAt(i);  
      }  
      return hash;  
   }

AP

由Arash Partow貢獻的一個哈希函數,繼承了上面以旋轉覺得和加操做

public long APHash(String str)  
{  
      long hash = 0xAAAAAAAA;  
      for(int i = 0; i < str.length(); i++)  
      {  
         if ((i & 1) == 0)  
         {  
            hash ^= ((hash << 7) ^ str.charAt(i) * (hash >> 3));  
         }  
         else  
         {  
            hash ^= (~((hash << 11) + str.charAt(i) ^ (hash >> 5)));  
         }  
      }  
      return hash;  
   }

clipboard.png
其中數據1爲100000個字母和數字組成的隨機串哈希衝突個數。數據2爲100000個有意義的英文句子哈希衝突個數。數據3爲數據1的哈希值與 1000003(大素數)求模後存儲到線性表中衝突的個數。數據4爲數據1的哈希值與10000019(更大素數)求模後存儲到線性表中衝突的個數。

通過比較,得出以上平均得分。平均數爲平方平均數。能夠發現,BKDRHash不管是在實際效果仍是編碼實現中,效果都是最突出的。APHash也是較爲優秀的算法。DJBHash,JSHash,RSHash與SDBMHash各有千秋。PJWHash與ELFHash效果最差,但得分類似,其算法本質是類似的。

相關文章
相關標籤/搜索