在 C++98 中, STL 提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到O(logN) ,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想,所以提出了哈希的映射思想
1. unordered_map 是存储键值对的关联式容器,其允许通过 keys 快速的索引到与其对应的value 。 2. 在 unordered_map 中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。3. 在内部 ,unordered_map 没有对按照任何特定的顺序排序 , 为了能在常数范围内找到key 所对应的 value , unordered_map 将相同哈希值的键值对放在相同的桶中。 4. unordered_map 容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭代方面效率较低。5. unordered_maps 实现了直接访问操作符 (operator[]) ,它允许使用 key 作为参数直接访问value 。6. 它的迭代器至少是前向迭代器
因为这个学习比较简单,就直接附上链接,unordered_map的学习其实和map很相似,因为stl使用的统一的封装的方式。所以基本每个容器的使用方式都很接近。
unordered_map使用
unordered_set使用
在长度 2N 的数组中找出重复 N 次的元素
思路:我们可以使用unordered_map来统计每个元素出现的次数,然后遍历就可以得到结果
class Solution {
public:
int repeatedNTimes(vector& nums) {
//使用unordered map 把元素放入其中,然后再看哪个元素出现的次数等于N
unordered_map m;
for(auto& e: nums)
{
m[e]++;
}
//查找出现n次的
int n = nums.size()/2;
for(auto& e: m)
{
if(e.second == n)
{
return e.first;
}
}
return 0;
}
};
两个数组的交集
思路:1.我们可以采用unordered_set 对其进行去重,然后在其中一个集合找另一个集合的元素即可
2.我们也可以使用set进行排序加去重,然后通过滑动窗口的方法来得到它们相同的元素
解法1:
class Solution {
public:
vector intersection(vector& nums1, vector& nums2) {
//我们可以采用unordered_set 对其进行去重,然后在其中一个集合找另一个集合的元素即可
vector result;
unordered_set s1;
unordered_set s2;
for(auto& e: nums1)
{
s1.insert(e);
}
for(auto& e: nums2)
{
s2.insert(e);
}
//在遍历s1的时候找s2
for(auto& e: s1)
{
if(s2.find(e) != s2.end())
{
result.push_back(e);
}
}
return result;
}
};
解法2:
class Solution {
public:
vector intersection(vector& nums1, vector& nums2) {
//使用set进行排序加去重,然后在两个set中找相同的元素
set s1(nums1.begin(),nums1.end());
set s2(nums2.begin(),nums2.end());
vector result;
auto it1 = s1.begin();
auto it2 = s2.begin();
//比较谁的元素小就++谁的iterator
while(it1 != s1.end() && it2 != s2.end())
{
if(*it1 == *it2)
{
result.push_back(*it1);
++it1;
++it2;
}
else if(*it1 > *it2)
{
++it2;
}
else{
++it1;
}
}
return result;
}
};
2. 底层结构
unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构
2.1 哈希概念顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素 时,必须要经过关键码的多次比较 。 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即 O(logN) ,搜索的效率取决于搜索过程中元素的比较次数。理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素 。如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素
插入元素根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放搜索元素对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
但是如果插入了%capacity和之前的值一样时就没有办法解决了,这里的例子就可以举例14,这样就会产生哈希冲突。
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为 “ 同义词 ” 。
哈希函数设计原则 :哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 到 m-1 之间哈希函数计算出来的地址能均匀分布在整个空间中哈希函数应该比较简单
1. 直接定址法 --( 常用 )取关键字的某个线性函数为散列地址: Hash ( Key ) = A*Key + B优点:简单、均匀缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况经典例题:字符串中的第一个唯一字符这题体现了哈希思想,我们可以用一个绝对映射的方式,因为其中字母的ASCLL值很接近,就可以利用这点来解决这题:class Solution { public: int firstUniqChar(string s) { //使用哈希映射思想 int hash[27] = {0}; //遍历统计次数 for(int i = 0;i
2. 除留余数法 --( 常用 )设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址3. 平方取中法 --( 了解 )假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 位 227 作为哈希地址;再比如关键字为 4321 ,对它平方就是 18671041 ,抽取中间的 3 位 671( 或 710) 作为哈希地址平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况4. 折叠法 --( 了解 )折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况5. 随机数法 --( 了解 )选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中random 为随机数函数。通常应用于关键字长度不等时采用此法6. 数学分析法 --( 了解 )设有 n 个 d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况
解决哈希冲突 两种常见的方法是: 闭散列 和 开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把 key 存放到冲突位置中的 “ 下一个 ” 空位置中去1. 线性探测从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止插入通过哈希函数获取待插入元素在哈希表中的位置如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素删除采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索 。比如删除元素 4 ,如果直接删除掉, 44 查找起来可能会受影响。因此 线性探测采用标记的伪删除法来删除一个元素 。 (这里使用的是通过标记状态来解决)
线性探测优点:实现非常简单,线性探测缺点: 一旦发生哈希冲突,所有的冲突连在一起,容易产生数据 “ 堆积 ” ,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低 。
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题, 找下一个空位置的方法为: H_i = (H_0 + i^2 )% m, 或者: H_i = (H_0 - i^2 )% m 。其中: i =1,2,3… , H_0 是通过散列函数 Hash(x) 对元素的关键码 key 进行计算得到的位置, m 是表的大小。(也就是如果我们从i从1开始找,如果已经被占用了,就把i改成2,再探测,直到探测到空为止研究表明: 当表的长度为质数且表装载因子 a 不超过 0.5 时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子 a 不超过 0.5 , 如果超出必须考虑增容
1. 开散列概念开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中 。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素
我们把这个结构叫哈希桶,这也是单链表的应用的地方,正常来说单链表是不会单独拿来存储数据的,而是其他结构的子结构。但是如果这个哈希表如果太小也会导致单链表太长而导致查找效率低,所以我们可以通过保证全部的元素和哈希表的大小保持相等就可以达到最好的效果。这样既不会浪费空间,而且查找效率也高。
2. 开散列实现
放在下面模拟实现中
1. 面试题给 40亿个不重复的无符号整数 ,没排过序。给一个无符号整数,如何快速判断一个数 是否在这 40 亿个数中。【腾讯】1. 遍历,时间复杂度 O(N)2. 排序 (O(NlogN)) ,利用二分查找 : logN3.红黑树以及哈希表,但是内存太大,不适合4.位图解决(最优)数据是否在给定的整形数据中,结果是 在或者不在 ,刚好是 两种状态 ,那么可以使用 一个二进制比特位来代表数据是否存在的信息 ,如果二进制比特位为 1 ,代表存在,为 0代表不存在。2. 位图概念所谓位图,就是用每一位来 存放某种状态 , 适用于海量数据 ,数据无重复的场景。通常是用来判断某个数据存不存在的。
我们使用位图是利用哈希绝对映射的思想,因为在海量数据中需要使用到的空间必然很多,而且数据量十分庞大,我们使用绝对映射即可。
我们一般使用3个主要的函数:一个是记录,在位图置为1中,一个是销毁,在位图中置为0,还有一个是查找,看看该元素是否在位图中。
template
class bit_set
{
public:
bit_set()
{
//_bit.resize((N >> 3) + 1, 0);
_bit.resize(N/8 + 1, 0);
}
void set(size_t x)
{
size_t i = x >> 3;//等价于N/8
size_t j = x % 8;
//把那个bit置为1
_bit[i] |= (1 << j);
}
void reset(size_t x)
{
size_t i = x >> 3;//等价于N/8
size_t j = x % 8;
//把除了那个bit都保持不变
_bit[i] &= (~(1 << j));
}
bool test(size_t x)
{
size_t i = x >> 3;//等价于N/8
size_t j = x % 8;
return _bit[i] & (1 << j);
}
private:
vector _bit;
};
void test_bit_set()
{
/*bit_set<100> bs1;
bit_set<1000> bs2;
bit_set<10000> bs3;*/
bit_set<0xffffffff> bs;
//bit_set<((size_t)-1)> bs;
bs.set(1024);
bs.set(10);
bs.set(104);
bs.set(102401);
cout << bs.test(10) << endl;
bs.reset(10);
cout << bs.test(10) << endl;
}
1. 快速查找某个数据是否在一个集合中2. 排序 + 去重3. 求两个集合的交集、并集等4. 操作系统中磁盘块标记
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?1. 用哈希表存储用户记录,缺点:浪费空间2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。3. 将哈希与位图结合,即布隆过滤器
布隆过滤器是 由布隆( Burton Howard Bloom )在 1970 年提出的 一种紧凑型的、比较巧妙的 概率型数据结构 ,特点是 高效地插入和查询,可以用来告诉你 “ 某样东西一定不存在或者可能存 在 ” ,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式 不仅可以提升查询效率,也可以节省大量的内存空间。布隆过滤器的理论理解,这篇文章写的很好,可以值得学习
我们可以通过使用stl库中的bitset来进行布隆过滤器的实现:
布隆过滤器的模拟实现
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1 。所以可以按照以下方式进行查找: 分别计算每个哈希值对应的比特位置存储的是否为 零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中 。注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可 能存在,因为有些哈希函数存在一定的误判。比如:在布隆过滤器中查找 "alibaba" 时,假设 3 个哈希函数计算的哈希值为: 1 、 3 、 7 ,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。如果我们一定要删除就要付出相当大代价,就是使用计数器的方式,使用多个位图来实现,但是这样的空间消耗太大了,还不如直接使用hash表,所以实际应用场景不多。缺陷:1. 无法确认元素是否真正在布隆过滤器中2. 存在计数回绕
1. 增加和查询元素的时间复杂度为 :O(K), (K 为哈希函数的个数,一般比较小 ) ,与数据量大小无关2. 哈希函数相互之间没有关系,方便硬件并行运算3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
给一个超过 100G 大小的 log fifile, log 中存着 IP 地址 , 设计算法找到出现次数最多的 IP 地址?与上题条件相同,如何找到 top K 的 IP ?如何直接用 Linux 系统命令实现?
思路:我们可以把这100G的数据分成100份,然后我们把其中的每一个数据都转换成整型,然后%100,这样就可以知道该元素在哪个分区,这样出现哈希冲突的的元素都会放在一个分区中,那么就会有下面两种情况:
1.很多数据在同一个分区中,分区大小很大,但是同时数据大量重复存在,可以使用map去记录元素的个数2.很多数据在同一个分区中,分区大小很大,无法使用map去记录元素的个数面对这两种情况,我们可以这样解决:无论如果我们都把这个分区的数据放入map中去,如果map可以存下,说明是情况1。这样使用map就可以解决数据最多的问题;如果map在insert的时候抛异常,说明空间不够,就是情况2,那么我们就可以换另一种hash函数去把这里的数据再分类,之后再放入map中。
思路:因为整数的存储最多就是42亿个数,存在大量重复,如果我们使用位图去存储这些数据就是512M就可以存下了(状态),然后遍历位图去找即可
思路:跟上面的方式一样,我们使用两个位图,这样内存刚好是1G,去中它们相同的状态即可
思路:出现次数不超过2次就是出现1次或者两次,我们可以使用两个位图,因为其中涉及到的就是4中状态:0,1,2,2次以上,所以我们可以使用00,01,10,11来记录。使用两个位图就可以知道当前的bit的出现的次数了。
如果是近似算法我们可以使用布隆过滤器来把找到的值全部放入交集中,如果我们想要找到精确的算法,那么我们就应该把100亿个query(假设一个query(查询指令)大概是50字节,这里就是500G),那么我们就把它们分成500份,使用分治以及hash的思想,和5.1的解决方式相同。
这样我们只能增加一个计数器来记录有多少个元素映射到了这个空间,每次删除就把这个位置的值--即可,但是消耗空间很大,一般不支持删除操作。