1. 程式人生 > >雜湊表的java實現

雜湊表的java實現

一、為什麼要用雜湊表

樹的操作通常需要O(N)的時間級,而雜湊表中無論存有多少資料,它的插入和查詢(有時包括刪除)只需要接近常量級的時間,即O(1)的時間級。

但是雜湊表也有一定的缺點:它是基於陣列的,陣列建立後難以擴充套件。而某些雜湊表在基本填滿時,效能下降明顯,所以事先必須清楚雜湊表中將要儲存多少資料。而且目前沒有一種簡便的方法可以對雜湊表進行有序(從大到小或者從小到大)的遍歷,除非雜湊表本身是有序的,但事實上這是違背雜湊原則的。

綜合以上:當不需要有序遍歷資料,而且可以提前預測需要儲存的資料項的數目,使用雜湊表的結構是十分方便的。

二、雜湊化

把巨大的整數(關鍵字)範圍壓縮到一個可接受的陣列範圍內,便於儲存和查詢。通常來說,我們要儲存5000個數據,但資料的關鍵字範圍可能是0-200000。我們不可能去開闢200000的陣列去儲存這5000個數據,這就需要一個函式把關鍵字和陣列下標對應起來。這就是雜湊函式。通常的做法是取餘操作。i=N%size;i為下標,N為關鍵字,size為陣列大小。不過通常來說,size設為要儲存資料項數目的兩倍。

如果雜湊表存滿時,需要擴充套件雜湊表。我們需要新建一個更大的陣列來儲存資料,然後把原表中資料一一取出放入新表中。需要注意的是資料放入新表時需要重新用雜湊函式計算雜湊值,不能直接進行陣列的複製,因為雜湊函式的size已經變了。

通常而言我們把雜湊陣列的容量設為一個質數。首先來說假如關鍵字是隨機分佈的,那麼無所謂一定要模質數。但在實際中往往關鍵字有某種規律,例如大量的等差數列,那麼公差和模數不互質的時候發生碰撞的概率會變大,而用質數就可以很大程度上回避這個問題。對於除法雜湊表h(k)=k mod m,注意二進位制數對取餘就是該二進位制數最後r位數。這樣一來,Hash函式就和鍵值(用二進位制表示)的前幾位數無關了,這樣我們就沒有完全用到鍵值的資訊,這種選擇m的方法是不好的。所以好的方法就是用質數來表示m,使得這些質數,不太接近2的冪或者10的冪。

三、解決衝突

首先一般雜湊表是不允許重複的關鍵字,否則查詢函式只能返回最先查到的關鍵字,無法找到所有的對應資料項。如果重寫查詢函式讓它可以找到所有的對應資料項,這又會使得無論是否是重複關鍵字,查詢操作都要搜尋整個表,非常耗時。

儲存過程中可能出現儲存的資料項關鍵字不同,但計算出來的雜湊值是相同的,這就是衝突。

通常採用以下兩種方法來解決衝突。

1、開放地址法

直接在雜湊表中找到一個空位,把衝突的資料項存進去。

2、鏈地址法

把雜湊表中儲存的資料格式設為連結串列,這樣可以把衝突的資料放入對應位置的連結串列中即可。

四、開放地址法的java實現

根據在查詢下一個空位置時採用的方法,可以把開放地址法分為三種:線性探測、二次探測和再雜湊法。

1、線性探測法

線性探測就是根據陣列下標一個挨著一個去檢測,直到找到一個空位置。

java實現:

DataItem類定義了雜湊表儲存的資料內容和關鍵字。

package hash;

class DataItem    //hash表中存放的資料格式
{                                
private int iData;               // 設為關鍵字
//--------------------------------------------------------------
public DataItem(int ii)          // 構造器
   { iData = ii; }
//--------------------------------------------------------------
public int getKey()  //獲取關鍵字
   { return iData; }
//--------------------------------------------------------------
}  // end class DataItem
////////////////////////////////////////////////////////////////
HashTable類作為雜湊表的實現
package hash;

class HashTable        //定義雜湊表
{
private DataItem[] hashArray;    // 陣列形式
private int arraySize;           //雜湊表的大小
private DataItem nonItem;        // 刪除資料時,將被刪除的資料設為nonItem
//-------------------------------------------------------------
public HashTable(int size)       //構造器,指定雜湊表的大小
   {
   arraySize = size;
   hashArray = new DataItem[arraySize];
   nonItem = new DataItem(-1);   // 把nonItem的關鍵字設為-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;       // 雜湊函式
   }
//-------------------------------------------------------------
public void insert(DataItem item) // 插入資料
// 預設表未滿,事實上雜湊表是不允許存滿的,雜湊表的大小比實際儲存的資料數要大。
   {
   int key = item.getKey();      // 獲取資料項的關鍵字,用於計算雜湊值
   int hashVal = hashFunc(key);  // 計算雜湊值
                                 // 當前位置存有資料並且該資料未被刪除
   while(hashArray[hashVal] != null &&
                   hashArray[hashVal].getKey() != -1)
      {
      ++hashVal;                 // 查詢下一個位置
      hashVal %= arraySize;      // 到達表的末尾時,hashVal值變成1,。構成迴圈,從而可以查詢整個表
      }
   hashArray[hashVal] = item;    // 找到位置
   }  // end insert()
//-------------------------------------------------------------
public DataItem delete(int key)  // 根據關鍵字刪除資料
   {
   int hashVal = hashFunc(key);  // 根據關鍵字計算雜湊值

   while(hashArray[hashVal] != null)  // 該位置存有資料
      {                               // 兩者的關鍵字是否相同
      if(hashArray[hashVal].getKey() == key)
         {
         DataItem temp = hashArray[hashVal]; // 儲存刪除的資料項,用於返回
         hashArray[hashVal] = nonItem;       // 刪除
         return temp;                        // 返回刪除的資料項
         }
      ++hashVal;                 // 關鍵字不相同,繼續查詢下一個
      hashVal %= arraySize;      //迴圈
      }
   return null;                  // 未找到
   }  // end delete()
//-------------------------------------------------------------
public DataItem find(int key)    // 表中是否存在該關鍵字的資料項
   {
   int hashVal = hashFunc(key);  

   while(hashArray[hashVal] != null)  
      {                               
      if(hashArray[hashVal].getKey() == key)
         return hashArray[hashVal];   
      ++hashVal;              
      hashVal %= arraySize;      
      }
   return null;                
   }
//-------------------------------------------------------------
}  // end class HashTable
////////////////////////////////////////////////////////////////

HashTableApp類中包含了主程式
package hash;
import java.io.*;

class HashTableApp
{
public static void main(String[] args) throws IOException
   {
   DataItem aDataItem;
   int aKey, size, n, keysPerCell;
   System.out.print("Enter size of hash table: ");
   size = getInt();//從控制檯獲取一個整數作為雜湊表的大小
   System.out.print("Enter initial number of items: ");
   n = getInt();  //隨機生成n個數作為資料存入雜湊表
   keysPerCell = 10;//隨機生成函式的因子
                                
  HashTable theHashTable = new HashTable(size);//初始化

   for(int j=0; j<n; j++)        // 生成並插入
      {
      aKey = (int)(java.lang.Math.random() *
                                      keysPerCell * size);
      aDataItem = new DataItem(aKey);//封裝為雜湊表中的資料格式
      theHashTable.insert(aDataItem);//插入
      }

   while(true)                   
      {
      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");
         }  // end switch
      }  // end while
   }  // end main()
//--------------------------------------------------------------
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);
   }
//--------------------------------------------------------------
}  // end class HashTableApp
////////////////////////////////////////////////////////////////

2、二次探測法

線性探測法會發生集聚現象,即衝突資料項會集聚在一起,原因是查詢空資料項是一步一步移動的。

二次探測法是為了防止集聚產生的一種嘗試方法,思想是探測間隔較遠的單元,而不是臨近的單元。具體方法是把步長設為探測次數的平方,比如第1次探測步長為1,第2次為4,第3次為9以此類推。

但是二次探測法會產生二次集聚。通常不採用該方法,因為有更好的解決方案。

3、再雜湊法

方法是對衝突的關鍵字用另一個雜湊函式計算其值,把結果作為搜尋時的步長。這就使得不同的關鍵字步長不同,避免了集聚現象。

第二個雜湊函式必須具備以下條件:

(1)與第一個雜湊函式不同

(2)不能得出結果為0,否則步長為0.

通常第二個雜湊函式採用如下函式:

step=constant-(key%contant)

constant是一個質數且小於陣列容量,key是關鍵字。step範圍在1-constant之間。

再雜湊法要求表的容量是一個質數,這是為了使查詢操作可以遍歷整個表。否則假設表的容量為15,不是一個質數。而查詢初始位置為4,查詢步長為5,那麼每次查詢都是固定的三個數,即下標為9,14,4對應的資料。設為質數可以避免這種情況。

再雜湊法的java實現

package hashDouble;

class DataItem
{                                 
private int iData;                // 關鍵字
//--------------------------------------------------------------
public DataItem(int ii)           // 構造器
   { iData = ii; }
//--------------------------------------------------------------
public int getKey() //獲取關鍵字
   { return iData; }
//--------------------------------------------------------------
}  // end class DataItem
////////////////////////////////////////////////////////////////
package hashDouble;

class HashTable
{
private DataItem[] hashArray; 
private int arraySize;
private DataItem nonItem;        
//-------------------------------------------------------------
HashTable(int size)               // 構造器
   {
   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) //再雜湊
   {
   return 5 - key % 5;
   }
//-------------------------------------------------------------
                                  
public void insert(int key, DataItem item)
// 假設表未滿
   {
   int hashVal = hashFunc1(key);  // 計算雜湊值
   int stepSize = hashFunc2(key); // 計算步長
                                  
   while(hashArray[hashVal] != null &&
                   hashArray[hashVal].getKey() != -1)//非空且資料未刪除
      {
      hashVal += stepSize;        // 加步長
      hashVal %= arraySize;       // 迴圈到表頭
      }
   hashArray[hashVal] = item;     // 插入
   }  // end insert()
//-------------------------------------------------------------
public DataItem delete(int key)   // 刪除
   {
   int hashVal = hashFunc1(key);      //計算雜湊值
   int stepSize = hashFunc2(key);     // 計算步長

   while(hashArray[hashVal] != null)  // 非空
      {                               
      if(hashArray[hashVal].getKey() == key)//找到
         {
         DataItem temp = hashArray[hashVal]; 
         hashArray[hashVal] = nonItem;       
         return temp;                       
         }
      hashVal += stepSize;           
      hashVal %= arraySize;           
      }
   return null;                   // 無法找到
   }  // end delete()
//-------------------------------------------------------------
public DataItem find(int key)     // 查詢
// 假設表未滿
   {
   int hashVal = hashFunc1(key);       
   int stepSize = hashFunc2(key);     

   while(hashArray[hashVal] != null)  // 非空
      {                               
      if(hashArray[hashVal].getKey() == key)
         return hashArray[hashVal];   // 找到返回
      hashVal += stepSize;            // 加步長
      hashVal %= arraySize;          
      }
   return null;                   // can't find item
   }
//-------------------------------------------------------------
}  // end class HashTable
////////////////////////////////////////////////////////////////
package hashDouble;
import java.io.*;
class HashDoubleApp
{
public static void main(String[] args) throws IOException
   {
   int aKey;
   DataItem aDataItem;
   int size, n;
                             
   System.out.print("Enter size of hash table: ");
   size = getInt();
   System.out.print("Enter initial number of items: ");
   n = getInt();
                           
   HashTable theHashTable = new HashTable(size);

   for(int j=0; j<n; j++)     
      {
      aKey = (int)(java.lang.Math.random() * 2 * size);
      aDataItem = new DataItem(aKey);
      theHashTable.insert(aKey, aDataItem);
      }

   while(true)               
      {
      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");
         }  // end switch
      }  // end while
   }  // end main()
//--------------------------------------------------------------
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);
   }
//--------------------------------------------------------------
}  // end class HashDoubleApp
////////////////////////////////////////////////////////////////

五、鏈地址法的java實現

因為鏈地址法採用連結串列作為表中的資料格式,所以允許儲存相同的關鍵字資料。而且我們可以把鏈設為有序連結串列。

Link類定義了連結串列結構

package hashChain;

class Link        //定義連結串列
{                                   // 可以動態儲存資料,擴充容量
private int iData;                  // 關鍵字
public Link next;                   // 連結到下一個
//-------------------------------------------------------------
public Link(int it)                 //構造器
   { iData= it; }
//-------------------------------------------------------------
public int getKey()       //獲取關鍵字
   { return iData; }
//-------------------------------------------------------------
public void displayLink()           // 顯示
   { System.out.print(iData + " "); }
}  // end class Link
////////////////////////////////////////////////////////////////

SortedList定義了有序連結串列
package hashChain;

class SortedList   //有序連結串列
{
private Link first;               // 連結串列頭
//-------------------------------------------------------------
public void SortedList()          // 構造器
   { first = null; }
//-------------------------------------------------------------
public void insert(Link theLink)  // 插入並有序
   {
   int key = theLink.getKey();
   Link previous = null;          // 前一個數據項
   Link current = first;
                                 
   while( current != null && key > current.getKey() )//非空且關鍵字大於當前資料關鍵字
      {                          
      previous = current; //繼續查詢下一個
      current = current.next;     
      }
   if(previous==null)             // 如果表為空
      first = theLink;            //    表頭指向該資料項
   else                           // 表非空
      previous.next = theLink;    //   key<current.getKey()時,資料項應插入previous後,previous-->theLink
   theLink.next = current;        // theLink --> current
   }  // end insert()
//-------------------------------------------------------------
public void delete(int key)       //刪除
   {                         
   Link previous = null;        
   Link current = first;
                              
   while( current != null && key != current.getKey() )//未找到
      {                        
      previous = current;
      current = current.next;     // 查詢下一個
      }
                               
   if(previous==null)             //  要刪除資料項為表頭
      first = first.next;         //      刪除表頭
   else                           //   不是表頭
      previous.next = current.next; //    刪除current
   }  // end delete()
//-------------------------------------------------------------
public Link find(int key)         // 查詢
   {
   Link current = first;       
                                
   while(current != null &&  current.getKey() <= key)
      {                      
      if(current.getKey() == key)    // 找到
         return current;          // 返回
      current = current.next;     //未找到繼續查詢下一個
      }
   return null;               //未找到
   }  // end find()
//-------------------------------------------------------------
public void displayList()//顯示
   {
   System.out.print("List (first-->last): ");
   Link current = first;     
   while(current != null)   
      {
      current.displayLink();  
      current = current.next;  
      }
   System.out.println("");
   }
}  // end class SortedList
////////////////////////////////////////////////////////////////
HashTable定義雜湊表,表中資料為SortedList形式
package hashChain;

class HashTable
{
private SortedList[] hashArray;   
private int arraySize;
//-------------------------------------------------------------
public HashTable(int size)        //構造器
   {
   arraySize = size;
   hashArray = new SortedList[arraySize];  // 初始化陣列,陣列中儲存的是連結串列
   for(int j=0; j<arraySize; j++)          // 初始化每個陣列元素
      hashArray[j] = new SortedList();  
   }
//-------------------------------------------------------------
public void displayTable()
   {
   for(int j=0; j<arraySize; j++) 
      {
      System.out.print(j + ". "); 
      hashArray[j].displayList(); 
      }
   }
//-------------------------------------------------------------
public int hashFunc(int key)      // 計算雜湊值
   {
   return key % arraySize;
   }
//-------------------------------------------------------------
public void insert(Link theLink)  // 插入資料
   {
   int key = theLink.getKey();  //獲取關鍵字
   int hashVal = hashFunc(key);   // 計算關鍵字雜湊值
   hashArray[hashVal].insert(theLink); // 插入雜湊表中對應的位置
   }  // end insert()
//-------------------------------------------------------------
public void delete(int key)       // 根據關鍵字刪除資料
   {
   int hashVal = hashFunc(key);   // 計算關鍵字雜湊值
   hashArray[hashVal].delete(key); // 刪除雜湊表中對應資料
   }  // end delete()
//-------------------------------------------------------------
public Link find(int key)         // 查詢
   {
   int hashVal = hashFunc(key);   
   Link theLink = hashArray[hashVal].find(key);  
   return theLink;               
   }
//-------------------------------------------------------------
}  // end class HashTable
////////////////////////////////////////////////////////////////
主函式
package hashChain;
import java.io.*;

class HashChainApp
{
public static void main(String[] args) throws IOException
   {
   int aKey;
   Link aDataItem;
   int size, n, keysPerCell = 100;
   System.out.print("Enter size of hash table: ");
   size = getInt();
   System.out.print("Enter initial number of items: ");
   n = getInt();
   HashTable theHashTable = new HashTable(size);

   for(int j=0; j<n; j++)        
      {
      aKey = (int)(java.lang.Math.random() *
                                       keysPerCell * size);
      aDataItem = new Link(aKey);
      theHashTable.insert(aDataItem);
      }
   while(true)                 
      {
      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");
         }  // end switch
      }  // end while
   }  // end main()
//--------------------------------------------------------------
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);
   }
//--------------------------------------------------------------
}  // end class HashChainApp
////////////////////////////////////////////////////////////////
六、如何設計雜湊函式

1、不使用無用資料項

關鍵字的選取時,要提出原始資料中的無用資料項,例如起始位、校驗位、結束位等,因為這些資料位沒有攜帶資訊。

2、使用所有的有用資料位

所有的有用資料位在雜湊函式中都應當有體現。不要使用前四位或者後五位等其他方法。

3、使用質數作為取模運算的基數。

若關鍵字完全隨機分佈,質數和非質數的表現差不多。但是當關鍵字不是隨機分佈時,就應該使用質數作為雜湊表的大小。使用質數可以是關鍵字較為平均的對映到雜湊表的各個位置。

七、開放地址法和鏈地址法的比較

開放地址法在錶快滿時,效能有明顯下降,且對雜湊表進行擴充套件時操作複雜。鏈地址法需要設計連結串列類,但是不會隨著資料項的增多導致效能快速下降,而且可以動態擴充套件雜湊表。

參考文獻:
java資料結構與演算法(第二版)