《編程珠璣,字字珠璣》1234讀書筆記——多路歸並排序


寫在前面的

2012年3月25日買下《編程珠璣》,很期待但不知道它能給我帶來什么!
編程珠璣,字字珠璣。但是翻譯有點拗口,有時候整句話讀下來都不知道在講什么,多少有點掩飾了珠璣的魅力,真懷疑是不是直接有道翻譯了。

位圖數據結構法

在“開篇”的里,講述了排序的一個問題,大意就是,對一個“最多占n位的(就是n位的整數),隨機的,無重復的(互異無序)”的整數序列進行排序,那么這個序列的總長度len<=10^n。例如:這個序列中的每個整數最多占3位,那么序列最多有0~999的1000個數。

無重復會有很大的啟發,可以試着使用“位圖數據結構”來解決。位圖數據結構?位圖中的每個像素都被存儲在計算機當中,並用一定的字節數來標記它的屬性值。啟發:如果是黑白的位圖,那么位圖中的每一個像素都可以用一個bool值來標記,因為位圖無非就是黑與白,同理,可以應用到這個問題當中。

如果出現了,就用‘1’來標記;反之,用‘0’來標記,而用一個數組來表示給出的序列,那么數組的下標就是對應的出現的整數了。例如:dl[1]=1,那么,1在這個序列中有出現。這就是在統計出現與否的同時達到了排序了效果,而且節省了大量的時間與空間。這個效果與用排序模板和經常使用的外部(歸並)排序比起來是絕對勝出的。

這是它“神奇”的地方,輸入序列和輸出序列都只有一次。1kw個上述整數序列也只用到了大約1.25M的內存空間。只能說,“位圖數據結構法”在解決這樣的問題有優勢,只要把條件做小小的改動,情況會變得非常糟糕:把“互異”改為“非互異”,“位圖法”無法勝任,還是要回到原始的外部排序算法。居然這樣,那就一窺芳容。

多路歸並排序 

外部排序,也就是借助了磁盤,所有的排序過程並不是都在內存中完成。所以,外部排序沒什么可怕的,常用的外部排序就是多路歸並排序,它歸並排序是一樣的。關於多路歸並排序,在多路歸並排序這篇博文當中寫得很清楚,不羅嗦了,這篇博文寫的很認真,這篇博文當是自己的學習筆記。

珠璣“開篇”的課后習題第3題所要解決的就是測試多路歸並排序的首要問題,所以沒了它,也是無米之炊。產生隨機數可以用rand庫函數,但是它有缺陷的,rand函數產生的隨機數最大是32767,但是這里需要KW,甚至更大的數據。 
解決方法原理:可以依據“洗牌原理”,一副牌,將其中一張(任意挑選)與另一張(任意挑選)置換,...如此重復n次。牌沒有增減,但是順序打亂。

於是有下面的random代碼:(不給出核心代碼了,故意省略的,有心的根據上面的原理寫出來,“原理”寫的很清楚,借助rand庫函數,還猶豫什么,快點寫出來吧

 

// range:范圍 
// num:個數 
void random( int range, int num) 

     int * a =  new  int[range],i,j;
    srand(unsigned(time(NULL)));
     for(i= 0; i<range; i++) 
        a[i] = i +  1;
     ....核心代碼去哪里啦!
     //     寫入文件 
     for(i= 0; i<num; i++) 
        cout << a[i] <<  "   ";
     //     回收 
    delete [] a; 
}

 

 

有了上面的基礎,操刀就容易了。有下面的圖,多路歸並也就浮出水面
image 

其實,數據結構課程中的歸並排序就把原始數據分成了2個分隊(二路),並且它的所有工作只在內存中完成,不借助磁盤,另外二路更多采用遞歸算法的。上圖舉例:1kw的個整數,每個整數4B,規定內存只有1M,那么每次讀入內存(1MB/4B)=250k個整數,所以有(1kw/250k=40)個分隊,下面稱為“段文件”。

 

多路歸並排序編程實現細節

  • 內存排序環節:磁盤中的數據序列被分割成多個段(假定內存有限)讀入到內存中,在內存中用模板sort實現排序過程,效率高! 
  • 多路歸並排序環節:依次從從每個已排序的段文件中(什么是段文件,看上面的內存排序環節,形象了點!!)讀入一個數據,注意是一個;挑選最小的寫入的目標文件中。

 

數據的准備上面的random函數可以完全可以勝任。我測試的時候只准備了只有100w的數據,已經通過測試;小數據也可以通過,只是時間上有差異。

 

下面是代碼(不怎么喜歡貼代碼的,折疊起來)
View Code
#include <iostream>
#include < string.h>
#include <fstream>
#include <Algorithm>
#include <time.h>
using  namespace std;

#define MAX 10000                 //     總數據量,可修改
#define MAX_ONCE 2500         //     內存排序MAX_ONCE個數據
#define FILENAME_LEN 20    

// range:范圍
// num:個數
void random( int range, int num)
{
     int * a =  new  int[range],i,j;
    fstream dist_file;

    srand(unsigned(time(NULL)));

     for(i= 0; i<range; i++)
        a[i] = i +  1;

     //     打表預處理
     for(j= 0; j<range; j++)
    {
         //     rand函數產生的隨機數最大是32767,不能直接調用rand,做一下處理
         int ii = (rand()*RAND_MAX+rand()) % range,        
            jj = (rand()*RAND_MAX+rand()) % range;
        swap(a[ii],a[jj]);
    } //  for

    dist_file.open( " data.txt ",ios:: out);

     //     寫入文件
     for(i= 0; i<num; i++)
        dist_file << a[i] <<  "   ";

     //     回收
    delete [] a;
    dist_file.close();
}

bool cmp( int &a, int &b)
{ return a<b;}

// index:文件的下標
char * create_filename( int index)
{
     char * a =  new  char[FILENAME_LEN];
    sprintf(a, " data %d.txt ",index);
     return a;
}

// num:每次讀入內存的數據量
void mem_sort( int num)
{
    fstream fs( " data.txt ",ios:: in);
     int temp[MAX_ONCE],     //     內存數據暫存
        file_index =  0,             //     文件下標
        i,
        cnt;                             //     實際讀入內存數據量

     bool eof_flag =  false;     //     文件末尾標識

     while(!fs.eof())
    {
         for(i= 0,cnt =  0; i<MAX_ONCE; i++)
        {
            fs >> temp[cnt];

             //     讀入一個數據后進行判斷是否到了末尾
             if(fs.peek()==EOF)    
            {
                eof_flag =  true;
                 break;
            } //  if

            cnt++;
        } //  for

         if(eof_flag)
             break;

         //     內存排序
        sort(temp,temp+cnt,cmp);

         char * filename = create_filename(++file_index);
        fstream fs_temp(filename,ios:: out);

         //     寫入
         for(i= 0; i<cnt; i++)
            fs_temp << temp[i] <<  "   ";

        fs_temp.close();
        delete [] filename;
    } //  while

    fs.close();
}

void merge_sort( int filecnt)
{
    fstream * fs =  new fstream[filecnt],ret( " ret.txt ",ios:: out);
     int index =  1,temp[MAX_ONCE],eofcnt =  0;
     bool * eof_flag =  new  bool[filecnt];

    ::memset(eof_flag, false,filecnt* sizeof( bool));

     for( int i= 0; i<filecnt; i++)
        fs[i].open(create_filename(index++),ios:: in);

     for( int i= 0; i<filecnt; i++)
        fs[i] >> temp[i];

     while(eofcnt<filecnt)
    {    
         int j =  0;
         //     找到第一個未結束處理的文件
         while(eof_flag[j])
            j++;

         int min = temp[j],fileindex =  0;
         for( int i=j+ 1; i<filecnt; i++)
        {
             if(temp[i]<min && !eof_flag[i])
                min = temp[i],fileindex = i;
        } //  for

        ret << min <<  "   ";
        fs[fileindex] >> temp[fileindex];
         //     末尾判斷
         if(fs[fileindex].peek()==EOF)
            eof_flag[fileindex] =  true,
            eofcnt++;
    } //  while

    delete [] fs;
    delete [] eof_flag;
    ret.close();
}

int main()
{
    random(MAX,MAX);
    clock_t begin = clock();
    mem_sort(MAX);
    merge_sort( 4);
    clock_t end = clock();

     double cost = (end - begin)* 1.0 / CLK_TCK;
    cout <<  " 耗時: " << cost <<  " ms " << endl;
}

我的實現過程小瓶頸 

在用到c++的輸入輸出的流要特別注意,特別是輸入流,因為在內存排序環節和多路歸並排序環節都有涉及文件的讀入,分別是從源文件中讀入原始數據和從段文件中讀入數據。在這個實驗中,如果時沒處理好文件末尾的判斷,會出現小小的瑕疵(在合並文件的末尾會出現數據丟失或者重復)。
根據經驗最好的解決方法:每次處理文件數據之前,先讀入一個數據之后,進行下面的判斷,如果符合這個判斷就表示要結束接下來的處理。因為即使剛好到了文件末尾,輸入流也需要一個輸入的犧牲來判斷是否到了文件末尾,如果到了文件末尾,設置EOF標志。

 

if(fs.peek()==EOF)
{
    ...
     break;
} //  if

 

只要依准這一原則,問題可以得到解決。

 

問題分析,數據結構選擇,算法,和技巧 

1234章涉及了上面的話題,它決不能馬上讓你都有提高,重要的要在自己的學習中多加留意。比如第一章就提到的排序問題。想必很多人都會蹦出“外部排序”,但是卻恰有“位圖數據結構法”如此巧妙令人拍案叫絕的,這得益於問題的分析到位,數據結構選擇得當,...

 

一個新的小問題 

給出一篇文章,統計每個字符出現的次數!!問題不難,但是該怎么解決?

 

  • 分析:仔細看,字符無非是ascii表中的可打印的字符,數據結構選擇應該是一個struct,里面包含每個字符c和字符出現次數cnt。這個想法很中規中矩。
  • 仔細分析:字符的ascii值是固定而且是連續的,是不是可以仿照“位圖數據結構法”的方法?上面的“位圖數據結構法”用到了bool數組,但是只能記錄數據是否出現,也就是說對多次出現的數據無法判斷,所以把bool變為int就ok了,數組的下標就是每個ascii字符的值!

 

總結

有個壞習慣,一道題(可以是一道算法題或者具體的軟件),只要有思路,手就有癮似的放到鍵盤上,壞毛病,后來才發現。一個軟件的開發過程,代碼是時間只占了30%左右的時間,前面的架構才是重頭戲!所以編程始終是后話。試着在做題之前,在紙上分析問題,寫出偽代碼,然后才敲鍵盤,你會發現可以大大減少在電腦面前為某個編程細節而苦惱的時間。當然,你可以完全從“紙和筆”中完全解脫出來,代之以mspaint!

 

本文完 Thursday, March 29, 2012 

搗亂小子 http://daoluanxiaozi.cnblogs.com/
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM