当前位置: 首页 > news >正文

崇明建设镇乡镇府网站宜兴城乡建设局网站

崇明建设镇乡镇府网站,宜兴城乡建设局网站,网站建设方案免费下载,移动开发平台文章目录 前言一、unordered系列关联式容器1、unordered_map2、性能测试 二、哈希1、哈希概念2、哈希冲突3、哈希冲突解决3.1 闭散列3.2 开散列3.3 字符串Hash函数3.4 哈希桶实现的哈希表的效率 三、哈希表封装unordered_map和unordered_set容器1、unordered_map和unordered_se… 文章目录 前言一、unordered系列关联式容器1、unordered_map2、性能测试 二、哈希1、哈希概念2、哈希冲突3、哈希冲突解决3.1 闭散列3.2 开散列3.3 字符串Hash函数3.4 哈希桶实现的哈希表的效率 三、哈希表封装unordered_map和unordered_set容器1、unordered_map和unordered_set插入结点的实现2、unordered_map和unordered_set迭代器的实现3、unordered_map容器的[]操作符重载函数的实现4、unordered_map容器中使用自定义类型做key5、面试题map/set容器和unordered_map/unordered_set容器使用的条件6、unordered_map和unordered_set容器的const_iterator迭代器 四、哈希的应用1、位图1.1 位图实现1.2 位图应用11.3 位图应用21.4 位图应用31.5 位图优缺点 2、布隆过滤器2.1 布隆过滤器概念2.2 布隆过滤器实现2.3 布隆过滤器使用场景 前言 一、unordered系列关联式容器 在学习哈希之前我们先来学习四个c11中新添加的两个容器。我们知道在C98中STL提供了底层为红黑树结构的一系列关联式容器在查询时效率可达到 l o g 2 N log_2 N log2​N即最差情况下需要比较红黑树的高度次当树中的节点非常多时查询效率也不理想。最好的查询是进行很少的比较次数就能够将元素找到因此在C11中STL又提供了4个unordered系列的关联式容器这四个容器与红黑树结构的关联式容器使用方式基本类似只是其底层结构不同所以我们只要知道怎么用set和map容器就应该可以很快学会使用unordered系列的容器。 1、unordered_map unordered_map是存储key, value键值对的关联式容器其允许通过key快速的索引到与其对应的value。在unordered_map中键值通常用于唯一地标识元素而映射值是一个对象其内容与此键关联。键和映射值的类型可能不同。在内部,unordered_map没有对kye, value按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的valueunordered_map将相同哈希值的键值对放在相同的桶中。unordered_map容器通过key访问单个元素要比map快但它通常在遍历元素子集的范围迭代方面效率较低。unordered_maps实现了直接访问操作符(operator[])它允许使用key作为参数直接访问value。它的迭代器是前向迭代器。 可以看到unordered_map和map的接口函数都类似需要注意的是因为unordered_map容器只有单向迭代器所以在迭代器中没有提供rbegin、rend等接口函数。 下面我们来看一下unordered_set的使用 2、性能测试 下面我们根据 二、哈希 1、哈希概念 顺序结构以及平衡树中元素关键码与其存储位置之间没有对应的关系因此在查找一个元素时必须要经过关键码的多次比较。顺序查找时间复杂度为O(N)平衡树中为树的高度即O( l o g 2 N log_2 N log2​N)搜索的效率取决于搜索过程中元素的比较次数。 理想的搜索方法可以不经过任何比较一次直接从表中得到要搜索的元素。 如果构造一种存储结构通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系那么在查找时通过该函数可以很快找到该元素。 当向该结构中 插入元素 根据待插入元素的关键码以此函数计算出该元素的存储位置并按此位置进行存放。搜索元素 对元素的关键码进行同样的计算把求得的函数值当做元素的存储位置在结构中按此位置取元素比较若关键码相等则搜索成功。 该方式即为哈希(散列)方法哈希方法中使用的转换函数称为哈希(散列)函数构造出来的结构称为哈希表(Hash Table)(或者称散列表)。 例如数据集合{176459} 哈希函数设置为hash(key) key % capacity; capacity为存储元素底层空间总的大小。 用上面的方法进行搜索时不必进行多次关键码的比较因此搜索的速度比较快。 但是当按照上述哈希方式向集合中插入元素44会出现什么问题 此时44%10 4而arr[]中下标为4的位置已经存了数据了所以这时候就会产生哈希冲突。 2、哈希冲突 哈希冲突/碰撞其实就是不同的值映射到哈希表的相同的位置。引起哈希冲突的一个原因可能是哈希函数设计不够合理。 哈希函数设计原则 哈希函数的定义域必须包括需要存储的全部关键码而如果散列表允许有m个地址时其值域必须在0到m-1之间。哈希函数计算出来的地址能均匀分布在整个空间中。哈希函数应该比较简单。 常见哈希函数 直接定址法–(常用) 取关键字的某个线性函数为散列地址HashKey A*Key B 优点简单、均匀。 缺点需要事先知道关键字的分布情况。 使用场景适合查找比较小且连续的情况。除留余数法–(常用) 设散列表中允许的地址数为m取一个不大于m但最接近或者等于m的质数p作为除数按照哈希函数Hash(key) key% p(pm),将关键码转换成哈希地址。平方取中法–(了解) 假设关键字为1234对它平方就是1522756抽取中间的3位227作为哈希地址 再比如关键字为4321对它平方就是18671041抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合不知道关键字的分布而位数又不是很大的情况。折叠法–(了解) 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些)然后将这几部分叠加求和并按散列表表长取后几位作为散列地址。 折叠法适合事先不需要知道关键字的分布适合关键字位数比较多的情况。随机数法–(了解) 选择一个随机函数取关键字的随机函数值为它的哈希地址即H(key) random(key),其中random为随机数函数。数学分析法–(了解) 设有n个d位数每一位可能有r种不同的符号这r种不同的符号在各位上出现的频率不一定相同可能在某些位上分布比较均匀每种符号出现的机会均等在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小选择其中各种符号分布均匀的若干位作为散列地址。例如假设要存储某家公司员工登记表如果用手机号作为关键字那么极有可能前7位都是 相同的那么我们可以选择后面的四位作为散列地址如果这样的抽取工作还容易出现冲突还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成123446)等方法。 数字分析法通常适合处理关键字位数比较大的情况如果事先知道关键字的分布且关键字的若干位分布较均匀的情况。 注意哈希函数设计的越精妙产生哈希冲突的可能性就越低但是无法避免哈希冲突。 3、哈希冲突解决 解决哈希冲突两种常见的方法是闭散列和开散列。 3.1 闭散列 闭散列也叫开放定址法当发生哈希冲突时如果哈希表未被装满说明在哈希表中必然还有空位置那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢 (1). 通过线性探测 比如下面的场景现在需要插入元素44先通过哈希函数计算哈希地址hashAddr为4因此44理论上应该插在下标为4的位置但是该位置已经放了值为4的元素即发生哈希冲突。 线性探测从发生冲突的位置开始依次向后探测直到寻找到下一个空位置为止。 下面我们来分析哈希表使用线性探测时进行元素插入和删除的场景。 插入 通过哈希函数获取待插入元素在哈希表中的位置。如果该位置中没有元素则直接插入新元素如果该位置中有元素发生哈希冲突使用线性探测找到下一个空位置插入新元素。 删除 采用闭散列处理哈希冲突时不能随便物理删除哈希表中已有的元素若直接删除元素会影响其他元素的搜索。比如删除元素4如果直接删除掉44查找起来可能会受影响因为如果直接将4删除那么4的位置就为空然后查找44时44%104会去arr[4]的位置查找然后发现arr[4]为空就会返回没有44的结果。因此线性探测采用标记的伪删除法来删除一个元素。即将数组中的位置分为3种状态存在、删除、空。这样当删除元素4时就将元素4的位置状态变为删除然后查找44时遇到删除后也继续向后查找直到遇到空的位置才停止查找。 下面我们就来实现利用线性探测法解决哈希冲突的哈希表。 下面是哈希表中要存的数据类型的定义和哈希表的定义我们底层使用vector来实现哈希表然后在vector中存的数据类型为HashData类型数据类型中还记录了当前位置的状态。 下面我们来实现哈希表中元素的插入我们使用的哈希函数为除留余数法。上面我们的举例中除留余数法是直接模的数组的容量capacity但是如果我们底层使用vector容器来充当哈希表的话再模vector容器的容量capacity的话就会出现错误。因为vector的[]操作符重载函数里面有一个断言来判断传入的下标值如果下标值小于0或者大于size就会报错。 所以我们应该模size。 下面我们来看哈希表的扩容当哈希表中的元素很多时即哈希表快要满时此时插入一个新元素产生冲突的概率会很大而且遇到冲突后需要向后面进行多次查找才能找到一个空位置。所以为了降低哈希表插入新元素时产生冲突的概率不能等哈希表满时再进行扩容而是当哈希表中的元素达到一定量时就要进行扩容。这就引入了负载因子。 负载因子/载荷因子就是表存储数据量的百分比。 下面我们实现时如果负载因子超过0.7时就对哈希表进行扩容。 下面使用reserve函数来给vector容器扩容是错误的因为reserve扩容只改变vector的capacity而size不会改变。那么当访问到size后面的位置后还是越界访问。并且因为我们使用模vector的size来得到每个元素在哈希表中的位置而vector的size没有变那么插入元素时产生冲突的概率还是不会降低。 所以我们需要使用resize函数来进行扩容即将vector的size也改变。并且我们还需要考虑size为0的情况。 但是上面的扩容写法也有问题因为当我们将size改变后采用除留余数法计算元素在哈希表中的下标时模的是新的size的大小而我们向哈希表中插入元素时模的是旧的size。这样插入和寻找时使用的不是同一个size所以计算得到的位置也不一样这肯定是错误的。例如当我们将哈希表扩容后进行查找时我们查找元素1313%2013所以我们会去_tables[13]中查找元素13而tables[13]位置肯定是找不到元素13的。 所以在哈希表扩容后我们需要使用新的size重新计算每一个元素的位置。 下面我们来使用代码实现。下面的这种写法重新计算哈希表中元素的新位置我们可以看到使用了两次线性检测的代码即有代码的冗余。 所以我们也可以使用下面的写法。即我们新创建一个HashTable然后将旧表中的元素都插入到新的哈希表中因为我们的HashTable不进行扩容时就按照线性检测来插入数据而且我们新创建的HashTable的大小为扩容之后的大小所以在新的HashTable中插入元素时不会出现扩容情况。 然后我们进行测试可以看到不扩容时哈希表插入元素正常。 下面我们再来测试当哈希表扩容时元素是否都重新计算了位置。我们看到当哈希表扩容后哈希表中的元素都重新计算了位置。 下面我们再来实现Find函数。 然后我们再实现哈希表中元素的删除我们删除哈希表中的元素只是改变了要删除元素的状态并没有真正的将元素的数据删除。 但是像上面这样写删除会出现bug例如当我们删除13后还可以查找到13元素这是因为在删除函数中我们并没有将数据删除只是将元素的状态改变了。所以使用Find函数查找时还能找到13元素。 所以我们在Find函数中还需要加一个判断即检测当前元素的状态是否为EXIT如果为EXIT时才算查找到该元素。然后我们看到这个bug就被解决了。 然后我们再来完善Insert函数即在Insert函数刚开始的地方调用Find函数查看要插入的元素是否已经存在哈希表中如果已经存在那么直接返回插入失败因为哈希表中不允许数据冗余。 但是我们运行时看到程序出现了异常这是因为当第一次调用Find时此时哈希表的大小为0所以出现了模0操作。所以我们需要在Find函数中加一个判断如果哈希表是空表那么直接返回nullptr。然后我们就可以看到当重复插入元素时就会插入失败。 上面我们实现的哈希表有很小的概率可能表中全是删除状态此时如果再调用Find函数进行查找就会陷入死循环。下面的操作可能导致这种情况出现即先插入数据后哈希表没有扩容然后删除一部分数据然后再插入数据(因为删除数据时n会减减所以哈希表不会扩容)并且数据刚好占了哈希表中的空位此时就可能会导致哈希表里面除了存在就是删除状态的元素。 所以我们可以在Find函数中再判断一下如果查找了一圈还没有找到元素就直接break。这样我们就简单实现了使用线性探测方法解决冲突的哈希表。 线性探测优点实现非常简单 线性探测缺点一旦发生哈希冲突所有的冲突连在一起容易产生数据“堆积”即不同关键码占据了可利用的空位置使得寻找某关键码的位置需要许多次比较导致搜索效率降低。如何缓解呢 二次探测 线性探测的缺陷是产生冲突的数据堆积在一块这与其找下一个空位置有关系因为找空位置的方式就是挨着往后逐个去找因此二次探测为了避免该问题找下一个空位置的方法为 H i H_i Hi​ ( H 0 H_0 H0​ i 2 i^2 i2 )% m, 或者 H i H_i Hi​ ( H 0 H_0 H0​ - i 2 i^2 i2 )% m。其中i 1,2,3… H 0 H_0 H0​是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置m是表的大小。 可以看到二次探测和线性探测类型只不过二叉探测向哈希表后面找空位置时不是一个位置一个位置的去找而是跳着去找空位置。 研究表明当表的长度为质数且表负载因子a不超过0.5时新的表项一定能够插入而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置就不会存在表满的问题。在搜索时可以不考虑表装满的情况但在插入时必须确保表的负载因子a不超过0.5如果超出必须考虑增容。 因此闭散列最大的缺陷就是空间利用率比较低这也是哈希的缺陷。 3.2 开散列 因为闭散列的缺点所以在实际当中是不建议用线性探测或二次探测的。更建议用开散列来解决哈希冲突。 开散列概念 开散列法又叫链地址法(开链法)首先对关键码集合用散列函数计算散列地址具有相同地址的关键码归于同一子集合每一个子集合称为一个桶各个桶中的元素通过一个单链表或双链表链接起来各链表的头结点存储在哈希表中。 从下图可以看出开散列中每个桶中放的都是发生哈希冲突的元素。 可以看到使用拉链法来实现哈希表时插入元素时的冲突概率减少了而且插入元素的开销低了。 下面我们来实现使用开散列法解决哈希冲突的哈希表。 哈希桶法中的哈希表中存的是这个桶中第一个结点的指针所以我们将vector容器中存的数据类型为Node *。哈希表中每一个结点中除了存储数据外还有一个_ next指针用来存储和该结点在一个桶内的下一个结点的指针。 下面我们来实现结点的插入采用哈希桶的方法时插入新结点其实就是先算出该结点在哪个桶内然后向这个桶内插入新结点即向链表中插入新结点。这里使用头插或尾插法都可以我们可以看到哈希桶方法的结点插入效率是很高的可以达到O(1)。注意下面我们采用的是头插但是图画错了图画的为尾插 下面我们再来分析哈希表的扩容问题。采用哈希桶的方式实现哈希表时当桶的个数和结点的个数相同时即负载因子为1时我们进行哈希表的扩容即增加桶的个数。 我们可以采用前面的扩容办法即新创建一个哈希表并且为该哈希表开辟的空间为扩容后的大小。然后再将旧表中的每个结点都重新计算位置并且插入到新表中最后让旧表和新表交换。 但是上面的这种方法在新表插入结点时会将旧表中的每个结点重新创建一次然后还需要将原来的表中链表的结点都释放。所以我们需要自己写析构函数将哈希表中的每个链表的结点都进行释放。因为自动生成的析构函数只会将指针数组释放不会释放链表中的结点。 这样的话上面的方法进行扩容就会开销很大了因为需要先重新创建每一个新结点然后将旧表中的链表中的旧结点都释放。所以我们可以使用下面的方法即将原表中的结点重新计算位置挪动到新表中这样就不用创建新结点和释放旧结点了。 下面我们来测试插入结点是否正确。我们看到哈希表可以正确的插入新结点并且进行扩容而且扩容后每个结点重新计算了在新表中的位置。 下面我们继续完善哈希表我们来实现哈希表中查找元素的Find函数。 然后我们再完善Insert函数在插入新结点时先进行查找如果当前哈希表中已经有了该元素那么就不重复插入而是直接返回插入失败。 然后我们再来实现删除结点删除结点有两种情况即删除头结点和删除中间结点。 下面我们来测试删除结点。我们看到12结点和3结点都被成功删除。 3.3 字符串Hash函数 但是我们实现的哈希表还存在一些问题当我们向哈希表中存储的数据为string类或其它自定义类型时因为我们使用的是pair对象的first的值来计算元素在哈希表中的位置所以当pair对象的first存的不是整型类型而是string类或其它自定义类型时就不能来做取模运算了那么也就不能计算元素在哈希表中的位置了。 此时我们就需要给HashTable模板中再添加一个仿函数的模板参数即如果向哈希表中存入的数据不可以转换为整型时就需要用户自己提供一个仿函数来将自定义类型转换为整型。这个仿函数也有两种情况第一种情况为K本身就为整数家族的一个即char或long等的类型可以直接转为整数的类型此时我们就可以直接使用这个模板参数的缺省值HashFunc仿函数HashFunc仿函数里面用到了隐式类型转换例如如果K为char或double类型都会隐式转换为size_t类型返回。 然后我们将Insert、Find、Erase函数中的取模运算中的取pair对象的first的值都改为使用仿函数。 这样当我们将string类或自定义类型存入到哈希表中时只要自己提供了将自定义类型转换为整型的仿函数后就不会报错了。下面我们提供一个将字符串的第一个字母的ASCII码值转换为整型的仿函数来求出这个字符串在哈希表中的位置可以看到此时string类类型也可以直接存入哈希表中了。 但是上面我们提供的方法很容易产生冲突因为如果一个字符串的首字母相同时就会产生冲突。所以我们也可以将字符串的所有字符都加起来当作整型。如果是自定义类型的话那么可以取自定义类型中比较不容易重复的数据来当作算这个元素在哈希表中位置的整数。例如身份证号学号等。但是将字符串相加的仿函数也很容易出现冲突例如当字符串中字母相同但是顺序不同时也会冲突。 那么我们应该采用什么样的仿函数才能使字符串转换为整型后不容易产生冲突呢 各种字符串的Hash算法也有很多人研究我们可以看下面的文章来了解一些基本的算法。 文章链接 下面我们就使用一种综合性能比较优的算法BKDR算法来实现仿函数。此时可以看到字符串的顺序不一样算出来的整数也不一样虽然很接近但是不一样这样就可以减少字符串存储在哈希表中冲突的概率。 上面的方法我们在实际工作中可能也会用到例如当用一个字符串和大量一一字符串比较是否相等时如果一个字符串一个字符串的比较那么效率会很低。我们就可以在存入每一个字符串时多存一个整数项这个整数项是采用某种算法让一个字符串生成一个整数项并且相同的字符串生成的整数项一致这样当查找字符串是否相同时先比较这两个字符串生成的整数项是否相同。如果整数项相同了再一一比较这些整数项相同的字符串是否相同。这样就减少了不相等字符串的比较从而提高了比较效率。 我们看到库里面的unordered_map容器也提供了Hash仿函数即当使用自定义类型做key时就需要自己提供转换为整数的仿函数。而且库里面的unordered_map容器还提供了一个采用什么样的方法比较key相等的equal_to仿函数。例如你可以自己定义个位数都为5的key值就算相等的仿函数然后作为模板参数传递过去即可。 但是当我们使用库里面的unorder_map容器并且存入string类类型作为key时发现自己没有提供Hash仿函数但是也可以将string做key。即库里面的unorder_map支持了string作为key。这是因为库里面对string类型进行了特化。如果是string类型就会走特化的函数。这样做的原因就是因为string类型经常被用作key值。 所以我们也这样实现。我们将自己实现的HashTable也支持string类型做key。不需要用户自己提供string的仿函数。这样当编译器检查到HashTable表的key值中存入的是string类类型时就会调用特化的仿函数了。 3.4 哈希桶实现的哈希表的效率 哈希桶增删查改时间复杂度为O(1)最坏的情况是大部分数据都在一个桶内但是我们不考虑最坏情况只算平均情况。 因为有扩容的存在最坏情况基本不会出现因为有负载因子在控制着哈希表的扩容。如果真的出现最坏情况后当扩容后桶里面的数据会发生改变然后最坏情况就没有了。所以我们只看平均时间复杂度平均时间复杂度为O(1)。即如果最坏情况很少出现我们就算平均复杂度。例如快排取到的参考值为最小值的概率非常低所以我们只看平均复杂度。 下面我们来查看向哈希表中随机插入数据哈希表中哈希桶的最大长度。 我们看到哈希桶的最大长度为4所以很难出现最坏情况。如果真的出现了最坏情况此时我们也有解决办法我们可以做一个判断当桶的长度超过一定值后就将这个桶改成红黑树这样查找效率就会提高了。而java中有的容器其实就采用了这种方法。 还有一个需要注意的点是在大多数书上会建议除留余数法时最好模一个素数这样可能冲突的概率更小。并且SGI版本的stl库中实现的hashtable就采用了这种方法。即源码中提供了一个素数表每次都会取一个大于哈希表长度并且最接近哈希表长度的素数来进行取模运算。如果我们自己的扩容也想使用这个办法也可以向下面这样即每次会返回一个新的素数这个新的素数是在原素数2倍附近的一个素数。这样每一次哈希表扩容后的大小都为一个素数大小。 三、哈希表封装unordered_map和unordered_set容器 我们查看源码可以看到stl的源码中unordered_map 和 unordered_set容器的底层都是使用hashtable而且我们还看到hashtable的结点模板中只有一个模板参数所以我们使用自己写的哈希表来封装unordered_map和unordered_set容器也需要将我们的哈希表结点模板的模板参数改为只有一个。 1、unordered_map和unordered_set插入结点的实现 我们先改变自己的HashNode结构体的结构使这个模板只有一个模板参数当显示传入的为int或pair类型时该结点存的数据就是int或pair类型的数据。 然后因为默认存储数据的类型不是pair类型了所以我们在Insert等函数中就不能使用first来获取key值了因为我们也不知道哈希表中会存入什么样的数据类型所以此时我们需要给HashTable类模板加一个仿函数模板参数用来返回标识元素在哈希表中的位置的key值。 然后我们来实现unordered_map和unordered_set容器。这两个容器底层都是创建了一个哈希表因为我们想让这两个容器都可以复用我们写的哈希表的模板所以我们需要给这两个容器都写一个仿函数用来返回key的值。 然后我们将HashTable类中的获取key值的步骤都改为使用仿函数来获取。 下面我们来测试unordered_map和unordered_set容器的插入可以看到unordered_map和unordered_set容器都成功的插入了数据。 2、unordered_map和unordered_set迭代器的实现 下面我们来实现unordered_map和unordered_set容器的迭代器。 我们看到在源码中unordered_map和unordered_set容器的迭代器是复用的hashtable的迭代器所以我们只需要实现hashtable的迭代器即可。 下面我们来实现HashTable类的迭代器。HashTable的迭代器中因为需要搜索下一个结点所以迭代器中除了需要结点的指针外还需要HashTable的指针因为这样才能通过HashTable的指针找到哈希表然后搜索下一个结点。并且因为我们需要在迭代器中定义HashTable的指针所以我们需要提前声明HashTable类模板。 下面我们再来分析哈希表的迭代器的情况。因为哈希表的迭代器为单向迭代器所以我们只需要实现迭代器即可。哈希表的迭代器有两种情况第一种情况当前迭代器指向的结点的_next指针域不为空即桶中还有下一个结点此时直接将迭代器指向下一个结点即可。 第二种情况为当前迭代器指向的结点的_next指针域为空即桶中已经没有结点了此时需要先算出当前桶在HashTable中的位置然后通过HashTable指针向后遍历HashTable找到下一个不为空的桶然后将迭代器指向这个桶内的第一个结点。 实现了__HashIterator迭代器类的基本函数后我们再来实现HashTable类中的迭代器。HashTable类中的begin函数返回的是指向哈希表中第一个结点的迭代器所以我们需要遍历哈希表并且找到哈希表中的第一个结点end函数就返回一个指向空的迭代器即可。 下面我们复用HashTable的迭代器来实现unordered_map和unordered_set容器的迭代器。 下面我们来测试unordered_map和unordered_set容器的迭代器然后我们运行时发现报出了无法访问私有成员的错误这是因为在HashTable类中_tables成员为私有成员所以在__HashIterator中不能访问_tables成员。此时我们有两种办法来解决这个问题第一个办法就是在HashTable类中实现gettables和settables等函数。第二种办法就是在HashTable类中将__HashIterator类设置为友元类。 下面我们在HashNode类模板中设置__HashIterator类模板为友元类因为 __HashIterator为类模板所以我们在设置友元类时还需要加上 __HashIterator的模板。然后我们看到就可以使用unordered_map和unordered_set容器的迭代器来访问容器内的元素了。 3、unordered_map容器的[]操作符重载函数的实现 下面我们来实现unordered_map容器的[]操作符重载函数但是在实现这个函数之前我们需要先修改HashTable类之前的Find函数和Insert函数的返回值。 然后我们再来实现unordered_map容器的[]操作符重载函数。并且因为HashTable类里面的Insert函数的返回值改变了所以我们将unordered_map容器和unordered_set容器的insert函数的返回值也改为返回一个pair类类型对象。 下面我们来测试unordered_map容器的[]操作符重载函数。我们看到unordered_map容器的[]操作符重载函数可以正常使用。 下面我们来完善unordered_map容器和unordered_set容器的find函数和erase函数。 4、unordered_map容器中使用自定义类型做key 下面我们来完善当unordered_map容器中使用自定义类型做key值时 我们实现一个Date日期类然后将日期类作为unordered_map容器的key值。 当我们运行时发现报出了下面的错误这是因为当key值为自定义类型时不能转换为整型时就需要自己提供一个将自定义类型转换为整型的仿函数。 但是现在还存在一个问题就是unordered_map模板中只有两个模板参数如果我们实现了仿函数也无法传递给unordered_map容器因为这个仿函数参数是在HashTable模板中定义的。所以此时我们需要改为在unordered_map模板和unordered_set模板中传递仿函数。 然后我们采用DKRS算法来实现一个将Date日期类转换为整型的仿函数但是在实现的过程中因为Date日期类的_year、_month、_day为私有成员所以我们可以在Date日期类中将仿函数设置为友元类。然后我们创建unordered_map容器时将仿函数也传进去。 但是我们运行时发现出现了下面的错误这是因为在HashTable中我们使用了key类型的 的比较所以我们想要将自定义类型作为unordered_map的key值就需要给自定义类型提供 比较运算符的重载函数。 当我们实现了Date日期类的 的比较后此时就可以在unordered_map容器中将Date日期类作为key值了。 那么如果遇到我们要存到unordered_map容器作为key值的自定义类型中没有提供 的比较并且我们也无法修改这个自定义类型时此时我们可以再给unordered_map模板多加一个仿函数这个仿函数用来提供怎样判断key值相等。我们可以看到库里面就提供了一个这样的仿函数。 5、面试题map/set容器和unordered_map/unordered_set容器使用的条件 一个类型要做unordered_map/unordered_set的key时要满足支持转换成取模的整型还需要满足 比较运算符的重载函数。 一个类型要做map/set的key时要满足支持小于或者大于比较中的一个。因为 可以通过下面的这种if else 逻辑来得到。 if(cur-key key) {} else if(key cur-key) {} else {}6、unordered_map和unordered_set容器的const_iterator迭代器 我们上面实现的unordered_set容器还有一个没有解决的问题那就是我们可以使用unordered_set容器的迭代器来修改对应元素的值。这是肯定不可以的因为unordered_set容器中的元素都在插入哈希表时已经计算出了对应的位置而使用迭代器来修改元素的值的话就会使哈希表中存储的元素和其对应的位置不符。 所以下面我们要解决unordered_set容器中可以使用迭代器修改容器中元素值的问题。我们看到源码中采用的办法是unordered_set容器的普通迭代器和const迭代器都复用hashtable的const_iterator迭代器。这样就不能通过unordered_set容器的普通迭代器修改哈希表中元素的值了。 下面我们也使用源码中的方法我们先实现HashTable类的const_iterator迭代器。 然后下面我们再实现unordered_set的const_iterator迭代器此时我们先不将unordered_set的普通迭代器也复用HastTable的const_iterator迭代器。我们先测试unordered_set的const_iterator迭代器。 我们写一个方法来测试unordered_set的const_iterator迭代器然后发现报出了这样的错误这是因为发生了权限放大在print函数中s是被const修饰的对象所以在调用begin和end时传入的也是被const修饰的this指针而在__HashIterator的构造函数中第二个参数接收的是不被const修饰的HashTable的指针所以发生了权限放大。 此时我们可以将__HashIterator中的表示HashTable指针的成员使用const修饰因为我们在迭代器中只是遍历哈希表而不需要修改哈希表所以可以将_ht成员变量使用const修饰这样就解决了上面的权限放大的问题。 下面我们再让unordered_set的普通迭代器也复用HashTable的const_iterator迭代器。但是这样就会出现一个问题即unordered_set的begin函数内部调用_ht.begin函数因为_ht没有被const修饰所以会去调用HashTable中的不被const修饰的begin函数然后返回的也是HastTable的普通迭代器iterator但是unordered_set中将普通迭代器也复用了HastTable的const迭代器所以unordered_set的begin返回的其实是一个HastTable的const迭代器。即我们需要将_ht.begin函数返回的HastTable普通迭代器iterator隐式类型转换为HashTable的const迭代器。但是我们的__HashIterator类中没有将HashTable的普通迭代器变为HashTable的const迭代器的构造函数所以会出现下面的错误。 此时我们需要在__HashIterator中写一个构造函数支持HashTable的普通迭代器转换为const迭代器。然后我们就可以看到不能通过unordered_set的迭代器来修改哈希表中元素的值了。 下面我们再来实现unordered_map容器的const迭代器我们看到源码中unordered_map的普通迭代器就是复用的hashtable的普通迭代器const迭代器就是复用的hashtable的const迭代器这是因为unordered_map容器在实例化hashtable时就将pair对象的Key使用const修饰了那么在hashtable中就不能修改pair对象的first的值了。 下面我们也仿照源码中的方法来完善我们实现的unordered_map容器的const迭代器。我们看到使用迭代器不能修改unordered_map容器中的pair对象的first的值但是可以修改second的值。 四、哈希的应用 1、位图 1.1 位图实现 所谓位图就是用每一位来存放某种状态适用于海量数据数据无重复的场景。通常是用来判断某个数据存不存在的。 下面我们通过一个面试题来体会位图的应用。 给40亿个不重复的无符号整数没排过序。给一个无符号整数如何快速判断一个数是否在这40亿个数中。 上面这个问题我们通常会想到两种办法。一是遍历这40亿个数字二是先将这40亿个数字进行排序然后采用二分查找。但是这两种办法都需要很大的内存显然这两种办法是行不通的。因为我们此时只需要判断数据在不在所以此时就可以使用位图来解决。 位图其实就是一个采用直接定址法映射的哈希表我们用每一个比特位来标识一个数字在或是不在那么就只需要512MB的空间即可。这样当我们想要查看一个数是否存在时只需要查看位图中对应的比特位为0还是为1即可。 下面我们就来模拟实现位图。下面是位图的基本结构我们使用一个vector容器来当作位图。 位图中需要提供set函数和reset函数set函数就是将位图中第x个比特位置为1而reset就是将位图中第x个比特位置为0。 我们想要找到位图中第x个比特位我们需要先计算x映射的位在第几个char数组位置然后再计算x映射的位在这个char的第几个位置。 当我们想要让位图中第x个位置的比特位置为1时可以通过下面的按位或计算来实现。我们想要让位图中第x个位置的比特位置为0可以通过下面的按位与计算来实现。 下面我们来写test函数test函数就是判断位图中是否有指定的数字如果有的话就返回true如果没有就返回false。我们可以先计算出x在位图中相对应的位置然后让1与这个位置相与如果1还是1那么就说明位图中这个位置也为1即x数字存在。如果1相与后变为0那么说明位图中这个位置本来就为0即x数字不存在。 下面我们来写bitset的构造函数即给位图开多大空间。需要注意的是我们是根据数据的范围来开的位图的空间并不是根据数据的个数。例如有一组数据为 1 3 5 1000。那么我们也需要开1000个比特位大小的位图。并且因为size_t无符号整型的最大值为42亿多所以如果有100亿个size_t无符号整型数据时那么我们也只需要开42亿个比特位大小的位图因为size_t无符号整型最多能标识42亿多个数字100亿个数据中肯定有大量的重复数据故我们只需要申请42亿个比特位大小的位图存储即可。 我们看到当我们申请42亿比特位大小的位图时此时位图的空间为512MB。即我们只需要512MB的内存就可以存储42亿多个数据在或者不在的信息。 1.2 位图应用1 1. 给定100亿个整数设计算法找到只出现一次的整数 这一题我们可以创建两个位图即一个数字的状态采用两个比特位来表示那么就可以表示4个状态我们将00表示该数字一次没有出现01表示出现一次10表示出现一次以上。下面我们来实现代码。 我们通过测试可以看到print函数将只出现一次的数据打印了出来。 1.3 位图应用2 2. 给两个文件分别有100亿个整数我们只有1G内存如何找到两个文件交集 第一种办法 将其中一个文件的值读取到内存的一个位图中然后再读取另一个文件的值判断这个值在不在内存中的位图中如果在就是交集。但是这样的做法会使找到的交集中存在重复的值。例如下面的这个例子所以我们在找到交集后还需要进行去重。我们可以使用下面的方法来进行改进即每次找到交集后都将内存中的位图中对应的值设置为0这样就可以解决找到的交集中有重复值的问题。 下面我们使用数组来模拟文件中的数据可以看到两个数组的交集中并没有重复的数据。 第二种办法 我们也可以创建两个位图如果x在位图1和位图2中都存在那么x就是交集。或者我们也可以将位图1和位图2进行相与运算然后检查运算结果中为1的位置就是交集。 1.4 位图应用3 3.位图应用变形1个文件有100亿个int1G内存设计算法找到出现次数不超过2次的所有整数 这个题就是第一个题的变形。我们可以使用00表示出现0次01表示出现1次10表示出现2次11表示出现3次以上。 我们看到出现0次1次2次的数据都被打印了出来。 1.5 位图优缺点 位图的优点就是搜索一个整型在不在时的速度快可以达到O(1)的效率并且位图存储数字在不在的信息还节省空间。 位图的缺点就是只能映射整型如果是其它类型例如浮点数、string等类型不能存储映射。 2、布隆过滤器 我们在使用新闻客户端看新闻时它会给我们不停地推荐新的内容它每次推荐时要去重去掉那些已经看过的内容。问题来了新闻客户端推荐系统如何实现推送去重的 用服务器记录了用户看过的所有历史记录当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选过滤掉那些已经存在的记录。 那么如何快速查找用户的历史记录中有没有这条新闻呢 1.用哈希表存储用户记录缺点浪费空间。 2.用位图存储用户记录缺点位图一般只能处理整形如果内容编号是字符串就无法处理了。 3.将哈希与位图结合即布隆过滤器。 2.1 布隆过滤器概念 布隆过滤器是由布隆Burton Howard Bloom在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构特点是高效地插入和查询可以用来告诉你 “某样东西一定不存在或者可能存在”它是用多个哈希函数将一个数据映射到位图结构中。此种方式不仅可以提升查询效率也可以节省大量的内存空间。 当我们将字符串通过hash函数转换为整型后存储到位图中时因为有可能出现不同的字符串转换为同一个整型的情况所以这样的方法是一定会存在冲突问题的而且也无法避免。那么就可能会造成下面的误判情况例如下面的美团和B站的状态都存储在位图的第3个比特位那么当美团存在后虽然我们还没有插入B站但是此时判断B站也是存在的这样就造成了误判。 而布隆过滤器的思想就是降低冲突的概率即一个字符串映射一个位置容易产生误判那么让一个字符串采用多种hash函数生成多个整型值然后映射到多个位置这样就可以降低误判率。布隆过滤器并不是完全解决冲突而是降低冲突的概率。 但是这样的情况也会出现误判的概率例如下面的情况腾讯字符串转换的两个整型都和其它字符串冲突了而此时腾讯字符串虽然不在但是也会被判断为存在这就出现了误判的情况。所以布隆过滤器是不那么靠谱的一个数据结构。但是布隆过滤器只会在判断字符串在的时候出现误判而判断字符串不在的时候是准确的。因为要判断字符串不在必须要有一个位置为0而为0的位置说明没有冲突所以不会存在误判。 2.2 布隆过滤器实现 下面我们来简单实现一下布隆过滤器。 2.3 布隆过滤器使用场景 布隆过滤器是一个会产生误判的数据结构那么我们就需要在允许误判的场景下才能使用布隆过滤器。例如在注册账号时一般都会让添一个昵称然后需要快速判断这个昵称是否已经存在这个场景就可以使用布隆过滤器。 例如下面的场景数据库中存了10亿个用户的信息但是数据是存在磁盘中如果当用户输入昵称后从数据库中开始查找当前昵称是否存在这是很慢的会影响与用户的交互体验。而如果我们提前将用户的昵称存在一个布隆过滤器中那么当用户输入昵称时我们直接从布隆过滤器中查找昵称是否存在即可。而且虽然布隆过滤器会存在误判但是在用户层是不知道的。如果布隆过滤器判断这个昵称不存在那么这个昵称一定是不存在的因为布隆过滤器不会误判不存在的情况。而如果布隆过滤器判断这个昵称存在有可能是这个昵称真的存在还有可能是布隆过滤器的误判但是不管是不是误判用户都需要换一个新的昵称了而且用户也不知道这个昵称在数据库中是否是真的存在的。 但是判断用户的电话是否存在我们不能只使用布隆过滤器来判断因为如果布隆过滤器误判这个电话已存在时而用户是知道这个电话号码还没有注册账号的。所以当使用布隆过滤器判断用户的电话已经存在时此时我们需要再次去数据库进行查找判断当前电话号是否真的存在然后再将结果返回。而如果布隆过滤器判断电话号不存在那么这个电话号就一定是不存在的此时用户可以使用这个电话号。
http://www.pierceye.com/news/694077/

相关文章:

  • 网站稳定性不好的原因wordpress仿站维护
  • 银行管理系统网站建设最专业的医疗网站建设
  • 网站应该怎么做住建官网查询
  • 建设网站类型条形码生成器在线制作图片
  • 邯郸广告公司网站建设seo排名怎么做
  • 大眼睛网站建设做艺术品的网站
  • 自助免费网站建设平台网站开发php还是jsp
  • 网站建设成本多少北京怎么进行网页设计
  • 给个网站做导航违法吗游戏推广员每天做什么
  • 交互式网站开发技术全国企业信用公示信息公示网官网
  • 大连网站设计公司排名班级优化大师的功能有哪些
  • 旅游网站建设的概念ppt模板自己制作
  • 重庆网站建设首选承越网站开发建设方案
  • 创建一个网站的费用网站服务器租用报价
  • 潍坊企化网站建设大型免费网站制作
  • 松原网站制作网页制作的基本步骤流程
  • 太原网站建设制作机构西安网络seo公司
  • 移动网站建设报价表抖音代运营商
  • 镇平县两学一做网站服装网站建设推荐
  • 苏州建网站的公wordpress添加侧栏广告
  • 企业商城网站 .networdpress模板作者怎样去除
  • 强生网站还要怎样做衡水网站推广的网络公司
  • 茂名建站公司南通长城建设集团有限公司网站
  • 网络平台怎么建立网站吗做暧暧视频网站安全吗
  • 免费域名x网站网站前期准备工作
  • 陕西网站建设公司排名智能优化网站
  • 做瞹瞹网站萍乡做网站的公司有哪些
  • 网站建设的类型有几种wordpress搜索返回页面内容
  • 建设网站备案与不备案区别招远建网站首选公司
  • 四川住房和城乡建设厅网站三类人员软文网站备案如何查询