算法在某个集合中找个一个或者多个的和等于某个固定值_数据库+算法=?

在开始文章之前,分享一个有趣的小故事:

1927年第五届索维尔会议上,爱因斯坦与波尔关于量子力学的争论达到了白热化。爱因斯坦严肃的说,“波尔,上帝不会投骰子!”。而波尔则回应说,“爱因斯坦,别去指挥上帝应该怎么做!”。爱因斯坦坚决不相信物理学最本质的规律是统计性的。

我们今天聊的也是关于统计的算法,看一看抛硬币的故事

一、提出问题

现在我提出这样一个问题:假设一个网站每日有数以亿计的IP访问,如何高效统计ip访问的规模?

这个问题的规模很大,ip访问记录数以亿计的规模,看上去是很吓人的,但其实我们并不关心具体哪些ip访问,也不是特别在意具体的精确数字,或许这只是个模糊的需求,提问题的同学只想要知道大概的规模,但是并不关心具体的精确数字。

二、解决问题

这个问题可以怎么来解决?我们可以非常容易的想到以下这些方法:

1. 字典或者哈希

将ip放到字典中,我们可以很容易的去重统计ip数。这个方法问题在于,当ip数量非常多时,非常消耗内存,我们假设一个IP占用4B,那么10亿个IP需要的内存为40GB,这个容量太大了!

如果一个应用中的一个统计功能需要40GB,显然我们是不能接受的。同时插入的复杂度也不低,并且两个计数量要合并的话,无论是树结构还是哈希结构效率都是不高的。

2. bitmap

内存太高,我们会想到用一个比特位来表示一个ip,ip本来就是个32位的整数,我们把这个整数映射到对应的一个比特位,这就成了一个bitmap。使用bitmap占用的空间约为上面方法的1/32,这也需要1.25GB的容量。减少了非常多,bitmap对于多个计数量的合并要简单的多。

但还是感觉不太满意。

三、更好的方法?

那么,有没有更好的方法呢?答案当然是有的。统计学里面有一类专门处理这个问题的方法,叫做基数统计。

基数统计(cardinality counting)指的是统计一批数据中的不重复元素的个数,常见于计算独立用户数(UV)、维度的独立取值数等等。实现基数统计最直接的方法,就是采用集合(Set)这种数据结构,当一个元素从未出现过时,便在集合中增加一个元素;如果出现过,那么集合仍保持不变。

cee3c1f70079e21f2a7324ea60817790.png

我们通过一个均匀的hash函数将元素hash到一个bit数组的一位,将其置为1。看起来和bitmap很像是不是?这里不一样的点是bitmap每个元素都通过一个唯一的bit位相关联,而这里则通过的hash函数。hash函数是必然可能存在冲突的,不会保证一一对应。设元素个数为n,bit数组的长度为m,n个元素hash后还有s个bit位为0。

我们可以通过下面这个公式来估计n, 这个方法称为LC(见参考文献1)。

d4c0d7690e3a8ef91533c8ce0ba63905.png

推导过程如下:由hash函数定义可知:n个元素的hash结果服从独立均匀分布,设Xi 为n个元素映射后第i位任为0。

52303b7a6c3af3e5b6bff85f13e33783.png

因此,可用上述公式估计元素规模。LC为了控制误差率,要求m为n约十分之一。相比bitmap只做了空间的常数级优化。

四、更上一层楼?

这样就结束了吗,能不能再精致点?我们先从一个游戏开始,我们叫它伯努利实验——没错这个名字就是借用统计学的伯努利硬币实验设计的游戏。

95e637a7832bd8385f51df3173de5014.gif

假设A和B两个人进行抛硬币的游戏,A来抛硬币,B来猜,规则如下:

1.A每轮抛硬币直到出现一次正面为止,记为一次伯努利实验,并记下抛的次数,记为伯努利值K;

2.A进行n轮伯努利实验,并记下n次伯努利值的最大值M;

3.A将M告诉B,B猜A大概进行了多少次伯努利实验即n的大概大小。

如果你是B,如何猜测n的大小呢?答案是:N约等于2的M次方。

为什么会是这样一个结果呢?我们来简单分析一下:

回忆伯努利实验的规则,我们可以得出以下两个结论:

1. n次伯努利过程,每轮投掷次数都不大于M;

2. n次伯努利过程,至少有一轮投掷次数等于M;

ae4c9558bce8603ac76b1db27c8fa590.png

显然,我们可以用上述公式来估计n的规模,详细数学论证及误差率控制参考文末论文LLC(见参考文献2)和HLLC(见参考文献3)。那么如何实现LLC和HLLC呢?

664cf6a350fa6c49bd0ae55f5b5606a0.png

首先我们将元素a通过hash映射为长度为L的bit串,从左向右扫描第一个不为0的比特位的过程,可以理解为统计意义上的伯努利过程,设M为n个元素第一个不为0的比特位最大的位置。则可根据N约等于2的M次方,通过M可估计n的大小规模。

为减少偶然性因素对算法的影响,如某个元素不为0的比特位非常靠后。可以将比特位分组。

2f571959d2f0f4e80dbd46595c48cb5c.png

证明过程来啦:假设哈希长度为16bit,分桶数m定为32。设一个元素哈希值为“00010 01010001010”,前5个bit为桶编号,所以元素放入“00010”即2号桶,而剩下部分第一个非0比特位为2。这里设元素的hash值为“00010 01010001010”,如果不分桶的情况下,我们用从左向右扫描的方式来模拟伯努利过程,找到第一个非0的位置为4,这个也就是这轮的情况的情况,我们可以用一个变量记录这个最大值,假设n个元素映射后最大的任然为4,那我们可以猜测这个元素集合个数为16;但这里存在一个问题,假设某个元素映射值极为偶然,导致前面位数都为0,1的位置非常靠后(当然我们可以按照均匀分布计算这种情况的概率极低),这个时候会导致计算的n值非常大,也就是存在较低的概率使计算结果偏差非常大。


但是如果我们把hash值的前面s位用来分桶,也就是我们用多个变量来记录最大值M,这种情况下,多个桶同时出现偏差值的概率可以忽略不计,多个平均值将异常值平衡掉。

4834625fad3e5f0974b3fef5c1f2fc39.png

LLC和HLLC的不同点,就在于如何统计M值,LLC采用算术平均值;HLLC采用调和平均数。两者差别在于算术平均数更容易受离群值的影响,导致容易受偶然因素干扰。

根据估算公式,可以很容易的得到LLC和HLLC的算法空间复杂度为O(log(log(n))。

最后就是见证奇迹的时候,假设有一个2的64次方的元素个数,采用LLC或者HLLC只需要6个比特位就可以估算元素规模。

回忆下我们前面提到的字典或者bitmap方法,按照这样的方法,我们就需要2^64/2^10/2^10/2^10=2^36GB,需要EB级的存储,数学的魅力在这里就体现的淋漓尽致!而Redis的HyperLogLog正是采用的HLLC算法来实现集合元素统计的。

参考文献:

【1】A linear-time probabilistic counting algorithm for database applications

【2】Loglog Counting of Large Cardinalities

【3】HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm

85765f2d9b43e31737b9a8feb4807a6b.png

↓↓更多惊喜优惠请点这儿~

云数据库 超值采购专场​cloud.tencent.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据库系统概念:存储和⽂件结构 ⽂章⽬录 1、基本概念 1、内存数据库与磁盘数据库的特征⽐较 内存数据库 内存数据库 磁盘数据库 磁盘数据库 存取时间 s 量级 s 量级 数据存储 不需要连续存储 连续存储 缓冲管理 不需要 需要 索引结构 哈希,AVL树,T树,B树 B树,B+树,Hash 并发控制 ⼤粒度锁 细粒度锁加锁,解锁,死锁检测 查询优化 基于处理器代价以及 cache 代价 基于 I/O 代价 2、LRU: Least Recently Used,该算法的设计原则是,如果⼀个数据在最近⼀段时间没有被访问到,那么在将来它被访问的可能性也很 ⼩,也就是说,当限定的空间已经存满数据时,应当把最久没有被访问到的数据淘汰。 2、⽂件组织 ⼀个数据库被映射到多个不同的⽂件,这些⽂件由底层的操作系统来维护。⽂件在逻辑上是数据记录的集合,⽂件的结构取决于所属的⽂件 系统。 块(block) 是数据在⽂件存储和读取的基本单元,其⼤⼩固定,⼤多数数据库默认使⽤ 4~8KB 的块⼤⼩,在创建数据库实例时,有些 数据库允许指定块的⼤⼩,要根据数据的特征来决定多个的块更合理。 ⼀个块可能包含多条记录,对于⼤数据,⽐如图⽚,视频等,⽐块要⼤得多,需要单独存储,并在记录保存指向⼤数据的指针。 每条记录都存储在相同的块,不能⼀部分在 A 块,另⼀部分在 B 块,这样的设计能简化并加速数据访问。 关系型数据库,不同表的记录通常具有不同的⼤⼩,这时数据库可以选择映射到多个⽂件,同⼀⽂件只存储固定长度的记录;也可以 构建特殊的⽂件,⽀持容纳不定长度的记录。很显然,后者实现起来更复杂。 2.1 定长记录 定长记录占⽤固定⼤⼩的存储空间,管理上⽐较简单,有两点需要注意: 单条记录不能跨 块 存储,⼀个块上只能存储整数个记录; ⽂件的记录是连续存储的,在删除记录后,需要对剩余的记录进⾏整理,不然会出现⼤量零碎的空闲空间。 记录整理⼤体有两个⽅向: 1. 移动填充: 当删除记录时,移动剩余的记录,使存储保持连续;可以把后续的记录往前移,使⽂件存储的记录依然保持连续;也可 以把最后⼀个移过来,填充删除的记录所在的位置。 2. 空闲列表: 在⽂件头维护⼀个空闲列表,在删除记录时,将其所在的位置添加到空闲列表,在下⼀个记录插⼊时再填充。 10 8 10 3 对定长记录⽂件的插⼊和删除是容易实现的,因为被删除记录留下的可⽤空间恰好是插⼊记录所需的空间。 2.2 变长记录 数据库⽂件使⽤变长记录的场景: 1. 多种记录类型在⼀个⽂件存储; 2. 允许⼀个或多个字段是变长的记录类型; 3. 允许可重复字段的记录类型,例如数组或多重集合; 实现变长记录需要解决两个问题: 1. 如何存取块的记录:在块如何存储变长记录,使得块的记录可以轻松地抽取; 2. 如何存取记录的属性:如何描述⼀条记录,使得单个属性可以轻松地抽取; . 1、下⾯是⼀条变长记录,它包含了四个部分; 空值位图⽤来表⽰记录的按个属性是空值, 0000 表⽰四个属性都不为空, 0010 表⽰第三个属性的值为空。这⾥需要注意,空值位图之 前的部分为记录的 定长部分 ,⽆论哪个属性为空,这个结构的⼤⼩从始⾄终都是不变的,当某个属性为空值时,只表明可以忽略其所对应 的 定长部分 对应的值。 也有⼀些设计,空值位图存储在记录的开头,并且对空属性不存储数据(值或偏移量/长度),这种⽅式是典型的 时间换空间 设计,对于 稀疏型记录⽐较有效。 2、分槽的页结构(slotted-page structure)⼀般⽤于在块组织记录: 分槽页结构 是 变长记录 的存储管理技术的⼀种,⽤于在块组织记录。 每个块的开始处有⼀个块头,块头包含的信息有: 块头已经存储的记录的条⽬个数; 块空闲空间的末尾地址; 条⽬数组,每个条⽬存储了该条⽬所对应变长记录的⼤⼩和地址; 记录在块是从后往前连续存储的, 空闲空间 处于 块头 和 记录 之间,当插⼊新的记录时,记录的 (⼤⼩,位置) 对应的 entry 添加 到块头的末尾,记录本⾝的值添加空闲空间的末尾。 2.3 ⽂件记录的组织 ⽂件记录组织的⼏种⽅式: 堆⽂件组织(heap file organization):⼀条记录可以放在⽂件的任何地⽅,只要那个地⽅有空间存放这条记录,记录是没有顺序 的,通常每个关系使⽤⼀个单独的⽂件。 顺序⽂件组织(sequential file organization):记录根据其 search key 的值顺序存储。 散列⽂件组织(hashing file organization): 在每条记录的某些属性上计算⼀个散列函数,散列的结果确定了记录应放到⽂件的哪 个块。 通常,每个关系的记录⽤⼀个单独

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值