HashTable--哈希表,是一種典型的 "key--value" 形式的數據結構,構建這種數據結構的目的,是爲了使用戶經過 key 值快速定位到個人 value ,從而進行相應的增刪查改的工做。當數據量較小時,簡單遍歷也能達到目的,但面對大量數據處理時,形成時間和空間上的消耗,不是通常人能夠承擔的起的。
算法
首先,先簡單瞭解一下,什麼是哈希。
數組
咱們的目的是在一堆數據中查找(這裏以×××爲例),爲了節省空間,咱們不可能列出一個有全部數據範圍那麼大的數組,所以這裏咱們須要創建一種映射關係HashFunc,把個人全部數據經過映射放到這段空間當中,所以哈希表也叫做散列表。
數據結構
關於映射關係 HashFunc ,常見的有如下幾種:
框架
1>直接定值法
ide
直接定址法是最簡單的方法,取出關鍵字以後,直接或經過某種線性變換,做爲個人散列地址。例如:Hash(key) = key*m+n;其中m、n爲常數。函數
二、除留餘數法
測試
除留餘數法是讓個人關鍵碼 key 經過對個人數組長度取模,獲得的餘數做爲當前關鍵字的哈希地址。ui
除了以上的兩種方法,還有平方取中法、摺疊法、隨機數法、平方分析法等,雖然方法不一樣,但目的都是同樣的,爲了獲得每一個關鍵字 key 的地址碼,這裏再也不贅述。this
哈希衝突 spa
通過 HashFunc 函數處理以後,獲得了每一個關鍵字的哈希地址,正如上面提到的,很容易出現兩個關鍵碼的哈希地址相同或者哈希地址已經被其餘關鍵字佔用的狀況,這種狀況咱們叫作哈希衝突,或者哈希碰撞。這是在散列表中不可避免的。
這裏定義了一個新名詞--載荷因子α
α = 填入表中的元素個數 / 散列表的長度
載荷因子表示的是填入表中的數據佔表總長度的比例。當咱們在哈希表中查找一個對象時,平均查找長度是載荷因子 α 的函數。散列表的底層是一個vector,當載荷因子超過必定量的時候,咱們須要對vector進行resize擴容,來減少哈希表的插入及查找壓力。
爲了解決哈希衝突,這裏有兩種方法閉散列法<開放定址法>和拉鍊法<哈希桶>
須要指出的一點,開放地址法構造的HashTable,對載荷因子的要求極其重要。應嚴格限制載荷因子在0.7~0.8如下,超過0.8,查表時的不命中率會成指數上升。
上面咱們提到了兩種HashFunc,針對這兩種方法獲得的哈希地址以後,咱們能夠作以下處理。當獲得的地址碼已經被佔用,則我當前 key 的地址向後推移便可。這種方法叫作線性探測。
另外還有一種方法,二次探測法。解決思想是當個人哈希地址衝突後,每次再也不是加1,而是每次加1^2,2^2,3^2...直到找到空的地址,這種方法很明顯能夠將各個數據分開,但會引入一個問題,會致使二次探測了屢次,依然沒有找到空閒的位置。這裏用同一組例子來講明。
除此以外,爲了減小哈希衝突,前人總結出了一組素數表,通過數學計算代表,使用蘇鼠標對其作哈希表的容量,能夠有效的下降哈希衝突。
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 };
代碼實現與解釋
這裏首先給出哈希表中每一個元素結點的類,同時給出 HashTable 的框架除了key、value以外,多增長了一個狀態位,當下面用到的時候,會具體給出緣由。
enum State { EMPTY, EXIST, DELETE }; template <typename K ,typename V> struct KVNode { K _key; V _value; State _state; KVNode(const K& key = K(), const V& value = V()) :_key(key) , _value(value) , _state(EMPTY) {} };
//Hashtable類 template <typename K, typename V> class HashTable { typedef KVNode<K, V> Node; public: HashTable() :_size(0) {} bool Insert(const K& key,const V& value) {} Node* Find(const K& key) {} bool Remove(const K& key) {} protected: vector<Node> _table; size_t _size; };
對於哈希表的插入,能夠分爲如下幾步:
a) 進行容量檢查
b) 經過取模獲得該關鍵字在HashTable中的位置
c) 對獲得的位置進行調整
這裏要注意的一點,由於對於key_value類型的數據結構而言,關鍵字 key 是惟一 的,所以,當在調整的時候發現了和待插入 key 同樣的值,直接返回 false 結束插入函數。
d) 對該位置的key、value、state進行調整
對於哈希表的刪除,這裏採用的是僞刪除法。即找到該 key 以後,將該點的狀態改成DELETE便可,由於咱們在對 vector 進行擴容的時候,是經過resize實現的,不管是增長元素仍是刪除,resize出來的空間不須要去釋放,這裏能夠減小內存的屢次開闢與釋放,提升效率。
另外,假設咱們能夠將這段空間的內容清空,會帶來的問題就是,以前咱們插入的時候,全部通過該結點調整過的key都須要從新移動,不然這個元素咱們再也找不到。這就是咱們這裏引入三個狀態的緣由。
對於哈希表的查找,要考慮的東西就相對比較多了。
當找到該結點的位置以後,若是 key 值不是咱們想要的 key 值,就須要繼續向後找,只有當結點的狀態位EMPTY時,查找纔會中止,固然若是找到的EMPTY仍是沒有找到咱們想要的 key 值,那麼該關鍵字必定不在當前的哈希表中。須要注意,會不會存在一種狀況,當咱們在vector中遍歷的時候,循環條件是當前結點的狀態不爲EMPTY,進入了死循環?會的。這是由於咱們引入了DELETE的結果,設想表中的全部節點都是DELETE或者EXIST狀態,且咱們要查找的key不在HashTable中,死循環是必然的狀況。
下面給出完整的實現代碼:
template <typename K, typename V> class HashTable { typedef KVNode<K, V> Node; public: HashTable() :_size(0) {} bool Insert(const K& key,const V& value) { //容量檢查 _CheckSize(); //獲取關鍵字在HashTable中的位置 size_t index = _GetHashIndex(key); //對位置進行調整 while (_table[index]._state == EXIST) { // 若是插入的key存在,返回false if (_table[index]._key == key) return false; index++;//線性探測 if (index == _table.size()) index = 0; } //找到位置以後改變該位置的狀態 _table[index]._key = key; _table[index]._value = value; _table[index]._state = EXIST; _size++; return true; } Node* Find(const K& key) { // 空表,直接返回 if (_table.empty()) return NULL; size_t index = _GetHashIndex(key); int begin = index; while (_table[index]._state != EMPTY) { if (_table[index]._key == key) { // 該位置爲已刪除結點 if (_table[index]._state == DELETE) return NULL; else return &_table[index]; } //改變循環變量 index++; if (index == _table.size()) index = 0; // 循環一圈,沒有找到 if (index == begin) return NULL; } return NULL; } bool Remove(const K& key) { if (_table.empty()) return false; Node* ret = Find(key); if (ret != NULL) { ret->_state = DELETE; --_size; return true; } else return false; } protected: //獲取key在HashTable中的位置 size_t _GetHashIndex(const K& key) { return key % _table.size(); } //現代寫法 void Swap(HashTable<K, V, HASHTABLE>& ht) { _table.swap(ht._table); } //容量檢查 void _CheckSize() { //空表,或載荷因子大於等於8 if ((_table.size() == 0) || ((_size * 10) / _table.size() >= 8)) { size_t newSize = _GetPrimeSize(_table.size()); HashTable<K, V, HASHTABLE> hasht; hasht._table.resize(newSize); // 將原來的HashTable中的key,value插入到新表中 for (size_t i = 0; i < _table.size(); i++) { if (_table[i]._state == EXIST) { hasht.Insert(_table[i]._key, _table[i]._value); } } this->Swap(hasht); } } // 從素數表找到下次擴容的容量 size_t _GetPrimeSize(const size_t& size) { 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 }; for (size_t i = 0; i < _PrimeSize; i++) { if (_PrimeList[i] > size) return _PrimeList[i]; } return _PrimeList[_PrimeSize-1]; } protected: vector<Node> _table; size_t _size; };
對於拉鍊法,這裏其實是構建了哈希桶。如圖:
一樣一組數據放到拉鍊法構成的哈希表中,每個結點再也不是隻記錄key、value值,這裏多存放了一個指向next的指針,這樣的話vector的每一個點上均可以向下追加任意個結點。拉鍊法彷佛更加的有效,載荷因子在這裏也顯得不是那麼重要,但咱們依舊須要考慮這個問題。載荷因子雖然在這裏的限制比開放地址法更加寬鬆了些,可是若是咱們只是在10個結點下無限制的串加結點,也是會增大查找的時間複雜度。這裏咱們把載荷因子提升到1,減輕增容的壓力。另外,vector中保存的是 Node* ,這是爲了兼容個人向下指針,這樣的話,也就再也不須要狀態標誌。初次以外,其餘部分和開放地址法相比,思想大體相同。
template<typename K,typename V> struct KVNode { K _key; V _value; KVNode<K, V>* _next; KVNode(const K& key, const V& value) :_key(key) , _value(value) , _next(NULL) {} }; template<typename K, typename V> class HashTableList { typedef KVNode<K, V> Node; public: HashTableList() :_size(0) {} Node* Find(const K& key) {} bool Insert(const K& key,const V& value) {} bool Remove(const K& key) {} protected: vector<Node*> _htlist; size_t _size; };
插入結點:這裏我採用的是頭插法,緣由其實很簡單,由於頭插的效率比較高,並且只要簡單想一想,就能夠發現,除告終點已經存在之外,其餘這裏的全部狀況能夠統一來處理,這就大大簡化了代碼的冗雜。
查找結點:查找的話,首先定位到哈希地址,而後只須要在對應的位置向下遍歷便可。
刪除結點:刪除結點不建議調用Find函數,如沒有找到的話,直接返回,但若是找到的話,還須要再找一遍去刪除。因此這裏直接去哈希中找相應的key。首先仍是須要定位到key所對應的哈希地址,只要不爲NULL,就一直向下查找,找不到就返回false,找到了直接 delete 掉就好。
下面給出完整的實現代碼:
template<typename K, typename V> class HashTableList { typedef KVNode<K, V> Node; public: HashTableList() :_size(0) {} Node* Find(const K& key) { if (_htlist.empty()) return NULL; int index = GetHashIndex(key); Node* cur = _htlist[index]; while (cur) { if (cur->_key == key) return cur; cur = cur->_next; } return NULL; } bool Insert(const K& key,const V& value) { _Check(); size_t index = GetHashIndex(key); if (Find(key)) return false; Node* tmp = new Node(key, value); tmp->_next = _htlist[index]; _htlist[index] = tmp; _size++; return true; } bool Remove(const K& key) { if (_htlist.empty()) return false; int index = GetHashIndex(key); Node* cur = _htlist[index]; Node* prev = NULL; while (cur) { if (cur->_key == key) { if (prev == NULL) _htlist[index] = cur->_next; else prev->_next = cur->_next; delete cur; cur = NULL; _size--; return true; } cur = cur->_next; } return false; } void Print() // 測試函數 { for (size_t i = 0; i < _htlist.size(); i++) { Node* cur = _htlist[i]; cout << "the "<< i << "th " << "->"; while (cur) { cout << cur->_key << "->"; cur = cur->_next; } cout << "NULL" << endl; } } protected: int GetHashIndex(const K& key) { return key % _htlist.size(); } void Swap(HashTableList<K, V, __HashList>& ht) { _htlist.swap(ht._htlist); } void _Check() { if (_htlist.empty() || (_size == _htlist.size())) // 載荷因子提高到1 { size_t newsize = GetNewSize(_size); vector<Node*> tmp; tmp.resize(newsize); // 拷貝 for (size_t i = 0; i < _htlist.size(); i++) { Node* cur = _htlist[i]; while (cur) // 哈希鏈處理 { Node* next = cur->_next; _htlist[i] = next; size_t k = cur->_key; cur->_next = tmp[k % newsize]; tmp[k % newsize] = cur; cur = next; } } _htlist.swap(tmp); } } size_t GetNewSize(const size_t& sz) { 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 }; for (size_t i = 0; i < _PrimeSize;i++) { if (sz < _PrimeList[i]) return _PrimeList[i]; } return _PrimeList[_PrimeSize - 1]; } protected: vector<Node*> _htlist; size_t _size; };
關於整數的哈希算法就到這裏,下面給出一張本篇文章的一張簡圖,便於你們理解HashTable。對於字符串哈希的處理,下一篇會進行介紹。