最近在看吴军的《数学之美》对其中的一些技术和算法很感兴趣,看到布隆过滤器的时候突然很想自己去动手实现一个自己的布隆过滤器(至于什么是布隆过滤器,传送门在这。
在我看来,一个布隆过滤器的核心包括两部分:一个位向量,一组设计精巧的hash函数。今天我要实现的就是第一个核心部件,位向量。
位向量,其实就是位数组(bit array),本质就是一个由位构成的序列。如果在C++/JAVA当中,这根本算不上一个问题,因为它们已经提供bitset(位集)这样现成的工具。而在C语言中却没有这样的工具,而且C语言对内存的管理都是以字节(byte)为单位的,从它提供的最小数据类型char来看,它都是占用一个字节,我们并没有一种直接操作位(bit)的数据类型?那么用一个数组,比如char bitarr[]来模拟行不行呢,它的每一个数组元素就对应一个位?当然是可以的,都是这并不是一种好的设计方案,我们知道一个char通常是一个字节,而一个字节又等于8个位。如果我们用一个char来模拟一个位的话,意味着会有7个位被浪费,而布隆过滤器通常是应用在大数据的情形下,这样一来就很不划算了。那么有没有办法把一个char的所有位都利用起来呢?
虽然C语言没有提供面向位的数据类型,但它也提供了丰富的位运算符,这使得我们有了访问一个char中某些位的能力,我在bitarray的实现中,在底层虽然我也是把char数组作为储存位(bit)的容器,但是我不是用一个char来代表一个位,而是把一个char的8个位全部能用上,这不仅更符合“位数组”的概念也大大提高了空间效率。
在具体实现之前先稍微讲一下要用到的基础的知识:
如何访问一个整型变量中的某个特定的位?
这个问题需要用到C语言提供的几个位运算符:~(按位取反)、&(按位与)、|(按位或)、<<(左移)、>>(右移)。这些运算符的性质是基础知识就不赘述。这里还需要用到掩码技术。举个例子,
char i=1;如果要把i的第5位变为1的话,就需要构造一个第5位为1,其他位均为0的掩码,然后把这个掩码与i进行“按位或”的运算在把运算结果赋给i。这样做是因为“按位或”运算,只用当两个操作数有一个为1时,其结果就为1,这样一来就可以发现,一个第5位为1其他位为0的掩码与i进行“按位或”时,就可以保证结果的第5位会被置为1,而结果的其他位不变。可能怎么说比较抽象,下面具体演示一下:
char i = 1;
//这时i的位模式是00000001
//构建一个第5位为1,其他位为0的掩码,它的位模式应该是:00100000
//注意位是从0开始计数的
char mask=0x20;//0x20是十六进制,等于00100000
i |= mask;
//过程如下:
/**
**i: 0000 0001
** |
**mask: 0010 0000
** ——————————————————————
** 0010 0001
**/
那么如何来构造一个特定位为1其他位为0的掩码呢?
使用<<运算符就行了:1 << j,j就是要置为1的位。
这里有一个惯用法:
将第j位置位1:
i |= 1<<j;
这里要考虑一下数据类型的问题
同样的也有将第j位置为0的惯用法:
i &= ~(1<<j);
还有获取第j位状态的惯用法:
if(i & 1<<j)
{
//某位不为0的处理
}
else
{
//某位为0的处理
}
按照上面的思路应该都很好理解,这里不赘述。
有了上面基础知识的准备那么就很好实现这个数据结构了,由于我想要的是一个可以复用、易于维护的位数组(bit array),所以我把它设计成抽象数据结构,把实现和接口分离开来,并且采用C语言提供的“不完整类型”把实现的细节(主要是真正的bitarray数据结构)隐藏起来,仅提供一个指针(bit_array),这样就能最大限度的防止外部去访问和改变内部数据,避免了外部行为影响内部逻辑的风险。对位数组的一切访问都要通过我对外提供的接口。由于C语言没有提供足够多的用于“隐藏控制”(访问权限控制)和抽象的语言特性,所以相比JAVA来说,要实现这一点要难得多。下面是接口部分,存放在头文件“bitarray.h”里面:
#ifndef _BIT_ARRAY_H
#define _BTI_ARRAY_H
//定义错误代码
#define ERROR_BIT -1
#define ERROR_NULL -2
//定义位的两种状态
#define BIT_STATE_ON 1 //状态:开
#define BIT_STATE_OFF 0 //状态:关
#include <limits.h>
//位数组指针类型,bit_array是指向bitarray类型的指针
typedef struct bitarray* bit_array;
//
//下面是接口部分
/*
* 创建一个bit_array
* name: bitarray_create
* @param bit 需要创建的位数组宽度,需要注意的是bit的值不能为0且必须为A_BYTE的整数倍,否则会创建失败
* @return 创建成功返回指向bitarray对象的指针(其实就是bit_array),创建失败返回NULL
*
*/
bit_array bitarray_create(unsigned long bits);
/*
* 销毁已经创建的bit_array对象,释放为它分配的空间
* name: bitarray_destroy
* @param target 需要销毁的对象
* @return 成功返回1,失败返回ERROR_NULL(当target为NULL时)
*
*/
int bitarray_destroy(bit_array target);
/*
* 将目标对象中的某位设置为BIT_STATE_ON,即1
* name: bitarray_set_bit
* @param target 目标位数组对象
* @param bit 要设置的位
* @return 成功返回1,失败返回ERROR_BIT(当bit超过bitarray的最大位时)或者ERROR_NULL(当target为NULL时)
*
*/
int bitarray_set_bit(bit_array target,unsigned long bit);
/*
* 将目标对象中的某位设置为BIT_STATE_OFF,即0
* name: bitarray_clear_bit
* @param target 目标位数组对象
* @param bit 要设置的位
* @return 成功返回1,失败返回ERROR_BIT(当bit超过bitarray的最大位时)或者ERROR_NULL(当target为NULL时)
*
*/
int bitarray_clear_bit(bit_array target,unsigned long bit);
/*
* 测试目标对象中的某位的状态
* name: bitarray_test_bit
* @param target 目标位数组对象
* @param bit 要设置的位
* @return 成功返回目标位的状态(BIT_STATE_ON或者BIT_STATE_OFF),失败返回ERROR_BIT(当bit超过bitarray的最大位时)或者ERROR_NULL(当target为NULL时)
*
*/
int bitarray_test_bit(bit_array target,unsigned long bit);
/*
* 获取目标位数组对象中可容纳的位的数量;注意:位数组的索引是从0开始的,所以max_index=max_bits-1
* name: bitarray_max_bits
* @param target 目标位数组对象
* @return 成功返回目标位数组的位数量,失败返回0
*/
unsigned long bitarray_max_bits(bit_array target);
#endif
接口部分应该来说是比较清晰的,下面是具体的实现部分,存放在“bitarray.h”里:
#include <stdlib.h>
#include "bitarray.h"
//常量,将一个字符所占的位数定义为1字节,通常CHAR_BIT=8bit=1byte
const unsigned char A_BYTE = CHAR_BIT;
//掩码
const unsigned char MASK_1 = 1;
//定义真实的bitarray类型
struct bitarray{
//这就是bitarray真正的核心了,在底层我们使用unsigned char数组来模拟位数组并且用它来存储位信息
unsigned char* byte_arr;
//最大位
unsigned long max_bits;
//byte_arr数组的长度
unsigned long len;
} bitarray;
/**接口的实现部分**/
/*
* 创建一个bit_array
* name: bitarray_create
* @param bit 需要创建的位数组宽度,需要注意的是bit的值不能为0且必须为A_BYTE的整数倍,否则会创建失败
* @return 创建成功返回指向bitarray对象的指针(其实就是bit_array),创建失败返回NULL
*
*/
bit_array bitarray_create(unsigned long bits){
//首先定义一个临时变量
bit_array tmp=NULL;
unsigned long len=0;
//检查位数bits是否符合要求,不符合要求返回NULL
if(bits == 0 || (bits%A_BYTE) != 0)
return NULL;
//接下来为对象分配空间
tmp = malloc(sizeof(bitarray));
//检查有没有分配失败,分配空间失败返回NULL
if(tmp == NULL)
return NULL;
//计算所需的Byte数,也就是byte_arr数组的长度
len = bits / A_BYTE;
//为底层的byte_arr分配空间
tmp->byte_arr = calloc(len,sizeof(unsigned char));
//检查一下有没有分配成功
if(tmp->byte_arr == NULL)
{
//释放为tmp分配的空间
free(tmp);
//返回NULL
return NULL;
}
//
tmp->len=len;
tmp->max_bits=bits;
//返回对象
return tmp;
}
/*
* 销毁已经创建的bit_array对象,释放为它分配的空间
* name: bitarray_destroy
* @param target 需要销毁的对象
* @return 成功返回1,失败返回ERROR_NULL(当target为NULL时)
*
*/
int bitarray_destroy(bit_array target){
if(target == NULL)
return ERROR_NULL;
else{
free(target->byte_arr);//先释放底层数组对象的空间
free(target);//再释放对象本身
return 1;
}
}
/*
* 获取目标位数组对象中可容纳的位的数量;注意:位数组的索引是从0开始的,所以max_index=max_bits-1
* name: bitarray_max_bits
* @param target 目标位数组对象
* @return 成功返回目标位数组的位数量,失败返回0
*/
unsigned long bitarray_max_bits(bit_array target){
//检查参数
if(target == NULL)
return 0;
//
return target->max_bits;
}
/*
* 将目标对象中的某位设置为BIT_STATE_ON,即1
* name: bitarray_set_bit
* @param target 目标位数组对象
* @param bit 要设置的位
* @return 成功返回1,失败返回ERROR_BIT(当bit超过bitarray的最大位时)或者ERROR_NULL(当target为NULL时)
*
*/
int bitarray_set_bit(bit_array target,unsigned long bit){
//检查参数
if(target == NULL)
return ERROR_NULL;
if(bit >= target->max_bits)
return ERROR_BIT;
//
unsigned long idx;//bit在底层数组中元素的索引
unsigned int pos;//bit在其元素的第几位
//定位元素
idx = target->len - 1 - (bit/A_BYTE);
//定位到元素中的位
pos = (bit % A_BYTE);
//将指定位设置为开,即BIT_STATE_ON
target->byte_arr[idx] |= MASK_1<< pos;
//返回
return 1;
}
/*
* 将目标对象中的某位设置为BIT_STATE_OFF,即0
* name: bitarray_clear_bit
* @param target 目标位数组对象
* @param bit 要设置的位
* @return 成功返回1,失败返回ERROR_BIT(当bit超过bitarray的最大位时)或者ERROR_NULL(当target为NULL时)
*
*/
int bitarray_clear_bit(bit_array target,unsigned long bit){
//检查参数
if(target == NULL)
return ERROR_NULL;
if(bit >= target->max_bits)
return ERROR_BIT;
//
unsigned long idx;//bit在底层数组中元素的索引
unsigned int pos;//bit在其元素的第几位
//定位元素
idx = target->len - 1 - (bit/A_BYTE);
//定位到元素中的位
pos = (bit % A_BYTE);
//将指定位设置为关,即BIT_STATE_OFF
target->byte_arr[idx] &= ~(MASK_1<< pos);
//返回
return 1;
}
/*
* 测试目标对象中的某位的状态
* name: bitarray_test_bit
* @param target 目标位数组对象
* @param bit 要设置的位
* @return 成功返回目标位的状态(BIT_STATE_ON或者BIT_STATE_OFF),失败返回ERROR_BIT(当bit超过bitarray的最大位时)或者ERROR_NULL(当target为NULL时)
*
*/
int bitarray_test_bit(bit_array target,unsigned long bit){
//检查参数
if(target == NULL)
return ERROR_NULL;
if(bit >= target->max_bits)
return ERROR_BIT;
//
unsigned long idx;//bit在底层数组中元素的索引
unsigned int pos;//bit在其元素的第几位
//定位元素
idx = target->len - 1 - (bit/A_BYTE);
//定位到元素中的位
pos = (bit % A_BYTE);
//测试位状态
if(target->byte_arr[idx] & (MASK_1<< pos) )
return BIT_STATE_ON;
else
return BIT_STATE_OFF;
}
代码比较简单,结合注释应该比较容易看懂,下面是测试代码:
#include <stdio.h>
#include <stdlib.h>
#include "bitarray.h"
int main(void){
bit_array tes;
tes = bitarray_create(CHAR_BIT*800);//创建一个包含8*800=6400位的位数组
if(tes !=NULL){
//测试bitarray_max_bits函数
int max=bitarray_max_bits(tes);
printf("max_bits=%ld\n",bitarray_max_bits(tes));
printf("\n set \n");
//把偶数位全部置为1,并将位数组打印出来
for(int i=0;i<max;i+=2)
bitarray_set_bit(tes,i);
for(int j=0;j<max;j++)
printf("%d",bitarray_test_bit(tes,j));//按64位每行打印
printf("\n clear \n");
//再把所有偶数位设置为0,再打印
for(int i=0;i<max;i+=2)
bitarray_clear_bit(tes,i);
for(int j=0;j<max;j++)
printf("%d",bitarray_test_bit(tes,j));//按64位每行打印
printf("\n done \n");
bitarray_destroy(tes);//别忘了销毁,不然就内存泄露了
tes=NULL;
}
}
//把偶数位全部置为1,并将位数组打印出来
for(int i=0;i<max;i+=2)
bitarray_set_bit(tes,i);
for(int j=0;j<max;j++)
printf("%d",bitarray_test_bit(tes,j));//按64位每行打印
上面这段代码的运行结果如下
//再把所有偶数位设置为0,再打印
for(int i=0;i<max;i+=2)
bitarray_clear_bit(tes,i);
for(int j=0;j<max;j++)
printf("%d",bitarray_test_bit(tes,j));//按64位每行打印
上面这段代码的运行结果如下
看来我的位数组起作用了!下一步就是实现我自己的布隆过滤器了。
这里是额外的福利。
在写代码的过程中不免要进行调试,比如按指定的位数打印一个变量的位模式啊,于是我写了下面这个算法,送给有相同需要的童鞋吧!
void print_in_bitmode(unsigned int to_print, unsigned bit){
unsigned char *p_chs;
p_chs = malloc(bit+1);
unsigned char *reset= p_chs;
int result,reminder;
result = to_print;
reminder = 0;
if(p_chs == NULL)
{
printf("NULL Pointer!\n");
return;
}
//清零
for(int i=0;i<bit;i++)
{
*p_chs++ ='0';
}
*p_chs++ = (char)0x00;//空字符
//把指针重新指向数组的开头
p_chs=reset;
//p_chs -=(bit+1);//这种方式也可以把指针重新移到开头但是不好理解,而且存在隐患容易出错
//把to_print转换成bit位的二进制
while(result != 0)
{
reminder = result % 2;
result /= 2;
if(*p_chs != (char)0x00 )
{
*p_chs =(unsigned char) ('0'+reminder);
p_chs++;
}
else
{
printf("to_print的实际位数超出指定的bit位数!\n");
exit(-1);
}
}
p_chs=reset;
//倒序打印,从bit位(空字符前一位)到 0位,因为字符串的有效部分是0到空字符前一位
for(int i= bit ; i >= 0;i--)
{
printf("%c",*(p_chs+i));
}
//printf("%s",p_chs);
p_chs=reset;
free(p_chs);
p_chs = NULL;
reset = NULL;
}
同样是测试代码:
int main(void){
int i=12138;
print_in_bitmode(i,16);
return 0;
}
输出
0010111101101010
原来16位下的12138的位模式长这样^-^!
对了,我是在ubuntu15.04 64位平台上进行的测试,用的是GCC编译器,还有编译的时候记得把 C99选项打开(-std=c99)哦!
好了,今天就到这里。
bye~