【C++】unordered关联式容器 与 哈希结构(哈希函数的设计)

时间:2024-10-28 13:55:43

一、unordered 关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器(set、map、multiset、multimap)在查询时效率可到到 O(logN) ,每次查找都需要比较节点的左右子树,当要查找的数据刚好位于叶子节点处时,就要比较树的高度次,当树的高度较大时,查找效率也不理想。

理想的查找效率是:1次就可以将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,它们虽然不能保证每次都能1次找到数据,但是它们需要进行的比较次数比红黑树少的多。这四个容器底层是运用了哈希结构,因为unordered容器与红黑树结构的容器使用方式基本蕾西,所以下面就只简单列举一下 unordered_map 的使用方式。

unordered_map 的使用

1.构造

unordered_map<int, int> myMap;  // 定义一个空的 unordered_map
unordered_map<string, int> myStringMap = {{"apple", 1}, {"banana", 2}, {"orange", 3}};

2.容量

bool empty() const :检测unordered_map 是否为空

size_t size() const  :获取unordered_map 的有效元素个数 

3.迭代器

begin   :返回unordered_map第一个元素的迭代器

end      :返回unordered_map最后一个元素下一个位置的迭代器

cbegin :返回unordered_map第一个元素的const迭代器

cend    :返回unordered_map最后一个元素下一个位置的const迭代器

4.元素访问

operator[] :返回与key对应的value,没有默认值

5.查询

iterator find(const K& key)  :返回key在哈希桶中的位置

size_t count(const K& key) :返回哈希桶中关键码为key的键值对的个数

6.修改

insert  :向容器插入键值对

erase  :删除容器中的键值对

void clear()   :清空容器中有效元素的个数

void swap(unordered map&) :交换两个容器中的元素

7.桶操作

size_t bucket_count()const           :返回桶的总个数

size_t bucket_size(size_t n)const :返回n号桶中有效元素的总个数

size_t bucket(const K& key)          :返回元素key所在的桶号

二、哈希结构

1.概念

在红黑树(平衡树)中,查找一个元素时,需要经过关键码的多次比较,时间复杂度为 O(logN);而在顺序结构中,时间复杂度为 O(N)。搜索的效率取决于元素比较的次数

而哈希结构可以近乎做到理想的搜索方式:不进行任何比较,1次直接找到目标元素

通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时,根据关键码就可以很快找到该元素

每一个存储位置都有对应的下标,但是下标个数是有限的,而且关键码可以是任意的,此时就需要通过哈希函数,将关键码与下标建立映射关系(比如这里就是取 关键码/10 的余数),再将元素存放当对应的位置(25/10余5,所以存放到下标为5的位置)

搜索元素时,也是将关键码进行同样的计算,求得存储位置(下标),就能直接搜索到,不需要进行元素之间的比较(事实上还是需要一定程度的比较的,后面会提到)。

上述的哈希函数设置为:hash(key) = key % capacity(capacity为存储元素底层空间总的大小) 

还是上述这张表,当我想继续插入关键码为17的元素,会出现什么问题?根据哈希函数,关键码为17,存储下标为7,但当前位置已存储了元素7,不能继续存储,此时出现的情况就叫哈希冲突

2.哈希冲突

对于不同元素的关键码,通过相同的哈希函数计算出相同的哈希地址,这种现象称为哈希冲突

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。上述元素7和17就是一对同义词

那么发生哈希冲突该如何处理呢?

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

3.哈希函数

哈希函数的设计遵循以下原则:

a. 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表(哈希表)允许有x个地址      时,其值域必须在0到x-1之间

b. 哈希函数计算出来的地址能均匀分布在整个空间中

c. 哈希函数应该比较简单

常见哈希函数:

1.直接定址法:

   取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

   优点:简单、均匀

   缺点:需要知道关键字的分布情况

   使用场景:适合查找小且连续的关键字

2.除留余数法:

   设散列表中地址数为m,取一个不大于m但接近或等于m的质数p作为除数

   :Hash(key) = key% p(p<=m) 

   优点:简单、均匀

   缺点:性能依赖表大小

   使用场景:大范围且不连续的关键字

3.平方区中法:

   假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址

   使用场景:不知道关键字的分布且位数不大

4.折叠法

   将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

   使用场景:不知道关键字的分布且位数较大

5.随机数法

   选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key)

   使用场景:关键字长度不等

哈希函数设计的越精妙,产生哈希冲突的可能性越低,但是终究无法避免哈希冲突。