布谷过滤器的基础知识 可以参考 布谷过滤器 这篇文章的内容学习 简单的讲
布谷过滤器(Cuckoo Filter)是一种空间效率高且支持快速插入和查找的概率数据结构,主要用于集合的成员检测。它是基于 Cuckoo Hashing(布谷哈希)的原理,具有以下几个关键特性:
1. 基本概念
- 成员检测:布谷过滤器用于判断一个元素是否在一个集合中。它可以返回“可能在集合中”或“肯定不在集合中”的结果。
- 空间效率:相较于传统的哈希表,布谷过滤器在存储相同数量的元素时占用更少的内存。
2. 数据结构
- 桶(Buckets):布谷过滤器将元素存储在多个桶中,每个桶可以存储多个元素。
- 哈希函数:使用两个或多个哈希函数来计算元素的存储位置,确保元素可以在多个位置存储。
3. 插入操作
- 当插入一个元素时,布谷过滤器首先计算该元素的两个哈希值,确定两个可能的桶。
- 如果其中一个桶未满,则将元素插入该桶。
- 如果两个桶都已满,则会“踢出”一个现有元素,并尝试将其插入到另一个桶中。这一过程可能会递归进行,直到成功插入或达到最大重试次数。
4. 查找操作
- 查找一个元素时,布谷过滤器计算该元素的哈希值,并检查对应的桶。
- 如果在任何一个桶中找到该元素,则返回“可能在集合中”;如果在两个桶中都未找到,则返回“肯定不在集合中”。
5. 优点
- 高效性:插入和查找操作的平均时间复杂度为O(1)。
- 动态扩展:可以在负载因子达到一定阈值时动态扩展,增加桶的数量。
6. 缺点
- 假阳性:布谷过滤器可能会错误地报告某个元素在集合中(假阳性),但不会错误地报告某个元素不在集合中(假阴性)。
- 内存使用:虽然比传统哈希表更节省空间,但在高负载情况下,内存使用仍然可能较高
综上所述 我们可以发现 布谷过滤器是一个内存密集型的数据结构 在实际应用中 布谷过滤器常常使用在 路由 数据库索引 缓存中 这些涉及到高并发请求、数据处理和实时响应的场景都可以通过协程进行优化。使用协程可以提高系统的并发处理能力,减少响应时间,从而提升整体性能和用户体验。
因此 我们也许可以考虑 使用并发编程的技巧 实现对他的优化
在下使用的是go语言的goroutines 尝试优化布谷过滤器 因为本人学艺不精 所写代码必然漏洞百出 但也希望能抛砖引玉 因为go语言属于比较小众的语言 故我会一句一句分析这些代码
package main
import (
"crypto/sha256"
"fmt"
"sync"
)
首先 是导入使用的包 sha256用以计算哈希值 fmt用以格式化输出 sync是核心 用于控制并发编程
实现互斥锁和等待组。这些工具可以帮助管理多个 goroutine 的并发执行,确保数据的一致性和安全性
const (
bucketsize = 20
start = 4
load = 0.75
)
定义常量 bucketsize 每个桶存储的最大元素数量 start 初始化时桶的数量 load 承载系数 当元素的数量达到桶负载量的75%时 触发动态扩展
type cuckoofilter struct {
buckets [][]string
mu sync.RWMutex
count int
}
建立cuckoofilter结构体 包括 buckets 一个二维切片,表示存储元素的桶 mu 读写互斥锁,用于保护对过滤器的并发访问 count 用以统计桶内元素数量
func main() {
cf := Newone()
var wg sync.WaitGroup
values := []string{
.....................一大堆元素
}
for _, value := range values {
wg.Add(1)
go func(val string) {
defer wg.Done()
if cf.firstinsert(val) {
fmt.Printf("插入: %s\n", val)
} else {
fmt.Printf("插入失败: %s\n", val)
}
}(value)
}
wg.Wait()
for _, value := range values {
wg.Add(1)
go func(val string) {
defer wg.Done()
if cf.Lookup(val) {
fmt.Printf("找到: %s\n", val)
} else {
fmt.Printf("查找失败: %s\n", val)
}
}(value)
}
wg.Wait()
for _, value := range values {
wg.Add(1)
go func(val string) {
defer wg.Done()
if cf.remove(val) {
fmt.Printf("删除: %s\n", val)
} else {
fmt.Printf("删除失败: %s\n", val)
}
}(value)
}
wg.Wait()
for _, value := range values {
wg.Add(1)
go func(val string) {
defer wg.Done()
if cf.Lookup(val) {
fmt.Printf("在删除后仍能找到: %s\n", val)
} else {
fmt.Printf("在删除后当然找不到: %s\n", val)
}
}(value)
}
wg.Wait()
}
主函数 我们先看他 然后再看别的 我们也是在此处实现goroutines优化 首先创建一个 `sync.WaitGroup` 用于等待所有 goroutine 完成 保证安全性 而后 我们对values中的各个元素进行遍历 每个元素开一个协程 defer wg.Done():在 goroutine 完成时调用,减少等待组的计数 wg.wait 会阻塞主 goroutine,直到所有的插入操作完成(即所有的 goroutine 都调用了 wg.Done())。 查找和插入 删除 删除后查找 的结构是一样的所以在下就不赘述了
接下来 是具体的 实现布谷过滤器的代码 (前方大量下饭代码预警)
func Newone() *cuckoofilter {
buckets := make([][]string, start)
for i := range buckets {
buckets[i] = make([]string, 0, bucketsize)
}
return &cuckoofilter{buckets: buckets}
}
创建并返回一个新的 Cuckoo Filter 实例。初始化 buckets,为每个桶分配一个空的字符串切片,容量为 bucketsize。
func (cf *cuckoofilter) firstinsert(value string) bool {
cf.mu.Lock()
defer cf.mu.Unlock()
if float64(cf.count)/float64(len(cf.buckets)*bucketsize) > load {
fmt.Printf("扩大\n")
cf.expand()
}
return cf.trueinsert(value, 0)
}
func (cf *cuckoofilter) trueinsert(value string, count int) bool {
if count >= 10 { // 防止无限循环
return false
}
index1 := cf.hash1(value)
index2 := cf.hash2(value)
if len(cf.buckets[index1]) < bucketsize {
cf.buckets[index1] = append(cf.buckets[index1], value)
cf.count++
fmt.Printf("%s插入到了%d\n", value, index1)
return true
}
if len(cf.buckets[index2]) < bucketsize {
cf.buckets[index2] = append(cf.buckets[index2], value)
cf.count++
fmt.Printf("%s插入到了%d\n", value, index2)
return true
}
evicted := cf.buckets[index1][0]
cf.buckets[index1][0] = value
fmt.Printf("将%s从%d移动\n", evicted, index1)
if cf.insert(evicted, count+1) {
return true
}
return false
}
这一大段为插入函数 各位应该会疑惑 为什么插入函数要分成两个来写 因为我在主函数中使用了协程的技巧 而插入操作不仅仅是主函数 我们在后面动态扩展的时候 需要将old 内的数据复制到new 里面 而复制的这一步 没有使用协程 如果仍然直接使用firstinsert 会死锁 所以分成两个来写 我们先使用互斥锁 mu 来确保线程安全,防止并发写入 如果当前元素数量与桶的容量比超过75%,则动态扩展桶的容量 而后调用trueinsert 进行真正的插入 设定10次为上限 防止无限循环 (其实如果发现达到十次上限 就需要进行扩容了 但是后文会讲为什么做不到(因为我水平太低了))
使用 hash1 和 hash2 函数计算两个桶的索引。
func (cf *cuckoofilter) hash1(value string) int {
h := sha256.Sum256([]byte(value))
return int(h[0]) % len(cf.buckets)
}
func (cf *cuckoofilter) hash2(value string) int {
h := sha256.Sum256([]byte(value))
return (int(h[0]) + int(h[1])) % len(cf.buckets)
}
判断主桶和备用桶是否已满 如果主桶满了 则将元素插入第二个桶。如果两个桶都已满,则踢出第一个桶中的第一个元素,并将新元素插入。递归插入被踢出的元素:尝试将被踢出的元素插入到 Cuckoo Filter 中。
if len(cf.buckets[index1]) < bucketsize {
cf.buckets[index1] = append(cf.buckets[index1], value)
cf.count++
fmt.Printf("%s插入到了%d\n", value, index1)
return true
}
if len(cf.buckets[index2]) < bucketsize {
cf.buckets[index2] = append(cf.buckets[index2], value)
cf.count++
fmt.Printf("%s插入到了%d\n", value, index2)
return true
}
evicted := cf.buckets[index1][0]
cf.buckets[index1][0] = value
fmt.Printf("将%s从%d移动\n", evicted, index1)
接下来 是 查找操作
func (cf *cuckoofilter) Lookup(value string) bool {
cf.mu.RLock()
defer cf.mu.RUnlock()
index1 := cf.hash1(value)
index2 := cf.hash2(value)
for _, v := range cf.buckets[index1] {
if v == value {
fmt.Printf("在桶%d找到%s\n", value, index1)
return true
}
}
for _, v := range cf.buckets[index2] {
if v == value {
fmt.Printf("在桶%d找到%s\n", value, index2)
return true
}
}
fmt.Printf("哪里都找不到%s\n", value)
return false
}
布谷鸟过滤器的查找过程很简单,给定一个项x,算法首先根据上述插入公式,计算x的指纹和两个候选桶。然后读取这两个桶:如果两个桶中的任何现有指纹匹配,则布谷鸟过滤器返回true,否则过滤器返回false。此时,只要不发生桶溢出,就可以确保没有假阴性
动态扩展(在下在测试的时候 发现 一旦调用这个函数 对过滤器动态扩展 准确度就会下载到可怕的程度 但是因为我学艺不精 不太清楚是哪里出了差错(感觉逻辑应该是对的) 所以写出来 权当抛砖引玉 求大佬指导)(目前的解决方法是先大概确定要插入的元素总数 而后直接提取设定好bucketsizes的大小)
func (cf *cuckoofilter) expand() {
old := cf.buckets
news := make([][]string, len(old)*2)
for i := range news {
news[i] = make([]string, 0, bucketsize)
}
// 重新哈希现有元素
for _, bucket := range old {
for _, value := range bucket {
cf.trueinsert(value, 0) // 重新插入到新桶中
}
}
cf.buckets = news // 更新桶为新桶
}
将旧的桶存储在 old 中,创建一个新的桶切片 news,其长度是旧桶的两倍。
重新哈希:遍历旧桶中的所有元素,通过trueinsert(如果使用firstinsert 会出现协程死锁的错误) 将它们重新插入到新的桶中。
更新桶:将 cf.buckets 更新为新的桶。
插入 查找 扩展 这样几个基础功能就完成了(存疑) 但是既然是cuckoofilter了 我们还得把删除功能也实现了
func (cf *cuckoofilter) remove(value string) bool {
cf.mu.Lock()
defer cf.mu.Unlock()
index1 := cf.hash1(value)
index2 := cf.hash2(value)
for i, v := range cf.buckets[index1] {
if v == value {
cf.buckets[index1] = append(cf.buckets[index1][:i], cf.buckets[index1][i+1:]...)
cf.count--
fmt.Printf("将%s从%d删除\n", value, index1)
return true
}
}
for i, v := range cf.buckets[index2] {
if v == value {
cf.buckets[index2] = append(cf.buckets[index2][:i], cf.buckets[index2][i+1:]...)
cf.count--
fmt.Printf("将%s从%d删除\n", value, index2)
return true
}
}
fmt.Printf("我没找到%s删个毛\n", value)
return false
}
删除的过程也很简单,检查给定项的两个候选桶;如果任何桶中的指纹匹配,则从该桶中删除匹配指纹的一份副本
好了 这样大概就写完了 在下是个小小编程萌新 这也是第一篇博客 所写代码必然充满下饭操作 求大佬多多指教 (求大佬骂轻点 ,我是下水道鼠鼠 ,骂我的话我就哭给你看(ಥ﹏ಥ) )