1.  概述

位图(bitmap)是一种非常常用的结构,在索引,数据压缩等方面有广泛应用。本文介绍了位图的实现方法及其应用场景。

2. 位图实现

编程珠玑-位图_#include


在位图中,每个元素为“0”或“1”,表示其对应的元素不存在或者存在。


C语言版本:

 


/* Copyright (C) 1999 Lucent Technologies */
/* From 'Programming Pearls' by Jon Bentley */

/* bitsort.c -- bitmap sort from Column 1
* Sort distinct integers in the range [0..N-1]
*/

#include <stdio.h>

#define BITSPERWORD 32
#define SHIFT 5 //2^5 = 32, 移动5个位,左移则相当于乘以32,右移相当于除以32取整
#define MASK 0x1F //2^5 = 32, 16进制下的31
#define N 10000000
int a[1 + N/BITSPERWORD];

//i >> SHIFT 相当于 i / (2 ^ SHIFT), i&MASK相当于mod操作 m mod n 运算, 当n = 2的x次幂的时候,m mod n = m&(n-1)
//设置第i位, 用"|"操作符
void set(int i) { a[i>>SHIFT] |= (1<<(i & MASK)); }
//清除第i位, 用"&~"操作符
void clr(int i) { a[i>>SHIFT] &= ~(1<<(i & MASK)); }
//获取第i位, 用"&"操作符
int test(int i) { return a[i>>SHIFT] & (1<<(i & MASK)); }

int main()
{ int i;
for (i = 0; i < N; i++)
clr(i);
/* Replace above 2 lines with below 3 for word-parallel init
int top = 1 + N/BITSPERWORD;
for (i = 0; i < top; i++)
a[i] = 0;
*/
//在控制台上用键盘输入CTRL-Z可以实现scanf的输入EOF
while (scanf("%d", &i) != EOF)
set(i);
for (i = 0; i < N; i++)
if (test(i))
printf("%d\n", i);
return 0;
}


 (掩码mask怎么求,比如要求sequenceBits位的掩码,

    private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095

如果sequenceBits为2,

-1L为111111111.。

 

clr(int i) {        a[i>>SHIFT] &= ~(1<<(i & MASK));

数组a

    a[1 + N/BITSPERWORD]:一个int占4个字节,所以数组的一位表示4*8 = 32

我们用int a[]来存储数,由于一个int有4个Byte,我们i>>5,相当于除以32,选择了相应的数组,

i&&mask

MASK的值为0x1F = 00011111,i&mask 等于 i%mask.

    i>>shift)中的操作可以确定,i在数组a中的index1;通过该取模操作,可以确定i在该index表示的32为二进制序列中的index2。

    因此,1<<(i & MASK)实际上就是仅将该二进制序列中的index2位置设置为1,其余位置全部设置为0.

我们假设现在使用的 是char数组,原来char【0】里面内容如下:

00100000(我们这是从左到右,从高位在地位读,但是在存储的时候是从低位到高位,小端存储)

我们假设i=5. i&mask(现在mask为111) 0101&0111=0101,即5.

1<<5 为100000. 取反为11011111.

成功把第5为置0.

//获取第i位, 用"&"操作符
int test(int i) { return a[i>>SHIFT] & (1<<(i & MASK)); }
理解了上面,这个就好理解了,
看第i为是否为1还是0.
使用C++的bitset:



#include<iostream>
#include<bitset>
using namespace std;

#define N 10000000
bitset<N> b;
int main()
{
int n;
while(cin>>n)
{
b.set(n);
}
for(int i=0;i<N;i++)
{
if(b.test(i))
cout<<i<<ends;
}


}


注意bitset<N>不能放在main内,否则stack overflow,如果要放在main内,要加static或new一个。 


#include <stdio.h>  
#include <stdlib.h>
#define WORD 32
#define SHIFT 5 //移动5个位,左移则相当于乘以32,右移相当于除以32取整
#define MASK 0x1F //16进制下的31
#define N 10000000

//bitmap的基本操作:
/*
* 置位函数——用"|"操作符,i&MASK相当于mod操作
* m mod n 运算,当n = 2的X次幂的时候,m mod n = m&(n-1)
*/
void set(int *bitmap, int i) {
bitmap[i >> SHIFT] |= (1 << (i & MASK));
}
/* 清除位操作,用&~操作符 */
void clear(int *bitmap, int i) {
bitmap[i >> SHIFT] &= ~(1 << (i & MASK));
}
/* 测试位操作用&操作符 */
int test(int *bitmap, int i) {
return bitmap[i >> SHIFT] & (1 << (i & MASK));
}

//排序:
void sort() {
int bitmap[1 + N / WORD];
FILE *in = fopen("in.txt", "r");
FILE *out = fopen("out.txt", "w");
if (in == NULL || out == NULL) {
exit(-1);
}
int i = 0;
int m;
for (i = 0; i < N; i++) {
clear(bitmap, i);
}
while (!feof(in)) {
fscanf(in, "%d", &m);
printf("%d/n", m);
set(bitmap, m);
}
printf("abnother");
for (i = 0; i < N; i++) {
if (test(bitmap, i)) {
printf("%d/n", i);
fprintf(out, "%d/n", i);
}
}
fclose(in);
fclose(out);
}

//求交集:void merge() {
int A[] = { 1, 3, 5, 7, 9, 435, 3454, 345543, 3453455 };
int B[] = { 4, 5, 6, 8, 9, 435, 3454, 345543 };
int bitmapA[1 + N / WORD];
int bitmapB[1 + N / WORD];
int i = 0;
int j = 0;
for (i = 0; i < N; i++) {
clear(bitmapA, i);
clear(bitmapB, i);
}
for (i = sizeof(A) / sizeof(*A) - 1; i >= 0; i--) {
set(bitmapA, *(A + i));
}
for (j = sizeof(B) / sizeof(*B) - 1; j >= 0; j--) {
set(bitmapB, *(B + j));
}
for (i = 0; i < N; i++) {
if (test(bitmapA, i) & test(bitmapB, i)) {//交集
printf("%d/n", i);
}
}
}

int main(void) {
//sort();
merge();
return EXIT_SUCCESS;
}


 javascript版本;



<script>
//其实稍加改进,这个算法是支持数组内的重复元素的.
var arr=[5,7,1,9,3,4,6,0,50,23,4,3,56,99,87];
var out=[];
var m=arr[0];
for(var i=0; i<arr.length; i++){
m=Math.max(m,arr[i]);
if (out[arr[i]]){
out[arr[i]]=out[arr[i]]+1;
}else{
out[arr[i]]=1;
}
}
//alert(out);
for(j=0; j<=m; j++){
if (out[j]) {
for(k=0;k<out[j];k++){
document.write(j,"<br>");
}
}
}
</script>


(2)函数库实现


 C++的STL中有bitmap类,它提供了很多方法 ​

 上面的解法还可以用c遇到qsort排序,或是:

基于集合的排序算法:用C++ STL中的set容器。



#include <cstdio>
#include <iostream>
#include <set>
using namespace std;
int main(void){
set<int> sint;
int i;
set<int>::iterator j;
while(cin>>i)
sint.insert(i); //会进行有序的插入
for(j=sint.begin();j!=sint.end();++j)
cout<<*j<<"/n";
return 0;
}


 


3.  位图应用


 3.1    枚举



(1)全组合

字符串全组合枚举(对于长度为n的字符串,组合方式有2^n种),如:abcdef,可以构造一个从字符串到二进制的映射关系,通过枚举二进制来进行全排序。

null——> 000000

f——> 000001

e——> 000010

ef——> 000011

……

abcedf——> 111111

如果不是求字符的所有排列,而是求字符的所有组合应该怎么办呢?还是输入三个字符 a、b、c,则它们的组合有​​a​​ ​​b​​ ​​c​​ ​​ab​​ ​​ac​​ ​​bc​​ ​​abc​​。当然我们还是可以借鉴全排列的思路,利用问题分解的思路,最终用递归解决。不过这里介绍一种比较巧妙的思路 —— 基于位图。

假设原有元素 n 个,则最终组合结果是 $2^n-1$ 个。我们可以用位操作方法:假设元素原本有:a,b,c 三个,则 1 表示取该元素,0 表示不取。故取​​a​​则是​​001​​,取​​ab​​则是​​011​​。所以一共三位,每个位上有两个选择 0 和 1。而​​000​​没有意义,所以是$2^n-1$个结果。

这些结果的位图值都是 1,2…2^n-1。所以从值 1 到值 $2^n-1$ 依次输出结果:

​001​​,​​010​​,​​011​​,​​100​​,​​101​​,​​110​​,​​111​​ 。对应输出组合结果为:​​a​​,​​b​​,​​ab​​,​​c​​,​​ac​​,​​bc​​,​​abc​​。

因此可以循环 1~2^n-1,然后输出对应代表的组合即可。有代码如下:



#include<stdio.h>
#include<string.h>

void Combination(char *str)
{
if(str == NULL)
return ;
int len = strlen(str);
int n = 1<<len;
for(int i=1;i<n;i++) //从 1 循环到 2^len -1
{
for(int j=0;j<len;j++)
{
int temp = i;
if(temp & (1<<j)) //对应位上为1,则输出对应的字符
{
printf("%c",*(str+j));
}
}
printf("\n");
}
}

void main()
{
char str[] = "abc";
Combination(str);
}


参考:http://wuchong.me/blog/2014/07/28/permutation-and-combination-realize/


(2)哈米尔顿距离


看如下题目:

编程珠玑-位图_#define_02

如果用枚举算法,复杂度是O(N^2),怎样降低复杂度呢?


如果是N 个二维的点,那么我们可以怎么用较快的方法求出


编程珠玑-位图_#define_03


通过简单的数学变形,我们可以得到这样的数学公式:


编程珠玑-位图_#define_04



 通过观察,我们发现每一对相同元的符号必定相反,如:x_i-y_i,于是我们有了一个二进制思想的思路,那就是枚举这些二i维的点的x 轴y 轴前的正负号,这样就可以用一个0~3 的数的二进制形式来表示每个元素前面的正负号,1表示+号,0表示−号,如:2 表示的二进制位形式为10表示x_i-y_i。这样我们就可以通过2^2*N次记录下这些二元组的不同的符号的数值,对于每个二进制来表示的不同的式子只 需记录下他们的值,这样我们只需求max_i 和min_i出这些相同的二进制表示的式子max_i –min_i,最后我们就可以解出ans=max{max_i-min_i}。



通过位图,算法时间复杂度可将为O(N)。

 



3.2   搜索

设计搜索剪枝时,需要保存已经搜索过的历史信息,有些情况下,可以使用位图减小历史信息数据所占空间。

3.3    压缩

(1)在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数?

(2)腾讯面试题:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?

  海量数据搜索问题:一个顺序文件包含40亿个随机排列的32位整数,找出一个不在文件中的32位整数。

  (1)位图技术:如果有足够的内存,可以使用位图技术。使用536870912个8位字节形成的位图来表示文件的整数。通过扫描位图即可找到缺失的整数。

  (2)排序技术:通过对文件进行快速排序,我们能够找到缺失的整数。这时总的运行时间正比于nlogn。

  (3)二分搜索技术:如果仅有几百个字节的内存和几个外部的临时顺序文件可用呢?采用二分搜索技术并结合多趟算法。第一遍通过多趟读取40亿个输入整数,并把起始位为0的整数写入一个顺序文件,把起始位为1的整数写入另一个顺序文件。这两个文件中有一个文件最多包含20亿个整数,接下来将该文件用作当前输入并重复探测过程,但这次探测的是第第二个位。如果原始的输入文件包含n元素,那么第一遍将读取n个整数,第二遍最多读取n/2个整数,第三遍最多读取n/4个整数,依此类推,最后我们可以找到缺失的整数。所以总的运行时间正比于n。

  关键算法设计思想:位图数据结构、集合数据结构、多趟策略、排序策略、二分搜索策略。

4. 总结

Bitmap是一种非常简洁快速的数据结构,他能同使证存储空间和速度最优化(而不必空间换时间)。

 

 

补充理解位图:

 


1.位图的理解 

我们都明白图形格式中位图储存方式,其实就是以象素为单位的小方块,一格一格的纵横累积起来. 每一个小方块代表一种颜色,当然,如果对于黑白的二色图来说更加简单,只需要一个bit位即可表示. 这和我们的数据在计算机中的存储格式是相似的,内存条的也像是一格一格的bit位纵横交错而成. 因为这样的启发,我们发现一个个bit位象列队一样排列着,顺序相当严谨,如果我们的数据能够通过一种转换方式(逻辑上)能有序的和bit位一一对应起来 的话,那么我们按照bit位的顺序把它输出来不就是排序的数据集合吗? 


2.索引的概念 

通过上面的描述,我们很容易联想到一样东西-索引。索引对于我们数据库的使用无疑相当重要,以至于现在很多数据量巨大的单表查询的性能完全仰仗于它.它和 位图的相似性在于:如果我们把每一行数据看作一个单位的数据,那么索引可以看作是该数据通过一种转化方式映射到某个存储空间,如果数据的顺序和索引的顺序 是一致的话,那么当我们按序对该存储空间访问时,就得到了有序的数据集.当然很多情况下,索引都是数据的一部分,然而在Oracle中有函数索引的概念, 它就完全表达了这种转化方式和映射关系了. 


3.排序的一种巧妙方法 

位图天生和排序分不开,因为它是最本质的有序载体.


有一种问题如下:现有某市的所有7位数字的电话号码,要求我们按序输出. 

分析: 问题的目标-是对数字进行排序 问题的条件-7位数字,简单看作0000000到9999999 

问题的环境-一个市的电话号码,数量不菲,极有可能接近1000万,任何排序方法的时间 

和空间代价都很大. 

联想: 抓住问题的意义,电话号码在本问题上的一个现实意义就是该电话号码在整个电话号码集合上的位子,更具有特征的是,电话号码本身就反应了这么一个位子信息. 如果我们设立1000万个bit位,每一位表示该位置上电话号码是否存在(设定1为存在,0-不存在),位号就是电话号码本身,那么我们遍历所有的位,输 出位号为1的电话号码,不就是排序的电话号码吗? 巧妙之处: 因为我们利用了数据本身的意义! 


描述过程: 

1.把整个bit位组初始化为0(000000000000......00000000) 

2.读入所有的号码,在号码对应的Bit上置1 

3.循环: for(int i=0;i<10000000;++i) { //i就是电话号码 if(bit[i]==1){ print i; } 

4.扩展位图排序本身需要一定的环境,就像上面描述的数量大,且和位置数字序号的意义吻合. 当然,我们看到了位图排序的高效与精彩巧妙之处,对于我们的数据进行排序的时候,可不可以思考一下: 分析我们的数据特征很关键,任何问题可能都是从分析特征找突破口的,考虑一下我们的数据存不存在一种转化方法使得他能映射到这种数字关系上来.构造的过程 也是你的创造.

具体代码见文中之前部分。

 


为什么说这个算法时空效率达到及致呢?我们对100万个不重复的正整数(1000,0000以内)的文件进行测试:


 

系统排序

C++/STL.set

C/qsort

C/位图

总时间(s)

89

38

12.6

10.7

计算时间(s)

79

28

2.4

0.5

内存使用(MB)

0.8

70

4

1.25

(本测试数据是在较旧的电脑上测试的,但还是体现性能的差距)

  第一行是总时间,第二行的计算时间是总时间减去数据读取耗时10.2秒。虽然通用C++程序使用内存和CPU时间是专用C程序(C/位图)的50倍,但是它的使用仅需要一半的代码,并能很容易扩展到其他问题上,这也是专用C程序最大的缺点吧。