哈希表
根據設定的哈希函數 H(key)和所選中的處理沖突的方法,將一組關鍵字映射到一個有限的、地址連續的地址集 (區間) 上,並以關鍵字在地址集中的“映像”作為相應記錄在表中的存儲位置,如此構造所得的查找表稱之為“哈希表”。
構造哈希函數的方法
1. 直接定址法(數組)
哈希函數為關鍵字的線性函數H(key) = key 或者 H(key) = a*key + b
此法僅適合於:地址集合的大小 == 關鍵字集合的大小
2. 數字分析法
假設關鍵字集合中的每個關鍵字都是由 s 位數字組成 (u1, u2, …, us),分析關鍵字集中的全體, 並從中提取分布均勻的若干位或它們的組合作為地址。
此方法僅適合於:能預先估計出全體關鍵字的每一位上各種數字出現的頻度。
3. 平方取中法
以關鍵字的平方值的中間幾位作為存儲地址。求“關鍵字的平方值”的目的是“擴大差別” ,同時平方值的中間各位又能受到整個關鍵字中各位的影響。
此方法適合於:關鍵字中的每一位都有某些數字重復出現頻度很高的現象。
4. 折疊法
將關鍵字分割成若干部分,然后取它們的疊加和為哈希地址。有兩種疊加處理的方法:移位疊加和間界疊加。
此方法適合於:關鍵字的數字位數特別多;
5. 除留余數法
設定哈希函數為:{H(key) = key % p | 其中,p≤m(表長)並且p 應為不大於 m 的素數或是不含 20 以下的質因子}
為什么要對 p 加限制?
例如:給定一組關鍵字為:12, 39, 18, 24, 33,21,若取 p=9, 則他們對應的哈希函數值將為:3, 3, 0, 6, 6, 3;
可見,若 p 中含質因子 3, 則所有含質因子 3 的關鍵字均映射到“3 的倍數”的地址上,從而增加了“沖突”的可能。
6. 隨機數法
設定哈希函數為:H(key) = Random(key)其中,Random 為偽隨機函數;
通常,此方法用於對長度不等的關鍵字構造哈希函數。
(如果關鍵字並不是數字, 則還需先對其進行數字化處理。)
實際造表時,采用何種構造哈希函數的方法取決於建表的關鍵字集合的情況(包括關鍵字的范圍和形態),總的原則是使產生沖突的可能性降到盡可能地小(下面我們將以除留余數法構造哈希函數)。
處理沖突的方法
“處理沖突” 的實際含義是:為產生沖突的地址尋找下一個哈希地址。
1. 開放定址法
為產生沖突的地址 H(key) 求得一個地址序列:{ H0, H1, …, Hs|1≤ s≤m-1}
其中: H0 = H(key)
Hi = ( H(key) + di ) % m {i=1, 2, …, s}
對增量 di 有三種取法:
1) 線性探測再散列
di = c * i 最簡單的情況 c=1
2) 平方探測再散列
di = 1^2, -1^2, 2^2, -2^2, …,
3) 隨機探測再散列
di 是一組偽隨機數列或者di=i×H2(key) (又稱雙散列函數探測)
注意:增量 di 應具有“完備性”,即:產生的 Hi 均不相同,且所產生的s(m-1)個 Hi 值能覆蓋哈希表中所有地址。則要求:
※ 平方探測時的表長 m 必為形如 4j+3 的素數(如: 7, 11, 19, 23, … 等);
※ 隨機探測時的 m 和 di 沒有公因子。
2. 鏈地址法(又稱拉鏈法)
將所有哈希地址相同的記錄都鏈接在同一鏈表中(我們將采用的方法)。
哈希表的設計與實現
//哈希表設計
template <typename HashedObj>
class HashTable
{
public:
typedef typename vector<HashedObj>::size_type size_type;
public:
explicit HashTable(int tableSize = 101)
: theList(tableSize), currentSize(0) {}
~HashTable()
{
makeEmpty();
}
//判斷元素x是否存在於哈希表中
bool contains(const HashedObj &x) const;
void makeEmpty();
bool insert(const HashedObj &x);
bool remove(const HashedObj &x);
private:
vector< list<HashedObj> > theList;
size_type currentSize;
void rehash();
int myHash(const HashedObj &x) const;
};
哈希函數
//如果關鍵字並不是數字, 則需先對其進行數字化處理
template <typename Type>
int hash(Type key)
{
return key;
}
template<>
int hash<const string &>(const string &key)
{
int hashVal = 0;
for (size_t i = 0; i < key.length(); ++i)
{
hashVal = 37 * hashVal * key[i];
}
return hashVal;
}
//哈希函數
template <typename HashedObj>
int HashTable<HashedObj>::myHash(const HashedObj &x) const
{
//首先對key進行數字化處理
int hashVal = hash(x);
//計算哈希下標
hashVal = hashVal % theList.size();
if (hashVal < 0)
hashVal += theList.size();
return hashVal;
}
哈希表的插入
//插入
template <typename HashedObj>
bool HashTable<HashedObj>::insert(const HashedObj &x)
{
//首先找到應該插入的桶(鏈表)
list<HashedObj> &whichList = theList[ myHash(x) ];
//哈希表中已經存在該值了
if (find(whichList.begin(), whichList.end(), x) != whichList.end())
return false;
//插入桶中
whichList.push_back(x);
//如果此時哈希表已經"滿"了(所存儲的元素個數 = 哈希表的槽數)
//裝載因子 == 1, 為了獲取更好的性能, 再哈希
if (++ currentSize > theList.size())
rehash();
return true;
}
再哈希
//判斷是否是素數
bool is_prime(size_t n)
{
if (n == 1 || !n)
return 0;
for (size_t i = 2; i*i <= n; i++)
if (!(n%i))
return 0;
return 1;
}
//尋找下一個素數
size_t nextPrime(size_t n)
{
for (size_t i = n; ; ++i)
{
if (is_prime(i))
return i;
}
return -1;
}
//再哈希
template <typename HashedObj>
void HashTable<HashedObj>::rehash()
{
vector< list<HashedObj> > oldList = theList;
//以一個大於原表兩倍的第一個素數重新設定哈希桶數
theList.resize( nextPrime(2*theList.size()) );
//將原表清空
for (typename vector< list<HashedObj> >::iterator iter = theList.begin();
iter != theList.end();
++ iter)
iter -> clear();
//將原表的數據插入到新表中
for (size_type i = 0; i < oldList.size(); ++i)
{
typename list<HashedObj>::iterator iter = oldList[i].begin();
while (iter != oldList[i].end())
{
insert(*iter ++);
}
}
}
哈希表的查找
查找過程和造表過程一致。假設采用開放定址處理沖突,則查找過程為:對於給定值 K, 計算哈希地址 i = H(K),若 r[i] = NULL 則查找不成功,若 r[i].key = K 則查找成功否則 “求下一地址 Hi” ,直至 r[Hi] = NULL (查找不成功)或r[Hi].key = K (查找成功) 為止。
而我們采用比較簡單的鏈地址法(也稱拉鏈法的查找實現):
//查找:判斷哈希表中是否存在該元素
template <typename HashedObj>
bool HashTable<HashedObj>::contains(const HashedObj &x) const
{
const list<HashedObj> &whichList = theList[ myHash(x) ];
if (find(whichList.begin(), whichList.end(), x) != whichList.end())
return true;
return false;
}
從查找過程得知,哈希表查找的平均查找長度實際上並不等於零。決定哈希表查找的ASL的因素
一般情況下,可以認為選用的哈希函數是“均勻”的,則在討論ASL時,可以不考慮它的因素。因此,哈希表的ASL是處理沖突方法和裝載因子的函數。
Dictionary
Dictionary常用用法:以 key 的類型為 int , value的類型為string 為例
1、創建及初始化
Dictionary<int,string>myDictionary=newDictionary<int,string>();
2、添加元素
myDictionary.Add(1,"C#");
myDictionary.Add(2,"C++");
myDictionary.Add(3,"ASP.NET");
myDictionary.Add(4,"MVC");
3、通過Key查找元素
if(myDictionary.ContainsKey(1))
{
Console.WriteLine("Key:{0},Value:{1}","1", myDictionary[1]);
}
4、通過KeyValuePair遍歷元素
foreach(KeyValuePair<int,string>kvp in myDictionary)
...{
Console.WriteLine("Key = {0}, Value = {1}",kvp.Key, kvp.Value);
}
5、僅遍歷鍵 Keys 屬性
Dictionary<int,string>.KeyCollection keyCol=myDictionary.Keys;
foreach(intkeyinkeyCol)
...{
Console.WriteLine("Key = {0}", key);
}