1.引子
問題描述:
輸入:一個最多含有n個不重複的正整數(也就是說可能含有少於n個不重複正整數)的文件,其中每個數都小於等於n,且n=10^7。
輸出:得到按從小到大升序排列的包含所有輸入的整數的列表。
條件:最多有大約1MB的內存空間可用,但磁盤空間足夠。且要求運行時間在5分鐘以下,10秒爲最佳結果。
分析:下面咱們來一步一步的解決這個問題,
1、歸併排序。你可能會想到把磁盤文件進行歸併排序,但題目要求你只有1MB的內存空間可用,所以,歸併排序這個方法不行。
2、位圖方案。熟悉位圖的朋友可能會想到用位圖來表示這個文件集合。例如正如編程珠璣一書上所述,用一個20位長的字符串來表示一個所有元素都小於20的簡單的非負整數集合,邊框用如下字符串來表示集合{1,2,3,5,8,13}:
參考編程珠璣一書上的位圖方案,針對我們的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