C++ STL -- HashTable

XTG111發表於2024-04-21

HashTable

一般常用的unordered_set、unordered_map都是基於雜湊表實現的,雜湊表主要需要注意的是雜湊衝突,迭代器等

基礎

雜湊對映

使用雜湊函式將鍵對映到索引的資料結構。
即將原始陣列索引透過雜湊函式對映到一個雜湊值上,從而將一個大範圍索引,減小到一個小的固定範圍

雜湊衝突

由於是大範圍變為小範圍所以顯然對映並不是一對一的,所以對於雜湊值為相同索引的多個不同鍵的儲存通常使用鏈式儲存方法,即對於雜湊表中的一個索引位置,其存放的是一個鏈式結構,其中每個元素為不同的鍵

雜湊表的擴容

如果鍵值數量不變,隨著索引的數量增加連結串列會變得越來越長導致效率降低,所以此時需要擴容。
需要重新計算所有的雜湊值,然後分配到更大的雜湊表中

效能最佳化

二次雜湊函式、空間配置器、記憶體池等

併發性和多執行緒安全性

簡單實現

實現插入、刪除、查詢、列印功能

成員變數

雜湊表節點

class HashNode
{
  public:
    Key key;
    Value value;
    explicit HashNode(const Key& key):key(key),value(){}
    HashNode(const Key &key, const Value &value) : key(key), value(value) {}
    bool operator==(const HashNode &other) const { return key == other.key; }
    bool operator!=(const HashNode &other) const { return key != other.key; }
    bool operator<(const HashNode &other) const { return key < other.key; }
    bool operator>(const HashNode &other) const { return key > other.key; }
    bool operator==(const Key &key_) const { return key == key_; }
    void print() const 
    {
       std::cout << key << " "<< value << " ";
    }
};

其餘成員變數

//每個節點對於的連結串列結構
using Bucket = std::list<HashNode>;
//一個陣列用來儲存所有陣列
vector<Bucket> buckets;
Hash HashFunction;
//雜湊表的大小
size_t tablesize;
//雜湊表中元素的數量
size_t numElements;
//負載因子
float maxLoadFactor = 0.75f;
//雜湊函式
size_t hash(const Key& key) const {return HashFunction(key)%tablesize;}
//重新分配雜湊值
void rehash(size_t newSize)
{
  std::vector<Bucket> newBuckets(newSize);
  for(Bucket& bucket:buckets)
  {
    for(HashNode& hashnode : bucket)
    {
      size_t newIndex = (hashnode.key)%newSize;
      newBuckets[newIndex].push_back(hashnode);
    }
  }
  buckets = std::move(newBuckets);
  tablesize = newSize;
}

建構函式

需要傳入雜湊表大小,和雜湊函式等

HashTable(size_t size = 10, const Hash &hashFunc = Hash())
: buckets(size), hashFunction(hashFunc), tableSize(size), numElements(0) 
{}

插入

首先考慮是否需要rehash,接著計算插入的key對應的桶的index,然後判斷當前key是否已經存在桶裡

void insert(const Key& key,const Value& value)
{
  if((numElements+1) > maxLoadFactor*tablesize)
  {
    if(tablesize == 0) tablesize += 1;
    rehash(tablesize*2);
  }
  size_t index = hash(key);
  //獲取對應桶
  std::list<HashNode>& bucket = buckets[index];
  //判斷是否已經存在
  if(std::find(bucket.begin(),bucket.end(),key) == bucket.end())
  {
    bucket.push_back(HashNode(key,value));
    numElements++; 
  }
}

移除某個鍵

當尋找到了某個鍵利用erase從桶裡面刪除掉

void erase(const Key &key) 
{
    size_t index = hash(key);  
    auto &bucket = buckets[index];
    auto it = std::find(bucket.begin(), bucket.end(), key); 
    if (it != bucket.end()) 
    {                               
      bucket.erase(it); 
      numElements--; 
    }
}

查詢鍵是否存在於雜湊表中

Value* find(const Key &key) 
{
    size_t index = hash(key);  
    auto &bucket = buckets[index];
    auto it = std::find(bucket.begin(), bucket.end(), key); 
    if (it != bucket.end()) 
    {                               
      return &it->value;
    }
    return nullptr;
}

總結

雜湊表的工作原理

雜湊表透過雜湊函式實現快速插入和搜尋。透過雜湊函式將鍵對映到表中的索引位置儲存鍵值對,為了解決不同鍵對映到了同一索引的雜湊衝突,一般採用連結串列或者開放地址

雜湊衝突的解決

一般有三種方法

  1. 連結串列法:每個雜湊表索引維護一個連結串列,對映到該索引的鍵值對將加入到對應連結串列中
  2. 開放定址法:傳送衝突,尋找下一個空閒的索引
  3. 雙重雜湊

雜湊函式的選擇

應該滿足以下條件

  1. 快速加速
  2. 雜湊值分佈均勻
  3. 一致性
  4. 不同的鍵因儘可能對應不同的索引

負載因子

已儲存元素數量與雜湊表中總位置數量的最大比率。衡量雜湊表滿程度的指標
當負載因子過大時,更容易造成雜湊衝突,因為一般當比率超過負載因子時,雜湊表會進行擴容來增加儲存位置,從而減少衝突和維護操作的效率

rehash

rehash發生在比率超過負載因子時,會進行新的索引雜湊值,然後將原有的鍵值對移動到新建立的雜湊表中。

插入、刪除、查詢的時間複雜度

如果沒有衝突,那麼都是\(O(1)\)
最壞的情況,即所有鍵都對應到一個索引那麼就是\(O(n)\)

擴容

建立一個更大的雜湊表:即將tablesize擴大,然後計算新的雜湊值,逐個move鍵值對

執行緒安全

  1. 互斥鎖:執行緒訪問雜湊表時,需要先獲取鎖
  2. 讀寫鎖:適用於讀操作多於寫操作的情況,讀資料時可以多個執行緒訪問,但訪問時只能有1個執行緒
  3. 原子操作
  4. 細粒度鎖:對雜湊表中的一個list或者一個桶加鎖

實現問題

  1. 記憶體使用不當:雜湊表過大造成空桶過多
  2. 衝突處理不佳
  3. 雜湊函式選擇不當:不同鍵值對對應同一個索引
  4. 擴容代價過高

相關文章