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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

哈希原理

發布時間:2024/4/11 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 哈希原理 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

哈希原理

C++11提供的unordered系列的容器之所以在查找方面能夠達到O(1)O(1)O(1)的復雜度,是因為其底層使用了哈希的結構

一、哈希的概念

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

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

當向該結構中:

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

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

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

    用該方法進行搜索不必進行多次關鍵碼的比較,因此搜索的速度比較快

    問題:按照上述哈希方式,向集合中插入元素44,會出現什么問題?

    二、哈希沖突

    對于兩個數據元素的關鍵字ki 和kj (i != j),有 ki != kj ,但有:Hash( ki) == Hash(kj),即:不同關鍵字通過 相同哈希哈數計算出相同的哈希地址,該種現象稱為哈希沖突或哈希碰撞。

    把具有不同關鍵碼而具有相同哈希地址的數據元素稱為“同義詞”。 發生哈希沖突該如何處理呢?

    三、哈希函數

    引起哈希沖突的一個原因可能是:哈希函數設計不夠合理。 哈希函數設計原則:

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

    常見哈希函數

  • 直接定制法–(常用)
    取關鍵字的某個線性函數為散列地址:Hash(Key)= A*Key + B
    優點:簡單、均勻 。
    缺點:需要事先 知道關鍵字的分布情況
    使用場景:適合查找比較小且連續的情況

  • 除留余數法--(常用)
    設散列表中允許的地址數為m,取一個不大于m,但最接近或者等于m的質數p作為除數,按照哈希函 數:Hash(key) = key% p(p<=m),將關鍵碼轉換成哈希地址

  • 平方取中法
    假設關鍵字為1234,對它平方就是1522756,抽取中間的3位227作為哈希地址; 再比如關鍵字為4321,對它平方就是18671041,抽取中間的3位671(或710)作為哈希地址 平方取中法比較適合:不知道關鍵字的分布,而位數又不是很大的情況

  • 折疊法
    折疊法是將關鍵字從左到右分割成位數相等的幾部分(最后一部分位數可以短些),然后將這幾部分疊加 求和,并按散列表表長,取后幾位作為散列地址。
    折疊法適合事先不需要知道關鍵字的分布,適合關鍵字位數比較多的情況

  • 隨機數法
    選擇一個隨機函數,取關鍵字的隨機函數值為它的哈希地址,即H(key) = random(key),其中random為 隨機數函數。
    通常應用于關鍵字長度不等時采用此法

  • 數學分析法

  • 四、解決哈希沖突的方法

    1. 閉散列

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

    那如何尋找下一個空位置呢?

  • 線性探測
    比如前面的場景,現在需要插入元素44,先通過哈希函數計算哈希地址,hashAddr為4,因此44理論 上應該插在該位置,但是該位置已經放了值為4的元素,即發生哈希沖突。
    線性探測:從發生沖突的位置開始,依次向后探測,直到尋找到下一個空位置為止
    • 插入
      a.通過哈希函數獲取待插入元素在哈希表中的位置
      b.如果該位置中沒有元素則直接插入新元素,如果該位置中有元素發生哈希沖突,使用線性探 測找到下一個空位置,插入新元素
    • 刪除
      采用閉散列處理哈希沖突時,不能隨便物理刪除哈希表中已有的元素,若直接刪除元素會影響其他 元素的搜索。比如刪除元素4,如果直接刪除掉,44查找起來可能會受影響。因此線性探測采用標 記的偽刪除法來刪除一個元素
    // 哈希表每個空間給個標記 // EMPTY此位置空, EXIST此位置已經有元素, DELETE元素已經刪除 enum State{EMPTY, EXIST, DELETE}

    線性探測的實現

    // 注意:假如實現的哈希表中元素唯一,即key相同的元素不再進行插入 // 為了實現簡單,此哈希表中我們將比較直接與元素綁定在一起 template<class K, class V> class HashTable {struct Elem{pair<K, V> _val;State _state;};public:HashTable(size_t capacity = 3): _ht(capacity), _size(0){for(size_t i = 0; i < capacity; ++i)_ht[i]._state = EMPTY;}bool Insert(const pair<K, V>& val){// 檢測哈希表底層空間是否充足// _CheckCapacity();size_t hashAddr = HashFunc(key);// size_t startAddr = hashAddr;while(_ht[hashAddr]._state != EMPTY){if(_ht[hashAddr]._state == EXIST &&_ht[hashAddr]._val.first == key)return false;hashAddr++;if(hashAddr == _ht.capacity())hashAddr = 0;/*// 轉一圈也沒有找到,注意:動態哈希表,該種情況可以不用考 慮,哈希表中元素個數到達一定的數量,哈希沖突概率會增大,需要擴容來降低哈希沖突, 因此哈希表中元素是不會存滿的if(hashAddr == startAddr)return false;*/}// 插入元素_ht[hashAddr]._state = EXIST;_ht[hashAddr]._val = val;_size++;return true;}int Find(const K& key){size_t hashAddr = HashFunc(key);while(_ht[hashAddr]._state != EMPTY){if(_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)return hashAddr;hashAddr++;}return hashAddr;}bool Erase(const K& key){int index = Find(key);if(-1 != index){_ht[index]._state = DELETE;_size++;return true;}return false;}size_t Size()const;bool Empty() const;void Swap(HashTable<K, V, HF>& ht);private:size_t HashFunc(const K& key){return key % _ht.capacity();}private:vector<Elem> _ht;size_t _size; };

    思考:哈希表什么情況下進行擴容?如何擴容?

    void CheckCapacity() {if(_size * 10 / _ht.capacity() >= 7){HashTable<K, V, HF> newHt(GetNextPrime(ht.capacity));for(size_t i = 0; i < _ht.capacity(); ++i){if(_ht[i]._state == EXIST)newHt.Insert(_ht[i]._val);}Swap(newHt);} }

    線性探測優點:實現非常簡單,

    線性探測缺點:一旦發生哈希沖突,所有的沖突連在一起,容易產生數據“堆積”,即:不同關鍵碼占據 了可利用的空位置,使得尋找某關鍵碼的位置需要許多次比較,導致搜索效率降低。如何緩解呢?

    2.二次探測

    線性探測的缺陷是產生沖突的數據堆積在一塊,這與其找下一個空位置有關系,因為找空位置的方式就 是挨著往后逐個去找,因此二次探測為了避免該問題,找下一個空位置的方法為:Hi = (H0 + i^2)%m, 或者: Hi = (H0 + i^2)% m,H(i + 1) = (H0 + (i + 1)^2)% m,所以H(i + 1) = Hi + 2i + 1。其中:i = 1,2,3…, 是通過散列函數Hash(x)對元素的關鍵碼 key 進行 計算得到的位置,m是表的大小。

    對于前面如果要插入44,產生沖突,使用解決后的情況為:

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

    因此:閉散列最大的缺陷就是空間利用率比較低,這也是哈希的缺陷

    2. 開散列

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

  • 從上圖可以看出,開散列中每個桶中放的都是發生哈希沖突的元素。

  • 開散列實現
  • template<class V> struct HashBucketNode {HashBucketNode(const V& data): _pNext(nullptr), _data(data){}HashBucketNode<V>* _pNext;V _data; }; // 本文所實現的哈希桶中key是唯一的template<class V> class HashBucket {typedef HashBucketNode<V> Node;typedef Node* PNode; public:HashBucket(size_t capacity = 3): _size(0){ _ht.resize(GetNextPrime(capacity), nullptr);}// 哈希桶中的元素不能重復 PNode* Insert(const V& data){// 確認是否需要擴容。。。// _CheckCapacity();// 1. 計算元素所在的桶號size_t bucketNo = HashFunc(data);// 2. 檢測該元素是否在桶中PNode pCur = _ht[bucketNo];while(pCur){if(pCur->_data == data)return pCur;pCur = pCur->_pNext;}// 3. 插入新元素pCur = new Node(data);pCur->_pNext = _ht[bucketNo];_ht[bucketNo] = pCur;_size++; return pCur;}// 刪除哈希桶中為data的元素(data不會重復),返回刪除元素的下一個節點PNode* Erase(const V& data){size_t bucketNo = HashFunc(data);PNode pCur = _ht[bucketNo];PNode pPrev = nullptr, pRet = nullptr;while(pCur){if(pCur->_data == data){if(pCur == _ht[bucketNo])_ht[bucketNo] = pCur->_pNext;elsepPrev->_pNext = pCur->_pNext;pRet = pCur->_pNext;delete pCur;_size--;return pRet;}}return nullptr;}PNode* Find(const V& data);size_t Size()const;bool Empty()const;void Clear();bool BucketCount()const;void Swap(HashBucket<V, HF>& ht;~HashBucket();private:size_t HashFunc(const V& data){return data%_ht.capacity();} private:vector<PNode*> _ht;size_t _size; // 哈希表中有效元素的個數 }
  • 開散列增容
    桶的個數是一定的,隨著元素的不斷插入,每個桶中元素的個數不斷增多,極端情況下,可能會導致一個桶中鏈表節點非常多,會影響的哈希表的性能,因此在一定條件下需要對哈希表進行增容,那該條件
  • 怎么確認呢?開散列最好的情況是:每個哈希桶中剛好掛一個節點,再繼續插入元素時,每一次都會發生哈希沖突,因此,在元素個數剛好等于桶的個數時,可以給哈希表增容

    void _CheckCapacity() {size_t bucketCount = BucketCount();if(_size == bucketCount){HashBucket<V, HF> newHt(bucketCount);for(size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx){PNode pCur = _ht[bucketIdx];while(pCur){// 將該節點從原哈希表中拆出來_ht[bucketIdx] = pCur->_pNext;// 將該節點插入到新哈希表中size_t bucketNo = newHt.HashFunc(pCur->_data);pCur->_pNext = newHt._ht[bucketNo];newHt._ht[bucketNo] = pCur;pCur = _ht[bucketIdx];}}newHt._size = _size;this->Swap(newHt);} }
  • 開散列的思考
  • 只能存儲key為整形的元素,其他類型怎么解決?

    // 哈希函數采用處理余數法,被模的key必須要為整形才可以處理,此處提供將key轉化為整形的方法 // 整形數據不需要轉化 template<class T> class DefHashF { public:size_t operator()(const T& val){return val;} };// key為字符串類型,需要將其轉化為整形 class Str2Int { public:size_t operator()(const string& s){const char* str = s.c_str();unsigned int seed = 131; // 31 131 1313 13131 131313unsigned int hash = 0;while (*str){hash = hash * seed + (*str++);}return (hash & 0x7FFFFFFF);} };// 為了實現簡單,此哈希表中我們將比較直接與元素綁定在一起 template<class V, class HF> class HashBucket {// …… private:size_t HashFunc(const V& data){return HF()(data.first)%_ht.capacity();} };

    除留余數法,最好模一個素數,如何每次快速取一個類似兩倍關系的素數?

    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]; }

    開散列與閉散列比較

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

    最后最后再說一下開散列和閉散列能否用vector自定義的擴容方式?
    答案是不可以。

    • vector底層的擴容方式為:申請新空間–>拷貝元素—>釋放舊空間,
    • 在閉散列和開散列中共同的問題是:在計算哈希地址的時候用到data%capacity,而vector是直接拷貝過來,在完成擴容后,capacity已經改變,再去用data%capacity就無法正確找到元素
    • 而在開散列中,vector每個元素存放的是每個哈希鏈的首地址,在釋放舊空間的時候就已經把哈希鏈釋放掉了,在新空間中卻仍然在使用,就會發生錯誤

    五、模擬實現哈希

    github源碼

    總結

    以上是生活随笔為你收集整理的哈希原理的全部內容,希望文章能夠幫你解決所遇到的問題。

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