• Nie Znaleziono Wyników

Sortowanie licznikowe

Jak wiemy, nie może istnieć algorytm sortowania oparty na porównaniach, który sortuje każdy ciąg n-elementowy wykonując mniej niż O(n log n) porównań. Jeśli jednak zbiór wartości kluczy można odwzorować z zachowaniem uporządkowania w ograniczony zbiór liczb całkowitych, to istnieje alternatywa, która umożliwia sortowanie w czasie proporcjonalnym do n.

Pomysł polega na „rozrzucaniu” sortowanych elementów do ponumerowanych

„kubełków” na podstawie wartości kluczy (nie ma tu porównań kluczy, zamiast tego na podstawie wartości klucza należy obliczyć numer kubełka). Na przykład, mając do posortowania obiekty, których klucze są słowami, możemy przygotować kubełki oznaczone literami alfabetu. Do pierwszego kubełka trafią wszystkie słowa

9.4

na literę „A”, do drugiego wszystkie na „Ą”, do trzeciego wszystkie na „B” itd.

Następnie w obrębie każdego kubełka możemy posortować słowa ze względu na drugą literę, itd. Ten pomysł jeszcze nie daje algorytmu działającego w czasie liniowym, ale nieco go zmodyfikujemy.

Posortujemy tablicę elementów, których klucze są liczbami całkowitymi — typu unsigned int, o którym założymy, że każdy element tego typu zajmuje cztery bajty (liczby typu unsigned char — ośmiobitowe; bajt jest najmniejszą jednostką pamięci adresowaną przez procesory komputerów PC), b0, . . . , b3, przy czym x = b0+ 256b1+ 2562b2+ 2563b3. Potraktujemy bajty jak cyfry rozwinięcia liczby x w układzie o podstawie 256. Aby mieć do nich dostęp, zdefiniujemy typ

typedef struct { union {

unsigned int x;

unsigned char b[4];

} k;

} typklucza;

Istotne założenie użyte w procedurze jest takie, że że w danym kluczu element b[0]jest najmniej znaczącym bajtem (tj. b0), a b[3] jest bajtem najbardziej znaczącym (b3). W związku z oparciem algorytmu o sposób przechowywania liczb w pamięci podprogram podany niżej może być nieprzenośny na komputery z procesorami niezgodnymi z PC, a także te, w których typ unsigned int jest zbiorem liczb 64-bitowych (to zależy od systemu operacyjnego, ewentualnie ustawień kompilatora).

Zasada działania algorytmu jest następująca: najpierw posortujemy elementy ze względu na najmniej znaczące bajty kluczy, tj. b0, następnie ze względu na bajty b1, b2i na końcu b3. Stabilność algorytmu sortowania (którą musimy zapewnić) spowoduje, że jeśli dwa klucze mają identyczne bajty bardziej znaczące, a różnią się tylko bajtami mniej znaczącymi, to po posortowaniu ze względu na mniej znaczące bajty kolejność elementów z tymi kluczami w tablicy będzie właściwa i podczas sortowania ze względu na bardziej znaczące bajty (które w obu kluczach są identyczne) kolejność ta nie zostanie zmieniona.

Użyjemy 256 kubełków. Numer kubełka, do którego wrzucimy element, jest oczywiście wartością odpowiedniego bajtu. Wariant sortowania kubełkowego zrealizowany przez podaną niżej procedurę nazywa się sortowaniem licznikowym.

Kubełek jest reprezentowany przez licznik elementów wrzuconych do niego.

Tablicę t indeksujemy od 0 do n − 1.

9.5

void CountSort ( element t[], unsigned int n ) {

unsigned int i, j, k;

unsigned int licznik[256];

element u[n];

if ( n < NMIN ) { ... /* posortuj przez wstawianie */ } else {

for ( j = 0; j < 4; j++ ) { /* od najmniej do najbardziej */

/* znaczących bajtów */

for ( k = 0; k < 256; k++ ) licznik[k] = 0;

/* skasuj liczniki */

for ( i = 0; i < n; i++ ) licznik[t[i].klucz.k.b[j]] ++;

/* teraz wiadomo, ile jest elementów w każdym kubełku */

for ( k = 1; k < 255; k++ ) licznik[k] += licznik[k-1];

/* teraz wartość każdego licznika wskazuje */

/* koniec obszaru, do którego trzeba */

/* wyrzucić zawartość odpowiedniego kubełka */

for ( i = n-1; i >= 0; i-- ) { k = t[i].klucz.k.b[j];

u[licznik[k]] = t[i];

licznik[k] --;

}

for ( i = 0; i < n; i++ ) t[i] = u[i];

} /* for ( j ... ) */

}

} /*CountSort*/

Do sortowania kubełkowego i w szczególności do podanej wyżej procedury odnoszą się następujące uwagi: czas jej działania jest proporcjonalny do sumy długości ciągu n i liczby kubełków — stąd sortowanie ciągów krótkich (krótszych niż odpowiednio dobrana stała NMIN, np. 30) metodą kubełkową nie opłaca się, bo lepszy wtedy jest algorytm sortowania przez wstawianie (który też jest stabilny).

Ponadto procedura sortowania korzysta z pamięci dodatkowej — oprócz tablicy liczników jest potrzebna tablica u, do której dane są przepisywane w odpowiedniej kolejności z tablicy t, a na końcu przenoszone z powrotem. Można (za pomocą dalszych trików, których opis tu pominę) sortować tablice według kluczy całkowitych nie tylko dodatnich, a także kluczy zmiennopozycyjnych.

9.6

Zadania i problemy

1. Czy algorytm MergeSort jest stabilnym algorytmem sortowania (tj. czy jeśli w tablicy pewne elementy mają identyczne klucze, to kolejność tych elementów nie może zostać zmieniona)?

2. Oblicz optymistyczną złożoność sortowania przez scalanie, dla tablicy o długości n = 2k.

3. Napisz procedurę sortowania pliku metodą scalania (aby otrzymać nazwy plików roboczych, wywołaj pewną funkcję typu char*, której dokładna treść jest tu nieistotna).

4. W tablicy a o dwóch indeksach, przyjmujących wartości od 0 do n, gdzie n = 2k− 1dla pewnego całkowitego k > 0, są liczby uporządkowane niemalejąco w każdym wierszu i w każdej kolumnie. Mamy następujący podprogram:

char Jest ( float x, int i0, int i1, int j0, int j1, int *i, int *j ) {

int k, l;

if ( i0+1 == i1 ) { /* wtedy również j0+1 == j1 */

/* zbadaj, czy któryś z czterech elementów, */

/* a[i0][j0], a[i0][j1], a[i1][j0], a[i1][j1], jest równy x */

/* jeśli tak, to przypisz zmiennym i, j jego indeksy, */

/* i zwróć wartość 1, w przeciwnym razie zwróć 0 */

...

} else {

k = (i0+i1) / 2; l = (j0+j1) / 2;

if ( x == a[k][l] ) { *i = k; *j = l; return 1; } else {

if ( Jest ( x, i0, k, l, j1, i, j ) ) return 1;

else if ( Jest ( x, k, i1, j0, l, i, j ) ) return 1;

else if ( x > a[k][l] ) return Jest ( x, k, i1, l, j1, i, j );

else return Jest ( x, i0, k, j0, l, i, j );

} } } /*Jest*/

Możemy wywołać procedurę Jest ( x, 0, n-1, 0, n-1, &i, &j ) w celu wyszukania w tablicy a liczby x i otrzymania jej pozycji (procedura przypisze zmiennym i i j odpowiednie indeksy), jeśli liczba ta jest obecna (procedura ma

9.7

wtedy wartość 1). Wyjaśnij sposób potraktowania zasady „dziel i zdobywaj”

w algorytmie realizowanym przez ten podprogram. Przyjmując za operację dominującą porównanie wartości parametru x z elementem tablicy, oblicz pesymistyczną złożoność tego algorytmu i podaj jej rząd.

5. Popraw procedurę z poprzedniego zadania tak, aby umożliwić wyszukiwanie liczb w tablicach o dowolnych wymiarach (a także w tablicach prostokątnych), oraz wyeliminować wielokrotne sprawdzania obecności liczby x w tej samej pozycji tablicy. Czy dokonana poprawka zmniejszyła rząd złożoności pesymistycznej?

10.1