从零开始手写STL库:HashTable

从零开始手写STL库–HashTable的实现

Gihub链接:miniSTL



HashTable是什么

HashTable在STL中直接出现的情况并不多,但却是很重要的底层结构。

STL库中的unordered_set和unordered_map均由它构成。

实际上也就是大家所熟悉的哈希表,是通过哈希函数将键映射到索引的一种数据结构,一般是不允许重复键值存在的,

所以一般都用来做搜索工作。

HashTable需要包含什么函数

通常来讲HashTable需要解决哈希函数映射冲突解决迭代器等等功能。本文提供一种简化的HashTable实现。

基础成员部分

首先需要知道的是,STL库中HashTable这一数据结构的实现方式为桶数组+链表
在这里插入图片描述
当输入一个键值时,通过哈希函数计算它的索引,寻找到对应的桶,再插入该桶后面的链表中,这就是一次插入过程

所以首先需要构建基础元素,HashNode,也就是上图中每一行组成的基础元素,多个基础元素拼起来也就成了一个HashTable

template <typename Key, typename Value, typename Hash = std::hash<Key>>
class HashTable {
  class HashNode {
  public:
    Key key;
    Value value;

    explicit HashNode(const Key &key) : key(key), value() {}

    // 从Key和Value构造节点
    HashNode(const Key &key, const Value &value) : key(key), value(value) {}

    // 比较算符重载,只按照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 HashNode &other) const { return key > other.key; }

    bool operator==(const Key &key_) const { return key == key_; }

    void print() const {
      std::cout << key << " "<< value << " ";
    }
  };
};

当有了基础元素,就可以将这些单独元素统一起来成为一个HashTable了

private:
  using Bucket = std::list<HashNode>; // 定义桶的类型为存储键的链表
  std::vector<Bucket> buckets;        // 存储所有桶的动态数组
  Hash hashFunction;                  // 哈希函数对象
  size_t tableSize;                   // 哈希表的大小,即桶的数量
  size_t numElements;                 // 哈希表中元素的数量

需要注意的是,哈希表需要动态地调整桶数组的大小来保持较好的性能,不然一个桶后面跟了太多的数字,会降低搜索效率

所以定义一个负载因子,当链表数量与桶数量之比大于这个因子,就进行rehash工作

float maxLoadFactor = 0.75; // 默认的最大负载因子

基础函数部分

当结构完整了,就可以开始定义函数了。

首先就是哈希映射函数

size_t hash(const Key &key) const { return hashFunction(key) % tableSize; }

用来接收键值(Key),并返回一个索引值(Index),来储存该键值

接着就是上文提到的rehash函数,与vector、deque的resize函数相同,这个函数也需要放在private中,不能随意让外界调用

  void rehash(size_t newSize) {
    std::vector<Bucket> newBuckets(newSize); // 创建新的桶数组

    for (Bucket &bucket : buckets) {      // 遍历旧桶
      for (HashNode &hashNode : bucket) { // 遍历桶中的每个键
        size_t newIndex =
            hashFunction(hashNode.key) % newSize; // 为键计算新的索引
        newBuckets[newIndex].push_back(hashNode); // 将键添加到新桶中
      }
    }
    buckets = std::move(newBuckets); // 使用move来更新,从而省去复制操作,优化性能
    tableSize = newSize;             // 更新哈希表大小
  }

接下来就是可以为外界使用的函数了

可用函数部分

1、构造函数:

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

这里的hash函数如果用户不指定,那就用STL库中自带的hash函数来初始化

2、插入函数
通过该函数,将键插入到哈希表中

  void insert(const Key &key, const Value &value) {
    if ((numElements + 1) > maxLoadFactor * tableSize) { // 检查是否需要重哈希
      
      if (tableSize == 0) tableSize = 1;// 防止之前进行了 clear 导致的 tableSize = 0 的情况
      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; // 增加元素数量
    }
  }
  
  void insertKey(const Key &key) { insert(key, Value{}); } // 只插入键值,没有value的情况

3、移除函数
通过该函数,将哈希表中存在的值删除,当然也需要区分存在和不存在的情况,不存在的话要注意不能内存溢出

  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--;    // 减少元素数量
    }
  }

4、查找函数
通过给定的键来查找该元素是否存在,按照STL库的函数,本函数需要返回一个指针,指向找到的值,否则返回end()

  Value *find(const Key &key) {
    size_t index = hash(key);      // 计算键的索引
    auto &bucket = buckets[index]; // 获取对应的桶
    // 返回键是否在桶中
    auto ans = std::find(bucket.begin(), bucket.end(), key);
    if (ans != bucket.end()) {
      return &ans->value;
    };
    return nullptr;
  }

其他函数

  // 获取哈希表中元素的数量
  size_t size() const { return numElements; }

  // 打印哈希表中的所有元素
  void print() const {
    for (size_t i = 0; i < buckets.size(); ++i) {
      for (const HashNode &element : buckets[i]) {
        element.print();
      }
    }
    std::cout << std::endl;
  }

  // 清空哈希表
  void clear() {
    this->buckets.clear();
    this->numElements = 0;
    this->tableSize = 0;
  }

总结

哈希表需要注意底层实现是桶数组+链表,并且需要知道rehash的用处,什么时候要rehash。

另外哈希表解决冲突不止本文这种方式,桶+链表虽然实现简单,但是极端情况下查找时间会到达O(n),而且占用内存较多

还有些方法比如

线性探测:当前桶有数字了,就顺序向下,一直到空桶;
二次探测:线性探测中把线性函数变成平方项;
双重哈希:两个哈希表,一个计算出来冲突了,就用另一个计算

知道即可,一般还是用桶+链表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值