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

    科技2022-07-13  160

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


    二次探测:若当前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

    Processed: 0.010, SQL: 8