哈希表基础
- 哈希表是一类数据结构(哈希表包含数组、集合和映射,和前两篇文章叙述的字符串、链表平级)
- 哈希表概念:类似于Python里的字典类型,哈希表把关键码key值通过哈希函数来和哈希表上的索引对应起来,之后输入key值可直接定位到对应索引位置,然后进行取值
- 哈希表的好处:主要为查找方便,可快速判断一个元素是否在集合中
- 哈希函数:即关键码key值和存储位置(索引)的对应关系,一个散列函数,比如把小明映射为0,小李映射为1,如图
- 哈希碰撞
- 定义:当哈希函数的映射关系把多个关键码映射到了同一个哈希表索引上时,这种现象称为哈希碰撞,如图所示(哈希碰撞有时候是因为关键码的数量大于哈希表的长度,这时不可避免发生碰撞;但是也可能是哈希函数的对应关系不合理,使得即便仍有空索引,还是把部分关键码分配到了同一索引上)
- 其实发生哈希碰撞不见得是个坏事,如果是因为关键码的数量大于哈希表的长度,说明此时哈希表的所有索引都被完全利用,没有发生内存浪费
- 解决方案一:拉链法
- 当发生冲突时,在对应索引位置通过链表结构储存多个关键码,如图所示
- 解决方案二:线性探测法
- 如果是哈希函数的对应关系不合理,使得即便仍有空索引,还是把部分关键码分配到了同一索引上,此时可以利用线性探测法,从发生冲突的索引位置开始,线性查找找到下一个空索引,然后把多余的关键码分配过去,如图所示
常见的三种哈希表结构
- 数组
- set集合
- 是一种数据结构,常用于元素查找的场景
- 特点:元素不重复(有的具体类实现会允许元素重复,如multiset)、有序(有些具体类实现是无序的如unordered-set)
- 具体的类实现
- 第一种,set:有序,即set类中的元素按值的大小排序;基于红黑树实现
- 第二种,unordered-set:无序,即set类中的元素不按照值的大小排序,而是根据哈希函数的映射结果来;基于哈希表实现
- 第三种,multiset:允许元素重复;按照元素值的大小排序;基于红黑树实现
- 集合类的使用
- 第一步,引入头文件
#include <unordered_set>
和#include <set>
,其中后者同时包含set和multiset类 - 第二步,创建集合变量,如
unordered_set<int> myset;
、set<int> myset;
、multiset<int> myset;
- 第三步,插入或删除元素,使用
myset.insert(value);
或myset.erase(value);
- 第四步,查找元素,使用
myset.find(value)
,如果找到了,find会返回指向该元素的迭代器(具体见下面对于迭代器的介绍);如果没找到,会返回一个指向集合的 end() 的迭代器。所以通过if myset.find(value) == myset.end()
可以判断集合中是否有对应元素
- map映射
- 是一种数据结构,定义和Python中的字典非常像,一个key一个value
- 特点:key的值唯一,与每个key对应的value值唯一(有的具体类实现可以不唯一,如multimap)
- 具体类的区别:
- 第一种:map,key值的存储有序(默认),如果想要自定义排序可以进行修改;基于红黑树实现
- 第二种:multimap,key值的存储有序;基于红黑树实现
- 第三种,unordered_map,key值的存储无序;基于哈希表实现,查找速度比前两种更快,因此如果不需要排序而只是快速查找key对应的value,可以考虑使用
- 映射类的使用
- 第一步,引入头文件
#include <unordered_map>
和#include <map>
,其中后者同时包含map和multimap类 - 第二步,创建映射变量,如
map<string, int> my_map;
,其中string代表key的类型为string,int代表value的类型为int - 第三步,插入元素,使用
my_map.insert(make_pair(key, value));
、my_map.insert(pair<int, string> (key, value));
或my_map[key] = value
;其中insert函数在插入时,如果key已经存在,则不会插入(pair的具体内容见下面映射类的遍历);而使用【】进行赋值的方式,如果key已经存在,将会用新的value覆盖原值 - 第四步,查找元素,使用
my_map.find(key)
,如果找到了,find会返回指向该键值对的迭代器(具体见下面对于迭代器的介绍);如果没找到,会返回一个指向映射的 end() 的迭代器。所以通过if my_map.find(key) == myset.end()
可以判断映射中是否有对应键值对
- 映射类的遍历
- 通过范围for循环进行(有关范围for循环见我的另一篇文章),`for (const pair<int, int> &kv : my_map) {}
- const用来表示所声明的变量为不可修改的;由于后续不可修改,因此必须进行初始化;这里用const类型的主要目的是保证只会读取映射中的键值对,而不会修改它们(其实范围for循环默认只能遍历读取容器的值,而不能修改它们,因此不用const可能也可以)
- pair是一种数据结构,可以把两个变量组合成一个单元,常用于存储键值对(如上面insert方法里就用到了该数据结构)
pair<int, int> my_pair;
pair的定义类似于容器、集合和映射的定义方式- pair有内置的成员first和second,可以分别访问pair储存的第一个值和第二个值
- 在遍历的时候把kv创建为引用类型,是为了避免每轮循环都把my_map中的键值对拷贝一遍到kv里,使用引用将只赋值对应的地址,可以避免不必要的内存浪费和拷贝时间
迭代器iterator
- 迭代器是一种类似于指针的接口,其作用类似于容器(如数组、集合)的下标,可以用来遍历访问容器中的元素,并执行各种操作
- 容器都拥有
begin()
和end()
成员,分别为指向第一个元素和末尾元素的下一个元素(尾后迭代器)的迭代器;如果容器为空,则begin和end返回同一迭代器 - 对于迭代器的操作
- 比较大小,直接使用比较运算符比较两个迭代器
- 移动位置,通过自加、自减运算符移动迭代器的位置
- 取值,通过解引用
*
可以获得迭代器指向的对象内容
- 使用迭代器遍历容器元素的代码如下
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> myVector = {1, 2, 3, 4, 5};
// 使用迭代器遍历容器
// vector<int>::iterator it 用于创建一个能读取<vector>int 元素的迭代器it,最初指向begin()
// ++it表示迭代器的移动
for (vector<int>::iterator it = myVector.begin(); it != myVector.end(); ++it) {
cout << *it << " "; // 通过解引用获取迭代器所指的对象
}
cout << endl;
return 0;
}