哈希的故事 之 二次探测和hashtable

本文详细介绍了哈希表的二次探测法解决冲突,包括插入、查找和删除操作。在哈希表扩容时,讨论了如何通过位运算简化哈希运算步骤,以避免在扩容时重新计算每个key的哈希位置。同时,提到了`unordered_map`的扩容策略,通过负载因子和素数表确定何时及如何进行扩容。
摘要由CSDN通过智能技术生成

哈希真的给我留下了巨大的阴影


二次探测:若当前key与原来key产生相同的哈希地址,则当前key存在该地址后偏移量为(1,2,3…)的二次方地址处
key1:hash(key)+0
key2:hash(key)+1^2
key3:hash(key)+2^2
···
二次探测法中会每一个空间采用一个状态标识来解决直接删除带来的问题,当删除元素时将该对应空间设置为DELETE;在进行查找某个元素时,判断标识位如果是EXIST和DELETE就继续往后探测查找,直到遇到EMPTY结束。
代码实现:

//二次探测
#pragma once

#include <iostream>
#include <string>

using namespace std;

enum State{EMPTY,DELETE,EXIST};
template <class K, class V>
struct HashTableNode
{
    K _key;
    V _value;
};
template <class K>
struct __HashFunc //默认的返回哈希键值key的 仿函数
{
    size_t operator()(const K &key)
    {
        return key;
    }
};
//特化string的__HashFunc 仿函数
template <>
struct __HashFunc<string>
{
    size_t operator()(const string &str)
    {
        size_t key = 0;
        for (size_t i = 0; i < str.size(); i++)
        {
            key += str[i];
        }
        return key;
    }
};
//实现哈希表的Key/Value形式的二次探测
template <class K, class V, class HashFunc = __HashFunc<K>>
class HashTable
{
    typedef HashTableNode<K, V> Node;

public:
    HashTable(size_t capacity = 10): _tables(new Node[capacity]), _size(0), _states(new State[capacity]), _capacity(capacity)
    {
        // memset 有问题 是以字节为单位初始化的 但第二个参数值为int
        //memset(_states, EMPTY, sizeof(State) * capacity);
        for (size_t i = 0; i < capacity; i++)
        {
            _states[i] = EMPTY;
        }
    }
    ~HashTable()
    {
        if (NULL != _tables)
        {
            delete[] _tables;
            _tables = NULL;
        }
        if (NULL != _states)
        {
            delete[] _states;
            _states = NULL;
        }
    }
    bool Insert(const K &key, const V &value)
    {
        _CheckCapacity();
        //用GetNextIndex 解决哈希冲突
        size_t index = _HashFunc(key);
        // 二次探测
        size_t i = 1;
        while (_states[index] == EXIST)
        {
            index = _GetNextIndex(index, i++);
            if (index >= _capacity)
            {
                index = index % _capacity;
            }
        }
        _tables[index]._key = key;
        _tables[index]._value = value;
        _states[index] = EXIST;
        _size++;
        return true;
    }
    Node *Find(const K &key)
    {
        size_t index = _HashFunc(key);
        size_t start = index;
        size_t i = 1;
        // 存在 或者 被删除 两种状态
        while (_states[index] != EMPTY)
        {
            if (_tables[index]._key == key)
            {
                if (_states[index] == EXIST)
                {
                    return index;
                }
                else // 被删除 DELETE
                {
                    return -1;
                }
            }
            index = _GetNextIndex(index, i++);
            if (index >= _capacity)
            {
                index = index % _capacity;
            }
            // 因为有填充因子 不为100%  不会出现全满且key!=_key 导致死循环的情况
        }
        return -1;
    }
    bool Remove(const K &key)
    {
        int index = Find(key);
        if (index != -1)
        {
            _states[index] = DELETE;
            --_size;
            return true;
        }
        return false;
    }
    // 二次探测计算出存放位置
    size_t _HashFunc(const K &key)
    {
        HashFunc hf;
        return hf(key) % _capacity; //  仿函数hf()
    }
    //   哈希冲突时 得到下一个index的可以利用上一个index的值 这样能提高效率 比如 string的index计算就比较费时
    size_t _GetNextIndex(size_t prev, size_t i)
    {
        return prev + 2 * i - 1;
    }
    void Print()
    {
        for (size_t i = 0; i < _capacity; i++)
        {
            if (_states[i] == EXIST)
            {
                cout << i << "EXIST:" << _tables[i]._key << "-------" << _tables[i]._value << endl;
            }
            else if (_states[i] == DELETE)
            {
                cout << i << "DELETE:" << _tables[i]._key << "-------" << _tables[i]._value << endl;
            }
            else
            {
                cout << i << "EMPTY:" << _tables[i]._key << "-------" << _tables[i]._value << endl;
            }
        }
    }
    void Swap(HashTable<K, V, HashFunc> &ht)
    {
        swap(_size, ht._size);
        swap(_states, ht._states);
        swap(_tables, ht._tables);
        swap(_capacity, ht._capacity);
    }

protected:
    void _CheckCapacity() // 扩容
    {
        // 动态的 可扩容的
        // 高效哈希表的载荷因子大概在0.7-0.8较好
        if (10 * _size / _capacity >= 7) // _size/_capacity为0 因为都是××× 所以乘10
        // 保证载荷因子在0.7之内
        {
            HashTable<K, V, HashFunc> tmp(2 * _capacity);
            for (size_t i = 0; i < _capacity; i++)
            {
                if (_states[i] == EXIST)
                {
                    tmp.Insert(_tables[i]._key, _tables[i]._value);
                }
            }
            Swap(tmp);
        }
    }

protected:
    Node *_tables;
    State *_states; //状态表
    size_t _size;
    size_t _capacity;
};

unordered_map本质是对hashtable封装,所以这里直接看hashtable的源码。

//rehash判断
inline std::pair<bool, std::size_t>
prime_rehash_policy::
need_rehash(std::size_t n_bkt, std::size_t n_elt, std::size_t n_ins) const
{
  if (n_elt + n_ins > m_next_resize)
    {
  float min_bkts = (float(n_ins) + float(n_elt)) / m_max_load_factor;
  if (min_bkts > n_bkt)
    {
      min_bkts = std::max(min_bkts, m_growth_factor * n_bkt);
      const unsigned long* const last = X<>::primes + X<>::n_primes;
      const unsigned long* p = std::lower_bound(X<>::primes, last,
                            min_bkts, lt());
      m_next_resize =
        static_cast<std::size_t>(std::ceil(*p * m_max_load_factor));
      return std::make_pair(true, *p);
    }
  else
    {
      m_next_resize =
        static_cast<std::size_t>(std::ceil(n_bkt * m_max_load_factor));
      return std::make_pair(false, 0);
    }
    }
  else
    return std::make_pair(false, 0);
}

用一个 m_max_load_factor 的因子来判断目前的容量需要多少个哈希桶,如果需要 rehash,那么使用素数表来算出新的桶需要多大。
素数表:

template<int ulongsize>
    const unsigned long X<ulongsize>::primes[256 + 48 + 1] =
    {
      2ul, 3ul, 5ul, 7ul, 11ul, 13ul, 17ul, 19ul, 23ul, 29ul, 31ul,

初始的时候,m_max_load_factor(1), m_growth_factor(2), m_next_resize(0),比 10 大的最小素数是 11,于是就分配为 11 个桶。

rehash方法:

template<typename K, typename V,
     typename A, typename Ex, typename Eq,
     typename H1, typename H2, typename H, typename RP,
     bool c, bool ci, bool u>
  void
  hashtable<K, V, A, Ex, Eq, H1, H2, H, RP, c, ci, u>::
  m_rehash(size_type n)
  {
    node** new_array = m_allocate_buckets(n);
    try
  {
    for (size_type i = 0; i < m_bucket_count; ++i)
      while (node* p = m_buckets[i])
        {
      size_type new_index = this->bucket_index(p, n);
      m_buckets[i] = p->m_next;
      p->m_next = new_array[new_index];
      new_array[new_index] = p;
        }
    m_deallocate_buckets(m_buckets, m_bucket_count);
    m_bucket_count = n;
    m_buckets = new_array;
  }
    catch(...)
  {
    // A failure here means that a hash function threw an exception.
    // We can't restore the previous state without calling the hash
    // function again, so the only sensible recovery is to delete
    // everything.
    m_deallocate_nodes(new_array, n);
    m_deallocate_buckets(new_array, n);
    m_deallocate_nodes(m_buckets, m_bucket_count);
    m_element_count = 0;
    __throw_exception_again;
  }
  }

一个关于哈希扩容的问题:如果我想在扩容的时候简化哈希的运算步骤,不需要重新对每一个key进行重新哈希运算,而是直接想要找到它在某个位置该怎么做?
用位运算。
重新取余计算index的花费是比较大的,位运算的引入可以大大降低计算的复杂度,具体可参考:https://blog.csdn.net/jiange_zh/article/details/81588983

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是用C语言编写的二次探测再散列的哈希查找的代码示例: ```c #include <stdio.h> #include <stdlib.h> #define SIZE 10 int hash(int key) { return key % SIZE; } int hash2(int key) { return 7 - (key % 7); } int hashSearch(int hashTable[], int key) { int index = hash(key); int h2 = hash2(key); int i = 0; while (hashTable[(index + i * h2) % SIZE] != -1) { if (hashTable[(index + i * h2) % SIZE] == key) { return (index + i * h2) % SIZE; } i++; } return -1; } void hashInsert(int hashTable[], int key) { int index = hash(key); int h2 = hash2(key); int i = 0; while (hashTable[(index + i * h2) % SIZE] != -1) { i++; } hashTable[(index + i * h2) % SIZE] = key; } int main() { int hashTable[SIZE]; for (int i = 0; i < SIZE; i++) { hashTable[i] = -1; } hashInsert(hashTable, 5); hashInsert(hashTable, 25); hashInsert(hashTable, 15); hashInsert(hashTable, 35); printf("Key 15 found at index %d\n", hashSearch(hashTable, 15)); printf("Key 35 found at index %d\n", hashSearch(hashTable, 35)); printf("Key 5 found at index %d\n", hashSearch(hashTable, 5)); printf("Key 25 found at index %d\n", hashSearch(hashTable, 25)); printf("Key 10 not found, returned index %d\n", hashSearch(hashTable, 10)); return 0; } ``` 在上述代码中,我们定义了两个哈希函数,`hash()` 和 `hash2()`。`hash()` 函数用于计算键的初始哈希值,`hash2()` 函数用于计算键的二次哈希步长。 `hashSearch()` 函数用于在哈希表中查找给定的键。它使用二次探测再散列的方来解决哈希冲突。如果键存在于哈希表中,则返回键的索引。如果键不存在于哈希表中,则返回 -1。 `hashInsert()` 函数用于将键插入哈希表中。它也使用二次探测再散列的方来解决哈希冲突。 在 `main()` 函数中,我们首先初始化哈希表。然后插入一些键值对,并使用 `hashSearch()` 函数查找这些键的索引。最后,我们查找一个不存在于哈希表中的键,并检查 `hashSearch()` 函数是否返回了 -1。 希望这个示例能够帮助你理解如何使用C语言编写二次探测再散列的哈希查找。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值