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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

高级数据结构与算法 | 哈希 :哈希冲突、负载因子、哈希函数、哈希表、哈希桶

發布時間:2024/4/11 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 高级数据结构与算法 | 哈希 :哈希冲突、负载因子、哈希函数、哈希表、哈希桶 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • 哈希
  • 哈希函數
    • 常見的哈希函數
    • 字符串哈希函數
  • 哈希沖突
    • 閉散列的解決方法
    • 開散列的解決方法
    • 負載因子以及增容
      • 對于閉散列
      • 對于開散列結構
  • 具體實現
    • 哈希表(閉散列)
      • 插入
      • 查找
      • 刪除
      • 完整代碼實現
    • 哈希桶(開散列)
      • 插入
      • 查找
      • 刪除
      • 完整代碼實現

哈希

在之前介紹的數據結構中,元素與其所存儲的位置之間沒有對應的關系,所以在查找的時候就需要經過多次的比較,才能找到具體的位置。對于順序結構來說,這樣查找的時間復雜度一般都是O(N),而對于樹形結構的如搜索樹等則也需要O(logN)。
但是,還存在著這樣一種數據結構,他通過某種方法的處理,使得元素存儲的位置與元素本身建立起了映射關系,此時如果要查找改數據,就可以直接到對應的位置去,使得時間復雜度達到了O(1),這就是哈希(散列)。


哈希函數

哈希函數就是建立起元素與其存儲位置的映射關系。
對于哈希函數來說,必須具有以下特點;

  • 哈希函數的定義域必須包括需要存儲的全部關鍵碼,而如果散列表允許有m個地址時,其值域必須在0 到m-1之間
  • 哈希函數計算出來的地址能均勻分布在整個空間中(防止產生密集的哈希沖突)
  • 哈希函數應該比較簡單

哈希沖突大量出現往往都是因為哈希函數設計的不夠合理,但是即使再優秀的哈希函數,也只能減少哈希沖突的次數,無法避免哈希沖突

常見的哈希函數

  • 直接定址法(常見)
    哈希函數:Hash(Key)= A*Key + B;
    這是最簡單的哈希函數,直接取關鍵字本身或者他的線性函數來作為散列地址。
  • 除留余數法(常見)
    哈希函數 :Hash(key) = key % capacity
    幾乎是最常用的哈希函數,用一個數來對key取模,一般來說這個數都是容量。
  • 平方取中法
    對關鍵字進行平方,然后取中間的幾位來作為地址。
  • 折疊法
    折疊法是將關鍵字從左到右分割成位數相等的幾部分(最后一部分位數可以短些),然后將這幾部分疊加
    求和,并按散列表表長,取后幾位作為散列地址。
    折疊法適合事先不需要知道關鍵字的分布,適合關鍵字位數比較多的情況**
  • 隨機數法
    選擇一個隨機函數,取關鍵字的隨機函數值為它的哈希地址,即H(key) = random(key),其中random為
    隨機數函數。
    通常應用于關鍵字長度不等時采用此法
  • 數學分析法
    設有n個d位數,每一位可能有r種不同的符號,這r種不同的符號在各位上出現的頻率不一定相同,可能
    在某些位上分布比較均勻,每種符號出現的機會均等,在某些位上分布不均勻只有某幾種符號經常出
    現。可根據散列表的大小,選擇其中各種符號分布均勻的若干位作為散列地址

  • 字符串哈希函數

    因為哈希函數的常用方法如直接定址、除留余數、平方取中等方法需要用的key值為整型,而大部分時候我們的key都是string,對于string來說,上面的方法都行不通,因為無法對string進行算數運算,所以需要考慮新的方法。

    常見的字符串哈希算法有BKD,SDB,RS等,這些算法大多通過一些公式來對字符串每一個字符的ascii值或者字符串的大小進行計算,來推導出一個不容易產生沖突的key值。

    例如BKDHash

    struct _Hash<std::string> {const size_t& operator()(const std::string& key){//BKDR字符串哈希函數size_t hash = 0;for (size_t i = 0; i < key.size(); i++){hash *= 131;hash += key[i];}return hash;} };

    這里推薦兩篇文章,一篇具體對比各類字符串哈希函數的效率,一篇是實現。

    字符串Hash函數對比
    各種字符串Hash函數


    哈希沖突

    哈希沖突就是兩個不同的數據通過同一個哈希函數計算出了相同的位置,這種現象就是哈希沖突。

    哈希沖突使得多個數據映射的位置相同,但是每個位置又只能存儲一個數據,所以就需要通過某種方法來解決哈希沖突。

    對于哈希沖突的解決方法,一般根據不同的結構,分為以下幾種


    閉散列的解決方法

    因為閉散列是順序的結構,所以可以通過遍歷哈希表,來將沖突的數據放到空的位置上

  • 線性探測
    線性探測即為從發生沖突的位置開始,依次向后探測,直到尋找到下一個空位置為止。
    這種方法實現起來極為簡單,但是效率也不高,因為如果同一位置產生了大量的哈希沖突,就會導致每次都在同一個位置進行探測,例如我在10這里連續沖突100次,此時所有探測的次數加起來就會高達100!,這樣方法效率十分的低下。

  • 二次探測
    線性探測即為從發生沖突的位置開始,每次往后探測i^2個位置,如1, 2, 4, 8等,這樣的話就將每次探測的效率從O(N)提升到了O(logN),即使有著大量的沖突堆積,也不會導致效率過低。


  • 開散列的解決方法

    因為開散列本身就是一種鏈式的結構,所以其本身就是一種解決方法,這種方法也叫做鏈地址法
    鏈地址法

    鏈地址法在每一個映射位置都建立起一個鏈表(數據過多時可能會轉為建立紅黑樹),將每次插入的數據都直接連接上這個鏈表,這樣就不會像閉散列一樣進行大量的探測,但是如果鏈表過長也會導致效率低下。


    負載因子以及增容

    哈希沖突出現的較為密集,往往代表著此時數據過多,而能夠映射的地址過少,這就導致了隨著數據的增多,沖突的次數就越來越多,而要想解決這個問題,就需要通過負載因子的判斷來進行增容。

    負載因子的大小 = 表中數據個數 / 表的容量

    對于閉散列

    對于閉散列來說,因為其是一種線性的結構,所以一旦負載因子過高,就很容易出現哈希沖突的堆積,所以當負載因子達到一定程度時就需要進行增容,并且增容后,為了保證映射關系,還需要將數據重新映射到新位置。

    經過算法科學家的計算, 負載因子應當嚴格的控制在0.7-0.8以下,所以一旦負載因子到達這個范圍,就需要進行增容。

    因為除留余數法等方法通常是按照表的容量來計算,所以科學家的計算,當對一個質數取模時,沖突的幾率會大大的降低,并且因為增容的區間一般是1.5-2倍,所以算法科學家列出了一個增容質數表,按照這樣的規律增容,沖突的幾率會大大的降低。

    這也是STL中unordered_map/unordered_set使用的增容方法。

    //算法科學家總結出的一個增容質數表,按照這樣增容的效率更高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};

    對于開散列結構

    因為哈希桶是開散列的鏈式結構,發生了哈希沖突是直接在對應位置位置進行頭插,而桶的個數是固定的,而插入的數據會不斷增多,隨著數據的增多,就可能會導致某一個桶過重,使得效率過低。

    所以最理想的情況,就是每個桶都有一個數據。這種情況下,如果往任何一個地方插入,都會產生哈希沖突,所以當數據個數與桶的個數相同時,也就是負載因子為1時就需要進行擴容

    具體實現

    哈希表(閉散列)

    對于閉散列,我們需要通過狀態來記錄一個數據是否在表中,所以這里會使用枚舉來實現。

    enum State {EMPTY,//空EXITS,//存在DELETE,//已經刪除 };template<class T> struct HashData {HashData(const T& data = T(), const State& state = EMPTY): _data(data), _state(state){}T _data;State _state; };

    插入

    插入的思路很簡單,計算出映射的地址后,開始遍歷判斷下面幾種狀態

  • 如果映射位置已存在數據,并且狀態為存在,則說明產生沖突,繼續往后查找
  • 如果映射位置的數據與插入的數據相同,并且狀態為存在,則說明此時數據已經插入過,此時就不需要再次插入
  • 如果映射位置的狀態為刪除或者空,則代表著此時表中沒有這個數據,在這個位置插入即可
  • bool Insert(const T& data) {KeyOfT koft;//判斷此時是否需要增容//當裝填因子大于0.7時增容if (_size * 10 / _table.size() >= 7){//增容的大小按照別人算好的近似兩倍的素數來增,這樣效率更高,也可以直接2倍或者1.5倍。std::vector<HashData> newTable(getNextPrime(_size));for (size_t i = 0; i < _table.size(); i++){//將舊表中的數據全部重新映射到新表中if (_table[i]._state == EXITS){//如果產生沖突,則找到一個合適的位置size_t index = HashFunc(koft(_table[i]._data));while (newTable[i]._state == EXITS){i++;if (i == _table.size()){i = 0;}}newTable[i] = _table[i];}}//最后直接將數據進行交換即可,原來的數據會隨著函數棧幀一起銷毀_table.swap(newTable);}//用哈希函數計算出映射的位置size_t index = HashFunc(koft(data));//從那個位置開始探測, 如果該位置已經存在時,有兩種情況,一種是已經存在,一種是沖突,這里使用的是線性探測while (_table[index]._state == EXITS){//如果已經存在了,則說明不用插入if (koft(_table[index]._data) == koft(data)){return false;}else{index++;index = HashFunc(index);}}//如果走到這里,說明這個位置是空的或者已經被刪除的位置,可以在這里插入_table[index]._data = data;_table[index]._state = EXITS;_size++;return true; }

    查找

    查找也分幾種情況

  • 如果映射的位置為空,則說明查找失敗
  • 如果映射的位置的數據不同,并且狀態為存在,則說明產生沖突,繼續向后查找
  • 如果映射的位置的數據相同,如果狀態為刪除,則說明數據已經刪除,查找失敗,而如果數據為存在,則說明查找成功。
  • HashData* Find(const K& key) {KeyOfT koft;size_t index = HashFunc(key);//遍歷,如果查找的位置為空,則說明查找失敗while (_table[index]._state != EMPTY){//此時判斷這個位置的數據是否相同,如果不同則說明出現哈希沖突,繼續往后查找if (koft(_table[index]._data) == key){//此時有兩個狀態,一種是數據已經被刪除,一種是數據存在。if (_table[index]._state == EXITS){return &_table[index];}else if (_table[index]._state == DELETE){return nullptr;}}index++;//如果index越界,則歸零if (index == _table.size()){index = 0;}}return nullptr; }

    刪除

    直接遍歷查找數據,如果找不到則說明已經被刪除,如果找到了則直接將狀態改為刪除即可

    bool Erase(const K& key) {HashData* del = Find(key);//如果找不到則說明已經被刪除if (del == nullptr){return false;}else{//找到了則直接更改狀態即可del->_state = DELETE;_size--;return true;} }

    完整代碼實現

    #pragma once #include<vector>namespace lee {//算法科學家總結出的一個增容質數表,按照這樣增容的效率更高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};enum State{EMPTY,EXITS,DELETE,};template<class T>struct HashData{HashData(const T& data = T(), const State& state = EMPTY): _data(data), _state(state){}T _data;State _state;};template<class K, class T, class KeyOfT>class HashTable{public:typedef HashData<T> HashData;HashTable(size_t capacity = 10): _table(capacity), _size(0){}size_t getNextPrime(size_t num){size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){//返回比那個數大的下一個質數 if (primeList[i] > num){return primeList[i];}}//如果比所有都大,還是返回最后一個,因為最后一個已經是32位最大容量return primeList[PRIMECOUNT - 1];}//除留余數法size_t HashFunc(const K& key){return key % _table.size();}bool Insert(const T& data){KeyOfT koft;//判斷此時是否需要增容//當裝填因子大于0.7時增容if (_size * 10 / _table.size() >= 7){//增容的大小按照別人算好的近似兩倍的素數來增,這樣效率更高,也可以直接2倍或者1.5倍。std::vector<HashData> newTable(getNextPrime(_size));for (size_t i = 0; i < _table.size(); i++){//將舊表中的數據全部重新映射到新表中if (_table[i]._state == EXITS){//如果產生沖突,則找到一個合適的位置size_t index = HashFunc(koft(_table[i]._data));while (newTable[i]._state == EXITS){i++;if (i == _table.size()){i = 0;}}newTable[i] = _table[i];}}//最后直接將數據進行交換即可,原來的數據會隨著函數棧幀一起銷毀_table.swap(newTable);}//用哈希函數計算出映射的位置size_t index = HashFunc(koft(data));//從那個位置開始探測, 如果該位置已經存在時,有兩種情況,一種是已經存在,一種是沖突,這里使用的是線性探測while (_table[index]._state == EXITS){//如果已經存在了,則說明不用插入if (koft(_table[index]._data) == koft(data)){return false;}else{index++;index = HashFunc(index);}}//如果走到這里,說明這個位置是空的或者已經被刪除的位置,可以在這里插入_table[index]._data = data;_table[index]._state = EXITS;_size++;return true;}HashData* Find(const K& key){KeyOfT koft;size_t index = HashFunc(key);//遍歷,如果查找的位置為空,則說明查找失敗while (_table[index]._state != EMPTY){//此時判斷這個位置的數據是否相同,如果不同則說明出現哈希沖突,繼續往后查找if (koft(_table[index]._data) == key){//此時有兩個狀態,一種是數據已經被刪除,一種是數據存在。if (_table[index]._state == EXITS){return &_table[index];}else if (_table[index]._state == DELETE){return nullptr;}}index++;//如果index越界,則歸零if (index == _table.size()){index = 0;}}return nullptr;}bool Erase(const K& key){HashData* del = Find(key);//如果找不到則說明已經被刪除if (del == nullptr){return false;}else{//找到了則直接更改狀態即可del->_state = DELETE;_size--;return true;}}private:std::vector<HashData> _table;size_t _size;}; };

    哈希桶(開散列)

    開散列也叫哈希桶,桶為每一個映射的位置,桶一般用鏈表或者紅黑樹實現(這里我用的是鏈表)。當我們通過映射的地址,找到存放數據的桶,再對桶進行插入或者刪除操作即可。

    插入

    通過計算映射位置找到對應的桶,再判斷數據是否存在后將數據頭插進去即可(也可以尾插)

    bool Insert(const T& data) {KeyofT koft;/*因為哈希桶是開散列的鏈式結構,發生了哈希沖突是直接在對應位置位置進行頭插,而桶的個數是固定的,而插入的數據會不斷增多,隨著數據的增多,就可能會導致某一個桶過重,使得效率過低。所以最理想的情況,就是每個桶都有一個數據。這種情況下,如果往任何一個地方插入,都會產生哈希沖突,所以當數據個數與桶的個數相同時,也就是負載因子為1時就需要進行擴容。*/if (_size == _table.size()){//按照素數表來增容size_t newSize = getNextPrime(_table.size());size_t oldSize = _table.size();std::vector<Node*> newTable(newSize);_table.resize(newSize);//接著將數據重新映射過去for (size_t i = 0; i < oldSize; i++){Node* cur = _table[i];while (cur){//重新計算映射的位置size_t pos = HashFunc(koft(cur->_data));//找到位置后頭插進對應位置Node* next = cur->_next;cur->_next = newTable[pos];newTable[pos] = cur;cur = next;}//原數據置空_table[i] == nullptr;}//直接和新表交換,交換過去的舊表會和函數棧幀一塊銷毀。_table.swap(newTable);}size_t pos = HashFunc(koft(data));Node* cur = _table[pos];//因為哈希桶key值唯一,如果已經在桶中則返回falsewhile (cur){if (koft(cur->_data) == koft(data)){return false;}else{cur = cur->_next;}}//檢查完成,此時開始插入,這里選擇的是頭插,這樣就可以減少數據遍歷的次數。Node* newNode = new Node(data);newNode->_next = _table[pos];_table[pos] = newNode;++_size;return true; }

    查找

    直接根據映射的位置到桶中查找數據即可

    Node* Find(const K& key) {KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];while (cur){if (koft(cur->_data) == key){return cur;}else{cur = cur->_next;}}return nullptr; }

    刪除

    bool Erase(const K& key) {KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];Node* prev = nullptr;while (cur){if (koft(cur->_data) == key){//如果要刪除的是第一個節點,就讓下一個節點成為新的頭節點,否則直接刪除。if (prev == nullptr){_table[pos] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_size;return true;}else{prev = cur;cur = cur->_next;}}return false; }

    完整代碼實現

    #pragma once #include<vector> #include<string>namespace lee {//算法科學家總結出的一個增容質數表,按照這樣增容的效率更高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};/*因為哈希函數的常用方法如直接定地、除留余數、平方取中等方法需要用的key值為整型,而大部分時候我們的key都是string,或者某些自定義類型,這個時候就可以提供一個仿函數的接口給外部,讓他自己處理如何將key轉換成我們需要的整型*/template<class K>struct Hash{const K& operator()(const K& key){return key;}};template<>struct Hash<std::string>{const size_t & operator()(const std::string& key){//BKDR字符串哈希函數size_t hash = 0;for (size_t i = 0; i < key.size(); i++){hash *= 131;hash += key[i];}return hash;}};template<class T>struct HashNode{HashNode(const T& data = T()): _data(data), _next(nullptr){}T _data;HashNode<T>* _next;};template<class K, class T, class KeyofT, class Hash = Hash<K>>class HashBucket{public:typedef HashNode<T> Node;HashBucket(size_t capacity = 10): _table(capacity), _size(0){}~HashBucket(){Clear();}size_t getNextPrime(size_t num){size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){//返回比那個數大的下一個質數 if (primeList[i] > num){return primeList[i];}}//如果比所有都大,還是返回最后一個,因為最后一個已經是32位最大容量return primeList[PRIMECOUNT - 1];}size_t HashFunc(const K& key){Hash hash;return hash(key) % _table.size();}bool Insert(const T& data){KeyofT koft;/*因為哈希桶是開散列的鏈式結構,發生了哈希沖突是直接在對應位置位置進行頭插,而桶的個數是固定的,而插入的數據會不斷增多,隨著數據的增多,就可能會導致某一個桶過重,使得效率過低。所以最理想的情況,就是每個桶都有一個數據。這種情況下,如果往任何一個地方插入,都會產生哈希沖突,所以當數據個數與桶的個數相同時,也就是負載因子為1時就需要進行擴容。*/if (_size == _table.size()){//按照素數表來增容size_t newSize = getNextPrime(_table.size());size_t oldSize = _table.size();std::vector<Node*> newTable(newSize);_table.resize(newSize);//接著將數據重新映射過去for (size_t i = 0; i < oldSize; i++){Node* cur = _table[i];while (cur){//重新計算映射的位置size_t pos = HashFunc(koft(cur->_data));//找到位置后頭插進對應位置Node* next = cur->_next;cur->_next = newTable[pos];newTable[pos] = cur;cur = next;}//原數據置空_table[i] == nullptr;}//直接和新表交換,交換過去的舊表會和函數棧幀一塊銷毀。_table.swap(newTable);}size_t pos = HashFunc(koft(data));Node* cur = _table[pos];//因為哈希桶key值唯一,如果已經在桶中則返回falsewhile (cur){if (koft(cur->_data) == koft(data)){return false;}else{cur = cur->_next;}}//檢查完成,此時開始插入,這里選擇的是頭插,這樣就可以減少數據遍歷的次數。Node* newNode = new Node(data);newNode->_next = _table[pos];_table[pos] = newNode;++_size;return true;}bool Erase(const K& key){KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];Node* prev = nullptr;while (cur){if (koft(cur->_data) == key){//如果要刪除的是第一個節點,就讓下一個節點成為新的頭節點,否則直接刪除。if (prev == nullptr){_table[pos] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_size;return true;}else{prev = cur;cur = cur->_next;}}return false;}Node* Find(const K& key){KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];while (cur){if (koft(cur->_data) == key){return cur;}else{cur = cur->_next;}}return nullptr;}void Clear(){//刪除所有節點for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}private:std::vector<Node*> _table;size_t _size;}; };

    總結

    以上是生活随笔為你收集整理的高级数据结构与算法 | 哈希 :哈希冲突、负载因子、哈希函数、哈希表、哈希桶的全部內容,希望文章能夠幫你解決所遇到的問題。

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