【转】数据结构之位图

转自:http://dongxicheng.org/structure/bitmap/

http://blog.csdn.net/QIBAOYUAN/article/details/5914662 

http://www.cnblogs.com/djbone/archive/2008/08/20/1271816.html 

参考:http://www.velocityreviews.com/forums/t444388-bit-vector-question-why-shift-5-a.html 

 

1.  概述

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

2. 位图实现

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

C语言版本:

 改自这里的代码:http://netlib.bell-labs.com/cm/cs/pearls/bitsort.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;
}

 

 C语言的另外一个类似版本,来自:http://blog.csdn.net/qibaoyuan/article/details/5914746

#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[] = { 1357943534543455433453455 };  
    int B[] = { 456894353454345543 };  
    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;  
}

 

 C#语言版本(本人未作测试):

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            BitMap bm = new BitMap(100000010000);
            bm.CreateRandomData();//产生随机数
            bm.Sort();//排序
            bm.PrintDataAfterSort();//输出
            Console.ReadLine();
        }
    }
    class BitMap
    {
        public int DateLenth;
        public int MaxNumber;
        public int[] DataForStore;
        public int[] DataForSort;
        /// <summary>
        
/// 
        
/// </summary>
        
/// <param name="datelenth">带排序个数</param>
        
/// <param name="maxnumber">带排序最大数</param>
        public BitMap(int datelenth, int maxnumber) 
        {
            DateLenth = datelenth;
            MaxNumber = maxnumber;
            DataForStore = new int[maxnumber];
        }
        /// <summary>
        
/// 产生随机数,便于测试
        
/// </summary>
        public void  CreateRandomData()
        {
            Random r = new Random();
            DataForSort = new int[DateLenth];             
            for (int i = 0; i < DateLenth; i++)
            {
                DataForSort[i] = r.Next(MaxNumber); 
            }
        }
        /// <summary>
        
/// 排序
        
/// </summary>
        public void Sort()
        {
            for (int i = 0; i < DateLenth; i++)
            {
                DataForStore[DataForSort[i]]++; 
            }
        }
        /// <summary>
        
/// 输出排序后的数据
        
/// </summary>
        public void PrintDataAfterSort()
        {
            for (int i = 0; i < MaxNumber; i++)
            {
                for (int j = 0; j < DataForStore[i]; j++)
                {
                    Console.Write(i+",");
                }
            }
        }
    }
}

 

JavaScript语言版本(本人未作测试):
<script language="JavaScript">
    var arr=[5,7,1,9,3,4,6,0,50,23,56,99,87];   
    var out=[];
    var m=arr[0];
    for(var i=0; i<arr.length; i++){   
        m=Math.max(m,arr[i]);   
        out[arr[i]]=1;   
    }   
    //alert(out);
    for(j=0; j<=m; j++){
        if (out[j]) document.write(j,"<br>");
    }
</script>

 

<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类,它提供了很多方法,详见:http://www.cplusplus.com/reference/stl/bitset/

 
3.  位图应用

 3.1    枚举

(1)全组合

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

null——> 000000

f——> 000001

e——> 000010

ef——> 000011

……

abcedf——> 111111

(2)哈米尔顿距离

看如下题目:

 

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

如果是N 个二维的点,那么我们可以怎么用较快的方法求出
 通过简单的数学变形,我们可以得到这样的数学公式:

 通过观察,我们发现每一对相同元的符号必定相反,如: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亿个数当中?

4. 总结

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

5.  参考资料

(1)《C实现bitmap位图》:

http://blog.csdn.net/QIBAOYUAN/archive/2010/09/29/5914662.aspx

(2)武森《浅谈信息学竞赛中的“0”和“1”》

 http://files.getdropbox.com/u/1256772/OI/PPT/1.%E6%AD%A6%E6%A3%AE%E3%80%8A%E6%B5%85%E8%B0%88%E4%BF%A1%E6%81%AF%E5%AD%A6%E7%AB%9E%E8%B5%9B%E4%B8%AD%E7%9A%84%E2%80%9C0%E2%80%9D%E5%92%8C%E2%80%9C1%E2%80%9D%E3%80%8B.rar

 

补充理解位图:

 

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程序最大的缺点吧。

 

 

posted on 2011-11-02 01:25  风在竹林  阅读(859)  评论(0编辑  收藏  举报