Bloom Filter em Zig — Implementação Completa
Um Bloom Filter (filtro de Bloom) é uma estrutura de dados probabilística que testa se um elemento pertence a um conjunto. Ele pode retornar falsos positivos (dizer que algo pertence quando não pertence), mas nunca falsos negativos (se diz que não pertence, é certeza). É extremamente eficiente em espaço — usa apenas um array de bits, independente do tamanho dos elementos.
Conceito
Bloom Filter com 16 bits e 3 funções hash:
Inserindo "maçã":
h1("maçã") % 16 = 2 → bit[2] = 1
h2("maçã") % 16 = 7 → bit[7] = 1
h3("maçã") % 16 = 11 → bit[11] = 1
Inserindo "banana":
h1("banana") % 16 = 1 → bit[1] = 1
h2("banana") % 16 = 7 → bit[7] = 1 (já era 1)
h3("banana") % 16 = 14 → bit[14] = 1
Array de bits: 0 1 1 0 0 0 0 1 0 0 0 1 0 0 1 0
^b^m ^ambos ^m ^b
Consulta "uva":
h1("uva") % 16 = 2 → bit[2] = 1 ✓
h2("uva") % 16 = 5 → bit[5] = 0 ✗ → NÃO pertence (certeza!)
Consulta "kiwi":
h1("kiwi") % 16 = 1 → bit[1] = 1 ✓
h2("kiwi") % 16 = 7 → bit[7] = 1 ✓
h3("kiwi") % 16 = 11 → bit[11] = 1 ✓
→ POSSIVELMENTE pertence (pode ser falso positivo!)
Implementação em Zig
const std = @import("std");
const Allocator = std.mem.Allocator;
/// Bloom Filter com k funções hash baseadas em double hashing.
pub fn BloomFilter(comptime tamanho_bits: usize) type {
const num_palavras = (tamanho_bits + 63) / 64;
return struct {
const Self = @This();
bits: [num_palavras]u64,
num_hashes: u8,
contagem_insercoes: usize,
pub fn init(num_hashes: u8) Self {
return .{
.bits = [_]u64{0} ** num_palavras,
.num_hashes = num_hashes,
.contagem_insercoes = 0,
};
}
fn hashPrimario(dados: []const u8) u64 {
// FNV-1a hash
var h: u64 = 14695981039346656037;
for (dados) |byte| {
h ^= byte;
h *%= 1099511628211;
}
return h;
}
fn hashSecundario(dados: []const u8) u64 {
// djb2 hash
var h: u64 = 5381;
for (dados) |byte| {
h = ((h << 5) +% h) +% byte;
}
return h;
}
fn obterBit(self: *const Self, pos: usize) bool {
const palavra = pos / 64;
const bit: u6 = @intCast(pos % 64);
return (self.bits[palavra] >> bit) & 1 == 1;
}
fn definirBit(self: *Self, pos: usize) void {
const palavra = pos / 64;
const bit: u6 = @intCast(pos % 64);
self.bits[palavra] |= @as(u64, 1) << bit;
}
/// Insere um elemento no filtro.
pub fn inserir(self: *Self, dados: []const u8) void {
const h1 = hashPrimario(dados);
const h2 = hashSecundario(dados);
for (0..self.num_hashes) |i| {
const pos = (h1 +% @as(u64, @intCast(i)) *% h2) % tamanho_bits;
self.definirBit(@intCast(pos));
}
self.contagem_insercoes += 1;
}
/// Verifica se um elemento POSSIVELMENTE pertence ao conjunto.
/// Retorna false → certeza que NÃO pertence.
/// Retorna true → POSSIVELMENTE pertence (pode ser falso positivo).
pub fn possivelmenteContem(self: *const Self, dados: []const u8) bool {
const h1 = hashPrimario(dados);
const h2 = hashSecundario(dados);
for (0..self.num_hashes) |i| {
const pos = (h1 +% @as(u64, @intCast(i)) *% h2) % tamanho_bits;
if (!self.obterBit(@intCast(pos))) return false;
}
return true;
}
/// Estimativa da taxa de falsos positivos atual.
pub fn taxaFalsosPositivos(self: *const Self) f64 {
const n: f64 = @floatFromInt(self.contagem_insercoes);
const m: f64 = @floatFromInt(tamanho_bits);
const k: f64 = @floatFromInt(self.num_hashes);
// p ≈ (1 - e^(-kn/m))^k
const expoente = -k * n / m;
return std.math.pow(f64, 1.0 - @exp(expoente), k);
}
/// Conta bits ativos.
pub fn bitsAtivos(self: *const Self) usize {
var total: usize = 0;
for (self.bits) |palavra| {
total += @popCount(palavra);
}
return total;
}
/// Reseta o filtro.
pub fn limpar(self: *Self) void {
@memset(&self.bits, 0);
self.contagem_insercoes = 0;
}
};
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// Bloom Filter com 1024 bits e 5 funções hash
var filtro = BloomFilter(1024).init(5);
// Insere URLs visitadas
const urls_visitadas = [_][]const u8{
"https://ziglang.org",
"https://ziglang.com.br",
"https://github.com/ziglang/zig",
"https://ziglearn.org",
"https://zig.news",
};
try stdout.writeAll("=== Inserindo URLs ===\n");
for (urls_visitadas) |url| {
filtro.inserir(url);
try stdout.print(" Inserido: {s}\n", .{url});
}
// Consultas
try stdout.writeAll("\n=== Consultas ===\n");
const urls_teste = [_][]const u8{
"https://ziglang.org", // Deve retornar true
"https://rust-lang.org", // Deve retornar false
"https://ziglang.com.br", // Deve retornar true
"https://golang.org", // Deve retornar false
"https://example.com", // Provavelmente false
};
for (urls_teste) |url| {
const resultado = filtro.possivelmenteContem(url);
const status: []const u8 = if (resultado) "possivelmente sim" else "definitivamente não";
try stdout.print(" {s:<40} → {s}\n", .{ url, status });
}
// Estatísticas
try stdout.print("\n=== Estatísticas ===\n", .{});
try stdout.print(" Inserções: {d}\n", .{filtro.contagem_insercoes});
try stdout.print(" Bits ativos: {d}/1024\n", .{filtro.bitsAtivos()});
try stdout.print(" Taxa estimada de falsos positivos: {d:.6}\n", .{filtro.taxaFalsosPositivos()});
}
Análise de Complexidade
| Operação | Tempo | Espaço |
|---|---|---|
| Inserir | O(k) | O(1) |
| Consultar | O(k) | O(1) |
| Espaço total | — | O(m) bits |
Onde k = número de funções hash, m = tamanho do array de bits.
Fórmula para Dimensionamento
Para n elementos e taxa de falsos positivos p:
- Tamanho ideal:
m = -n * ln(p) / (ln(2))^2 - Funções hash ideais:
k = (m/n) * ln(2)
Casos de Uso
- Cache: verificar rapidamente se URL/chave já foi vista
- Banco de dados: evitar leitura em disco para chaves inexistentes
- Rede: filtrar pacotes spam ou duplicados
- Navegadores: verificar URLs maliciosas (Safe Browsing do Chrome)
Recursos Relacionados
- Hash Set — Conjunto exato (sem falsos positivos)
- Hash Table — Tabela hash chave-valor
- BitSet — Conjunto de bits
- Perguntas de Entrevista: Algoritmos