实现布谷鸟过滤器,每当有一个小说被存储后将其加入布谷鸟过滤器,并能够使用布谷鸟过滤器查询上述小说是否已经被存储
一、解题思路
在介绍布谷鸟过滤器之前,首先需要了解布谷鸟哈希的结构。最简单的布谷鸟哈希结构是一维数组结构,会有两个hash算法将新来的元素映射到数组的两个位置,如果两个位置中有一个位置为空,那么就可以将元素直接放进去;但是如果这两个位置都满了,它就不得不随机踢走一个,然后自己霸占这个位置。被踢走的元素首先会看看自己的另一个位置有没有空,如果空了,自己就挪过去;但是如果这个位置也被别的元素占据,它就会再将受害者的角色转嫁给的元素,然后这个新的受害者还会重复这个过程直到所有的元素都找到了自己的位置为止。当然这种循环不可能一直无限重复下去,所以要设置一个最大踢出次数MAX_NUM_KICKS,如果超过这个次数仍没有找到插入位置,就认为过滤器已满,插入失败。
布谷鸟过滤器和布谷鸟哈希结构一样,它也是一维数组,但是不同于布谷鸟哈希的是,布谷鸟哈希会存储整个元素,而布谷鸟过滤器中只会存储元素的指纹信息(几个bit,类似于布隆过滤器)。这里过滤器牺牲了数据的精确性换取了空间效率。正是因为存储的是元素的指纹信息,所以会存在误判率,这点和布隆过滤器如出一辙。首先布谷鸟过滤器还是只会选用两个 hash 函数,但是每个位置可以放置多个座位。这两个 hash 函数选择的比较特殊,因为过滤器中只能存储指纹信息。当这个位置上的指纹被挤兑之后,它需要计算出另一个对偶位置,而计算这个对偶位置是需要元素本身的。布谷鸟过滤器巧妙地设计了一个独特的hash 函数,使得可以根据p1和元素指纹直接计算出p2,而不需要完整的x元素。
byte f = fingerprint(o);
int i1 = hash(o);
int i2 = i1 ^ hash(f);
当我们知道f和i1,就可以直接算出i2,同样地如果我们知道i2和f,也可以直接算出i1 (对偶性),所以我们根本不需要知道当前的位置是i1还是i2,只需要将当前的位置和hash(o) 进行异或计算就可以得到对偶位置,而且只需要确保 hash(o) != 0 就可以确保 p1 != p2,如此就不会出现自己踢自己导致死循环的问题。因为采用了这种候选桶与首选桶可以通过位置和存储值互相异或得出的备用候选桶的方案,所以这种对应关系要求桶的大小必须是2的幂次方。
下面对cuckooFilter类的主要方法进行介绍:
insert方法用于向布谷鸟过滤器中插入一个元素o,如果o为空,则插入失败;否则如果桶中有空位置就直接插入,否则调用relocateAndInsert方法。
public boolean insert(Object o) {
if (o == null)
return false;
byte f = fingerprint(o);
int i1 = hash(o);
int i2 = i1 ^ hash(f);
if (buckets[i1].insert(f) || buckets[i2].insert(f)) {
//有空位置
size++;
return true;//插入成功
}
//没有空位置,relocate再插入
return relocateAndInsert(i1, i2, f);
}
relocateAndInsert方法随机在两个位置挑选一个将其中的一个值标记为旧值,用新值覆盖旧值,旧值会在重复上面的步骤进行插入。Flag是一个随机的boolean类型变量,如果为true就把i1踢出,如果为false就把i2踢出,并把被提出的元素赋值给itemp;之后进行最多为最大踢出次数的循环,在桶中随机找一个位置position,将新的指纹放入position,插入元素个数size自增,同时这一轮的受害者进入下一轮寻找新的受害者踢出。
private boolean relocateAndInsert(int i1, int i2, byte f) {
boolean flag = random.nextBoolean();
int itemp = flag ? i1 : i2;
for (int i = 0; i < MAX_NUM_KICKS; i++) {
//在桶中随机找一个位置
int position = random.nextInt(Bucket.BUCKET_SIZE);
//踢出
f = buckets[itemp].swap(position, f);
itemp = itemp ^ hash(f);
if (buckets[itemp].insert(f)) {
size++;
return true;
}
}//超过最大踢出次数,插入失败
return false;
}
完整代码:
import java.util.Random;
public class cuckooFilter {
static final int MAXIMUM_CAPACITY = 1 << 30;
//最大的踢出次数
private final int MAX_NUM_KICKS = 500;
//桶的个数
private int capacity;
//存入元素个数
private int size = 0;
//存放桶的数组
private Bucket[] buckets;
private Random random;
//构造函数
public cuckooFilter(int capacity) {
capacity = tableSizeFor(capacity);
this.capacity = capacity;
buckets = new Bucket[capacity];
random = new Random();
for (int i = 0; i < capacity; i++) {
buckets[i] = new Bucket();
}
}
/*
* 向布谷鸟过滤器中插入一个元素
*
* 插入成功,返回true
* 过滤器已满或插入数据为空,返回false
*/
public boolean insert(Object o) {
if (o == null)
return false;
/*
* 当我们知道 f 和 i1,就可以直接算出 i2,同样如果我们知道 i2 和 f,也可以直接算出 i1 (对偶性)
* 所以我们根本不需要知道当前的位置是 p1 还是 p2,
* 只需要将当前的位置和 hash(o) 进行异或计算就可以得到对偶位置。
* 而且只需要确保 hash(o) != 0 就可以确保 i1 != i2,
* 如此就不会出现自己踢自己导致死循环的问题。
*/
byte f = fingerprint(o);
int i1 = hash(o);
int i2 = i1 ^ hash(f);
if (buckets[i1].insert(f) || buckets[i2].insert(f)) {
//有空位置
size++;
return true;//插入成功
}
//没有空位置,relocate再插入
return relocateAndInsert(i1, i2, f);
}
/**
* 对插入的值进行校验,只有当未插入过该值时才会插入成功
* 若过滤器中已经存在该值,会插入失败返回false
*/
public boolean insertUnique(Object o) {
if (o == null || contains(o))
return false;
return insert(o);
}
/**
* 随机在两个位置挑选一个将其中的一个值标记为旧值,
* 用新值覆盖旧值,旧值会在重复上面的步骤进行插入
*/
private boolean relocateAndInsert(int i1, int i2, byte f) {
boolean flag = random.nextBoolean();
int itemp = flag ? i1 : i2;
for (int i = 0; i < MAX_NUM_KICKS; i++) {
//在桶中随机找一个位置
int position = random.nextInt(Bucket.BUCKET_SIZE);
//踢出
f = buckets[itemp].swap(position, f);
itemp = itemp ^ hash(f);
if (buckets[itemp].insert(f)) {
size++;
return true;
}
}//超过最大踢出次数,插入失败
return false;
}
/**
* 如果此过滤器包含对象的指纹,返回true
*/
public boolean contains(Object o) {
if(o == null)
return false;
byte f = fingerprint(o);
int i1 = hash(o);
int i2 = i1 ^ hash(f);
return buckets[i1].contains(f) || buckets[i2].contains(f);
}
/**
* 从布谷鸟过滤器中删除元素
* 为了安全地删除,此元素之前必须被插入过
*/
public boolean delete(Object o) {
if(o == null)
return false;
byte f = fingerprint(o);
int i1 = hash(o);
int i2 = i1 ^ hash(f);
return buckets[i1].delete(f) || buckets[i2].delete(f);
}
/**
* 过滤器中元素个数
*/
public int size() {
return size;
}
//过滤器是否为空
public boolean isEmpty() {
return size == 0;
}
//得到指纹
private byte fingerprint(Object o) {
int h = o.hashCode();
h += ~(h << 15);
h ^= (h >> 10);
h += (h << 3);
h ^= (h >> 6);
h += ~(h << 11);
h ^= (h >> 16);
byte hash = (byte) h;
if (hash == Bucket.NULL_FINGERPRINT)
hash = 40;
return hash;
}
//哈希函数
public int hash(Object key) {
int h = key.hashCode();
h -= (h << 6);
h ^= (h >> 17);
h -= (h << 9);
h ^= (h << 4);
h -= (h << 3);
h ^= (h << 10);
h ^= (h >> 15);
return h & (capacity - 1);
}
//hashMap的源码 有一个tableSizeFor的方法,目的是将传进来的参数转变为2的n次方的数值
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
static class Bucket {
public static final int FINGERPINT_SIZE = 1;
//桶大小为4
public static final int BUCKET_SIZE = 4;
public static final byte NULL_FINGERPRINT = 0;
private final byte[] fps = new byte[BUCKET_SIZE];
//在桶中插入
public boolean insert(byte fingerprint) {
for (int i = 0; i < fps.length; i++) {
if (fps[i] == NULL_FINGERPRINT) {
fps[i] = fingerprint;
return true;
}
}
return false;
}
//在桶中删除
public boolean delete(byte fingerprint) {
for (int i = 0; i < fps.length; i++) {
if (fps[i] == fingerprint) {
fps[i] = NULL_FINGERPRINT;
return true;
}
}
return false;
}
//桶中是否含此指纹
public boolean contains(byte fingerprint) {
for (int i = 0; i < fps.length; i++) {
if (fps[i] == fingerprint)
return true;
}
return false;
}
public byte swap(int position, byte fingerprint) {
byte tmpfg = fps[position];
fps[position] = fingerprint;
return tmpfg;
}
}
public static void main(String args[]){
cuckooFilter c=new cuckooFilter(100);
c.insert("西游记");
c.insert("水浒传");
c.insert("三国演义");
System.out.println(c.contains("水浒传"));
}
}