日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

【C++】哈希(闭散列,开散列)

發布時間:2024/1/18 c/c++ 47 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【C++】哈希(闭散列,开散列) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

哈希

  • unordered系列關聯式容器
    • unordered_map&& unordered_set
      • 關聯性容器介紹
      • 介紹
    • 底層結構
      • 哈希概念
      • 哈希沖突
  • 閉散列
    • 閉散列介紹
      • 實現
  • 開散列
    • 介紹
    • 開散列增容機制
    • 實現
      • 開散列和閉散列比較

unordered系列關聯式容器

unordered_map&& unordered_set

關聯性容器介紹

在STL底層map和set使用紅黑樹封裝實現的,而其復雜度為基本為logn因為高度可控,但往后發展還是有大神想出了另一種容器結構也就是哈希。也就是底層用哈希來封裝出map和set

介紹

  • unordered_map是存儲<key, value>鍵值對的關聯式容器,其允許通過keys快速的索引到與其對應的value。
  • 在unordered_map中,鍵值通常用于惟一地標識元素,而映射值是一個對象,其內容與此鍵關聯。鍵和映射值的類型可能不同。
  • 在內部,unordered_map沒有對<kye, value>按照任何特定的順序排序, 為了能在常數范圍內找到key所對應的value,unordered_map將相同哈希值的鍵值對放在相同的桶中。
  • unordered_map容器通過key訪問單個元素要比map快,但它通常在遍歷元素子集的范圍迭代方面效率較低。
  • unordered_maps實現了直接訪問操作符(operator[]),它允許使用key作為參數直接訪問value。
  • 它的迭代器至少是前向迭代器。
    注意:unordered_map中key是不能重復的,因此count函數的返回值最大為1
    無論是unordered_map/se其使用規則和map和set沒有多大差別就是底層實現不同。效率不同。
  • 底層結構

    哈希概念

    順序結構以及平衡樹中,元素關鍵碼與其存儲位置之間沒有對應的關系,因此在查找一個元素時,必須要經過關鍵碼的多次比較。順序查找時間復雜度為O(N),平衡樹中為樹的高度,即O(N),搜索的效率取決于搜索過程中元素的比較次數。

    理想的搜索方法:可以不經過任何比較,一次直接從表中得到要搜索的元素。 如果構造一種存儲結構,通過某種函數(hashFunc)使元素的存儲位置與它的關鍵碼之間能夠建立一一映射的關系,那么在查找時通過該函數可以很快找到該元素。

    插入元素:
    根據待插入元素的關鍵碼,以此函數計算出該元素的存儲位置并按此位置進行存放

    搜索元素:
    對元素的關鍵碼進行同樣的計算,把求得的函數值當做元素的存儲位置,在結構中按此位置取元素比較,若關鍵碼相等,則搜索成功

    該方式即為哈希(散列)方法,哈希方法中使用的轉換函數稱為哈希(散列)函數,構造出來的結構稱為哈希表(Hash Table)(或者稱散列表)

    例如:
    每次給一個數對其進行取模,得到一個值對其進行插入,有點之前計數排序那意思。

    差的元素多了不可避免地就會有重復的字眼。

    哈希沖突

    對于不同關鍵字取模相同出一樣的哈希地址,這個我們叫做哈希沖突。
    通過哈希函數來解決。
    設計原則:

  • 哈希函數的定義域必須包括需要存儲的全部關鍵碼,而如果散列表允許有m個地址時,其值域必須在0到m-1之間
  • 哈希函數計算出來的地址能均勻分布在整個空間中
  • 哈希函數應該比較簡單
  • 常見哈希函數:直接定制法,除留余數法,平方取中法,折疊法,隨機數法,數學分析法。
    經常用的就是前兩個

    注意:哈希函數設計的越精妙,產生哈希沖突的可能性就越低,但是無法避免哈希沖突

    閉散列

    閉散列介紹

    閉散列:也叫開放定址法,當發生哈希沖突時,如果哈希表未被裝滿,說明在哈希表中必然還有空位置,那么可以把key存放到沖突位置中的“下一個”空位置中去

    1.線性探測
    對于已經沖突的關鍵字,我們對其進行線性查找,通過哈希函數找到對應的位置,若沒有就插入,若有就一次往后找空位置插入。

    這里就對于散列的每一個位置進行狀態的定義,采用閉散列處理哈希沖突時,不能隨便物理刪除哈希表中已有的元素,若直接刪除元素會影響其他元素的搜索。比如刪除元素4,如果直接刪除掉,44查找起來可能會受影響。因此線性探測采用標記的偽刪除法來刪除一個元素

    擴容機制:
    當所有元素插滿時候,其狀態都為存在那么就無法找出對應空的位置一致循環下去,所以我們要保證閉散列不能為滿或者不能使它快滿了,在達到一定的比例時,就要對其進行擴容機制。

    2.二次探測
    可以通過一次探測得出如果同一塊區域有大量的數據堆積在一起,效率會降低,也就是將一個個探測變成次方的查找

    研究表明:當表的長度為質數且表裝載因子a不超過0.5時,新的表項一定能夠插入,而且任何一個位置都不會被探查兩次。因此只要表中有一半的空位置,就不會存在表滿的問題。在搜索時可以不考慮表裝滿的情況,但在插入時必須確保表的裝載因子a不超過0.5,如果超出必須考慮增容。
    空間利用率比較低,哈希的缺陷

    實現

    enum State{EXIST,EMPTY,DELETE,};template<class T>struct HashNode{State _state = EMPTY; // 狀態T _t;};template<class K, class T, class HashFunc = Hash<K>>class HashTable{public:bool Insert(const T& t){// 負載因子>0.7就增容if (_tables.size() == 0 || _size * 10 / _tables.size() == 7){size_t newsize = GetNextPrime(_tables.size());HashTable<K, T> newht;newht._tables.resize(newsize);for (auto& e : _tables){if (e._state == EXIST){// 復用沖突時探測的邏輯newht.Insert(e._t);}}_tables.swap(newht._tables);}HashNode<T>* ret = Find(t);if (ret)return false;size_t start = t % _tables.size();// 線性探測,找一個空位置size_t index = start;size_t i = 1;while (_tables[index]._state == EXIST){index = start + i;index %= _tables.size();++i;}_tables[index]._t = t;_tables[index]._state = EXIST;_size++;return true;}

    如果傳入的是個字符串,那么我們就需要將其轉成ASC||碼進行計算,就需要引入仿函數特化一個結構體如果傳入的是string就去調特化的。

    template<class K> struct Hash {size_t operator()(const K& key){return key;} };// 特化 template<> struct Hash < string > {size_t operator()(const string& s){size_t hash = 0;for (auto ch : s){//hash += ch;hash = hash * 131 + ch;}return hash;} };

    當然查找和刪除也類似:查找存在并且相等的值,遇到空則失敗。刪除就得先查找然后設置狀態。插入的時候要先對其進行查找如果有了就失敗

    開散列

    介紹

    開散列法又叫鏈地址法(開鏈法),首先對關鍵碼集合用散列函數計算散列地址,具有相同地址的關鍵碼歸于同一子集合,每一個子集合稱為一個桶,各個桶中的元素通過一個單鏈表鏈接起來,各鏈表的頭結點存儲在哈希表中。

    開散列增容機制

    開散列的擴容機制不會讓自己的插入影響,如果通過一個桶中插入的節點過多不回去影響其他的只會影響自己的效率,最好的情況就是每個桶中一個節點就是不需要二次搜索,不會出現哈希沖突。

    實現

    當進入size為0就給第一個素數(這里大神們研究的通過開成某些素數大小的容量不容易沖突),創建一個新對象并將舊表上的點一個個的插入新表當中,然后釋放舊表節點并至空。

    pair<Node*, bool> Insert(const T& t){KeyOfT kot;// 負載因子 == 1時增容if (_size == _tables.size()){size_t newsize = GetNextPrime(_tables.size());vector<Node*> newtables;newtables.resize(newsize, nullptr);for (size_t i = 0; i < _tables.size(); i++){// 舊表中節點直接取下來掛到新表Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t index = HashFunc(kot(cur->_t), newtables.size());// 頭插cur->_next = newtables[index];newtables[index] = cur;cur = next;}_tables[i] = nullptr;}newtables.swap(_tables);}size_t index = HashFunc(kot(t), _tables.size());// 查找t在在不在Node* cur = _tables[index];while (cur){if (kot(cur->_t) == kot(t))return make_pair(cur, false);cur = cur->_next;}Node* newnode = new Node(t);newnode->_next = _tables[index];_tables[index] = newnode;return make_pair(newnode, true);}

    開散列和閉散列比較

    應用鏈地址法處理溢出,需要增設鏈接指針,似乎增加了存儲開銷。事實上: 由于開地址法必須保持大量的空閑空間以確保搜索效率,如二次探查法要求裝載因子a <= 0.7,而表項所占空間又比指針大的多,所以使用鏈地址法反而比開地址法節省存儲空間

    總結

    以上是生活随笔為你收集整理的【C++】哈希(闭散列,开散列)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。