1、爲何要用哈希表java
樹的操做一般須要O(N)的時間級,而哈希表中不管存有多少數據,它的插入和查找(有時包括刪除)只須要接近常量級的時間,即O(1)的時間級。算法
可是哈希表也有必定的缺點:它是基於數組的,數組建立後難以擴展。而某些哈希表在基本填滿時,性能降低明顯,因此事先必須清楚哈希表中將要存儲多少數據。並且目前沒有一種簡便的方法能夠對哈希表進行有序(從大到小或者從小到大)的遍歷,除非哈希表自己是有序的,但事實上這是違背哈希原則的。數組
綜合以上:當不須要有序遍歷數據,並且能夠提早預測須要存儲的數據項的數目,使用哈希表的結構是十分方便的。數據結構
2、哈希化dom
把巨大的整數(關鍵字)範圍壓縮到一個可接受的數組範圍內,便於存儲和查找。一般來講,咱們要存儲5000個數據,但數據的關鍵字範圍多是0-200000。咱們不可能去開闢200000的數組去存儲這5000個數據,這就須要一個函數把關鍵字和數組下標對應起來。這就是哈希函數。一般的作法是取餘操做。i=N%size;i爲下標,N爲關鍵字,size爲數組大小。不過一般來講,size設爲要存儲數據項數目的兩倍。函數
若是哈希表存滿時,須要擴展哈希表。咱們須要新建一個更大的數組來存儲數據,而後把原表中數據一一取出放入新表中。須要注意的是數據放入新表時須要從新用哈希函數計算哈希值,不能直接進行數組的複製,由於哈希函數的size已經變了。性能
一般而言咱們把哈希數組的容量設爲一個質數。首先來講假如關鍵字是隨機分佈的,那麼無所謂必定要模質數。但在實際中每每關鍵字有某種規律,例如大量的等差數列,那麼公差和模數不互質的時候發生碰撞的機率會變大,而用質數就能夠很大程度上回避這個問題。對於除法哈希表h(k)=k mod m,注意二進制數對取餘就是該二進制數最後r位數。這樣一來,Hash函數就和鍵值(用二進制表示)的前幾位數無關了,這樣咱們就沒有徹底用到鍵值的信息,這種選擇m的方法是很差的。因此好的方法就是用質數來表示m,使得這些質數,不太接近2的冪或者10的冪。spa
3、解決衝突設計
首先通常哈希表是不容許重複的關鍵字,不然查找函數只能返回最早查到的關鍵字,沒法找到全部的對應數據項。若是重寫查找函數讓它能夠找到全部的對應數據項,這又會使得不管是不是重複關鍵字,查找操做都要搜索整個表,很是耗時。code
存儲過程當中可能出現存儲的數據項關鍵字不一樣,但計算出來的哈希值是相同的,這就是衝突。
一般採用如下兩種方法來解決衝突。
一、開放地址法
直接在哈希表中找到一個空位,把衝突的數據項存進去。
二、鏈地址法
把哈希表中存儲的數據格式設爲鏈表,這樣能夠把衝突的數據放入對應位置的鏈表中便可。
4、開放地址法的java實現
根據在查找下一個空位置時採用的方法,能夠把開放地址法分爲三種:線性探測、二次探測和再哈希法。
一、線性探測法
線性探測就是根據數組下標一個挨着一個去檢測,直到找到一個空位置。
java實現:
DataItem類定義了哈希表存儲的數據內容和關鍵字。
package hash;
class DataItem //hash表中存放的數據格式
{
private int iData; // 設爲關鍵字
//--------------------------------------------------------------
public DataItem(int ii) // 構造器
{ iData = ii; }
//--------------------------------------------------------------
public int getKey() //獲取關鍵字
{ return iData; }
//--------------------------------------------------------------
} // end class DataItem
////////////////////////////////////////////////////////////////
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
////////////////////////////////////////////////////////////////
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
////////////////////////////////////////////////////////////////
二、二次探測法
線性探測法會發生集聚現象,即衝突數據項會集聚在一塊兒,緣由是查找空數據項是一步一步移動的。
二次探測法是爲了防止集聚產生的一種嘗試方法,思想是探測間隔較遠的單元,而不是臨近的單元。具體方法是把步長設爲探測次數的平方,好比第1次探測步長爲1,第2次爲4,第3次爲9以此類推。
可是二次探測法會產生二次集聚。一般不採用該方法,由於有更好的解決方案。三、再哈希法
方法是對衝突的關鍵字用另外一個哈希函數計算其值,把結果做爲搜索時的步長。這就使得不一樣的關鍵字步長不一樣,避免了集聚現象。
第二個哈希函數必須具有如下條件:
(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
////////////////////////////////////////////////////////////////
由於鏈地址法採用鏈表做爲表中的數據格式,因此容許存儲相同的關鍵字數據。並且咱們能夠把鏈設爲有序鏈表。
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
////////////////////////////////////////////////////////////////
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
////////////////////////////////////////////////////////////////
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
////////////////////////////////////////////////////////////////
一、不使用無用數據項
關鍵字的選取時,要提出原始數據中的無用數據項,例如起始位、校驗位、結束位等,由於這些數據位沒有攜帶信息。
二、使用全部的有用數據位
全部的有用數據位在哈希函數中都應當有體現。不要使用前四位或者後五位等其餘方法。
三、使用質數做爲取模運算的基數。
若關鍵字徹底隨機分佈,質數和非質數的表現差很少。可是當關鍵字不是隨機分佈時,就應該使用質數做爲哈希表的大小。使用質數能夠是關鍵字較爲平均的映射到哈希表的各個位置。
7、開放地址法和鏈地址法的比較
開放地址法在錶快滿時,性能有明顯降低,且對哈希表進行擴展時操做複雜。鏈地址法須要設計鏈表類,可是不會隨着數據項的增多致使性能快速降低,並且能夠動態擴展哈希表。
參考文獻: java數據結構與算法(第二版)