上個月項目荷蘭大佬要檢查,搞的我想寫的東西不斷推遲,現在檢查完了,我決定繼續把我想寫的這整個一個系列寫完,上一次寫的是最簡單的無損編碼行程編碼,這一次我想要寫的是算術編碼。這種編碼的原理就是用一個數來代替一組數,我第一次看這個思想的時候深深的被這些大牛的思維方式所折服,用一個數代替一組數,這其實就是壓縮的最基本思想,雖然看起來是那么的遙不可及,但是在這種大的思想的指引下,總能開創出接近於完美的方法,所以我一直覺得一個人敢想,有主意,無論這個主意多么的不靠譜,都是應該的,因為你總能從一定的想法中找到合適的啟發,可惜的是,經過這么多年的教育,我經常都感覺自己的思想根本跳不出某一個圈子,很難獲得一些個啟發。
言歸正傳,算術編碼的原理簡單的說就是利用統計概率將一串數字表現出來,完整的算術編碼的實現是很復雜的,基本上可以寫一篇初級本科畢業論文,所以這里為了說明原理,我刪去了一些實現上的細節部分,但是總體原理是完整保存的下來的,下面是wiki連接介紹算術編碼的基本原理,太長了,復制了太占篇幅:
http://zh.wikipedia.org/wiki/%E7%AE%97%E6%9C%AF%E7%BC%96%E7%A0%81
簡單的說就是所有待壓縮的數字根據自己的出線概率在有限長度的數軸上按比例划出自己的地盤,最后得出一個精確值,看看這個值在位於哪兩個坐標之間,就說明待壓縮的原始數值是幾。比如現在待壓縮的數值有三個001,那么0出現的概率是0.67,1是0.33,於是在一個長度為1的數軸上分成兩個部分,一個是0到0.67,這個部分是0的管轄范圍,0.67到1是1的管轄范圍。那么在壓縮的過程中,依次讀入三個數,讀到第一個數,是0,應該在0管轄的地盤上選取一個數(如果你只壓縮一個數的時候),或者我們將目光投向0的地盤[0,0.67),繼續將這個地盤按照0.67:0.33的比例划分成兩個部分,依次這樣,直到沒有要壓縮的數,最后一定會得到一個區間,在這區間內選取一個數,這就是無損壓縮后的結果。
如果你對計算機編程有初步深入的認識的話,這里你應該意識到一個問題,計算機保存的浮點數精度是有限的,如果大型數據的話,那么最后壓縮出來的小數一定要求精度非常高,這個時候計算機本身的浮點數無法表示,這樣會導致無損變有損,這時候就需要你自己開發高精度的浮點表示方式,因為這次我只是為了說明算術編碼的原理和實現,我用的壓縮數據並不大,所以這個細節這次我並沒有做。但是這些算法和數據結構和東西我將在下一個系列中說明。
原理差不多了,下面就是實現的部分。算術編碼除了壓縮,還需要一個統計概率的預處理過程,這里我使用c++ stl庫的map來完成這個使命的。代碼如下:

1 map<char,double> GetProbability(string fileName) 2 { 3 ifstream fin(fileName.c_str()); 4 char input; 5 //vector<int> result; 6 map<char,int> tmp; 7 map <char, int>::iterator iter; 8 9 map <char, double> result; 10 11 int count=0; 12 13 while(!fin.eof()) 14 { 15 fin>>input; 16 if(fin.fail()) 17 break; 18 count++; 19 iter=tmp.find(input); 20 if(iter==tmp.end()){ 21 tmp.insert(map <char,int>::value_type(input,1)); 22 } 23 else{ 24 ++iter->second; 25 } 26 27 } 28 29 30 31 32 for (iter = tmp.begin(); iter != tmp.end(); iter++ ) 33 { 34 //count+=iter->second; 35 result.insert(map <char,double>::value_type(iter->first,iter->second/(double)count)); 36 } 37 38 39 fin.close(); 40 41 return result; 42 }
這里我覺得需要注意的一個小細節就是在文件流這一塊,在這個循環之中除了判斷文件流有沒有讀到最后一個字符(fin.eof())之外還加了一個fin.fail(),這是一個很容易被忽略的地方,因為eof()返回true時是讀到文件結束符0xFF,而文件結束符是最后一個字符的下一個字符。所以會造成一個現象是最后一個字符讀了兩次,這樣就導致最后計算出來的概率是不對的。當然這個問題有很多解決辦法,比如用file.peek()==EOF,這里采用的是看是否讀文件失敗,如果失敗直接退出。
統計出概率,下面要做的就是算術編碼壓縮了,其實現代碼如下:

1 1 double compress(map<char,double> m,string fileName) 2 2 { 3 3 double result=1.0; 4 4 vector<double> freqs; 5 5 ifstream fin(fileName.c_str()); 6 6 char input; 7 7 map <char, double>::iterator iter; 8 8 double pre=0.0; 9 9 freqs.push_back(pre); 10 10 11 11 double w=0.0,begin=0.0,end=1.0,length; 12 12 while(!fin.eof()) 13 13 { 14 14 fin>>input; 15 15 if(fin.fail()) 16 16 break; 17 17 int value=input-'0'; 18 18 w=0; 19 19 int k=0; 20 20 iter=m.begin(); 21 21 while (k<value) 22 22 { 23 23 w+=iter->second; 24 24 iter++; 25 25 k++; 26 26 } 27 27 length=end-begin; 28 28 end=begin+length*(w+iter->second); 29 29 begin+=length*w; 30 30 31 31 } 32 32 result=begin*0.01+end*0.99; 33 33 fin.close(); 34 34 return result; 35 35 36 36 }
原理很簡單,就是按照待壓縮符號的概率,不停地計算新的區間,最后保存為一個浮點數,再次說明的一點是,如果你需要開發完全的應用,你需要自己寫一個數據結構保存該浮點值。
解壓縮的原理就是和壓縮的代碼相反,不停地縮小區間,每次縮小區間都計算出相應的區間的兩個端點,然后判斷出是哪個信源符號(這里只有兩個信源,多個信源可以類推),代碼如下:

1 double tmpEnd=1.0; 2 void decompress(int length,double result,map<char,double> m) 3 { 4 double begin=0.0,end=1.0,tmp=0.0; 5 int n=0; 6 double valueLength; 7 map <char, double>::iterator iter; 8 vector<double> probs; 9 10 for (iter = m.begin(); iter != m.end(); iter++ ) 11 { 12 //count+=iter->second; 13 probs.push_back(iter->second); 14 // result.insert(map <char,double>::value_type(iter->first,iter->second/(double)count)); 15 } 16 17 for(int i=0;i<length;i++) 18 { 19 20 n=0; 21 tmp=0.0; 22 valueLength=end-begin; 23 while(result-begin>tmp*valueLength) 24 { 25 tmp+=probs[n++]; 26 27 } 28 29 30 n--; 31 end=begin+tmp*valueLength; 32 if(end==tmpEnd) 33 begin=end-probs[n]*valueLength; 34 // begin=end-probs[n]*valueLength); 35 //cout<<begin<<" "<<end<<endl; 36 tmpEnd=end; 37 cout<<n<<" "; 38 } 39 cout<<endl; 40 41 }
我使用的測試文件如下:
里面有15個信源符號,執行程序,得出的結果如下:
第一行是0出現的概率,第二行是1出現的概率,第三行是驗證它們加起來等於,第四行是壓縮得到的浮點數結果,第五行是解壓縮后的數值,可以看到是無損的。
最后,附上測試代碼:

1 int _tmain(int argc, _TCHAR* argv[]) 2 { 3 string fileName="input.txt"; 4 map <char, double>::iterator iter; 5 map<char,double> result=GetProbability(fileName); 6 double sum=0.0; 7 for (iter = result.begin(); iter != result.end(); iter++ ) 8 { 9 cout<<iter->first<<" "<<iter->second<<endl; 10 sum+=iter->second; 11 } 12 cout<<sum<<endl; 13 cout<<(sum=compress(result,fileName))<<endl; 14 15 decompress(15,sum,result);//第一個參數待壓縮集合長度,第二個參數壓縮后的浮點值,第三個參數概率集合 16 int i; 17 cin>>i; 18 return 0; 19 }
好了,算術編碼寫完了,在我的計划中,下一步是要寫霍夫曼編碼,但是由於要設計的樹的結構,我寫這全部的文章的目的是讓初學者可以通過程序實際實現各種看似枯燥的算法,既然要用到樹等數據結構,所以我決定我下一步先把你所能用到的數據結構寫完,9,10月我會更新的比較勤快的,也希望各路高手能給我提出意見。