哈希表
- 1.unorderd系列关联式容器
- 1.1unordered_map+unordered_set介绍
- 2.哈希表
- 2.1闭散列 -- 开放地址法
- 2.1.1线性探测
- 插入
- 查找
- 删除
- 针对插入查找做的修改
- 线性探测完整代码
- 2.1.2二次探测
- 2.2开散列 -- 拉链法(哈希桶)
- 插入
- 查找
- 删除
- 除留余数法取质数
- 拷贝构造
- 赋值重载
- 哈希桶完整代码
喜欢的点赞,收藏,关注一下把!
1.unorderd系列关联式容器
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和unordered_set进行介绍。
1.1unordered_map+unordered_set介绍
unordered_map在线文档说明
unordered_set在线文档说明
从功能上来说unordered_map和unordered_set和map和set基本上是一样的,只不过unorder_map和unordered_set底层是哈希表。map和set底层是搜索树,所以迭代器在走的时候map和set是有序的,unordered_map和unordered_set是无序的。
因为底层不同,所以给它们一个Hash的仿函数,这个是什么我们学了底层就知道了。
并且还是因为底层不同,它们只有单向迭代器。只支持++,不支持- -
哈希表有多种实现方式,库选择桶的实现方式封装unordered_map和unordered_set,因此它们的底层也可以叫做哈希桶。
下面是对桶操作,不过这些接口我们平常很少用,了解即可。
size_t bucket_count()const 返回哈希桶中桶的总个数 size_t bucket_size(size_t n)const 返回n号桶中有效元素的总个数 size_t bucket(const K& key) 返回元素key所在的桶号 这是有关负载因子的操作,也不用管。
这些东西等我们学过它们的底层之后我们就懂了。
剩下的接口就和map和set几乎差不多,这样的好处也是减轻我们的学习成本。既然unordered_map,unordered_set和map,set接口这么相似,那封装出它们有什么用呢,我们从各自的效率对比来看一看。
以unordered_set和set对比为例,注意对比性能走Release
插入
int main() { size_t N = 1000000; unordered_set us; set s; vector v; v.reserve(N); srand((unsigned int)time(nullptr)); for (size_t i = 0; i
插入完整代码
bool Insert(const pair& kv) { //防止插入相同的值 if (Find(kv.first)) return false; //负载因子控制在1,超过就扩容 if (_n == _tables.size()) { vector newtable; newtable.resize(_tables.size() * 2); for (size_t i = 0; i _next; size_t hashi = Hash()(cur->_kv.first) % newtable.size(); cur->_next = newtable[hashi]; newtable[hashi] = cur; cur = next; } _tables[i] = nullptr; } _tables.swap(newtable);//旧表和新表交换一下 } //匿名对象去调用仿函数,算在第几个桶里面 int hashi = Hash()(kv.first) % _tables.size(); //头插 Node* newnode = new Node(kv);//调用Node的构造,因此Node写个构造 newnode->_next = _tables[hashi]; _tables[hashi] = newnode; ++_n; return true; }
删除
删除不能再直接查找了,然后就删除了。因为我们这是单链表,需要知道该节点前一个位置!也不能使用单链表那种O(1)那种覆盖删除法。
所以老老实实的去找,然后再删。
bool Erase(const K& key) { size_t hashi = Hash()(key) % _tables.size(); Node* cur = _tables[hashi]; Node* prev = nullptr;//记录被删节点前一个位置 while (cur) { if (cur->_kv.first == key) { if (cur == _tables[hashi])//被删节点是头一个节点 { _tables[hashi] = cur->_next; } else { prev->_next = cur->_next; } delete cur; return true; } else { prev = cur; cur = cur->_next; } } return false; }
除留余数法取质数
这里我们看看库是怎么取的
库里给我们定义了一个质数表,第一次是53扩容2倍是106肯定不是质数了,因此选了一个靠近106的质数97,也算是接近2倍空间扩容了。
在我们的代码哈希表也加上这个取质数的函数。
inline unsigned long __stl_next_prime(unsigned long n) { static const int __stl_num_primes = 28; static const unsigned long __stl_prime_list[__stl_num_primes] = { 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741, 3221225473, 4294967291 };//最大质数取得是靠近整型最大数得质数 for (size_t i = 0; i n) return __stl_prime_list[i]; } //不用担心会哈希表会扩到最大质数,因为这时对于整型来说都已经差不多48G了 return __stl_prime_list[__stl_num_primes - 1]; }
构造和插入扩容也都需要改一下
构造获取比0大的质数
插入扩容获取比当前空间大的质数
拷贝构造
HashTable(const HashTable& ht) :_n(ht._n) { _tables.resize(ht._tables.size()); for (size_t i = 0; i _kv); _tables[i] = copy; //如果该链表不止一个节点,把其他节点拿下来重新申请节点尾插 while (cur->_next) { cur = cur->_next; //尾插 copy->_next = new Node(cur->_kv); copy = copy->_next; } } } }
赋值重载
//赋值重载现代写法 复用拷贝构造 HashTable& operator=(HashTable ht) { _n = ht._n; _tables.swap(ht._tables); return *this;//返回之前会自动调用HashTable的析构,释放对象ht }
哈希桶完整代码
template struct HashFunc { //凡是能转成整型的就转成整型 如负数,如指针,如浮点数 //string不能转 size_t operator()(const K& key) { return (size_t)key; } }; //模板特化 template struct HashFunc { //BKDR size_t operator()(const string& key) { size_t num = 0; for (auto& ch : key) { num *= 131; num += ch; } return num; } }; template struct HashNode { pair _kv; HashNode* _next; HashNode(const pair& kv) :_kv(kv) ,_next(nullptr) {} }; template class HashTable { typedef HashNode Node; public: HashTable() :_n(0)//这里虽然没有明确写调用vector构造,但是编译器会按照声明顺序去调用的,所以会自动调用vecto的构造 { //_tables.resize(10);//调用HashNode的构造 _tables.resize(__stl_next_prime(0)); } ~HashTable() { for (size_t i=0;i Node* cur = _tables[i]; while (cur) { Node* next = cur-_next; delete cur; cur = next; } _tables[i] = nullptr; } }//走到这里会自动调用vector的析构 HashTable(const HashTable& ht) :_n(ht._n) { _tables.resize(ht._tables.size()); for (size_t i = 0; i _kv); _tables[i] = copy; while (cur->_next) { cur = cur->_next; //尾插 copy->_next = new Node(cur->_kv); copy = copy->_next; } } } } //赋值重载现代写法 复用拷贝构造 HashTable& operator=(HashTable ht) { _n = ht._n; _tables.swap(ht._tables); return *this; } bool Insert(const pair& kv) { if (Find(kv.first)) return false; //负载因子控制在1,超过就扩容 if (_n == _tables.size()) { vector newtable; //newtable.resize(_tables.size() * 2); newtable.resize(__stl_next_prime(_tables.size())); for (size_t i = 0; i _next; size_t hashi = Hash()(cur->_kv.first) % newtable.size(); cur->_next = newtable[hashi]; newtable[hashi] = cur; cur = next; } _tables[i] = nullptr; } _tables.swap(newtable);//旧表和新表交换一下 } //匿名对象去调用仿函数,算在第几个桶里面 int hashi = Hash()(kv.first) % _tables.size(); //头插 Node* newnode = new Node(kv);//调用Node的构造,因此Node写个构造 newnode->_next = _tables[hashi]; _tables[hashi] = newnode; ++_n; return true; } Node* Find(const K& key) { size_t hashi = Hash()(key) % _tables.size(); Node* cur = _tables[hashi]; while (cur) { if (cur->_kv.first == key) return cur; else cur = cur->_next; } return nullptr; } bool Erase(const K& key) { size_t hashi = Hash()(key) % _tables.size(); Node* cur = _tables[hashi]; Node* prev = nullptr;//记录被删节点前一个位置 while (cur) { if (cur->_kv.first == key) { if (cur == _tables[hashi])//被删节点是头一个节点 { _tables[hashi] = cur->_next; } else { prev->_next = cur->_next; } delete cur; return true; } else { prev = cur; cur = cur->_next; } } return false; } inline unsigned long __stl_next_prime(unsigned long n) { static const int __stl_num_primes = 28; static const unsigned long __stl_prime_list[__stl_num_primes] = { 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741, 3221225473, 4294967291 };//最大质数取得是靠近整型最大数得质数 for (size_t i = 0; i n) return __stl_prime_list[i]; } //不用担心会哈希表会扩到最大质数,因为这时对于整型来说都已经差不多48G了 return __stl_prime_list[__stl_num_primes - 1]; } private: vector _tables; size_t _n; };