HashTable
前话:
今天北大松哥给王道训练营的成员们讲了桶排序(计数/基数排序): 将要排序的序列的元素全部添加到 其计数的数组中:
- 排序的序列元素值对应计数的数组下标, 比如序列为 2,2,3 , 元素2 对应 计数的数组角标为 2 的桶,遍历到即 arr[2] += 1, 表示将元素 2 放入桶 2 中,之后再将下个2也放入桶2中,再将下个 3 放入 桶 3 中: arr[3]+=1;
- 从角标 0 开始遍历桶,如果该位置上的桶有元素,arr[i] > 0 则输出桶的元素(根据对应值来决定输出的个数), 1,3,2,3,1 输出为 1,1,2,3,3
经历上述步骤就完成了桶排序,桶排序的时间是 O(n), 但是致命的问题就是数组的额外负担是很大的,假定我们有 67777 这个值,那么是不是我们的计数的数组就至少有 67777 个int类型空间,这是非常恐怖的!!!限制性很强。
那么,稍后我们会提到上面的解决办法。
HashTable 概述
在上一节我们介绍了一种被广泛应用的平衡二叉搜索树: RB-tree(红黑树)。红黑树指针树形的平衡上表现不错, 在效率表现和实现复杂度上也保持相当的平衡,其因此称为 STL set 和 map 的标准底层机制。
二叉搜索树具有对数平均时间 O(logn) 去查找特定值, 这次我们介绍一个更厉害的在 插入,删除,搜索上具有 ”常数平均时间“ 的表现的 哈希表结构, 而且这种表现和桶排数组的统计类似, 不需依赖输入元素的随机性。
哈希表的特点介绍:
- 本质上是数组,数组的元素是其 有名项 。
- 提供对任何有名项的存取和删除操作, 其本身可视为一种字典结构。
- 建表操作通过映射函数计算其插入的桶的位置,并将其插入, pos = get_bucket(key)
查找操作将键值通过映射函数得到其桶位置,直接访问到其元素。
由于桶是固定的,可能会有不同的键值映射到相同的桶上,我们利用开放寻址法 和 开链法 来避免哈希冲突。
避免哈希冲突的方法
开放寻址法:
- 若发生了哈希冲突,则循序的按该位置后的下一位置去寻找一个可用空间
- 或者 按 pos += 2^1 ,发生一次冲突跳跃 2 的一次方,两次 2的平方,依次类推
缺点很明显,若数目十分多,光寻找位置就要反复跳跃,违背了其 O(1)查找
开链法:
- 哈希表中的每一个元素维护一个链表,当出现哈希冲突时,就插入到链表中,表示他们共用一个 桶
- 查找时,利用哈希函数映射到该链表中,若链表为空,则视为查找不到,不为空,则遍历查找,若遍历到链表尾部也没找到,也是查找不到。
好处:虽然说针对 list 而进行的搜寻 只能是一种线性操作, 但如果 list 够短, 速度还是够快。
STL 的 hash_table 就是采用 开链法 。
hashtable 的 桶子 和 节点
从桶排序到 hashtable 中的桶,我们大概能推导出桶这个现实意思:
存放了很多东西的 物品 桶: BUCKET
STL hashtable 给出桶的概念即 hashtable 的元素(一整个链表,存放了很多映射到该桶的元素节点) , 其节点即存放了元素信息的节点
hashtable 的节点定义:
template <class value>
struct hashtable_node {
hashtable_node *next;
value val;
}
hashtable 的数据结构
我们知道 hashtable 使用开链法时很有可能导致一个桶放多个元素, 这样降低了其查找的效率,STL 的做法完美了解决了这个问题:
- 使用vector<node*> 作为其 hashtable 的主要数据结构
- 当现存元素大于等于当前vector 的容量,这样即描述为(hashtable 的平均查找时间复杂度大于 O(1), 有些桶内元素大于1),这时对 vector 进行扩充,重建哈希表。
1. 扩充? 扩充的多少,桶的变化
STL 以质数来设计表格大小, 并且先将 28 个质数(逐渐呈现大约两倍的关系)计算好,最小 53, 以备随时访问,同时提供一个函数 STL_NEXT_PRIME 来查询在 28 个质数中,最接近某个数并大于某数的质数。
也就是说:我们桶的变化是根据当前所需的量(元素的数量)找到最接近其并大于其的桶数量进行扩充:
static const int stl_num_primes = 28;
static const int unsigned long stl_prime_list[stl_num_primes] = {
53,97,193,389,769//。。。。
};
//STL 中以质数设计表格大小, 并先将 28 个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问。
//提供如下函数,去查询在 28 个质数中,最接近某数并大于某数的质数。
inline unsigned long stl_next_prime(unsigned long n) {
const unsigned long *first = stl_prime_list;
const unsigned long *last = stl_prime_list + stl_num_primes;
const unsigned long *pos = std::lower_bound(first, last, n);
//low_bound 找到第一个不小于该值的数
return pos == last ? *(last - 1) : *pos;
}
2 . hashtable 的数据结构内部
template<class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
class hashtable {
public:
typedef HashFcn hasher;
typedef EqualKey key_equal;
typedef size_t size_type;
private:
hasher hash; //根据元素键值计算其桶的位置
key_equal equals; //根据元素键值的相等性判断,(用于判定是否
//与查找目标相等)
ExtractKey get_key;
typedef my_hashtable_node<Value> node;
typedef std::allocator<node> node_allocater;
std::vector<node*> buckets; //桶
size_type num_elements;
node* new_node(const Value &obj) {
node *n = node_allocater::allocate(1);
n->next = 0;
try {
node_allocater::construct(&n->val, obj);
return n;
} catch (...) {
node_allocater::deallocate(n);
}
}
void delete_node(node *n) {
node_allocater::destroy(&n->val);
node_allocater::deallocate(n);
}
public:
size_type bucket_count() const { return buckets.size(); }
size_type max_bucket_count() const { return stl_prime_list[stl_num_primes - 1]; }
};
hash_set ( unorder_set ) / hash_map
C++11 中定义了 4 个无序关联容器,这些容器不是使用比较运算符来组织元素,而是以哈希函数和关键字 == 运算符, 当关键字类型的元素没有明显的序关系的情况下, 无序容器是非常有用的。
C++primer 上 p394 这样说道;
很明显, unordered_set 和 unordered_map 底层采用的 hash_table 机制,基本上 其无序容器的操作接口其哈希表结构全部已经提供,也可以说无序容器的操作都是转调 hashtable 的操作行为而已。
- 由于hashtable 没有自动排序功能, 所以其 无序容器也都没有
- hashtable 有一些无法处理的型别(自定义的 抽象数据结构),除非我们自己提供其 hash函数与 两个元素之间的相等比较操作,否则无法处理
这里我们谈一下 无序容器除了自身的增删改查元素之外,对 桶 的管理操作:
桶接口
c.bucket_count() 正在使用的桶数目
c.max_bucket_count()
c.bucket_size(n)
c.bucket(k)
哈希策略:
c.load_factor() 每个桶的平均元素数目,返回 float 值
c.max_load_factor() c 试图维护的平均桶大小,返回 float值,
c 会在需要时添加新的桶,使得 load_factor <= max_load_factor
c.rehash() : 重组哈希表,
使得 bucket_count >= n 并且 bucket_count > size/max_load_factor
c.reserve(n) : 扩容哈希表(vector<node*>)
无序容器与 多关键字无序容器
从字面上来说:
unorder_set 与 unorder_multiset 的区别就在于关键字能否重复,我们来看看在hashtable中是怎么处理这一情况的。
在 STL 的 hashtable 的源码中终于找到了!!! 原来呢,在hashtable 插入节点时, 提供了不同的两个插入函数:
- insert_unique( obj ) : 插入元素不允许重复,重复时直接不插入(忽略)
- insert_equal( obj) : 插入元素允许重复,重复时插入重复的元素节点之后。
这样看来:
- 当 unorder_set 插入元素时底层哈希表添加元素时使用的是 insert_unique
- 当 unorder_multiset 插入元素时底层哈希表添加元素时使用的是 insert_equal
谢谢观看,能观看本博客真的是有缘, 如果有想看的内容(游戏客户端/服务器 方面都可以)可以直接评论!