bitmap解析

1.引子

         問題描述:
         輸入:一個最多含有n個不重複的正整數(也就是說可能含有少於n個不重複正整數)的文件,其中每個數都小於等於n,且n=10^7。
         輸出:得到按從小到大升序排列的包含所有輸入的整數的列表。
         條件:最多有大約1MB的內存空間可用,但磁盤空間足夠。且要求運行時間在5分鐘以下,10秒爲最佳結果。

         分析:下面咱們來一步一步的解決這個問題,
         1、歸併排序。你可能會想到把磁盤文件進行歸併排序,但題目要求你只有1MB的內存空間可用,所以,歸併排序這個方法不行。
         2、位圖方案。熟悉位圖的朋友可能會想到用位圖來表示這個文件集合。例如正如編程珠璣一書上所述,用一個20位長的字符串來表示一個所有元素都小於20的簡單的非負整數集合,邊框用如下字符串來表示集合{1,2,3,5,8,13}:

0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0
上述集合中各數對應的位置則置1,沒有對應的數的位置則置0。
          參考編程珠璣一書上的位圖方案,針對我們的10^7個數據量的磁盤文件排序問題,我們可以這麼考慮,由於每個7位十進制整數表示一個小於1000萬的整數。我們可以使用一個具有1000萬個位的字符串來表示這個文件,其中,當且僅當整數i在文件中存在時,第i位爲1。採取這個位圖的方案是因爲我們面對的這個問題的特殊性:1、輸入數據限制在相對較小的範圍內,2、數據沒有重複,3、其中的每條記錄都是單一的整數,沒有任何其它與之關聯的數據。
         所以,此問題用位圖的方案分爲以下三步進行解決:
                      第一步,將所有的位都置爲0,從而將集合初始化爲空。
                      第二步,通過讀入文件中的每個整數來建立集合,將每個對應的位都置爲1。
                      第三步,檢驗每一位,如果該位爲1,就輸出對應的整數。
         經過以上三步後,產生有序的輸出文件。令n爲位圖向量中的位數(本例中爲1000 0000),程序可以用僞代碼表示如下:

//第一步,將所有的位都初始化爲0
for i ={0,....n}
    bit[i]=0;
//第二步,通過讀入文件中的每個整數來建立集合,將每個對應的位都置爲1。
for each i in the input file
    bit[i]=1;
//第三步,檢驗每一位,如果該位爲1,就輸出對應的整數。
for i={0...n}
	if bit[i]==1
		write i on the output file
           問題:

           用此位圖方法,嚴格說來還是不太行,空間消耗10^7/8還是大於1M(1M=1024*1024空間,小於10^7/8)。
           既然如果用位圖方案的話,我們需要約1.25MB(若每條記錄是8位的正整數的話,則10000000/(1024*1024*8) ~= 1.2M)的空間,而現在只有1MB的可用存儲空間,那麼究竟該作何處理呢?

          改進的位圖方案,共需要掃描輸入數據兩次,具體執行步驟如下:
          第一次,只處理1—4999999之間的數據,這些數都是小於5000000的,對這些數進行位圖排序,只需要約5000000/8=625000Byte,也就是0.625M,排序後輸出。
          第二次,掃描輸入文件時,只處理4999999-10000000的數據項,也只需要0.625M(可以使用第一次處理申請的內存)。
          因此,總共也只需要0.625M。

          對應源碼如下:

//位圖方式解決海量數據排序,數據不能有重複

//使用 C++ stl的 bitset
#include <iostream>
#include <bitset>
#include <assert.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

const int max_each_scan = 5000000;

int main()
{
    clock_t begin = clock();
    bitset<max_each_scan> bit_map;
    bit_map.reset();
    // open the file with the unsorted data
    FILE *fp_unsort_file = fopen("data.txt", "r");
    assert(fp_unsort_file);
    int num;
    
    // the first time scan to sort the data between 0 - 4999999
    while (fscanf(fp_unsort_file, "%d ", &num) != EOF)
    {
        if (num < max_each_scan)
			//有這個數字,將bit_map的對應的位設置爲1
			bit_map.set(num, 1);
    }
    FILE *fp_sort_file = fopen("sort.txt", "w");
    assert(fp_sort_file);
    int i;
    // write the sorted data into file
    for (i = 0; i < max_each_scan; i++)
    {
        if (bit_map[i] == 1)
        fprintf(fp_sort_file, "%d ", i);
    }
    
    // the second time scan to sort the data between 5000000 - 9999999
    int result = fseek(fp_unsort_file, 0, SEEK_SET);
    if (result)
		cout << "fseek failed!" << endl;
    else
    {
        bit_map.reset();
		while (fscanf(fp_unsort_file, "%d ", &num) != EOF)
		{
			if (num >= max_each_scan && num < 10000000)
			{
				num -= max_each_scan;
				bit_map.set(num, 1);
			}
		}
		for (i = 0; i < max_each_scan; i++)
		{
			if (bit_map[i] == 1)
				fprintf(fp_sort_file, "%d ", i + max_each_scan);
		}
    }
    clock_t end = clock();

    cout<<"用位圖的方法,耗時:"<<endl;
    cout << (end - begin) / CLK_TCK << "s" << endl;
    fclose(fp_sort_file);
    fclose(fp_unsort_file);
    return 0;
}

2.bitmap原理

          所謂的Bit-map就是用一個bit位來標記某個元素對應的Value, 而Key即是該元素。由於採用了Bit爲單位來存儲數據,因此在存儲空間方面,可以大大節省。

          假設我們要對0-7內的5個元素(4,7,2,5,3)排序(這裏假設這些元素沒有重複)。那麼我們就可以採用Bit-map的方法來達到排序的目的。要表示8個數,我們就只需要8個Bit(1Bytes),首先我們開闢1Byte的空間,將這些空間的所有Bit位都置爲0,如下圖:


         然後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置爲1(可以這樣操作 p+(i/8)|(0×01<<(i%8)) 當然了這裏的操作涉及到Big-ending和Little-ending的情況,這裏默認爲Big-ending),因爲是從零開始的,所以要把第五位置爲一(如下圖):


         然後再處理第二個元素7,將第八位置爲1,,接着再處理第三個元素,一直到最後處理完所有的元素,將相應的位置爲1,這時候的內存的Bit位的狀態如下:


         然後我們現在遍歷一遍Bit區域,將該位是一的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。下面的代碼給出了一個BitMap的用法:排序。

//定義每個Byte中有8個Bit位
#include <memory.h>
#include <stdio.h>

#define BYTESIZE 8

void SetBit(char *p, int posi)
{
	for(int i=0; i < (posi/BYTESIZE); i++)//找出所在字節
	{
		p++;
	}

	*p = *p|(0x01 << (posi%BYTESIZE));//將該Bit位賦值1
	return;
}

void BitMapSortDemo()
{
	//爲了簡單起見,我們不考慮負數
	int num[] = {3,5,2,10,6,12,8,14,9};
	int i;

	//BufferLen這個值是根據待排序的數據中最大值確定的
	//待排序中的最大值是14,因此只需要2個Bytes(16個Bit)
	//就可以了。
	const int BufferLen = 2;
	char *pBuffer = new char[BufferLen];

	//要將所有的Bit位置爲0,否則結果不可預知。
	memset(pBuffer,0,BufferLen);

	for(i=0; i < 9; i++)
	{
		//首先將相應Bit位上置爲1
		SetBit(pBuffer,num[i]);
	}

	//輸出排序結果
	for(i=0;i < BufferLen;i++)//每次處理一個字節(Byte)
	{
		for(int j=0;j < BYTESIZE;j++)//處理該字節中的每個Bit位
		{
			//判斷該位上是否是1,進行輸出,這裏的判斷比較笨。
			//首先得到該第j位的掩碼(0x01<<j),將內存區中的
			//位和此掩碼作與操作。最後判斷掩碼是否和處理後的
			//結果相同
			if((*pBuffer&(0x01 << j)) == (0x01 << j))
			{
				printf("%d ",i*BYTESIZE + j);
			}
		}
		pBuffer++;
	}
}

int main(int argc, char * argv[])
{
	BitMapSortDemo();
	return 0;
}


參考:

1.http://www.kuqin.com/algorithm/20111006/312574.html

2.http://blog.redfox66.com/post/2010/09/26/mass-data-4-bitmap.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章