哈希表
来吧!一文彻底搞定哈希表! - 知乎 (zhihu.com)
百科解释:
“散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度,O(1)。这个映射函数称做散列函数,存放记录的数组(哈希表本质上是数组)称做散列表。
实现方式
- 数组 + 链表
- 数组 + 二叉树
散列函数
由关键值key加工获得数据存放位置
常见的散列函数
一文看懂哈希表并学会使用C++ STL 中的哈希表_哈希表end函数-CSDN博客
1)线性定址法:直接取关键字的某个线性函数作为存储地址,散列函数为:
Hash(key)=a×key+b
优点:简单、均匀
缺点:需要事先知道关键字的分布情况,如果关键字的分布比较分散,会很浪费空间
使用场景:适合查找比较小且连续的情况
2)除留余数法:将关键字对某一小于散列表长度的数p取余的结果作为存储地址,散列函数为:
Hash(key)=keymodp
缺陷:
- 适用于整数的存储(字符串、浮点数不能直接存储,因为不能直接取模,后面会讲如何解决)
- 余数相同时,会出现哈希冲突
3)平方取中法:对关键字取平方,然后将得到结果的中间几位作为存储地址;
4)折叠法:将关键字分割为几部分,然后将这几部分的叠加和作为存储地址。
处理哈希冲突
闭散列 -- 开放寻址法
①线性探测法:找该位置的后一个位置直到找到空位置为止,当查看到存储空间的末尾时还是找不到空位置,就返回从头开始查看;
缺点:
如果某个位置冲突的多,会导致一片冲突很多,数据堆积在一起。
插入和查找的效率都会降低很多,插入元素时,从冲突位置开始不断往后找到下一个空位置;查找元素时,从冲突位置开始不断往后找,需要比较许多次,导致搜索效率降低。最坏情况下要直到找到空位置时,才能说明没有该元素。
②平方探测法:不同于前面线性探测法依次顺序查看下一个位置是否能存储元素,平方探测的规则是以1^2,-1^2,2^2,-2^2,...,探测新的存储位置能否存储元素;
如果一个位置有很多数据冲突,那么二次探测会让这些数据存储位置会比较分散,不会集中在一起,导致一片一片的冲突。
③再散列法:利用两个散列函数,当通过第一个散列函数得到关键字的存储地址发生冲突时,再利用第二个散列函数计算出地址增量,地址计算方式如下:
Hi=(Hash1(key)+i∗Hash2(key))%p
④伪随机数法: 当发生地址冲突时,加入一个随机数作为地址增量寻找新的存储地址,地址计算方式如下:
Hi=(Hash(key)+di)%p,其中di为随机数
拉链法
链表长度大于等于8的话链表就会转换成二叉树,小于等于6就会还原链表,空一个7是为了避免频繁的转换链表和树消耗性能
哈希表的扩容
为了避免过于频繁的哈希冲突,当占的位置达到负载因子(0.7)时就会扩容为原来的二倍,还需要把原来数组的所有键值对重新 Hash 一遍放到新的数组
如何使用STL库中的哈希表
(1)导入头文件
#include<unordered_map>
(2)哈希表的声明和初始化
1)声明
unordered_map<elemType_1, elemType_2> var_name; //声明一个没有任何元素的哈希表,
//其中elemType_1和elemType_2是模板允许定义的类型,如要定义一个键值对都为Int的哈希表:
unordered_map<int, int> map;
2)初始化
以上在声明哈希表的时候并没有给unordered_map传递任何参数,因此调用的是unordered_map的默认构造函数,生成一个空容器。初始化主要有一下几种方式:
a)在定义哈希表的时候通过初始化列表中的元素初始化:
unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
//如果知道要创建的哈希表的元素个数时,也可以在初始化列表中指定元素个数
unordered_map<int, int> hmap{ {{1,10},{2,12},{3,13}},3 };
b)通过下标运算来添加元素:
//当我们想向哈希表中添加元素时也可以直接通过下标运算符添加元素,格式为: mapName[key]=value;
//如:hmap[4] = 14;
//但是这样的添加元素的方式会产生覆盖的问题,也就是当hmap中key为4的存储位置有值时,
//再用hmap[4]=value添加元素,会将原哈希表中key为4存储的元素覆盖
hmap[4] = 14;
hmap[4] = 15;
cout << hmap[4]; //结果为15
c)通过insert()函数来添加元素:
//通过insert()函数来添加元素的结果和通过下标来添加元素的结果一样,不同的是insert()可以避免覆盖问题,
//insert()函数在同一个key中插入两次,第二次插入会失败
hmap.insert({ 5,15 });
hmap.insert({ 5,16 });
cout << hmap[5]; //结果为15
d)复制构造,通过其他已初始化的哈希表来初始新的表:
unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
unordered_map<int, int> hmap1(hmap);
STL中哈希表的常用函数
(1) begin( )函数:该函数返回一个指向哈希表开始位置的迭代器
unordered_map<int, int>::iterator iter = hmap.begin(); //申请迭代器,并初始化为哈希表的起始位置
cout << iter->first << ":" << iter->second;
(2) end( )函数:作用于begin函数相同,返回一个指向哈希表结尾位置的下一个元素的迭代器
unordered_map<int, int>::iterator iter = hmap.end();
(3) cbegin() 和 cend():这两个函数的功能和begin()与end()的功能相同,唯一的区别是cbegin()和cend()是面向不可变的哈希表
const unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
unordered_map<int, int>::const_iterator iter_b = hmap.cbegin(); //注意这里的迭代器也要是不可变的const_iterator迭代器
unordered_map<int, int>::const_iterator iter_e = hmap.cend();
(6) erase()函数: 删除某个位置的元素,或者删除某个位置开始到某个位置结束这一范围内的元素, 或者传入key值删除键值对
unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
unordered_map<int, int>::iterator iter_begin = hmap.begin();
unordered_map<int, int>::iterator iter_end = hmap.end();
hmap.erase(iter_begin); //删除开始位置的元素
hmap.erase(iter_begin, iter_end); //删除开始位置和结束位置之间的元素
hmap.erase(3); //删除key==3的键值对
(7) at()函数:根据key查找哈希表中的元素
unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
int elem = hmap.at(3);
(8) clear()函数:清空哈希表中的元素
hmap.clear()
(9) find()函数:以key作为参数寻找哈希表中的元素,如果哈希表中存在该key值则返回该位置上的迭代器,否则返回哈希表最后一个元素下一位置上的迭代器
unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
unordered_map<int, int>::iterator iter;
iter = hmap.find(2); //返回key==2的迭代器,可以通过iter->second访问该key对应的元素
if(iter != hmap.end()) cout << iter->second;
(10) bucket()函数:以key寻找哈希表中该元素的储存的bucket编号(unordered_map的源码是基于拉链式的哈希表,所以是通过一个个bucket存储元素)
int pos = hmap.bucket(key);
(11) bucket_count()函数:该函数返回哈希表中存在的存储桶总数(一个存储桶可以用来存放多个元素,也可以不存放元素,并且bucket的个数大于等于元素个数)
int count = hmap.bucket_count();
(12) count()函数: 统计某个key值对应的元素个数, 因为unordered_map不允许重复元素,所以返回值为0或1
int count = hmap.count(key);
【C++ STL】哈希 Hash(闭散列、开散列介绍及其实现)_c++ stl hash-CSDN博客
顺序结构以及二叉平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序结构的查找时间复杂度为O(N),二叉平衡树中查找时间复杂度为树的高度O(log2N),搜索的效率取决于搜索过程中元素的比较次数。