最近在看吳軍的《數學之美》對其中的一些技術和算法很感興趣,看到布隆過濾器的時候突然很想自己去動手實現一個自己的布隆過濾器(至於什麼是布隆過濾器,傳送門在這。
在我看來,一個布隆過濾器的核心包括兩部分:一個位向量,一組設計精巧的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~