• [珠玑之椟]位向量/位图的定义和应用


      位向量/位图是一个很有用的数据结构,在充分利用小空间存储大量数据方面非常具有优势,Linux内核中很多地方都是用了位图。同时,它不但基础,而且用到了很多编程语言的知识,以及对细节的把握,常常作为面试题出现。这里将要介绍它的实现、操作、应用。

      与位图(bitmap)比,我更倾向于用位向量(bit vector),前者比较容易与图形学里的名词混淆,其实提到位图,多指的是“是使用像素阵列来表示的图像”(维基百科),为了避免这一点,下文中使用位向量。先来看看产生位向量的需求:

      一般地,对于多个对象和一个性质,这些对象可能满足(true)也可能不满足(false)这条性质。那么,为了表示所有对象对这个性质的满足情况,最容易想到的方式是分配一个int型变量(这里不讨论布尔型,C++没有对布尔型占用空间有明确规定;本文主要讨论C)作为标志,用1表示满足条件,用0表示不满足条件。同时为了方便查询,把这些对象的标志整合成一个数组。显然,使用int来表示是0还是1有些太浪费空间了,即使把int改为char,浪费的情况也只是减轻了一部分,仍有很大的空间被浪费。考虑到计算机中最小的数据单位是非0即1的二进制位,对于一个对象,使用一个二进制位就足够了。很多语言都具有位运算,利用位运算是可以完成本段开始处提出的要求的。当然,因为不能用一个变量名直接表示一个位,那么可以将多个位组合成一个基本数据类型,通过对这个基本数据类型进行操作,达到使用位的方法。同时,为了方便,延续使用int数组的做法,把这些由位组合成的基本数据类型也组成数组。

      经过这样分析,位向量的实现方法大体是:多个位组成一个基本数据类型,基本数据类型组合成数组。根据这个思路就可以写出位向量的表示了。在阅读下面代码前,建议读者尝试自己独立完成,这是一些提示:简单起见,使用int作为位组成的基本数据类型,且int使用32位表示;int数组中元素的个数如何计算?

    #define N           10000000 //number of elements
    #define BITPERWORD  32 //bits of int depends on machine
    int a[(N-1)/BITPERWORD + 1]; //allocate space for bitmap 
    //《编程珠玑》原书中是N/BITPERWORD + 1
    //如果N恰为BITPERWORD的倍数,那么又要浪费一个int的空间了
    //N取0时,仍会浪费一个int的空间
    //N非0非BITPERWORD的倍数时,最多是最后一个int不完全利用而已
    位向量的表示

      写完了表示,就需要为这个数据结构增加对应的操作了。对于每个位的操作,有三种:设置为0、设置为1、读取当前值。根据上文位向量的表示,实现这三种操作。同样建议读者先尝试独立完成。以下代码参考自《编程珠玑》习题1.2。

    #define SHIFT       5    //32 = 2^5
    #define MASK        0x1F // vaule 11111 in binary
    
    //i代表需要进行操作的第i个对象
    //i>>SHIFT相当于i/32,将i定位到具体是哪个int中,即a[i>>SHIFT]
    //i&MASK相当于i%32 只保留i的0至4位,即i在int中的第几位,然后把1左移这么多位
    //将a[i>>SHIFT]和(1<<(i&MASK))视需要进行操作
    //同1做或运算或即为位设置
    //同1取反再做与运算即为位清除
    //同1做与,结果为0则原位为0,为1则原位为1
    void set(int i)     {        a[i>>SHIFT] |=  (1<<(i&MASK));}
    void clr(int i)     {        a[i>>SHIFT] &= ~(1<<(i&MASK));}
    int test(int i)    { return a[i>>SHIFT] &   (1<<(i&MASK));}
    位向量操作

      使用位向量前不要忘记对所有位进行初始化:

    for(i=0;i<N;i++)
        clr(i);

      当然,你也可以用这个或许更快的初始化方式:

    int temp = N/BITPERWORD+1;
    for (i=0; i< temp;i++) 
        a[i] = 0;

    补充讨论:

    Q:为什么要用看上去并不那么直接的位运算而不是i/32和i%32?

    A:为了速度而不是易读性。你所写下的最初版本如果使用的是/和%,当然没有错;当你着手于提高效率的时候,把它们改成移位运算就势在必行了。虽然编译器可能会对/和%在除数为2的幂时进行优化,写成移位运算,你当然可以自己来完成这件事来保证确实优化了。

     

    Q:是否可以将存放位向量的数组作为参数?

    A:当然可以,上文代码只是为了叙述方便。

     

    Q:是否可以把这三个函数写成宏?

    A:其实我曾经被面试过一道题,就是要求写一个宏,完成上面clr(i)的任务。写成宏当然没问题,注意加好括号就行,具体的用法让调用的人去担心吧(笑)。

     

    Q:位向量和位字段/位域有什么区别和联系?可以用位字段来实现位向量吗?

    A:C语言允许将结构的整数成员存入比编译器通常允许的更小的空间里。然而它有三个风险:不同计算机中对齐限制不同;位字段宽度限制不同;将位包装成字的字节顺序不同。在位向量里,虽然也有依赖于具体计算机特性的限制(需要预先知道int的位数),但是位的包装是由程序员来控制的,也不必考虑对其限制。移植性略好于将成员声明为1位整数位字段的结构。

     

    应用:

    1.Linux中分配唯一pid的算法、内存管理的伙伴分配系统等,详细可以google,关键词:linux+位图。

    2.一个最多包含n个正整数的文件,每个数都小于n,其中n=107,并且没有重复。最多有1MB内存可用。要求用最快方式将它们排序并按升序输出。(《编程珠玑》第一章正文)方法是一次读入文件,把出现过的数字对应位置1;读取完毕后从低位到高位输出位向量为1的位所代表的数。

    3.如果有用时间换空间的必要,可以将寻找1至某个数之间所有质数的埃氏筛法用位向量实现。原始版本的代码见于《编程珠玑(续)》习题1.2,算法简介:一个n比特的数组,对应1至n-1,初值均为真。从2开始,每发现一个质数,就把这个数组中所有这个数的倍数设为假。下一个质数就是数组中下一个为真的比特。循环至所有数都被遍历,即可得到该范围内所有的质数。

     

    引申和扩展:

    1.(习题1.6)每个整数最多出现10次而不是1次;如果内存限制不变,又应该怎么做?提示:多趟排序

    (更一般的扩展:用几个位表示一个对象的多个性质,也即一个对象不仅仅只占用一位而是多位,但每个对象占用的位数是相同的。重写位图。)

    2.(习题1.9)对于稀疏的位向量,初始化很浪费时间。一般地,对于一个向量(不仅限于位向量),怎样利用辅助数据结构,使得第一次访问位向量的某一位时才将其初始化为0?

    解法:

    对于向量int data[N],分配辅助数据结构int from[N]和to[N],以及一个整数top,初始化top=0。

    插入data[i]时,from[i]填入top,to[top] = i,top++;

    这样维持了一个性质:top代表下一个被插入的值的次序;from[i]代表了data[i]是第几个被插入的;to[]中小于top的元素to[j]保存了第j个被插入对应在data[]的下标。

    若data[i]未初始化,则from[i]也未初始化,此时即使from[i]中未初始化的垃圾值<top,to[from[i]]!= i,仍能判断出data[i]未初始化。

     

    “珠玑之椟”系列简介与索引

  • 相关阅读:
    前端开发 Knockout
    一套基于Spring Boot+Vue+Shiro前后端分离的代码生成器
    七个开源的 Spring Boot 前后端分离项目
    Java老司机:把这些主流技术搞懂,拿20K没问题
    svn无法cleanup解决方案
    软件测试-1挡板测试
    电子琴
    myeclipse10激活注册码生成器代码
    LNK2005
    无法打开包含文件:"fstream.h"
  • 原文地址:https://www.cnblogs.com/wuyuegb2312/p/3136831.html
Copyright © 2020-2023  润新知