哈希表是一種數據結構,它能夠提供快速的插入操做和查找操做。哈希表有很是多的優勢,不論哈希表中有多少數據,插入和刪除數據只須要接近常量的時間,即O(1)時間級。
哈希表一樣葉存在這一些缺點:它是基於數組的,數組建立後難以擴展。某些哈希表被基本填滿時,性能降低的很是嚴重,因此咱們在使用哈希表以前必定要清楚表中要存儲多少數據。
一樣,也沒有一種簡便的方法能夠以任何一種順序遍歷表中的數據。若是須要遍歷,那就只能選擇其餘的數據結構。數組
首先咱們須要明白哈希表的插入刪除查找的時間將近是常量的緣由,咱們能夠這樣去想其實這也是由於咱們在操做哈希表的時候,其實就是在操做數組,至關於咱們在操做數組的時候已經知道了咱們要操做的數據的下標值。那個速度可想而知的快。
如今咱們就要想一下怎麼將咱們key-value對中的key轉化爲數組的下標:
1. 把單詞轉化爲數組下標
在咱們平時的計算機應用中,有不少的編碼表合一使用,其中一種是ASCII編碼,其中a是97,b是98,以此類推,知道122表明z。然而ASCII碼從0到255。英文字母中有26個字母,因此能夠設計出一種本身編碼的方案,好比a是1,b是2,c是3,以此類推,直到26表明z。還要把空格表明0,因此有27個字符。可是如何把單個字母的數字組合成表明整個單詞的數字呢?
a. 把數字相加
把每一個單詞的各個字母用上面的數字代替後相加求和。咱們假設每一個單詞有10個字母,那麼字典的第一個單詞a的編碼就是0+0+0+0+0+0+0+0+0+1 = 1,那麼最後一個單詞就是zzzzzzzzzz,全部字符的編碼和爲10個26相加爲260,從某種角度來講,這個方案就只能保存260個單詞,確定有問題。
b. 冪的連乘
上面的那種方案沒有足夠的空間來存儲咱們想要存儲的東西,因此咱們決定把存儲的空間加大一點,咱們採用的是,把單詞分解爲字母組合,把字母分解爲他們的數字代碼,乘以適當的27的冪,而後將結果相加,這樣應該能夠增長咱們想要的空間,可是實際狀況是這樣的。最長的單詞zzzzzzzzzz將轉化26*279+26*278+26*277+26*276+26*275+26*274+26*273+26*272+26*271+26*270 咱們會發現這個結果大的可怕,因此兩種方案好像都走了一個極端。
第一種方案產生的數組的下標太少,第二種方案產生的數組下標太多。數據結構
咱們如今就須要一種方法,把巨大的數組下標壓縮一下。
有一種簡單的方法是使用取餘運算符,假設咱們如今有0-199的數字,咱們須要將其壓縮爲0-9的數字。咱們就可讓原來的數字對10求餘,咱們就會獲得 0-9 的數字。這就是一種哈希函數,它把一個大範圍的數字哈希化爲一個小範圍的數字,這個小的範圍對應着數組的下標。使用哈希函數向數組插入數據以後,這個數組就稱爲哈希表。
在原始的範圍中,每一個數字表明這一個潛在的數據項,可是他們之間只有不多一部分表明真實數據。哈希函數把這個巨大的整數範圍轉換成小的多的數組下標範圍。函數
一個key通過哈希化以後會出現相同值的狀況。這種狀況就叫作衝突。出現了這樣的問題,確定是要解決的,目前在咱們面前就有兩種可行的方案:
方案1:當發生衝突的時候,經過系統的方法找到數組的下一個空位,並把這個單詞添加進去。這個方法叫作開放地址法。
方案2:建立一個存放單詞鏈表的數組,數組內不直接存儲單詞。當發生衝突時,新的數據項直接接到這個數組下標所指的鏈表中。這種方法叫作,鏈地址法。性能
顧名思義就是把地址開發出來,供給數據進行存儲,咱們再也不侷限於只把數據存在哈希值對應的數組下標中,咱們能夠存在周圍。只要下面還有位置,就能夠存,取的時候也是同樣,先在對應的位置找,找不到就在周圍找,直到周圍沒有數據爲止,刪除也是同理。開放地址法中有包含着其它的子方法:編碼
// 數據項類
public class DataItem {
private int iData;
public DataItem(int i) {iData = i;
public int getKey() {return iData;}
}
// hash
public class hash{
private DataItem[] hashArray; // 存儲數據的數組
private int arraySize; // 數組的大小
private DataItem nonItem; // 沒有數據的數據項
public hash(int size) {
arraySize = size;
hashArray = new DataItem[arraySize];
nonItem = new DataItem(-1); // 沒有數據則讓其數據項爲-1
}
// 顯示函數
public void display() {
System.out.print("Table: ");
for (int i=0; i<arraySize; i++) {
if (hashArray[i] != null)
System.out.print(hashArray[i].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(); // 獲取數據項的key
int hashVal = hashFunc(key); // 獲取key的哈希值
while (hashArray[hashVal] != null && hashArray[hashVal] != -1) { // 位置被佔用了
hashVal++; // 向下走一個單位
hashVal = hashVal%arraySize; // 從新獲取哈希值
}
// 直到找到對應的位置
hashArray[hashVal] = item; // 插入數據
}
// 刪除數據
public DataItem delete(int key) {
int hashVal = hashFunc(key);
while (hashArray[hashVal] != null) {
if (hashArray[hashVal].getKey() == key) { // 找到要刪除的數據
DataItem temp = hashArray[hashVal];
hashArrayp[hashVal] = nonItem;
return temp; // 找到並返回
}
hashVal++;
hashVal = hashVal%arraySize;
}
return null; // 沒找到
}
// 尋找數據
public DataItem find(int key) {
int hashVal = hashFunc(key);
while (hashArray[hashVal] != null) {
if (hashArray[hashVal].getKey() == key) {
return hashArray[hashVal];
}
hashVal ++;
hashVal = hashVal%arraySize;
}
return null;
}
}
可是上述的方式存在很大的隱患,就是數據可能在某個地方堆積起來致使哈希表的效率大幅降低,爲了解決這種隱患能夠採起另一種方案,那就是二次探索,就是在二次探索的過程當中判斷數據有沒有發生彙集的狀況。spa
已經填入哈希表中數據和表長的比率就叫作裝填因子,有10000個單位的哈希表填入6667個數據後,它的裝填因子就是2/3。
當裝填因子不大時,彙集分佈得比較連貫。二次探測,顧名思義就是在線性探測得基礎上,線性探測每次探測得步長爲1,而二次探測得步長爲12,22,32……,它得初衷是探測較遠得單元。
其中也會出現問題:雖然二次探測消除了線性探測中得彙集問題(原始彙集),可是,它有產生了新的問題,「二次彙集」,由於它的步長其實也是固定的,若哈希值相同的元素太多,一樣會產生彙集問題。
爲了解決線性探測和二次探測的原始彙集和二次彙集問題,提出了另一種方案:「再哈希法」設計
解決問題的思路:產生一種依賴關鍵字的探測序列,而不是每一個關鍵字都同樣,那麼不一樣的關鍵字即便映射到相同的數組下標,也可使用不一樣的探測序列。
實現方法:用不一樣的哈希函數將關鍵字再作一遍哈希化,用這個結果做爲步長。
第二個哈希函數通常具備以下特色:1. 和第一個哈希函數不相同 2. 不能輸出0code
代碼來了:內存
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 i=0; i<arraySize; i++) {
if(hashArray[i] != null)
System.out.print(hashArray[j].getKey() + " ");
else
System.out.print("** ");
}
System.out.println();
}
// 哈希函數1
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; // 首先給以前的哈希值加上由key計算的來的步長
hashVal %= arraySize; // 再獲取一次哈希值
}
hashArray[hashVal] = item; // 若是找到滅有數據的位置 直接插入數據
}
// 刪除數據
public DataItem delete(int key) {
int hashVal = hashFunc1(key); // 獲取哈希值
int stepSize = hashFunc2(key); // 獲取其惟一的步長(根據其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;
}
// 查找數據項
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;
}
}
數據項並不存儲在哈希值映射的數組中,而是存儲在數組某一項對應的鏈表中,這樣這要遇到哈希衝突的問題,直接去對應的鏈表裏插入、尋找、刪除就行。
直接上代碼:開發
public 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 + " "); }
}
// 鏈表類
public class SortedList {
private Link first;
public 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;
theLink.next = current;
}
// 刪除數據
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;
}
// 尋找數據
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;
}
// 打印鏈表
public void displayList() {
System.out.print("List (first --> last): ");
Link current = first;
while (current != null) {
current.displayLink();
current = current.next;
}
System.out.println();
}
}
// 使用鏈地址法的哈希表類
public 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); // 直接利用哈希值找到對應的鏈表進行數據的插入
}
// 刪除
public void delete(int key) {
int hashVal = hashFunc(key); // 先找到數據在哪一個鏈表中
hashArray[hashVal].delete(key); // 在鏈表中刪除對應的數據
}
// 尋找數據項
public Link find(int key) {
int hashVal = hashFunc(key); // 找到數據項應該存在的位置
Link theLink = hashArray[hashVal].find(key); // 直接在對應的鏈表中查找數據
return theLink; // 法妞數據
}
}
若是使用開放地址法,對於小型的哈希表,再哈希法彷佛比二次探測的效果好。可是有一個狀況例外,就是內存充足,而且哈希表一經建立,就再也不改變其容量,在這種狀況下,線性探測相對容易實現,而且,若是裝填因子低於0.5,幾乎沒有什麼性能的降低。 若是再哈希表建立的時候,要填入的項數未知,鏈地址法要好過開發地址法。若是用開發地址法,隨着裝填因子變大,性能會降低很快,可是使用鏈地址法,性能只能線性的降低。 當二者均可選的時候,選擇鏈地址法。它須要使用鏈表類,但回報是增長比預期更多的數據項,不會致使性能快速的降低。