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使得它支持删除元素的操作。

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

相关文章
|
9天前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
15 1
|
22天前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
42 7
|
2月前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
72 4
|
2月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
85 5
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
65 2
|
2月前
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
65 0
|
25天前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
32 0
|
3月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
90 5
|
3月前
|
存储 自然语言处理 程序员
C++常用基础知识—STL库(1)
C++常用基础知识—STL库(1)
82 1
|
3月前
|
算法 数据处理 C++
c++ STL划分算法;partition()、partition_copy()、stable_partition()、partition_point()详解
这些算法是C++ STL中处理和组织数据的强大工具,能够高效地实现复杂的数据处理逻辑。理解它们的差异和应用场景,将有助于编写更加高效和清晰的C++代码。
54 0