【C++】哈希思想的应用(位图、布隆过滤器)及海量数据处理方法

文章目录

  • 前言
  • 位图
    • 什么是位图
    • 简单实现一个自己的位图
    • 位图的应用场景
  • 布隆过滤器
    • 位图的缺陷及布隆过滤器的提出
    • 布隆过滤器的概念
    • 简单实现一个自己的布隆过滤器
    • 布隆过滤器的优缺点
    • 布隆过滤器的应用场景
  • 海量数据处理

前言

哈希思想的在实际中的应用除了哈希表这个数据结构之外还有其他的一些数据结构,比如说位图、布隆过滤器、基数树等,在了解和学习哈希表之后,个人又去了解了位图和布隆过滤器,这里对这两个数据结构的概念、实现、应用场景、优缺点做一个简单的总结,这是第一部分;第二部分就是总结一下,当遇到海量数据处理问题时该怎么用哈希思想来进行解决(这里说的海量数据不是指大数据那个级别,而是解决问题时数据量过大,无法在内存中直接处理的场景)。

这里附上之前总结哈希表时写的文章《由浅入深一步步了解什么是哈希(概念向)》。

位图

什么是位图

位图是一种基于哈希思想实现的数据结构,数据结构这个东西是为了解决实际问题创造出来,因此结合实际问题问题来理解什么是位图比单纯讲概念更有价值和更有效率。

这里是一道腾讯的面试题:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中?

首先来分析一下题目:

  1. 在内存中存储一个无符号整数需要 4 Byte,有40亿个就需要消耗 160亿 Byte。
  2. 在计算机中,1 GB = 1024 MB,1 MB = 1024 KB,1 KB = 1024 Byte,经过换算之后,可以发现要想在内存中完全存的下这40亿个数字,至少得 14.9 GB 的空间。
  3. 这个级别的空间消耗可以推断出这份数据大概率不会在内存中而是存储在外设上,所以在解决问题的时候就需要将数据读取到内存中,同时考虑到读取数据过程中内存会满的问题,就需要分段式读取。

【方法一】
分析完题目之后,首先想到的第一个解决问题的方法就是遍历,将目标值和读取到内存中的数据依次比较,相等就说明存在,反之则不存在,这个方法的时间复杂度是 O ( N ) O(N) O(N),空间复杂度取决与内存的承受能力。

如果说只进行一次判断的话,这个方法其实还好,如果要进行多次判断这个问题就有巨大的缺陷了,因为每次判断都要将40亿个数字遍历一遍,遍历就要读取数据,读取数据的本质其实就是内存在向外设拿数据,数据读取的速度受限于内存和外设之间的数据传输速率,同时随着内存空间的减少得承担计算机性能下降的风险,总的来说是不推荐的。

【方法二】
说到快速判断,这里可以考虑二分查找算法,它的时间复杂度是 O ( log ⁡ N ) O(\log N) O(logN),但是二分查找有个前提,数据得是有序的,题目中说过数据没有排序,这里就需要对数据进行排序,选用的排序算法是外排序的归并排序,时间复杂度是 O ( N log ⁡ N ) O(N \log N) O(NlogN);虽然这个方法在二次及以上判断时同样面临着数据读取的消耗问题,但是由于二分查找的时间复杂度足够小,查找的速度还是很快的。

这个方法虽然克服了方法一的缺点,但是二分查找算法要求数据是有序的这个前提本身就是个缺陷,假设对有序数据插入新数据之后不一定保证插入之后数据仍然是有序的,这时候就得重新排序了,因此这个方法也不是最优解。

【方法三】
第三个方法也是较为不错的解决方法——位图。

在这个问题中,我们可以将无符号整数的数据范围中的从 0 0 0 2 32 − 1 2^{32}-1 2321(约等于42亿9千万多)的这 2 32 2^{32} 232 个数字看作全集,题目中给出的这40亿个不重复的无符号整数是这个全集的子集

判断一个子集中的数字在或不在这个全集中,只有两种状态,这时候我们就可以用一个二进制位(比特位)来表示这个数字的状态,“1” 就是 “在”,“0” 就是 “不在”,换言之,只需要 2 32 2^{32} 232 个比特位就能够表示题目中给出的这40亿个不重复的无符号整数存储状态

这时候我们可以先遍历一遍子集中的所有数字来获取它们的存储状态,而有了这么一组二进制位之后,往后我们需要判断某个数字在或不在子集中时就从文件中查找转变成到这组比特位中判断这个数字对应的比特位是 0 还是 1,从而快速地得到结果,这个操作的时间复杂度是 O ( 1 ) O(1) O(1),而且 2 32 2^{32} 232 bit 经过换算也才 512 MB,对于内存来说,这完全是小意思;

此外,新增数据也不是问题,我们可以先将新增数据对应的比特位设置位 1,再写入到文件中,这样判断存不存在的效率依旧是 O ( 1 ) O(1) O(1)

像方法三中这样的,和数据建立映射关系,用于表示数据状态的一组比特位集合,我们通常将这种结构称之为 “ 位图 ”

在这里插入图片描述

简单实现一个自己的位图

像C++,Java这样的主流语言其实已经提供有位图这样的工具了,比如说,在C++中位图可以使用标准库中的 std::bitset 容器实现;在Java中位图可以使用 java.util.BitSet 类实现。我们只需要去了解怎么使用就好了,为什么还要去手搓轮子呢?

  1. 深入理解: 通过手动实现位图,可以更深入地理解其原理和实现细节,有助于提高对数据结构和算法的理解和掌握。
  2. 学习编程技能: 手动实现位图可以锻炼编程能力和技巧,提高代码设计和实现的能力。
  3. 定制需求: 有时候现有的位图实现可能无法满足特定的需求,手动实现可以根据具体情况进行定制和优化。
  4. 面试准备: 在面试中,展示手动实现位图的能力可能会给面试官留下深刻印象,展示出对基本数据结构的扎实掌握。

自己的实现位图当然不需要像标准库中提供的那样复杂,只需要实现主要的功能就可以了,因为个人主修C++,所以这里的实现主要是模拟C++的STL中的 bitset 容器来实现,大致框架如下:

#include <cassert>namespace MyBitSet
{// 每个位图的大小由非类型模板参数 N 指定template<size_t N>class bitset{public:bitset() {}				// 构造函数void set(size_t x) {}	// 将x映射的比特位设置为1void reset(size_t x) {} // 将x映射的比特位设置为0void test(size_t x) {}	// 返回x映射的比特位的状态protected:vector<char> _bits;		// 用于存储位图数据的数组};
}

第一步要实现的是【构造】
首先,需要考虑如何表示位图的每个元素。在C++中,最小的存储单位是字节(byte),无法直接以比特位为单位存储数据。因此,需要以字节为单位来存储每个比特位的状态,每个字节可以表示8个比特位的状态。这时候,问题就变成了从如何开辟 N 个比特位大小的位图转换成了如何根据位图的大小 N 计算需要多少个字节来存储位图数据。

计算字节数也不难,但是要注意向上取整的问题,比如说,我要开辟一个拥有9个比特位的位图,但是一个字节才8个比特位,这时候我们就需要两个字节的空间。

由此我们可以知道,位图中实际的比特位个数 ≥ 需求的 N 个比特位的,为了防止越界访问,这就要求在后续操作中要进行越界检查。

bitset()   				// 构造函数
{// 向上取整,有时候会多开辟一些比特位_bits.resize(N / 8 + 1, 0);
}

第二步要实现的是【set】和【reset】
要将某个数字映射的比特位设置为 1 或 0,首先就得通过这个数字找到这个比特位。

假设以数字 27 为例,要在下面这个位图中找到数字 27 映射的比特位。
在这里插入图片描述

步骤1:先找到这个比特位在 vector 的哪个元素上,假设是第 i 个,由于比特位是8个为一组,因此,i = 27 / 8 = 3,即在 vector[3] 上。
在这里插入图片描述

步骤2:再找到这个比特位在 vector[3] 上的哪个位置,假设是第 j 个,由于比特位是8个为一组,因此,j = 27 % 8 = 3,即在第 3 个比特位。但是这时候新的问题产生了,数字 27 映射的比特位是 vector[3] 上从左往右数的第三个比特位还是从右往左数的第三个比特位呢?
在这里插入图片描述

如果你有这个疑问说明,说明你对计算机的大小端存储模式有一定了解,在小端模式下,低位字节存储在低地址,高位字节存储在高地址;在大端模式下,高位字节存储在低地址,低位字节存储在高地址。因此,在不同的存储模式下,字节的排列顺序是不同的。

但是实际上,我们不需要去在乎这个差异问题,因为一般来说语言是有可移植性,它能够屏蔽底层差异,从而一份代码既能在大端存储模式的机器上运行,也能在小端存储模式下运行,比如说C/C++的<<(左移)运算符就是默认往高地址位移动,>>(右移)运算符就是往低地址移动,j == 3,就表示是从低往高数的3个比特位(vector[3] << 3),至于底层实际怎样移动,我们不需要关心。

能够找到这个比特位了,下面就是位运算的问题了,【set】在如何不影响位图中其他的比特位的情况下将某个比特位设置位 1,【reset】在如何不影响位图中其他的比特位的情况下将某个比特位设置位 0。

在这里插入图片描述

// 将x映射的比特位设置为1
void set(size_t x) 
{// 越界检测assert(x <= N);int i = x / 8;int j = x % 8;_bits[i] |= (1 << j);
}// 将x映射的比特位设置为0
void reset(size_t x) 
{// 越界检测assert(x <= N);int i = x / 8;int j = x % 8;_bits[i] &= ~(1 << j);
}

第三步要实现的是【test】
和前面的类似,都是先找到比特位,但是有一点不同的是,【test】仅仅只是查看,而前面两个是修改,因此这里用的是 & 而不是 &=

// 返回x映射的比特位的状态
void test(size_t x) 
{// 越界检测assert(x <= N);int i = x / 8;int j = x % 8;return _bits[i] & (1 << j);
}

到这里,关于位图最核心的三个接口已经实现好了,如果有需求的话可以在这个基础之上扩展。

位图的应用场景

  1. 快速查找某个数据是否在一个集合中
    例子: 上面提到的那道腾讯面试题就是一个典型的例子。

  2. 排序 + 去重
    例子: 对一个包含大量重复元素的数组进行排序并去重。使用位图记录数组中出现过的元素,然后进行排序和去重操作。通过遍历位图,可以得到排序后的唯一元素集合。

  3. 求两个集合的交集、并集等
    例子: 求两个用户的好友列表中共同的好友。将每个用户的好友列表表示为位图,然后进行位运算操作,如与操作得到共同好友,或者或操作得到两个用户的所有好友。

  4. 操作系统中磁盘块标记

    例子: 在文件系统中标记磁盘块的分配情况。
    在这里插入图片描述

布隆过滤器

位图的缺陷及布隆过滤器的提出

位图的性能和效率确实很优秀,但位图也有很强的局限性,位图的底层实现是基于比特位的,每个比特位只能表示0或1,因此位图比较适合处理整数等固定长度的数据类型。但对于字符串等变长数据类型,位图难以处理。

为什么这么说呢?

位图将数据与比特位建立映射关系的方法类似与哈希方法中的直接地址法,即采用绝对编址的方式给一定范围内的所有数据分配一个比特位来表示数据状态,这种建立映射关系的方法对于整数来说是不存在哈希冲突的。

字符串不同于整数,字符串不能不能直接建立映射关系,必须得先经过字符串哈希函数转换成一个整数才可以建立映射关系。

问题有两点,一是经函数转换出来的整数的范围是不确定的,有可能会超出位图的范围,但是可以通过取余操作来加以限制;二是字符串的范围在不加以限制的情况下可以说是无限的,不同字符串经过字符串哈希函数有极大的概率转换成的相同的整数,换言之,会出现不同数据映射到同一个比特位的情况出现,这个就是哈希冲突,而位图无法处理哈希冲突。

在这里插入图片描述

于是,就有人针对位图的这个缺陷提出了一种新的结构,叫做布隆过滤器。

布隆过滤器的概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “ 某样东西一定不存在或者可能存在 ”,换言之,布隆过滤器存在误判的情况。

布隆过滤器看似很高级的样子,但本质上就是位图的一个变形和延申,增加空间消耗和处理手段来尝试解决位图无法处理哈希冲突的问题。

相比起位图,数据是怎么映射到布隆过滤器中的?
布隆过滤器的底层还是一个位图,当一个数据映射到位图中时,布隆过滤器会用多个哈希函数将其映射到多个比特位,当判断一个数据是否在位图当中时,需要分别根据这些哈希函数计算出对应的比特位,如果这些被映射的比特位都被设置为1则判定为该数据存在,否则判定为该数据不存在。

同样以字符串 “hello” 和 “你好” 举个例子,假设布隆过滤器使用的哈希函数有3个。
在这里插入图片描述

从图中可以看到,字符串 “hello” 经过哈希函数3 和字符串 “你好” 经过哈希函数1映射到的是同一个比特位,虽然这一个比特位发生了冲突,但是这并不影响对于这两个字符串存在或不存在的判断。

那为什么说布隆过滤器存在误判的情况呢?
假设现在有一个没有建立映射关系的字符串 “张三”,可是在布隆过滤器中查询之后神奇地发现,它会返回字符串 “张三” 已经存在了的这样一个错误结果,这就是为什么说布隆过滤器是一个概率型数据结构。
在这里插入图片描述

通过上面的分析可以发现,布隆过滤器并没有真正的解决位图发生哈希冲突的问题,而是通过应用多个哈希函数将数据映射到多个比特位的方法来降低哈希冲突发生的概率来近似解决问题。

既然布隆过滤器存在误判,那么该如何控制误判率呢?
误判率和两个因素有关:

  1. 布隆过滤器的长度
    布隆过滤器的长度即位图的大小,它直接影响了误判率。过小的布隆过滤器容易发生碰撞,导致误判率升高。因此,在不增加哈希函数的情况下可以通过增加位图的大小来降低误判率。通常情况下,布隆过滤器的长度越长,误判率越低。

  2. 哈希函数的长度
    当布隆过滤器的长度一定时,哈希函数的个数的个数越多,位图中比特位被设置为1的速度就越快,同时也会增加误判率,因此,需要权衡哈希函数的个数,选择一个适当的值来保证误判率在可接受的范围内。

至于长度和个数到底该怎么设置,个人建议可以参考下面这张表。

图片来自文章《布隆过滤器设置合适指标》,表中的 k k k 指的是哈希函数的个数 p p p 指的是误判率 r r r 指的是当插入的数据量位 n n n 时,需要将布隆过滤器的长度设置为 r × n r \times n r×n
在这里插入图片描述

简单实现一个自己的布隆过滤器

在了解和学习布隆过滤器过程中,个人发现应用到布隆过滤器的场景还是非常多的,只是了解概念终归是之上谈兵,纸上得来终觉浅,绝知此事要躬行,所以接下来就要手搓一个自己的布隆过滤器,搓完之后再对它的性能作一番测试。

这里实现的布隆过滤器的考量大致有以下几点:

  1. 这里实现的布隆过滤器用到的哈希函数个数为 3 3 3
  2. 这里实现的布隆过滤器默认处理的数据的类型为字符串(string)。
  3. 假设布隆过滤器处理的数据量为 N N N,布隆过滤器的长度为 M M M,那么参考上面 的表格, M = 5 N M = 5N M=5N
  4. 布隆过滤器用的位图是STL提供的std::bitset
  5. 布隆过滤器使用的哈希函数参考自文章《各种字符串Hash函数》。
  6. 布隆过滤器实现的接口主要有【Set】和【Test】,至于为什么没有【Reset】后面细讲。

【代码如下】

struct BKDRHash
{size_t operator()(const string& str){register size_t hash = 0;for (auto& ch : str){hash = hash * 131 + ch;    }return hash;}
};struct DJBHash
{/// @brief DJB Hash Function  /// @detail 由Daniel J. Bernstein教授发明的一种hash算法。  size_t operator()(const string& str){if (str == "")return 0;register size_t hash = 5381;for (auto& ch : str){hash += (hash << 5) + ch;}return hash;}
};struct APHash
{/// @brief AP Hash Function  /// @detail 由Arash Partow发明的一种hash算法。   size_t operator()(const string& str){register size_t hash = 0;for (size_t i = 0; i < str.size(); i++){if ((i & 1) == 0){hash ^= ((hash << 7) ^ str[i] ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ str[i] ^ (hash >> 5)));}}return hash;}
};template<size_t N,               // 处理数据个数class T = string,            // 布隆过滤器处理的数据类型class Hash1 = BKDRHash,      // 第一个哈希函数class Hash2 = DJBHash,       // 第二个哈希函数class Hash3 = APHash>        // 第三个哈希函数
class BloomFilter
{
public:void Set(const T& key){_bs->set(Hash1()(key) % M);_bs->set(Hash2()(key) % M);_bs->set(Hash3()(key) % M);}bool Test(const T& key){if (_bs->test(Hash1()(key) % M) == false)return false;if (_bs->test(Hash2()(key) % M) == false)return false;if (_bs->test(Hash3()(key) % M) == false)return false;// 存在误判(有可能3个位都是跟别人冲突的,所以误判)return true;}protected:static const size_t M = 5 * N;  // 位图的长度std::bitset<M>* _bs = new std::bitset<M>;   // 在堆上申请位图的空间
};

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

现有一个布隆过滤器,其中映射(插入)了三个字符串,分别是"hello""你好""张三"
在这里插入图片描述
现在要删除字符串"张三",但是发现连带地将字符串"你好"也删除了,这就是为什么不建议加上【Reset】接口原因。
在这里插入图片描述

布隆过滤器的优缺点

【优点】

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

【缺点】

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

布隆过滤器的应用场景

我们在某网站或者某游戏注册账号时都要填写用户信息,其中一般有 “用户名” 或者 “昵称” 这一项,当我们填写完之后,网站或者游戏会快速反馈一个 “该昵称未被使用” 或者 “该昵称已被使用” 的信息,这里网站或者游戏快速检测昵称是否被使用就可以应用布隆过滤器。

一般网站或者游戏都有自己的后端数据库存储和管理账号信息,这时候就可以将数据库中所有的账号的昵称或者名字映射到布隆过滤器中,这样新用户注册新账号时填写昵称就可以快速获得反馈。

由于布隆过滤器存在误判,返回未被使用的昵称一定是未被使用的,但是返回已被使用的昵称却不一定被使用了,这时候就可以直接到数据库中查询该昵称是否真的已经被使用,来解决误判问题。

至于为什么不直接到数据库中查询,而是要再加上布隆过滤器这一层主要是为了查询效率,随着网站或者游戏的体量越大,其数据库中的数据量越多,查询所花费的时间就越多,这么做反馈到用户上就是优化差、延迟高的差评,从而导致用户流失,这是难以承受的。

而结合布隆过滤器和数据库查询,可以在保证查询速度的同时,有效地减少数据库查询的压力,从而提高系统的响应速度和性能,避免用户因查询耗时而产生不满。

海量数据处理

第一道:给定一个拥有100亿个整数的数据集,设计算法如何找到其中只出现一次的整数?

题目中与统计次数相关,首先考虑使用map容器或者哈希表,但是经过分析发现超过内存容量,无法直接处理,分析如下:

map容器底层为红黑树,一个结点要存储三个指针、一个颜色位、一个键值对,在32位机器下一个结点消耗 24 Byte内存,处理数据集至少要 24 × 5e9 ≈ 1200 亿 ≈ 120 GB 24 \times \text{5e9} \approx 1200 \text{ 亿} \approx 120 \text{ GB} 24×5e91200 亿120 GB 的内存空间,在64位机器下一个结点消耗 36 Byte内存,处理数据集至少需要 36 × 5e9 ≈ 1800 亿 ≈ 180 GB 36 \times \text{5e9} \approx 1800 \text{ 亿} \approx 180 \text{ GB} 36×5e91800 亿180 GB 的内存空间。

哈希表假设底层是哈希桶,挂桶的表的大小在32位下约为 4 × 5e9 ≈ 200 亿 ≈ 20 GB 4 \times \text{5e9} \approx 200 \text{ 亿} \approx 20 \text{ GB} 4×5e9200 亿20 GB,在62位下约为 8 × 5e9 ≈ 400 亿 ≈ 40 GB 8 \times \text{5e9} \approx 400 \text{ 亿} \approx 40 \text{ GB} 8×5e9400 亿40 GB,每个结点存储一个键值对和一个指针,在32位机器下一个结点消耗 12 Byte内存,所有结点至少要 12 × 5e9 ≈ 600 亿 ≈ 60 GB 12 \times \text{5e9} \approx 600 \text{ 亿} \approx 60 \text{ GB} 12×5e9600 亿60 GB 的内存空间,在64位机器下一个结点消耗 16 Byte内存,处理所有结点至少需要 16 × 5e9 ≈ 900 亿 ≈ 90 GB 16 \times \text{5e9} \approx 900 \text{ 亿} \approx 90 \text{ GB} 16×5e9900 亿90 GB 的内存空间。

5 e 9 5e9 5e9 指的是数据集中只有一个数字出现一次,其余数据出现次数至少两次,假设是两次,就要为50亿个不重复的整数统计次数。

但是,题目只要求找到出现次数为 1 的数字,这样我们就可以数据集中的数字分成出现次数为 1 的数字和出现次数为其他的数字,然后通过两个位图组合的方式来进行处理,而两个100亿bit的位图大小总共也才2.5 GB而已,这是空间复杂度。
操作上,首先得遍历一遍数据集统计次数(如果出现次数超过 3 次就不管了),再遍历一遍位图,时间复杂度是 O ( N ) O(N) O(N)
在这里插入图片描述
第二道:给定A、B两个文件,其中分别存储了100亿个整数,最大可用内存只有1 GB,如何找到两个文件交集?

第二道题也是用位图处理,将文件A的数字映射到位图A中,将文件B中的数据映射到文件B中,然后同时遍历这两个位图,如果对应位置的比特位的值都为 1,那说明这个位置对应的数字就是交集的数字。

问题就在于第二道题对内存容量做出了限制,限定最大可利用内存容量只有 1 GB,两个100亿bit的位图大小至少要 2.5 GB 内存容量,这时候就可以分段处理(假设最大值是100亿),将数据分成[0, 25亿),[25亿,50亿),[50亿,75亿),[75亿,100亿]四个区间,找到4个区间的交集之后再组合就是A、B两个文件的交集了。

第三道:现有两个分别有100亿个 query 的文件,最大可用内存只有1 GB,如何找到两个文件交集?分别给出精确算法和近似算法。

题目分析
问题中的 query 指的是查询的意思,一个 query 可以理解为一个字符串,假设一个 query 大小为 50 Byte,一个文件总共有 100 亿个 query,一个文件的大小约为 500 GB,A、B两个文件总计约占 1 T的存储空间。

近似算法:布隆过滤器

  1. 将文件A中的 query 映射到布隆过滤器中。
  2. 遍历文件B中的 query,并在布隆过滤器中检查它们的存在性。
  3. 如果文件B中的一个 query 被布隆过滤器检测为存在,那么它就是交集之一。
  4. 通过这种方法找到的交集是不完全正确的,交集中的一部分是因为布隆过滤器的误判而放进来的。
    在这里插入图片描述

精确算法:哈希切分 + 去重 + 求交集
在C++中,想要找到一个精确的交集,就需要std::set(集合)或者std::unordered_set(哈希表)这样的容器来处理,可是由于A、B文件都属于大型文件,哪怕去重之后都有可能超过内存容量,因此可以考虑将源文件切分成能直接被内存处理的小文件。

文件切分有着平均切分和哈希切分两种做法。

第一种做法,平均切分,又称均匀切分,指的是将源文件均匀地分割成多个子文件,每个子文件的大小大致相同。 例如,如果有两个大小约 500 GB 的文件,可以将每个文件切分成500个大小约为1 GB的子文件。这个做法的优点是切分容易实现,每个子文件的大小相近,可以保证每个子文件中的数据分布比较均匀;缺点是在查找交集时存在一个严重的问题,即会导致时间复杂度为 O ( N 2 ) O(N^2) O(N2)

这是因为,在找到A、B两个文件的交集时,需要分别比较A文件中的每个query是否在B文件中,而由于A、B文件被切分成了多个子文件,因此需要对每个A文件中的query都与B文件中的所有query进行比较,这样的比较次数将会非常多,导致时间复杂度非常高。因此,平均切分不适用于大型文件的交集查找问题。

因此选择第二种做法,哈希切分,指的是在切分过程中,通过对数据进行哈希计算,将相同哈希值的数据划分到同一个子文件中。 例如,建立1000个小文件(尽可能让每个小文件的大小小于1 GB)同时给它们取一个编号(比如说, A 1 A_1 A1, A 2 A_2 A2, A 3 A_3 A3,……, A 999 A_{999} A999),然后遍历源文件使用哈希函数对query进行哈希计算,然后根据哈希值的范围将query划分到不同的子文件中(经过取余操作)。

就这样,经过哈希切分之后,就将A、B两个大型文件找交集的文件变成了1000组的 A i A_i Ai B i B_i Bi子文件找交集再组合的问题。
在这里插入图片描述

这样做的优点是文件A、B中相同的query一定会进入编号相同的子文件 A i A_i Ai B i B_i Bi中,找到A、B两个文件的交集时不用花费过多的力气;缺点是这样切分出来的子文件大小分布不均匀,有可能某个文件特别大(比如说,5 GB),有个文件特别小(比如说,50 MB)。

导致某个文件特别大的原因可能有两个,一是子文件中重复的query太多(重复指的是哈希值相同且query相同);二是子文件中冲突太多(冲突指的是哈希值相同但query不相同)。

当然这也是有处理方法的,这里可以不管子文件大小是否超过内存容量,统一将 A i A_i Ai子文件用std::set<string> setA容器来存储, B i B_i Bi子文件用std::set<string> setB容器来存储,假设遇到情况一,std::set容器会自动去重,这个完全不用担心;如果遇到情况二,也是先进行存储,如果存储过程中出现内存不足的异常,就说明文件过大,这时候就可以对这个子文件进行二次哈希切分再处理。

这样通过哈希切分源文件 + std::set容器去重 + 在子交集组合的方式以 O ( N ) O(N) O(N)的时间复杂度,在最大可用内存容量只有1 GB的情况下,完成两个大型文件找交集的问题。

【总结】
从上面这三道题来看,处理这些大规模数据,

  1. 考虑mapset、哈希表、位图、布隆过滤器这样的数据结构能不能直接处理。
  2. 第一步失败,考虑使用哈希切分出子文件,然后通过数据结构来处理,最后将子问题答案进行总结就是最终结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/798905.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Redis 知识储备】读写分离/主从分离架构 -- 分布系统的演进(4)

读写分离/主从分离架构 简介出现原因架构工作原理技术案例架构优缺点 简介 将数据库读写操作分散到不同的节点上, 数据库服务器搭建主从集群, 一主一从, 一主多从都可以, 数据库主机负责写操作, 从机只负责读操作 出现原因 数据库成为瓶颈, 而互联网应用一般读多写少, 数据库…

C++的List类(一):List类的基本概念

目录 前言 List类的基本概念 List的构造函数 List类迭代器的使用 List的功能 List的元素访问 List与vector比较 前言 vector的insert和erase都会导致迭代器失效list的insert不会导致迭代器失效&#xff0c;erase会导致迭代器失效 insert导致失效的原因是开辟了新空间后…

Visual Studio 2022-C语言如何防止头文件多次引入

头文件的包含 本地⽂件包含 #include "filename" 查找策略&#xff1a;先在源⽂件所在⽬录下查找&#xff0c;如果该头⽂件未找到&#xff0c;编译器就像查找库函数头⽂件⼀样在 标准位置查找头⽂件。 如果找不到就提⽰编译错误。 Linux环境的标准头⽂件的路径&…

如何自定义项目启动时的图案

说明&#xff1a;有的项目启动时&#xff0c;会在控制台输出下面的图案。本文介绍Spring Boot项目如何自定义项目启动时的图案&#xff1b; 生成字符图案 首先&#xff0c;找到一张需要设置的图片&#xff0c;使用下面的代码&#xff0c;将图片转为字符文件&#xff1b; impo…

【Unity每日一记】鼠标相关API

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

Linux初学(十七)redis

一、简介 redis就是一个内存数据库 redis中的数据&#xff0c;都是保存在内存中 端口&#xff1a;6379 二、安装redis 方法一&#xff1a;编译安装 方法二&#xff1a;yum安装-epel 第一步&#xff1a;配置epel源 详见&#xff1a;http://t.csdnimg.cn/AFl1K第二步&#xff1a…

GaN肖特基势垒二极管(SBD)的多阴极应用建模与参数提取

GaN Schottky Barrier Diode (SBD) Modeling and Parameter Extraction for Multicathode Application&#xff08;TED 24年&#xff09; 摘要 本文提出了一种适用于多阴极应用的紧凑型可扩展GaN肖特基二极管大信号模型。详细给出了外在和内在模型参数的可扩展规则。实验和理…

Stm32 HAL库 访问内部flash空间

Stm32 HAL库 访问内部flash空间 代码的部分串口配置申明文件main函数 在一些时候&#xff0c;需要存储一些数据&#xff0c;但是又不想接外部的flash&#xff0c;那我们可以知道&#xff0c;其实还有内部的flash可以使用&#xff0c; 需要注意的是内部flash&#xff0c;读写次数…

书生浦语训练营二期第三次作业

文章目录 基础作业1. 在茴香豆 Web 版中创建自己领域的知识问答助手第一轮对话第二轮对话第三轮对话第四轮对话第五轮对话 2.在 InternLM Studio 上部署茴香豆技术助手修改配置文件创建知识库运行茴香豆知识助手 基础作业 1. 在茴香豆 Web 版中创建自己领域的知识问答助手 我…

神经网络中的超参数调整

背景 在深度神经网络学习和优化中&#xff0c;超参数调整一项必备技能&#xff0c;通过观察在训练过程中的监测指标如损失loss和准确率来判断当前模型处于什么样的训练状态&#xff0c;及时调整超参数以更科学地训练模型能够提高资源利用率。在本研究中使用了以下超参数&#x…

iOS 17.5系统或可识别并禁用未知跟踪器,苹果Find My技术应用越来越合理

苹果公司去年与谷歌合作&#xff0c;宣布将制定新的行业标准来解决人们日益关注的跟踪器隐私问题。苹果计划在即将发布的 iOS 17.5 系统中加入这项提升用户隐私保护的新功能。 科技网站 9to5Mac 在苹果发布的 iOS 17.5 开发者测试版内部代码中发现了这项反跟踪功能的蛛丝马迹…

XML HTTP传输 小结

what’s XML XML 指可扩展标记语言&#xff08;eXtensible Markup Language&#xff09;。 XML 被设计用来传输和存储数据&#xff0c;不用于表现和展示数据&#xff0c;HTML 则用来表现数据。 XML 是独立于软件和硬件的信息传输工具。 应该掌握的基础知识 HTMLJavaScript…

使用pytorch构建有监督的条件GAN(conditional GAN)网络模型

本文为此系列的第四篇conditional GAN&#xff0c;上一篇为WGAN-GP。文中在无监督的基础上重点讲解作为有监督对比无监督的差异&#xff0c;若有不懂的无监督知识点可以看本系列第一篇。 原理 有条件与无条件 如图投进硬币随机得到一个乒乓球的例子可以看成是一个无监督的GAN&…

从0到1搭建文档库——sphinx + git + read the docs

sphinx git read the docs 目录 一、sphinx 1 sphinx的安装 2 本地构建文件框架 1&#xff09;创建基本框架&#xff08;生成index.rst &#xff1b;conf.py&#xff09; conf.py默认内容 index.rst默认内容 2&#xff09;生成页面&#xff08;Windows系统下&#xf…

实战webSocket压测(三)Jmeter真实接口联调

背景&#xff1a; 接口地址为&#xff1a;ws://sunlei.demo 接口说明&#xff1a;websocket接口&#xff0c;首次连接&#xff0c;通过Text请求设置开启标志&#xff0c;然后通过wav文件流传输&#xff0c;达到后端服务可以根据传输信息进行解析满足指定标准后&#xff0c;web…

锂电池算法学习集合---基于matlab/simulink的电池参数辨识、充放电、SOC估计算法。

整理了锂电池的多种算法合集&#xff1a;涵盖电动汽车Simulink模型、电动汽车动力电池SOC估算模型、动力电池及电池管理系统BMS。 电动汽车动力电池SOC估算模型含有:电池参数辨识模型、电池的充放电数据、电池手册、卡尔曼滤波电池SOC文献、卡尔曼滤波算法的锂电池SOC估算模型…

C++ | Leetcode C++题解之第16题最接近的三数之和

题目&#xff1a; 题解&#xff1a; class Solution { public:int threeSumClosest(vector<int>& nums, int target) {sort(nums.begin(), nums.end());int n nums.size();int best 1e7;// 根据差值的绝对值来更新答案auto update [&](int cur) {if (abs(cur…

Word wrap在计算机代表的含义(自动换行)

“Word wrap”是一个计算机术语&#xff0c;用于描述文本处理器在内容超过容器边界时自动将超出部分转移到下一行的功能。在多种编程语言和文本编辑工具中&#xff0c;都有实现这一功能的函数或选项。 在编程中&#xff0c;例如某些编程语言中的wordwrap函数&#xff0c;能够按…

甘特图/横道图制作技巧 - 任务组

在甘特图中通过合理的任务分组可以让项目更加清晰&#xff0c;修改也更方便。 列如下面的甘特图一眼不太容易看清楚整体的进度。或者需要把所有的任务整体的延迟或者提前只能这样一个一个的任务调整&#xff0c;就比较麻烦。 通过给任务分组&#xff0c;看这上面整体的进度就…

【运输层】传输控制协议 TCP

目录 1、传输控制协议 TCP 概述 &#xff08;1&#xff09;TCP 的特点 &#xff08;2&#xff09;TCP 连接中的套接字概念 2、可靠传输的工作原理 &#xff08;1&#xff09;停止等待协议 &#xff08;2&#xff09;连续ARQ协议 3、TCP 报文段的首部格式 &#xff08;1…