十3、哈希表

哈希表是一種數據結構,它能夠提供快速的插入操做和查找操做,不論哈希表中有多少數據,插入和刪除只須要接近常量的時間,即O(1)的時間級。java

哈希表的缺點:它是基於數組的,數組建立後難於擴展。某些哈希表被基本填滿時,性能降低得很是嚴重,因此程序員必需要清楚表中將要存儲多少數據(或者準備好按期地把數據轉移到更大的哈希表中,這是個費時的過程。)程序員

 

假設想在內存中存儲50000個英文單詞。起初可能考慮每一個單詞佔據一個數組單元,那麼數組大小是50000,同時可使用數組下標存取單詞。這樣,存取確實很快。可是數組下標和單詞有什麼關係呢?例如給出一個單詞cats,怎麼能找到它的數組下標呢?算法

把單詞轉化爲數組下標數據庫

把數字相加c=3,a=1,t=20,s=19,3+1+20+19=43,那麼單詞cats存儲在數組下標爲43的單元中。編程

第一個單詞a的編碼是0+0+0+0+0+0+0+0+0+1=1,字典最後一個可能的單詞是zzzzzzzzzz,編碼是26+26+26+26+26+26+26+26+26+26=260。所以,單詞編碼的範圍是從1到260。不幸的是,字典中有50000個單詞,因此沒有足夠的數組下標數來索引那麼多的單詞。每一個數組數據項大概要存儲192個單詞。用一個單詞佔用一個數組單元的方案會發生問題。也許能夠考慮每一個數組數據項包含一個子數組或鏈表。不幸的是,這個辦法嚴重下降了存取速度。存取數據項確實很快,可是要在192個單詞中找到其中一個,速度就很慢。、數組

冪的連乘cats=3*273 + 1*272 + 20*271 + 19*270=60337。若是是十位字符串,數字值會很是大。若是爲每一個可能的單詞分配一個數組單元,無論這個單詞是否是真正的英語單詞。從aaaaaaaaaa到zzzzzzzzzz,這些單元只有一小部分存放了存在的英語單詞,而大多數單元是空的。在內存中的數組也根本不可能有這麼多的單元。數據結構

第一種方案(數字相加求和)產生的數組下標太少。第二種方案(與27的冪相乘並求和)產生的數組下標又太多。less

哈希化dom

如今須要一種壓縮方法,把數位冪的連乘系統中獲得的巨大的整數範圍壓縮到可接受的數組範圍中。函數

假如只有50000個單詞,須要容量爲100000的數組(多一倍的空間效率更高)。有個簡單的方法,使用取餘操做符,把0到7000000000000的範圍壓縮爲0到100000。

arrayIndex = hugeNumber % arraySize

這就是一種哈希函數。它把一個大範圍的數字哈希(轉化)成一個小範圍的數字。這個小的範圍對應着數組的下標。使用哈希函數向數組插入數據後,這個數組就稱爲哈希表。

 

衝突

把巨大的數字空間壓縮成較小的數字空間,必然要付出代價,即不能保證,每一個單詞都映射到數組的空白單元。以前設置了數組的大小是須要存儲的數據量的兩倍。所以,可能一半的單元是空的。當衝突發生時,一個方法是經過系統的方法找到數組的一個空位,並把這個單詞填入,而再也不用哈希函數獲得的數組下標。這個方法叫作開放地址法。例如,若是cats哈希化的結果是5421,但它的位置已經被parsnip佔用,那麼可能會考慮把cats放在5422的位置上。第二種方法是建立一個存放單詞鏈表的數組,數組內不直接存儲單詞。這樣,當發生衝突時,新的數據項直接接到這個數組下標所指的鏈表中。這種方法叫作鏈地址法。

 

開放地址法

在開放地址法中,若數據不能直接放在由哈希函數計算出來的數組下標所指的單元時,就要尋找數組的其餘位置。下面要探索開放地址法的三種方法,它們在找下一個空白單元時使用的方法不一樣。這三種方法分別是線性探測,二次探測和再哈希法。

線性探測

在線性探測中,線性地查找空白單元。若是5421是要插入數據的位置,它已經被佔用了,那麼就使用5422,而後是5423,依次類推,數組下標一直遞增,直到找到空位。

彙集

當哈希表變得愈來愈滿時,彙集變得愈來愈嚴重。這致使產生很是長的探測長度。意味着存取序列最後的單元會很是耗時。

二次探測

在開放地址法的線性探測中會發生彙集。一旦彙集造成,它會變得愈來愈大。那些哈希化後的落在彙集範圍內的數據項,都要一步一步移動,而且插在彙集的最後,所以使彙集變得更大。彙集越大,它增大得也越快。二次探測是防止彙集產生的一種嘗試。思想是探測相隔較遠的單元,而不是和原始位置相鄰的單元。步驟是步數的平方。在線性探測中,若是哈希函數計算的原始下標是x,線性探測就是x+1,x+2,x+3,依次類推。而在二次探測中,探測的過程是x+1,x+4,x+9,x+16,x+25,依次類推。

二次探測的問題

二次探測消除了在線性探測中產生的彙集問題,這種彙集問題叫作原始彙集。然而,二次探測產生了另一種,更細的彙集問題。之因此會發生,是由於全部映射到同一個位置的關鍵字在尋找空位時,探測的單元都是同樣的。好比將184,302,420和544依次插入到表中,它們都映射到7。那麼302須要以一爲步長的探測,420須要以四爲步長的探測,544須要以九爲步長的探測。只要有一項,其關鍵字映射到7,就須要更長步長的探測。這個現象叫作二次彙集。

// to run this program: C:>java HashTableApp import java.io.*; class DataItem { // (could have more data) private int iData; // data item (key) public DataItem(int ii) // constructor { iData = ii; } public int getKey() { return iData; } } class HashTable { private DataItem[] hashArray; // array holds hash table private int arraySize; private DataItem nonItem; // for deleted items public HashTable(int size) // constructor  { arraySize = size; hashArray = new DataItem[arraySize]; nonItem = new DataItem(-1); // deleted item key is -1  } public void displayTable() { System.out.print("Table: "); for(int j=0; j<arraySize; j++) { if(hashArray[j] != null) System.out.print(hashArray[j].getKey() + " "); else System.out.print("** "); } System.out.println(""); } public int hashFunc(int key) { return key % arraySize; // hash function  } public void insert(DataItem item) // insert a DataItem // (assumes table not full)  { int key = item.getKey(); // extract key int hashVal = hashFunc(key); // hash the key // until empty cell or -1, while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) { ++hashVal; //線性探測法 // go to next cell //hashVal+=i*i 二次探測法 hashVal %= arraySize; // wraparound if necessary  } hashArray[hashVal] = item; // insert item  } public DataItem delete(int key) // delete a DataItem  { int hashVal = hashFunc(key); // hash the key while(hashArray[hashVal] != null) // until empty cell, { // found the key? if(hashArray[hashVal].getKey() == key) { DataItem temp = hashArray[hashVal]; // save item hashArray[hashVal] = nonItem; // delete item return temp; // return item  } ++hashVal; //線性探測法 // go to next cell //hashVal+=i*i 二次探測法 hashVal %= arraySize; // wraparound if necessary  } return null; // can't find item  } public DataItem find(int key) // find item with key  { int hashVal = hashFunc(key); // hash the key while(hashArray[hashVal] != null) // until empty cell, { // found the key? if(hashArray[hashVal].getKey() == key) return hashArray[hashVal]; // yes, return item ++hashVal; //線性探測法 // go to next cell //hashVal+=i*i 二次探測法 hashVal %= arraySize; // wraparound if necessary  } return null; // can't find item  } } class HashTableApp { public static void main(String[] args) throws IOException { DataItem aDataItem; int aKey, size, n, keysPerCell; // get sizes System.out.print("Enter size of hash table: "); size = getInt(); System.out.print("Enter initial number of items: "); n = getInt(); keysPerCell = 10; // make table HashTable theHashTable = new HashTable(size); for(int j=0; j<n; j++) // insert data  { aKey = (int)(java.lang.Math.random() * keysPerCell * size); aDataItem = new DataItem(aKey); theHashTable.insert(aDataItem); } while(true) // interact with user  { System.out.print("Enter first letter of "); System.out.print("show, insert, delete, or find: "); char choice = getChar(); switch(choice) { case 's': theHashTable.displayTable(); break; case 'i': System.out.print("Enter key value to insert: "); aKey = getInt(); aDataItem = new DataItem(aKey); theHashTable.insert(aDataItem); break; case 'd': System.out.print("Enter key value to delete: "); aKey = getInt(); theHashTable.delete(aKey); break; case 'f': System.out.print("Enter key value to find: "); aKey = getInt(); aDataItem = theHashTable.find(aKey); if(aDataItem != null) { System.out.println("Found " + aKey); } else System.out.println("Could not find " + aKey); break; default: System.out.print("Invalid entry\n"); } } } public static String getString() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static char getChar() throws IOException { String s = getString(); return s.charAt(0); } public static int getInt() throws IOException { String s = getString(); return Integer.parseInt(s); } }

 

 

再哈希法

爲了消除原始彙集和二次彙集,可使用另外的一個方法:再哈希法。二次彙集產生的緣由是,二次探測的算法產生的探測序列步長老是固定的:1,4,9,16,依次類推。如今須要的一種方法是產生一種依賴關鍵字的探測序列,而不是每一個關鍵字都同樣。那麼不一樣的關鍵字即便映射到相同的數組下標,也可使用不一樣的探測序列。方法是把關鍵字用不一樣的哈希函數在作一遍哈希化,用這個結果做爲步長,對指定的關鍵字,步長在整個探測中是不變的,不過不一樣的關鍵字使用不一樣的步長。

第二個哈希函數必須具有以下特色:和第一個哈希函數不一樣,不能輸出爲0(算法會陷入死循環)

stepSize = constant - (key % constant)會工做的比較好。

// to run this program: C:>java HashDoubleApp import java.io.*; class DataItem { // (could have more items) private int iData; // data item (key) public DataItem(int ii) // constructor { iData = ii; } public int getKey() { return iData; } } class HashTable { private DataItem[] hashArray; // array is the hash table private int arraySize; private DataItem nonItem; // for deleted items  HashTable(int size) // constructor  { arraySize = size; hashArray = new DataItem[arraySize]; nonItem = new DataItem(-1); } public void displayTable() { System.out.print("Table: "); for(int j=0; j<arraySize; j++) { if(hashArray[j] != null) System.out.print(hashArray[j].getKey()+ " "); else System.out.print("** "); } System.out.println(""); } public int hashFunc1(int key) { return key % arraySize; } public int hashFunc2(int key) { // non-zero, less than array size, different from hF1 // array size must be relatively prime to 5, 4, 3, and 2 return 5 - key % 5; } // insert a DataItem public void insert(int key, DataItem item) // (assumes table not full)  { int hashVal = hashFunc1(key); // hash the key int stepSize = hashFunc2(key); // get step size // until empty cell or -1 while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) { hashVal += stepSize; // add the step hashVal %= arraySize; // for wraparound  } hashArray[hashVal] = item; // insert item  } public DataItem delete(int key) // delete a DataItem  { int hashVal = hashFunc1(key); // hash the key int stepSize = hashFunc2(key); // get step size while(hashArray[hashVal] != null) // until empty cell, { // is correct hashVal? if(hashArray[hashVal].getKey() == key) { DataItem temp = hashArray[hashVal]; // save item hashArray[hashVal] = nonItem; // delete item return temp; // return item  } hashVal += stepSize; // add the step hashVal %= arraySize; // for wraparound  } return null; // can't find item  } public DataItem find(int key) // find item with key // (assumes table not full)  { int hashVal = hashFunc1(key); // hash the key int stepSize = hashFunc2(key); // get step size while(hashArray[hashVal] != null) // until empty cell, { // is correct hashVal? if(hashArray[hashVal].getKey() == key) return hashArray[hashVal]; // yes, return item hashVal += stepSize; // add the step hashVal %= arraySize; // for wraparound  } return null; // can't find item  } } class HashDoubleApp { public static void main(String[] args) throws IOException { int aKey; DataItem aDataItem; int size, n; // get sizes System.out.print("Enter size of hash table: "); size = getInt(); System.out.print("Enter initial number of items: "); n = getInt(); // make table HashTable theHashTable = new HashTable(size); for(int j=0; j<n; j++) // insert data  { aKey = (int)(java.lang.Math.random() * 2 * size); aDataItem = new DataItem(aKey); theHashTable.insert(aKey, aDataItem); } while(true) // interact with user  { System.out.print("Enter first letter of "); System.out.print("show, insert, delete, or find: "); char choice = getChar(); switch(choice) { case 's': theHashTable.displayTable(); break; case 'i': System.out.print("Enter key value to insert: "); aKey = getInt(); aDataItem = new DataItem(aKey); theHashTable.insert(aKey, aDataItem); break; case 'd': System.out.print("Enter key value to delete: "); aKey = getInt(); theHashTable.delete(aKey); break; case 'f': System.out.print("Enter key value to find: "); aKey = getInt(); aDataItem = theHashTable.find(aKey); if(aDataItem != null) System.out.println("Found " + aKey); else System.out.println("Could not find " + aKey); break; default: System.out.print("Invalid entry\n"); } } } public static String getString() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static char getChar() throws IOException { String s = getString(); return s.charAt(0); } public static int getInt() throws IOException { String s = getString(); return Integer.parseInt(s); } }  

 

鏈地址法

開放地址法中,經過在哈希表中再尋找一個空位解決衝突問題。另外一個方法是在哈希表每一個單元中設置鏈表。某個數據項的關鍵字值仍是像一般同樣映射到哈希表的單元,而數據項自己插入到這個單元的鏈表中。其餘一樣映射到這個位置的數據項只須要加到鏈表中,不須要在原始的數組中尋找空位。鏈地址法中的裝填因子(數據項和哈希表容量的比值)與開放地址法的不一樣。在鏈地址法中,須要在有N個單元的數組中裝入N個或更多的數據項;所以,裝填因子通常爲1,或比1大。這沒有問題,由於,某些位置包含的鏈表中包含兩個或兩個以上的數據項。

// to run this program: C:>java HashChainApp import java.io.*; class Link { // (could be other items) private int iData; // data item public Link next; // next link in list public Link(int it) // constructor { iData= it; } public int getKey() { return iData; } public void displayLink() // display this link { System.out.print(iData + " "); } } class SortedList { private Link first; // ref to first list item public void SortedList() // constructor { first = null; } public void insert(Link theLink) // insert link, in order  { int key = theLink.getKey(); Link previous = null; // start at first Link current = first; // until end of list, while( current != null && key > current.getKey() ) { // or current > key, previous = current; current = current.next; // go to next item  } if(previous==null) // if beginning of list, first = theLink; // first --> new link else // not at beginning, previous.next = theLink; // prev --> new link theLink.next = current; // new link --> current  } public void delete(int key) // delete link { // (assumes non-empty list) Link previous = null; // start at first Link current = first; // until end of list, while( current != null && key != current.getKey() ) { // or key == current, previous = current; current = current.next; // go to next link  } // disconnect link if(previous==null) // if beginning of list first = first.next; // delete first link else // not at beginning previous.next = current.next; // delete current link  } public Link find(int key) // find link  { Link current = first; // start at first // until end of list, while(current != null && current.getKey() <= key) { // or key too small, if(current.getKey() == key) // is this the link? return current; // found it, return link current = current.next; // go to next item  } return null; // didn't find it  } public void displayList() { System.out.print("List (first-->last): "); Link current = first; // start at beginning of list while(current != null) // until end of list,  { current.displayLink(); // print data current = current.next; // move to next link  } System.out.println(""); } } class HashTable { private SortedList[] hashArray; // array of lists private int arraySize; public HashTable(int size) // constructor  { arraySize = size; hashArray = new SortedList[arraySize]; // create array for(int j=0; j<arraySize; j++) // fill array hashArray[j] = new SortedList(); // with lists  } public void displayTable() { for(int j=0; j<arraySize; j++) // for each cell,  { System.out.print(j + ". "); // display cell number hashArray[j].displayList(); // display list  } } public int hashFunc(int key) // hash function  { return key % arraySize; } public void insert(Link theLink) // insert a link  { int key = theLink.getKey(); int hashVal = hashFunc(key); // hash the key hashArray[hashVal].insert(theLink); // insert at hashVal  } public void delete(int key) // delete a link  { int hashVal = hashFunc(key); // hash the key hashArray[hashVal].delete(key); // delete link  } public Link find(int key) // find link  { int hashVal = hashFunc(key); // hash the key Link theLink = hashArray[hashVal].find(key); // get link return theLink; // return link  } } public class HashChainApp { public static void main(String[] args) throws IOException { int aKey; Link aDataItem; int size, n, keysPerCell = 100; // get sizes System.out.print("Enter size of hash table: "); size = getInt(); System.out.print("Enter initial number of items: "); n = getInt(); // make table HashTable theHashTable = new HashTable(size); for(int j=0; j<n; j++) // insert data  { aKey = (int)(java.lang.Math.random() * keysPerCell * size); aDataItem = new Link(aKey); theHashTable.insert(aDataItem); } while(true) // interact with user  { System.out.print("Enter first letter of "); System.out.print("show, insert, delete, or find: "); char choice = getChar(); switch(choice) { case 's': theHashTable.displayTable(); break; case 'i': System.out.print("Enter key value to insert: "); aKey = getInt(); aDataItem = new Link(aKey); theHashTable.insert(aDataItem); break; case 'd': System.out.print("Enter key value to delete: "); aKey = getInt(); theHashTable.delete(aKey); break; case 'f': System.out.print("Enter key value to find: "); aKey = getInt(); aDataItem = theHashTable.find(aKey); if(aDataItem != null) System.out.println("Found " + aKey); else System.out.println("Could not find " + aKey); break; default: System.out.print("Invalid entry\n"); } } } public static String getString() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static char getChar() throws IOException { String s = getString(); return s.charAt(0); } public static int getInt() throws IOException { String s = getString(); return Integer.parseInt(s); } } 

 

 

 

使用質數做爲取模的基數

若是許多關鍵字共享一個數組容量做爲除數,它們會趨向於映射到相同的位置,這會致使彙集。使用質數,能夠消除這種可能性。使用質數能夠保證關鍵字會較平均地映射到數組中。

 public static int getPrime(int min){ for(int j = min+1;true; j++) if(isPrime(j)) return j; } public static boolean isPrime(int n){ for(int j=2;(j*j<=n);j++) if(n %j == 0) return false; return true; }

 

 

擴展數組

當哈希表變得太滿時,一個選擇是擴展數組。在java中,數組有固定的大小,並且不能擴展。編程時只能另外建立一個新的更大的數組,而後把舊數組的全部內容插入到新的數組中。哈希函數根據數組大小計算給定數據項的位置,因此這些數據項不能再放在新數組中和老數組相同的位置上。所以,不能簡單地從一個數組向另外一個數組拷貝數據。須要按順序遍歷老數組,用insert()方法向新數組中插入每一個數據項。這叫作從新哈希化。這是一個耗時的過程,但若是數組要進行擴展,這個過程就是必要的。

 

哈希化字符串

 把短小的字符串轉換成數字,方法是每一個數位乘以對應的一個常數的冪。cats轉化爲一個數字,key=3*273 + 1*272 +20*271 +19*270

public static long hashFunc2(String key){ long hashVal = key.charAt(0) - 96; for(int j=1;j<key.length();j++){ int letter = key.charAt(j) - 96; hashVal = hashVal*27 + letter; hashVal %=101;//先求餘,而不是最後一次求餘,防止溢出  } return hashVal%101; }

 

 

哈希化和外部存儲

文件指針表

外部哈希化的關鍵部分是一個哈希表,它包含塊成員,指向外部存儲器中的塊。哈希表有時叫作索引。它能夠存儲在內存中,若是它太大,也能夠存儲在磁盤上,而把它的一部分放在內存中。即便把哈希表都放在內存中,也要在磁盤中保存一個備份,文件打開時,把它讀入內存。

未滿的塊

在外部哈希化中,重要的是塊不要填滿。所以,每一個塊平均存儲8個記錄。有的塊多些,有的塊少些。全部關鍵字映射爲同一個值的記錄都定位到相同塊。爲找到特定關鍵字的一個記錄,搜索算法哈希化關鍵字,用哈希值做爲哈希表的下標,獲得某個下標中的塊號,而後讀取這個塊。這個過程是有效的,由於定位一個特定的數據項,只須要訪問一次塊。缺點是至關多的磁盤空間被浪費了,由於設計時規定,塊是不容許填滿的。爲了實現這個方案,必須仔細選擇哈希函數和哈希表的大小,爲的是限制映射到相同的值關鍵字的數量。

填滿的塊

即便一個好的哈希函數,塊偶爾也會填滿。這時,可使用在內部哈希表中討論的處理衝突的不一樣方法:開放地址法和鏈地址法。

開放地址法中,插入時,若是發現一個塊是滿的,算法在相鄰的塊插入新記錄。在線性探測中,這是下一個塊,但也能夠用二次探測或再哈希法選擇。在鏈地址法中,有一個溢出塊,當發現已滿時,新紀錄插在溢出塊中。填滿的塊是不合須要的,由於有了它就須要額外的磁盤訪問,這就須要兩倍的訪問時間。然而,若是這種狀況不常常發生,也能夠接受。

 

小結

哈希表基於數組。

關鍵字值的範圍一般比數組容量大。

關鍵字值經過哈希函數映射爲數組的下標。

英文字典是一個數據庫的典型列子,它能夠有效的用哈希表來處理。

一個關鍵字哈希化到已佔用的數組單元,這種狀況叫作衝突。

衝突能夠用兩種方法解決:開放地址法和鏈地址法。

在開放地址法中,把衝突的數據項放在數組的其餘位置。

在鏈地址法中,每一個數組單元包含一個鏈表。把全部映射到同一個數組下標的數據項都插在這個鏈表中。

討論了三種開放地址法:線性探測、二次探測和再哈希法。

在線性探測中,步長老是1,因此若是x是哈希函數計算獲得的數組下標,那麼探測序列就是x,x+1,x+2,x+3,依次類推。

找到一個特定項須要通過的步數叫作探測長度。

在線性探測中,已填充單元的長度不斷增長。它們叫作首次彙集,這會下降哈希表的性能。

二次探測中,x的位移是步數的平方,因此探測序列就是x,x+1,x+4,x+9,x+16,依次類推。

二次探測消除了首次彙集,可是產生了二次彙集,它比首次彙集的危害略小。

二次彙集的發生是由於全部映射到同一個單元的關鍵字,在探測過程當中執行了相同的序列。

發生上述狀況是由於步長只依賴於哈希值,與關鍵字無關。

在再哈希法中,步長依賴於關鍵字,且從第二個哈希函數中獲得。

在再哈希法中,若是第二個哈希函數返回一個值s,那麼探測序列就是x,x+s,x+2s,x+3s,依次類推,這裏s由關鍵字獲得,但探測過程當中保持常量。

裝填因子是表中數據項數和數組容量的比值。

開放地址法中的最大裝填因子應該在0.5附近,若具備相同的裝填因子,對於再哈希法來講,查找的平均探測長度是2。

在開放地址法中,當裝填因子接近1時,查找時間趨於無限。

在開放地址法中,關鍵是哈希表不能填的太滿。

對於鏈地址法,裝填因子爲1比較合適。這時,成功的探測長度平均是1.5,不成功的是2.0。

字符串能夠這樣哈希化,每一個字符乘以常數的不一樣次冪,求和,而後用取模操做符%縮減結果,以適應哈希表的容量。

若是在Horner方法中用多項式表達哈希化,每一步中都應用取模操做符,以避免發生溢出。

哈希表的容量一般是一個質數。這在二次探測和再哈希法中很是重要。

哈希表可用於外部存儲,一種作法是用哈希表的單元存儲磁盤文件的塊號碼。

相關文章
相關標籤/搜索