最近刷题遇到哈希表(散列表)的应用,对这方面不太了解的话用起来还是有点茫,首先总结一下什么是哈希表。
散列表查找(哈希表)概述
一般用来存储数据的顺序表我们是比较熟悉的,存储时依次存入,但是要查找顺序表中某个关键字的记录,就需要从表头依次查找。比如日常用的数组或链表中,要知道某个数据在数组或链表中的下表或者第几个位置需要遍历数组找到该数据,才能得到查询结果,这样查找的时间复杂度较高。因此能不能能不遍历直接查找呢?当然是可以的,这就用到我们所要讲的散列技术了。
散列技术是在记录的存储位置和它的关键字之间简历一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找是,根据这个这个对应关系就可轻松找到关键字key的映射f(key)。这种对应关系f成为散列函数,又成为哈希函数。采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表或哈希(Hash table)表。
散列表(哈希表)的构造
就像数学中的映射关系,不同的x值通过函数运算可能得到相同值,这样的现象称为冲突。给定一组数据,进行存储时需要构造一个好的散列函数,尽量避免产生冲突。好的散列函数一般有两个原则:计算简单、散列地址均匀。
- 若一个好的散列函数可以让说有的关键字都不产生冲突,但是这个算法复杂,则违背了散列技术想要节省查找时间的初衷,这对于频繁查找来说会大大降低查找效率。
- 尽量让散列地址均匀分布在存储空间中可以保证存储空间的有效利用,并减少为处理冲突而耗费时间
具体的构造方法包括:
- 直接定址法
- 数字分析法
- 平方取中法
- 折叠法
- 除留余数法
- 随机数法
- 开放定址法
- 链地址法
- 再散列函数法
- 公共溢出区法
上述方法的详细说明均可在《大话数据结构》一书中找到,或者看下面这位博主写得非常详细:
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.youkuaiyun.com/weixin_38169413/article/details/81612307
总的来说就是一些构造数据映射关系的方法。若还是不太理解构造散列函数,再此我就自己的理解简单举个例子,看如下数据:
对关键字集合{12,67,56,16,25,37,22,29,15,47,48,34},表长为m=12。
对上述数据用开放定址法如何构造散列函数呢?首先要知道开放定址法的公式:
MOD就是取模(求余数)的意思。利用该公式来计算:
f(12)=12MOD12=0;f(67)=67MOD12=7;......前五个数字{12,67,56,16,25}都没有冲突的散列地址,如下表:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
关键字 | 12 | 25 | 16 | 67 | 56 |
计算key=37时,发现f(37)=1,此时就与25所在的位置冲突。于是我们应用上面的公式f(37)=(f(37)+1) mod 12=2。于是将37存入下标为2的位置。通过上述方法最终得到下表:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
关键字 | 12 | 25 | 37 | 15 | 16 | 29 | 48 | 67 | 56 | 34 | 22 | 47 |
通过该例子应该了解基本的散列函数构造思想了,不同的构造方法效率、性能都不同。
通过散列表查找时,通过散列函数和关键字便可很容易得到关键字对应的地址。
哈希表的C++实现
好了,哈希表的基本概念了解清楚了,进入重点:STL中实现的容器.
我们平常用到的哈希表通常不是自己进行存储,用的最多的是已知某对应关系,然后直到某关键字值,希望立马能知道与其相关联的数据值。哈希表在STL中存在多种实现。从网上搜索会发现有map、unordered_map、unordered_multimap等容器均可实现哈希表的相似功能。功能上相似,但是使用上又不尽相同。接下来我将分别对几种容器进行讲解,最后进行比较。
STL中的map
map是STL中的一个关联容器,它提供一对一的数据处理能力,从而方便我们处理一对一的数据,为编程提供快速通道。而哈希表是一种基于键值映射的快速搜索的数据结构,注意两者的关联和区别。map以模板(泛型)方式实现,可以存储任意类型包括使用者自定义的数据类型。map中的第一个数据称为关键字(key),每个关键字只能在map中出现一次。第二个称为该关键字的值。map内部自建一颗红黑树(一 种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的。在红黑树上做查找、插入、删除操作的时间复杂度为O(logN)。
1、使用map
接下来说一下在编程中应该如何使用map:
- 需要包含map类所在的头文件,注意STL头文件没有扩展名
#include <map>
- 要创建map模板对象,可使用<type,type>表示法来指出关键字和对应值的类型。
std:map<int, string> mymap; //定义一个空的map对象mymap
std::map<int, string> mymap2(mymap); //创建了mymap的副本mymap2
这样便定义了一个map模板对象。
2、map的插入、删除、遍历及其他常用操作函数
-
map的插入
(1)使用insert(pair<type,type>(value,value))函数插入数据。返回值是一个pair结构,其中第一个元素是一个迭代器,第二个元素是一个bool类型,根据以上结果:如果原来的map中不含有插入的元素,则bool为true,迭代器指向插入的元素;如果map中已经含有插入的元素了,则bool为false,返回的迭代器指向对应的map中已经存在的元素.
std::pair主要的作用是将两个数据组合成一个数据,两个数据可以是同一类型或者不同类型。C++标准程序库中凡是“必须返回两个值”的函数, 也都会利用pair对象。class pair可以将两个值视为一个单元。容器类别map和multimap就是使用pairs来管理其健值/实值(key/value)的成对元素。两个pairs互相比较时, 第一个元素具有较高的优先级.。pair实质上是一个结构体,其主要的两个成员变量是first和second,这两个变量可以直接使用。
mymap.insert(pair<int,string>(1,"aaa"));
(2)使用insert(make_pair<make_type,type>(value,value))函数插入数据。与第一种方法类似,区别在于make_pair的用法无需写出型别, 就可以生成一个pair对象。
mymap.insert(make_pair(1,"aaa"));
(3)使用value_type方法。先说说什么是value_type,每个STL中的类都有value_type这种东西,通俗的说value_type 就是stl容器盛装的数据的数据类型。比如说:
vector<double> myvec;
vector::value_type x;
上述两句代码,第一句是声明一个盛装数据类型是int的数据的vector,第二句是使用vector::value_type定义一个变量x,这个变量x实际上是double类型的,因为vector::value_type中声明的为double型。
进入正题:
mymap.insert(map<int,string>::value_type(1,"aaa"));
(4)使用[ ]数组方式插入。使用该方法时,出现重复键时,会覆盖掉之前的键对值。
mymap[1]="aaa";
-
map的其他操作操作函数
函数 | 举例 | 功能(返回) | 备注 |
begin() | map<int, string>::iterator it=mymap.begin(); | 返回指向第一个元素(头部)的迭代器iterator | |
end() | map<int, string>::iterator it=mymap.end(); | 返回指向最后一个元素的下一个元素(末尾)的迭代器iterator | |
count() | int num=mymap.count(1); | 返回指定元素出现的次数 | 注意,map中不存在相同元素,所以返回值只能是1或0。 |
empty() | mymap.empty(); | 如果map为空则返回true | |
equal_range() | mymap[1]="aaa"; mymap[2]="bbb"; mymap[3]="ccc"; pair<map<int,string>::iterator, map<int,string>::iterator> ret; ret=mymap.equal_range(1); 返回的结果如下: ret.first=pair<int,string>(1,"aaa"); ret.second=pair<int,string>(2,"bbb"); | 返回特殊条目的迭代器对,C++ STL中的一种二分查找的算法,试图在已排序的[first,last)中寻找value,它返回一对迭代器i和j,其中i是在不破坏次序的前提下,value可插入的第一个位置(亦即lower_bound),j则是在不破坏次序的前提下,value可插入的最后一个位置(亦即upper_bound), | 我们可把它想成是[first,last)内"与value等同"之所有元素形成的区间A,由于[fist,last)有序(sorted),所以我们知道"与value等同"之所有元素一定都相邻,于是,算法lower_bound返回区间A的第一个迭代器,算法upper_bound返回区间A的最后一个元素的下一个位置,算法equal_range则是以pair的形式将两者都返回 |
erase() | map<int, string>::iterator iter,nextiter; nextiter=mymap.erase(iter); | 删除一个元素,返回下一个元素的迭代器 | 注意对于关联容器来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。使用删除之前的迭代器定位下一个元素 |
find() | map<int, string>::iterator it; it=mymap.find(1); | 用find函数来定位数据出现位置,它返回的一个迭代器,当数据出现时,它返回数据所在位置的迭代器,如果map中没有要查找的数据,它返回的迭代器等于end函数返回的迭代器 | |
get_allocator() | mymap.get_allocator(); | 返回map的配置器 | |
value_comp() | mymap[1]="aaa"; mymap[2]="bbb"; mymap[3]="ccc"; map<int, string>::iterator iter1,iter2; iter1=mymap.find(“aaa”); iter2=mymap.find("bbb"); mymap.value_comp()(iter1,iter2); 返回1表示iter1在前,返回0表示iter2在前 | 其返回值是一个比较类的对象,这个类是map::value_compare,并且是map的一个内部类。返回的这个对象可以用来通过比较两个元素的value来判决它们对应的key在map的位置谁在前面谁在后面。 | |
key_comp() | 返回比较元素key位置前后的函数 | ||
max_size() | int maxnum=mymap.max_size(); | 返回可以容纳的最大元素个数 | |
rbegin() | map<int, string>::iterator iter; iter=mymap.rbegin(); | 返回一个指向map尾部的逆向迭代器 | |
rend() | map<int, string>::iterator iter; iter=mymap.rend(); | 返回一个指向map头部的逆向迭代器 | |
size() | int num=mymap.size(); | 返回map中元素的个数 | |
swap() | mymap1.swap(mymap2) OR swap(mymap1, mymap2) | swap()函数用于交换两个Map的内容,但是Map的类型必须相同,尽管大小可能会有所不同. | |
lower_bound() | upper_bound() | ||
upper_bound() | |||
at() | mymap.at(1); | at()函数用于将引用返回给与键k关联的元素, 它返回对键值等于k的元素的关联值的引用。 |
|
clear() | mymap.clear(); | 清空所有元素。 |
|
STL中的unorederd_map
unordered_map与map功能相似,也是一个关联容器,其中的元素根据键来引用,而不是根据索引来引用。在内部,std::unordered_map中的元素不会根据其键值或映射值按任何特定顺序排序,而是根据其哈希值组织到桶中,以允许通过键值直接快速访问各个元素(常量的平均时间复杂度)。std::unorederd_map中的元素的键是唯一的。
map与unordered_map的区别
std::map对应的数据结构是红黑树。红黑树是一种近似于平衡的二叉查找树,里面的数据是有序的。在红黑树上做查找、插入、删除操作的时间复杂度为O(logN)。而std::unordered_map对应哈希表,哈希表的特点就是查找效率高,时间复杂度为常数级别O(1), 而额外空间复杂度则要高出许多。所以对于需要高效率查询的情况,使用std::unordered_map容器,但是std::unordered_map对于迭代器遍历效率并不高。而如果对内存大小比较敏感或者数据存储要求有序的话,则可以用std::map容器。
1、使用unordered_map
接下来说一下在编程中应该如何使用unordered_map:
- 需要包含unordered_map类所在的头文件<unordered_map>,注意STL头文件没有扩展名
#include <unordered_map>
- 要创建unordered_map模板对象,可使用<type,type>表示法来指出关键字和对应值的类型。
std:unordered_map<int, string> myunmap; //定义一个空的map对象myunmap
std::unordered_map<int, string> myunmap2(myunmap); //创建了myunmap的副本myunmap2
这样便定义了一个map模板对象。
2、unordered_map的插入、删除、遍历及其他常用操作函数
-
unordered_map的构造函数
-
typedef std::unordered_map<int,std::string> mynumaptype;
mynumaptype firstmap; //建立空容器
mynumaptype secondmap({{1,"aaa"},{2,"bbb"}}); //初始化
mynumaptype thirdmap({{3,"ccc"},{4,"ddd"}}); //初始化
mynumaptype fourthmap(secondmap); //复制
mynumaptype fifthmap(merge(thirdmap,fourthmap)); //移动,merge函数的作用是:将两个有序的序
//列合并为一个有序的序列
-
unordered_map的插入
1)使用insert(pair<type,type>(value,value))函数插入数据
2)使用insert(make_pair<make_type,type>(value,value))函数插入数据。
3)使用value_type方法
4)使用[ ]数组方式插入。使用该方法时,出现重复键时,会覆盖掉之前的键对值。
注意:const std::unordered_map 不能使用 operator[] 操作!!!!
-
unordered_map的其他操作操作函数
函数 | 举例 | 功能(返回) | 备注 |
begin() | unordered_map<int, string>::iterator it=myunmap.begin(); | 返回指向第一个元素(头部)的迭代器iterator | |
end() | unordered_map<int, string>::iterator it=myunmap.end(); | 返回指向最后一个元素的下一个元素(末尾)的迭代器iterator | |
count() | int num=myunmap.count(1); | 返回指定元素出现的次数 | 注意,map中不存在相同元素,所以返回值只能是1或0。 |
empty() | myunmap.empty(); | 如果map为空则返回true | |
equal_range() | myunmap[1]="aaa"; myunmap[2]="bbb"; myunmap[3]="ccc"; pair<unordered_map<int,string>::iterator, unordered_map<int,string>::iterator> ret; ret=myunmap.equal_range(1); 返回的结果如下: ret.first=pair<int,string>(1,"aaa"); ret.second=pair<int,string>(2,"bbb"); | 返回特殊条目的迭代器对,C++ STL中的一种二分查找的算法,试图在已排序的[first,last)中寻找value,它返回一对迭代器i和j,其中i是在不破坏次序的前提下,value可插入的第一个位置(亦即lower_bound),j则是在不破坏次序的前提下,value可插入的最后一个位置(亦即upper_bound), | 我们可把它想成是[first,last)内"与value等同"之所有元素形成的区间A,由于[fist,last)有序(sorted),所以我们知道"与value等同"之所有元素一定都相邻,于是,算法lower_bound返回区间A的第一个迭代器,算法upper_bound返回区间A的最后一个元素的下一个位置,算法equal_range则是以pair的形式将两者都返回 |
iterator erase (const_iterator position);//根据元素位置 | unordered_map<int, string>::iterator iter,nextiter; nextiter=myunmap.erase(iter); | 删除一个元素,返回下一个元素的迭代器 | 注意对于关联容器来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。使用删除之前的迭代器定位下一个元素 |
size_type erase (const key_type& k);//根据元素的键 |
int num=myunmap.erase(1); | 返回被删除元素的数目,此处为1 | |
find() | unordered_map<int, string>::iterator it; it=myunmap.find(1); | 用find函数来定位数据出现位置,它返回的一个迭代器,当数据出现时,它返回数据所在位置的迭代器,如果map中没有要查找的数据,它返回的迭代器等于end函数返回的迭代器 | |
get_allocator() | myunmap.get_allocator(); | 返回map的配置器 | |
value_comp() | myunmap[1]="aaa"; myunmap[2]="bbb"; myunmap[3]="ccc"; unordered_map<int, string>::iterator iter1,iter2; iter1=myunmap.find(“aaa”); iter2=myunmap.find("bbb"); myunmap.value_comp()(iter1,iter2); 返回1表示iter1在前,返回0表示iter2在前 | 其返回值是一个比较类的对象,这个类是map::value_compare,并且是map的一个内部类。返回的这个对象可以用来通过比较两个元素的value来判决它们对应的key在map的位置谁在前面谁在后面。 | |
key_comp() | 返回比较元素key位置前后的函数 | ||
max_size() | int maxnum=myunmap.max_size(); | 返回可以容纳的最大元素个数 | |
rbegin() | map<int, string>::iterator iter; iter=mymap.rbegin(); | 返回一个指向map尾部的逆向迭代器 | |
rend() | unordered_map<int, string>::iterator iter; iter=mymap.rend(); | 返回一个指向map头部的逆向迭代器 | |
size() | int num=myunmap.size(); | 返回map中元素的个数 | |
swap() | myunmap1.swap(myunmap2) OR swap(myunmap1, myunmap2) | swap()函数用于交换两个Map的内容,但是Map的类型必须相同,尽管大小可能会有所不同. | |
map::lower_bound(key): | 返回map中第一个大于或等于key的迭代器指针 | 不小于 | |
map::upper_bound(key) | 返回map中第一个大于key的迭代器指针 | 大于 | |
at() | myunmap.at(1); | 如果 k 匹配容器中某个元素的键,则该函数返回该映射值的引用。 如果 k 与容器中任何元素的键都不匹配,则该函数将抛出 out_of_range 异常。 |
|
clear() | myunmap.clear(); | 清空所有元素。 |
|
const_iterator cbegin() const noexcept; | myunmap.cbegin(); | 返回一个常量迭代器,指向第一个元素 |
|
const_iterator cend() const noexcept; | myunmap.cend(); | 返回一个常量迭代器,指向尾后元素 |
|
STL中的multimap
有了上述两个容器的学习,multimap就很好理解了。multimap容器保存的是有序的键/值对,但是可以保存重复的元素。multimap中会出现具有相同键值的元素序列。multimap大部分成员函数的使用方式和map相同。因为重复键的原因,multimap有一些函数的使用方式和map有一些区别。
1、访问元素的区别
multimap 不支持下标运算符,因为键并不能确定一个唯一元素。和 map 相似,multimap 也不能使用 at() 函数。
find函数
multimap 的成员函数 find() 可以返回一个键和参数匹配的元素的迭代器。例如:
std::multimap<std::string, size_t> people {{"Ann",25},{"Bill", 46}, {"Jack", 77}, {"Jack", 32},{"Jill", 32}, {"Ann", 35} };
std::string name {"Bill"};
auto iter = people.find(name);
if (iter ! = std::end (people))
std::cout << name << " is " << iter->second << std::endl;
iter = people.find ("Ann");
if (iter != std::end(people))
std::cout << iter->first << " is " << iter->second <<std::endl;
如果没有找到键,会返回一个结束迭代器,所以我们应该总是对返回值进行检查。第一个 find() 调用的参数是一个键对象,因为这个键是存在的,所以输出语句可以执行。第二个 find() 调用的参数是一个字符串常量,它说明参数不需要和键是相同的类型。对容器来说,可以用任何值或对象作为参数,只要可以用函数对象将它们和键进行比较。最后一条输出语句也可以执行,因为有等于 "Ann" 的键。事实上,这里有两个等于 "Ann" 的键,你可能也会得到不同的运行结果。
count函数
通过调用 multimap 的成员函数 count() 可以知道有多少个元素的键和给定的键相同。
erase函数
multimap 的成员函数 erase() 有 3 个版本:
- 以待删除元素的迭代器作为参数,这个函数没有返回值;
- 以一个键作为参数,它会删除容器中所有含这个键的元素,返回容器中被移除元素的个数;
- 接受两个迭代器参数,它们指定了容器中的一段元素,这个范围内的所有元素都会被删除,这个函数返回的迭代器指向最后一个被删除元素的后一个位置。
到这基本对哈希表这个数据结构以及STL中三种容器的实现基本有了了解,至少基本的使用是不成问题了。其实还有很多内容之后再补充吧!
参考文章:https://blog.youkuaiyun.com/qq_28584889/article/details/83855734#3.%20multimap%E5%AE%B9%E5%99%A8
https://blog.youkuaiyun.com/sevenjoin/article/details/81943864