雜湊表的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資料結構與演算法(第二版)