哈希相關概念
哈希:用來進行高效查找的一種數據結構
首先,我們查找的方式有:
(1)順序查找,它的時間複雜度是O(n)
(2)二分查找(有序),它的時間複雜度是O(log2N)
(3)利用二叉平衡搜索樹(AVL、紅黑樹):時間複雜度是O(log2N)
(4)哈希,時間複雜度是O(1)
前三種方法元素之間都要進行比較,因此時間複雜度降不下來,而哈希元素之間不需要比較(最差情況下只需少許比較即可找到),因此時間複雜度小。
那麼哈希的原理就是通過某種方式,將元素與其在空間中的位置建立一一對應的關係,例如:
給一個容量爲capacity的空間,按照func(x)=x%capacity的方法來存儲元素,比如存儲21、67、90、33、5,func(21)=21%10=1,也就是21放在1號位置;func(67)=67%10=7,放在7號位置,func(90)=0,放在0號位置,func(33)=3,放在3號位置,func(5)=5,放在5號位置,這樣就把元素放好了,也就是一個表格;
然後進行查找,第一步就是通過func(x)找元素在表格中的存儲位置,然後驗證是否爲所找的元素即可;
這種思想就是哈希的思想,也可以稱作爲散列,func(x)稱爲哈希函數,建立的表格就是哈希表。
但是這種方法有一個缺陷,例如向這個哈希表中放入一個11,func(11)=1,應該將11放在1號位置,而這時1號位置已經放了一個元素,這時如果再向裏面放元素就發生了覆蓋(衝突),也就是不同的元素計算出相同的哈希地址,這種情況稱爲哈希衝突。
哈希衝突的解決方式:
(1)哈希函數可能會導致哈希衝突:可以重新設計哈希函數:
注意:哈希地址必須在哈希表格的範圍內,產生的哈希地址儘可能的均勻分佈,哈希函數儘可能簡單;但是一個哈希函數無論設計的多精妙,都無法完全解決哈希衝突,只能將發生哈希衝突的概率降低。
(2)存放的元素
常見的哈希函數:
(1)直接定值法:Hash(key)=A*key+B,它的優點是簡單和均勻,缺點是事先要知道關鍵字的分佈,適用於查找比較小連續的情況,例如在字符串中找第一個只出現一次的字符
class Solution {
public:
int firstUniqChar(string s) {
int count[256]={0};//一個字符有256種狀態
//統計每個字符出現的次數
for(int i=0;i<s.size();++i)
{
count[s[i]]++;
}
for(int i=0;i<s.size();++i)
{
if(1==count[s[i]])
return i;
}
return -1;
}
};
(2)除留餘數法,也就是上述我們所說的func(x)=x%capacity的方法,也就是Hash(key)=key%p(如果p是素數,出現哈希衝突的概率較低)
(3)平方取中法(瞭解)例如關鍵字是1234,對他取平方得到1522756,取227作爲他的關鍵字
(4)摺疊法(5)隨機數法(6)數學分析法
解決哈希衝突的方法
- 閉散列
也叫開放定址法,當發生哈希衝突時,如果哈希表未被裝滿,說明在哈希表中必然還有空位置,那麼可以把key存放到衝突位置中的“下一個” 空位置中去,那麼如何找到下一個空位置呢?
1、線性探測
就是從發生衝突的位置開始,依次向後探測,直到尋找到下一個空位置爲止。
那麼怎麼區分要插入的位置是否存在元素或者爲空?
我們可以給個標記:EMPTY表示沒有元素,EXIST表示有元素
如圖:
(1)插入:
通過哈希函數獲取待插入元素在哈希表中的位置,如果該位置中沒有元素則直接插入新元素,如果該位置中有元素髮生哈希衝突,使用線性探測找到下一個空位置,插入新元素,如上圖插入44,44%10=4,但是4這個位置已經放了值爲4的元素,因此要向後找下一個空位置插入
(2)查找:通過哈希函數計算元素在哈希表中的位置,檢測該位置是否有元素,然後檢測該元素是否爲要查找元素
(3)刪除:採用閉散列處理哈希衝突時,不能隨便物理刪除哈希表中已有的元素,若直接刪除元素會影響其他元素的搜索,例如上圖如果將4刪除,將標記改爲EMPTY,會影響44的搜索,導致44找不到(44的哈希函數計算出來應該在4位置,但是此時4位置標記沒有元素,所以會影響)因此不能這樣進行刪除,因此線性探測採用標記的僞刪除法來刪除一個元素,也就是使用DELETE標記這個位置刪除了元素
當刪除一個元素,理論上這個位置就可以插入元素了,但是有特殊情況,例如上圖,將5刪除,此時如果要插入44,按理來說可以插到5這個位置,但是此時哈希表中已經有44這個元素,就會衝突(如果哈希表要求元素唯一性)
(4)判斷是否增容?
那麼在插入操作時,什麼時候增容,怎麼增容,這裏涉及到散列表的負載因子(填入表中的個數/散列表的長度),負載因子越大,表示填入表中的元素越多,產生衝突的可能性就越大;負載因子越小,填入表中的元素越少,產生衝突的可能性就越小;
對於線性探測,負載因子一般控制在0.7到0.8之間,這裏我們實現增容將負載因子控制在0.7,在進行擴容時,我們不能使用傳統的方法進行擴容(傳統的是將容量擴大,然後原封不動地將元素拷貝過來),因爲在容量發生改變後,哈希函數也會發生改變,這樣在擴容之後,就找不到元素了,因此我們必須再創建一個哈希表,然後設置它的容量是原來哈希表的容量的擴大版(我們這裏是擴大2倍),然後將原來哈希表中存在的元素(即狀態是EXIST的元素)插入到新的哈希表中,然後將2個哈希表交換即可(這裏實現交換函數先交換內容,再交換有效元素個數)
最終的代碼是:
#pragma once
#include <iostream>
using namespace std;
#include <vector>
//假設哈希表格中的元素是唯一的
enum State
{
EMPTY, EXIST, DELETE
};
template<class T>
struct Elem
{
T _value;//元素值域
State _state;//狀態
};
template<class T>
class HashTable
{
public:
HashTable(size_t capacity = 10)
:_ht(capacity)
, _size(0)
{
for (auto& e : _ht)
{
e._state = EMPTY;//將表格的初始狀態初始化爲空
}
}
bool Insert(const T& val)
{
//檢測是否需要擴容
CheckCapacity();
//通過哈希函數計算元素在哈希表中的存儲位置
size_t HashAddr = HashFunc(val);
//檢測該位置是否可以插入元素
//發生哈希衝突,使用線性探測來解決
while (_ht[HashAddr]._state != EMPTY)
{
if (EXIST == _ht[HashAddr]._state && val == _ht[HashAddr]._value)
{
//就不用插入了,衝突
return false;
}
//使用線性探測繼續往後找,直到找到空位
++HashAddr;
if (HashAddr == _ht.capacity())
HashAddr = 0;//如果找到最後一個還沒有找到空位,從頭開始
}
//肯定不會讓哈希表中的元素放的太多,因爲發生衝突的概率會提高,這樣哈希表查找的效率就會降低
//所以不用考慮造成死循環的情況
//找到空的位置,進行插入
_ht[HashAddr]._value = val;
_ht[HashAddr]._state = EXIST;
++_size;
return true;
}
int Find(const T& val)
{
size_t HashAddr = HashFunc(val);//計算哈希地址
while (_ht[HashAddr]._state != EMPTY)//這個位置可能有元素
{
if (_ht[HashAddr]._state == EXIST && _ht[HashAddr]._value == val)
{
return HashAddr;
}
//如果這個位置是刪除或者不等於要找的值,就哈希衝突,線性探測
HashAddr++;
if (HashAddr == _ht.capacity())
HashAddr = 0;
}
return -1;//沒有這個元素
}
bool Erase(const T& val)
{
int index = Find(val);//在哈希表中找這個元素的位置
if (index != -1)//找到了
{
_ht[index]._state = DELETE;
_size--;
return true;
}
return false;
}
void Swap(HashTable<T>& ht)
{
_ht.swap(ht._ht);//先交換內容
swap(_size, ht._size);
}
private:
size_t HashFunc(const T& val)
{
return val%_ht.capacity();
}
void CheckCapacity()
{
//有效元素與容量的比率稱爲負載因子,因爲_size/_ht.capacity()永遠是0,因爲都是整形,所以給_size乘10/容量>7即可
if (_size*10 / _ht.capacity() >= 7)//需要擴容
{
//無法使用原來的哪種方法擴容,因爲現在哈希函數是val%容量,擴容使容量發生改變,哈希函數也就會變化,
//如果採用原來的方法將元素進行搬移,可能導致元素找不到了,因此要重新找一種方法來擴容
//涉及兩個問題:元素怎麼搬移和搬移哪些元素(只搬移狀態爲存在的元素)
//(1)構造新的哈希表,將容量給爲新容量(這裏是原容量的2倍)
HashTable<T> newHT(_ht.capacity() * 2);
//(2)將原哈希表中狀態爲存在的元素插入到新的哈希表中
for (size_t i = 0; i < _ht.capacity(); ++i)
{
if (_ht[i]._state == EXIST)
newHT.Insert(_ht[i]._value);
}
//(3)交換兩個哈希表
Swap(newHT);
}
}
private:
std::vector<Elem<T>> _ht;
size_t _size;//哈希表中有效元素個數
};
void TesthashTable()
{
HashTable<int> ht;
ht.Insert(4);
ht.Insert(7);
ht.Insert(8);
ht.Insert(27);
ht.Insert(9);
ht.Insert(5);
ht.Insert(3);
ht.Insert(1);
ht.Erase(2);
ht.Erase(8);
}
線性探測的優點:實現比較簡單
線性探測的缺點:容易產生數據堆積,一旦發生衝突,發生衝突的元素可能會連在一起,因爲線性探測解決哈希衝突的方式是從發生哈希衝突的位置挨着往後逐次找的。
線性探測解決哈希衝突:源代碼(github):
https://github.com/wangbiy/C-3/commit/39295e148283facd9865e395f476f484ce241ba2
2、二次探測
由於線性探測的缺陷是產生的數據堆積在一起,是因爲解決哈希衝突的方式是從發生衝突的位置逐次往後找的,因此我們從這裏着手,二次探測爲了避免這個問題,找下一個空位置的方法是:比如H0是第一次計算出的哈希地址,Hi代表第i次的哈希地址,Hi=H0+i ^ 2或者Hi=H0-i ^ 2
我們可以利用數學方法將第i+1次的哈希地址算出來,Hi=H0+i ^2,Hi+1=H0+(i+1) ^2,Hi+1-Hi=2i+1,則Hi+1=Hi+2i+1,採用二次探測來解決的話,如果越界不採用線性探測直接將哈希地址置爲0的方法,而是隻要讓哈希地址取模容量即可,這樣結果肯定不一樣。
線性探測和二次探測多必須考慮負載因子,超過0.7-0.8就增容,增大效率,其中刪除是惰性刪除,即只標記刪除記號。
即實現代碼:
bool Insert(const T& val)
{
//檢測是否需要擴容
CheckCapacity();
//通過哈希函數計算元素在哈希表中的存儲位置
size_t HashAddr = HashFunc(val);
int i = 0;//代表第i次探測
//檢測該位置是否可以插入元素
//發生哈希衝突,使用二次探測來解決
while (_ht[HashAddr]._state != EMPTY)
{
if (EXIST == _ht[HashAddr]._state && val == _ht[HashAddr]._value)
{
//就不用插入了,衝突
return false;
}
i++;
if (IsLine)//使用線性探測
{
++HashAddr;
if (HashAddr == _ht.capacity())
HashAddr = 0;//如果找到最後一個還沒有找到空位,從頭開始
}
else//使用二次探測
{
HashAddr = HashAddr + 2 * i + 1;//利用數學方法
//不能直接使用線性探測越界的方法,因爲很有可能造成死循環
HashAddr %= _ht.capacity();//我們採用這種取模的方式來解決越界,這樣每次的取模的結果都是不一樣的
}
}
//肯定不會讓哈希表中的元素放的太多,因爲發生衝突的概率會提高,這樣哈希表查找的效率就會降低
//所以不用考慮造成死循環的情況
//找到空的位置,進行插入
_ht[HashAddr]._value = val;
_ht[HashAddr]._state = EXIST;
++_size;
return true;
}
int Find(const T& val)
{
size_t HashAddr = HashFunc(val);//計算哈希地址
int i = 0;
while (_ht[HashAddr]._state != EMPTY)//這個位置可能有元素
{
if (_ht[HashAddr]._state == EXIST && _ht[HashAddr]._value == val)
{
return HashAddr;
}
i++;
if (IsLine)//使用線性探測
{
++HashAddr;
if (HashAddr == _ht.capacity())
HashAddr = 0;//如果找到最後一個還沒有找到空位,從頭開始
}
else//使用二次探測
{
HashAddr = HashAddr + 2 * i + 1;//利用數學方法
//不能直接使用線性探測越界的方法,因爲很有可能造成死循環
HashAddr %= _ht.capacity();//我們採用這種取模的方式來解決越界,這樣每次的取模的結果都是不一樣的
}
}
return -1;//沒有這個元素
}
在進行插入和查找時判斷使用線性探測還是二次探測,即多增加一個IsLine類型來判斷。
但是二次探測的缺陷是0000000,讓他的負載因子再小一點即可。
閉散列最大的缺陷就是空間利用率比較低,這也是哈希的缺陷。
- 開散列
開散列法又叫鏈地址法(開鏈法),首先對關鍵碼集合用散列函數計算散列地址,具有相同地址的關鍵碼歸於同一子集合,每一個子集合稱爲一個桶(哈希桶),各個桶中的元素通過一個單鏈錶鏈接起來,各鏈表的頭結點存儲在哈希表中。
開散列中每個桶中放的都是發生哈希衝突的元素,這裏哈希桶中放的是鏈表第一個結點的地址(元素的類型就是Node*)。
但是如果每個桶是單鏈表的結構,桶的個數是一定的,隨着元素的不斷插入,每個桶中元素的個數不斷增多,極端情況下,可能會導致一個桶中鏈表節點非常多,會影響的哈希表的性能,因此,我們的哈希桶可以用紅黑樹實現,這樣的效率就比較高,會緩解這種極端的壓力(但是一般不使用這種方式),又或者我們可以對當前的哈希表進行擴容,那麼我們在什麼時機進行增容呢?開散列最優的情況是每個哈希桶中剛好掛一個元素,再進行插入元素時,每一次都會發生衝突,因此,在元素數剛好等於哈希桶的個數時,就開始給哈希表增容。
開散列的實現:
#pragma once
#include <iostream>
using namespace std;
#include <vector>
template <class T>
//默認哈希表中的元素是唯一的
struct HBNode
{
T _data;
HBNode<T>* _pNext;
HBNode(const T& data)
:_data(data)
, _pNext(nullptr)
{}
};
template <class T>
class HashBucket
{
public:
HashBucket(size_t capacity)
:_table(capacity,nullptr)
, _size(0)
{}
~HashBucket()
{
clear();
}
bool Insert(const T& data)
{
CheckCapacity();
size_t bucketNo = HashFunc(data);//計算桶號,即第一個結點
//檢測該元素是否在桶中
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return false;
pCur = pCur->_pNext;
}
//插入元素
pCur = new HBNode<T>(data);
//頭插,性能比較高
pCur->_pNext = _table[bucketNo];
_table[bucketNo] = pCur;
++_size;
return true;
}
HBNode<T>* Find(const T& data)
{
size_t bucketNo = HashFunc(data);
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return pCur;
pCur = pCur->_pNext;
}
return nullptr;
}
bool Erase(const T& data)
{
size_t bucketNo = HashFunc(data);
HBNode<T>* pCur = _table[bucketNo];
HBNode<T>* pPre = nullptr;
while (pCur)
{
if (pCur->_data == data)//找到了想要刪除的數
{
if (pCur == _table[bucketNo])//如果想要刪的數的位置在這個桶的第一個結點,也就是_table[bucketNo]
{
_table[bucketNo] = pCur->_pNext;//刪除的是第一個結點,也就是頭刪
}
else//刪除的不是第一個結點,就是任意位置刪除
{
pPre->_pNext = pCur->_pNext;
}
delete pCur;
--_size;
return true;
}
pPre = pCur;
pCur = pCur->_pNext;
}
return false;
}
size_t Size()const
{
return _size;
}
void Swap(HashBucket<T>& hb)
{
_table.swap(hb._table);
swap(_size, hb._size);
}
void clear()
{
for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
{
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
//頭刪
_table[bucketNo] = pCur->_pNext;
delete pCur;
pCur = _table[bucketNo];
}
}
_size = 0;
}
void Print()
{
for (size_t i = 0; i < _table.capacity(); ++i)
{
cout << "H[" << i << "]" << ":";
HBNode<T>* pCur = _table[i];
while (pCur)
{
cout << pCur->_data << "----->";
pCur = pCur->_pNext;
}
cout << "NULL" << endl;
}
}
private:
//哈希函數
size_t HashFunc(const T& data)
{
return data % _table.capacity();
}
void CheckCapacity()
{
if (_size == _table.capacity())
{
HashBucket<T> newHB(_size * 2);
for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
{
HBNode<T>* pCur = _table[bucketNo];
////但是這種方式不太好,效率比較低,因爲又重新創建了結點
//while (pCur)
//{
// newHB.Insert(pCur->_data);
// pCur = pCur->_pNext;
//}
while (pCur)
{
//1、計算當前結點在新哈希桶中的新桶號
size_t newbucketNo = newHB.HashFunc(pCur->_data);
//2、將結點從哈希表的哈希桶中拆下來
_table[bucketNo] = pCur->_pNext;//將pCur拿出來
//頭插
pCur->_pNext = newHB._table[newbucketNo];
newHB._table[newbucketNo] = pCur;
pCur = _table[bucketNo];//讓pCur到原哈希桶的下一個節點
}
}
Swap(newHB);
}
}
private:
vector<HBNode<T>*> _table;//哈希表
size_t _size;
};
但是這個還是有缺陷,由於我們的哈希函數使用的是除留餘數法,最好模一個素數,因此如果哈希函數中快速找到具有兩倍關係的素數,這樣的就比較高效,還有一個問題就是我們實現的開散列只能存放整形元素,現在對他進行改造讓哈希桶可以存儲任意類型的元素
(1)我們使用一個比當前元素的兩倍的素數的集合即可,即common.cpp實現這一功能
(2)我們想要存儲任意類型的元素,可以使用仿函數,最終的代碼就是:
common.h
#pragma once
size_t GetNextPrime(size_t prime);//獲得prime的下一個素數
common.cpp
#include "common.h"
const int PRIMECOUNT = 28;
const size_t primeList[PRIMECOUNT] =
{
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
};
size_t GetNextPrime(size_t prime)
{
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[PRIMECOUNT-1];
}
HashBucket.hpp
#pragma once
#include "common.h"
#include <iostream>
using namespace std;
#include <vector>
#include <string>
template <class T>
//默認哈希表中的元素是唯一的
struct HBNode
{
T _data;
HBNode<T>* _pNext;
HBNode(const T& data)
:_data(data)
, _pNext(nullptr)
{}
};
//T--->整形系列
template <class T>
class DFDef//實現仿函數
{
public:
T operator()(const T& data)
{
return data;
}
};
//T--->string
size_t BKDRHash(const char* str)
{
register size_t hash = 0;
while (size_t ch = (size_t)*str++)
{
hash = hash * 131 + ch;
}
return hash;
}
class StringToINT
{
public:
size_t operator()(const string& s)
{
return BKDRHash(s.c_str());//直接返回地址
}
};
template <class T, class HF = DFDef<int>>//HF設置爲按照默認爲整數的方式來處理
class HashBucket
{
public:
HashBucket(size_t capacity=10)
:_table(GetNextPrime(capacity), nullptr)
, _size(0)
{}
~HashBucket()
{
clear();
}
bool Insert(const T& data)
{
CheckCapacity();
size_t bucketNo = HashFunc(data);//計算桶號,即第一個結點
//檢測該元素是否在桶中
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return false;
pCur = pCur->_pNext;
}
//插入元素
pCur = new HBNode<T>(data);
//頭插,性能比較高
pCur->_pNext = _table[bucketNo];
_table[bucketNo] = pCur;
++_size;
return true;
}
HBNode<T>* Find(const T& data)
{
size_t bucketNo = HashFunc(data);
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return pCur;
pCur = pCur->_pNext;
}
return nullptr;
}
bool Erase(const T& data)
{
size_t bucketNo = HashFunc(data);
HBNode<T>* pCur = _table[bucketNo];
HBNode<T>* pPre = nullptr;
while (pCur)
{
if (pCur->_data == data)//找到了這個桶
{
if (pCur == _table[bucketNo])//找到了
{
_table[bucketNo] = pCur->_pNext;//刪除的是第一個結點,也就是頭刪
}
else//刪除的不是第一個結點,就是任意位置刪除
{
pPre->_pNext = pCur->_pNext;
}
delete pCur;
--_size;
return true;
}
pPre = pCur;
pCur = pCur->_pNext;
}
return false;
}
size_t Size()const
{
return _size;
}
void Swap(HashBucket<T,HF>& hb)
{
_table.swap(hb._table);
swap(_size, hb._size);
}
void clear()
{
for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
{
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
//頭刪
_table[bucketNo] = pCur->_pNext;
delete pCur;
pCur = _table[bucketNo];
}
}
_size = 0;
}
void Print()
{
for (size_t i = 0; i < _table.capacity(); ++i)
{
cout << "H[" << i << "]" << ":";
HBNode<T>* pCur = _table[i];
while (pCur)
{
cout << pCur->_data << "----->";
pCur = pCur->_pNext;
}
cout << "NULL" << endl;
}
}
private:
//哈希函數
size_t HashFunc(const T& data)
{
return HF()(data) % _table.capacity();//仿函數的方式
}
void CheckCapacity()
{
size_t oldCapacity = _table.capacity();
if (_size == oldCapacity)
{
HashBucket<T,HF> newHB(GetNextPrime(oldCapacity));//改爲兩倍的素數關係
for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
{
HBNode<T>* pCur = _table[bucketNo];
////但是這種方式不太好,效率比較低,因爲又重新創建了結點
//while (pCur)
//{
// newHB.Insert(pCur->_data);
// pCur = pCur->_pNext;
//}
while (pCur)
{
//1、計算當前結點在新哈希桶中的新桶號
size_t newbucketNo = newHB.HashFunc(pCur->_data);
//2、將結點從哈希表的哈希桶中拆下來
_table[bucketNo] = pCur->_pNext;//將pCur拿出來
//頭插
pCur->_pNext = newHB._table[newbucketNo];
newHB._table[newbucketNo] = pCur;
pCur = _table[bucketNo];//讓pCur到原哈希桶的下一個節點
}
}
Swap(newHB);
}
}
private:
vector<HBNode<T>*> _table;//哈希表
size_t _size;
};
void TestHashBucket1()
{
HashBucket<int> ht(10);
ht.Insert(3);
ht.Insert(8);
ht.Insert(4);
ht.Insert(0);
ht.Insert(7);
ht.Insert(13);
ht.Insert(33);
cout << ht.Size() << endl;
ht.Print();
ht.Erase(13);
ht.Print();
ht.Erase(3);
if (nullptr == ht.Find(3))
cout << "3 is not in" << endl;
else
cout << "3 is in" << endl;
ht.clear();
cout << ht.Size() << endl;
}
void TestHashBucket2()
{
HashBucket<string, StringToINT> ht;
ht.Insert("hello");
ht.Insert("CCP");
ht.Insert("I");
ht.Insert("Love");
ht.Insert("You");
ht.Print();
cout << ht.Size() << endl;
ht.Erase("I");
ht.Print();
ht.Erase("hello");
if (nullptr == ht.Find("hello"))
{
cout << "hello is not in" << endl;
}
else
cout << "hello is in" << endl;
cout << ht.Size() << endl;
}
創建兩個仿函數類,一個是默認的整形,一個是由字符串轉爲整形,使用提供的哈希方法BKDRHash來講字符串轉換爲整形,然後增加一個模板類型HF,在哈希函數那裏按照仿函數調用的方法將元素按照類型實現即可。
(3)接下來我們要給這個開散列增加迭代器操作
我們先封裝迭代器類,然後實現即可
//實現迭代器的操作
template <class T,class KeyOfValue,class HF=DFDef<int>>//通過KeyOfValue實現通過key來獲取value
class HashBucket;
template <class T,class KeyOfValue,class HF>//通過KeyOfValue實現通過key來獲取value
struct HBIterator
{
typedef HBNode<T> Node;
typedef HBIterator<T,HF> Self;
public:
HBIterator(Node* pNode,HashBucket<T,HF>* ht)//構造函數
:_pNode(pNode)
, _ht(ht)
{}
T& operator*()
{
return _pNode->_data;
}
T* operator->()
{
return &(operator*());
}
//迭代器移動,不能--,因爲哈希桶的結構是單鏈表
//遍歷存在的桶,將一個桶的鏈表遍歷完成後再遍歷下一個桶的鏈表
Self& operator++()//前置++
{
Next();
return *this;
}
Self& operator++(int)
{
Self tmp(*this);
Next();
return tmp;
}
void Next()
{
if (_pNode->_pNext)//不爲空,當前鏈表還沒有處理完成
{
_pNode = _pNode->_pNext;
}
else//找下一個存在的桶
{
size_t bucketNo = _ht->HashFunc(_pNode->_data) + 1;//哈希函數是哈希桶類的私有成員函數,需要使用友元類
for (; bucketNo < _ht->BucketCount(); ++bucketNo)//BucketCount表示桶的數量
{
if (_ht->_table[bucketNo])
{
_pNode = _ht->_table[bucketNo];
return;
}
}
_pNode = nullptr;
}
}
bool operator!=(const Self& s)const
{
return _pNode != s._pNode && _ht == s._ht;//同一個哈希桶的不同結點
}
bool operator==(const Self& s)const
{
return !(*this != s);
}
private:
Node* _pNode;
HashBucket<T, KeyOfValue,HF>* _ht;
};
分析:定義成員變量_pNode和哈希桶,(1)對指針的操作(operator* 和operator->)進行重載,然後進行迭代器的移動,最後實現迭代器的比較(記得是同一個哈希桶中結點的比較);(2)在哈希桶類中typedef實現的迭代器名 Iterator,然後實現Begin和End即可。
實現代碼:
#pragma once
#include "common.h"
#include <iostream>
using namespace std;
#include <vector>
#include <string>
template <class T>
//默認哈希表中的元素是唯一的
struct HBNode
{
T _data;
HBNode<T>* _pNext;
HBNode(const T& data)
:_data(data)
, _pNext(nullptr)
{}
};
//T--->整形系列
template <class T>
class DFDef//實現仿函數
{
public:
T operator()(const T& data)
{
return data;
}
};
//T--->string
size_t BKDRHash(const char* str)
{
register size_t hash = 0;
while (size_t ch = (size_t)*str++)
{
hash = hash * 131 + ch;
}
return hash;
}
class StringToINT
{
public:
size_t operator()(const string& s)
{
return BKDRHash(s.c_str());//直接返回地址
}
};
//實現迭代器的操作
template <class T,class HF=DFDef<int>>//通過KeyOfValue實現通過key來獲取value
class HashBucket;
template <class T,class HF>//通過KeyOfValue實現通過key來獲取value
struct HBIterator
{
typedef HBNode<T> Node;
typedef HBIterator<T,HF> Self;
public:
HBIterator(Node* pNode,HashBucket<T,HF>* ht)//構造函數
:_pNode(pNode)
, _ht(ht)
{}
T& operator*()
{
return _pNode->_data;
}
T* operator->()
{
return &(operator*());
}
//迭代器移動,不能--,因爲哈希桶的結構是單鏈表
//遍歷存在的桶,將一個桶的鏈表遍歷完成後再遍歷下一個桶的鏈表
Self& operator++()//前置++
{
Next();
return *this;
}
Self& operator++(int)
{
Self tmp(*this);
Next();
return tmp;
}
void Next()
{
if (_pNode->_pNext)//不爲空,當前鏈表還沒有處理完成
{
_pNode = _pNode->_pNext;
}
else//找下一個存在的桶
{
size_t bucketNo = _ht->HashFunc(_pNode->_data) + 1;//哈希函數是哈希桶類的私有成員函數,需要使用友元類
for (; bucketNo < _ht->BucketCount(); ++bucketNo)
{
if (_ht->_table[bucketNo])
{
_pNode = _ht->_table[bucketNo];
return;
}
}
_pNode = nullptr;
}
}
bool operator!=(const Self& s)const
{
return _pNode != s._pNode && _ht == s._ht;//同一個哈希桶的不同結點
}
bool operator==(const Self& s)const
{
return !(*this != s);
}
private:
Node* _pNode;
HashBucket<T,HF>* _ht;
};
template <class T, class HF>//HF設置爲按照默認爲整數的方式來處理
class HashBucket
{
friend HBIterator<T, HF>;//是迭代器類的友元,迭代器類可以訪問哈希桶類的私有成員
typedef HashBucket<T, HF> Self;
public:
typedef HBIterator<T,HF> Iterator;//給迭代器取別名
public:
HashBucket(size_t capacity=10)
:_table(GetNextPrime(capacity), nullptr)
, _size(0)
{}
~HashBucket()
{
clear();
}
Iterator Begin()
{
for (size_t bucketNo = 0; bucketNo < BucketCount(); ++bucketNo)
{
if (_table[bucketNo])
return Iterator(_table[bucketNo],this);
}
return End();
}
Iterator End()
{
return Iterator(nullptr, this);
}
bool Insert(const T& data)
{
CheckCapacity();
size_t bucketNo = HashFunc(data);//計算桶號,即第一個結點
//檢測該元素是否在桶中
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return false;
pCur = pCur->_pNext;
}
//插入元素
pCur = new HBNode<T>(data);
//頭插,性能比較高
pCur->_pNext = _table[bucketNo];
_table[bucketNo] = pCur;
++_size;
return true;
}
HBNode<T>* Find(const T& data)
{
size_t bucketNo = HashFunc(data);
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return pCur;
pCur = pCur->_pNext;
}
return nullptr;
}
bool Erase(const T& data)
{
size_t bucketNo = HashFunc(data);
HBNode<T>* pCur = _table[bucketNo];
HBNode<T>* pPre = nullptr;
while (pCur)
{
if (pCur->_data == data)//找到了這個桶
{
if (pCur == _table[bucketNo])//找到了
{
_table[bucketNo] = pCur->_pNext;//刪除的是第一個結點,也就是頭刪
}
else//刪除的不是第一個結點,就是任意位置刪除
{
pPre->_pNext = pCur->_pNext;
}
delete pCur;
--_size;
return true;
}
pPre = pCur;
pCur = pCur->_pNext;
}
return false;
}
size_t Size()const
{
return _size;
}
size_t BucketCount()
{
return _table.capacity();
}
void Swap(HashBucket<T,HF>& hb)
{
_table.swap(hb._table);
swap(_size, hb._size);
}
void clear()
{
for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
{
HBNode<T>* pCur = _table[bucketNo];
while (pCur)
{
//頭刪
_table[bucketNo] = pCur->_pNext;
delete pCur;
pCur = _table[bucketNo];
}
}
_size = 0;
}
void Print()
{
for (size_t i = 0; i < _table.capacity(); ++i)
{
cout << "H[" << i << "]" << ":";
HBNode<T>* pCur = _table[i];
while (pCur)
{
cout << pCur->_data << "----->";
pCur = pCur->_pNext;
}
cout << "NULL" << endl;
}
}
private:
//哈希函數
size_t HashFunc(const T& data)
{
//HF表示按照仿函數的方式來進行數據類型的實現,如果整形,就是默認的,否則就是字符串轉換爲整形的元素
return HF()(data) % _table.capacity();//仿函數的方式
}
void CheckCapacity()
{
size_t oldCapacity = _table.capacity();
if (_size == oldCapacity)
{
Self newHB(GetNextPrime(oldCapacity));//改爲兩倍的素數關係
for (size_t bucketNo = 0; bucketNo < _table.capacity(); ++bucketNo)
{
HBNode<T>* pCur = _table[bucketNo];
////但是這種方式不太好,效率比較低,因爲又重新創建了結點
//while (pCur)
//{
// newHB.Insert(pCur->_data);
// pCur = pCur->_pNext;
//}
while (pCur)
{
//1、計算當前結點在新哈希桶中的新桶號
size_t newbucketNo = newHB.HashFunc(pCur->_data);
//2、將結點從哈希表的哈希桶中拆下來
_table[bucketNo] = pCur->_pNext;//將pCur拿出來
//頭插
pCur->_pNext = newHB._table[newbucketNo];
newHB._table[newbucketNo] = pCur;
pCur = _table[bucketNo];//讓pCur到原哈希桶的下一個節點
}
}
Swap(newHB);
}
}
private:
vector<HBNode<T>*> _table;//哈希表
size_t _size;
};
//這兩個測試用例是沒有增加KeyOfValue的方法時測試的
void TestHashBucket1()
{
HashBucket<int> ht(10);
ht.Insert(3);
ht.Insert(8);
ht.Insert(4);
ht.Insert(0);
ht.Insert(7);
ht.Insert(13);
ht.Insert(33);
ht.Insert(53);
cout << ht.Size() << endl;
auto it = ht.Begin();
while (it != ht.End())
{
cout << *it<<" ";
++it;
}
cout << endl;
ht.Print();
ht.Erase(13);
ht.Print();
ht.Erase(3);
if (nullptr == ht.Find(3))
cout << "3 is not in" << endl;
else
cout << "3 is in" << endl;
ht.clear();
cout << ht.Size() << endl;
}
void TestHashBucket2()
{
HashBucket<string, StringToINT> ht;
ht.Insert("hello");
ht.Insert("CCP");
ht.Insert("I");
ht.Insert("Love");
ht.Insert("You");
ht.Print();
cout << ht.Size() << endl;
ht.Erase("I");
ht.Print();
ht.Erase("hello");
if (nullptr == ht.Find("hello"))
{
cout << "hello is not in" << endl;
}
else
cout << "hello is in" << endl;
cout << ht.Size() << endl;
}
這個就是哈希桶的實現,也就是開散列的總體實現。
下一節我們將使用這個哈希桶類封裝unordered_map結構,此時實現的哈希桶並不能封裝unordered_map和unordered_set,因爲unordered_map是鍵值對<key,value>結構,unordered_set是key結構,因此我們必須實現KeyOfValue的仿函數,在unordered_map中表示從value中提取出key,在unordered_set表示單個key。