• Nie Znaleziono Wyników

Do implementacji kodowania potrzebny jest jeden element „techniczny”, mianowicie możliwość pisania do pliku i czytania z pliku pojedynczych bitów.

Najmniejszą jednostką pamięci, którą można jednorazowo przesyłać w większości systemów komputerowych, jest jeden bajt, tj. zespół ośmiu bitów. Ponieważ chcemy dokonać kompresji, konieczne jest „pakowanie” bitów w bajt, oraz ich

„rozpakowywanie”.

15.7

Rozwiązanie problemu polega na zastosowaniu bufora, tj. zmiennej pomocniczej, w której możemy gromadzić kolejno wyprowadzane bity, i którą po zapełnieniu możemy wyprowadzić do pliku i „opróżnić”. Różne bufory mają znacznie więcej zastosowań; system operacyjny tworzy odpowiedni bufor dla każdego pliku otwartego przez program, ponieważ dane są czytane i pisane na dysku „porcjami”

znacznie większymi niż jeden bajt — zwykle to jest od 512 bajtów do kilku kilobajtów (obsługa buforów systemowych jest niewidoczna dla programów). Do obsługi buforów bitowych odpowiednich na potrzeby kodowania i dekodowania, możemy użyć następujących podprogramów:

FILE *f;

unsigned char bufor, maska;

char koniec; /* zmienna koniec jest potrzebna podczas czytania */

void ZacznijPisanie ( char nazwa[] ) {

f = fopen ( nazwa, "w+");

maska = 1;

bufor = 0; /* ustaw na zero wszystkie 8 bitów */

} /*ZacznijPisanie*/

void ZapiszBit ( char b ) {

if ( b ) bufor += maska;

maska *= 2;

if ( !maska ) { /* maska == 0 po wystąpieniu nadmiaru */

/* czyli po zapisaniu w buforze ośmiu bitów */

fwrite ( &bufor, 1, 1, f );

bufor = 0;

maska = 1;

}

} /*ZapiszBit*/

void ZakonczPisanie ( void ) {

if ( maska != 1 )

fwrite ( &bufor, 1, 1, f );

fclose ( f );

} /*ZakonczPisanie*/

15.8

void ZacznijCzytanie ( char nazwa[] ) {

f = fopen ( nazwa, "r+");

maska = koniec = 0;

} /*ZacznijCzytanie*/

char CzytajBit ( void ) {

char bit;

if ( !maska ) {

if ( !fread ( &bufor, 1, 1, f ) ) koniec = 1;

else maska = 1;

}

bit = bufor & 1;

bufor /= 2;

maska *= 2;

return bit;

} /*CzytajBit*/

void ZakonczCzytanie ( void ) {

fclose ( f );

} /*ZakonczCzytanie*/

Wyjaśnienie, jak to działa: typ char jest typem liczbowym całkowitym; liczby tego typu są przechowywane w bajtach, tj. w ośmiobitowych komórkach pamięci.

Zmienna maska kolejno przyjmuje wartości 0, 1, 2, 4, 8, 16, 32, 64, −128, a potem to samo od początku; z wyjątkiem zera każda z tych liczb jest reprezentowana przez ciąg bitów, w którym jest 7 zer i jedynka — dla kolejnych liczb ta jedynka znajduje się na kolejnej pozycji w bajcie. W każdym wywołaniu procedury ZapiszBitwartość zmiennej maska jest mnożona przez 2, przy czym wynik mnożenia 2 · 64 z powodu nadmiaru jest równy −128, a wynik mnożenia 2 · −128 jest równy 0. W ten sposób w zmiennej maska zawsze mamy tylko 1 bit niezerowy.

Jeśli parametr b ma wartość niezerową (czyli wypisujemy bit o wartości 1), to wykonujemy dodawanie bufor += maska;, co powoduje przypisanie wartości 1 odpowiedniego bitu w buforze. Jeśli maska ma wartość −128, to po pomnożeniu przez 2 następuje nadmiar, wskutek czego maska otrzymuje wartość 0. Wskazuje to, że bufor został zapełniony (wpisaliśmy do niego 8 bitów), w związku z czym należy go wypisać do pliku i wyczyścić. Kasujemy wszystkie bity, przez

15.9

przypisanie bufor = 0;, a następnie ustawiamy maskę tak, aby jej najmniej znaczący bit (i tylko on) miał wartość 1.

Procedura ZakonczCzytanie tylko zamyka plik, z którego były odczytywane bity, natomiast procedura ZakonczPisanie musi wykonać jeszcze jedną czynność, tj.

opróżnienie bufora. Rzecz w tym, że jeśli bufor nie jest pełny, to zgromadzone w nim bity też trzeba wypisać, bez czego zakodowany ciąg byłby niekompletny.

Ale to prowadzi do następnego problemu. Podczas czytania nie wiadomo, ile bitów ostatniego bajtu w pliku należy do zakodowanego ciągu, a ile z nich tylko

„dopełnia” ostatni bajt. Można by sobie z tym poradzić, dostarczając osobno informację o długości (liczbie bitów) zakodowanego ciągu. Innym sposobem jest rozszerzenie zbioru s0, . . . , sn−1 o jeszcze jeden element, który oznacza koniec ciągu. Elementowi temu trzeba przyporządkować odpowiedni kod, który dołączymy na końcu zakodowanego ciągu.

Zmienna koniec służy do zasygnalizowania, że wszystkie bajty z pliku zostały przeczytane. Po wywołaniu funkcji CzytajBit powinniśmy zbadać, czy zmienna ta ma wartość niezerową, co oznacza, że wartość funkcji nie opisuje bitu

należącego do ciągu. Niestety, zanim zmienna koniec otrzyma wartość 1, możemy odczytać nawet 7 bitów „dołożonych” do ciągu. Właśnie dlatego trzeba zastosować jeden z opisanych wyżej sposobów przekazania informacji o długości ciągu bitów.

W procedurze CzytajBit maska działa podobnie jak w ZapiszBit, ale jest używana tylko jako licznik bitów odczytanych z bufora. Odczytanie bitu z bufora polega na zbadaniu, czy wartość zmiennej bufor jest nieparzysta (jeśli tak, to odczytujemy bit 1, w przeciwnym razie 0; służy do tego operator &, który umożliwia sprawdzenie, czy dany bit (w tym przypadku najmniej znaczący) zmiennej bufor ma wartość 1. Po odczytaniu bitu dzielimy wartość zmiennej buforprzez 2 (z odrzuceniem reszty), co „przesuwa” następny bit do odczytania na najmniej znaczącą pozycję. Po odczytaniu ośmiu bitów czytamy kolejny bajt z pliku (konieczność czytania jest sygnalizowana przez wartość 0 zmiennej maska).

Procedury fwrite i fread (których nagłówki są zamieszczone w pliku stdio.h) służą do pisania i czytania plików, przy czym dane w plikach są przechowywane w postaci binarnej (bez konwersji liczb na ciągi cyfr). Parametry tych procedur opisują wskaźnik bufora (tj. adres obszaru pamięci z danymi do zapisania lub miejsca, do którego przeczytane dane należy wpisać), wielkość elementu danych (w podanych procedurach 1 bajt), liczbę elementów (tu też 1) i wskaźnik struktury typu FILE, opisującej plik. Wartości zwracane przez te procedury to odpowiednio liczba zapisanych albo przeczytanych elementów (wartość 0 procedury fread oznacza błąd czytania lub dojście do końca pliku, co wykorzystujemy w procedurze czytania bitu).

15.10

Zadania i problemy

1. Użyj drzewa Huffmana przedstawionego na wykładzie do rozkodowania ciągu bitów 11000110001000001000001001101011101.

2. Opisz, jak można zbudować drzewo Huffmana przy użyciu jednej kolejki priorytetowej, zamiast dwóch kolejek, których sposób użycia był na wykładzie.

Który sposób działa szybciej (należy zbadać liczby potrzebnych porównań wag w obu sposobach)? Czy sposób szybszy ma mniejszy rząd złożoności

obliczeniowej?

3. Napisz w C procedurę budowania drzewa Huffmana (na podstawie tablicy prawdopodobieństw wystąpienia poszczególnych znaków; typem indeksu tej tablicy jest char). Skorzystaj w tym celu z gotowych procedur sortowania i obsługi kolejek (należy napisać tylko nagłówki procedur i w napisanym kodzie odpowiednio je wywoływać).

4. Napisz procedury kodowania i dekodowania przy użyciu drzewa kodu. Do wprowadzania i wyprowadzania kolejnych bitów użyj procedur przedstawionych na wykładzie.

5. Co można powiedzieć o drzewie Huffmana i o efektywności kodu, jeśli wszystkie znaki występują w ciągu z jednakowym prawdopodobieństwem?

16.1