【資料結構】雜湊表/散列表
本篇博文,旨在介紹雜湊表的基本概念及其用法;介紹了減少雜湊衝突的方法;並用程式碼實現了雜湊表的線性探測和雜湊桶
雜湊表的基本概念
雜湊表是一種儲存結構,它通過key值可以直接訪問該key值在記憶體中從儲存位置;
將關鍵值對映到表中的位置訪問資料,這個對映函式叫做雜湊函式,儲存資料的表叫做散列表;
構造雜湊表的幾種方法
1、直接定址法
根據關鍵值直接確定元素存取的位置,該雜湊函式是一個線性函式;例如 Hash(key) = A*key+B,其中,A和B都為常數
2、除留餘數法
取出關鍵值後,將關鍵值除以該散列表的長度從而獲得的餘數,作為雜湊地址;Hash(key) = key%m
3、平方取中法
取關鍵字平方後的中間幾位為雜湊地址。由於一個數的平方的中間幾位與這個數的每一位都有關,因而,平方取中法產生衝突的機會相對較小。
平方取中法中所取的位數由表長決定。
例: K = 456 , K2 = 207936 若雜湊表的長度m=102,則可取79(中間兩位)作為雜湊函式值。
4、摺疊法
將關鍵字分割成位數相同的幾部分,最後一部分位數可以不同,然後取這幾部分的疊加和(去除進位)作為雜湊地址。數位疊加可以有移位疊加和間界疊加兩種方法。
5、隨機數法
選擇一隨機函式,取關鍵字的隨機值作為雜湊地址,通常用於關鍵字長度不同的場合。
6、數學分析法
找出數字的規律,儘可能利用這些資料來構造衝突機率較低的雜湊地址。
雜湊衝突/雜湊碰撞
雜湊衝突出現的原因
關鍵值通過任何的雜湊函式獲得的雜湊地址都有可能是重複的,這種情況叫做雜湊衝突;
載荷因子
散列表的載荷因子的定義為:a = 表中的元素個數/散列表的長度
對於開放定址法,載荷因子特別重要,一般情況下嚴格控制在0.7-0.8
超過0.8,雜湊表的效率會很低
過低的話,浪費的空間會更大
任何的雜湊函式都是無法避免的,在處理雜湊衝突的方法中,我們介紹下面兩種方法
a 用開放定址法來處理雜湊衝突
a1 線性探測
當有需要插入元素的位置已經有元素存在時,則向後遍歷尋找空位置
a2 二次探測
當插入位置衝突時,則向後尋找,但不是每次只挪動一個位置,而是衝突次數的平方次(1,4,9....)
b 用開鏈法處理雜湊衝突
雜湊桶是一種順序表和連結串列的結合體,定義一個數組,陣列儲存的內容是節點的指標,下面用一個個桶來儲存元素;
這樣可以提高雜湊表的效率
如何減少雜湊衝突
a 素數
使用素數做除數可以有效的減少雜湊衝突
//素數表
const int _PrimeSize = 28;
static const unsigned long _PrimeList[_PrimeSize] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul,
786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul,
25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul,
805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
b 字串雜湊演算法
字串雜湊演算法就是將一個字串轉化成一個Key值
字串雜湊演算法有很多
下面這個連結有詳細的介紹
程式碼實現
開放定址法的線性檢測
節點的定義:
//定義列舉,表示節點的狀態
enum Status
{
EXIST,
DELETE,
EMPTY,
};
//節點的定義
template<typename K,typename V>
struct HashNode
{
K _key;
V _value;
Status _status;
HashNode(const K& key = K(), const V& value = V())
:_key(key)
, _value(value)
, _status(EMPTY)
{}
};
狀態位標識著這個位置有沒有值存在,還是之前存在已被刪除
雜湊表的定義:
//HashFunc是採用的字串雜湊演算法
template<typename K, typename V, typename HashFunc = __HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
//預設的建構函式
HashTable()
{}
//建構函式
HashTable(size_t size);
//將K值轉換成雜湊值
size_t HashFunC(const K& key);
//插入一組值
pair<Node*, bool> Insert(const K& key,const V& value);
//查詢元素
Node* find(const K& key);
//刪除一個節點
void Remove(const K& key);
protected:
vector<Node> _v;//用vector的下標標誌位置
size_t _size;//雜湊表中元素的數量
protected:
//交換兩個雜湊表
void Swap(HashTable<K, V> &h);
//進行容量的判別,進行擴容
void CheckCapacity();
};
插入函式的實現:
pair<Node*, bool> Insert(const K& key,const V& value)
{
//檢查是否需要擴容
CheckCapacity();
//對K值進行取餘,判斷插入的位置
size_t index = HashFunC(key);
//如果存在,則迴圈著繼續尋找
while (_v[index]._status == EXIST)
{
index++;
if (index == _v.size())
index = 0;
}
_v[index]._key = key;
_v[index]._value = value;
_v[index]._status = EXIST;
_size++;
return make_pair<Node*,bool>(&_v[index] ,true);
}
插入函式,要找到關鍵值對應的位置,衝突的話後移;找到後放入對應的Key和value值,並且將狀態位改變即可
查詢函式的實現:
Node* find(const K& key)
{
//對K值進行取餘,判斷插入的位置
size_t index = HashFunC(key);
//如果存在,則繼續尋找
while (_v[index]._status == EXIST)
{
//若相等,判斷狀態是否是刪除
//若刪除,則沒找到,返回空
//若沒刪除,則返回該位置的地址
if (_v[index]._key == key)
{
if (_v[index]._status == DELETE)
return NULL;
return &_v[index];
}
index++;
if (index == _size)
index = 0;
}
return &_v[index];
}
線性探測中,查詢這塊有個小問題。當一個關鍵值是因為衝突後移的話,若後移中的某個值之後刪除了,就無法找到這個值了
刪除函式的實現:
void Remove(const K& key)
{
//刪除僅需要將狀態修改
Node* delNode = find(key);
if (delNode)
delNode->_status = DELETE;
}
先查詢是否存在,存在的話只需要將狀態位修改成刪除即可
擴容函式的實現:
載荷因子超過0.7時,進行擴容
void CheckCapacity()
{
//如果_v為空,則擴容到7
if (_v.empty())
{
_v.resize(_PrimeList[0]);
return;
}
//如果超過載荷因子,則需要擴容
if (_size*10 / _v.size() >= 7)
{
/*size_t newSize = 2 * _v.size();*/
size_t index = 0;
while (_PrimeList[index] < _v.size())
{
index++;
}
size_t newSize = _PrimeList[index];
//用一個臨時變數來儲存新生成的雜湊表
//生成完成後,將其和_v交換
HashTable<K, V> tmp(newSize);
for (size_t i = 0; i < _v.size(); ++i)
{
//如果存在,則將該位置的雜湊值插入到臨時的雜湊表中
if (_v[i]._status == EXIST)
tmp.Insert(_v[i]._key,_v[i]._value);
}
//交換兩個雜湊表
Swap(tmp);
}
}
開鏈法 雜湊桶
節點的定義:
//節點的定義
template<class K, class V>
//struct HushNode e2:拼寫錯誤,Hash not hush
struct HashNode
{
K _key;
V _value;
HashNode<K, V>* _next;
HashNode(const K& key, const V& value)
:_key(key)
, _value(value)
, _next(NULL)
{}
};
插入函式的實現:
//插入函式的實現
pair<Node*, bool> Insert(const K& key, const V& value)
{
//插入之前要先進行是否擴容的檢查
CheckCapacity();
//找到這個數的位置
size_t index = _HushFunc(key);
Node* cur = _ht[index];
while (cur)
{
if (cur->_key == key)
return make_pair(cur, false);
cur = cur->_next;
}
Node* temp = new Node(key, value);
temp->_next = _ht[index];
_ht[index] = temp;
return make_pair(temp, true);
}
用pair可以返回插入成功後節點的指標(或插入失敗,相同關鍵值的指標),以及判斷插入成功與否的TRUE或FALSE
刪除函式的實現:
//刪除函式的實現
void Erase(const K& key)
{
//先進行查詢這個數的位置
size_t index = _HushFunc(key);
Node* pre = NULL;
Node* cur = _ht[index];
//刪除的時候要進行情況的分析討論
while (cur)
{
if (pre == NULL)
{
_ht[index] = cur->_nex;
delete cur;
}
else
{
pre->_next = cur->_next;
}
cur = cur->_next;
}
delete cur;
}
刪除元素的時候,需要分兩種情況
a、鏈上有且僅有該節點
出現這種情況,就是pre為空時,僅需修改vector對應位置儲存的指標
b、鏈上除該節點外,還有多個節點
將cur的指向下一個節點的指標給pre的下一個
查詢函式的實現:
//查詢函式的實現
Node* Find(const K& key)
{
for (int i = 0; i<_size; i++)
{
Node* cur = _ht[i];
while (cur)
{
if (cur->_key == key)
return cur;
cur = cur->_next;
}
}
return NULL;
}
擴容函式的實現:
void CheckCapacity()
{
//當雜湊桶為空的時候或者負載因子為1的時候,進行擴容
if (_ht.size() == 0 || _size / _ht.size() == 1)
{
//建一個新表
HushTable temp;
size_t newsize = GetNewSize();
temp._ht.resize(newsize);
//將元素放進去
for (int i = 0; i<_ht.size(); i++)
{
Node* cur = _ht[i];
while (cur)
{
temp.Insert(cur->_key, cur->_value);
cur = cur->_next;
}
}
}
}
與線性探測不同的是,這裡當載荷因子為1的時候進行擴容;
雜湊桶的一種極端情況是,所有值都往同一個位置進行插入
如下:
解決方法:
當一個鏈上的節點數量達到一個百分比(自己定義,可以是100%,50%,10%),則不再儲存連結串列,而是儲存一顆紅黑樹
這一避免出現這種極端情況而帶了的弊端
雜湊表的github程式碼連結
雜湊桶的github程式碼連結