C++STL——哈希(下)

简介: C++STL——哈希(下)

其他哈希函数

  1. 平方取中法
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
    再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。
  2. 折叠法
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
    折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。
  3. 随机数法
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
    通常应用于关键字长度不等时采用此法。
  4. 数学分析法
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定
    相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
    有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
    列地址。
    例如:

假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同

的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还

可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移

位、前两数与后两数叠加(如1234改成12+34=46)等方法。

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况。

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

哈希的应用

哈希切割(面试题)

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?

IP可以视为一个字符串,无法直接转成整形。

这道题可以将这100G的文件切割成1G的文件(100份)。

将100G文件中的IP通过哈希切割(哈希表与桶上面将string转成int类型的仿函数)转成整形,遍历一遍然后挨个%100放进这100份的文件中。

然后用map统计次数,统计完一个小文件释放掉这个map,在新创建一个map用来统计下一个,最后找到IP最多的。

那么如果一个小文件大小超过1G呢?这会有两种情况:

1.这个小文件不同IP很多,大多数都是不重复的,map统计不下。

2.这个小文件相同IP很多,大多数都是重读的,map可以统计下。

此时只要解决了第一种情况即可:

换个哈希切割的函数(方法),一定要换方法,不然切割出来的内容还是相同的,然后在从头开始进行上面的方法,分成100份,再用map统计即可。

区分这两种方式是,直接用map统计,如果是第一种情况map就会插入结点失败,抛异常。

第二种不会有任何的错误,会统计完,不会报错。

位图

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用

来判断某个数据存不存在的。(也是一种哈希的思想)

例如:用23个比特位就能判断这组数据是否存在。

位图应用(面试题)

1. 给40亿个不重复的无符号整数,没排序过,给一个无符号整数,如何快速判断一个数是否在这40亿个数。【腾讯】

40亿个整数大概是16G的大小,我们电脑台式机一般都是32G,笔记本一般是16G,这些数据要是放进内存肯定是不行的。

所以这里就适合用位图去搞。

一个无符号整形数据的范围是:

这里就放2^23-1个比特位的位图,大小算起来就是512MB的大小。

然后,因为一个字节是8个比特位,那么在开辟空间的时候就是按照字节开辟,存储比特位也通过字节位移的方式存储。

那么对应值如何映射到位图中呢?首先确定是在哪一个字节,也就是要/8,然后确定是该字节中的哪个比特位当中%8。

#include<iostream>
#include<vector>
using namespace std;
namespace baiye
{
  template<size_t N>
  class bit_set
  {
  public:
    bit_set()
    {
      _arr.resize(N / 8 + 1, 0);//这里是开辟多少个字节,+1是为了多余的比特位能有容身之所
    }
    void set(size_t x)//标记比特位
    {
      size_t i = x / 8;
      size_t j = x % 8;
      _arr[i] |= (1 << j);
    }
    void reset(size_t x)//清除比特位
    {
      size_t i = x / 8;
      size_t j = x % 8;
      _arr[i] &= (~(1 << j));
    }
    bool test(size_t x)//查找这个数是否存在
    {
      size_t i = x / 8;
      size_t j = x % 8;
      return _arr[i] &= (1 << j);//0就是不存在,非0就是存在
    }
  private:
    vector<char> _arr;
  };
}
void test()
{
  baiye::bit_set<100> arr;
  arr.set(10);
  arr.set(20);
  arr.set(30);
  cout << arr.test(30) << endl;
  cout << arr.test(20) << endl;
  cout << arr.test(10) << endl;
  arr.reset(30);
  cout << arr.test(30) << endl;
}
int main()
{
  test();
  return 0;
}

库里一个写好的位图:https://legacy.cplusplus.com/reference/bitset/bitset/?kw=bitset

2. 给定100亿个整数,设计算法找到只出现一次的整数?

这道题使用位图表示数据的状态,其实可以用两个比特位就能表示:

0次 00

1次 01

1次以上 11

我们可以开辟两个大小相同的位图结构:

一个位图对应另一个位图的相同位置成为一组。

#include<iostream>
#include<vector>
#include<bitset>
using namespace std;
namespace baiye
{
  template<size_t N>
  class bit_set
  {
  public:
    void set(size_t x)
    {
      if (arr1.test(x) == 0)
        arr1.set(x);
      else
        arr2.set(x);
    }
    void Printf()
    {
      for (size_t i = 0; i < arr1.size(); i++)
      {
        if (arr1[i] && !arr2[i])
        {
          cout << i << ' ';
        }
      }
      cout << endl;
    }
  private:
    bitset<N> arr1;
    bitset<N> arr2;
  };
}
void test()
{
  baiye::bit_set<100> arr;
  int a[] = { 10,25,25,66,66,66,78,49,32, };
  for (auto& e : a)
  {
    arr.set(e);
  }
  arr.Printf();
}

3. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

这道题和上一道题区别不大,开两个位图,遍历两个位图,如果都为1就是交集。

4. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整
数。

可以在第二题的基础上改动,:

0次 00

1次 01

2次 10

2次以上 11

布隆过滤器

给一组英文单词,想看这几个单词是否存在,我们可以用位图,但是冲突是不可避免的,完全不相同的单词通过HashFunc函数可能就转成了同一个整形,映射到了同一个位置,如果某个单词其实不存在,但是他映射的位置是1,那么这里就有了误判。

那么有什么办法能降低误判率呢?我们可以让每个单词同时映射2个位置。

C单词看两个位置如果都为1,就是存在,D单词也是,如果D单词不存在,C单词存在,D单词红线映射的部分就是0,黑线还是1,这样两个单词就不冲突了。(降低了误判率)

布隆过滤器的改进:映射多个位置,降低误判率。

多少个哈希函数(映射关系)与误判率的概率:

原文章地址:https://zhuanlan.zhihu.com/p/43263751/

m = k*n/0.7

假设k = 3 ,m = 4.2n,也就是说存一个值要开4个位出来。

代码实现

#include<iostream>
#include<string>
#include<bitset>
using namespace std;
struct Func1
{
  size_t operator()(const string str)
  {
    size_t hash = 0;
    for (auto& ch : str)
    {
      hash = hash * 131 + ch;
    }
    return hash;
  }
};
struct Func2
{
  size_t operator()(const string str)
  {
    unsigned int hash = 0;
    int i = 0;
    for (auto& ch : str)
    {
      if ((i & 1) == 0)
      {
        hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
      }
      else
      {
        hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
      }
      i++;
    }
    return hash;
  }
}; 
struct Func3
{
  size_t operator()(const string str)
  {
    size_t hash = 5381;
    for (auto& ch : str)
    {
      hash += (hash << 5) + ch;
    }
    return hash;
  }
};
template<size_t N,//假设N是最多储存数据的个数
class HashFunc1 = Func1,//映射几个位置就给几个哈希函数
class HashFunc2 = Func2,
class HashFunc3 = Func3,
class K = string>//因为大部分都是string类型,给个缺省值
class BloomFilter
{
public:
  void set(const K& key)//存数据
  {
    arr.set(HashFunc1()(key) % (5 * N));//将key分别映射到三个位置
    arr.set(HashFunc2()(key) % (5 * N));
    arr.set(HashFunc3()(key) % (5 * N));
  }
  bool test(const K& key)
  {
    size_t hashi1 = HashFunc1()(key) % (5 * N);
    size_t hashi2 = HashFunc2()(key) % (5 * N);
    size_t hashi3 = HashFunc3()(key) % (5 * N);
    if (!arr.test(hashi1))
      return false;
    if (!arr.test(hashi2))
      return false;
    if (!arr.test(hashi3))
      return false;
    return true;
  }
private:
  bitset<N * 5> arr;//原本是4.2,这里多扩大一点
};
int main()
{
  BloomFilter<10> arr;
  string str[] = { "666", "ckx","小黑子","鸡哥","树枝","蒸虾头","蒸乌鱼","香翅捞饭" };
  for (auto& e : str)
  {
    arr.set(e);
  }
  for (auto& e : str)
  {
    cout << arr.test(e) << " ";
  }
  cout << endl;
  string str1[] = { "1白龙马", "白1龙马","白龙1马","白龙马1","1白1龙1马1","白1龙1马","1白龙1马","白1龙马1" };
  for (auto& e : str1)
  {
    arr.set(e);
  }
  for (auto& e : str1)
  {
    cout << arr.test(e) << " ";
  }
  return 0;
}

第二组测试用例是近似的,但是也区分开来了,说明冲突的概率确实不大。(但是无法避免,只能缩小概率)

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

布隆过滤器的优缺点

优点

  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无
    关。
  2. 哈希函数相互之间没有关系,方便硬件并行运算。
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。
  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能。
  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算。

缺陷

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中。(补救方法:再
    建立一个白名单,存储可能会误判的数据)
  2. 不能获取元素本身。
  3. 一般情况下不能从布隆过滤器中删除元素。
  4. 如果采用计数方式删除,可能会存在计数回绕问题。

布隆过滤器应用(面试题)

1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。

精准算法:

query一般是查询指令,比如是一个网络请求,一个数据库sql语句等等。

假设每个query每个是50字节,那么100亿个就是500G。

想要精准计算,就要用到上面讲100G的文件分成100个文件的方法。

这里两个大文件各分成1000份小文件:HashFunc(query)%1000

然后通过一个两个小文件组成一对,找出他们的交集即可。

这里如果某个小文件超过规定的大小,那么就从头开始继续分,像之前的哈希切割一样。

近似算法:

用一个布隆过滤器,两个数据中是交集的一定会映射到一起去,但是也会有不是的映射到一起去。

2. 如何扩展BloomFilter使得它支持删除元素的操作。

可以在每个位图上面加一个计数的,每有一个映射在这个位置上就++,少一个映射就- - 。

相关文章
|
1月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
8月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
223 2
|
8月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
439 73
|
9月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
8月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
343 3
|
9月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
507 1
|
10月前
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
249 21
|
9月前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
11月前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
243 1
|
11月前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
365 7