algorytmy i struktury danych

58
ALGORYTMY i STRUKTURY DANYCH Dr inŜ. Dokimuk Jerzy KAE (K-26), gmach A11, IVp. pok. 409, tel. 042-631-2666 [email protected] 1. Pojęcie algorytmu: klasyfikacje, metody zapisu, operacje dominujące, przyklady. 2. Analiza algorytmów: pojęcie zloŜoności, notacje, klasa algorytmu, opis matematyczny. 3. Rekurencja: definicje, problemy, typy rekurencji. 4. Projektowania algorytmów: dziel i zwycięŜaj, programowanie dynamiczne, algorytmy zachlanne, algorytmy z powrotami. 5. Sortowanie: przez wstawianie, bąbelkowe, Shella, QuickSort, przez zliczanie, pozycyjne, kubelkowe, i inne, drzewa decyzyjne. 6. Wyszukiwanie wzorca: Brute Force, Knutha-Morrisa-Pratta, Rabina-Karpa. 7. Haszowanie: idealna funkcja haszująca, kolizje, haszowanie lańcuchowe. 8. Abstrakcyjne struktury danych: stos, kolejka, lista, przyklady implementacji i zastosowań. 9. Drzewa: binarne, BST, wstawianie/usuwanie kluczy, przechodzenie, równowaŜenie, algorytm DSW, drzewa AVL. 10. Kopiec: operacje, implementacja, sortowanie przez kopcowanie, zastosowanie. 11. B–drzewa: tworzenie, wstawianie i usuwanie kluczy. 12. Grafy: definicje, przeszukiwanie BFS/DFS, algorytm Kruskala, Dijkstry. 13. Wstęp do kompresji danych: kodowanie prefiksowe, metoda Huffmana. 14. Zarys Problemów NP - zupelnych L I T E R A T U R A [1] Cormen T. H., Leiserson C. E., Rivest R. L.: Wprowadzenie do algorytmów. WNT, Warszawa, 2006. [2] Drozdek A.: C++. Algorytmy i struktury danych. Helion, Gliwice, 2004. [3] Neapolitan R., Naimipour K.: Podstawy algorytmów z przykladami w C++. Helion, Gliwice, 2004. --------------------------------------------------------------------------------------------------------------------------- [4] Harris S., Ross J.: Od podstaw Algorytmy. Helion, Gliwice, 2006. [5] Adamski T., Ogrodzki J.: Algorytmy komputerowe i struktury danych. PW, Warszawa 2005 [6] Drozdek A., Simon D. L.: Struktury danych w języku C. WNT, Warszawa, 1996. [7] Sedgewick R.: Algorytmy w C++. RM, Warszawa, 1999. [8] Sedgewick R.: Algorytmy w C++. Grafy. RM, Warszawa, 2003. [9] Aho Alfred V., Hopcroft John E., Ullman Jeffrey D.: Projektowanie i analiza algorytmów. Helion, Gliwice, 2003 (1974). [10] Wróblewski P.: Algorytmy, struktury danych i techniki programowania. Helion, Gliwice, 2003. [11] Wirth N.: Algorytmy+struktury danych=programy. WNT, Warszawa, 1989. [12] Banachowski L., Diks K., Rytter W.: Algorytmy i struktury danych. WNT, Warszawa, 1996. [13] Sysło M. M.: Algorytmy. Wydawnictwa szkolne i pedagogiczne, Warszawa, 1997.

description

ALGORYTMY i STRUKTURY DANYCHDr inŜ. Dokimuk Jerzy

Transcript of algorytmy i struktury danych

Page 1: algorytmy i struktury danych

ALGORYTMY i STRUKTURY DANYCH

Dr inŜ. Dokimuk Jerzy KAE (K-26), gmach A11, IVp. pok. 409, tel. 042-631-2666

[email protected]

1. Pojęcie algorytmu: klasyfikacje, metody zapisu, operacje dominujące, przykłady. 2. Analiza algorytmów: pojęcie złoŜoności, notacje, klasa algorytmu, opis matematyczny.

3. Rekurencja: definicje, problemy, typy rekurencji. 4. Projektowania algorytmów: dziel i zwycięŜaj, programowanie dynamiczne, algorytmy

zachłanne, algorytmy z powrotami. 5. Sortowanie: przez wstawianie, bąbelkowe, Shella, QuickSort, przez zliczanie, pozycyjne,

kubełkowe, i inne, drzewa decyzyjne. 6. Wyszukiwanie wzorca: Brute Force, Knutha-Morrisa-Pratta, Rabina-Karpa.

7. Haszowanie: idealna funkcja haszująca, kolizje, haszowanie łańcuchowe.

8. Abstrakcyjne struktury danych: stos, kolejka, lista, przykłady implementacji i zastosowań. 9. Drzewa: binarne, BST, wstawianie/usuwanie kluczy, przechodzenie, równowaŜenie,

algorytm DSW, drzewa AVL. 10. Kopiec: operacje, implementacja, sortowanie przez kopcowanie, zastosowanie. 11. B–drzewa: tworzenie, wstawianie i usuwanie kluczy. 12. Grafy: definicje, przeszukiwanie BFS/DFS, algorytm Kruskala, Dijkstry. 13. Wstęp do kompresji danych: kodowanie prefiksowe, metoda Huffmana. 14. Zarys Problemów NP - zupełnych

L I T E R A T U R A

[1] Cormen T. H., Leiserson C. E., Rivest R. L.: Wprowadzenie do algorytmów. WNT, Warszawa, 2006.

[2] Drozdek A.: C++. Algorytmy i struktury danych. Helion, Gliwice, 2004.

[3] Neapolitan R., Naimipour K.: Podstawy algorytmów z przykładami w C++. Helion, Gliwice, 2004.

--------------------------------------------------------------------------------------------------------------------------- [4] Harris S., Ross J.: Od podstaw Algorytmy. Helion, Gliwice, 2006.

[5] Adamski T., Ogrodzki J.: Algorytmy komputerowe i struktury danych. PW, Warszawa 2005

[6] Drozdek A., Simon D. L.: Struktury danych w języku C. WNT, Warszawa, 1996.

[7] Sedgewick R.: Algorytmy w C++. RM, Warszawa, 1999.

[8] Sedgewick R.: Algorytmy w C++. Grafy. RM, Warszawa, 2003.

[9] Aho Alfred V., Hopcroft John E., Ullman Jeffrey D.: Projektowanie i analiza algorytmów. Helion, Gliwice, 2003 (1974).

[10] Wróblewski P.: Algorytmy, struktury danych i techniki programowania. Helion, Gliwice, 2003.

[11] Wirth N.: Algorytmy+struktury danych=programy. WNT, Warszawa, 1989.

[12] Banachowski L., Diks K., Rytter W.: Algorytmy i struktury danych. WNT, Warszawa, 1996.

[13] Sysło M. M.: Algorytmy. Wydawnictwa szkolne i pedagogiczne, Warszawa, 1997.

Page 2: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 1 Instytut Aparatów Elektrycznych Algrm1 Dr J. Dokimuk

1. POJĘCIE ALGORYTMU

Pojęcie algorytm związane jest z nazwiskiem perskiego matematyka Muhammada Ibn Musa Al Chorezmi (IX w), który podał reguły wykonywania operacji arytmetycznych na liczbach dziesiętnych. Jego praca została w XII wieku przetłumaczona na łacinę.

Pierwszym algorytmem był przepis znajdowania największego wspólnego podzielnika /NWD/ dwóch liczb naturalnych a i b, podany przez Euklidesa (365 -300 p.n.e.).

Algorytm : uporządkowany zbór operacji, których wykonanie udostępnia rozwiązanie zadania z określonej klasy zadań.

Algorytm jest ściśle określoną procedurą obliczeniową, która dla właściwych danych Wejścio-wych generuje oczekiwane dane Wyjściowe (zwane teŜ wynikami).

Rozwiązanie określonego problemu obliczeniowego polega na sprecyzowaniu wyma-gań odnośnie relacji danych Wejściowych i Wyjściowych.

Algorytm opisuje stosowną technikę obliczeniową, która realizuje relację We/Wy.

Specyfikacja: dokładny opis zadania lub problemu do rozwiązania, obejmujący:

Dane: dla postawionego zadania i warunki, które muszą spełniać,

Wyniki: z uwzględnienie relacji do danych np.:

≤>

=0dla

0dla)(),()( 21

xError

xxaxaxf

L

Wskazane jest zamieszczenie pełnej specyfikacji problemu przed podaniem algorytmu.

Opracowując algorytm zakłada się poziom szczegółowości, będący podstawą algorytmu.

Na wstępie naleŜy sprawdzić, czy zadanie jest jednoznaczne rozwiązywalne. Algorytm powinien uwzględniać wszystkie moŜliwe teoretyczne warianty obliczeń, wynikające z róŜnorodności danych Wejściowych.

Cztery cechy algorytmu

❶❶❶❶ Ogólność: powinien rozwiązywać zadania określonej klasy a nie pojedyncze przypadki. ❷❷❷❷ Skończoność: uzyskanie rozwiązania zadania w skończonej liczbie kroków.

Skończoność algorytmu warunkowana jest jednoznacznie podanym warunkiem zakończenia p ętli iteracyjnej jak teŜ prawidłowym zdefiniowa-niem ciągu wywoła ń rekurencyjnych . MoŜe być badana w trakcie testowania programu (czy program zatrzymuje się na danych testowych).

❸❸❸❸ Określoność: kaŜdy krok algorytmu musi być jednoznacznie określony.

❹❹❹❹ Efektywność: czas wykonania (np. liczba operacji) lub zapotrzebowanie na pamięć.

Poprawność: naleŜy dowie ść Ŝe (cechy dowodu twierdzeń matematycznych): –kaŜde wykonanie algorytmu dla danych zawartych w specyfikacji DANE, które

dobiegnie do końca, udostępni wynik, spełniający warunek końcowy (podany w specyfikacji jako Wynik ).

–dla kaŜdych poprawnych danych obliczenia w algorytmie skończą się.

Poprawność algorytmu wiąŜę się z poprawnością działania programu, będącego jego realizacją.

W programie mog ą wyst ąpić błędy: składniowe lub logiczne. Błędy logiczne sprawiają, Ŝe dla poprawnych danych wejściowych program generuje złe wyniki.

Najgorszymi błędami są błędy w algorytmach, automatycznie przenoszone do programów.

Algorytm szeregowy : wykonuje instrukcje sekwencyjnie, jedna po drugiej, wg kolejności określonej grafami przepływu tzn.

i n∈∀[ , ]1

t(koniec( oi )) # t(początek ( oi+1 )). (∀∀∀∀ czytaj: dla kaŜdego)

Oznaczenia: n –liczba operacji w algorytmie; oi - i-ta operacja; t(x) –chwila wykonania operacji x. Kolejność wykonywania operacji ponumerowana jest od 1 do n włącznie.

Algorytm równoległy: moŜliwość wykonywania jednocześnie róŜnych operacji (instrukcji) w tej samej chwili czasowej tzn.

i n∈∃[ , ]1

t(koniec( oi )) > t(początek ( oi+1 )). (∃∃∃∃ – czytaj: istnieje)

Algorytm: liniowy (bezwarunkowa lista kroków), -z rozgał ęzieniami (spełnienie warunków określa kolejność).

Algorytmy i Struktury Danych – wykład 2 Instytut Aparatów Elektrycznych Algrm1 Dr J. Dokimuk

1.1. Metody zapisu algorytmu

➀➀➀➀ Język naturalny -prostota, obszerne słownictwo, mała precyzja, moŜliwość błędnej interpretacji.

Algorytmu testowania parzysto ści liczby nieujemnej: Zmniejsz jej wartość o 2 tak długo, aŜ otrzymasz wartość 0 lub 1. Wynik 0 oznacza liczbę parzystą zaś 1 liczbę nieparzystą.

➁➁➁➁ Schemat blokowy Graficzny, sformalizowany zapis algorytmu, trudności w fizycznej prezentacji duŜych problemów

Wykorzystuje się zazwyczaj następujące bloki:

początek i koniec algorytmu;

operacyjny –zawiera opisy operacji na danych;

decyzyjny –określa kierunek sterowania.

➂➂➂➂ Funkcje rekurencyjne – gdy problem moŜna przedstawić za pomocą zapisu rekurencyjnego.

➃➃➃➃ Języki formalne lub pseudojęzyki –język programowania lub specjalnie stworzona notacja.

➄➄➄➄ Lista kroków –kolejne kroki zawierają opis czynności wykonywanych przez algorytm.

Poszukiwanie elementu w wektorze Dane: wektor X[p…k] ze zbiorem elementów; wartość poszukiwanego element key. Wynik: wskaźnik m określający pozycję elementu w X (m= -1 jeŜeli brak elementu w X)

Krok 0 : Wprowadź elementy do wektora X //moŜna pominąć Krok 1 : xl+1 = key //wstawienie wartownika na koniec Krok 2 : i = p Krok 3 : Czy xi = key?, jeŜeli tak to KONIEC, jeŜeli nie idź do 4 Krok 4 : i = i + 1 idź do 3 KONIEC: // JeŜeli m=l+1 to m = -1 co oznacza, Ŝe wektor X nie zawiera elementu key

1. pętle iteracyjne while - do , for - do , repeat , instrukcje warunkowe if , then , else 2. // początek komentarza aŜ do końca wiersza 3. ab; oznacza przypisanie wartości wyraŜenia z prawej strony do lewej strony. 4. Dopuszczalne są podstawienia wielokrotne abc, 5. uŜyte zmienne są lokalne w procedurze (zmienne globalne zaznaczane są słowem global ), 6. A[i] jest i-tym element tablicy A; A[3…k] jest podtablicą złoŜoną z elementów A[3] ÷ A[k], 7. długość tablicy określa się pisząc length (A), 8. parametry do procedury przekazywane są przez wartość. 9. strukturę blokową sygnalizuje się poprzez wci ęcia .

START

dane wejsciowe

operacjazwieksz

warunek KONIEC

N T

R1.1

≥−+−<

=2dla122dla

n) Fib(n)Fib(n

n n Fib(n)

>−⋅=

=0dla )!1(

0dla 1!

nnn

nn

Liniowe przeszukiwanie wektora Dane: wektor X[1..n] zawierający n liczb; key -poszukiwany Wynik: zmienna OK zwracająca indeks lub -1 gdy brak klucza i 1 X[n+1] key // wartownik while X[i] <> key do i i + 1 if ( i ≤ n ) then OK i else OK –1

Location(int n, type a, X[ ] ) index loc= 1; while (loc <=n && X[loc] != a)

loc ++; if (loc > n) return -1; else return loc;

Page 3: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 3 Instytut Aparatów Elektrycznych Algrm1 Dr J. Dokimuk

➅➅➅➅ Algorytm drzewiasty (drzewo decyzyjne) Forma schematu blokowego, przyjmująca postać drzewa. KaŜde dwie drogi mają tylko wspólny początek i dalej nie spotykają się. Przydatny do wyznaczania liczby wykonywanych operacji.

Elementy drzewa: korzeń –wierzchołek rozpoczynający algorytm, gałęzie –połączenia w drzewie (krawędzie), liście –wierzchołki końcowe, odpowiadają wynikom działania algorytmu.

Sortowanie 3–elementowego zbioru X = x1, x2, x3 1-sze porównanie x1<x2 sortuje podzbiór x1, x2.

Następnie wstawiamy x3 w odpowiednie miejsce względem x1 i x2.

Wynik sortowania: stan (1a i 1b) po 2-ch porównaniach pozostałe: po 3-ch porównaniach

Wysoko ść drzewa: największa odłegłość od korzenia do wierzchołka końcowego, jest to te Ŝ najwi ększa liczba operacji wykonanych w algorytmie .

Dla algorytmu sortującego (przez porównanie) n róŜnych kluczy, drzewo decyzyjne zawiera n! liści. Projektowanie algorytmu nie jest czynnością prostą, gdyŜ brak jest uniwersalnych

reguł jego tworzenia Jedną z reguł jest upraszczanie algorytmu tak, aby uzyskać maksymalnie prostą, krótką i czytelną postać, w której łatwo będzie wyodrębnić niezale Ŝne fragmenty .

1.2. Pojęcie złoŜoności obliczeniowej ZłoŜoność obliczeniowa algorytmu: liczba zasobów komputerowych, potrzebnych do jego wykonania.

ZłoŜoność pesymistyczna –liczba zasobów komputerowych potrzebnych przy ”najgorszych” danych wejściowych o określonym rozmiarze n.

ZłoŜoność oczekiwana –liczba zasobów komputerowych potrzebnych dla ”typowych ” danych wejściowych o określonym rozmiarze n.

Rozmiar danych wejściowych nie jest pojęciem jednoznacznym i zaleŜy od problemu. Dla sortowania rozmiarem danych jest liczba elementów w zbiorze do posortowania.

W analizie efektywności mnoŜenia liczb całkowitych, najbardziej wiarygodnym rozmiarem jest liczba bitów potrzebnych do przechowywania tych liczb.

Równolegle z pojęciem złoŜoność obliczeniowa, uŜywane jest pojęcie pokrewne: -efektywność algorytmu, odnoszące się do złoŜoności w sensie praktycznym.

Występuje gdy porównujemy róŜne algorytmy rozwiązujące ten sam problem lub przy praktycznej ocenie przydatności określonych algorytmów.

Algorytm A1 jest efektywniejszy od A2 chociaŜ oba maja złoŜoność obliczeniową klasy O(n2). Efektywność algorytmu: czas jego wykonania oraz zapotrzebowanie na pamięć operacyjną. Efektywność czasowa: liczba wykonań poszczególnych operacji algorytmu, niezaleŜnie od

zastosowanego komputera czy środowiska programistycznego.

Czas wykonania algorytmu jak i jego zapotrzebowanie na pamięć zaleŜą od: -rozmiaru, -rodzaju danych wejściowych.

Stosując ulepszoną metodę sortowania bąbelkowego dla dwóch zbiorów danych: [1,5,8,9,3] oraz [9,8,5,3,1], szybciej będzie posortowana lista pierwsza

ZłoŜoność czasowa powinna być niezaleŜna od komputera, języka programowania lub sposobu kodowania, dlatego wyróŜnia się w algorytmie charakterystyczne operacje zwane operacjami dominującymi, których łączna liczba jest proporcjonalna do liczby wykonań wszystkich operacji jednostkowych dla dowolnej realizacji algorytmu.

Dla algorytmów sortowania operacjami dominującymi są: –porównywanie elementów, –przestawianie elementów.

Dla algorytmów numerycznych , operacje dominujące to operacje arytmetyczne +, -, * /. Jednostka złoŜoności czasowej: czas wykonania jednej operacji dominującej.

Algorytm optymalny: bezwzględnie najlepszy algorytm (z dostępnych), dla danego problemu.

R1.2

Rys.1.2. Przykład drzewa decyzyjnego

Algorytmy i Struktury Danych – wykład 4 Instytut Aparatów Elektrycznych Algrm1 Dr J. Dokimuk

1.3. Przykłady

Przykład 1.1. Schemat Hornera (1819 r.) - wartość wielomianu n-go stopnia w zadanym punkcie.

f(n,x) = anxn + an-1x

n-1 + …+ a2x2 + a1x + a0

f(n,x) = (…((anx + an-1)x + an-2)x + … + a1)x + a0

Dane: a0, a1,…,an –współczynniki; z – argument. Wynik : wartość wielomianu dla zadanego argumentu.

g(n,x) = (b0x

n-1 + b1xn-2 + …+ bn-1)x + bn = g(n-1,x)⋅x + bn

Przykład 1.2. Maksymalna wartość wektora. y = max xi dla i ∈ [1, n]

ZłoŜoność: liczba porówna ń Pr = n - 1 Przykład 1.3. Rozpiętość wektora.

Rozpiętość: r = maxxi - minxi dla i ∈ [1, n].

Dane: wektor X zawierający n liczb. Wynik: rozpiętość wektora X.

1. połącz elementy wektora w pary, 2. porównaj elementy w parach, 3. wartości mniejsze umieść w wektorze Xmin, 4. wartości większe umieść w wektorze Xmax, 5. znajdź max w wektorze Xmax jak poprzednio, 6. znajdź min w wektorze Xmin jak poprzednio.

ZłoŜoność: liczba porównań dla n parzystego: Pr = n/2 + 2*(n/2 - 1) = 1.5n - 2

Dla liniowego poszukiwania liczba porównań: Pr = 2 * (n - 1) = 2n – 2 (lub 2n - 3)

w = an i = n - 1,…,1, 0 w = w⋅⋅⋅⋅z + ai f(z) = w

procedure MaxVec (n: integer; var X:vec; var Max:real;); var i: integer; max: real; begin max := X[1] for i:=2 to n do if X[i] > max then max:=X[i] Max:=max; end ;

8

78

86 57>

> > >>

>

>

Rys. P1.3. Algorytm poszukiwania maksimum

Dane: wektor X zawierający n liczb Wynik: największy element w wektorze X 1. przyjmij za max 1-szy element w zbiorze X; 2. dla wszystkich kolejnych elementów wykonuj czynności:

jeŜeli i-ty element > max to za max przyjmij ten element

1. przyjmij współczynnik an za wartość początkową, 2. bieŜącą wartość wielomianu pomnóŜ przez z, 3. dodaj kolejny współczynnik wielomianu,

4. powtarzaj 2÷3 do wyczerpania lwspółczynników.

Rys. P1.2. Wartość maksymalna

// zapis w pseudokodzie max X[1] fo r i 2 to n do

if X[i] > max then max X[i]

MinMax(int n; type X[ ], min, max) index i; min = max =X[1]; for (i = 2; i <= n; i++) if (X[i] < min) min = X[i]; else if (X[i] > max) max = X[i];

f n x,( ) w an←

w w x⋅ ai+←

i n 1− 0..∈for

f w←

:=

g n x,( ) bn n 0if

g n 1− x,( ) x⋅ bn+ n 0>if

:=

Page 4: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 5 Instytut Aparatów Elektrycznych Algrm1 Dr J. Dokimuk

Przykład 1.4. Miejsce zerowe funkcji ciągłej jednej zmiennej. Dane: przedział (a, b), zawierający jedno miejsce zerowe. Wynik : wartość, dla której funkcja przyjmuje wartość zero.

Przykład 1.4a

Przykład 1.5. MnoŜenie dwóch macierzy.

Dane: Macierze A [n x m] i B [m x r], (liczba kolumn macierzy A musi być równa liczbie wierszy macierzy B).

Wynik : C[n x r] = A [n x m] * B [m x r], gdzie ∑ ⋅==

m

kjkkiji bac

1,,, , dla i=1,2,...,n ; j=1,2,...r.

Przykład: jjiiij babaaccccc

bbbbbb

aaaaaa

3132212221

1211

3231

2221

1211

232221

131211 gdzie ++=

=

MultiplyMat(A, B) if col[A] ≠ row[B] then return Error ”Błąd wymiarów” for i 1 to row[A] do

for j to col[B] do C[i,j] 0 for k 1 to col[A] do C[i,j] C[i,j]+A[i,k]@B[k,j]

return C

procedure macMult(m n,r: integer; var A, B, C:mac); var i, j, k: integer; begin for i:=1 to m do for k:=1 to r do begin C[i,k]:=0; for j:=1 to n do C[i,k] := C[i,k] + A[i,j]*B[j,k]; end ; end ;

j=1

ST

AR

T

i>m

cij=0

k=1

cij=cij+aijbjk

k>r

j>n

i=i+1NT

k=k+1N

j=j+1T

NT

KONIEC

i=1

Rys.P1.5. Algorytm mnoŜenia macierzy

jeŜeli wartość funkcji na jednym z krańców przedziału jest równa zero to wartość ta jest rozwiązaniem

w przeciwnym razie jeŜeli na krańcach przedziału wartość funkcji na identyczne znaki

to w przedziale nie ma miejsca zerowego

powtarzaj podziel przedział na pół jeŜeli dla wartości środkowej funkcja przyjmie wartość zero

to wartość ta jest rozwiązaniem w przeciwnym razie

jeŜeli przedział jest dostatecznie mały to środek przedziału jest wystarczającym przybliŜeniem rozwiązania

w przeciwnym razie wybierz tę połowę przedziału, w której ma miejsce zmiana znaku dopóki nie zostanie znalezione rozwiązanie

odczytaj znak dopóki istnieją dalsze dane wejściowe

zwiększ licznik znaków jeŜeli odczytano słowo to zwiększ o licznik_slow jeŜeli odczytano wiersz to zwiększ licznik_wierszy odczytaj następny znak

Przykład 1.4b - SORT1a

Find the smallest key and exchange it with the first key. Then find the second smallest key and exchange it with the second key,

end so on.

Algorytmy i Struktury Danych – wykład 6 Instytut Aparatów Elektrycznych Algrm1 Dr J. Dokimuk

Przykład 1.6. Największy wspólny dzielnik NWD(a, b) dwóch liczb naturalnych dodatnich.

NWD(16, 8) = 8, NWD(90, 135) = 45, NWD(46, 48) = 2, NWD(16, 28) = 4 Własności: NWD(0,0) = 0, NWD(a, b) = NWD(b, a), NWD(a, 0) = a.

Algorytm Euklidesa NWD występuje pod nazwą gcd(a, b) (greatest commom divisor). MoŜna zbudować algorytm NWD na bazie: ❶ odejmowania ❷❷❷❷ dzielenia

NWD(153, 85) a ≠ b=true; a>b=true; a=a-b=68 a ≠ b=true; a >b=false; b=b-a = 85-68 = 17 a ≠ b=true; a>b=true; a=a-b = 68-17 = 51 a ≠ b=true; a>b=true a=a-b = 51-17 = 34 a ≠ b=true; a>b=true a=a-b = 34-17 = 17 a ≠ b=false; nwd =a =17

Przykład 1.7 - SORT1b Sortowanie zbioru liczb A = a1,a2,…,an w kolejności rosnącej.

Dane: wektor A[1..n] ze zbiorem liczb rzeczywistych. Wynik : wektor B[1..n] z posortowanym zbiorem A.

Pobieraj elementy ze zbioru A i twórz sukcesywnie nowy zbiór B z elementami posortowanymi.

KaŜdy element ze zbioru A jest porównywany kolejno z elementami zbioru B.

JeŜeli k-ty element w zbiorze B jest większy od aktualnie pobranego ze zbioru A to wszystkie elementy od k są przesuwane o jedną pozycję w prawo. W zwolnione k–te miejsce wstaw element ze zbioru A.

Komentarz do schematu blokowego P1.7. i –bieŜący wskaźnik elementu wektora A; j –aktualnie największy wskaźnik zbioru B; k –bieŜący wskaźnik elementu tablicy B, z którym

porównywany jest element ai. JeŜeli ai≥bk oraz k<j to następuje porównywanie ai z bk+1. JeŜeli ai≥bk oraz k≥j wtedy ai dopisujemy na koniec wektora B. JeŜeli ai < bk to podzbiór B[k..j] jest przesuwany o 1 w prawo,

zaś na pozycję k wstawiany jest element ai.

Dla a ≥≥≥≥ b wynik dzielenia liczb całkowitych a przez b jest następujący: a = q*b + r q = a/b jest ilorazem dzielenia, zaś reszta r = a - qb jest zapisywana jako a mod b

Twierdzenie: dla liczb całkowitych a ≥ 0 oraz b > 0 nwd(a, b) = nwd(b, a mod b). Dowód opiera się na pokazaniu, Ŝe nwd(a, b) i nwd(b, a mod b) dzielą się nawzajem, zatem muszą być równe ( jeŜeli a|b i b|a to a = b). nwd(64, 24) = nwd(24,16)

Dane: liczby naturalne a, b Wynik : nwd(a, b)

dopóki a ≠≠≠≠ b wykonaj jeŜeli a > b to a a – b przeciwnie b b – a nwd a

Dane: a, b Wyniki : nwd(a, b)

while b > 0 do r a mod b a b b r nwd a

h=j+1

START

i<n

h>k

h=h-1N

T

j=j+1

NT

N

T

KONIEC

i=j=1bj=a1

i=i+1

k=1

ai>bk

k<j

k=k+1j=j+1

bj=ai

bh=bh-1

bk=ai

R1.7 Rys. P1.7. Algorytm sortowania

R1.6

Rys. P1.6. Algorytm NWD

function NWD(a, b:integer):integer begin if b=0 then NWD:=a else NWD:=NWD(b, a mod b) end ;

NWD(30, 21) NWD(21, 9) NWD(9, 3) NWD(3, 0) 3

2 mod 2 = 0

0 mod 23 = 0

46 mod 4 = 2

46 mod 2 = 0

2 mod 48 = 2

46 mod 48 = 46

48 mod 46 = 2

64 mod 24 = 16

Rekurencyjna implementacja algorytmu NWD jest skończona, gdyŜ w kaŜdym wołaniu ulega zmniejszeniu wartość 2-go argumentu.

Page 5: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 7Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

2. ANALIZA ALGORYTMÓW Zadaniem analizy algorytmów, -działu informatyki- jest poszukiwanie najlepszych algorytmów

realizujących określone zadania komputerowe, próbując odpowiedzieć na pytania: • Czy problem opisany algorytmem moŜe być zrealizowana na komputerze w realnym czasie? • Który z algorytmów naleŜy zastosować dla określonego problemu i zadanego sprzętu? • Czy przyjęty algorytm jest optymalny? MoŜe istnieje lepszy od przyjętego? • Czy moŜna udowodnić, Ŝe dla wybranego algorytmu uzyska się jednoznaczne rozwiązanie?

2.1. Pojęcie złoŜoności

Identyczny problem moŜe być rozwiązany z wykorzystaniem algorytmów o róŜnej efekty-wności, dającej się odczuć dopiero przy przetwarzaniu narastających liczebnie zbiorów.

ZłoŜoności obliczeniowa jako miarę porównywania efektywności algorytmów – wprowadzili J. Hartmanis i R. E. Stearns.

Określa ona ile zasobów zuŜywa dany algorytm lub jak jest on kosztowny.

Zbiór : zestaw oddzielnych, nie numerowanych obiektów, zwanych elementami zbioru. MoŜe zostać zdefiniowany przez wypisanie elementów w nawiasie np. Z = 2, 5, 4, 7 .

Zapis A ⊆⊆⊆⊆ B oznacza: wszystkie elementy zbioru A zawierają się w zbiorze B, tym samym jeŜeli x ∈ A to x ∈ B. Zbiór A jest podzbiorem zbioru B.

Oznaczenia typowych zbiorów: Ø –pusty, nie zawiera elementów, Z –liczb całkowitych Z = …-2,-1,0,1,2,…, R –liczb rzeczywistych, N –liczb naturalnych, N = 0,1,2,3,….

MoŜna zdefiniować nowy zbiór, bazując istniejących zbiorach, stosując zapis z dwukropkiem ( takie, Ŝe ). Zbiór liczb parzystych moŜna zdefiniować: P = X: X ∈∈∈∈ Z i reszta z X/2 jest zerem

Rozmiar danych wejściowych –liczba pojedynczych danych wchodzących w skład zbioru danych. Dla sortowania -liczba elementów zbioru; dla analizy drzewa –liczba węzłów w drzewie.

Wprowad źmy oznaczenia : Ωn –zbiór z zestawami danych wejściowych D o jednakowych rozmiarach, mDn –liczba operacji dominuj ących dla zestawu danych D, Xn – zmienna losowa zawierająca wartości mDn dla D ∈∈∈∈ Ωn, pk,n – prawdopodob., Ŝe algorytm wykona k operacji dominujących dla danych o rozmiarze n.

Podstawą określenia rozkładu prawdopodobieństwa zmiennej losowej Xn są informacje o aplikacjach algorytmu.

Często przyjmuje się, Ŝe kaŜdy zestaw danych o rozmiarze n, z jednakowym prawdo-podobieństwem moŜe stanowić zbiór wejściowy algorytmu. W praktyce, dla realnego zbioru danych nie zawsze tak musi być.

Pesymistyczna zło Ŝoność czasowa: W(n) = sup m Dn: D ∈∈∈∈ Ωn , kres górny zbioru

Oczekiwana zło Ŝoność czasowa: ∑ ⋅=k

nkpknA ,)( = ave(Xn) –wartość oczekiwana zm. losowej Xn

Pesymistyczna wraŜliwość czasowa: δW(n) = sup m D1 – mD2: D1, D2 ∈∈∈∈ Ωn

Oczekiwana wraŜliwość czasowa: )( nXdev=(

n

)(

n

)(

n

)(

n

)

δ Aδ Aδ Aδ A –std. odchylenie zmiennej losowej Xn

gdzie nX losowej zmiennej ncjąjest waria))(()var( ,2zaś)var()( ∑ ⋅−==

knknnnn pavek XXXXdev

Wzrost wartości funkcji wraŜliwości δW(n) i δA(n) sygnalizuje, Ŝe

algorytm jest bardziej wra Ŝliwy na dane wej ściowe, jego zachowanie mo Ŝe odbiega ć od oczekiwa ń po warto ściach funkcji W(n) i A(n).

ZłoŜoność czasowa algorytmy implementowanego jako procedura obliczeniowa w określonym środowisku programistycznym, ró Ŝni si ę od wyznaczonej teoretycznej.

Istotna jest, z praktycznego punktu widzenia, info rmacja o rz ędzie wielko ści funkcji złoŜoności W(n) i A(n), tzn. o ich zachowaniu dla n d ąŜącego do niesko ńczono ści.

A ∩∩∩∩ B -część wspólna, zbiór wspólnych elementów w obu zbiorach.

A ∪∪∪∪ B -suma, zbiór wszystkich elementów wystę-pujących w obu zbiorach.

A / B -róŜnica między A i B, zbiór elementów będących w A lecz nie występujących w B.

DDDD1111m1

DDDD2222m2

DDDD3333

DDDD4444

DDDD5555

Algorytmy i Struktury Danych - wykład 8Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

2.2. Definicje notacji

Efektywność algorytmu nie powinna być wyraŜana w jednostkach czasowych (ms, s) lecz jako zaleŜność między rozmiarem n zbioru danych a czasem jego przetworzenia.

Przykładowo, związek f(n) = c·n ma charakter liniowy i c–krotny wzrost rozmiaru danych daje ten sam wzrost czasu potrzebnego na wykonanie algorytmu.

ZaleŜności pomiędzy czasem t a rozmiarem n mają postać złoŜonych funkcji wieloskładnikowych i ich wyznaczanie ma sens jedynie dla duŜych zbiorów danych. W tej sytuacji składniki wzoru, które w sposób istotny nie zale Ŝą od n nale Ŝy wyeliminowa ć. Otrzymuje się przybliŜoną funkcję jako miarą efektywności algorytmu, której zgodność z oryginałem wzrasta ze wzrostem n.

RozwaŜmy przykładową funkcję efektywności: f(n) = n 2 + n1.5 + n·lg(n) + lg(n)

Z tabeli 2.1 wynika, Ŝe dla zbioru danych wyŜej 1 miliona funkcję opisująca złoŜoność obliczeniową f(n) moŜna ograniczyć do jednego składnika tzn. f(n) = n 2, bez popełnienia istotnego błędu obliczeniowego.

Asymptotyczna złoŜoność algorytmu: określa rząd wielkości działania algorytmu dla dostatecznie duŜych rozmiarów danych wejściowych. Stosowana jest gdy pomija się w niej mniej istotne składniki funkcji analitycznej lub gdy wyznaczenie wiarygodnej funkcji jest niemoŜliwe.

W notacji asymptotycznej wykorzystuje się funkcje, których argumentem jest zbiór liczb naturalnych. Jest ona przydatna do opisu pesymistycznego czasu działania, będącego zazwyczaj funkcją wielkości danych wejściowych.

Funkcje czysto kwadratowe (pure-quadratic) nie zawierają składników liniowych, 5n2+22. Funkcje zupełnie kwadratowe (complete quadratic) zawierają składniki liniowe, 5n2 - 3n + 22.

ZłoŜoność czasową algorytmu A1 określa funkcja f1(n) = 0.05n 2, zaś algorytmu A2 f2(n) = 120n . Algorytm A2 będzie wydajniejszy gdy 0.05n2 > 120n n > 2400.

KaŜdy algorytm o liniowym czasie działania staje się w określonym momencie wydajniejszy od kaŜdego algorytmu o kwadratowym czasie wykonania.

2.2.1. Notacja O

Niech dane będą dwie funkcje: f(n) i g(n) o wartościach dodatnich.

Funkcja f(n) jest klasy O(g(n)) jeŜeli istnieją dwie stałe: rzeczywista c > 0 i naturalna n0, takie Ŝe relacja f(n) ≤ c‧‧‧‧g(n) zachodzi dla ka Ŝdego n ≥ n0.

Funkcja f(n) jest duŜym O z g(n).

Między funkcjami f(n) i g(n) istnieje związek mówiący, Ŝe funkcja g(n) jest górnym ograniczeniem wartości funkcji f(n), z dokładnością do stałej c.

Dla dostatecznie du Ŝych n funkcja f(n) rośnie nie szybciej niŜ funkcja g(n). Notację O wprowadził 1894 Paul Bachmann i jest ona powszechnie stosowana do wyznaczenia

złoŜoności asymptotycznej tzn. szacowania szybkości wzrostu funkcji. Niejednoznaczno ści notacji O:

nie podaje sposobu wyznaczenia stałych c i n0, nie nakłada Ŝadnych ograniczeń na ich wartości, brak kryteriów wyboru w przypadku gdy mamy kilka wartości.

W praktyce dla danej pary funkcji f(n) i g(n) moŜna znaleźć nieskończenie wiele odpowiednich par c i n0 .

Niech 2

)1()(

−= nnnT . Dla n≥ 0 zachodzi:

22

)1( 2nnn ≤−

Przyjmując c = 1/2 oraz n0 = 0 dowodzimy, Ŝe T(n) ∈ O(n2)

Mówimy: duŜe O nakłada na funkcję asymptotyczne ograniczenie górne.

Tabela 2.1. Szybkość wzrostu składników funkcji f(n) n n2 n1.5 n·lg(n) lg(n)

256 65536 4096 2048 8 4096 16777216 262144 49152 12 65536 4294967296 16777216 1048565 16 1048576 1099301922576 1073588228 20969520 20

lg(n) ≡≡≡≡ log2(n) lg(n) = ln(n)/ln(2)

ccccgggg((((nnnn))))

ffff((((nnnn))))

nnnn0000

ffff((((nnnn)=)=)=)=OOOO((((gggg((((nnnn))))))))

Page 6: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 9Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

Weźmy funkcję: f(n) = 2n 2 + 11n + 13 ≤ cg(n) = O(n 2), gdzie g(n) = n 2 i rozwaŜmy odpowiadającą jej nierówność z dwiema niewiadomymi:

cnn

≤++ 21311

2 dla róŜnych wartości n.

MoŜna wyznaczyć nieskończenie wiele par (c, n0) spełniających w/wym nierówność.

Wyznaczenie najlepszych wartości c i n0 wymaga określenia, dla jakich wartości n0 określony składnik w funkcji f(n) staje się dominujący i ze wzrostem n pozostaje.

Dla przykładowej funkcji kandydatami na składnik dominujący są: 2n2 raz 11n. MoŜna zauwaŜyć, Ŝe nierówność 2n2 ≤ 11n jest spełniona dla n > 5.

Poszukiwane wartości graniczne to n0 = 6 oraz c ≥ 3.28.

MoŜna wybrać nieskończenie wiele par c, n0 powyŜej obliczonych wartości granicznych. Wartość stałej c w definicji O zaleŜy od wyboru n0 i odwrotnie.

Liczba n0 moŜe być określona jako punkt przecięcia wykresów funkcji f(n) i c•g(n).

W analizowanym przykładzie funkcja g(n)=n2 została wybrana arbitralnie i bynajmniej nie jest ona jedyna.

Równie dobrze jako funkcj ę g(n) moŜna wybrać funkcje: n3, …,nk dla k > 2 wtedy np.: f(n) ∈∈∈∈ O(n4)

Istnieje zasada mówiąca o wyborze funkcji g(n) najmniejszej z moŜliwych.

Własności notacji O

1. JeŜeli funkcja f(n) jest klasy O(g(n)) i g(n) jest klasy O(h(n)), to f(n) jest O(h(n)) – prawo przechodniości.

2. JeŜeli funkcja f(n) jest klasy O(h(n)) i g(n) jest klasy O(h(n)), to f(n) + g(n) jest O(h(n)). 3. Funkcja f(n) = ank jest klasy O(nk).

4. Funkcja f(n) = nk jest klasy O(nk+i) dla dowolnego i > 0.

5. Funkcja f(n) = c·g(n) jest klasy O(g(n)). 6. Funkcja loga(n) jest klasy O(logb(n)) dla dowolnych a > 1 i b>1.

Podstawa logarytmu nie ma znaczenia w notacji O. Zachodzi loga(n) = c*logb(n), c = ln(b)/ln(a)

7. Funkcja loga(n) jest O(lg(n)) dla dowolnego a > 0.

Z przedstawionych własności notacji O wynika uogólniony wniosek:

f(n) = aknk + ak-1n

k-1 + …+a1n + a0 jest klasy O(nk)

Notacja O określa górną granicę asymptotycznego szacowania. Szacuj ąc pesymistyczny czas działania algorytmu, równie Ŝ szacuje si ę z góry czas działania danego algorytmu dla wszystkich danych wejściowych .

Orzeczenie, Ŝe czas działania algorytmu jest rz ędu O(g(n)) oznacza, Ŝe pesymistyczny czas działania szacuje O(g(n)) lub, Ŝe niezale Ŝnie od struktury danych o rozmiarze n, czas działania algorytmu szac uje O(g(n)).

Koszt algorytmu sortowania przez wstawienie dla najgorszego przypadku danych szacowany jest jako O(n2), z czego wynikałoby, Ŝe pesymistyczny czas działania tego algorytmu jest takŜe O(n2). Notacji O uŜywa się do opisu czasu działania algorytmu poprzez badanie jego ogólnej struktury.

Klasycznym przykładem moŜe być algorytm zawierający podwójnie zagnieŜdŜoną pętlę, gdzie wskaźniki pętli i oraz j osiągają wartość n.

Dla n ≥ 1 prawdziwa jest relacja: n ≤≤≤≤ 1⋅⋅⋅⋅n2. Przyjmując n0 = 1 oraz c = 1 dowodzimy, Ŝe n ∈∈∈∈ O(n2). Funkcja złoŜoności f(n) nie musi posiadać składnika kwadratowego aby naleŜeć do klasy O(n2). Jej wykres musi jedynie znajdować się pod wykresem pewnej czysto kwadratowej funkcji.

Tablica 2.2 n 2n2 11n 1 2 11 2 8 22 3 18 33 4 32 44 5 50 55 6 72 66

n0 2 4 6 8 10

0

100

200

300

400

500

5n2

3.5n2

f(n)=2n 2+11n+13

f(n)

Algorytmy i Struktury Danych - wykład 10Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

2.2.2. Notacja Ω

Funkcja f(n) jest klasy Ω(g(n)) jeŜeli istniej ą dwie stałe: rzeczywista c > 0 i naturalna n0, takie Ŝe relacja f(n) ≥ c·g(n) zachodzi dla ka Ŝdego n ≥ n0.

MoŜna powiedzieć, Ŝe dla dostatecznie duŜych n funkcja f(n) rośnie przynajmniej tak szybko jak funkcja g(n).

Notacja Ω określa asymptotycznie dolną granicę szacowania

Między funkcjami f(n) i g(n) istnieje związek mówiący, Ŝe funkcja g(n) jest dolnym ograniczeniem wartości funkcji f(n), z dokładnością do stałej c.

Szacuj ąc czas działania algorytmu dla najlepszego przypadku, z wykorzystaniem notacji Ω, szacuje si ę równie Ŝ czas działania algorytmu dla wszystkich danych wejściowych.

Orzeczenie, Ŝe czas działania określonego algorytmu jest rzędu Ω(g(n)), oznacza, Ŝe niezaleŜnie od struktury danych wej ściowych o rozmiarze n czas działania algorytmu dla tych danych wynosi co najmniej c·g(n) dla n odpowiednio duŜego.

Efektywno ść algorytmu sortowania przez wstawienie dla najlepszej struktury danych wynosi Ω(n), z czego wynika, Ŝe optymistyczny czas działania algorytmu jest tak Ŝe Ω(n).

Prawdą jest teŜ stwierdzenie, Ŝe pesymistyczny czas działania w/wym algorytmu sortowania wynosi Ω(n2), gdyŜ dla pewnych danych czas jego wykonania wynosi Ω(n2).

2.2.3. Notacja Θ

Funkcja f(n) jest klasy Θ(g(n)) jeŜeli istniej ą stałe rzeczywiste c1 > 0 i c2 > 0 oraz stała naturalna n0, takie Ŝe relacja c1·g(n) ≤f(n) ≤ c2·g(n) zachodzi dla ka Ŝdego n ≥ n0.

Funkcja f(n) jest klasy Θ(g(n)), jeŜeli istnieją dodatnie stałe c1 oraz c2 takie, Ŝe funkcja ta moŜe być zawarta miedzy c1g(n) i c2g(n) dla dostatecznie duŜych n. c1g(n) ≤ f(n) ≤ c2g(n)

Funkcja f(n) jest rzędu g(n) tzn. obie funkcje rosną tak samo szybko dla dostatecznie duŜych n. Funkcja g(n) jest asymptotycznie dokładnym oszacowaniem funkcji f(n), gdyŜ dla

wszystkich n ≥ n0 funkcja f(n) jest równa g(n) z dokładnością do stałego współczynnika. Dla dwóch dowolnych funkcji f(n) i g(n) zachodzi zaleŜność:

f(n) ∈ Θ(g(n)) wtedy i tylko wtedy, gdy f(n) ∈ Ω(g(n)) oraz f(n) ∈∈∈∈ O(g(n)).

Notacja O(g(n)) Nie moŜe być gorzej

Notacja Ω(g(n)) Nie moŜe być lepiej

Notacja Θ(g(n)) MoŜe być dokładnie

ΘΘΘΘ(g(n) = O(g(n) ∩ ΩΩΩΩ(g(n)

Notacja O dotyczy szacowania górnych ograniczeń funkcji f(n) z dokładnością do współczynnika c.

Notacja Ω dotyczy szacowania dolnych ograniczeń funkcji f(n) z dokładnością do współczynnika c.

Notacja Θ szacuje funkcję f(n) z dokładnością do stałych współczynników c1 i c2.

cg(n

)f(n)

n0

O(g(n))

Graficzna interpretacja notacji asymptotycznych

cg(n)

f(n)

n0

(g(n))

c1g(n)

f(n)

n0

(g(n))

c2g(n)

2n2+6n

8n2-11

12n2

5n+12

2lg(n)

6lg(n)-8

2n2+6n

8n2-11

12n2

3n6+6n

2

2n3-5n

2n+2n

2

2n2+6n

8n2-11

12n2

Przykłady funkcji naleŜących do róŜnych notacji dla g(n) = n2

Page 7: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 11Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

2.2.4. Przykłady algorytmów klasy O

Algorytm stały: czas wykonania jest rzędu O(1), jest niezmienny i nie zaleŜy od liczby przetwa-rzanych elementów. Przykładem jest dostęp do elementów tablicy o określonej strukturze.

Algorytm logarytmiczny: czas wykonania jest rzędu O(lg(n)). Dotyczy zadań, które sprowadzone zostały do rozmiaru n/2 plus pewne działania dodatkowe . Przykład: poszukiwanie binarne zadanego elementu w ciągu uporządkowanym rosnąco.

if x < an/2 then Szukaj w podciągu a1,…,an/2-1 else Szukaj w podciągu an/2+1,…,an

Algorytm liniowy: czas wykonania jest rzędu O(n). Oznacza to wykonywanie pewnej, stałej liczby działań dla kaŜdego z n elementów zbioru wejściowego. Przykładem moŜe być sekwencyjne przeszukiwanie zbioru.

Algorytm liniowo-logarytmiczny: czas wykonania rzędu O(nlg(n)). Dotyczy zadań, które sprowadzone zostały do dwóch podzadań rozmiaru n/2 plus pewne działania dodatkowe . Liniowość względem n wynika z działań prowadzących do utworzenia dwóch podzbiorów o liczności n/2 a następnie ich scalenia.

Przykładem jest sortowanie przez scalanie: sortuj podci ąg a1,…,an/2 oraz an/2+1,…,an następnie scal oba podciągi.

Algorytm kwadratowy: czas wykonania rzędu O(n2). Dotyczy sytuacji, w której dla kaŜdej pary elementów wykonywana jest określona stała liczba działań. Występuje w przypadku podwójnych instrukcji iteracyjnych oraz takŜe szacowania czasu pesymistycznego sortowania metodą wstawiania.

Algorytm sześcienny: czas wykonania rzędu O(n3). Przykładem jest mnoŜenie macierzy kwadratowych o wymiarze n x n.

Algorytm wykładniczy: czas wykonania jest rzędu O(2n). Dotyczy sytuacji, w której dla kaŜdego podzbioru danych wejściowych wykonywana jest pewna stała liczba działań.

Tablica 2.3. Przykładowe czasy realizacji róŜnej klasy algorytmów

Liczba operacji / Czas wykonania [1 operacja/uS] Klasa algorytmu

n = 100 n = 1.000 n = 1.000.000

Stały O(1) 1 <<s 1 <<s 1 <<s Logarytmiczny O(log(n)) 6.64 <<s 9.97 <<s 19.93 <<s Liniowy O(n) 100 <<s 1000 1 ms 106 1 s Kwadratowy O(n2) 10000 10 ms 106 1 s 1012 11.6 dni Wykładniczy O(2n) 1030 3.2*1016lat 10301 ∞ 10301030 ∞

Tabela 2.3. pokazuje, Ŝe dla pewnej klasy algorytmów ich implementacja na współczesnych komputerach jest bezowocna, ze względu na nierealny czas wykonania. Komputery ”nanosekundowe” tylko w pewnym stopniu będą w stanie urealnić ich implementację.

ZłoŜoności wykładnicza jest czasami myląca, gdyŜ dotyczy „najgorszej” struktury danych wejściowych, której prawdopodobieństwo pojawienia się moŜe być niewielkie.

JeŜeli algorytm na pesymistyczną złoŜoność W(n)=2n + O(1) oraz wraŜliwość δW= 2n + O(1) to nie moŜna stwierdzić, Ŝe jest on nierealizowalny, gdyŜ dane pesymistyczne mogą się nigdy nie pojawić.

Przykładem moŜe być metoda programowania liniowego simplex , mającą złoŜoność wykładniczą dla danych najgorszych. Dane wejściowe, które pojawiają się w praktyce dają algorytmowi klasę liniową lub w najgorszym razie klasę wielomianową.

Analiza złoŜoności algorytmów nigdy nie powinna być lekcewaŜona, a w szczególności dla duŜych struktur danych. Rosnąca szybkość działania komputerów niewiele pomoŜe, jeŜeli wykonywane programy będą realizować nieefektywne algorytmy.

Algorytmy i Struktury Danych - wykład 12Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

Dlaczego zazwyczaj szacuje si ę pesymistyczny czas działania algorytmu ? Pesymistyczny czas działania jest górn ą granic ą czasów działania algorytmu dla kaŜdego

zbioru danych. Znając ten czas mamy gwarancję, Ŝe algorytm nie b ędzie działał dłu Ŝej; Dla wielu algorytmów pesymistyczny czas działania występuje bardzo często, szczególnie w

trakcie poszukiwania w bazie danych informacji, której tam nie umieszczono ; Bardzo często średni czas działania, związany z typowym zbiorem danych jest tego

samego rzędu co klasyczny przypadek pesymistyczny .

Uwarunkowania uwzgl ędniane w analizie zło Ŝono ści algorytmów: opracowany algorytm i jego płaszczyzna implementacyjna to nie całkiem to samo; wraŜliwo ść określonego algorytmu na dane wejściowe, moŜe być przyczyną Ŝe realne

czasy działania będą znacznie odbiegać od przewidywanych rozwaŜań teoretycznych; prawidłowe określenie prawdopodobie ństwa rozkładu liczby operacji dominujących moŜe

być niemoŜliwe, co skutkuje trudnościami z określeniem oczekiwanej złoŜoności czasowej; jeden algorytm działa dobrze dla pewnej klasy danych, drugi zaś dla innej; istotne jest jak cz ęsto określona implementacja algorytmu będzie wykonywana; istotne jest czy algorytm będzie pracował tylko z małymi lub tylko z duŜymi zbiorami danych.

Pojęcie funkcji anonimowej

Zapis 2.5n2 + 3n + 4 = 2.5n2 + O(n) jest równowaŜny 2.5n2 + 3n + 4 = 2.5n2 + h(n) gdzie h(n) jest pewną funkcją naleŜącą do zbioru O(n).

Jest oczywiste, Ŝe h(n) = 3n + 4 rzeczywiście naleŜy do O(n). Zapis powyŜszy pozwala wyeliminować niepotrzebne detale, przyczyniając się do upraszczania równań.

JeŜeli badamy tylko asymptotyczne zachowanie funkcji T(n) = 2T(n/2) + O(n) to nie interesują nas mało istotne składniki zawarte w anonimowej funkcji O(n).

Zapis 2.5n2 + O(n) = O(n2) oznacza: bez względu na to jakie anonimowe funkcje znajdują się po lewej stronie znaku równości, zawsze moŜna wybrać funkcje po prawej stronie aby zachodziła relacja. Tak więc prawa strona równania jest zgrubnym opisem lewej strony.

MoŜna tworzyć łańcuch relacji: 2.5n2 + 3n + 4 = 2.5n2 + O(n) = O(n2)

Dlaczego jedni mówią, Ŝe algorytm jest klasy O(n2) inni zaś, Ŝe klasy Θ(n2)

Zapis f(n) = O(g(n)) oznacza, Ŝe funkcja f(n) jest elementem zbioru O(g(n)). Zapis teoriomnogościowy Θ(g(n)) ⊆⊆⊆⊆ O(g(n)) mówi, Ŝe wystąpienie f(n) = Θ(g(n)) implikuje f(n)=O(g(n)), poniewaŜ notacja Θ jest silniejsza niŜ notacja O.

JeŜeli funkcja 2.5n2 + 3n + 4 naleŜy do zbioru Θ(n2) to naleŜy teŜ do zbioru O(n2).

Notacja Θ jest uŜywana do opisu asymptotycznie dokładnych oszacowań, zaś notacja O do oszacowania asymptotycznie górnego .

Notacja O jest u Ŝywana nieformalnie do opisu asymptotycznie dokładnych oszacowa ń. Zaś zapis f(n) = O(g(n)) mówi jedynie, Ŝe po przemnoŜeniu c·g(n) otrzymuje się asymptotycznie górn ą granicę, bez podania jej stopnia dokładności.

Opracowano dwa algorytmy A1 i A2, których implementacyjna

złoŜoność wynosi odpowiednio f1(n) = 200*lg(n) i f2(n) = 10*n. Algorytm A1 jest klasy O(lg(n)) zaś A2 klasy O(n), co oznacza, Ŝe algorytm A1 jest lepszy od A2. Czy jest tak rzeczywiście skoro dla n=100 mamy: 200*lg(100) > 10*100.

Problem wyjaśnia się, jeŜeli weźmiemy odpowiednio duŜe n, gdyŜ notacja O jest notacją asymptotyczną.

Dostępne są dwa algorytmy B1 i B2 rozwiązujące ten sam problem. Algorytm B1 wykonuje 108n operacji i jest klasy O(n), zaś B2 n2 operacji i jest klasy O(n2).

Zgodnie z zasadą O algorytm B2 naleŜy odrzucić, poniewaŜ czas jego wykonania rośnie szybciej niŜ B1. Jest to prawdą ale dla odpowiednio duŜego zbioru danych. Dla zbioru danych o liczności n ≤ 1.000.000 algorytm B2 wykonuje mniej operacji niŜ B1.

n 200lg(n) 10n 10 664 100 50 1129 500 100 1329 1000 150 1446 1500 1000 1993 10000

Page 8: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 13Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

2.3. ZłoŜoność obliczeniowa algorytmu numerycznego Miarą złoŜoności moŜe być liczba wykonywanych operacji zmiennoprzecinkowych.

Za pojedynczą operację zmiennoprzecinkową uwaŜa się sum ę, róŜnicę, iloczyn oraz iloraz dwóch liczb rzeczywistych.

W przypadku obliczeń realizowanych na liczbach zespolonych mamy dwie (suma i róŜnica) lub sześć (iloczyn i iloraz) operacji zmiennoprzecinkowych.

Niektóre środowiska programistyczne jak MATLAB udostępniają moŜliwość wyznaczenia liczby wykonywanych operacji zmiennoprzecinkowych.

Matlab udostępnia funkcję flops , zwracającą liczbę wszystkich wykonanych operacji zmiennoprzecinkowych od czasu uruchomienia środowiska.

Funkcja flops(0) zeruje licznik operacji. Pomiar liczby operacji zmiennoprzecinkowych moŜna zrealizować w pakiecie MATLAB wg schemat podany obok.

Dla macierzy kwadratowej o rozmiarach n, podaje się liczby operacji zmiennoprzecinkowych:

suma i róŜnica: n2 iloczyn: 2n3 rozkład LU: 0.7n3. Wyznaczanie liczby operacji zmiennoprzecinkowych

» n=5; A=randn(n); B=rand(n); p = flops; C=A+B; w = flops - p

% n=5 25, n=10 100, n=50 500, n=100 104

» n=5; A=randn(n); B=rand(n); p = flops; C=A*B; w = flops - p

% n=5 250, n=10 2000, n=50 250000, n=100 2*106

Pomiar czasu wykonania

» n=50; A=randn(n); B=rand(n); tic; C=A*B; time = toc - tic

% n=50 time=0.05s, n=100 time=0.16s. 2.4. Uwagi o złoŜoności obliczeniowej

Analizując konkretny algorytm, określamy jego złoŜoność czasową lub pamięciową, albo przynajmniej klasę złoŜoności czasowej lub pamięciowej.

Nie analizujemy na tym etapie problemu rozwiązywanego przez ten algorytm. Klasyczny algorytm mnoŜenie macierzy ma złoŜoność czasową n3. Nie oznacza to, Ŝe mnoŜenie macierzy wymaga algorytmu o złoŜoności O(n3).

Funkcja n3 jest własnością określonego algorytmu i nie musi być własnością problemu mnoŜenia macierzy. Strassen V. przedstawił algorytm mnoŜenia macierzy o złoŜoności O(n2.81).

Zmodyfikowana odmiana tego algorytmu pozwala uzyskać złoŜoności czasową O(n2.38). WaŜne jest określić, czy istnieje moŜliwość znalezienia jeszcze bardziej efektywnego algorytmu.

Analiza zło Ŝoności obliczeniowej jest próbą określenia dolnej granicy efektywności wszystkich algorytmów rozwiązujących dany problem.

Istnieje dowód, Ŝe problem mnoŜenia macierzy moŜna rozwiązać za pomocą algorytmu, o złoŜoności nie mniejszej niŜ ΩΩΩΩ(n2). Stwierdzenie jest efektem analizy złoŜoności obliczeniowej.

Na podstawie przeprowadzonej analizy moŜemy powiedzieć, Ŝe dolne ograniczenie dla problemu mnoŜenia macierzy to ΩΩΩΩ(n2). Nie oznacza to, Ŝe musi istnieć moŜliwość skonstruowania algorytmu mnoŜącego macierze o złoŜoności czasowej O(n2). Oznacza to jedynie, Ŝe teoretycznie taka moŜliwość istnieje.

Celem dla danego problemu jest wyznaczenie dolnego ograniczenia w postaci ΩΩΩΩ(g(n)) i stworzenie algorytmu o złoŜoności O(g(n)), który rozwiązuje ten problem.

Kiedy uda się osiągnąć ten cel, many pewność, Ŝe znalezionego algorytmu nie moŜna juŜ poprawić (oczywiście, poza poprawą stałych, których nie uwzględniamy w zapisie złoŜoności).

Dla sortowania stworzono algorytmy o złoŜoności czasowej zbliŜonej do dolnego ograniczenia . Dla klasy algorytmów sortujących na podstawie operacji porówna ń kluczy dolne ograniczenie wynosi ΩΩΩΩ(nlg(n)). Powstały juŜ algorytmy O(nlg(n) ).

Algorytmy sortujące wyłącznie poprzez porównanie kluczy mogą porównywać dwa klucze aby określić, który z nich jest większy, oraz kopiować klucze, ale nie mogą wykonywać na kluczach Ŝadnych innych operacji.

poczatek = flops; FUNKCJA wynik = flops – poczatek

Algorytmy i Struktury Danych - wykład 14Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

2.5. Równania rekurencyjne w analizie złoŜoności

W wielu przypadkach opis algorytmu zawiera rekurencyjne wywołanie samego siebie. Wówczas obliczenia związane z wyznaczaniem klasy algorytmu (czasu działania) moŜna

sprowadzić do rozwiązania równania rekurencyjnego.

2.5.1. Metoda iteracyjna z zamianą zmiennej

Metoda rozwija równanie do sumy, po uprzednim podstawienie n = 2k, po czym następuje ograniczenie składników otrzymanej sumy i powrót poprzez podstawienie k = lg(n)).

❶❶❶❶ Równanie złoŜoności dla problem analizy zbioru wielkości n, sprowadzonego do analizy podzbioru n/2 plus pewna stała liczba działań.

>+=

=1dla)2/(

1dla0)(

ncnT

nnT

T(2k) = T(2k-1) + c = T(2k-2) + c +c = T(20) + c·k = c·lg(n) T(n) = O(lg(n))

❷❷❷❷ Równanie złoŜoności dla problem analizy zbioru wielkości n, sprowadzonego do analizy dwóch podzbiorów n/2 plus pewna stała liczba działań.

>++=

=1dla)2/()2/(

1dla0)(

ncnTnT

nnT

T(2k) = 2T(2k-1) + c = 2(2T(2k-2) + c) +c = 22T(2k-2) + 21c + 20c =

2kT(20) +c(2k-1+2k-2+…+20) = c(2k-1) = c(n-1) T(n) = O(n)

❸❸❸❸ Równanie złoŜoności dla problem analizy zbioru wielkości n, sprowadzonego do analizy dwóch podzbiorów n/2 plus pewna liniowa liczba działań.

>++=

=1dla)2/()2/(

1dla0)(

ncnnTnT

nnT

T(2k) = 2T(2k-1) + c2k = 2(2T(2k-2) + c2k-1) +c2k = 22T(2k-2) + c2k +c2k =2kT(20) + kc2k =

= c·n·lg(n) T(n) = O(nlg(n))

Rekurencja uniwersalna

Jest metodą pozwalającą na „szablonowe” rozwiązanie równań postaci: T(n) = a·T(n/b) + f(n)

gdzie a > 1, b > 1 –pewne stałe, zaś f(n) jest funkcja asymptotycznie dodatnią. Równanie opisuje czas działania algorytmu, który dzieli zadany problem o rozmiarze n na a podproblemów o rozmiarze n/b kaŜdy.

KaŜdy podproblem a rozwiązywany jest rekurencyjnie w czasie T(n/b). Funkcja f(n) opisuje czas dzielenia i łączenia wyników częściowych.

Upraszczanie równań rekurencyjnych

Rozwiązując rekurencyjne równanie warto zastanowić się nad pominięciem szczegółów.

Klasycznym załoŜeniem upraszczającym jest przyjęcie za argument funkcji liczby całkowitej, gdyŜ dla innych wartości prezentowane zaleŜności na czas działania algorytmu nie muszą być aktualne.

RozwaŜmy równanie ❸❸❸❸ , które dla poprawnego rozwinięcia potrzebuje warunków brzegowych. MoŜna przyjąć załoŜenie, Ŝe dla małych n funkcja T(n) jest stała i pominąć warunki brzegowe otrzymując:

T(n) = 2T(n/2) + O(n)

Nie podaje się wartości funkcji T(n) dla małych n , gdyŜ ich uwzględnienie zmienia jedynie rozwiązanie o pewną stałą bez wpływu na rząd wielkości.

x –największa liczba całkowita nie większa niŜ x 31/2 = 15.5 = 15 x –najmniejsza liczba całkowita, nie mniejsza niŜ x 31/2 = 15.5 = 16

Page 9: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 15Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

Stosując ideę upraszczania równań rekurencyjnych moŜna rozwiązać równanie złoŜoności:

12

2)( −+

= nn

TnT bez podanych warunków początkowych.

2.5.2. Metoda dodawania stronami

Rozpoczynamy od problemu stopnia n i kolejno schodzimy wstecz, następnie dodajemy stronami otrzymane wyraŜenia, uwzględniając warunek początkowy.

Rozwiązać równanie:

>+−=

=1dla)1(

1dla1)(

nnnT

nnT

W rezultacie dodania stronami otrzymuje się:

T(n) = 1 + 2 +…+ n-2 + n-1 + n = ∑+=

=

n

i

nni

1 2)1(

Rozwiązać równanie:

>+−

==

1dla2

)1(

1dla0)(

nn

nT

nnT

Dodając stronami i redukując otrzymuje się:

∑ ≈=+−

+−

+++==

n

in

innnnT

2)ln(2

12

21

22

222

0)( L

Rozwiązać równanie:

≥+−=

=1dla1)1(

0dla1)(

nnT

nnT

Dodając stronami otrzymuje się:

T(n) = 1+1+…+1+1 = n + 1

T(n) = T(n-1) + n T(n-1) = T(n-2) + n - 1 T(n-2) = T(n-3) + n - 2

… … … … … T(2) = T(1) + 2 T(1)= 1

T(n) = T(n-1) + 2/n T(n-1) = T(n-2) + 2/(n-1) T(n-2) = T(n-3) + 2/(n – 2)

… … … … … T(2) = T(1) + 2/2 T(1)= 0

T(n) = T(n-1) + 1 T(n-1) = T(n-2) + 1 T(n-2) = T(n-3) + 1 … … … … … T(1) = T(0) + 1 T(0) = 1

1)lg(12)lg(2

2)(

:to)lg(Jezeli

122

222

2)(

12438

8122148

24

1224

41124

2212

2)(

)lg()lg(

)lg(

1

0

+−=+−+

=

=

+−⋅+

=−⋅+

=

−−−+

=−−+

−+

=−−+

=−+−

+

=−+

=

∑−

=

nnnnnn

TnT

nk

nkn

Tnkn

TnT

nn

Tnnn

T

nn

Tnnn

Tnn

TnT

nn

n

ki

kk

i

ik

k

LLLL

Algorytmy i Struktury Danych - wykład 16Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

2.6. Przykłady

Wzory uŜyteczne przy szacowaniu złoŜoności.

∑+=

=

n

i

nni

1 2

)1( ∑

++∑ =++=

+

= =

1

1 1 2

)2)(1()1(

n

i

n

i

nnnii )ln(

11

3

1

2

11

1n

inH

n

in =∑=++++=

=L

Przykład 2.1. Sprawdzić czy w zbiorze A = a1,a2,…,an znajduje się określony obiekt. Dane: tablica A zawierająca n obiektów oraz poszukiwany obiekt key. Wynik : zmienna OK > 0 gdy element znajduje się w tablicy A, przeciwnie OK = -1.

Operacja dominująca: porównanie

Pesymistyczna złoŜoność: W(n) = n + 1

Pesymistyczna wraŜliwość: δW(n) = n

ZałoŜenia: –poszukiwany obiekt key znajduje się w zbiorze A, gdzie wszystkie elementy są róŜne. –nie ma podstaw do stwierdzenia, Ŝe występowanie key na jednej pozycji jest bardziej

prawdopodobne niŜ na innej, stąd prawdopodobie ństwo znalezienia się poszukiwanego obiektu key na dowolniej pozycji tablicy A jest jednakowe i wynosi: pk,n = 1/n dla k = 1,2,…,n.

Oczekiwana zło Ŝoność czasowa: 2

12

)1(1111

)(+=+⋅=∑⋅∑ =⋅

===

nnnn

kn

pkn

k

n

kknnA

Oczekiwana wraŜliwość czasowa: nn

n

nkXnA

n

kn 29.0

12

11

2

1)var()(

2

1

2

≅−=∑

+−===

δ

Wnioski: wraŜliwość czasowa jest klasy O(n) co sugeruje wraŜliwość algorytmu na strukturę danych wejściowych i moŜliwość odstępstwa od oczekiwanej złoŜoności czasowej.

Gdy key moŜe nie występować w tablicy, przypisujemy pewne prawdopodobieństwo p do zdarzenia polegającego na tym, Ŝe key występuje w tablicy. Przyjmujemy, jednakowe prawdopodobne, Ŝe key znajdzie się na którejkolwiek pozycji tablicy.

Prawdopodobieństwo tego, Ŝe key znajduje się na k-tej pozycji, wynosi wówczas p/n , natomiast prawdopodobieństwo, Ŝe nie znajduje się w tablicy, wynosi 1 - p.

22

1)1()1(21

)1(1

),(pp

npnnnn

pnn

pk

ppnA

n

k+

−=−++⋅=∑ −+

⋅=

= A(n, 0.5) = 0.75n+0.25.

Przykład 2.2.

Wypełnianie fragmentu macierzy od przekątnej włącznie.

ZłoŜoność obliczeniowa implementacji algorytmu.

∑ +++++=

= =

n

i

i

jprpprpprp ttttttnT

1 1)2(22)(

( )∑ +⋅++++==

n

iprpprpprp ttittttnT

1)2(22)(

)2)(1(5.0)(2)( prpprpprp ttnnttnttnT ++++++=

prp tnntnnnT )5.05.21()31()( 22 +++++=

Algorytm wypełniania macierzy jest klasy O(n2) dla operacji przypisywania i porównywania.

Algorytm 2.1 Poszukiwanie elementu w wektorze

i 1 A[n+1] key // wartownik while A[i] <> key do i i+1 if ( i ≤ n ) then OK i else OK –1

int main() //P25b int n=6, A[6][6] =0, i, j; i=0; / / przypisanie tp

while (i < n) // porównania tpr j=0; // przypisanie tp

while (j <= i) // porównanie tpr A[i][j] = 2; // przypisanie tp

j++; // przypisanie tp

i++; // przypisanie tp Drukuj;

int main() / /P25a wypełnianie fragmentu tablicy int n=6, A[6][6] = 0, i, j; for (i=0; i<n; i++) for (j=0; j<=i; j++) A[i][j]=2; Drukuj;

2 0 0 0 0 0 2 2 0 0 0 0 2 2 2 0 0 0 2 2 2 2 0 0 2 2 2 2 2 0 2 2 2 2 2 2

W pętli while Instrukcje wykonywane są n razy zaś warunek sprawdzany jest n+1 razy.

Page 10: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 17Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

Przykład 2.3. Dany jest zbiór liczb A = a1, a2,…, an. Utworzyć zbiór sum wszystkich podciągów tzn.:

As = a1, a1+a2, a1+a2+a3,…, a1+…+an)

Dane: tablica A zawierająca elementy zbioru A. // A = 1, 2, 3, 4, 5, 6, 7, 8 Wynik : tablica sA zwierająca sumy podtablic. // sA = 1, 3, 6, 10, 15, 21, 28, 36

Operacja dominująca:

przypisywanie

Pętla zewnętrzna po i obiega n razy, wykonując: • pętlę wewnętrzną, • przypisanie wartości zmiennym: i, sum , j, sA[j].

Pętla wewnęrzna wykonuje się i razy dla kaŜdego i ∈∈∈∈1…n, -dla kaŜdego obiegu przypada jedno przypisanie do sum oraz jedno do j.

Liczba przypisań: 2

)1(2424

1

++=∑+=

nnnkn

n

k

ZłoŜoność czasowa: W(n) = n2 + 5n. W(n) ∈ O(n2)

Przykład 2.4. Dany jest zbiór liczb A = a1, a2,…, an.

Utworzyć nowy zbiór , którego elementy stanowią sumy r elementów podciągu tzn.: Ar = (a1+…+ar, a2+…+ar+1, a3 +…+ ar+2, …)

Dane: tablica A zawierająca elementy zbioru A; r –liczba elementów podciągu. Wynik: tablica sA[1..n–r+1], zwierająca sumy podtablic r–elementowych.. Operacja dominująca: przypisywanie

Dane przykładowe dla r = 5: A = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 A = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 A = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 A = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 A = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 A = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 SA = 15, 20, 25, 30, 35, 40

Pętla zewnętrzna i wykonuje się n – r+1 razy:

-realizuje przypisania do i, sum, j oraz sA[j].

Pętla wewnętrzna j wykonuje się r razy: -na kaŜdą iterację przypada jedno przypisanie do sum oraz do j.

Liczba przypisań: (4 + 2r)(n – r + 1).

ZłoŜoność czasowa: W(n) = (r + 2)n - r 2- r + 2. W(n) ∈∈∈∈ O(n)

Dla 5-cio elementowego podciągu W(n) = 14(n – 4).

Uwaga: Algorytmy z pętlami zagnieŜdŜonymi mają zazwyczaj złoŜoność klasy większej niŜ O(n).

Są wyjątki (przykład 2.4), gdzie algorytm z pętlą podwójną jest klasy O(n).

Dotyczy to sytuacji, gdy pętla wewnętrzna nie realizuje pełnego obiegu dla wszystkich zewnętrznych n.

// Suma podciągów wektora A

for i 1 to n do

s 0

for j 1 to i do s s + A[j]

sA[i] s

for (int i=0; i<n; i++) s=0; for(j=0; j<=i; j++) s += A[j]; sA[i] = s; 1 3 6 10 15 21 28 36

Algorytm 2.4 . Suma podciągów r-el. wektora A

r1 r – 1 // zmienna robocza for i r to n do sum 0 for j i – r1 to i do

sum sum + A[j] sA[i – r1] sum

Algorytmy i Struktury Danych - wykład 18Instytut Aparatów Elektrycznych Algrm2 Dr J. Dokimuk

W analizie algorytmów naleŜy czasem uwzględnić oprócz czasu wykonania operacji podstawowych takŜe instrukcje dodatkowe i instrukcje sterujące, w kontekście

określonego środowiska, na którym zaimplementowano algorytm.

Instrukcje dodatkowe związane są z inicjalizacją zmiennych przed wejściem do pętli. Liczba wykonań tych instrukcji nie wzrasta ze zwiększaniem rozmiaru danych wejściowych.

Instrukcje sterujące to instrukcje zwiększające indeks w sterowaniu pętlą. Liczba ich powtórzeń wzrasta wraz ze zwiększaniem rozmiaru danych wejściowych.

Operacje podstawowe, instrukcje dodatkowe oraz instrukcje sterujące są właściwościami algorytmu i jego implementacji.

Nie są natomiast właściwościami problemu. Oznacza to, Ŝe mogą być one róŜne w przypadku dwóch róŜnych algorytmów rozwiązujących ten sam problem.

Page 11: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 19 Instytut Aparatów Elektrycznych Algrm3 Dr J. Dokimuk

3. REKURENCJA Rekurencja występuje wtedy, gdy obiekt składa się częściowo z samego siebie lub w definicji

odwołuje się do siebie samego. Pozwala opisywać zbiory nieskończone lub duŜe zbiory skończone, przy pomocy skończonego wyraŜenia.

Narzędziem implementacyjnym dla algorytmów rekurencyjnych jest pojęcie funkcji. W praktycznych realizacjach istotne jest pokazanie, Ŝe głębokość rekurencji jest skończona oraz, Ŝe ich liczba jest relatywnie mała.

Struktura definicji rekurencyjnej:

1. warunek pocz ątkowy : element podstawowy, będący częścią składową pozostałych elementów.

2. krok rekurencyjny: specyfikuje reguły tworzenia nowych obiektów z elementów podstawowych.

Czy 628 jest liczb ą naturaln ą ?

628 ∈ N jeŜeli 62 ∈ N ( reguła 2)

62 ∈ N jeŜeli 6 ∈ N (reguła 1 )

Definicja rekurencyjna poza tworzeniem nowych obiektów umo Ŝliwia sprawdzenie , czy dany element naleŜy do określonego zbioru.

Technika sprawdzania prowadzi do rozbicia problemu na podproblemy. JeŜeli podproblem jest nadal złoŜony to rozbija się go dalej aŜ do osiągnięcia poziomu warunku pocz ątkowego .

3.1. Silnia n! = n(n-1)(n-2)…(3)(2)(1) dla n ≥ 1 Wartość silni definiuje wzór rekurencyjny, składający się z:

-warunku brzegowego, -kroku rekurencyjnego.

Istotną cechą rekurencji jest to, Ŝe do wyznaczenia wartości elementu ak naleŜy obliczyć wcześniejsze elementy a1,…ak-1. 4! wymaga wcześniejszego obliczenia wartości 0!, 1!, 2!, 3!.

Zastosowanie definicji rekurencyjnej sprawia, Ŝe implementacja formuł matematycznych jest niemal automatyczna.

Funkcja Silnia(3) otrzymuje jako parametr liczbę 3 i analizuje czy jest ona równa zero (warunek początkowy). PoniewaŜ 3≠0 zatem przyjmuje wartość 3*Silnia(2).

Wartość Silnia(2) nie jest znana, funkcja wywołuje swój kolejny egzemplarz liczący wartość Silnia(2). Jednocześnie zamroŜony jest proces liczenia 3*Silnia(2).

Proces powtarza się aŜ wywołując swój kolejny egzemplarz dojdzie do obliczania Silnia(0), znanej z warunku początkowego.

Wtedy wynik cząstkowy poziomu elementarnego przesyłany jest na coraz wyŜszy poziom, aŜ do szczytu stosu.

Liczba mnoŜeń jest sumą mnoŜeń w wywołaniach rekurencyjnych, potrzebnych do obliczenia Silnia(n-1) plus jedno mnoŜenie n* Silnia(n-1) na najwyŜszym poziomie.

ZłoŜoność czasową algorytmu określa zatem zaleŜnością rekurencyjną T(n). Dodając stronami rozpisane wyraŜenie rekurencyjne i przeprowadzając redukcję czynników identycznych otrzymuje się wyraŜenie: T(n) = n + 1.

Przedstawia ono czas wykonania algorytmu, który wynosi: T(n) = (n + 1)*tm gdzie tm oznacza jednostkowy czas wykonania operacji mnoŜenia w określonym środowisku.

>−

==

0 )!1(*

0 1!

ndlann

ndlan

Zbiór liczb naturalnych 1. 0,1,2,3,4,5,6,7,8,9 ∈ N;

2. jeŜeli n ∈ N to n0,n1,n2,n3,n4,n5,n6,n7,n8,n9 ∈ N;

long Silnia( int n) if (!n) return 1; return n*Silnia(n-1 );

≥+−=

=1dla11

0dla1

nnT

nnT

)()(

n:=3

n:=0 3333!:=!:=!:=!:=3333****2222!!!!

n:=2

n=0 2222!:=!:=!:=!:=2222****1111!!!!

n:=1

n=0 1111!:=!:=!:=!:=1111****0000!!!!

n:=0

n=0 0000!:=!:=!:=!:=1111

N

N

N

T

Proces zagłębia siez poziomu n n n n na n n n n ---- 1111

az do osiągnięciapoziomu

elementarnego

poziom 1

poziom 2

poziom 3

R3.1

Silnia Silnia Silnia Silnia 3333!!!!

Algorytmy i Struktury Danych - wykład 20 Instytut Aparatów Elektrycznych Algrm3 Dr J. Dokimuk

3.2. Liczby Fibonacciego

Ciąg Fibonacciego 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... określa zaleŜność rekurencyjna Fib(n).

ZłoŜoność ciągu Fibonacciego rośnie wykładniczo

Aby obliczyć Fib(5), naleŜy wyznaczyć Fib(3) oraz Fib(4). Aby obliczyć Fib(4), obliczamy Fib(2) i Fib(3), stąd Fib(3) będzie liczone 2 razy

Aby obliczyć Fib(7), Fib(3) oblicza się 5 razy. Ogólnie dla Fib(n) wartość Fib(3) będzie liczona Fib(n-2) razy.

Wyznaczenie Fib(n) wymaga: Fib(n+1)-1 dodawań, 2*Fib(n+1)-1 wywołań funkcji Fib().

Algorytm rekurencyjny jest praktycznie przydatny dla małych wartości parametru.

Dla duŜych wartości parametru liczby Fibonacciego moŜna obliczać z przybliŜonego wzoru opartego na współczynniku ”złotych proporcji”.

Przyczyn ą nieefektywno ści algorytmu rekurencyj-nego jest powtarzanie tych samych oblicze ń, gdyŜ system zapomina co ju Ŝ wcześniej obliczono.

Lepszy efekt uzyskuje się zachowując wcześniejsze wyniki i wykorzystując je dla kolejnych obliczeń.

Iteracyjna implementacja algorytmu Fibonacciego zawiera n-2 przebiegi pętli, w których realizowane są:

-trzy podstawienia, -jedno dodawanie, -zwiększanie zmiennej i.

long Fib (int n) if (n<2) return n; else return Fib(n - 2) + Fib(n- 1)

nnnnF Φ⋅≈

Φ−Φ=

−=−

=+

55

ˆ)(

61803.02

51ˆ

61803.12

51

n Dodawanie Wywołanie 5 7 15 10 88 177 20 10945 21891 30 1346268 2692337 40 165580140 331160281

long FibIter(int n)

if (n < 2) return n; long i = 2, wrk, a = 1, b = 0;

for (; i<=n; i++)

wrk = a; a += b; b = wrk; return a;

Własności ciągu liczb Fibonacciego 1. KaŜda liczba ciągu jest sumą dwóch liczb bezpośrednio ją poprzedzających; 2. W wyniku podzielenia liczby ciągu przez jej poprzednik otrzymuje się iloraz oscylujący

wokół 1.618. Odwrotnością 1,618 jest 0.618. W miarę zwiększania się liczb, zmniejsza się odchylenie od tej wartości. Liczba 1.618 znana jest jako współczynnik ΜΜΜΜ ”złotych proporcji”.

3. Dowolna liczba ciągu podzielona przez liczbę o dwie pozycje wcześniejszą daje wynik 2.618, zaś przez liczbę o dwie pozycje późniejszą daje wynik 0.382.

F(5)

F(3)

F(1) F(2)

F(0) F(1)

0 1

F(4)

F(2) F(3)

F(0) F(1) F(1) F(2)

F(0) F(1)

0 1

0 1 1

1

Problem o rozmiarze n n n n rozbity zostaje na

dwa mniejsze podproblemy o rozmiarach nnnn----1111 i nnnn----2222

R3.2

≥−+−

=

=

=

2n dla 1)Fib(n2)Fib(n

1n dla1

0n dla 0

Fib(n)

Page 12: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 21 Instytut Aparatów Elektrycznych Algrm3 Dr J. Dokimuk

3.3. Rekurencja zagnieŜdŜona

Rekurencja zagnieŜdŜona występuje wówczas gdy funkcja jest zdefiniowana za pomocą samej siebie oraz jeŜeli uŜyta jest jako jeden z parametrów.

Dobrym przykładem rekurencji zagnieŜdŜonej jest funkcja Ackermanna:

−−=>−

=+=

hpozostalycdlamnAnAmindlamnA

ndlammnA

))1,(,1(0 0 ),1(

0 1),( 36553623

1622)1,4( −=−=A

Inna definicja funkcji Ackermanna jest następująca:

2,))1,(,(,1(),(

2)2,1()1,(

12),1(

≥−−=≥−=≥=

nmdlanmAmAmAnmA

mdlamAmA

ndlannA

162

22)2,3(

=N

A 6553622 222

22242

==N

3.4. Rekurencja końcowa

Rekurencja końcowa ma miejsce wówczas, gdy występuje tylko jedno wywołanie rekurencyjne na samym końcu funkcji.

Jest ostatnią wykonywaną instrukcją a wcześniej nie było innego wołania rekurencyjnego.

Rekurencja końcowa jest inną odmianą konstrukcji pętlowych i moŜe być przez nie zastąpiona.

3.5. Rekurencja pośrednia

Rekurencja pośrednia występuje wówczas, gdy funkcja f( ) wywołuje siebie poprzez łańcuch wywołań innych funkcji np.

funkcja F1() wywołuje funkcję H(), ta zaś wywołuje F1()

Łańcuch wywołań pośrednich moŜe być dowolnej długości:

F() h1() h2() … Hn() F()

_________________________________________________________________

Teoretycznie szybki algorytm rekurencyjny moŜe być realizowany znacznie wolnej niŜ jego odpowiednik iteracyjny.

Błędy algorytmów rekurencyjnych:

złe określenie warunku zakończenia procesu rekurencyjnego, nieefektywna dekompozycja problemu.

Implementacja rekurencyjna zapewnia prostotę i naturalność rozwiązania lecz jej wynik nie zawsze jest optymalny.

#include <stdio.h>

void RekKoncowa(int n) if (n>0) printf("%d ",n); RekKoncowa(n-1);

main( ) // P34 rekurencja koncowa int n=7; RekKoncowa(n); return 0 ; 7 6 5 4 3 2 1

#include <stdio.h>

void Non_RekKoncowa( int n) for (; n > 0; n--) printf("%d ",n);

main( ) int n=7; Non_RekKoncowa(n); return 0;

>≤+

==

4

4 ))2(2(

0 0

)(

ndlan

ndlangg

ndla

ng

Algorytmy i Struktury Danych - wykład 22 Instytut Aparatów Elektrycznych Algrm3 Dr J. Dokimuk

Anex – Implementacja rekurencji funkcji w C++

Funkcja main() wywołuje funcję F1(). Funkcja F1() wywołuje F2(). Funkcja F2() wywołuje F3(). Gdy funkcja F3() kończy działanie następuje powrót do F2(). Gdy kończy F2() powrót do F1(). Gdy kończy F1() następuje powrót do main().

Wołania rekurencyjne przebiegają identycznie a dotyczą działań na wielu kopiach jednej funkcji. Wywołanie funkcji związane jest z: -zapami ętaniem jej adresu powrotu ,

-przechowaniem zmiennych lokalnych i parametrów funkcji . JeŜeli funkcja F1() zawiera deklaracje zmiennej lokalnej x i wywołuje funkcję F2() równieŜ zawierającą zmienną lokalną x, to zmienne o identycznych nazwach muszą być rozróŜnialne.

Dane powyŜsze przechowywane są w obszarze zwanym rekordem wywołania lub ramk ą stosu. Obsługa wywołań funkcji w C/C++, realizowana jest poprzez dynamiczny przydział pamięci na stosie programu, gdzie przechowywane są rekordy wywołań (activation record).

Rekord wywołań istnieje tak długo, a Ŝ funkcja dla której został stworzony, zako ńczy swoje działanie.

Stanowi indywidualny bank danych dla funkcji, przechowując niezbędne informacje do jej poprawnego działania i powrotu.

KaŜde wywołanie funkcji związane jest z tworzeniem nowego rekordu wywoła ń, co zapewnia właściwą obsługę rekurencji.

Wywołanie rekurencyjne tworzy kolejne kopie z poprzednich, pochodzących od tego samego oryginału.

Tworzone kopie mają indywidualne rekordy wywoła ń, co sprawia, Ŝe są rozróŜnialne przez system.

Przykład. Podnoszenie liczby rzeczywistej do naturalnej potęgi n określa zaleŜność rekurencyjna.

Funkcja main() wywołuje funkcję Power(4.4, 2) – jest to "pierwszy poziom rekurencji".

Struktura rekordu wywoła ń funkcji 1. wartość zwracana: umieszczana bezpośrednio nad

rekordem wywołania procesu, wywołującego daną funkcję. 2. adres powrotu: adres instrukcji bezpośrednio po

wywołaniu; umoŜliwia powrót do miejsca wywołania. 3. dowi ązanie dynamiczne: wskaźnik do rekordu wywołania

procesu wywołującego funkcję. 4. warto ści zmiennych lokalnych: w przypadku gdy

przechowywane są w innym miejscu ich identyfikatory i informacje o miejscu ich ulokowania.

5. wartości parametrów funkcji: dla tablic adresy ich pierwszych elementów.

float Power( float x, int n) float wrk=-999; printf("poziom %d, wrk=%g\n", n,wrk); if (n==0) wrk= 1;

else wrk = x*Power(x, n-1); printf("POZIOM %d, wrk=%g\n", n,wrk); return wrk ; main() // P33a float f;

f = Power(4.4, 2); printf("Power=%g \n",f);

poziom 1 : Power(4.4, 2) poziom 2 : Power(4.4, 1) poziom 3: Power(4.4, 0)

poziom 3: 1 poziom 2: 4.4 poziom 1: 19.36

param. i zm. lokalne dowiązania dynam. adres powrotu

rekord wywołania

funkcji F3() warto ść zwracana

param. i zm. lokalne dowiązania dynam. adres powrotu

rekord wywołania

funkcji F2() warto ść zwracana

param. i zm. lokalne dowiązania dynam. adres powrotu

rekord wywołania

funkcji F1() warto ść zwracana main()

1. KaŜdy poziom rekurencji posiada swoje własne zmienne.

2. KaŜdemu wywołaniu odpowiada jeden powrót. Wykonanie return na poziomie 3, powoduje powrót na poziom 2 .

3. Instrukcje w funkcji rekurencyjnej znajdujące się przed miejscem, w którym funkcja wywołuje samą siebie, wykonywane są w kolejności wywoływania poziomów.

4. Instrukcje znajdujące się po miejscu, w którym funkcja wywołuje samą siebie, wykonywane są w kolejności odwrotnej do kolejności wywoływania poziomów.

>⋅=

=0dla n

0dla 11-n nx

nxn

poziom 2, wrk=-999 poziom 1, wrk=-999 poziom 0, wrk=-999

POZIOM 0, wrk=1 POZIOM 1, wrk=4.4 POZIOM 2, wrk=19.36

Page 13: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 23Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4. METODY PROJEKTOWANIA ALGORYTMÓW

Brak uniwersalnej metodologii budowy algorytmów, gdyŜ jest to proces indywidualny i twórczy.

Wiele algorytmów wykorzystuje określone schematy. Znając te schematy moŜna szybciej rozwiązywać nowe problemy, stosując znane

metody.

4.1. Strategia ”dziel i zwycięŜaj” (Divide and Conquer)

Istota strategii: podział problemu na mniejsze o tej samej strukturze co problem główny.

• Dzielenie podproblemów na coraz mniejsze aŜ otrzyma się zadanie tak małego rozmiaru, Ŝe jego rozwiązanie będzie oczywiste lub znana będzie efektywna metoda rozwiązania.

• Łączenie rozwiązania wszystkich podproblemów w całość.

Gdy problem zapisany jest w postaci rekurencyjnej, wówczas strategia ”dziel i zwyci ęŜaj” często moŜe być realizowana według schematu:

ZłoŜoność algorytmu rekurencyjnego, działającego według strategii ”dziel i zwyci ęŜaj” : • Dzielimy problem na k podproblemów o rozmiarze n/k kaŜdy. • Przyjmujemy, Ŝe jeŜeli rozmiar problemu jest mały tzn. n < c (c pewna stała) to na jego

rozwiązanie potrzebny jest stały czas, zatem jest klasy O(1).

α(n) –czas dzielenia problemu na k podproblemów.

β(n) –czas scalania rozwiązanych podproblemów.

4.1.1. Wyszukiwanie binarne (Binary Searching)

Problem: Sprawdzenie czy wartość key znajduje się w posortowanej tablicy o rozmiarze n. Dane: Klucz key, rozmiar tablicy n, tablica A z posortowanymi kluczami. Wynik: Zmienna ind lokalizująca element key w tablicy A (ind=-1, gdy brak element).

Poszukiwanie elementu w ciągu uporządkowanym:

a1, a2, a3,…an/2-1, an/2 an/2+1, an/2+2,…,an-1, an

przebiega zgodnie z regułą: IF key > an/2 then sprawdzaj prawy podciąg

else sprawdzaj lewy podciąg.

Algorytm rekurencyjny wyszukiwania binarnego wykorzystuje rekurencję końcową.

MoŜna ją zastąpić iteracją, co pozwala uniknąć budowania stosu, o głębokości do lg(n) + 1.

Niech n = 2k. Najgorszy przypadek wystąpi, gdy key jest większy (mniejszy) od wszystkich elementów tablicy.

JeŜeli n=1 nastąpi porównanie z key , a następnie rekurencyjne wołanie dla l > p, co kończy działanie.

key = 4 (czy key > 25)

2 4 8 11 14 25 27 33 34 45 55

Dziel : (czy key > 8) 2 4 8 11 14 Dziel: (czy key > 2) 2 4

Dziel: (czy key > 4) 4

Zwyci ęŜaj: idn = 2

BinSearch (array A, int Left, Right, key) do forever

if (Left >Right ) then return -1 m= (Right + Left )/2 if (key < A[m] then Left =m -1 else if (key > A[m]) then Left =m + 1 else return m;

Dziel: podział problemu na mniejsze ZwycięŜaj: rozwiąŜ mniejszy problem rekurencyjnie Scal: łączenie rozwiązanych podproblemów

Maksimum 4, 1, 6, 8, 2, 5, 7, 3

dziel 4 1 6 8 2 5 7 3 dziel 4 1 6 8 2 5 7 3

zwyci ęŜaj 4 8 5 7 zwyci ęŜaj 8 7 scal 8

>++≤

=cnnnknkT

cnOnT

dla)()()/(

dla)1()(

βα

BinarySRek(A[ ]; int l, p; type key) if (l > p) then return -1; // brak elementu

mid (l + p)/2;

if (A[mid]=key) then return mid;

if (key<A[mid]) then return BinarySRek(A, l, mid-1, key); else return BinarySRek(A, mid+1,p, key);

1)lg()(1dla1)2/(

1dla1)( +=⇒

>+

== nnT

nnT

nnT

Dziel i zwyciężaj

Zachłanne

Programowanie

dynamiczne

zpowrotami

ALGORYTMYALGORYTMYALGORYTMYALGORYTMY

Algorytmy i Struktury Danych - wykład 24Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

Sortowanie 8, 3, 6, 5, 4, 7, 2, 1

dziel 8 3 6 5 4 7 2 1 dziel 8 3 6 5 4 7 2 1 dziel 8 3 6 5 4 7 2 1

zwyci ęŜaj 3 8 5 6 4 7 1 2 zwyci ęŜaj 3 5 6 8 1 2 4 7 scal 1 2 3 4 5 6 7 8

4.1.2. Wyszukiwanie Maksimum i Minimum

Problem: Znaleźć wartość maksymalną i minimalną zbioru X. Dane: Tablica X[1..n] z danymi. Wyniki: Wartości max i min .

1. JeŜeli n = 1 to max = min = x1; 2. Dla n = 2, jeŜeli x1 > x2 to max = x1 przeciwnie max = x2;

3. Podziel wektor X na podwektory X1 oraz X2; 4. Wykonaj algorytm dla (X1, max1, min1); 5. Wykonaj algorytm dla (X2, max2, min2);

6. JeŜeli max1>max2 to max = max1, przeciwnie max=max2; 7. JeŜeli min1<min2 to min = min1, przeciwnie min = min2. Operacja dominująca algorytmu MaxMin : porównywanie .

ZłoŜoność czasowa: 22

3)(

2dla2)2/(2

2dla1

1dla0

)( −=⇒

>+==

= nnT

nnT

n

n

nT

na końcu porównuje się największy i najmniejszy element z obu podzbiorów.

4.1.3. Sortowanie przez scalanie

Dziel: podziel n-elementowy ciąg na dwa podciągi n/2-elementowe. ZwycięŜaj: wykonaj sortowanie podciągów, stosując technikę rekurencyjną. Scal: połącz dwa posortowane podciągi w jeden ciąg wyjściowy.

Powrót z wywołań rekurencyjnych rozpoczyna się z ciągami jednoelementowymi, które scalane są w ciągi dwuelementowe, następnie w ciągi wieloelementowe.

MergeSort (b, e, X[ ]) // tablica X[b…e]

if b < e then // b = e to długość 1

p (b + e)/2 // środkowy indeks MergeSort(b, p, X) // dziel X[b…p] MergeSort(p+1, e, X) // dziel X[p+1...e]

Merge(b, p, p+1,e, X) // scalanie podtablic

MaxMin(b, e; X[ ], min, max)

if b = e then min = max = x1

if e – b=1 then if x1<x2 then min=x1; max=x2 else min=x2; max=x1

p = (b + e)/2

MaxMin (b, p, X, min1, max1) MaxMin (p+1, e, X, min2, max2)

min = Min(min1, min2) max = Max(max1, max2)

50 25 18 40 88 19 4 72 15

50 25 18 40 88 19 4 72 15

50 25 18 40 88 19 4 72 15

50 25 18 40 88 19 4 72 15

25 50

18 25 50 40 88 40 88 40 88 40 88 4 19 4 19 4 19 4 19 15 72 15 72 15 72 15 72

18 25 40 50 88 4 15 19 72

4 15 18 19 25 49 50 72 88

25 50 25 50 25 50 25 50

poziom poziom poziom poziom 2222

poziom poziom poziom poziom 4444

poziom poziom poziom poziom 3333

poziom poziom poziom poziom 2222

poziom poziom poziom poziom 1111

poziom poziom poziom poziom 3333

poziom poziom poziom poziom 4444

poziom poziom poziom poziom 5555

poziom poziom poziom poziom 1111

R4.1

PP PPoo oodd ddzz zzii ii aa aałł łł

SS SScc ccaa aall ll aa aann nnii ii ee ee

dla n = 1 brak porównań dla n = 2 jedno porównanie

Page 14: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 25Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

void Merge (int first1, int last1, int first2, int last2, int A[ ]) int Temp[MaxArray], ind=0, ind1, ind2, num; ind1 = first1; ind2 = first2; num = last1 - first1 + last2 - first2 + 2;

while ((ind1 <= last1) && (ind2 <= last2))

if (A[ind1] < A[ind2]) Temp[ind++] = A[ind1++];

else Temp[ind++] = A[ind2++];

if (ind1 > last1) Move (ind2, last2, A, ind, Temp); else Move (ind1, last1, A, ind, Temp); Move(temp, 0, num-1, A, first1);

void Move (int lo1, int hi1, int W1[ ], int lo2, int W2[ ]) while (lo1 <= hi1) W2[lo2++] = W1[lo1++];

Scalanie dwóch posortowanych podciągów

Niech r, s oznaczają długości wejściowych podciągów X1, X2. W najgorszym przypadku złoŜoność scalania wynosi: T(r, s) = r + s -1

MoŜe to wystąpić, gdy s-1 elementów z X2 zostanie umieszczonych na Wy, a dopiero potem następuje umieszczenie r elementów z X1.

ZłoŜoność algorytmu MergeSort

Dla n = 1 złoŜoność jest O(1) (ciąg posortowany) w przeciwnym razie naleŜy rozpatrzyć 3 etapy. Dziel : Znalezienie środka nie zaleŜy od liczności zbioru, zatem α(n) = O(1). Zwyci ęŜaj: Rozwiązywane są 2 podproblemy o rozmiarze n/2. Łączny czas wynosi 2T(n/2). Scal : Liczba porównań zaleŜy od wzajemnej relacji elementów w obu podtablicach.

W najgorszym przypadku wynosi n – 1 dla n-elementowej tablicy wyjściowej.

)1()lg()(1dla1)2/(2

1dla0)( −−=⇒

>−+=

= nnnnTnnnT

nnT klasa O(nlg(n) )

4.1.4. Uwagi

Program P41a1 realizuje algorytm poszu-kiwania elementu w tablicy A, zgodnie ze strategią dziel i zwycięŜaj.

Problem główny rozbijany jest na podproblemy o analogicznej strukturze lecz mniejszym stopniu złoŜoności, gdyŜ z rozmiaru n schodzi się do n-1, poprzez sukcesywne zwiększanie zmiennej l.

Implementacja zrealizowana została z wykorzystaniem techniki rekurencyjnej, dla strategii dziel i zwycięŜaj.

Popełniony został istotny błąd: –nieefektywna dekompozycja problemu.

Merge(X1, X2, Xscalony) 1. Porównaj pierwsze elementy obu podciągów

i mniejszy wstaw do ciągu nowego. 2. Powtarzaj proces (1) aŜ jeden z ciągów będzie pusty. 3. Pozostały ciąg dołącz na koniec ciągu nowego.

2 4 16 18 11 13 15 20 30

2

4 16 18 11 13 15 20 30

2 4

16 18 11 13 15 20 30

2 4 11

16 18 13 15 20 30

2 4 11 13

16 18 15 20 30

2 4 11 13 15

16 18 20 30

2 4 11 13 15 16

18 20 30

2 4 11 13 15 16 18

20 30

2 4 11 13 15 16 18 20 30

int Szukanie_Lin_R (int A[ ] , int l, int p, int key) if (l > p) return -1; // brak elementu else if (A[l] == key) return l; else return Szukanie_Lin_R (A, l+1, p, key);

#include <stdio.h> int main() //P41a1 Liniowo-rekurencyjne szukanie int n=10, A[10] = 10,8,6,9,-11,15,13,19,33,23, key=13, l=0, p=n-1, res;

res = Szukanie_Lin_R(A, l, p, key); printf(" Znaleziono na pozycji=%d \n", res); Znaleziono na pozycji = 6

Algorytmy i Struktury Danych - wykład 26Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4.2. Algorytmy zachłanne (greedy algorithm)

Realizują najlepszą w danej chwili strategię wyboru a następnie rozwiązują podproblemy, wynikające z podjętej decyzji.

Nie analizują czy w następnych krokach działania teŜ będą optymalne.

Nie tracą czasu na analizowanie późniejszych skutków aktualnie podjętej decyzji. Wybierana jest ścieŜka lokalnie optymalna, zakładając, Ŝe moŜe ona prowadzić do rozwiązania

globalnie optymalnego.

Strategia algorytmów zachłannych nie gwarantuje optymalnych rozwiązań. Dla wielu problemów są wystarczająco dokładne w kontekście niezbędnych nakładów na ich opracowanie.

Metoda odpowiednia dla pewnej klasy zagadnień optymalizacyjnych jak:

• wybór najkrótszej drogi w grafie, • wyznaczenie optymalnej kolejności wykonywania pewnych czynności.

Strategia algorytmów zachłannych dobrze koresponduje z naturą ludzką. Człowiek otrzymując zadanie, często zadawala się rozwiązaniem szybkim i w miarę poprawnym, lecz niekoniecznie optymalnym.

4.2.1. Maksimum funkcji f(x)

1. Weź pewną liczbę a0 i oblicz wartość funkcji f(a0). 2. Powiększ liczbę bazową o stałą wartość, a0 = a0 +δ i oblicz wartość funkcji ponownie f(a0). 3. JeŜeli wartość funkcji narasta to kontynuuj krok (2), przeciwnie funkcja osiągnęła maksimum

dla wartości poprzedniej ( ai-1 ). JeŜeli otrzymane rozwiązanie nie stanowi globalnego maksimum to przynajmniej znaleziono maksimum lokalne.

4.2.2. Problem kasjera

NaleŜy wydać resztę wysokości Z. Dostępne są banknoty i monety w nominałach m1,m2,…mk, spełniające zaleŜność: m1 < m2 <…<mk,

NaleŜy określić najmniejszą liczbę nominałów niezbędną do wydania reszty Z.

Rozwiązanie: Kasjer rozpoczyna wydawanie reszty od największego nominału, utrzymując tę zasadę w kaŜdym kroku powstającej róŜnicy.

Rozwiązanie ma charakter metody zachłannej, gdyŜ w kaŜdym kroku minimalizowana jest liczba nominałów.

Kasjer wydał 5 monet co jest liczbą minimalną i algorytm zachłanny okazał się w tym przypadku optymalnym.

Czy algorytm kasjera jest optymalny globalnie dla kaŜdej struktury nominałów?

Dokonywane wybory mogą zaleŜeć od dotychczasowych decyzji lecz nie są uzaleŜnione od przyszłych wyborów lub metod rozwiązań podproblemów.

Rozwiązanie dokonywane jest w sposób zstępujący tzn. podejmowane są kolejne decyzje zachłanne, stopniowo redukując podproblemy do coraz mniejszych.

Metoda dowodzenia, Ŝe wybór strategii zachłannej prowadzi do rozwiązania optymalnego globalnie:

Dowodzi się, Ŝe moŜna problem sprowadzić do rozwiązania, rozpoczynającego się podjęciem decyzji zachłannej w pierwszym kroku.

Wykazuje się, Ŝe dokonany wybór redukuje problem do podobnego o mniejszym rozmiarze. Uzasadnia się przez indukcję, Ŝe strategię zachłanną moŜna stosować w następnych krokach.

JeŜeli wykaŜe się, Ŝe udało się sprowadzić problem do podobnego, lecz o mniejszych rozmiarach to dowód poprawności algorytmu sprowadzony został do udowodnienia optymalnej podstruktury rozwiązania optymalnego. Optymalną podstrukturę wykazuje problem, jeŜeli optymalne rozwiązanie jest funkcją

optymalnych rozwiązań podproblemów.

Własność wyboru zachłannego: za pomocą zachłannych wyborów (lokalnie optymalnych) moŜna uzyskać globalnie optymalne rozwiązanie.

Wydanie reszty w wysoko ści 87gr W pierwszym etapie wyda 50 gr Następnie wyda 20 gr (zostanie 17) Kolejno wyda 10 gr (zostanie 7) W końcu wyda 5 gr oraz 2 gr.

Page 15: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 27Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4.2.3. Problem plecakowy (knapsack problem) Ciągły problem plecakowy

Mamy n pojemników z substancjami vn, gdzie i-ty pojemnik waŜy wi kg i jego wartość wynosi pi zł. NaleŜy plecak o wyporności cap wypełnić najwartościowszymi substancjami.

Ciągłość problemu wynika z moŜliwości zabrania dowolnej ilości kaŜdej substancji.

Rozwiązanie ciągłego problemu plecakowego:

1. Oblicz wartość masy jednostkowej kaŜdej substancji pi / wi (wartość/waga)

2. Posegreguj substancje według rosnących cen jednostkowych : n

n

w

p

w

p

w

p ≥≥≥ L2

2

1

1

3. Wybierz najpierw największą ilość substancji najbardziej wartościowej. 4. JeŜeli wystąpi brak ‘najlepszej’ to weź następną pod względem ceny jednostkowej. 5. Wypełniaj plecak zgodnie z punktem (4) aŜ do jego zapełnienia.

Wymagane jest sortowania cen jednostkowych, zatem algorytm jest klasy O(nlg(n)).

Dyskretny problem plecakowy – dylemat złodzieja Mamy n róŜnych przedmiotów, gdzie przedmiot i-go typu waŜy w i kilogramów i jego wartość wynosi p i zł. NaleŜy wypełnić plecak takim przedmiotami, by ich łączna wartość była jak największa (suma rozmiarów zabranych przedmiotów nie moŜe przewyŜszać pojemności cap plecaka ).

Dyskretność problemu wynika z faktu, Ŝe nie moŜna zabrać ułamkowej części przedmiotu.

Próba rozwiązania dyskretnego problemu plecakowego Mamy 3 przedmioty oraz plecak o wyporności cap = 25kg.

Stosując strategię zachłanną jak dla problemu ciągłego, naleŜałoby jako pierwszy wybrać przedmiot nr 1.

Nie jest to rozwiązanie optymalne. Wybór optymalny to 2 i 3. Aby w dyskretnym problemie plecakowym osiągnąć optymalny wybór, naleŜy przed

podjęciem decyzji, który przedmiot zabrać: porówna ć rozwi ązanie podproblemu, w którym przedmiot występuje z podproblemem, w którym nie jest on brany jeszcze po uwag ę.

Rozwiązanie optymalne w dyskretnym problemie plecakowym nie jest strategią zachłanną.

Rekurencyjne rozwiązanie zakłada, Ŝe za kaŜdym razem, gdy wybiera się przedmiot i umiesz-cza go w plecaku, moŜna znaleźć optymalny sposób zapełnienia pozostałej części plecaka.

Mamy plecak o pojemności cap optymalnie spakowany. Wyjmujemy z niego przedmiot o rozmiarze s. Powstaje sytuacja jakbyśmy mieli plecak o pojemności space = cap - s wypełniony optymalnie.

W rzeczywistości nie wiemy, jak najlepiej spakować plecak o pojemności cap, zatem nie znamy wartości s.

I. Rezerwujemy w plecaku miejsce na 1-szy przedmiot, wypełniamy optymalnie resztę plecaka, a potem wkładamy 1-szy przedmiot i patrzymy na łączny koszt.

II. Następnie zaczynamy od nowa, zostawiając miejsce na 2-gi przedmiot, wypełniając plecak i dokładając 2-gi przedmiot. III… itd. dla wszystkich przedmiotów.

Rozwiązaniem problemu jest plecak o największej łącznej wartości spośród wypełnionych tym sposobem.

Niech Knap(cap) zwraca łączną wartość przedmiotów optymalnie wypełniających plecak o pojemności cap . Jest ona równa: max(Knap(cap - si) + ci), i = 1…n.

gdzie s i oraz c i – rozmiary i wartości przedmiotów. Program dla kaŜdego przedmiotu oblicza maksymalną wartość, którą moŜna uzyskać, a następnie pobiera największą z tych wartości.

Problem plecakowy wykazuje optymaln ą podstruktur ę. Niech w plecaku będzie najwartościowszy ładunek o masie M ≤ cap. Usuńmy z plecaka i-ty przedmiot. Pozostała zawartość o wadze cap – w i teŜ jest najwartościowszym ładunkiem jaki moŜna wybrać z n - 1 przedmiotów .

n wi [kg] pi [zł] pi/wi [zł/kg] 1 5 60 12 2 10 100 10 3 15 120 8

#include<stdio.h> #define nTyp 5 int size[nTyp] = 3, 7, 12, 15, 22 ; int cost[nTyp] = 5, 2, 10, 15, 30 ;

int Knap(int cap) int i, space, total, max; for (i=0, max=0; i < nTyp; i++) space = cap - size[i];

if (space >= 0) total = Knap(space) + cost[i]; if (total > max) max = total; return max;

int main() // P4_4c printf("maxVal=%d\n", Knap(50));

maxVal = 80

Algorytmy i Struktury Danych - wykład 28Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4.2.4. Problem wyboru zajęć

Poszukiwanie największego zbioru wzajemnie zgodnych czynności.

Dany jest zbiór S = 1, 2…, n zajęć, dla których przydzielona zostanie sala wykładowa, w której tylko jedno zajęcie moŜe odbywać się w danej chwili.

KaŜde zajęcie rozpoczyna się w chwili bi i kończy w chwili ei.

Wytypowane zajęcie o numerze i zajmuje prawostronnie otwarty przedział czasu [b i, ei). Zajęcia o numerach i oraz j są zgodne, jeŜeli przedziały [b i, ei) oraz [b j, ej) nie zachodzą na siebie.

Odpowiada to zaleŜnościom bi ≥ ej. lub bj ≥ ei mówiącym, Ŝe czas rozpoczęcia bi nie jest wcześniejszy niŜ czas zakończenia ej.

Problem wyboru zajęć sprowadza się do wyboru największego podzbioru parami zgodnych zajęć.

ZałoŜenie: zajęcia zostały uporządkowane ze względu na czas ich zakończenia: e1 ≤ e2 ≤ … ≤ en .

Wartości bi i ei przechowywane są w tablicach b i e.

Zbiór A zawiera wybrane zajęcia.

Istotą algorytmu jest wybieranie zawsze zajęć mających najwcześniejszy czas

zakończenia wśród zajęć, które mogą być dołączone do zbioru A.

Taki wybór w kaŜdym kroku maksymalizuje ilość wolnego czasu pozostającego do dyspozycji.

Jest algorytmem zachłannym (lokalnie optymalnym) i globalnie optymalnym.

ZłoŜoność czasowa algorytmu jest klasy O(n) (przy spełnionych załoŜeniach). Przykład 4.1.

Dany jest 8-elementowy zbiór zajęć 2-, 3- i 4-godzinnych oraz sala dostępna w godzinach 8 ÷ 20.

W 1-szym kroku do zbioru A przypisane zostanie zajęcie nr 1. JeŜeli czas rozpoczęcia b i zajęcia 2-go oraz dalszych jest wcześniejszy niŜ czas zakończenia ei zajęcia ostatnio przyjętego (pole kolorowane), to zajęcie takie jest odrzucane.

W przeciwnym razie jest ono wybrane i włączone do zbioru A.

Selector(b, e ) // zwraca zbiór A wybranych zajęć

n length(s) A 1 // wybranie zajęcia 1-go i wstawienie go do zbioru wynikowego A j 1 // przypisanie zmiennej j numeru zajęcia 1-go (wybranego)

for i 2 to n do if bi ≥ ej then A A ∪ i //dołącza do zbioru A i-tego zajęcia zgodnego, którego czas rozpoczęcia

//nie jest wcześniejszy niŜ czas zakończenia zajęcia ostatnio dodanego do A

j i // j zawiera informację o ostatnio dodanym do zbioru A zajęciu

return A

i nazwa dT bi ei

1 C1 3 815 11 2 C2 3 915 12 3 W1 2 1115 13 4 L1 4 1215 16 5 C3 3 1315 16 6 L1 4 1315 17 7 W1 2 1515 17 8 L2 4 1615 20

Page 16: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 29Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4.3. Programowanie dynamiczne

Programowanie oznacza tabelaryczną metodę rozwiązywania problemów i nie jest związane z programowaniem w określonym języku.

Wykorzystuje korzyści z rekurencyjnego formułowania problemu bez uŜywania rekurencji w procesie implementacji.

Problem rzędu N podzieli się na podproblemy o mniejszej złoŜoności i znanych rozwiązaniach. Mając rozwiązania problemów elementarnych naleŜy znaleźć optymalne rozwiązanie

podproblemu wyŜszego rzędu i kontynuować obliczenia, aŜ do rozwiązania problemu rzędu N.

Metoda rozwinięta została w latach 50-tych przez Richarda Bellmana. Jest rozwinięciem optymalności Bellmana, która zaleca aby na kaŜdym kroku podejmowana była najlepsza decyzja z uwzględnieniem stanu wynikającego z poprzednich decyzji.

Koncepcja programowania dynamicznego zbieŜna jest ze strategią dziel i zwyci ęŜaj, aczkolwiek dotyczy sytuacji, kiedy podprogramy zawierają te samy podproblemy.

Strategia dziel i zwycięŜaj w duŜo większym stopniu absorbuje zasoby komputerowe niŜ jest to rzeczywiście potrzebne, gdyŜ wielokrotnie rozwiązywany jest ten sam problem (liczby Fibonacciego).

Strategia programowania dynamicznego zakłada jednorazowe rozwiązanie danego podproblemu i zapamiętanie jego wyniku w tabeli, co eliminuje wielokrotne liczenie tego samego podproblemu.

Programowanie dynamiczne zmniejsza czas działania funkcji rekurencyjnych tak, Ŝe jest on równy co najwy Ŝej czasowi niezbędnemu do obliczenia funkcji dla wszystkich argumentów mniejszych (równych) od danego argumentu.

Metoda powyŜsza stosowana jest często do zagadnień optymalizacyjnych, w których moŜliwe jest wiele róŜnych rozwiązań, przy czym z kaŜdym z nich związany jest pewien koszt.

Metoda wstępująca (bottom-up) to technika rozpoczynająca obliczenia od najmniejszych podproblemów, poprzez coraz większe, zapamiętując po drodze wszystkie wcześniejsze rozwiązania w odpowiedniej tablicy.

Etapy projektowania algorytmów techniką programowania dynamicznego

1. charakterystyka struktury rozwiązania optymalnego; 2. rekurencyjna definicja kosztów (zapotrzebowania na zasoby) szukanego rozwiązania; 3. obliczenie optymalnych kosztów metod ą wstępującą; 4. budowanie optymalnego rozwiązania na podstawie wcześniejszych wyników, które powinny

być zapamiętywane w etapie 3-cim. Cechy problemu, zapewniające efektywność techniki programowania dynamicznego

1. Problem powinien wykazywa ć optymalną podstrukturę, która często sugeruje właściwą przestrzeń dla tworzonych podproblemów. Wybranie losowo jednej z wielu dost ępnych podprzestrzeni , moŜe być przyczyną wykonywania duŜo więcej obliczeń, niŜ jest potrzeba;

2. Odpowiednio mała przestrzeń istotnie ró Ŝnych podproblemów. Problem optymalizacyjny na własność wspólnych podproblemów, które w metodzie programowania dynamicznego obliczane są raz i zapamiętywane aby przy ponownym ich wystąpieniu moŜna było łatwo sięgnąć po rozwiązania.

Metodyka programowania dynamicznego

❶❶❶❶ koncepcja –budowa rekurencyjnego modelu rozwiązującego problem (z określeniem przypadków elementarnych), –budowa tablicy do przechowywania rozwiązań elementarnych lub podproblemów;

❷ inicjacja –zapełnianie tablicy obliczonymi wartościami, odpowiadającymi przypadkom elementarnym;

❸ realizacja –w oparciu o wartości wpisane do tablicy, obliczanie rozwiązań podproblemów coraz to

wyŜszego rzędu, uŜywając formuły rekurencyjnej, oraz wpisywanie ich do tablicy; –postępowanie kontynuowane jest aŜ do osiągnięcia poŜądanego rezultatu.

Programowanie dynamiczne eliminuje wszystkie przypadki obliczania tej samej wartości w procedurze rekurencyjnej, o ile moŜna zapamiętać wartości funkcji dla argumentów mniejszych od

argumentu bieŜącego jej wywołania.

Algorytmy i Struktury Danych - wykład 30Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4.3.1. Obliczanie ciągu Fibonacciego

❶❶❶❶ koncepcja: -wzór rekurencyjny jest powszechnie znany; -deklarujemy tablicę F[maxN] do przechowywania wartości elementarnych;

❷❷❷❷ inicjacja: -wartościami początkowymi będą warunki początkowe F[0] = 0 oraz F[1] = 1; ❸❸❸❸ realizacja: -rozwiązaniem problemów wyŜszego rzędu są wartości Fib(n) dla n ≥ 2, zapamięta-

wane jako: F[n] = F[n-1] + F[n-2]

Funkcja FibD1 realizuje programowanie dynamiczne wst ępuj ące, wyliczając w kaŜdym kroku bieŜące wartości funkcji na podstawie uprzednio obliczonych i zapamiętanych wartości. MoŜna zrezygnować z tablicy F, w przypadku rejestrowania ostatnich dwu wartości. ZłoŜoność obliczeniowa proporcjonalna do n. W programowaniu dynamicznym zstępującym funkcja rekurencyjna zapisuje kaŜdą obliczoną przez siebie wartość na końcu. Sprawdza zapisane wartości na początku, by nie obliczać jeszcze raz tych samych wartości.

Następuje redukcja liczby wywołań rekurencyjnych.

4.3.2. Dyskretny problem plecakowy

Mamy plecak o rozmiarze cap = 10 i zbiór przedmiotów (indeks i). kaŜda rzecz moŜe być wybrana co najwyŜej raz, kolejność rozpatrywanych rzeczy jest bez znaczenia.

Sprowadzamy problem do najmniejszego, czyli zaczynamy wypełniać plecak o pojemności 1kg . Tablica P[i, j] zawiera wartości j-tego plecaka wypełnionego rzeczami ∈∈∈∈ (1, i ).

Pola tablicy P wypełnia się wierszami : w 1-szym uwzględnia się tylko rzecz nr1, w 2-gim dysponujemy 2-ma rzeczami (nr1 i nr2),

w kolejnych wierszach narasta liczba rzeczy, które moŜemy dowolnie włoŜyć do plecaka. Tablica Q skojarzona jest z tablic ą P i pokazuje, które rzeczy zostały włoŜone.

Wartość Q[i, j] = 1 oznacza, Ŝe i-tą rzecz ostatnio włoŜono do plecaka o pojemno ści j . Tablica Q umo Ŝliwia identyfikację numerów rzeczy w plecaku poprzez ich odejmowanie.

Q5, 10 = 1 oznacza, Ŝe rzecz nr 5 o wadze 3 kg została włoŜona do plecaka. Nowa pojemność plecaka: space1=cap – 3 = 7, sprawdzamy element Q4, 7 (rzecz nr 4 2kg). Nowa pojemność plecaka: space2= space1 – 2 = 5, sprawdzamy element Q3, 5 (rzecz nr 3 3kg). Nowa pojemność plecaka: space3= space2 – 3 = 2, sprawdzamy element Q2, 2 (rzecz nr 2 2kg). Nowa pojemność plecaka: space4= space3 – 2 = 0, zatem wybrano rzeczy: 2, 3, 4, 5.

// metoda wstępująca

long FibD1(int arg)

if (arg <= 0) return 0; if (arg == 1) return 1; long F[47] = 0, 1 ; // inicjalizacja for (int i = 2; i <= arg; i++)

F[i] = F[i-1] + F[i-2] ;

return F[i -1];

Fib(46) = 1836311903

33331111 222210 5555 8888

++++

++++

++++

++++R4.2

13131313

long FibD2(int arg) // metoda zstępująca

if (arg <= 0) return 0; static long F[maxN]; // zerowanie tablicy F if (arg == 1) return F[1] = 1;

if (F[arg] != 0) return F[arg]; // czy juŜ jest wynik

if (arg > 1) return F[arg] = FibD2(arg-1) + FibD2(arg-2);

i 1 2 3 4 5 wartość pi 6 4 5 7 10 waga w i 6 2 3 2 3

P[i, j] –wartości plecaka o pojemności j

Narastające rozmiary plecaka j [kg] i /kg 1 2 3 4 5 6 7 8 9 10

1 /6 0 0 0 0 0 6 6 6 6 6 2 /2 0 2:4 2:4 2:4 2:4 1:6 1:6 1,2:10 1,2:10 1,2:10

3 /3 0 2:4 3:5 3:5 2,3:9 2,3:9 2,3:9 1,2:10 1,3:11 1,3:11

4 /2 0 4:7 4:7 2,4:11 3,4:12 12 16 2,3,4:16 16 1,2,4:17

5 /3 0 7 10 11 17 17 21 3,4,5:22 22 26

Q[i, j] –czy i-tą rzecz ostatnio włoŜono do j Narastające rozmiary plecaka [j ]

i 1 2 3 4 5 6 7 8 9 10

1 0 0 0 0 0 1 1 1 1 1

2 0 1 1 1 1 0 0 1 1 1

3 0 0 1 1 1 1 1 0 1 1

4 0 1 1 1 1 1 1 1 1 1

5 0 0 0 0 0 1 1 1 1 1

Page 17: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 31Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

Całkowita złoŜoność przedstawionego algorytmu plecakowego jest proporcjonalna do liczby elementów tablicy P[n, W], gdzie n jest liczbą rzeczy, zaś W maksymalną pojemnością plecaka. Nie istnieje Ŝadna relacja między n i W, stąd dla danego n wartość W moŜe być dowolnie duŜa.

Im wartość W staje się większa (cięŜarówka), tym efektywność algorytmu maleje, i moŜe być mniej wydajny od algorytmu siłowego (rozpatrzenia wszystkich podzbiorów).

Implementacja problemu plecakowego pozwala zastąpić tablice dwuwymiarowe P i Q wektorami. Ponadto kaŜda rzecz moŜe być wybrana dowolną liczbę razy.

Implementacją prowadząca do liniowej złoŜoności czasowej jest modyfikacja algorytmu rekurencyjnego Knap , poprzez zapisywanie w tablicy maxK obliczonych wcześniej wartości. Prowadzi to do ograniczenia liczby wywołań rekurencyjnych.

Pamiętane dodatkowo w funkcji Knap1 indeksy przedmiotów, umoŜliwiają odtworzenie zawartości plecaka po wykonaniu obliczeń, gdyŜ tablica S zawiera rozmiary przedmiotów wybrane przez funkcję. space=cap=17; w plecaku znajduje się przedmiot

o rozmiarze S[17] = 3; rozmiar plecaka po wyjęciu tego przedmiotu:

space – S[17] = 17 – 3 =14 S[14] = 7; space1 = 14; rozmiar plecaka po wyjęciu kolejnego przedmiotu:

space1 – S[4] = 14 – 7 = 7 S[7] = 7; Wybrano przedmioty A, C, C mające sumaryczną wartość 24, zatem optymalnie. Funkcja Knap2 realizuje metodę programowania wstępującego

Zaczyna od samego dołu tj. od plecaka o pojemności 0, i rozwaŜa kolejno pojemności aŜ do znamionowej cap , zapamiętując rozwiązania podproblemów w miarę ich obliczania w tablicy maxK.

Następnie odszukuje się zapisane wartości, gdy są one potrzebne, zamiast wykonywać wywołania rekurencyjne. Trzy kryteria wyboru kolejnych rzeczy do plecaka:

1. wybierać najcenniejsze rzeczy, czyli w kolejności nierosnących warto ści , 2. wybierać rzeczy zajmujące najmniej miejsca , czyli w kolejności niemalejących wag , 3. wybierać rzeczy najcenniejsze w stosunku do swojej wagi, czyli w kolejności nierosnących

wartości ilorazu warto ść/waga , czyli jednostkowej wartość rzeczy. PowyŜsze kryteria wyboru są przejawem strategii zachłannej, w której zawartość plecaka kompletowana jest krok po kroku i kaŜda decyzja dokonuje wyboru najlepszego na danym etapie, z oczekiwaniem, Ŝe ostatecznie doprowadzi to optymalnego rozwiązania. Strategia ta nie zawsze gwarantuje optymalne rozwiązanie dyskretnego problemu plecakowego.

Strategia programowania dynamicznego gwarantuje rozwiązanie optymalne.

#define num 5 int size[num] = 3, 4, 7, 8, 9; int cost[num] = 4, 5,10,11,13; int maxK[60], S[60];

int Knap1( int cap) // modyfikacja funkcji rekurencyjnej P4_4c1 int i, space, max, maxi = 0, t;

if (maxK[cap] != 0) return maxK[cap];

for (i = 0, max = 0; i < num; i++)

if ((space = cap - size[i]) >= 0)

if ((t = Knap1(space) + cost[i]) > max) max = t; maxi = i;

maxK[cap] = max;

S[cap] = size[maxi]; // rozmiary przedmiotów

return max; // zwraca max. wartość plecaka

maxVal = 24 dla cap = 17

cap S 0: 3 1: 3 2: 3 3: 3 4: 4 5: 4 6: 3 7: 7 8: 8 9: 9 10: 3 11: 3 12: 0 13: 3 14: 7 15: 0 16: 0 17: 3

0 1 2 3 4

s 3 4 7 8 9 c 4 5 10 11 13

A B C D E

#define num 5 int size[num] = 3, 7, 12, 15, 22 ; int cost[num] = 5, 2, 10, 15, 30 ;

int Knap2(int cap) //P4_4b // metoda wstepująca Int maxK[500] = 0, max, i, type, total, space;

for(i=1; i <= cap; i++) max = -3333; for(type=0; type < num; type++)

if((space = i-size[type]) >=0)

total = maxK[space] + cost[type]; if(max<total) max = total; maxK[i] = max > maxK[i-1] ? max : maxK[i-1];

return maxK[cap]; // zwraca max. wartość plecaka

maxVal = 80 dla cap = 50

Algorytmy i Struktury Danych - wykład 32Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4.3.3. Współczynniki dwumianu Newtona

Liczbę k–kombinacji zbioru n–elementowego opisuje rekurencyjna zaleŜność C(n, k).

Algorytm rekurencyjny na złoŜoność wykładniczą, wynikającą z powtarzających się wywołań funkcji.

Programowanie dynamiczne wymaga tablicy do przechowywania cząstkowych obliczeń.

Weźmy tablicę B(n, k), w której wiersze odpowiadają wartościom n, zaś kolumny wartościom k.

Wartość C(n, k) znajdziemy na skrzyŜowa-niu n-tego wiersza i k-tej kolumny.

Najpierw oblicza się pierwszy wiersz (0,0), następnie 2-gi wiersz pozycje: (1,0) i (1,1), dalej 3-ci wiersz pozycje: (2,0), (2,1), (2.2), itd. aŜ do znalezienia szukanej wartości. Algorytm wypełniania tablicy Pascala

-P(0,0) 1 -Wykonaj:

dla ka Ŝdego wiersza i ∈∈∈∈ (1, n) B(i, 0) B(i, i) 1

dla ka Ŝdej kolumny j ∈∈∈∈ (1, i - 1) B(i, j) B(i-1, j) + B(i-1, j-1)

Uwaga: Po obliczeniu wszystkich wartości w wierszu i nie są potrzebne wartości w wierszu i-1. Obliczając wartość C(n,k) moŜna uŜyć tablicę jednowymiarową, indeksowaną od 0 do k.

ZłoŜoność obliczeniowa algorytmu tablicowego

Tabelka obok pokazuje liczbę obiegów pętli for -j w zaleŜności od zmiennej i. Całkowitą liczbę obiegów określa zaleŜność:

nknkOknk

kknkk

kn

kkkk

≤⇒+−+=++−++

+−++++++++++++

czymprzy)(2

)22)(1()1)(1(

2

)1(

1

)1()1()1(432144444 344444 21

LL

Algorytm tablicowy ma złoŜoność klasy O(nk) czyli O(n2)

int Dwumian( int n, int k) // n ≥ k if ((n==k) || (k==0)) return 1; else return Dwumian(n-1,k)+Dwumian(n-1,k-1);

i j 0 1 2 3 4 5 6 7 8 9

0 1 1 1 1 2 1 2 1 3 1 3 3 1 4 1 4 6 4 1 5 1 5 10 10 5 1 6 1 6 15 20 15 6 1 7 1 7 21 35 35 21 7 1 8 1 8 28 56 70 56 28 8 1 9 1 9 36 84 126 126 84 36 9 1

void Pascal( int n, int B[ ][10]) int i, j; for (i=0; i<n; i++) B[i][0]=1; B[i][i]=1; ;

for (i = 1; i < n; i++)

for (j=1; j <= i-1; j++)

B[i][j] = B[i-1][j] + B[i-1][j-1];

<<−−+−

===

=

nk

kn

kn

kkn

kn

knC0dla

111

0lubdla1

),(

5,3

4,3 4,2

3,3 3,2 3,2 3,1

2,2 2,1+

+

+

+

+2,2 2,1 2,1 2,0+

+1111 1111 1111

1111

11111111 R4.3

1,1 1,0

11111111

1,1 1,0+ +

11111111

1,1 1,0

kjni

kj

jji

...0...0

0dla

0lubdla1

==

<<−−+−

===

1)j1,B(ij)1,B(ij)B(i,

i ∑∑∑∑j

0 1 1 2 2 3 3 4 … … k k+1

k+1 k+1 … … n k+1

Page 18: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 33Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4.4. Algorytmy z powrotami (backtracking algorithm) Poszukiwanie rozwiązań pośród całego zbioru moŜliwych przypadków.

Dana jest przestrzeń stanów, zawierająca rozwiązania problemu lub wskazująca metodę rozwiązania.

Znany jest sposób przechodzenia od jednego stanu do drugiego. Metoda pozwala na systematyczne testowanie wszystkich dróg wyjścia z danego stanu, w miarę jak odkrywamy, Ŝe niektóre z nich nie prowadzą do celu.

Wykonanie złego posunięcia moŜe doprowadzi ć do stanu, w którym nie ma rozwi ązania . NaleŜy wycofać ruch i kontynuować poszukiwania inną drogą. JeŜeli cofnięcie o krok nie pomaga, cofamy się o kolejny krok i poszukujemy w innym kierunku. NaleŜy zapamiętać wszystkie wykonane ruchy i odwiedzane stany aby moŜna było wycofać posunięcie.

W wielu problemach liczba moŜliwych stanów do odwiedzenia i dróg odwrotu moŜe być duŜa, stąd technika bezpośrednich powrotów moŜe okazać się kosztowna. Czasami moŜliwy jest inteligentny wybór następnego posunięcia, poprzez funkcj ę oceniaj ącą. Funkcja oceniaj ąca oblicza dla danego stanu prawdopodobie ństwo , Ŝe stan ten prowadzi do

rozwiązania problemu lub jakiej klasy rozwiązanie otrzymamy, jeŜeli szukamy rozwiązania optymalnego. Organizując przeszukiwania, moŜna tak wykorzystać funkcje oceniające aby uniknąć przeglądania nieistotnych fragmentów przestrzeni stanów.

4.4.1. Misjonarz i LudoŜerca

Na jednym brzegu rzeki znajduje się 3-ch misjonarzy i 2-ch ludoŜerców, zamierzających przeprawić się na drugi brzeg. Dostępna jest jedna łódka mieszcząca tylko dwie osoby .

Sterowa ć łodzi ą potrafi tylko misjonarz . Zakłada się, Ŝe jeŜeli na kaŜdym brzegu w danej chwili znajdzie się wiecej ludo Ŝerców

niŜ misjonarzy , to ci ostatni zostan ą zjedzeni. NaleŜy znaleźć rozwiązanie aby wszyscy bezpiecznie dotarli na drugi brzeg.

KaŜdy moŜliwa sytuacja czy stan na obu brzegach moŜe być kodowany trzema liczbami. Ułatwieniem w analizie i implementacji problemu moŜe być kodowanie poszczególnych stanów jedną liczbą ósemkową, gdzie poszczególne “trójki bitów” zawierają kolejno informacje o:

łodzi | ludo Ŝercach | misjonarzach 123 co w języku C daje liczbę: char kod = 0123;

NaleŜy pamiętać wszystkie stany realizacji przeprawy aby umoŜliwić wycofanie, gdy zajdzie potrzeba. Wykonanie ruchu wymaga analizy kolejnych moŜliwych stanów, celem znalezienia stanu poprawnego (uwzględniającego ograniczenia).

Po jego wykonaniu poszukujemy następnych efektywnych stanów aŜ dojdziemy do końca. JeŜeli znajdziemy się w stanie niedopuszczalnym cofamy się do poprzedniego i próbujemy wykonać inny ruch efektywny.

Problem został rozwiązany z jednym krokiem powrotnym. Sześć pierwszych kroków jest efektywnych. Stanu 7 uniemoŜliwia wykonanie efektywnego ruchu, stąd konieczność wycofania się do stanu 6 i pójście inną drogą (stan 6a), tzn. przeprawienie się dwóch misjonarzy, z których jeden powróci po ludoŜercę. Dalsze kroki prowadzą do sukcesu.

Misjonarze i LudoŜercy

Nr Kod Lewy brzeg Rzeka Prawy Uwagi 1 123 3M 2L 1B 0 0 ruch efektywny 2 012 2M 1L 0B 1M & 1L 1M 1L ruch efektywny 3 113 3M 1L 1B 1M 1L ruch efektywny 4 011 1M 1L 0B 2M 2M 1L ruch efektywny 5 112 2M 1L 1B 1M 1M 1L ruch efektywny 6 001 1M 0L 0B 1M & 1L 2M 2L ostatni efektywny ruch 7 103 3M 0L 1B 2M 0M 2L stan nieefektywny

8 (6a) 010 0M 1L 0B 2M 3M 1L powrót i zmiana stanu 6 7a 111 1M 1L 1B 1M 2M 1L znowu ruch efektywny 8a 000 0M 0L 0B 1M & 1L 3M 2L 1M ruch efektywny

Przykładem jest gra w szachy, gdzie przechodzi się od jednego stanu do drugiego, poprzez zmianę ustawień figur. Celem poszukiwań jest znalezienie stanu, z którego przeciwnik nie ma wyjścia. Przeszukując przestrzeń stanu, wykonuje się wiele ruchów, gdyŜ do kaŜdego stanu prowadzi wiele dróg.

Algorytmy i Struktury Danych - wykład 34Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

W pracy [2] zastosowano kodowanie jedną liczbą w zapisie czwórkowym, gdzie poszczególne “dwójki bitów” to informacje o: łodzi | ludo Ŝercach | misjonarzach ALL = 1234 = 2710

#include <stdio.h> #include <stdlib.h> #define MIS 1 #define CAN 4 #define BOAT 16 #define ALL BOAT+2*CAN+3*MIS //1 10 11 #define NONE 0 #define SUBSET(x,y) ((x)%4 <= (y)%4 && (x)/4%4 <= (y)/4%4 && (x)/16 <= (y)/16) int n, position[ALL+1], visited[ALL+1]; void printSide( int p) printf("%*dM %dC %dB\n", n+1, p%4, (p/4)%4, p/16);

int validMove( int pos1, int pos2) /* Returns 1 if move is valid from pos1 to pos2, pos2 is legal, and has not already been seen. Otherwise returns 0. */ //change in position int diff = (SUBSET(pos2,pos1)?pos1-pos2:(SUBSET(pos1,pos2)?pos2-pos1:0)); int mis2 = pos2%4; //missionaries in pos2 int can2 = pos2/4%4; //cannibals in pos2 return(!visited[pos2] //have not been there already

&& (diff==BOAT+MIS || diff==BOAT+MIS+CAN || diff==BOAT+2*MIS) //valid move

&& can2 <= 2 //pos2 has at most two cannibals && (mis2 == 0 || can2 <= mis2) //left bank missionaries uneaten && (3-mis2 == 0 || 2-can2 <= 3-mis2)); //right bank missionaries uneaten

int main() // P4.2 position[0] = ALL; //initial position is under consideration visited[ALL] = 1; printSide(position[0]); position[1] = NONE; //start with the first possible next move n = 1; while(position[n-1] != NONE) while(position[n]<=ALL && !validMove(position[n-1],position[n])) position[n]++; //loop until a valid next move, or no move

if(position[n]<=ALL) //there is a valid next move visited[position[n]] = 1; //mark that it is being considered

printSide(position[n]); position[++n] = NONE; //start its next moves at first possible one

else // no valid next move visited[position[--n]] = 0; // no longer considered prev state, backtrack if(n==0) return 0; position[n]++; //advance previous state's next move

return 1;

3M 2C 1B 2M 1C 0B 3M 1C 1B 1M 1C 0B 2M 1C 1B 1M 0C 0B 3M 0C 1B 0M 1C 0B 1M 1C 1B 0M 0C 0B

Page 19: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 35Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

4.4.2. Problem 8 Hetmanów NaleŜy ustawić na szachownicy 8 hetmanów tak aby Ŝaden z nich nie szachował innych. Hetman moŜe zbić inną figurę, jeŜeli stoi ona w tym samym rzędzie, kolumnie lub na przekątnej. Próba rozwi ązania problemu

Umieszczamy 1-go hetmana na szachownicy; następnie 2-go tak aby nie szachował 1-go; później 3-go tak aby nie szachował innych i sam nie był szachowany itd.

Niech i–ty hetman nie da się ustawić bez kolizji. Wówczas zmieniamy pozycję poprzedniego tak długo aŜ uda się umieścić i-tego bezkolizyjnie.

JeŜeli to nie pomaga wracamy do i-2, i-3, i rozpoczynamy od nowa. Problem moŜna zapisać rekurencyjnie, w postaci zwartego algorytmu HETMAN.

Dla Hetmana szachownica to rząd (row ) i kolumna (col ) oraz przekątne (diag ) lewe i prawe, które są jego własnością w chwili gdy stoi na określonym polu.

RozwaŜmy szachownicę 4 x 4 z czterema Hetmanami, której pola ponumerowane są zgodnie z konwencją indeksowania tablic w języku C.

Indeksy lewych przekątnych sumują się zgodnie z ir + jc, przy czym jest to numer tej przekątnej np.: ir + jc=2 .

Szachownica zawiera 7 lewych przekątnych, którym przypisujemy numery od 0 ÷÷÷÷ 6, czemu odpowiada tablica LDiag[7].

Indeksy prawych przekątnych mają róŜnice zgodnie z ir - jc co pozwala przypisać im numery od –3 ÷÷÷÷ 3. Tworzymy tablicę Pdiag[7], lecz do indeksów obliczonych ze wzoru ir – jc naleŜy dodać stałą liczbę norm, likwidującą ujemne wartości.

Dodatkowa tablica PosInRow[row] kojarzy wiersz z aktualnym stanem kolumny. Nie potrzeba pamiętać stanu wierszy, gdyŜ i–ty Hetman przesuwa się wzdłuŜ rzędu i-tego .

Hetmany o numerach < od i–tego są juŜ ustawione w wierszach o numerach mniejszych od i .

Tablica 4.6 przedstawia kolejne wykonane kroki przez funkcję Hetman, prowadzące do pierwszego udanego ustawienia czterech Hetmanów.

HETMAN(row)

for kaŜda pozycja col w rzędzie row if pozycja col nie jest szachowana then umieść kolejnego Hetmama na col if row < 8 then Hetman(row+1) else Disp else Usuń_Hetmana z pozycji col

void Hetman(int ir) int jc, norm=3, OK=1; for (jc=0; jc <= 3; jc++)

if (Col[jc]==OK && LDiag[ir+jc]==OK && RDiag[ir-jc+norm]==OK) //testuje dostępności pól PosInRow[ir] = jc; // lokalizacja Hetmana

Col[jc]=!OK; LDiag[ir + jc]=!OK; RDiag[ir-jc+norm]=!OK; //markuje pola jako niedostępne if (ir < 3) Hetman(ir+1); else Wynik();

Col[jc] = OK; LDiag[ir + jc] = OK; RDiag[ir-jc+norm] = OK; //przywraca dostęp do pól

(1) (2) (3) (4) (8) 1111 1111 1111 1111 ………… 1111

2222 2222 2222 … 2222 3333 … 3333

… 4444

Tablica 4.6 Rruch Nr H r c 1 1 0 0 2 2 1 2 no 3 2 1 3 4 3 2 1 no 5 1 0 1 6 2 1 3 7 3 2 0 8 4 3 2

0,0 0,1 0,2 0,3

1,0 1,1

1,2 1,3

2,0 2,1 2,2 2,3

3,0 3,1 3,2 3,3

8 ir=3, jc=2, Adr 4 ir=2, jc=1, Adr 7 ir=2, jc=0, Adr 2 ir=1, jc=2, Adr 3 ir=1, jc=3, Adr 6 ir=1, jc=3, Adr 1 ir=0, jc=0, Adr0 1 ir=0, jc=0, Adr0 5 ir=0, jc=1, Adr0

(2) (4) (8) Zawartość ramek wywołań funkcji Hetman

Algorytmy i Struktury Danych - wykład 36Instytut Aparatów Elektrycznych Algrm4 Dr J. Dokimuk

Działanie funkcji Hetman

1. Pierwsze wołanie Hetman(0) umieszcza 1–go H na poz. (0,0). Warunek pętli if jest spełniony, zerowa kolumna, prawa diagonala i skrajna lewa przekątna otrzymują status zajęte !OK .

2. Rekurencyjne wołanie Hetman(1), gdyŜ ir=1<3, tworzona jest ramka wywołania zawierającą: adres powrotu i wartość zmiennych ir, jc. Sprawdzana jest dostępność pola w 2-gim wierszu. Dla ic = 0 niedostępna jest kolumna zerowa, dla jc=1 niedostępna jest prawa diagonala. Dla jc = 2 warunek if jest spełniony i 2-gi H na pozycję (1,2), odpowiednie pola markowane są jako !OK.

3. Kolejne wywołanie Hetman(2), gdyŜ ir = 2<3 i próba umieszczenia hetmana w rzędzie 2. Warunek pętli if nie jest spełniony (brak wolnych miejsc), następuje wyjście z bloku if i koniec pętli for, a tym samym kończy się aktualne wywołanie funkcji Hetman(2).

Sterowanie przekazywane jest do miejsca wywołania, a więc do funkcji Hetman(1 ) zajmującej się rzędem nr 1 (drugi fizycznie).

4. Odtworzone zostają ze stosu wartości zmiennych ir oraz ic . Kontynuowane jest wykonanie 2-go wywołania funkcji Hetman(1), pola tablic otrzymują status OK, jc = 2 zatem pętla for działa dalej i po pozytywnym sprawdzeniu warunku if , 2-gi H umieszczony zostaje na poz. (1, 3).

5. Kolejne wywołanie Hetman (2), z ponownym ir = 2<3, umieszcza 3-go H na poz. (2, 1). Następne wywołanie Hetman(3) w celu ustawienia 4-go H jest nieskuteczne . Koniec wywołań.

6. Powrót do wywołań z kroku 4 i ponowna, bezskuteczna próba przesunięcia 3-go hetmana. Zmienna ic = 3 i pętla for kończy bieg.

7. Następuje aktywacja pierwszego wywołania Hetman(0) i ustawienie 1-go H na poz. (0,1). Dalsze działania przebiegają bez zakłóceń, prowadząc do prawidłowego rozwiązania.

#define OK 1 #define size 4 #define norm size-1 int Column[size], PosInRow[size], Count = 0; int LDiag[2*size - 1], RDiag[2*size - 1];

void Disp() int col, row; for (row = 0; row < size; row++) for (col = 0; col < PosInRow[row]; col++) putchar('.'); putchar('Q');

for (col = PosInRow[row] + 1; col < size; col++) putchar('.'); putchar('\n'); putchar('\n'); Count++;

void Queen(int row) int col; for (col = 0; col < size; col++) if (Column[col] == OK && LDiag[row+col] == OK && RDiag[row-col+norm] == OK) PosInRow[row] = col; Column[col] = LDiag[row+col] = RDiag[row-col+norm] = !OK;

if (row < size-1) Queen(row+1); else Disp();

Column[col] = LDiag[row+col] = RDiag[row-col+norm] = OK;

int main() int i; for (i = 0; i < size; i++) PosInRow[i] = -1; for (i = 0; i < size; i++) Column[i] = OK; for (i = 0; i < 2*size - 1; i++) LDiag[i] = RDiag[i] = OK; Queen(0); printf("%d solutions\n", Count);

.Q..

...Q Q... ..Q.

..Q. Q... ...Q .Q.. 2 solutions

.......Q .Q...... ....Q... ..Q..... Q....... ......Q. ...Q.... .....Q.. .......Q ..Q..... Q....... .....Q.. .Q...... ....Q... ......Q. ...Q.... .......Q ...Q.... Q....... ..Q..... .....Q.. .Q...... ......Q. ....Q... 92 solutions

Page 20: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 37 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

5. SORTOWANIE Sortowanie jest procesem ustawiania zbioru obiektów w określonym porządku.

Dana jest lista obiektów Q = [X1, X2, X3, ......, Xn ], będąca podzbiorem zbioru X. Sortowanie moŜe polegać na przestawianiu obiektów tak długo aŜ uzyska się ich ustawienie w ciągu niemalejącym X1 ≤ X2 ≤ X3 ≤......≤ Xn według zadanego klucza (funkcji sortującej).

W praktyce klucz sortujący jest przechowywany w postaci jawnej, jako składowe określonego pola obiektu. Wygodną formą opisu obiektu jest struktura. Dla analizy algorytmów sortowania istotne jest zdefiniowanie składowej struktury z kluczem, pozostałe pola są nieistotne.

Typ klucza dopuszcza duŜą dowolność np. char, integer, double.

MoŜna uniknąć opisu obiektów w postaci rekordów gdy elementami zbioru Q są: • pary wartości [xi, yi ], gdzie kluczem jest x i dla 1 ≤ i ≤ n. • liczby całkowite lub rzeczywiste znajdujące się w tablicy X[1..n].

Stabilna metoda sortowania: jeŜeli podczas sortowania pozostaje nie zmieniony początkowy porządek ustawienia obiektów o identycznych kluczach drugoplanowych. Dotyczy to sytuacji, gdy obiekty są juŜ uporządkowane według drugoplanowych kluczy.

Przykładem moŜe być tablica rekordów zawierająca listę studentów i wyniki egzaminów. W wyniku sortowania Ŝądamy dodatkowo aby nazwiska studentów mających te same oceny były podawane w porządku alfabetycznym.

JeŜeli sortowanie dotyczy obiektów znajdujących się w pamięci wewnętrznej np. tablic, to istotniejszym kryterium wyboru metody jest oszczędne korzystanie z pamięci, co oznacza, Ŝe przestawianie obiektów w procesie sortowania powinno być realizowane in situ.

Kryteria efektywności algorytmów sortowania: 1. liczba porównań klucza Po,

2. liczba przestawień Ps, // swap(a, b) ≡

3. liczba przesunięć obiektów Pr.

Kryteria efektywności są funkcjami liczby sortowanych obiektów: T(n) = f(n)

Metody sortowania moŜna podzielić na: proste, wymagające liczby porównań kluczy rzędu n2; efektywne, wymagające liczby porównań rzędu n⋅⋅⋅⋅lg(n).

Sens stosowania metod prostych: • implementacje są krótkie i zrozumiałe, • dobrze opisują i wyjaśniają pryncypia metod sortowania, • metody efektywne zawierają więcej operacji i dla małych n mogą być wolniejsze.

Poprawne drzewo: przy sortowania n kluczy dla kaŜdej permutacji tych kluczy zawiera ścieŜkę od korzenia do liścia.

Lemat 5.1 . Dla kaŜdego determistycznego algorytmu sortującego n róŜnych kluczy istnieje odpowiadające mu drzewo decyzyjne, zawierające dokładnie n! liści.

Lemat 5.2 .... Pesymistyczna liczba operacji porównania kluczy w drzewie decyzyjnym jest równa głębokości drzewa.

Lemat 5.3. JeŜeli n jest liczbą liści w drzewie decyzyjnym to głębokości drzewa h = lg(n) Twierdzenie 5.1. Determistyczny algorytm sortujący n kluczy wyłącznie poprzez porównywanie ich

wartości w najgorszym przypadku wykonuje lg(n!) porównań. Lemat 5.4 . Dla dodatniej liczby całkowitej n zachodzi: lg(n!) ≥≥≥≥ nlg(n) – 1.45n

Dowód: ∫ −≥+−

=≥∑==

nn

innn

nnndxxin

1245.1)lg(

)2ln(

1)ln()lg()lg()!lg(

Twierdzenie 5.2. Determistyczny algorytm sortujący n kluczy wyłącznie poprzez porównywanie ich wartości w najgorszym przypadku wykonuje nlg(n) – 1.45n porównań.

6 12 23 23 12 6

11 12 13 14 15 16

struct Osoba int wiek; char[30] nazwisko; char[20] zawod; double waga; ;

void swap (typ x1, typ x2)

typ temp = x1; x1 = x2; x2 = temp;

x1#x2

x3,x1,x2

T

T

T

T

TN

N

N

N

N

x3#x1 x3#x2

x3#x1x2#x3

x1,x2,x3

x3,x2,x1

x1,x3,x2 x2,x3,x1 x2,x1,x3

Algorytmy i Struktury Danych - wykład 38 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

5.1. Sortowanie przez wstawianie

Metoda stosowana automatycznie przy grze w karty, podczas układania ich w rozdaniu. Obiekty sortowane dzielone są umownie na dwa podciągi o zmieniającej się liczności.

Jeden to podciąg wynikowy (uporządkowany) x1,.≤≤≤≤..xi-1, drugi to podciąg źródłowy (nieuporządkowany) xi,....., xn.

Dla kaŜdego i począwszy od i=2 i-ty element podciągu źródłowego przenoszony jest do podciągu wynikowego i wstawiany w odpowiednie miejsce.

6 12 23 28 33 25 18 1 48

Istotą metody jest przesuwanie wielu elementów w prawo o 1 pozycję a następnie wstawienie wybranego.

Algorytm sortowania przez wstawianie jest stabilny.

Jest to algorytm klasy O(n2) i praktycznie nie nadaje się do sortowania duŜych tablic.

Biorąc pod uwagę jego prostotę i naturalność moŜe być przydatny dla małych zbiorów.

Tablica 5.1 zawiera przykład sortowania przez wstawianie 6 losowo pobranych liczb. Proces sortowania przebiega w 5-ciu krokach (n-1).

Podciągi z prawej strony są podciągami źródłowymi o zmniejszającej się liczbie elementów w kaŜdym kroku .

Proste wstawianie Miejsce do wstawienia elementu podciągu źródłowego moŜna przygotować poprzez naprzemienne wykonywanie operacji porównywania i przesuwania (przesiewanie).

Liczba porówna ń w i-tym kroku: 1<Po# i - 1, średnio 0.5i

Elementy X[1]…X[i-1] to podci ąg wynikowy. Elementy X[i]…X[n] to podci ąg źródłowy .

Indeks i wskazuje element aktualnie wstawiany; przesuwa się od lewej do prawej.

W kaŜdej iteracji pętli for element X[i] jest pobierany z tablicy i przypisywany do zmiennej roboczej v.

Porównuje się element v z poprzednim elementem X[j] , poczym albo wstawia się v albo przesuwa X[j] na prawo.

Zakończenie przesiewania: ➊ znaleziono element xi mniejszy od v; ➋ osiągnięto lewy koniec podciągu wynikowego.

Liczba porówna ń Po i przesuni ęć Pr dla tablicy X[n]: Pomin = n – 1 Posr = 0.25(n2 + n - 2) Pomax = 0.5(n2 + n) - 1 Prmin = 2(n-1) Prsr = 0.25(n2 +9n - 10) Prmax = 0.5(n2 + 3n - 4)

W/wym zaleŜności pokazują, Ŝe czas sortowania jest:

–najmniejszy gdy elementy są juŜ uporządkowane, –największy gdy są ustawione w kolejności odwrotnej.

for i 2 to length(X) do v X[i] v w odpowiednie miejsce:

x1,..,xi-1

Tablica 5.1 Początek: 22 15 12 30 40 33

Krok 1: 15 22 12 30 40 33

Krok 2: 12 15 22 30 40 33

Krok 3: 12 15 22 30 40 33

Krok 4: 12 15 22 30 40 33

Krok 5: 12 15 22 30 33 40

InsertSort1(X) // P5.1a // i –indeks zbioru źródłowego // j –indeks zbioru wynikowego // v –zmienna robocza

X[0] -32000 // wartownik

for i 2 to length(X) do v X[i] j i – 1

while v < X[j] do X[j+1] X[j] j j – 1

X[j+1] v

// bez wartownika byłoby:

// while v < X[j] && j > 0 do

Wartownik jest elementem ciągu i pilnuje jego końca, czyli

powstrzymuje przeszukiwania przed wyjściem poza ciąg.

Page 21: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 39 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

Polepszenie szybkości sortowania przez wstawianie moŜna osiągnąć optymalizując metodę znajdowania właściwego miejsca w podciągu wynikowym (uporządkowanym) do wstawiania obiektu podciągu źródłowego.

Jedną z moŜliwości moŜe być zastosowanie tzw. techniki wstawiania połówkowego (InsertSort2).

Wstawianie połówkowe Metoda wykorzystuje informację, Ŝe podci ąg wynikowy jest zawsze uporządkowany. Proces rozpoczyna się próbkowaniem podciągu wynikowego w środku.

JeŜeli miejsce to nie jest właściwe do wstawienia nowego elementu, dzieli się go dalej na połowę aŜ znajdzie się właściwą pozycję.

Miejsce do wstawienia nowego elementu uwaŜa się za znalezione, jeŜeli zachodzi relacja xj ≤ v ≤ xj+1.

Po = n[lg(n) - lg(e)±0.5] zaś Pr jest ciągle rzędu n2.

Metoda wstawiania połówkowego zmniejsza tylko liczbę porównań a nie przesunięć, które są bardziej czasochłonne.

W pewnych sytuacjach moŜna się spodziewać się gorszych wyników niŜ w metodzie prostego wstawiania.

Analiza zło Ŝoności algorytmu sortowania przez proste wstawianie Ins ertSort1

ci dla i∈(1, 7) –koszt wykonania wiersza programu

ki – liczba testowań warunku pętli while

Przypadek optymistyczny – ciąg uporządkowany:

Dla kaŜdego i ∈ (2, n) zachodzi relacja:

v ≥ X[j] jeŜeli j = i – 1. stąd ki = 1 dla i ∈ (2, n).

Tym samym minimalny czas działania wynosi:

T(n) = n·c1+c2(n-1) + c3(n-1)+c4(n-1)+c7(n-1) = = (c1+c2+c3+c4+c7)n – (c2+c3+c4+c7) =

= O(n) + O(c)

Algorytm sortowania przez wstawianie jest klasy O(n) dla zbioru posortowanego.

Przypadek pesymistyczny wyst ępuje dla ci ąg uporz ądkowanego w kierunku malejącym.

KaŜdy element tablicy X[i] naleŜy porównać z kaŜdym elementem podtablicy X[1…i-1], stąd ki = i dla i ∈∈∈∈ (2, n).

Z teorii sum mamy: 12

)1(

2

−+=∑=

nn

ii

n

oraz 2

)1()1(

2

−=∑=

− nn

ii

n

T(n) = c1n+c2(n-1)+c3(n-1)+c4[0.5n(n+1)-1]+c5[0.5n(n-1)]+c6[0.5n(n-1)]+c7(n-1) =

= O(n2) + O(n) + O(c) = O(n2) + O(n)

InsertSort2(X) // P5.1b. wstawianie połówkowe

for i2 to length (X) do vX[i] l1 pi-1 while l ≤ p do m (l + p) div 2 if v<X[m] then pm-1 else lm+1 for ji-1 downto l do X[j+1]X[j] X[l]v

InsertSort1(n, X) Lp Instrukcja Koszt Liczba wykonań

1 for i2 to n do c1 n 2 v X[i] c2 n-1 3 ji – 1 c3 n-1

4 while v < X[j] do c4 ∑=

n

iik

2

5 X[j+1]X[j] c5 ∑ −=

n

iik

2)1(

6 jj-1 c6 ∑ −=

n

iik

2)1(

7 X[j+1]v c7 n-1

112

−=∑=

ni

n

6 12 23 28 33 2 48 6 12 23 28 33 2 48 6 12 23 28 33 2 48

Algorytmy i Struktury Danych - wykład 40 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

5.2. Sortowanie bąbelkowe

Porównywanie i zamiana par s ąsiaduj ących ze sobą elementów trwa tak długo aŜ wszystkie będą posortowane. Tablica przeglądana jest wielokrotnie (pętla i) od końca do początku (pętla j), przy czym za kaŜdym razem najmniejszy element lokowany jest sukcesywnie od lewej strony tablicy.

JeŜeli postawimy tablicę pionowo to elementy wypychane są do góry jak bąbelki na pozycje proporcjonalne do ich wagi (najlŜejszy zawsze na górze) – stąd nazwa metody.

(n – 1)*(n – 1) = n2 – 2n + 1 O(n2)

Po = 0.5(n2 - n) Prmin = 0 Prsrednia = 0.75(n2 - n) Prmax = 1.5(n2 - n)

Dwie istotne wady metody BubbleSort: często występują niepotrzebne przebiegi gdyŜ elementy zostały juŜ posortowane (w tabeli

5.2. trzy ostanie przebiegi są juŜ zbędne, gdyŜ stan posortowania uzyskano juŜ dla i = 5);

wraŜliwa na strukturę danych: X1 =(8 4 5 22 30 66 50) sortuje w 1-szym przebiegu;

X2 = (8 5 22 30 50 66 4 ) wymaga 2-ch przebiegów.

MoŜliwości poprawy algorytmu to między innymi: • zapamiętanie czy w trakcie przejścia dokonano jakiejkolwiek permutacji - przejście, w

którym nie ma zmian wskazuje, Ŝe proces sortowania moŜe być zakończony;

• zapamiętanie indeksu ostatniej permutacji (k = j) – pozwala ograniczać zbędne przebiegi, gdyŜ wszystkie obiekty poniŜej tego indeksu (k) są uporządkowane.

Następne przejście moŜna zakończyć na tym indeksie zamiast iść do aktualniej granicy i. • przełączanie kierunku przeglądania tablicy – łagodzi niekorzystne konfiguracje danych.

Tablica uporządkowana z wyjątkiem „lekkiego elementu ” na końcu X1= 4 12 16 22 44 55 2 posortowana zostanie w 1-szym przebiegu.

Z „cięŜkim ” na początku X2= 55 2 4 12 16 22 44 w 6-ciu (X2(1-szy) = 2 55 4 12 16 22 44). Algorytm sortowania bąbelkowego, poprawiony wg w/wym

uwag nazywa się ShakerSort – sortowanie przez wytrząsanie.

Pomin = n – 1, zaś pozostałe parametry moŜna szacować wg zaleŜności podanych przez Knutha:

Posrednia ≈ 0.5(n2 - n(k1 - ln(n)); Prsrednia ≈. n - k2sqrt(n)

Szybki wzrost czasu sortowania ze wzrostem n. Modyfikacja nie zmniejsza liczby permutacji tylko redukuje liczbę porównań.

for i2 to n do for j n downto i do

if X[j-1] > X[j] then X[j-1] X[j]

void BubbleSor t(int n, int *X) int i, j; //P5.2a for ( i= 1; i < n; i++) for (j = n-1; j >= i; j--) if ( X[j] < X[j-1] ) swap (X[j-1], X[j]);

Tablica 5.2. Sortowanie bąbelkowe 1 2 3 4 5 6 7 8

Etapy

22 50 4

18 4

40 4

88 4

19 4

4 72

i=2 4 22 50 18 40 88 19 72 i=3 4 18 22 50 19 40 88 72 i=4 4 18 19 22 50 40 72 88 i=5 4 18 19 22 40 50 72 88 i=6 4 18 19 22 40 50 72 88 i=7 4 18 19 22 40 50 72 88 i=8 4 18 19 22 40 50 72 88

Tablica 5.3. Sortowanie bąbelkowe poprawione – ShakerSort

Etapy 1 2 3 4 5 6 7 8

1-szy 22 50 18 40 88 19 4 72 2-gi 4 22 50 18 40 88 19 72 3-ci 4 22 18 40 50 19 72 88 4-ty 4 18 22 19 40 50 72 88 5-ty 4 18 19 22 40 50 72 88

ShakerSort (n, X) // P5.2b lewy2; prawyn kn repeat

for jprawy downto lewy do if X[j-1] > X[j] then X[j-1] X[j]

k j

lewyk+1 for jlewy to prawy do if X[j-1] > X[j] then

X[j-1] X[j] kj

prawyk - 1 until lewy > prawy

Page 22: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 41 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

ShellSort1(n, X) // P5.3a

h n div 2 // wartośc poczatkowa odstępu

while (h > 0) do for i h + 1 to n do j i – h while (j > 0) do jh j + h if X[j] ≤ X[jh] then j0 //koniec pętli

else Swap(X[j], X[jh])

j j – h h h div 2

5.3. Sortowanie Shella Algorytm zaproponowany przez D.L. Shella w 1959 roku jest sposobem poprawienia efektywności zarówno metody BubbleSort jak i metody sortowania przez proste wstawianie.

Istotą metody jest sortowanie podtablic, utworzonych z elementów odległych od siebie. W kolejnych etapach liczba podtablic maleje (do wartość 1), zaś rośnie ich liczność. Ostatni etapem jest zwykłe sortowanie wszystkich elementów.

Algorytm o regularnej zmianie odstępu, w pierwszym etapie tworzy podzbiory z elementów oddalonych od siebie o k (np. k = 4) i sortuje je; w drugim o k/2 , itd. aŜ osiągnie się k = 1.

Dla tablicy 8–elementowej sortowanie moŜe przebiegać w 3 fazach ❶ co cztery –grupuje elementy oddalone o 4 miejsca i sortuje 4 podzbiory, ❷ co dwa –grupuje elementy oddalone o 2 miejsca i sortuje 2 podzbiory, ❸ co jeden –sortuje wszystkie elementy jedną z wybranych technik.

KaŜdy etap sortowania moŜna realizować dowolną techniką.

Pętla zewnętrzna while kontroluje odstęp między porównywanymi elementami. W kaŜdym kroku odstęp zmniejszany jest dwukrotnie.

W pętli środkowej porównuje się elementy odległe o h. W pętli wewn ętrznej elementy są przestawianie, jeŜeli nie są uporządkowane rosnąco.

Średnia efektywność algorytmu Shella wynosi n1.5 Analiza efektywności algorytmu Shella wykazała:

1. przyrosty nie musz ą być potęgą liczby 2 , 2. ciąg przyrostów nie musi być ściśle zdefiniowany.

Przyrosty odst ępów nie powinny by ć swoimi dzielnikami. Knuth wykazał, Ŝe dobrym ciągiem przyrostów jest ciąg:

hi+1=3hi +1 ` np.1,4,13,40,121… lub 1,3,15,31… Algorytm ShellSort2 zakłada dowolny ciągu przyrostów h1, h2,

...,h i ,przy czym kaŜde z h i–sortowa ń realizowane jest metodą prostego wstawiania.

Metoda z „z wartownikiem” upraszcza warunek szukania miejsca

wstawienia elementu. KaŜde z h–sortowań musi mieć własnego

wartownika, co rozszerza deklarację wektora X o hmax.

ShellSort2(n, X) //P5.3b

w3; k[1..w] (5, 3, 1) for m1 to w do hk[m] s–h //wartownik

for ih+1 to n do wrkX[i]; ji – h if s=0 then s–h ss+1 X[s]wrk while wrk < X[j] do

X[j+h]X[j] jj – h

X[j+h]wrk

Dziel tablice DANE na k podtablic D[k]

for i 1 to k do Sortuj(D[i])

Sortuj (DANE(n))

12 9 5 22 4 2 30 1 0 28 29 p 12 2 29 o c 9 30 d o 5 1 z 5 22 0 b 4 28 i 2 12 29 o 9 30 r 1 5 y 0 22

s o r t 4 28

wynik: 2 9 1 0 4 12 30 5 22 28 29 c 2 0 30 28 o 9 4 5 29 3 1 12 22 0 2 28 30 4 5 9 29

s o r t 1 12 22

wynik: 0 4 1 2 5 12 28 9 22 30 29 Sort co 1 0 4 1 2 5 12 28 9 22 30 29 0 1 2 4 5 9 12 22 29 28 30

22 50 18 40 88 19 4 72

22 19 4 40 88 50 18 72

4 19 18 40 22 50 88 72

22 19 4 40 88 50 18 72

4 18 19 22 40 50 72 80

h=4

h=2

h=1

R5_3

Algorytmy i Struktury Danych - wykład 42 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

5.4. Sortowanie szybkie Algorytm sortowania szybkiego QuickSort wykorzystuje ulepszoną metodę sortowania przez zamian ę, został opracowany przez C.A.R. Hoare’a w 1962 roku. Średni czas sortowania określa zaleŜność n ⋅⋅⋅⋅lg(n), pesymistyczny zaś szacuje zaleŜność O(n2). Istotą metody jest podział zbioru na dwie części. Wybieramy arbitralnie jeden element ze zbioru i tworzymy podzbiór lewy zawierający elementy nie większe (mniejsze) i podzbiór prawy elementy nie mniejsze (większe) od wybranego elementu.

Proces podziału powtarza się dla obydwu podzbiorów. Zbiór zostaje posortowany jeŜeli wszystkie podzbiory staną się zbiorami jednoelementowymi.

Algorytm oparty jest na strategii dziel i zwycięŜaj.

W przykładzie z rysunku R5.4a jako element osiowy wybrano środkowy z kaŜdego podzbioru.

Podstawa algorytmu to efektywnie zbudowana funkcja podziału (partition ), realizująca podział tablicy X. MoŜe być zrealizowana wg poniŜszego schematu:

Funkcja podziału metod ą przegl ądu dwustronnego ❶ ustaw wskaźniki przeglądające od lewej i = l i od prawej j = p ❷ wybierz element osiowy v z tablicy X (np. środkowy) Dla (i <= j) wykonaj

❸ przeglądaj tablicę od lleewweejj aŜ znajdziesz element Xi ≥≥≥≥ v ❹ przeglądaj tablicę oodd pprraawweejj aŜ znajdziesz element Xj ≤≤≤≤ v ❺ dla i < j: -zamień miejscami elementy, które spowodowały

zatrzymanie; oraz zmień dwa wskaźniki i = i + 1 , j = j – 1 w przeciwnym razie (jeŜeli i ≥ j) zmień tylko i = i+1 .

do skrzyŜowania wskaźników.

----------Wybierając za element osiowy v = X[1] lub wykonując na początku Swap(X[1], X[k] ≡≡≡≡ v) naleŜy dołoŜyć operację ❻

❻ zamień element osiowy z elementem wskazywanym przez prawy wska źnik (eliminacja osiowego z podtablic).

QuickSort(l, p, X) // lewy,prawy int pivot; if l < p then pivot = Partition(l, p); QuickSort(l, pivot-1, X); QuickSort(pivot+1, p, X);

Partition(l, p) // wersja gdy element osiowy nie wchodzi do podtablic

if l < p then // wybierz element osiowy v // rozdziel elementy X tak aby X[l], X[l+1],…X[k-1] ≤v ≤X[k+1],…X[p] return k

QuickSort1( int lo, int hi, int X[ ]) // element osiowy częścią podtablic

int i=lo, j=hi, v = X[(lo + hi)/2]; while (i <= j) // partition

while (X[i] < v) i++; // ❸

while (X[j] > v) j--; // ❹

if (i < j) swap(&X[i++], &X[j--]); else i++;

if (lo < j) QuickSort1(lo, j ,X); if (j+1 < hi) QuickSort1(j+1, hi, X);

Page 23: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 43 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

Algorytm QuickSort nie jest stabilny oraz nie jest efektywny dla małych n. Niech wartość osiowa v jest zawsze wybierana jako największa ze zbioru.

Przy kaŜdym podziale lewy podzbiór zawiera n-1 elementów zaś prawy tylko 1 element. Jest to najgorszy przypadek działania algorytmu, gdyŜ trzeba dokonać n takich podziałów.

Koszt podziału wynosi n - 1, koszt sortowania lewego podzbioru T(n-1), zaś prawego T(0).

2

)1()(

1dla1)1(

1dla0)(

−=⇒

>−+−=

= nnnT

nnnT

nnT T(n) = O(n2)

Niech zbiór wejściowy będzie zawsze dzielony w proporcjach 9:1. Wówczas czas działania ma postać podana obok. MoŜna dowieść, Ŝe prowadzi to do złoŜoność klasy O(n⋅lg(n))

JeŜeli v = X[1] to najgorszym przypadkiem jest tablica uporządkowana.

Efektywno ść metody zale Ŝy od wyboru elementu osiowego i pocz ątkowej struktury zbioru Dobór elementu osiowego , minimalizujący wystąpienie wartości największej lub najmniejszej:

losowy wybór liczby zawartej między X[l] i X[p] (generator liczb losowych), elementem osiowym jest mediana elementu 1-go, środkowego i ostatniego.

Funkcja podziału moŜe przekształcać tablicę X przeglądając ją tylko z lewej strony. Niech i będzie indeksem przeglądającym tablicę z lewej do prawej , zaś k indeksem elementu osiowego, wybranym jako skrajnie lewy element tablicy X,

Algorytm przegl ądu tablicy X tylko z lewej strony: • ustaw wartość osiową: k = l (lewy); v = X[l] oraz wskaźnik i = l + 1 • inkrementując i sprawdź relację X[i] < v , jeŜeli spełniona dokonaj wymiany X[k = k+1] i X[i]; • kontynuuj przeglądanie zwiększając i;

• po zakończeniu przeglądania dokonaj wymiany elementu osiowego X[l] oraz X[k].

Sortowanie QuickSort nie jest bezpośrednie. Kiedy algorytm sortuje 1-szą podtablicę, pierwszy i ostatni indeks drugiej podtablicy muszą być trzymany na stosie.

QuickSort nie daje gwarancji, podziału tablicy na pół. Funkcja Partition moŜe wielokrotnie dzielić tablicę na jedną pustą podtablicę. Na stosie znajdzie się wówczas n-1 par indeksów.

Zamiana rekordów wymaga trzech przypisań: w średnim przypadku złoŜoność czasowa liczby operacji przypisania rekordów wynosi: A(n) = 2.07(n+1)lg(n) .

QuickSort moŜe być zrealizowany techniką rekurencyjną. Nie absorbuje zbytnio zasobów komputera, gdyŜ nie jest tworzona kopia tablicy X. W procesie rekurencyjnym przechowywane są na stosie tylko indeksy wyznaczające segmenty tablicy, których elementy mają ulec przestawieniu. Głębokość stosu moŜe nie przekraczać wartości lg(n).

Efektywność algorytmu moŜna zwiększyć trzymając indeksy i, j tablic w rejestrach komputera.

QuickSort2( int l, int p, int X[ ]) // P5.4 // przegląd jednostronny int k = l, i; if (l >= p) Exit();

for(i = l+1; i <= p; i++) if (X[i] < X[l]) swap(&X[++k], &X[i]);

swap(&X[l], &X[k]);

QuickSort2 ( l, k-1, X); QuickSort2 ( k+1, p, X);

>++

==

1dla)(1010

9

1dla0

)(nnO

nTnT

n

nT

Algorytmy i Struktury Danych - wykład 44 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

5.5. Sortowanie przez zliczanie

Zakłada się, Ŝe zbór elementów do sortowania to liczby całkowite z przedziału (1, k), przy czym wartość k jest z góry znana. Dla k = O(n) złoŜoność algorytmu jest klasy O(n).

Istotą metody jest zliczanie liczby elementów mniejszych od określonej liczby a zbioru X.

Znając tę liczbę, znamy dokładną pozycję a w ciągu posortowanym. NaleŜy ją wstawić na stosowną (liczba el. mniejszych+1) pozycję.

Dane wejściowe o rozmiarze n zawarte są a tablicy A[n] Wykorzystywane są dwie tablice dodatkowe:

B[n] – zawiera posortowane dane wejściowe,

C – tablica robocza pamiętająca liczby elementów mniejsze lub równe od zadanej liczby .

Dana jest tablica A[8] zawierająca całkowite liczby dodatnie nie większe od k = 7.

W wierszu ❶ rozpatrujemy kolejno wszystkie elementy tablicy A. Wartość A[i] stanowi indeks dla wektora C. W C zliczane są wystąpienia identycznych wartości A[i] i wpisywane na pozycję C[A[i]].

W wierszu ❷ dodaje się narastająco wartości tablicy C, otrzymując dla i ∈ (1, k) ile elementów jest ≤ i. Ostatnim elementem jest liczności zbioru źródłowego.

W wierszu ❸ wartość A[i] stanowi adres dla tablicy C; wartość z tablicy C stanowi adres, pod którym w tablicy B umieścimy element ze zbioru źródłowego.

Zmniejszamy zawartośc komórki C[A[i]] o jeden, co pozwala wstawiać identyczne elementy źródłowe na właściwe miejsca tablicy wynikowej B.

Całkowity czas działania algorytmu: O( n + k )

W metodzie tej nie są wykonywane operacje porównywania dwóch elementów.

Metoda jest stabilna, co oznacza, Ŝe liczby o tych samych wartościach lokowane są w tablicy wynikowej w tej samej kolejności, co w tablicy wejściowej. Jest to istotne tylko wtedy, gdy z elementami sortowanymi związane są dodatkowe dane.

Metoda daje istotne korzyści, jeŜeli k = O(n). Klucze tablicy C stanowią indeksy dla tablicy

wynikowej B, wg. których umieszcza się elementy źródłowe z tablicy A.

CountSort(k, A, B) n length[A]

for i 1 to k do C[i] 0 // O(k)

// C[i] - liczba elementów równych i ❶❶❶❶ for i 1 to n do C[A[i]] C[A[i]] + 1

// C[i] - liczba elementów ≤ i ❷❷❷❷ for i 2 to k do C[i] C[i] + C[i-1]

❸❸❸❸ for i n downto 1 do // O(n)

B[C[A[i]]] A[i]

C[A[i]] C[A[i]] - 1

Po 5-tym wykonaniu pętli ❸ A[4] = 2 1 2 3 4 5 6 7 8

B 2 2 5 5 7

1 2 3 4 5 6 7 8

C 0 0 2 2 3 6 7 8

3 9 5 1 8 6 2 7 8 1 2 3 5 6 7 8 8 9

Stan po wykonaniu pętli ❶ 1 2 3 4 5 6 7 8

A 5 7 6 2 5 7 2 5

1 2 3 4 5 6 7 8

C 0 1 1

0 0 1 1 1

1 1 1

0

Stan po wykonaniu pętli ❷ ≤ 1 2 3 4 5 6 7 8

C 0 2 2 2 5 6 8 8

Po 1-szym wykonaniu pętli ❸ A[8]=5 1 2 3 4 5 6 7 8

B 5

1 2 3 4 5 6 7 8

C 0 2 2 2 4 6 8 8 Po 2-gim wykonaniu pętli ❸ A[7]=2 1 2 3 4 5 6 7 8

B 2 5

1 2 3 4 5 6 7 8

C 0 1 2 2 4 6 8 8 Po 3-cim wykonaniu pętli ❸ A[6]=7 1 2 3 4 5 6 7 8

B 2 5 7

1 2 3 4 5 6 7 8

C 0 1 2 2 4 6 7 8 Po 4-tym wykonaniu pętli ❸ A[5]=5 1 2 3 4 5 6 7 8

B 2 5 5 7

1 2 3 4 5 6 7 8

C 0 1 2 2 3 6 7 8

Page 24: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 45 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

5.6. Sortowanie pozycyjne (radix sort) Algorytm wymaga pewnej wiedzy o sortowanych obiektach:

1. klucze obiektów są nieujemnymi liczbami całkowitymi (n –liczba obiektów), 2. zapisane są w notacji dziesiętnej (podstawa r = 10), 3. składają się z tej samej liczby cyfr (m –liczba cyfr).

Niech dana będzie dodatnia liczba całkowita a zapisana w systemie o podstawie r. np. a = 8739 Cyfrą o numerze k jest następująca liczba: a/rk mod r .

JeŜeli liczba a naleŜy do przedziału (0, 1) to k-tą cyfrę liczby określa zaleŜność: a⋅⋅⋅⋅r k mod r .

5.6.1. Przetwarzanie cyfr od lewej do prawej

Etap 1. Zbiór Wejściowy dzielony jest na podzbiory wg skrajnie lewych kluczy. Tworzy się 10 pojemników numerowanych od 0 ÷ 9. Liczby o identycznych skrajnych lewych cyfrach trafiają odpowiednio do pojemników numerowanych od 0 ÷ 9.

Część pojemników moŜe być niewypełniona. Etap 2. Obiekty kaŜdego pojemnika z Etapu 1 przeglądane są wg wartości drugich cyfr od lewej i

lokowane w nowych pojemnikach, numerowanych od 0 ÷ 9. Rozdział elementów z kaŜdego pojemnika z poprzedniego etapu moŜe wymagać utworzenia dodatkowych 10-ciu pojemników numerowanych od 0 ÷ 9.

Etap 3 (i dalsze). Proces podziału podzbiorów z etapu poprzedniego kontynuowany jest wg trzecich i dalszych cyfr od lewej aŜ do zakończenia analizy wszystkich cyfr.

Po przeanalizowaniu wszystkich cyfr (liczba etapów = liczbie cyfr w kluczu), klucze zawarte w pojemni-kach utworzonych w ostatnim etapie są posortowane, wystarczy je odpowiednio zebrać i scalić.

Zbiór wejściowy moŜe składać się z liczb całkowitych (PESEL – 11 znaków) co daje 10 cyfr do analizy i numerację pojemników od 0 ÷ 9 lub alfabetu angielskiego co moŜe wymagać analizy 26 znaków i utworzenia pojemników numerowanych 0 ÷ 25.

Trudnością w implementacji tej metody jest występowanie zmiennej liczby pojemników.

Metoda wymaga dotrzymania załoŜenia (3), gdyŜ dla zmiennej liczby cyfr jest nieskuteczna. Zbiór 324 5 845 12 934 po posortowaniu pozostanie niezmieniony.

Problem moŜna ominąć wyrównując krótkie liczby zerami. Zbiór 324 005 845 012 934 po posortowaniu przyjmie postać 005 012 324 845 934 .

inline char Cyfra(long a, int k) return (a/(long)pow10l(k)) % 10; main() // P5.1 long a = 89624031; int m=8; for (int i = m-1; i >= 0; i--)

printf(" %d,", Cyfra(a,i));

8, 9, 6, 2, 4, 0, 3, 1,

Uproszczona metodyka implementacji:

a. Zapełnij w 1-szym przebiegu 10 pojemników ponumerowanych od 0 ÷ 9. b. Posortuj zawartość kaŜdego pojemnika indywidualnie. c. Połącz zawartości posortowanych pojemników w kolejności od 0 ÷ 9.

for i 0 to n -1 do ci Cyfra(We[i], m-1) Put (S[ci], We[i])

555589 222234 888867 222238 111123 999987 222232 111192 888876 555543 111124Zbiór Wejściowy

Etap Etap Etap Etap 1111 wg

skrajnie lewych

Etap Etap Etap Etap 2222wg

2-ich od lewej

Etap Etap Etap Etap 3 3 3 3 wg

3-ich od lewej

1 2 5 8 9

2 9 3 84 6 7 8

R5

.6a

122224199992122223

0

233332233338233334

3 4

544443588889

6 7

877776866667 988887

123333124444 192222

234444238888232222 543333 589999 876666 867777 987777

3 4 2 2 3 4 3 9 6 7 7

123 124 192 232 234 238 543 589 876 867 987

Nr pojemnika

Sortowanie pozycyjne od lewej do prawejSortowanie pozycyjne od lewej do prawejSortowanie pozycyjne od lewej do prawejSortowanie pozycyjne od lewej do prawej

Algorytmy i Struktury Danych - wykład 46 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

5.6.2. Przetwarzanie cyfr od prawej do lewej

Przetwarzanie w kolejnych krokach cyfr od prawej do lewej umoŜliwia sortowanie liczb całkowitych, mających róŜną liczbę cyfr.

Etap 1 . Utwórz 10 pojemników, numerowanych od 0 ÷ 9, tak skonstruowanych aby moŜna było z jednej strony wkładać zaś z drugiej wyjmować.

Wstaw wszystkie liczby ze zbioru Wejściowego do pojemników o numerach równych ostatniej cyfrze w liczbie. Liczby naleŜy wkładać od góry. Połącz zawartość pojemników w jedną listę, wyjmując liczby od spodu.

Część pojemników moŜe pozostać niewypełniona.

Etapy dalsze. Powtarzaj proces rozdziału liczb z nowej listy do odpowiednich pojemników wg drugiej i dalszych cyfr od prawej.

Proces ulega zakończeniu po przetworzeniu wszystkich cyfr w najdłuŜszej liczbie.

W sortowaniu pozycyjnym liczba kluczy n w zbiorze nie jest wystarczającym miernikiem złoŜoności, liczba znaków do przeanalizowania w cyfrze moŜe być większa od liczby n.

Dla zbiorów o małej liczności sortowanie pozycyjne nie jest zalecane.

Sortowanie pozycyjne nie realizuje porównań kluczy.

589999 234444 867777 238888 12222 987777 232222 197777 876666 5555 129 9 9 9 628888 522222Zbiór WeWeWeWejściowy

Etap Etap Etap Etap 1111 wg

skrajnie prawych

Etap Etap Etap Etap 2222wg

2-ich od prawej

Etap Etap Etap Etap 3 3 3 3 wg

3-ich od prawej

1 2 5 8 9

R5.6B

0 3 4 6 7Nr pojemnika

129

589234

197987

867

628

238

522

232

12 5 876

11112 233332 522222 233334 5 877776 866667 988887 199997 233338 622228 588889 122229

1 2 5 8 90 3 4 6 712

129628

522

238234

232 867 987

589987 197

5 12 555522 666628 111129 222232 222234 222238 888867 888876 999987 555589 111197

5

1 2 5 8 90 3 4 6 7

12

5

197

129

238

234

232

589

522 628

876

867 987

5 12 129 197 232 234 238 522 589 628 867 876 987

Sortowanie pozycyjne od prawej do lewejSortowanie pozycyjne od prawej do lewejSortowanie pozycyjne od prawej do lewejSortowanie pozycyjne od prawej do lewej

RadixSort1(We) r 10 //podstawa systemu liczbowego for i 0 to r–1 do P[i]Null // utwórz pojemniki

for k m to 1 do

for i 0 to n - 1 do ci Cyfra(We[i], m - k) Put (P[ci], We[i])

Połącz pojemniki w nowy zbiór We

We=5,234,867,238,123,98,232,192,876,543,124

Etap 1: 5, 4, 7, 8, 3, 8, 2, 2, 6, 3, 4 Etap 2: 0, 3, 6, 3, 2, 9, 3, 9, 7, 4, 2 Etap 3: 0, 2, 8, 2, 1, 0, 2, 1, 8, 5, 1

Page 25: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 47 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

Algorytm sortowania traktuje liczbę całkowitą jako rekord, gdzie kluczem są cyfry na kolejnych pozycjach poczynając od najmniej znaczącej.

Otrzymujemy wtedy tyle n-elementowych zbiorów, ile jest cyfr w liczbie. KaŜdy zbiór moŜe być sortowany pomocniczą stabilną funkcją sortującą.

ZłoŜoność algorytmu zaleŜy od wykorzystanej pomocniczej funkcji sortującej.

Cyfry naleŜą do przedziału 0 ÷ k, gdzie zazwyczaj k = 9. MoŜna więc stosować cząstkowe sortowanie przez zliczanie.

KaŜdy przebieg przez n liczb m-cyfrowych realizowany jest w czasie O(n + k). Wykonywanych jest m przebiegów, stąd całkowity czas

sortowania wynosi: O(m ⋅⋅⋅⋅n + m ⋅⋅⋅⋅k). JeŜeli m=const, zaś k=O(n) to sortowanie działa w czasie liniowym. Binarne sortowanie pozycyjne

RozwaŜmy sortowanie zbioru DUśYCH liter alfabetu angielskiego. Znaki kodu ASCII traktowane są jako ciągi 5-cio bitowe (rekordy znaków 0 ÷ 1). Operujemy więc na liczbach o podstawie r = 2.

Sortowaniu podlegają rekordy z kluczami zawierającymi 1 bit. Zbiór dzielony jest tak aby wszystkie rekordy z kluczami o wartości 0 wystąpiły przed rekordami z kluczami o wartości 1.

Sortowanie wymaga 5-ciu przejść, przesuwając się po kluczach od prawej do lewej .

MoŜna zaimplementować metodę zliczania indeksowanego kluczami lecz musi być ona stabilna.

Zbiór Wejściowy

Etap Etap Etap Etap 1111 wg

skrajnie prawych

Etap Etap Etap Etap 2222wg

2-ich od prawej

Etap Etap Etap Etap 3 3 3 3 wg

3-ich od prawej

R5.6c Sortowanie pozycyjne od prawej do lewej Sortowanie pozycyjne od prawej do lewej Sortowanie pozycyjne od prawej do lewej Sortowanie pozycyjne od prawej do lewej

58

99 99

23

44 44

86

77 77

23

88 88

12

33 33

98

77 77

23

22 22

19

22 22

86

77 77

54

77 77

12

44 44

43

11 115

88 889

233 33

4

866 66

7

233 33

8

122 22

3

988 88

7

233 33

2

199 99

2

866 66

7

544 44

7

122 22

4

433 33

1

55 558

9

22 223

4

88 886

7

22 223

8

11 112

3

99 998

7

22 223

2

11 119

2

88 886

7

55 554

7

11 112

4

44 443

1

58

9

23

4

86

7

23

8

12

3

98

7

23

2

19

2

86

7

54

7

12

4

43

1

9 4 7 8 3 7 2 2 7 7 4 19 4 7 8 3 7 2 2 7 7 4 19 4 7 8 3 7 2 2 7 7 4 19 4 7 8 3 7 2 2 7 7 4 1

3 3 9 2 3 2 6 8 6 4 3 83 3 9 2 3 2 6 8 6 4 3 83 3 9 2 3 2 6 8 6 4 3 83 3 9 2 3 2 6 8 6 4 3 8

1 1 4 2 2 2 5 8 8 9 5 11 1 4 2 2 2 5 8 8 9 5 11 1 4 2 2 2 5 8 8 9 5 11 1 4 2 2 2 5 8 8 9 5 1

RadixSort2(We) m length(Liczba)

for k 1 to m do

for j 0 to n - 1 do Klucz[j] Cyfra(We[j], m - k)

SortujStabilnie(We.Klucz)

WEWEWEWE

R5

.6dBinarne sortowanie pozycyjne od prawej do lewej Binarne sortowanie pozycyjne od prawej do lewej Binarne sortowanie pozycyjne od prawej do lewej Binarne sortowanie pozycyjne od prawej do lewej

AAAA 00001111

SSSS 10011111

O O O O 01111111

R R R R 10010000

TTTT 10100000

NNNN 01110000

G G G G 00111111

EEEE 00101111

XXXX 11000000

MMMM 01101111

P P P P 10000000

LLLL 01100000

EEEE 00101111

Etap Etap Etap Etap 1111

AAAA 00000001

SSSS 10011111

O O O O 01111111

R R R R 10011110

TTTT 10100000

NNNN 01111110

G G G G 00111111

EEEE 00100001

XXXX 11000000

MMMM 01100001

P P P P 10000000

LLLL 01100000

EEEE 00100001

AAAA 00000001

SSSS 10000011

O O O O 01111111

R R R R 10000010

TTTT 10111100

NNNN 01111110

G G G G 00111111

EEEE 00111101

XXXX 11000000

MMMM 01111101

P P P P 10000000

LLLL 01111100

EEEE 00111101

AAAA 00000001

SSSS 10000011

O O O O 01111111

R R R R 10000010

TTTT 10000100

NNNN 01111110

G G G G 00000111

EEEE 00000101

XXXX 11111000

MMMM 01111101

P P P P 10000000

LLLL 01111100

EEEE 00000101

AAAA 00000001

SSSS 11110011

O O O O 00001111

R R R R 11110010

TTTT 11110100

NNNN 00001110

G G G G 00000111

EEEE 00000101

XXXX 11111000

MMMM 00001101

P P P P 11110000

LLLL 00001100

EEEE 00000101

AAAA 00001

SSSS 10011

O O O O 01111

R R R R 10010

TTTT 10100

NNNN 01110

G G G G 00111

EEEE 00101

XXXX 11000

MMMM 01101

P P P P 10000

LLLL 01100

EEEE 00101

Etap Etap Etap Etap 2222 Etap Etap Etap Etap 3333 Etap Etap Etap Etap 4444 Etap Etap Etap Etap 5555

1010011111000

cons

t int

r =

1<

< b

yte;

int Digit(long a, int k)

// zw

raca bajty słowa

a nu

merow

ane od lewej do praw

ej,

// 0 do bytesw

ord-1

retu

rn (

a>>

byte

*(by

tesw

ord

k -1

)) &

(r

- 1)

; a=0x1234abcd

; Digit(a, 2) ab

Algorytmy i Struktury Danych - wykład 48 Instytut Aparatów Elektrycznych Algrm5 Dr J. Dokimuk

5.7. Sortowanie kubełkowe Algorytm zakłada, Ŝe dane wejściowe są liczbami rzeczywistymi z przedziału [0, 1) o rozkładzie jednostajnym.

Podziel przedział [0, 1) na n podprzedziałów o jednakowych rozmiarów, zwanych kubełkami. "Rozrzuć" zbiór wejściowy n liczb do kubełków, do których naleŜą.

Zakładała się, Ŝe liczby są jednostajnie rozłoŜone w przedziale [0, 1), więc oczekuje się, Ŝe w kaŜdym z kubełków nie będzie ich zbyt wiele.

Posortuj liczby w kaŜdym z kubełków i Wypisz je, przeglądając po kolei kubełki.

Zbiór wejściowy stanowi n-elementowa tablica We, której elementy spełniają nierówności 0 ≤ We[i] < 1. Algorytm korzysta z tablicy list B[0.. n - 1] .

Czas działania algorytmu BucketSort Pesymistyczny czas wykonywania wszystkich elementów funkcji oprócz sortowania wynosi O(n).

Niech sortowanie wszystkich list realizowane jest przez wstawianie. Niech Ni będzie zmienną losową oznaczającą liczbę elementów umieszczonych w liście B[i] . Sortowanie przez wstawianie działa w czasie O(n2), więc oczekiwany czas posortowania elementów na liście B[i] wynosi: E[O(N i

2)] = O(E[N i2]).

Całkowity oczekiwany czas posortowania elementów na wszystkich listach wynosi:

∑ ∑−

=

=

=1

0

1

0

22 ][])[(n

i

n

iii NENE OO (5.1)

Obliczenie w/wym sumy, wymaga określenia rozkładów zmiennych losowych Ni. Mamy n elementów i n list. Prawdopodobieństwo (sukcesu), Ŝe dany element trafi do listy B[i] wynosi 1/n. Prawdopodobieństwo sukcesu jest stałe-umieszczenia elementu w liście- i wynosi p=1/n.

Zmienna losowa Ni ma rozkład dwumianowy (Bernoulliego), stąd prawdopodobieństwo Ŝe zmienna przyjmie wartość k wynosi P(Ni = k; n, p) (k sukcesów w ciągu n badań).

Wartość oczekiwana wynosi E(Ni) = np = 1; wariancja V(Ni) = np(1 - p) = 1 - 1/n. Korzystając z zaleŜności E(X2) = V(X) + E2(X), otrzymujemy:

)1(12111)()()( 222 O≡−=+−=+= nnNENVNE iii (5.2)

Podstawiając obliczone oszacowanie O(1) do wzoru (5.1), otrzymujemy liniową zaleŜność na oczekiwany czas działania sortowania kubełkowego.

BUCKETSORT(We) n length[We] for i 0 to n - 1 do Wstaw We[i] do listy B[ i ] for i 0 to n – 1 do Sortuj listę B[i]

Połącz listy B[0], B[1], ..., B[n -1]

Wejście

Etap 1 Wstawianie

Etap 2Sortowanie

Etap 3 Łączenie

R5.6dSortowanie kubełkowe

0.110.140.19

0.220.27 0.960.45 0.66

0.740.77

1 2 5 8 9

0.140.190.11

0

0.270.22

3 6 7

0.960.45 0.660.770.74

4

0.11 0.14 0.19 0.22 0.27 0.45 0.66 0.74 0.77 0.96

0.11 0.96 0.45 0.66 0.19 0.22 0.14 0.74 0.27 0.77

0.77

R5.6e

0.110.960.450.660.190.220.140.740.270.77

0.74

1

2

3

4

5

6

7

0

9

8

0.96

0.66

0.45

0.27 0.22

0.14 0.19 0.11

We B

1 2

0.140.190.11

0

0.270.22

0000----0000....1 01 01 01 0....1111----0000....2 02 02 02 0....2222----0000....3333

Page 26: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 49 Instytut Aparatów Elektrycznych Algrm6 Dr J. Dokimuk

6. WYSZUKIWANIE WZORCA

Wyszukiwanie dotyczy znajdowania wszystkich wystąpień wzorca w tekście. MoŜe dotyczyć wyszukiwania zadanych wzorców w sekwencjach DNA.

Dany jest jednoznacznie zdefiniowany alfabet Ω np. Ω = 0,1,2,3,4,5,6,7 , Ω = a, b,…,z Tekst źródłowy zapisano w tablicy T[n] o długości n.

Wzorzec zawarto w tablicy W[m] o długości m. Tekstami zawartymi w tablicach T i W (zwanymi słowami) są symbole naleŜące do alfabetu Ω.

Wzorzec W występuje z przesunięciem s w tekście T, gdy zachodzą relacje: 0 ≤ s ≤ n - m oraz T[s+1, s+m] = W[1, m]

Wzorzec W występuje, począwszy od pozycji s + j tzn. gdy T[s + j] = W[j] dla 1 ≤ j ≤ m. s jest poprawnym przesunięciem jeŜeli W występuje z przesunięciem s w tekście T.

Wyszukiwanie wzorca to znajdowanie w tekście T wszystkich poprawnych przesunięć s dla danego wzorca W.

6.1. Algorytm "naiwny" (Brute Force)

Istotą algorytmu jest przeszukiwanie w pętli, określonej długością wzorca W[1, m]. Sprawdzany jest warunek W[1, m] = T[s+1, s+m] dla kaŜdej z n – m +1 moŜliwych wartości przesunięcia s.

Wzorzec W moŜna interpretować jako okienko przesuwające się sukcesywnie ( dla narastających wartości przesunięcia s ) nad analizowanym tekstem.

Po kaŜdym przesunięciu okienka (zmiana s) sprawdzane jest ponownie czy wszystkie symbole w okienku zgodne są z odpowiadającymi im symbolami w tekście.

RozwaŜmy tekst T składający się z ciągu n symboli a (an) i wzorzec W = am. Dla kaŜdego z n – m + 1 moŜliwych wartości przesunięcia s,

pętla for będzie wykonywana m razy, aby sprawdzić poprawność przesunięcia.

Pesymistyczny czas działania wynosi (porównania):

O((n - m + 1)⋅⋅⋅⋅m), zaś dla m = n/2 O(n2).

Algorytm wyszukiwania naiwnego nie jest optymalny, gdyŜ informacja o strukturze tekstu dla danej wartości S nie jest brana pod uwagę przy następnych poszukiwaniach.

Po kaŜdej zmianie s następuje cofanie się o całą długość wzorca, zapominając o wynikach testów wcześniejszych.

1 2 3 4 5 6 7 8 9 10 11 12 w y s z u k i w a n i e ↑ ↑ ↑ przesunięcie s=7 w a n i e

c e r c e r a c e r c e r a c e r c e r a ↑ ↑ ↑ ≠ ≠ ≠ s=0 c e r a s=1 c e r a s=2 c e r a

BruteForce(T, W)

n length[T] m length[W]

for s 0 to n – m do

if W[1, m] = T[s+1, s+m]

then "znaleziono wzorzec"

T = 1010100111 W = 1001

T: 1010100111 W: 1001 1001 1001 1001

1001 Znaleziono dla s=4

int BruteSearch( char *W, char *T) int i, j, N, M = strlen(W); // j –indeks dla wzorca N = strlen(T); // długość tekstu

for (i = 0, j = 0; (j < M) && (i < N); i++, j++)

while (W[j] != T[i]) i = i - (j - 1); j = 0;

if (j == M) return (i - M); else return (i);

Czy poniŜsza koncepcja jest moŜliwa?

c e r c e r a ↑ ↑ ↑ ≠ s=0 c e r a

←q = 3→ c e r c e r a s=s+q = 3 c e r a

Algorytmy i Struktury Danych - wykład 50 Instytut Aparatów Elektrycznych Algrm6 Dr J. Dokimuk

W5 A L A L A sufiks W3 A L A k=3

W3 A L A prefiks

6.2. Algorytm Knutha-Morrisa-Pratta

Dany jest tekst i wzorzec. Rozpoczynając porównanie łańcuchów pierwsza róŜnica pojawia się na pozycji q+1, gdzie q=5 jest liczbą zgodnych znaków wzorca i tekstu.

Informacje tego typu wykorzystywane są do poprawienia algorytmu naiwnego, poprzez uniknięcie zbytecznych testów i takie modyfikowanie wska źnika j aby nie był zerowany za kaŜdą zmianą s.

W algorytmie "naiwnym" wzorzec zawsze był przesuwany o jeden.

Czasami moŜliwe jest przesunięcie wzorca o większą wartość, wyznaczaną na podstawie informacji o liczbie q zgodnych znaków wzorca i tekstu.

Analiza wzorca pozwala na znalezienie optymalnego przesunięcia s .

Badając struktur ę wzorca, mo Ŝna okre ślić sposób modyfikacji indeksu j w przypadku stwierdzenia zgodno ści tekstu ze wzorcem na q pozycjach.

Przesunięto okienko ze wzorcem W = ALALAno względem tekstu T na pozycję s = 4

Stwierdzono, Ŝe q = 5 symboli wzorca pasuje do tekstu.

Analiza aktualnej cześć wspólną W i T

NajdłuŜszym prefiksem części wspólnej W i T, będącym jednocześnie sufiksem jest W3.

Informacja o części wspólnej zapamiętywana jest w tablicy Π[q] = k. gdzie k = 3 jest długością wspólnego sufiksu i prefiksu fragmentu wzorca; q = 5 jest wspólną częścią wzorca i tekstu. Π[5] = 3; Π[3] = 1

PoniewaŜ q symboli wzorca W pasuje do tekstu T dla przesunięcia s, to kolejne przesunięcie określa wzór: snowe = s + (q – Π[q])

Dla nowego przesunięcia si nie trzeba sprawdzać k–pierwszych symboli wzorca W z opowiadającym im tekstem T, gdyŜ prefix W k był jednocześnie sufiksem części wspólnej dla s.

Niech symbole wzorca W[1, q] pasują do symboli tekstu T[s + 1, s + q] . Dla zadanego wzorca W[1, m] funkcja prefiksowa Π określona jest zaleŜnością:

Π(q) = max k: k < q i Wk ⊐⊐⊐⊐ Wq

Π(q) jest maksymalną długością prefiksu wzorca W, będącego sufiksem Wq.

Najlepszy przypadek to snowe = s + q , gdy na części q nie ma wspólnego prefiksu i sufiksu.

j e s t A L A L A e f g f e f

s=4 A L A L A n o

← q = 5 →

Niech ∑Ω oznacza zbiór wszystkich słów utworzonych z alfabetu Ω. Długość słowa x oznaczmy |x|. Konkatenacja (złoŜenie) dwóch słów x oraz y (oznaczona xy) jest słowem o długości |x| + |y|.

Słowo w jest prefiksem słowa x (w ⊏⊏⊏⊏ x), gdy x = wy dla słowa y ∈ ∑Ω, |w| ≤ |x|. Słowo w jest sufiksem słowa x (w ⊐⊐⊐⊐ x), gdy x = yw dla słowa y ∈ ∑Ω, zatem |w| ≤ |x|. Dla słowa x = abcca: ab jest prefiksem (ab⊏⊏⊏⊏ abcca) zaś cca jest jego sufiksem (cca ⊐⊐⊐⊐ abcca)

Niech Wk oznacza k–symbolowy prefiks W[1, k] wzorca W[1, m]. Zatem Wm = W = W[1, m]. Niech Tk oznacza k–symbolowy prefiks T[1, k] tekstu T[1, n]. Zatem Tn = T =T[1, n]

Wyszukiwanie wzorca jest problemem znajdowania wszystkich przesunięć s w przedziale 0 ≤ s ≤ n – m takich, Ŝe W ⊐⊐⊐⊐ Tk+m.

j e s t A L A L A e f g f e f ↑↑↑↑ j=j=j=j= Π[5] + 1

s = 6 A L A L A n o nowa część wspólna←q=3→ s = 8 A L A L A n o

si = 4 + (5 – 3) = 6

T = 10101001110

W = 10101111

si+1 = 6 + (3 – 1) = 8

Page 27: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 51 Instytut Aparatów Elektrycznych Algrm6 Dr J. Dokimuk

Podsumowanie: 1. Dla kolejnych podtablic[1..q] wzorca W[1..m] zbuduj tablicę ∏[q] zawierającą

hipotetyczne długości wspólnych prefiksów i sufiksów. 2. Przesuwaj wzorzec nad tekstem w poszukiwaniu części wspólnych q wzorca i tekstu. 3. Nowe przesunięcie ramki wzorca określa wzór: snowe = s + (q – Π[q]). 4. Po wykonaniu nowego przesunięcia rozpocznij porównania od poz. ∏(q)+1 wzorca.

Funkcja Prefix() wykonuje analizę wzorca w poszukiwaniu najdłuŜszego wspólnego sufiksu i prefiksu, dla hipotetycznie narastającej załoŜonej części wspólnej wzorca i tekstu .

Tablica next[ ] (odpowiada ∏), reprezentuje maksymalną długo ść k sufiksu słowa W[j], będącego jednocześnie jego prefiksem i taką, Ŝe W[k+1] ≠ W[j+1] jeŜeli 0 < j < m.

Największy zysk osiąga się w przypadku tekstów o duŜym stopniu

samopowtarzalności.

W innych przypadkach zysk moŜe być słabo zauwaŜalny.

Czas działania funkcji Prefix(W) w najgorszym przypadku jest klasy O(m) Czas działania funkcji KMP_Search(W, T) wynosi O(n). Czas działania całego algorytmu KMP jest O(n + m), gdyŜ zawiera jedno wołanie funkcji Prefix .

Zaletą algorytmu jest to, Ŝe wskaźnik i dla tekstu T[i] nie jest nigdy zmniejszany. Oznacza to moŜliwość przeglądania plik danych od początku do końca bez cofania się w nim.

MoŜe mieć to znaczenie przy liniowym przeglądaniu duŜych plików bez ich buforowania, gdzie charakter wykonywanych czynności nie zezwala na cofanie się w otwartym pliku.

KMP_Search(W, T)

m length(W); n length(T) Π Prefix(W) q 0 for i1 to n do while q > 0 && W[q+1] ≠ T[i] do q Π[q] if W[q+1] = T[i] then q q + 1 if q = m then s i – m // znaleziono q Π[q]

Prefix(W) // analiza wzorca

mlength(W) Π[1]0; k0 for q2 to m do // q zakładana część wspólna

while k>0 && W[k+1] ≠ W[q] do k Π[k] ❶ if W[k+1] = W[q] then k k + 1 ❷ Π[q] k return Π

q 1 2 3 4 5 6 7 8 9 10 Wq a b c d b a b c a b ∏(q) 0 0 0 0 0 1 2 3 1 2

q 1 2 3 4 5 6 7 8 9 10

Wq B A T A i B A T A N ∏(g) 0 0 0 0 0 1 2 3 4 0

T o B A T A i B A T A m a m B A T

B A T A i B A T A N

s=2 B A T A i B A T A N ↑↑↑↑ j = Π[9] + 1

s=7 B A T A i B A T A N

s=11 B A T A i B

si = 2 + (9 – 4) = 7

si+1 = 7 + (4 – 0) = 11

T o B A T A i B A T A i B A T A N

B A T A i B A T A N

s=2 B A T A i B A T A N ↑↑↑↑ j = Π[9] + 1

s=7 B A T A i B A T A N

int next[100]; // odpowiada tablicy ∏(q) void Prefix( char *W) int j, q // q –zakładana długość części wspólnej int m = strlen(W); next[0] = -1; for ( q=0, j=-1; q<m; q++, j++, next[q] = j )

while ((j>=0) && (W[q] != W[j])) j = next[j]; q 1 2 3 4 5 6 7 8 9 10

Wq a b a b a b a b c a ∏(q) 0 0 1 2 3 4 5 6 0 1

Algorytmy i Struktury Danych - wykład 52 Instytut Aparatów Elektrycznych Algrm6 Dr J. Dokimuk

6.3. Algorytm Rabina–Karpa

Algorytm bazuje na pojęciach z teorii liczb, interpretując tekst złoŜony z k–symboli jako liczbę k–cyfrow ą w układzie dziesiętnym. Niech T = "2269538" zaś W = "695".

Schemat pokazuje, Ŝe istotą algorytmu jest umiejętność szybkiej zamiany tekstu w liczbę. Ogólnie moŜna przyjąć, Ŝe kaŜdy symbol jest liczb ą w systemie o podstawie d, co oznacza, Ŝe dysponujemy d–elementowym alfabetem ΩΩΩΩ = ℑℑℑℑ0, ℑℑℑℑ1, ℑℑℑℑ2, ℑℑℑℑ3,…, ℑℑℑℑd-1 .

Weźmy alfabet, w którym symbole są cyframi dziesiętnymi ΩΩΩΩ = 0,1,2,3,4,5,6,7,8,9 (d = 10).

• Dla zadanego wzorca W[1, m] moŜemy podać odpowiadającą mu liczbę dziesiętną p. • Dla tekstu T[1, n] wybieramy podsłowo o długości m tzn. T[s+1, s+m] i przypisujemy

wartość dziesiętną ts, gdzie s przyjmuje odpowiednio wartości s = 0, 1, … ,n – m.

Zgodność ts = p zachodzi wtedy i tylko wtedy, gdy T[s+1, s+m] = W[1, m].

Wartość s jest poprawnym przesunięciem wtedy i tylko wtedy gdy ts = p. Wartość p (wzorzec) moŜna obliczyć w czasie O(m) z reguły Hornera (2*103 +4*102 +3*101+6):

p = ((…(10W[1]+ W[2])10 + …+ W[m - 2])10 + W[m - 1])10 + W[m]

Dla tekstu T[1, n] odpowiadającą wartość t0 moŜna obliczyć wg tej samej reguły w czasie O(m).

Wartości t1, t2, … , tn - m moŜna obliczyć w czasie O(n – m) z zaleŜności iteracyjnej:

ts+1 = 10(ts – 10m – 1T[s + 1]) + T[s + 1+m]

ts – 10m – 1T[s + 1] –usuwa najbardziej znaczącą cyfrę z ts ,

10•(…) –przesuwa cyfry liczby o jedną pozycję w lewo, + T[s +m +1] –wstawia najmniej znaczącą cyfrę.

Długość wzorca m = 5, ts = 92345, zaś następny fragment tekstu ma postać T = 9234567 .

ts+1 = 10(92345 – 1049) + 6 = 23456 lub ts+1 = 10(92345 mod 104) + 6

Stała 10m – 1 jest obliczana raz, zatem kaŜdorazowa iteracja ts wymaga wykonania stałej liczby operacji arytmetycznych. Wartości p oraz t0, t1, … , tn -m moŜna obliczyć w czasie O(n + m). Algorytm umoŜliwia znalezienie wszystkich wystąpień wzorca W w tekście T w czasie O(n + m).

2 2 6 9 5 3 8 2 2 6 9 5 3 8 2 2 6 9 5 3 8

s=0 6 9 5 s=1 6 9 5 s=2 6 9 5

Dany jest dowolny ciąg M znaków T[i], T[i+1],…,T[i+M-1]. abcdefghlj

Niech d (podstawa systemu) będzie liczbą wszystkich moŜliwych znaków.

Ciąg znaków T moŜemy interpretować jako liczbę całkowitą postaci: x(0) = dM-1·T[i]+ dM-2·T[i+1]+ … + d·T[i+M-2] + T[i+M-1] abcdefghlj

JeŜeli przesuniemy się w tekście o jeden znak to otrzymamy nową liczbę postaci: x(1) = dM-1·T[i+1] + dM-2·T[i+2] + … + d·T[i+M-1] + T[i+M] abcdefghlj

Liczbę x(1) moŜna wyznaczyć iteracyjnie: x(1) = d(x(0) – dM-1·T[i]) + T[i + m]

lub x(1) = d(x(0) mod dM-1) + T[i + m]

podstawa d = 64 Ω = = 0 a = 1 A = 27 b = 2 B = 28 c = 3 C = 29 d = 4 D = 30 e = 5 E = 31 ... ... z = 26 Z = 52

W = ”Cabec ” m = 5 p = 29·644+1·643+2·642+5·64+3 = 486809923

p = (((’C’64+’a’)64+’b’)64+’e’)64+’c’

T=”GdziejestnaszCabec” t0=(((3364 + 4)64 + 26)64 + 9 )64 +5 = 554803781 t1 =”dzie”= 64(t0 – 644 33) + 0 = lub t1 = 64(t0 mod 644) + 0 =

T[s+1] gdyŜ wartość startowa s=0 , zaś wektory numerowane są [1, n] lub [1, m]

Page 28: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 53 Instytut Aparatów Elektrycznych Algrm6 Dr J. Dokimuk

Pojawiają się trudności, gdy wzorzec W[1, m] jest długi, co pociąga duŜe wartości p oraz ts.

Problem długiego wzorca W moŜna ominąć obliczając wartości p i ts mod q , gdzie wartość q jest odpowiednio dobrana. Pozwala to utrzymać czas działania na poziomie O(m + n).

Zazwyczaj za wartość q przyjmuje się taką liczbę pierwszą, Ŝe d*q zajmuje jedno słowo komputerowe, co umoŜliwia realizację operacji arytmetycznych z pojedynczą precyzją.

Dla d–elementowego alfabetu Ω=0, 1, … , d-1 wzór iteracyjny na wartości ts+1 przyjmuje postać:

ts + 1 = (d(ts – hT[s + 1]) + T[s + m +1]) mod q gdzie h = dm - 1 mod q.

ZauwaŜmy, Ŝe ts ≡≡≡≡ p (mod q) nie implikuje równo ści ts = p. Zatem przesunięcie s moŜe lecz nie musi być właściwe.

ZauwaŜmy: jeŜeli t s ≢≢≢≢ p (mod q) to na pewno t s ≠p i tym samym przesuniecie s jest niewła ściwe .

W praktyce test ts ≡≡≡≡ p (mod q) uŜywany jest do eliminowania niepoprawnych przesunięć. Gdy przesunięcie spełnia powyŜszy warunek naleŜy jeszcze zweryfikować czy rzeczywiście jest poprawne.

Weryfikację poprawności moŜna przeprowadzić bezpośrednio porównując W[1, m]=T[s+1, s+m]. Przyjmując wartość q dostatecznie duŜą moŜna spodziewać się rzadkiej potrzeby dodatkowej weryfikacji.

Zmienna h określa wartość najbardziej znaczącej cyfry w m–cyfrowym okienku.

Przy zgodności p = ts , bada się warunek W[1, m] = T[s+1, s + m] aby wyelimino-wać niejednoznaczność. Ostatni wiersz oblicza ts+1 mod q na bazie znanej wartości ts mod q.

Pesymistyczny czas działania algorytmu RabinaKarpa wynosi: O((n – m + 1)m), gdyŜ algorytm sprawdza kaŜde poprawne przesuniecie w sposób bezpośredni (podobnie jak "naiwny").

MoŜna dowieść, Ŝe liczba błędnych identyfikacji wynosi O(n/q), gdyŜ prawdopodobieństwo tego, Ŝe dowolny symbol T[i] będzie przystawał do p mod q nie przekracza 1/q.

Oczekiwany czas działania algorytmu Rabina-Karpa wy nosi O(n) + O(m(v + n/q)), gdzie v jest liczbą poprawnych przesunięć.

JeŜeli oczekiwana liczba poprawnych przesunięć jest klasy O(1) (mała), zaś wybrana liczba pierwsza q jest większa od długości wzorca m to algorytm działa w czasie O(n + m).

Podstawa systemu d powinna być potęgą liczby 2, nawet gdy jest to wartość większą od wielkości alfabetu. UmoŜliwia to implementację mnoŜenia poprzez operator przesunięcia bitowego.

m = 5; W = "34567"; p=34567; q = 17; p mod q = 6 t0 = "23683", t0 mod q = 2; t1=36834, t1 mod q =12

1 2 3 4 5 6 7 8 9 10 11 12 14 14 15 16 17

2 3 6 8 3 4 5 6 7 3 8 6 2 8 3 8 7

ti mod q 2 12 4 2 6 11 9 14 13 4 7 6 14 mod 17

prawidłowe dla s=4 NIEzgodne

RabinKarp(T,W, d, q) n length [T]; m length [P]

h dm - 1 mod q p 0; t0 0

for i 1 to m do p(d⋅p + W[i]) mod q // p = W[1, m] mod q

t0(d⋅t0 + T[i]) mod q // t0 = T[1, m] mod q

for s 0 to n – m do // iteracja dla wszystkich przesunięć

if p = ts then if W[1, m] = T[s+1, s+m] then "Znaleziono dla s=", s

if s < n - m then ts+1(d(ts-T[s+1]h)+T[s+m+1]) mod q

JeŜeli (a mod n) = (b mod n) to liczba a przystaje do liczby b modulo n i zapisuje się: a ≡ b (mod n).

Zapis a ≢≢≢≢ b (mod n) oznacza, Ŝe a nie przystaje do b (mod n) .

Algorytmy i Struktury Danych - wykład 54 Instytut Aparatów Elektrycznych Algrm6 Dr J. Dokimuk

#include <stdio.h> #include <string.h> #include <ctype.h>

int Index( char zn) // zwraca numer litery w alfabecie switch(zn) case ' ': return 0; // spacja=0 default: if(islower(zn)) return zn - 'a' + 1; else return zn - 'A' + 27;

int RK_Search ( char *W, char *T, int d, int q) long int i, M, N, h = 1, p = 0, tm = 0; M = strlen(W); N = strlen(T);

for (i = 1; i < M; i++) h = (d*h) % q; // oblicza wartości h = dm - 1 mod q

for (i = 0; i < M; i++) p = (d*p + Index(W[i])) % q; // oblicza wartości p (dla wzorca) tm = (d*tm + Index(T[i])) % q; // oblicza wartości t0 (dla 1-go podciągu)

for (i = 0; p != tm; i++) tm = (tm + d * q - Index(T[i]) * h) % q; // +d*q zabezpiecza przed wynikiem ujemnym tm = (tm * d + Index(T[i+M])) % q; if (i > (N - M)) return (-1); // nie znaleziono return (i);

int main () // P63 RabinKarp int l; char *t1 = "Gdzie jest nasz Cabec malowany", *w = "Cabec ";

l = RK_Search(w, t1, 64, 121);

if (l > 0) printf("\n Znaleziono na poz=%d", l); else printf("\n Nie znaleziono ");

return 0; Znaleziono na poz=16

A = 27 B = 28 C = 29 D = 30 E = 31 F = 32 G = 33 … … Z = 52

Alfabet: Ω = ,a, b, c, ...,z, A, B, C,...,Z

oraz postawę alfabetu d = 64

a = 1 b = 2 c = 3 d = 4 e = 5 f = 6 g = 7 … … z = 26

p = 29 644+1 643+2 642+5 64+3 = 486809923 p = c + 64(e + 64(b + 64(a + 64•C))) p mod 121 = 61

t0 = 5 + 64(9 + 64(26 + 64(4 + 64•G)))= 554803781

t0 mod 121 = 26

(a+b+c) mod q = ((a+b) mod q + c) mod q

Page 29: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 55 Instytut Aparatów Elektrycznych Algrm7 Dr J. Dokimuk

7. HASZOWANIE (MIESZANIE) Problem dotyczy wyszukiwania informacji w tablicach według pewnej specyficznej metody.

Technika wyszukiwania sekwencyjnego realizuje się w czasie O(n), zaś binarnego O(lg(n) ). Dla struktury danych jaką jest tablica z haszowaniem czas wyszukiwania informacji moŜna sprowadzić do teoretycznego poziomu O(1).

Istotą metody jest wyznaczenie pozycji klucza w tablicy na podstawie jego wartości. Wartość klucza jest podstawą określenia połoŜenia elementu. Znając klucz moŜna dostać się bezpośrednio do dowolnego miejsca w tablicy, pomijając kosztowne operacje przeszukiwania .

NaleŜy znaleźć odpowiednią funkcję h(KEY), która przekształci klucz KEY (liczba, rekord, łańcuch) w indeks tablicy przechowującej wartości tego samego typu co klucz .

Funkcja h(key) przekształcająca klucz w indeks nosi nazwę funkcji haszuj ącej (hash function). Idealna funkcja haszuj ącą: jeŜeli przekształca róŜne klucze w róŜne liczby. Mieszanie klucza: operacja zastosowania funkcji haszującej dla konkretnego klucza. Tablice z haszowaniem są wygodną strukturą danych, reprezentującą słowniki, na których

wykonywane są tylko trzy operacje: INSERT, SEARCH, DELETE. Przykładem mogą być kompilatory języków programowania, które generują tablice symboli o

kluczach, będących identyfikatorami zmiennych.

Idealna funkcja haszująca wymaga, aby na tablicę zarezerwowano tyle miejsca ile mo Ŝe wyst ąpić róŜnych elementów . Liczba elementów nie zawsze jest znana. Nie jest np. moŜliwe określenie pojemności tablicy symboli generowanych przez kompilator, gdyŜ zaleŜy to od wielkości programu. Przyjęcie wielkości maksymalnie moŜliwej, często jest nierealne fizycznie.

ZałóŜmy, Ŝe język programowania (np. C) dopuszcza długość zmiennych do 31 znaków jako

dowolną kombinację 36 znaków podstawowych. Daje to liczbę ∑=

30

0

3626i

i (uwzględniając, Ŝe nazwa

zmiennej nie moŜe zaczynać się od cyfry), która fizycznie nie moŜe być podstawą do rezerwowania takiego obszaru pamięci, aby zapewnić kaŜdej zmiennej osobną pozycję. W praktyce tworzone programy zawierają z reguły nie więcej niŜ kilkaset róŜnych zmiennych, tym samym zarezerwowanie tablicy o rozmiarze T[1000] jest na ogół wystarczające, jakkolwiek uniemoŜliwia to zbudowanie idealnej funkcji haszującej.

JeŜeli przyjmiemy za identyfikatory zmiennych symbole np. 11–sto znakowe (a ÷ k) i przypiszemy im odpowiedniki liczbowe (1 ÷ 11), to stosując identyczną funkcje haszujacą otrzymamy wyniki niejednoznaczne. Przykładowo h("ka") = 111 oraz h("aaa") = 111.

JeŜeli będziemy dodawać kody wszystkich znaków w nazwie, zaś sumy uŜyjemy jako indeksu to teŜ nie unikniemy niejednoznaczności.

Kolizja –funkcja haszująca generuje identyczne wartości liczbowe dla róŜnych kluczy.

Jakość funkcji haszującej zaleŜy od skutecznego unikania przez nią kolizji.

Niech dla potrzeb testowania nowego kompilatora przyjęto, Ŝe identyfikatory zmiennych będą maksymalnie trzyznakowe i stanowią kombinację 3 liter (a, b, c). Otrzymujemy ciąg zmiennych: a, b, c, aa, bb, cc, ab, ba, ac,ca, bc, cb, aaa, aab, aac, abc, aba, baa, caa, cba,

bca, bbc, bba, …,ccc o liczności ∑=

2

0

33i

i* = 39.

Poszczególnym literom przypiszemy cyfry a 1, b 2, c 3. Wówczas ciągowi zmiennych moŜna przypisać liczby typu integer ( ab12, aab112 ,…, ccc333), które stają się indeksami komórek tablicy symboli, przechowujących np. adresy.

Zmienna ab ulokowana zostanie w komórce 13, ccc w komórce 333. Dostęp przez program do dowolnej zmiennej realizowany je st w czasie O(1), gdy Ŝ klucz do konwersacji tekstu na liczb ę integer staje si ę generatorem adresu tekstu.

Wymagane jest zarezerwowanie tablicy o wymiarze T[333] mimo, Ŝe maksymalna liczba identyfikatorów zmiennych nie przekroczy 39.

Mówimy, Ŝe funkcją haszującą h jest algorytm zamieniający ciągu znaków ASCII w liczbę.

index i nazwa adres … … :… 11 aa :1008 … …… : 121 abc :2300 … …… 333 ccc :1204

Algorytmy i Struktury Danych - wykład 56 Instytut Aparatów Elektrycznych Algrm7 Dr J. Dokimuk

7.1. Haszowanie łańcuchowe

Weźmy 4000 rekordów z danymi osobowymi pracowników i 11-cyfrowe numery PESEL. Tablica bezpośrednia miałaby pojemność 100 miliardów elementów, z czynnym wykorzystaniem 4000. Funkcja mieszająca moŜe mieć postać: h(key) = key mod 100, zwraca ostatnie dwie cyfry klucza.

Klucz mieszany jest do indeksu i, po czym umieszczany wraz z jego rekordem w tablicy S[i]. Ta strategia nie rozmieszcza kluczy w posortowanej sekwencji.

Dla duŜej liczby kluczy, przynajmniej dwa klucze zostaną zmieszane do tego samego indeksu.

Metoda łańcuchowa jest jedną z najlepszych metod unikania kolizji.

Utworzyć listę dla kaŜdego moŜliwego indeksu i umieścić wszystkie wartości mieszane do tego samego indeksu w powiązaną z tym indeksem listą.

Jeśli mieszamy dwie pierwsze cyfry numeru PESEL, moŜemy stworzyć tablicę wskaźników Bucket[ ] , która będzie indeksowana od 0 do 99.

Wszystkie klucze zmieszane do indeksu i zostaną wówczas umieszczone na liście jednokierunkowej, wskazywanej przez element Bucket[i]. Jeśli mieszamy 2-pierwsze cyfry liczby, musimy mieć 100 list ( lecz moŜemy przechowywać np. 400 kluczy).

Liczba list nie musi być równa liczbie kluczy

Jeśli liczba kluczy jest większa od liczby list, moŜemy być pewni wystąpienia kolizji. Mamy 100 kluczy i 100 list oraz prawdopodobieństwo przydzielenia kaŜdego klucza do kaŜdej listy jest równe, to prawdopodobieństwo umieszczenia wszystkich kluczy w

jednej liście wynosi: 198100

10100

1100 −≈

⋅ .

Efektem zastosowania funkcji mieszającej jest moŜliwość stworzenia lepszego algorytmu przeszukującego niŜ algorytm przeszukiwania binarnego.

Analiza haszowania łańcuchowego Szukanie pewnego klucza wymaga sekwencyjnego przeszukiwania listy jednokierunkowej. Jeśli klucze znajdują się w jednej liście, mamy przeszukiwanie sekwencyjne O(n) fatalnie

Z najlepszą sytuacją mamy do czynienia przy równomiernym rozłoŜeniu kluczy pomiędzy listy. Jeśli istnieje n kluczy i m list, kaŜda lista powinna zawierać αααα = n/m kluczy (αααα -wsp. zapełnienia).

Wszystkie listy mogą przechowywać po n/m kluczy gdy n jest wielokrotnością liczby m.

Tw. 7.1. Jeśli n kluczy rozłoŜonych jest równomiernie w m listach, liczba porównań wykony-

wanych podczas przeszukiwania zakończonego niepowodzeniem wynosi αααα. Tw. 7.2. Jeśli n kluczy rozłoŜonych jest równomiernie w m listach i kaŜdy z kluczy jest z takim

samym prawdopodobieństwem poszukiwany, średnia liczba operacji porównania wykonywanych podczas przeszukiwania zakończonego sukcesem wynosi: )1(5.0 α+⋅ .

Tw. 7.3. Jeśli mamy n kluczy i m list, oraz zakładamy Ŝe prawdopodobieństwa umieszczenia kluczy w poszczególnych kubełkach są równe,

to prawdopodobieństwo tego, Ŝe przynajmniej jedna lista będzie przechowywać

co najmniej k kluczy, jest mniejsze lub równe wyraŜeniu: 1

1−

k

mk

n.

Niech liczba kluczy n jest równa liczbie list, czyli n = m. Obliczając (Tw.7.3) górne ograniczenia dla prawdopodobieństw tego, Ŝe przynajmniej jeden kubełek zawiera k = lg(n) kluczy moŜemy stwierdzić, Ŝe dla:

n = 128 szansa na to, Ŝe czas przeszukiwania przekroczy lg(n) jest mniejsza niŜ 0.021. n = 1024 szansa na to, Ŝe czas przeszukiwania przekroczy lg(n) jest mniejsza niŜ 0.00027.

Dla duŜych n moŜemy być pewni, Ŝe zastosowanie metody mieszania będzie bardziej efektywne od algorytmu przeszukiwania binarnego.

R7.1

…………....

…………....

11112222

33334444

50505050

0000

99999999

98989898

BucketBucketBucketBucket[[[[iiii]]]]

04040404122123873

01010101123012345

50505050110923762 50505050081223741

02020202122123873

04040404110525434

99999999111587654

hhhh((((keykeykeykey)))) ==== keykeykeykey div div div div 101010109999

Page 30: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 57 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

8. PODSTAWOWE STRUKTURY DANYCH

Abstrakcyjny Typ Danych – typ danych dostępny tylko za pośrednictwem stosownego interfejsu.

8.1. Stos (stack)

Stos – liniowa struktura danych , umoŜliwiającą zapis i odczyt informacji tylko z jednego końca. Ze zbioru usuwany jest ten element, który został do niego dodany jako ostatni.

Jest to strategia Last-in First-out czyli LIFO. Przypomina stos kartek papieru. Kartka moŜe zostać pobrana, gdy na stosie znajdują się inne kartki.

Kartkę moŜna bezpiecznie połoŜyć, gdy na stosie jest wolne miejsce. Operacje interfejsu stosu: Init(S) –inicjalizuje (opróŜnia) stos, Empty(S) –testuje czy obszar stosu jest pusty, Full(S) –testuje czy obszar stosu jest zajęty, Push(el, S) –umieszcza element na szczycie stosu, Pop(S) –pobiera (i usuwa) element ze szczytu stosu (destrukcja stosu).

Naturalną implementacją stosu wydaje się tablica. W tablicy znajdują się zawsze jakieś wartości i nigdy nie jest pusta. Jak oznaczać nieuŜywane komórki, umieszczając 0 czy –32000 ? MoŜna wprowadzić dodatkową zmienną, przechowującą informacje o 1–szej wolnej pozycji.

Niech stos zawiera n–elementów ulokowanych w tablicy S[1, n]. Wprowadźmy dodatkowy obiekt – wierzchołek stosu topS.

Zmienna topS związana ściśle z określonym stosem, jak w przykładowej implementacji.

UŜycie więcej niŜ jednego stosu wymaga pisania kolejnych pakietów procedur. Istotą implementacji jest jeden zestaw funkcji, umo Ŝliwiaj ący prac ę z wieloma stosami.

Naiwnym rozwiązaniem problemu byłoby umieszczenie zmiennej top jako parametru kaŜdej funkcji, który to jako szczegół implementacyjny powinien być przed programem uŜytkownika ukryty.

Wskaźnik stosu moŜna umieścić wewnątrz stosu, a dokładnie w zerowej komórce tablicy tzn. S[0] top .

Implementacja taka ogranicza typ elementu wstawianego do stosu. MoŜe to być tylko typ int lub char , gdyŜ wartość komórki S[0] jest modyfikowana przez funkcje Push() i Pop().

W przypadku danych typy char rozmiar stosu ograniczony jest do 255 elementów. Dla liczb rzeczywistych konieczna jest konwersji typów w funkcji Push np.: S[ (int) S[0]++].

Struktura o 2-ch polach jest uniwersalną implementacją stosu . Jedno z nich przechowuje informacje o wierzchołku stosu, drugie właściwe dane.

Init(S) topS 0

Empty(S) if topS =0 then return ok

else return !ok.

Full(S) if topS = max then return ok

else return !ok.

Push(el, S)

if Full(S) then Error ”Stos pełny” else S[topS] el topS topS + 1

Pop(S) if Empty(S) then Error ”Stos pusty” else topS topS – 1 return S[topS ]

1 2 3 4 5 6 7 8

22 44 2 12

topS = 5

(a) Push(33, S), Push(54, S) 1 2 3 4 5 6 7 8

22 44 2 12 33 54

topS=7

(b) Pop(S), Pop(S), Pop(S) 1 2 3 4 5 6 7 8

22 44 2 12 33 54

topS = 4

Elementy 12, 33, 54 nie naleŜą juŜ do stosu.

void Init(char *S) S[0] = 0

void Push(char zn, char *S ) if Full(S)) puts(”Stos pełny”); else S[S[0]++] = zn;

struct STACK int top; typS vec[maxStack];

;

void Init(char *S) topS = 0 int Empty(char *S) return (topS == 0) ;

int Full(char *S) return top == MAX) ;

void Push(char zn, char *S) if Full(S)) puts(”Stos pełny”); else S[topS++] = zn; char Pop(char *S ) if Empty(S)) puts(”Pusty”); else return S[--topS];

3 2

1

4

0

Algorytmy i Struktury Danych - wykład 58 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

8.1.1. Beznawiasowa algebra Łukasiewicza – Odwrotna Notacja Polską (ONP)

Zapisanie wyraŜeń arytmetycznych bez uŜycia nawiasów zmieniających kolejność działań.

W ONP symbole operandów poprzedzaj ą symbol operatora.

Translacja wyraŜeń arytmetycznych na zapis ONP wymaga ustalenia priorytetów dla operatorów arytmetycznych.

Infix Postfix Conversion (konwersja z lewej ):

5 + ((2 + 4) * (3 + 2)) 5 2 4 + 3 2 + * +

((9 + (3*(8 – 3)))/4) – 6 9383–*+4/6–

a*cos(b+c)^d – e*f abc + cos d ^ * ef * –

y := a+b*c yabc*+:=

Powszechne stosowanie ONP wynika z łatwości implementacji przy uŜyciu stosu. Algorytm obliczania wartości wyraŜenia postfiksowego

1. pobierz kolejno symbole wyraŜenia ONP w kolejności od lewej do prawej;

2. jeŜeli symbol jest liczbą (lub zmienną) to odłóŜ go na STOS;

3. jeŜeli jest operatorem to pobierz dwa argumenty ze STOSU, wykonaj operację, wynik odłó Ŝ na STOSIE.

Dla operatorów nieprzemiennych (dzielenie, odejmowanie) naleŜy ustalić w jakiej kolejności będą wykonywane dwie operacje Pop().

4. czynności powtarzaj do wyczerpania danych, wówczas na STOSIE znajdzie się obliczone wyraŜenie.

Operator Priorytet funkcja (sin, log,…) 0 potęgowanie ^ 1 mnoŜenie, dzielenie negacja ( * / neg )

2

dodawanie + odejmowanie -

3

2+3*(4+5) 2 3 4 5 + * +

2–ga kolumna zawiera symbole pojawiające się na wejściu.

3–cia zawartość stosu po kaŜdym obiegu pętli głównej.

L.p. Wejście Stos 1 2 2 2 3 2 3 3 4 2 3 4 4 5 2 3 4 5 5 + 2 3 9 6 * 2 27 7 + 29

EvalPost( char *bufor) // oblicza wartość postfix’a typeStack S; // definicja stosu Init(&S); // inicjalizacja stosu typElemS zn; int n = strlen(bufor), wrk, i;

for (i=0; i<=n; zn=bufor[i++])

if (zn == '+') Push( (int)Pop(&S) + (int)Pop(&S), &S);

if (zn == '–') Push(-(int)Pop(&S) + (int)Pop(&S), &S);

if (zn == '*') Push( (int)Pop(&S) * (int)Pop(&S), &S);

if (zn == '/') wrk = (int)Pop(&S);

Push((int)Pop(&S)/wrk, &S);

if ((zn>='0') && (zn <= '9')) Push(zn-48, &S);

return Pop(&S);

2345+*+ = 29 2345-*+ = -1 9383-*+4/6- =0

Rys. 8.2. Obliczanie wartości wyraŜenia w ONP

Page 31: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 59 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

2*((4+3)*(5+1)) == 243+51+**

Algorytm Infix to Postfix Conversion – wersja uproszczona ( tylko dla operatorów przemiennych * oraz + )

1. pobierz kolejno znaki od lewej do prawej,

2. jeŜeli operand to wyślij go do Wyjścia,

3. jeŜeli lewy nawias ( to pomiń go, / lub prześlij na stos / 4. jeŜeli operator to umieść go na STOSIE,

5. jeŜeli prawy nawias ) to pobierz operator ze STOSU i wyślij do Wyjścia,

6. jeŜeli koniec zbioru We to kopiuj STOS do Wyjścia.

Przykład: (5*(((9 + 8)*(4*6)) + 7)) 598+46**7+* (a + b)*(d + e)

We STOS Wy ( ( a ( a + (+ b (+ b ) + * * ( *( d *( d + *(+ e *(+ e ) * + /0 *

Stan stosu: 243+51+** 2

4 2

3 4 2

7 2

5 7 2

1 5 7 2

6 7 2

42 2

84

#include <iostream.h>

int sA[80], topA;

void InitStack () topA=0;

void Push (int data) sA[topA++] = data;

int Pop () return (sA[--topA]);

main() // P81 243+51+** InitStack();

Push(2); Push(4); Push(3); Push(Pop() + Pop());

Push(5); Push(1);

Push(Pop() + Pop());

Push(Pop() * Pop()); Push(Pop() * Pop()); cout <<"Wynik ="<<Pop()<<endl; Wynik = 84

2 + 3*(4 + 5)

We STOS Wy 2 2 + + 3 3 * +* ( +*( 4 4 + +*(+ 5 5 ) +* + /0 *

+

#include "stos.h1d"

void Infix_Postfix( char *We, char *Wy) // P8_3a Stack S; Init(&S); // inicjalizacja stosu typS zn; int i, k = 0, n = strlen(We);

for (i=0; i <= n; zn = We[i++])

if (zn == ')') Wy[k++] = Pop(&S);

if ((zn == '+') || (zn == '*')) Push(zn, &S);

if ((zn >= '0') && (zn <= '9')) Wy[k++] = zn;

while (!Empty(S)) Wy[k++] = Pop(&S); Wy[k] = '\0';

struct NODES typS Data;

NODES *top; ;

typedef NODES *Stack;

void Init(Stack *S) // void Init(NODES **S) *S = NULL;

int Empty(Stack S) return S == NULL;

void Push(typS el, Stack *S) Stack wrk = new(NODES); if (wrk==NULL) puts("\n Stos pelny"); else wrk->Data = el; wrk->top = *S; *S=wrk;

typS Pop(Stack *S) if (*S == NULL) puts("\n Stos pusty"); return -1; else Stack wrk = *S; typS el = (*S)->Data; *S = (*S)-> top; delete wrk; return el;

Algorytmy i Struktury Danych - wykład 60 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

8.2. Kolejka (Queue)

Kolejka jest strukturą, w której element dodaje się (wstawia) na koniec kolejki (ogon), zaś usuwa z początku (z głowy).

First-in First-out – FIFO –pierwszy wchodzi i pierwszy wychodzi. Operacje interfejsu kolejki:

Init(Q) –inicjalizuje (opróŜnia) kolejkę, Empty(Q) –testuje czy obszar kolejki jest pusty, Full(Q) –testuje czy obszar kolejki jest pełny, Enq(el, Q) –umieszcza el na końcu kolejki (w ogonie - Tail ). Deq(Q) –pobiera (i usuwa) element kolejki z głowy - Head .

Kolejkę moŜna implementować jako tablicę statyczną Q[1, n]. Zmienna head identyfikuje głowę kolejki (początek). Zmienna tail podaje następną wolną pozycję do wstawienia, lub wskazuje na ostatnio

wstawiony obiekt. Obiekty dostawia się na koniec kolejki za ś usuwa z początku – pozostaj ą wolne miejsca.

Wolne miejsca w tablicy wykorzystuje się do dalszego wstawiania elementów, przez co koniec kolejki moŜe znaleźć się na początku tablicy.

Jest to układ cyklicznej tablicy Q, implementowanej poprzez tablicę jednowymiarową. Cykliczność oznacza, Ŝe pozycja q1 jest bezpośrednim następnikiem qn.

Na starcie head = tail = -1 –kolejka pusta.

Jeśli tail + 1= head to kolejka jest pełna.

Błąd niedomiaru: próba usuni ęcia elementu z kolejki pustej operacją Deq.

Błąd przepełnienia : próba wstawienia elementu do kolejki pełnej operacją Enq.

Full(Q) // tail wskazuje ostatnio wstawiony el. if (head = tail + 1) or (head=1 and (tail = length(Q)) return OK else return !OK

Enq(el, Q) // wstawienie do kolejki if Full(Q) then Return(”Przepełnienie”) Q[tail] el if Q[tail] = length(Q) then tail 1 else tail tail +1

Deq(Q) // wyjęcie z kolejki wrk Q[head] if Q[head] = length(Q) then head 1 else head head + 1 return wrk

1 2 3 4 5 6 7 8

2 18 33 54

head=3 tail = 7

(a) Enq(10,Q), Enq(11,Q), Enq(12,Q), Deq(Q)

1 2 3 4 5 6 7 8

12 2 18 33 54 10 11

tail=2 head = 4

(b) Enq(13,Q) 1 2 3 4 5 6 7 8

12 13 2 18 33 54 10 11

tail=3 head =4

1-sza implementacja kolejki Tail wskazuje na wolne pole

1 2 3 4 5 6 7 8

2 3 4 5 6 ȕȕȕȕ

Head Tail

Enq(33, Q), Enq(44, Q) 1 2 3 4 5 6 7 8

44 ȕȕȕȕ 2 3 4 5 6 33 Tail Head

2-ga implementacja kolejki Tail wskazuje ostatnio wstawiony el.

1 2 3 4 5 6 7 8

2 3 4 5 6

ȕȕȕȕ

Head Tail

Enq(33, Q), Enq(44, Q), Enq(55, Q) 1 2 3 4 5 6 7 8

44 55 2 3 4 5 6 33 ȕȕȕȕ

Tail Head

struct QUEUE

int head,

int tail;

typ vec[max]; ;

Page 32: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 61 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

int main() // P8.2a test kolejki // program wpisuje do Q1 ciąg liczb losowych, // do Q ciąg kolejnych liczb integer int i = 0; Queue Q, Q1; InitQ(&Q); InitQ(&Q1);

while (!Full(&Q1)) Enq(rand()%20, &Q1); DisQ(Q1)

while (!Full(&Q)) Enq(10 + ++i, &Q);

Enq(111, &Q);

DisQht(Q); DisQ(Q);

Deq(&Q); Deq(&Q); Enq (21, &Q); Enq (22, &Q); DisQht(Q);

DisQ(Q);

while (!Empty(&Q)) cout <<Deq(&Q)<<" ";

Deq(&Q); return 0;

6 10 2 10 16 17 15 12 Kolejka pelna Head=0 Tail=7 11 12 13 14 15 16 17 18 Head=2 Tail=1 13 14 15 16 17 18 21 22 13 14 15 16 17 18 21 22 Kolejka pusta

// tail wskazuje ostatnio wstawiony el.

const int maxQ = 8; // rozmiar kolejki typedef int typ;

struct Queue int head, tail; typ vec[maxQ]; ;

void Init(Queue *Q) Q -> head = Q -> tail = -1;

int Full(Queue *Q) return (Q->head==0 && Q->tail==maxQ-1 || Q->head == Q->tail+1);

int Empty(Queue *Q)

return (Q->head == -1);

int Enq(typ el, Queue *Q) if (Full(Q)) cout << "Kolejka pelna\n"; return -1

if (Q -> tail==maxQ-1 || Q->tail==-1) Q->vec[0] = el; Q->tail = 0; if (Q->head==-1) Q -> head = 0; else Q -> vec[++Q -> tail] = el; return 0;

typ Deq(Queue *Q) typ wrk; if (Empty(Q)) cout << "Kolejka pusta\n"; else wrk = Q->vec[Q->head];

if (Q -> head == Q -> tail) Init(Q);

else if (Q -> head==maxQ-1) Q -> head = 0; else Q -> head++;

return wrk;

//_____________________________________

void DisQ(Queue q) if (Empty(&q)) cout<<"Kolejka pusta\n"; else for (int i=q.head; i != q.tail; i=(i+1) % maxQ) cout <<q.vec[i]<<" ";

cout <<q.vec[i] << "\n";

void DisQht(Queue q) cout <<"Head="<<q.head<<" Tail="<< q.tail<< "\n";

Algorytmy i Struktury Danych - wykład 62 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

8.3. Lista

Często maksymalny rozmiar struktury nie jest znany w trakcie jej projektowania. Przy zmiennym zapotrzebowaniu programu na pamięć moŜna stosować dynamiczny przydział pamięci. Język C++ udostępnia stosowne funkcje.

Mechanizm dynamicznego przydziału pamięci, umoŜliwia tworzenie wiązanych struktur danych, w których następny element nie musi bezpośredni przylegać do poprzedniego.

Mamy nieciągłą strukturę danych, róŜną od tradycyjnych ciągłych struktur tablicowych. Jest to istotne w procesie wyszukiwaniu i wstawian iu elementu do struktury .

Dowiązanie jest dodatkową informacją przechowywaną w kaŜdym węźle, wskazującą gdzie jest przechowywany następny element. Wstawiając nowy element do listy (gdziekolwiek w pamięci), naleŜy zmienić wartość dowiązania, aby nowy element wskazywał na następny.

Lista z dowiązaniem jest strukturą, w której elementy ułoŜone są liniowo, według porządku określonego przez wskaźniki dowiązania a nie przez indeksy danych.

8.3.1. Lista jednokierunkowa (singly linked list) KaŜdy element listy stanowi strukturę o dwóch polach:

• pole danych (Dane), • dowiązanie (next) –wskaźnik następnego elementu listy.

Ostatni element listy zawiera pusty wska źnik NULL .

Dostęp (identyfikacja) do całej listy umoŜliwia wskaźnik do jej pierwszego elementu L . Elementy listy nie muszą zajmować przestrzeni ciągłej i miejsce przydzielane jest w miarę potrzeb.

Przeszukiwanie listy zaczyna się od początku i przebiega kolejno po wszystkich elementach. Aby znaleźć element na liście naleŜy iść za wskaźnikiem w polu next .

Koniec listy sygnalizuje wskaźnik o wartości NULL . Przydatne jest pamiętanie wskaźnika do elementu poprzedzającego element bieŜący. MoŜna uŜyć np. zmiennej pomocniczej prior, której początkowa wartość wynosi NULL.

Wstawianie i usuwanie elementów listy wskazywanej przez wskaźnik L (Head).

Aby usunąć węzeł z listy naleŜy znać: -wskaźnik do usuwanego węzła (aby znaleźć klucz), -wskaźnik do węzła poprzedzającego węzeł usuwany.

W węźle poprzedzającym naleŜy zmodyfikować pole next aby nie wskazywało węzła usuniętego. JeŜeli usuwany element jest 1-szy na liście to wskaźnik prior = NULL. Aby wstawić węzeł do listy naleŜy:

-utworzyć przestrzeń pamięciową np.: wrk = new(ListNode), gdzie wrk zwraca adres nowego węzła. -znaleźć węzeł, za który wstawiamy nowy węzeł i zmienić jego pole next na wartość wrk.

Nowy węzeł jest wstawiony gdy jego pole next zawiera wartość pola next węzła poprzedniego.

Wskaźnik next nowo wstawionego elementu musi wskazywać na następny element (moŜe być NULL). NaleŜy teŜ zaktualizować wskaźnik next poprzedniego elementu.

Element moŜna usunąć lub dodać z/do listy w stałym czasie.

Dane

5

nextDane

12

next

Dane

2

nextDane

7

NULLNULLNULLNULL

1111----szyszyszyszyostatniostatniostatniostatni

bieżącypoprzedni

R8.3

HeadHeadHeadHead

LLLLadres

Dane5

nextnextnextnext

Del12

nextDane

22

next

Dane

55

NULLNULLNULLNULLnowe

nowe

ostatniNowy

25

next nowe

Dane2

next

w4

wwww3333 wnwnwnwn

Dane30

next

w5wnwnwnwn

adres

w1

wrkwrkwrkwrk

wwww2222

R8.4adres

wstawianie wn za w4

wn = new(ListNode)

wn->next = w4->next w4->next = wn

usuwanie węzła w3

wrk = w2–>next //adres w3 w2–>next = wrk–>next

delete wrk //operacja pomocnicza

Operator new zwraca adres przydzielonego bloku. typData *newNode; newNode = new(typData); Przydzieloną pamięć zwraca na stertę operator delete.

struct ListNode

ListNode *next;

int numer; float Data[1024];

;

Page 33: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 63 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

Węzeł-atrapa (Head) często umieszczany jest na początku kaŜdej listy. Nie zawiera pola Dane, zaś pole next zawsze wskazuje na pierwszy, uŜyteczny węzeł listy.

Odwiedzanie węzła: for (List w = L; w != NULL; w = w->next) visit(w->Dane);

Przykładowe Operacje na Liście

newList (L) -return new list emptyList (L) -test if list is empty head (L) -return first element in list DelFirst (L) -return all but first element in list AddBefore (e,L) -add element to head/front of list

List = ( 5, 7, 4, 1, 3 ) head(List) = 5 DelFirst(List) = ( 7, 4, 1, 3 )

head(DelFirst(list)) = 7 DelFirst(DelFirst(List)) = (4, 1, 3)

AddBefore(0, List) = ( 0, 5, 7, 4, 1, 3 ) head(newList()) = Error DelFirst(AddBefore(3, newList())) = ( )

typedef int typData;

struct ListNode typData Data; ListNode *next;

;

typedef ListNode *List;

List newList( ) // initialize a new list return ( NULL);

int emptyList(List L) // test if list is empty return (L == NULL);

ListNode *createNode(typData e) ListNode *idn // idn points to the new node

idn = new(ListNode); if (idn==NULL) puts("Error memory"); exit(1);

idn->Data = e; idn->next = NULL;

return idn;

HeadL

HeadL

Algorytmy i Struktury Danych - wykład 64 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

Aby znaleźć miejsce na wstawienia elementu lub jego usuniecie naleŜy przejrzeć listę.

List NewAdr = new ListNode(2.3, Null); Instrukcja zwraca wskaźnik do nowego węzła, inicjalizując pole danych wartością 2.3 zaś pole adresowe Null.

Konstruktor pozwala uniknąć błędów związanych z niezainicjowanymi polami.

Błędy w przetwarzaniu list: -odwołanie do niezdefiniowanego wskaźnika, -uŜycie wskaźnika nieświadomie zmienionego.

Usuwanie elementu z początku listyRemove first element in list

Del

Old links New links

HeadL firstNode

List DelFirstList(List L) List idn

If (emptyList(L)) puts("Empty List"); exit(-1);

idn = L->next // new head in idn

delete(L); // release space

return (idn); // return new head

R8_5d

idn

int lengthList(List L) int len =0; while (! emptyList(L)) len = len + 1; L = pointNextNode(L); return len;

Przeglądanie listy – zwraca adres węzła lub NULL

List searchList(typData e, List L) int OK = 0; while (!emptyList(L)) && !OK) if (e==headList(L) OK = 1; else L = pointNextNode(L); if (OK) return L; else return NULL;

List pointNextNode(List L) if (emptyList(L)) puts("Lista pusta"); exit(1); return (L->next);

int main() // List1 List L, r, t, f, prev; int i,n; L = newList(); puts("Podaj elementy listy: ");

L = readList(L); printf("Lista ="); printList(L); printf("Wartosc szukanego elementy: "); scanf("%d", &i);

f = searchList(i, L); if (f) printf ("Znaleziono %d\n",headList(f)); else puts("Nie znaleziono"); printf("Ktory element usunac: "); scanf("%d",&i);

prev= searchList(i, L); DelBehindNode(prev); printf("Po usunieciu="); printList(L);

printf("Po ktorym elemencie co dodac: "); scanf("%d %d", &i, &n); prev= searchList(i, L); AddAfterNode(n, prev); printf("Po dodaniu="); printList(L);

return 0;

List readList(List L) typData e; while(readElement(&e)) L = AddBeforeList(e, L); return L;

Pod

aj e

lem

enty

list

y: 1

3 5

7 9

9 88

66

44 3

3 Li

sta

=(3

3 ,4

4 ,6

6 ,8

8 ,9

9 ,7

,5 ,3

,1 )

War

tosc

szu

kane

go e

lem

enty

: 66

Zna

lezi

ono

66

Kto

ry e

lem

ent u

suna

c: 88

P

o us

unie

ciu=

(33

,44

,66

,88

,7 ,5

,3 ,1

)

Po

ktor

ym e

lem

enci

e co

dod

ac: 7

-77

P

o do

dani

u=(3

3 ,4

4 ,6

6 ,8

8 ,7

,-77

,5 ,3

,1 )

struct ListNode typData Data; ListNode *next; ListNode(typData x, ListNode *p) Data = x; next = p ; typedef ListNode *List;

List NewAdr = new ListNode(2.3, Adr);

Page 34: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 65 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

8.3.2. Lista cykliczna

Lista jednokierunkowa uniemoŜliwia dotarcie do elementów poprzedzaj ących element wybrany. Lista cykliczna, umoŜliwia dostęp z danego elementu do kaŜdego innego.

W rozwiązaniu tym pole next ostatniego elementu wskazuje na pierwszy element listy.

Brak wyraźnie zaznaczonego 1-go i ostatniego elementu.

Wejście do listy wymaga wskaźnika do dowolnego elementu, dostępnego na zewnątrz (Lidn).

Wartość Lidn = NULL zewn ętrznego wska źnika wskazuje na list ę pust ą. Przeglądając listę naleŜy zapewnić rozpoznanie jej końca, gdyŜ z braku wskaźnika NULL powstanie pętla nieskończona. Końcem listy jest napotkanie wskaźnika, od którego rozpoczęto przeglądanie (Lidn).

W implementacji naleŜy zwrócić uwagę na uŜycie pętli do zamiast while i niewykonywanie Ŝadnych czynności, gdy lista jest pusta (konieczność testowania tego stanu).

Aby wstawić element do listy cyklicznej musi być znany wskaźnik do poprzedniego elementu. Pozostałe operacje wykonuje się podobne jak dla listy jednokierunkowej.

Wstawienie elementu na początek listy wymaga uaktualnienia wskaźnika do całej listy (Lidn). Wstawiając obiekt do pustej listy, tworzymy listę z jednym rekordem, którego wskaźnik next powinien wskazywać na niego samego.

Problem Josephusa

Niech n osób wybiera spośród siebie przywódcę, stosując następującą strategię:

-wszyscy ustawiają się w kole, -stosując m-sylabową wyliczankę eliminują za kaŜdym razem m-tą osobę, -ostatnia pozostała osoba zostaje przywódcą.

Problem: jak z góry wyznaczyć, która osoba pozostanie w kole.

Modelem problemu Josephusa jest n węzłowa lista cykliczna (kaŜdy węzeł zawiera łącze wskazujące na osobę z prawej strony).

1. Najpierw tworzy się listę jednoelementową (węzeł wskazuje sam na siebie). 2. Następnie wstawia się pozostałe elementy czyli węzły (wstawianie nowego węzła za węzłem x). 3. Następuje odliczanie m – 1 elementów od elementu wskazanego i usunięcie m-go węzła

(usunięcie węzła za węzłem x). Przypisujemy węzłowi o numerze m-1 wskaźnik do węzła stojącego za węzłem m .

4. Odliczanie i usuwanie kontynuowane jest aŜ pozostanie jeden węzeł (wskazuje sam na siebie).

Dane5

next

Dane12

next

Dane44

next

Dane48

next

Lidn

typedef struct ListNode int Data; ListNode *next; ListNode ( int x, ListNode *p) Data = x; next = p;

*CList; int main () // P8_5 Problem Josephusa int i, n = 9, m = 6; CList First = new ListNode(1, First), x = First; // 1-szy węzeł

for (i = 2; i <= n; i++) x = (x->next = new ListNode(i, First));

while (x != x->next) // wyliczanie i usuwanie m-go

for (i=1; i < m; i++) x = x->next; // węzeł m-1 cout << x->next->Data << ", "; x->next = x->next->next; // wskaźnik z m+1 do węzła m-1 cout << "Boss = "<< x->Data << endl; return 0; 6, 3, 1, 9, 2, 5, 4, 8, Boss = 7

Algorytmy i Struktury Danych - wykład 66 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

8.3.3. Lista dwukierunkowa (doubly linked list) Lista dwukierunkowa umoŜliwia przeglądanie w obu kierunkach i składa się z 3 pól.

– Data, pole danych, – next, wskazuje adres do elementu następnego, – prev, wskazuje adres do elementu poprzedniego.

JeŜeli a jest aktualnym kluczem to next[a] jest adresem elementu następnego, zaś prev[a] poprzedniego.

JeŜeli prev[a] = NULL to element a jest pierwszym na liście i stanowi głowę danej listy L. JeŜeli next[a] = NULL to element a jest ostatnim na liście L i stanowi jej ogon.

Zmienna wskaźnikowa FirstL wskazuje na 1–szy element listy L.

JeŜeli FirstL = NULL to lista jest pusta.

Funkcja Szukaj() poszukuje pierwszego elementu o wartości el na liście L z wykorzystaniem algorytmu liniowego przeglądania. Zwraca wskaźnik do tego elementu lub NULL jeŜeli elementu nie ma na liście.

Pesymistyczny czas szukania wynosi O(n). Funkcja Wstaw() dołącza element na początek listy.

Przed wstawieniem naleŜy utworzyć nowy węzeł np. instrukcją new. Wstaw działa w czasie O(1)

Funkcja Usun() usuwa element el z listy L, przez modyfikowanie wartości odpowiednich wskaźników. Wcześniej naleŜy wykonać funkcję Szukaj() aby wyznaczyć i przekazać wskaźnika do usuwanego elementu.

Pesymistyczny czas usuwania elementu wynosi O(n) i determinowany jest czasem działania procedury Szuka j. Samo usuwanie realizowane jest w czasie O(1).

Usunięcie węzła z listy dwukierunkowej wymaga tylko wskaźnika do tego węzła.

Wady: zwiększony rozmiar węzła, podwojona liczby operacji na łączach w kaŜdym węźle.

Wartownicy

JeŜeli do listy dołoŜymy dodatkowy węzeł WartL (bez pola Data) to moŜna wyeliminować operacje na zmiennej FirstL (wskazuje na pierwszy element listy).

Mamy cykliczną listę dwukierunkową, w której węzeł WartL znajduje się między głową a ogonem.

Pole next[WartL] wskazuje na głowę listy zaś pole prev[WartL] na ogon. Lista pusta składa się tylko z wartownika i next[WartL] oraz prev[WartL] -wskazują na NULL.

Wartownik nie zmienia klasy złoŜoności operacji na danych, lecz upraszcza warunki brzegowe.

Szukaj(el, L)

wrk FirstL while wrk ≠ NULL and key[wrk] ≠ el do

wrk next[wrk] return wrk

Wstaw(el, L) // na początek

next[el] FirstL // wstaw adres poprzedniej głowy if FirstL ≠ NULL then prev[FirstL] el FirstL el // adres węzła z kluczem el prev[el] NULL //pole prev węzła z kluczem el

Usun(el, L)

if prev[el] ≠ NULL then next[prev[el]] next[el] else FirstL next[el]

if next[el] ≠ NULL then prev[next[el]] prev[el]

Szukaj(el, L)

wrk next[WartL]

while wrk ≠ WartL and key[wrk] ≠el do wrknext[wrk] return wrk

Wstaw(el, L)

next[el] next[WartL] prev[next[WartL]] el next[WartL] el prev[el] WartL

Usun(el, L)

next[prev[ el]] next[el]

prev[next[ el]] prev[el]

Page 35: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 67 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

8.3.4. Sortowanie listy jednokierunkowej

Sortowanie listy moŜna realizować poprzez zamianę: -miejscami tylko zawartości pól danych w węzłach (jak w tablicach), -wyłącznie łączy w węzłach.

Czasami wskaźniki do węzłów listy, przechowywane są w róŜnych częściach aplikacji. Węzły mogą być dostępne poprzez wskaźniki spoza funkcji sortującej.

Program powinien zmieniać jedynie pola adresowe zapisane w węzłach.

Nie powinien zmieniać kluczy zapisanych w węzłach gdyŜ mogłoby się okazać, Ŝe sięgając do węzła poprzez inne łącze, trafimy na zmienioną wartość - niepoŜądany skutek.

Sortowanie przez wstawianie (wstawianie nowego węzła do wnętrza listy) Dane: lista We z kluczami nieposortowanymi. Wynik: lista Wy z kluczami posortowanymi.

Pobierz pierwszy węzeł listy We, pamiętając wskaźnik do niego w zmiennej t. Znajdź w liście Wy węzeł x, taki Ŝe x->next->Data > t->Data (lub x->next = NULL)

i wstaw t do tej listy za węzłem x. Operacje powyŜsze skracają listę We o jeden węzeł i wydłuŜają listę Wy o jeden węzeł.

Powtarzaj w/wym operacje do wyczerpania listy We i przeniesienia jej węzłów do listy Wy. Program SortLis1 definiuje dwie listy, poprzez stworzenie węzłów-Atrap, inicjując ich pola wartością -1. Dalej tworzy n-węzłów listy We wypełniając je liczbami losowymi, poprzez konstruktor.

Część sortująca: 1-sza pętla for pobiera kolejne po-

czątkowe węzły z listy We. 2-ga pętla for poszukuje w liście Wy

wartości większej od aktualnie pobranej z listy We.

Następnie realizowane jest wstawia-nie węzła z listy We do listy Wy.

typedef struct ListNode int Data; ListNode *next; ListNode(int x, ListNode *t) Data=x; next = t; *List;

const int n = 11;

int main() // SortLis1 ListNode We(-1, 0), Wy(-1, 0); // wezly-Atrapy List t = &We, x, u; for (int i=0; i < n; i++) t = (t->next = new ListNode(rand() % 100, 0)); DispL(&We);

//---------------------------------------------------------------sortowanie for (t = We.next; t != 0; t = u) u = t->next; // następny węzeł z listy We czyli usunięcie poprzedniego

for (x = &Wy; x->next != 0; x = x->next) if (x->next->Data > t->Data) break;

t->next = x->next; x->next = t; // wstawianie – zmiana adresów DispL(&Wy); return 0;

void DispL(List L)

for (List w = L; w != NULL; w = w->next)

cout << w->Data <<" ";

cout << endl;

-1 46 30 82 90 56 17 95 15 48 26 4 -1 4 15 17 26 30 46 48 56 82 90 95

Algorytmy i Struktury Danych - wykład 68 Instytut Aparatów Elektrycznych Algrm8 Dr J. Dokimuk

Sortowanie przez selekcję (wstawianie nowego węzła na początek listy) Dane: lista We z kluczami nieposortowanymi. Wynik: lista Wy z kluczami posortowanymi.

Przeglądaj listę We aby znaleźć element największy spośród tych, które jeszcze pozostały. Pobierz go, usuwając jednocześnie z listy We.

Wstaw ten element na początek listy Wy. Operacje powyŜsze skracają listę We o jeden węzeł i wydłuŜają listę Wy o jeden węzeł.

Powtarzaj w/wym operacje do wyczerpania listy We i przeniesienia jej węzłów do listy Wy. Przeglądaj listę We tak aby zmienna prev wskazywała węzeł poprzedza-jący węzeł z wartością największą, zaś zmienna max wskazywała węzeł z wartością największą. Zmienna prev niezbędna jest do usunięcia węzła wybranego.

#include<iostream.h> #include<stdlib.h>

typedef struct ListNode int Data; ListNode *next; ListNode(int x, ListNode *t)

Data = x; next = t; *List;

void DisL(List); void GenL(List, int); void DelNode(List); List MaxL(List); const int n=11;

int main() // SortLis2 ListNode We(-1,NULL), Wy(-1,NULL); // wezly-Atrapy List in = &We, out = &Wy, prev, max, r; GenL(in, n); DisL(&We);

while ( in->next) prev = MaxL(&We); max = prev->next;

DelNode(prev); r = out->next; out->next = max; max->next = r; DisL(out); return 0;

List MaxL(List L) // zwraca adres węzła poprzedzającego węzeł max int max = L->Data; List prev; for (List x = L; x->next != 0; x = x->next) if (x->next->Data > max) max = x->next->Data; prev = x; return prev; void DelNode( List prev) List wrk; wrk = prev->next; // wrk points to be deleted prev->next=wrk->next; // skip node to be deleted void DisL(List L) for (List w = L; w != NULL; w = w->next) cout << w->Data <<" "; cout << endl; void GenL(List L, int n) for (int i=0; i < n; i++) L =(L->next = new ListNode(rand() % 100, 0));

-1 46 30 82 90 56 17 95 15 48 26 4 -1 4 15 17 26 30 46 48 56 82 90 95

Page 36: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 69 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

9. DRZEWA Listy lub kolejki w niewielkim stopniu odzwierciedlają

hierarchiczną strukturą obiektu. UniemoŜliwia to ich liniowa i jednowymiarowa budowa.

Pokonanie tych ogranicze ń umo Ŝliwiaj ą struktury zwane drzewami .

Drzewo składa się z wierzchołków (węzłów), od których odchodzą krawędzie (gałęzie), zakończone na dole liśćmi.

Korzeń –węzeł, który nie ma ojca (poprzednika), moŜe natomiast mieć synów (następników). Liście lub węzły końcowe –węzły, które nie mają następników (synów).

Syn lub dzieci –węzły znajdujące się bezpośrednio pod danym węzłem. Kraw ędź –linia, która łączy dwa wierzchołki. ŚcieŜka –jednoznacznie wyznaczony ciąg kraw ędzi , prowadzący z korzenia do kaŜdego wierzchołka.

Długo ść ścieŜki –liczba krawędzi na ścieŜce. Poziom w ęzła –długość ścieŜki od korzenia do węzła

Wysoko ść drzewa –największy poziom węzłów w drzewie. Własność drzewa: dwa dowolne węzły moŜe łączyć dokładnie jedna ścieŜka. Drzewo uniwersalne –ukorzenione, mające dowolną liczbę synów, ulokowanych w dowolny sposób. Drzewo uporządkowane –drzewo ukorzenione, w którym określona jest kolejność następników

kaŜdego z węzłów. JeŜeli węzeł ma k następników to są to kolejno ”ponumerowane” 1,2, …. Do kaŜdego węzła drzewa uporządkowanego dołącza się jego synów w określonej kolejności.

Drzewo dokładnie wywaŜone –jeŜeli dla kaŜdego węzła, liczba węzłów w lewym i prawym poddrzewie róŜni się co najwyŜej o 1.

M–drzewo: kaŜdy węzeł posiada określoną liczbę potomków (≤ m), uporządkowanych w określony sposób.

Pełne drzewo rzędu m –wszystkie liście mają tę samą głęboko ść a wszystkie węzły wewnętrzne mają stopie ń m.

Liczba węzłów wewnętrznych o wysokości h wynosi Ww(m, h) = 1

1

−−

m

mh

. Ww(3, 2) = 4

Niech drzewo ma więcej niŜ 2 następniki . Dla tego samego węzła numerowane są one róŜnymi liczbami całkowitymi. W drzewie brakuje i–tego następnika, jeŜeli nie ma następnika o etykiecie i.

Drzewo uporządkowane moŜe być reprezentowane z wykorzystaniem list, związanych z węzłami. Lewe łącze węzła wskazuje na pierwszy w ęzeł listy jego synów (syna pierwszego z lewej). Prawe łącza węzłów poprzez listy wskazuj ą na braci znajduj ących si ę po prawej stronie .

Algorytmy i Struktury Danych – wykład 70 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

Drzewo binarne –drzewo uporządkowane, w którym kaŜdy węzeł ma co najwyŜej dwóch synów, a kaŜdy syn jest opisany jako lewy lub prawy.

Drzewo binarne definiowane rekurencyjnie to struktura zdefiniowana

na skończonym zbiorze węzłów, która spełnia załoŜenia podane obok.

Pełne drzewo binarne o wysokości h ma 2h –1 węzłów wewnętrznych. Drzewo binarne zawierające n węzłów wewn ętrznych ma n+1 węzłów zewnętrznych.

Dla dowolnego drzewa binarnego, w którym węzły wewnętrzne mają dokładnie po dwóch synów liczb ę liści n i liczb ę węzłów wewnętrznych k wiąŜe relacja: n = k + 1

Drzewo binarne zawierające n węzłów wewn ętrznych ma 2n łączy, z których n-1 prowadzi do węzłów wewnętrznych, a n+1 do węzłów zewnętrznych. Wysoko ść drzewa binarnego zawierającego n węzłów wewnętrznych wynosi

co najmniej lg(n) a co najwy Ŝej (n-1). Regularne drzewo binarne: kaŜdy z węzłów ma stopień 2 lub jest liściem. 9.1. Implementacja drzew binarnych

Drzewa binarne mogą być implementowane z wykorzystaniem tradycyjnych tablic statycznych bądź dynamicznych, jak teŜ z uŜyciem dynamicznych struktur wiązanych.

Tablicowa implementacja wymaga przypisania kaŜdemu węzłowi struktury 3 polowej, złoŜonej z:

pola danych, 2 pól zawierających indeksy komórek, w których

umieszczono lewego i prawego syna. Korzeń tradycyjnie umieszcza się w komórce nr 0.

Dynamiczna implementacja drzewa binarnego wymaga zdefiniowania dla kaŜdego w ęzła:

-struktury składającej się z 3-ch pól, -podania wskaźnika do korzenia.

JeŜeli Left[W] = NULL to węzeł W nie ma lewego syna.

JeŜeli Right[W] = NULL to węzeł W nie ma prawego syna.

RootD -wskaźnik do korzenia drzewa.

Drzewo puste: jeŜeli rootD = NULL .

1.Nie zawiera Ŝadnych węzłów. 2. Składa się z trzech rozłącznych zbiorów węzłów:

korzenia, lewego drzewa binarnego, prawego drzewa binarnego.

Pozycje synów

ind Dane Lewy Prawy

0 1 1 3 1 2 -1 2 2 8 -1 -1 3 4 4 5 4 6 -1 -1 5 5 6 -1 6 9 -1 -1

struct TreeNode TreeNode *Left typData Data TreeNode *Right ;

1

2 4

6 58

9R9.6

0

1

2

3

4 5

6

Page 37: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 71 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

9.2. Drzewa poszukiwań binarnych BST (Binary Search Trees)

Drzewo poszukiwań binarnych jest strukturą drzewa binarnego, w którym klucze w węźle spełniają ściśle określone zaleŜności.

JeŜeli klucz węzła k ma wartość a to: • wszystkie wartości przechowywane w lewym poddrzewie są mniejsze od a,

• wszystkie wartości przechowywane w prawym poddrzewie są większe od a.

Niech w będzie węzłem drzewa BST, zaś pL i pR jego lewym i prawym poddrzewem, wtedy key[w] > key[pL] zaś key[w] < key[pR]

Wartości kluczy w drzewie BST nie powinny się powtarzać.

Zbiór danych napływa w kolejno ści :

NaleŜy utworzyć drzewo BST. Ten sam zbiór wartości moŜna przestawić za pomocą wielu róŜnych drzew BST.

W większości przypadków pesymistyczny czas wykonywania operacji na drzewach BST jest

proporcjonalny do ich wysokości. NaleŜy dąŜyć do budowy niskich drzew.

W przykładzie (R9.10) drzewo (b) o wysokości h =2 będzie miało większą efektywność analityczną niŜ drzewo (a) o wysokości h = 5.

Węzły ze zdublowanymi kluczami nie musz ą ze sob ą sąsiadowa ć w drzewie.

Wyszukiwanie kluczy w BST

Wyszukiwanie węzła w drzewie BST o zadanym kluczu k moŜna zrealizować, mając dany wskaźnik do korzenia drzewa, poprzez funkcje rekurencyjne lub iteracyjne o czasie O(h).

Funkcja SearchTree zwraca wskaźnik do węzła, zawierającego poszukiwany klucz k lub wartość NULL, gdy takiego węzeł nie ma.

Proces poszukiwania rozpoczyna się od korzenia, schodząc stopniowo w dół drzewa. JeŜeli w trakcie porównywania klucza k z wartością key[wx] okaŜe się, Ŝe k < key[wx] to dalsze poszukiwania realizowane są w lewym poddrzewie węzła wx , gdyŜ definicja drzewa BST wyklucza moŜliwość wystąpienia k w prawym.

Gdy pierwsze porównanie daje relację k>key[wx] to poszukuje się w prawym poddrzewie.

Implementacja iteracyjna funkcji SearchTree jest czytelniejsza i zazwyczaj efektywniejsza.

SearchTree(wx, k)

if wx = NULL or k = key[wx] then return wx

if k<key[wx] then return SearchTree (left[wx], k)

else return SearchTree (right[wx], k)

SearchTreeIter(wx, k)

while wx ≠ NULL and k ≠ key[wx] do if k<key[wz] then wx left[wx]

else wx right[wx] return wx

TreeNode SearchIter( TreeNode node, typData key)

while (node) if (key == node ->key) return node;

else if (key < node ->key) node = node ->left;

else node = node ->right;

return NULL;

1 2 3 4 5 6 7 5 13 15 16 25 17 15

Algorytmy i Struktury Danych – wykład 72 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

9.2.1. Przechodzenie drzewa

Jest to proces odwiedzania kaŜdego węzła w drzewie tylko jeden raz. Nie jest określona kolejność odwiedzania węzłów.

Potencjalne moŜliwości przejścia drzewa określa liczba permutacji węzłów. Dla n–węzłowego drzewa, liczba moŜliwych dróg wynosi n!. Praktyczne zastosowanie znajdują dwie klasy przechodzenia: przechodzenie wszerz i w głąb.

Przechodzenie wszerz (breadth-first traveral) Odwiedzanie kaŜdego węzła, rozpoczyna się od korzenia , następnie przesuwa się w dół na kolejne poziomy, przeglądając je od lewej do prawej.

Otrzymuje się ciąg: 15,10, 25, 3, 12, 20, 35, 1, 5,14, 30

Implementacja metody z wykorzystaniem kolejki Po dojściu do węzła na poziomie r jego synowe (na poziomie r+1) umieszczani są na końcu kolejki i odwiedzany jest węzeł z początku kolejki.

Gwarantuje to, Ŝe synowie odwiedzeni zostaną dopiero po przejściu wszystkich węzłów z poziomu rrrr. W algorytmie Qt jest kolejką, przechowującą w polu danych adresy struktury typu drzewo.

Przechodzenie w głąb (depth-first traversal) w celu odwiedzenia wszystkich węzłów

Rozpoczynając od korzenia przechodzi się maksymalnie w lewo (lub prawo), następnie cofa się do najbliŜszego rozgałęzienia, potem jeden krok w prawo (lewo)

i znowu maksymalnie w lewo (prawo).

W zaleŜności od tego, czy i które węzły zostaną odwiedzone przed marszem w dół czy teŜ po powrocie, istnieje 3! = 6 wariantów przechodzenia: vLR, LvR, LRv; vRL, RvL, RLv,

gdzie v –odwiedzenie , L i R –przejście lewego lub prawego poddrzewa .

Praktycznie wykorzystuje się taktykę poruszania od lewej prawej nadając standardowe nazwy.

inOrder LvR –węzeł wizytowany jest po przejściu lewego poddrzewa a przed wejściem do prawego.

preOrder vLR –węzeł wizytowany jest przed odwiedzeniem w obu poddrzewach.

postOrder LRv –węzeł wizytowany jest po odwiedzeniu w obu poddrzewach.

DuŜe znaczenie ma metoda InOrder, której wywołanie InOrder(rootBST) wypisze wszystkie elementy drzewa BST w porządku rosnącym.

Breadth (TreeNode node) if (node) Enq(node, Qt); //key[node]

while (!Empty(Qt) node = Deq(Qt);

Disp(key[node]);

if (left[node]) Enq(left[node], Qt); if (right[node]) Enq(right[node], Qt);

15 Deq 10 25

25 3 12

3 12 20 35

12 20 35 1 5

… … …

Page 38: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 73 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

W metodzie InOrder najpierw odwiedzane jest lewe poddrzewo bieŜącego węzła, potem sam węzeł a na końcu prawe poddrzewo.

Następnik w drzewie BST

Następnik węzła w:następny węzeł odwiedzany w porządku inOrder.

Następnik moŜna wyznaczyć nie wykonując porównań kluczy.

JeŜeli wszystkie klucze drzewa są róŜne to następnikiem(successor) węzła w jest węzeł o najmniejszym kluczu ze zbioru kluczy większych niŜ key[w].

(a) JeŜeli węzła w ma prawe poddrzewo dla to następnikiem jest najbardziej na lewo połoŜony węzeł w prawym poddrzewie (przykład – następnik węzła 15).

Następnik węzła bez prawego syna znajduje się gdzieś nad nim.

(b) JeŜeli węzeł w nie ma prawego poddrzewa to następnikiem jest:

-”najniŜszy” przodek ⇑⇑⇑⇑, którego lewy syn jest równieŜ przodkiem ⇑⇑⇑⇑ węzła w,

gdy węzeł w jest prawym synem.

NaleŜy przejść w górę drzewa, aŜ do napotkania węzła, który jest lewym synem swego ojca (ojciec– to następnik)

-ojciec węzła w, gdy węzeł w jest jego lewym synem.

InOrder(node)

if node ≠ NULL then InOrder(left[node]) (1) Disp(key[node]) InOrder(right[node]) (2)

PreOrder(node)

if node ≠ NULL then Disp(key[node]) PreOrder(left[node])

PreOrder(right[node])

PostOrder(node)

if node ≠ NULL then PostOrder(left[node])

PostOrder(right[node]) Disp(key[node])

15InOrder

1713

10

20

25

40115

35

1

2

3

7

5 8

9

10

11

12

L-v-R(1)

(1)

(1)

(1) (2)

Disp(5)

14

12 4

6

R9.12a

NastepnikBST(w) if right[w] ≠ NULL then return Minimum(right[w]) // (a)

y ojciec[w] while y ≠ NULL and w = right[y] do w y y ojciec[y] return y

Algorytmy i Struktury Danych – wykład 74 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

9.2.2. Operacje na drzewie BST

Maksimum i minimum

Węzeł o najmniejszym kluczu moŜna znaleźć idąc od korzenia aŜ do napotkania NULL według wskaźników left, zaś węzeł o największym kluczu według wskaźników right.

JeŜeli węzeł wx nie ma lewego poddrzewa, to najmniejsza wartość w poddrzewie o korzeniu wx jest key[wx].

JeŜeli węzeł wx nie ma prawego poddrzewa, to najwi ększa wartość w poddrzewie o korzeniu wx jest key[wx].

Czas wykonania funkcji jest rzędu O(h), poniewaŜ przegląd następuje tylko w dół drzewa.

Wstawianie

Wstawianie elementu a polega na przekazaniu do stosownej funkcji rekordu z nowym węzłem nw o strukturze obok. Poszukiwanie miejsca do wstawienia rozpoczyna się od korzenia i przebiega wzdłuŜ ścieŜek w dół drzewa, aŜ do napotkania węzła nie mającego odpowiedniego syna.

Jest to poszukiwane miejsce.

JeŜeli węzeł staje się prawym synem innego węzła, to dziedziczy następnik po swoim nowym ojcu. JeŜeli węzeł staje się lewym synem innego węzła, to jego nowy ojciec staje się jego następnikiem.

W pętli while wskaźniki x i y przesuwane są w dół drzewa w lewo lub w prawo aŜ do chwili gdy napotkają wartość NULL .

Jest to miejsce, w które naleŜy wstawić wskaźnik na nowy węzeł nw

InsertTree(nw, BST)

y NULL // y, x – wskaźniki robocze x root[BST]

while x ≠ NULL do // wyszukiwanie y x if key[nw] < key[x] then x left[x] else x right[x]

// --------------------------------------- operacja wstawiania if y = NULL then root[BST] nw

else if key[nw] < key[y] then left[y] nw

else right[y] nw

=→=→=→

=NULLrightnode

NULLleftnodeakeynode

nw

MaximumTree(wx) while right[wx] ≠ NULL do wx right[wx] return wx

MinimumTree(wx)

while left[wx] ≠ NULL do wx left[wx] return wx

15Ins(17)

2510

3 20 35

30

13

5 17

R9.1315Ins(14)

2510

3 20 35

30

13

5 14

Page 39: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 75 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

Usuwanie

MoŜliwe są 3 przypadki usuwania węzła uw

(1) jeŜeli węzeł uw nie ma syna to zostaje usuni ęty i u jego ojca[uw] wstawiany jest wskaźnik do uw równy NULL,

(2) jeŜeli węzeł uw ma jednego syna to uw zostaje wyci ęty przez uaktualnienie wskaźników między jego ojcem a synem.

(3) jeŜeli uw ma dwóch synów, to wyizolowany zostaje węzeł y, będący następnikiem uw , następnie węzeł y zastępuje węzeł uw

Izolowany jest węzeł y = 70, będący następnikiem usuwanego węzła 61.

W procedurze Delete() węzeł pośredni y staje się:

–węzłem usuwanym uw, gdy ma najwyŜej 1-go syna, –następnikiem uw, jeŜeli uw ma dwóch synów.

Delete(uw, BST) // działa w czasie O(h)

// -----------wyznaczenie węzła pośredniego y if left[uw] = NULL or right[uw] = NULL then y uw else y Następnik(uw)

// do zmiennej x przypisuje się y lub NULL gdy y nie ma synów if left[uw] ≠ NULL then x left[y] else x right[y]

if x ≠ NULL then ojciec[x] ojciec[y] // operacja usuwania y

if ojciec[y] = NULL then root[BST] x

else if y = left[ojciec[y]] then left[ojciec[y]] x

else right[ojciec[y]] x

if y ≠ wx then key[uw] key[y] // kopiowanie wszystkich pól y

Del(35)

Algorytmy i Struktury Danych – wykład 76 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

9.3. RównowaŜenie drzewa Drzewa wyszukiwania binarnego nie dają gwarancji uzyskania drzewa

zrównowaŜonego w przypadku dynamicznego dodawania i usuwania kluczy.

W krańcowym przypadku moŜemy otrzymać drzewo pochylone, będące w rzeczywistości listą jednokierunkową. Ma to miejsce w przypadku, gdy klucze dodawane będą zgodnie z ich kolejnością w rosnąco posortowanej sekwencji. W efekcie otrzymujemy szukanie sekwencyjne .

JeŜeli klucze dodawane są w losowej kolejności, drzewo powinno być zbliŜone do struktury drzewa zrównowaŜonego, zapewniając efektywny średni czas przeszukiwania.

Drzewo zrównowaŜone: róŜnica wysokości obu poddrzew kaŜdego węzła wynosi 0 lub 1.

Drzewo doskonale zrównowaŜone: drzewo zrównowaŜone, w którym wszystkie liście znajdują się na jednym lub dwóch poziomach.

9.3.1. Tablicowa metodyka równowaŜenia drzewa

1. Umieść napływające dane w tablicy X[1, n]. 2. Posortuj otrzymaną tablicę.

3. Wybierz jako korzeń środkowy element tablicy X. Powstają dwie podtablice: X[1, k-1] oraz X[k+1, n]. 4. Lewym synem korzenia jest środek lewej podtablicy

Powstają dwie kolejne lewe podtablice. 5. Prawy syn korzenia to środek prawej podtablicy

Powstają dwie kolejne prawe podtablice.

6. Powtarzaj czynności (3 ÷ 5) dla nowych podtablic.

JeŜeli dane napływają sukcesywnie:

-ulokować w roboczym drzewie niezrównowaŜonym,

-przechodząc metodą inOrder przenieść je do tablicy (uporządkowane), -wywołać funkcję Balance() dla otrzymanej tablicy.

void Balance(int lo, int hi, int X[ ]) int k = (lo + hi )/2 // middle if (lo <= h0)

Insert(X[k], &node);

Balance(lo,k-1, X);

Balance(k+1, hi, X);

0

0+1

Page 40: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 77 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

9.3.2. Algorytm DSW

Operacja rotacji Podstawową operacją przywracającą równowagę w drzewach BST jest rotacja.

Rotacja polega na „obrocie" wokół krawędzi między węzłami (O, S) (S, O).

Prawą rotację na węźle O moŜna wykonać tylko wtedy, gdy istnieje jego lewy syn S.

Lewą rotację (b) na węźle S moŜna wykonać tylko wtedy, gdy istnieje jego prawy syn O.

Operacja RotateRight () zamienia konfigurację węzłów przez zmianę wartości wskaźników.

Rotacja zachowuje porządek inOrder kluczy.

Z konfiguracji po prawej stronie (b) moŜna przejść do konfiguracji po lewej, stosując RotateLeft().

Prostowanie drzewa - 1-szy etap

Pierwszym etapem algorytmu DSW jest przekształcenie drzewa w linię. Następuje to przez wykonanie RotacjiPrawej na węzłach mających lewych synów.

Niech drzewo ma n węzłów. Pętla while wykona się 2n-1 razy i wykona n -1 rotacji w najgorszym przypadku.

Zatem czas działanie 1-szej fazy wynosi O(n).

2823

5

10

15

40

20

25

30

(aaaa)

(bbbb)

wrk

2823

5

10

15

40

20

25

30wrkwrkwrkwrk

(cccc)

4028

5

10

15

30

20

23

25wrkwrkwrkwrk

30

28

5

10

15

25

20

40

23

wrkwrkwrkwrk

(dddd)

28

40

5

10

15

25

20

30

23

(eeee)

wrk

wrkwrkwrkwrk

2823

5

10

20

40

15

25

30

RightRotate(D, O, S)

JeŜeli O nie jest korzeniem drzewa (D ≠ NULL):

Ojciec węzła Syn staje się Synem.

1. prawe poddrzewo S staje się lewym poddrzewem węzła O;

2. węzeł O staje się prawym synem swego dotychcza-sowego lewego syna S;

3. Dziadek węzła S(wnuk) staje się ojcem wnuka S

Prostowanie(Root)

wrk = Root; while (wrk ≠ NULL)

if wrk "ma lewego syna" RightRotate (tego syna względem wrk);

else wrk następny węzeł, który był prawym synem wrk;

Uwaga: wykonanie Rotacji wymaga zachowania ojca węzła wrk.

OOOO

SSSS

Lp2

Pp2

Pp1

OOOO

SSSS

Lp2

Pp2 Pp1

DDDD DDDD

DD DDziadek OO OOjciec

SSSSyn(wnuk)

(aaaa) (bbbb)

PrawaPrawaPrawaPrawa Rotacja SSSSyna względem OOOOjca

Algorytmy i Struktury Danych – wykład 78 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

Tworzenie drzewa zrównowaŜonego - 2-gi etap

Przekształcenie linii węzłów w drzewo zrównowaŜone realizują operacje RotacjaLewa .

Lewą rotację (a) na węźle O moŜna wykonać tylko wtedy, gdy istnieje jego prawy syn S.

JeŜeli n jest liczbą węzłów to 1))1(lg(2 −+= nfloorm jest liczbą węzłów w najbliŜszym pełnym drzewie. Dokonując ruch w dół drzewa wykonywane są LewaRotacja co 2-gi węzeł względem jego ojca.

KaŜdy przebieg skraca długość linii o połowę.

W pierwszym przebiegu wykonuje się n – m rotacji. W dalszych przebiegach rotacje wykonywane są o

góry do końca.

ZauwaŜmy, Ŝe nnnfloorm =−+≤−+= 1)1lg(21))1(lg(2

Liczbę rotacji określa zaleŜność: n duzychdla 21

2

1)lg(

1

1)lg(

1

nnm

mnm

kk

m

kk

≈+≤+− ∑∑−

=

=

Łączny koszt równowaŜenia drzewa algorytmem DSW jest klasy O(n).

LeftRotate (D, O, S)

JeŜeli O nie jest korzeniem drzewa (D ≠ NULL):

Ojciec węzła Syn staje się Synem.

1. lewe poddrzewo S staje się prawym poddrzewem węzła O;

2. węzeł O staje się lewym synem swego dotychcza-sowego prawego syna S;

3. Dziadek węzła S(wnuk) staje się ojcem wnuka S

SSSS

OOOO

Lp1

Pp2

Pp2

SSSS

OOOO

Lp1

Pp2 Pp2

DDDD DDDD (aaaa)(bbbb)

LewaLewaLewaLewa Rotacja SSSSyna względem OOOOjca

Przekształcanie(Root, n)

1)1lg(2 −+= nm for 1 = 1 to n - m step 2 do LeftRotate;

while (m > 1)

m = m/2; LeftRotate(węzła względem jego Ojca);

(aaaa)

(bbbb)

(cccc)28

40

5

10

15

25

20

30

23

5

10

20

2315

25

28

30

40

10

5

28

40

15

25

20

30

23

n n n n = = = = 9999m m m m = = = = 7777

1111----szy przebiegszy przebiegszy przebiegszy przebieg

(dddd) (eeee)

10

20

5 15

23

25

28

30

40

10

20

5 15

25

23 28

30

40

(ffff)

10

20

5 15

25

23 30

28 40

25

20

10

5 15

30

28 40

(gggg)

23

2222----gi przebieggi przebieggi przebieggi przebieg

3333----ci przebiegci przebiegci przebiegci przebieg

Page 41: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 79 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

9.3.3. Drzewa AVL - Adelson –Velskii, Landis (1962)

Drzewo AVL to drzewo przeszukiwań binarnych, gdzie wysokości lewego i prawego poddrzewa kaŜdego węzła róŜni się co najwyŜej o jeden.

UmoŜliwia lokalne równowaŜenie drzewa, jeśli zmiany niezbędne po wstawieniu lub usunięciu elementu dotyczą tylko części drzewa.

Współczynnik wywaŜenia (liczby w węzłach): róŜnica wysokości lewego i prawego poddrzewa. Współczynniki powinny być równe +1, 0 -1.

Gdy współczynnik wywaŜenia w dowolnym węźle AVL staje się róŜny od (+1, 0 -1), to drzewo musi zostać zrównowaŜone.

Wysokość h drzewa AVL:

lg(n + 1) < h< 1.44 lg(n + 2) – 0.328

co oznacza, Ŝe wyszukiwanie w najgorszym przypadku jest klasy O(lg(n)).

JeŜeli z powodu wstawienia węzła, jedno z poddrzew węzła zwiększy wysokość o 1, wtedy warunek równowagi moŜe zostać naruszony. Jedna lub podwójna Rotacja

przywróci węzeł do równowagi. Wstawienia węzła do prawego poddrzewa prawego syna

Wstawianiu węzła do lewego poddrzewa prawego syna

Znaleźć węzeł S, którego współczynnik wywaŜenia stał się nieakceptowalny po wstawieniu nowego węzła do drzewa.

Jak? : idąc w górę drzewa od miejsca wstawienia nowego węzła, w kierunku korzenia i uaktu-alniając współczynniki wywa Ŝenia napotykanych wierzchołków.

Pierwszy w ęzeł, którego współczynnik wywa Ŝenia zmieni się na ±2, jest korzeniem poddrzewa, którego równowagę trzeba przywrócić.

Dla doskonale zrównowaŜonego drzewa binarnego h = lg(n+ 1). W najgorszym przypadku wyszukiwanie w drzewie AVL wymaga 44% więcej porównań niŜ w drzewie doskonałym. Eksperymenty pokazały, Ŝe średnia liczba porównań jest znacznie lepsza niŜ wynikałoby to z najgorszego przypadku. Dlatego z drzewa AVL mogą być atrakcyjne w róŜnych implementacjach.

8

186

19

1

7 11

22

2

17

9

12

14 20

1

drzewo AVLAVLAVLAVL +1

-1 -1

0-1

0

0 0

000

2

3

4

0

+1 02

9

186

197 15

22

2

12 16 20

+1

44

9999

181818186

197 15

22

2

12 16 20

++++2222

+1

+1

18181818

9999

6

72 44

19

22

+1

2015

12 16

0000LewaRotacja (18)

względem Ojca (9) SSSS

9

186

197 15151515

22

2

12 16 20

+1

11111111

6

19

7

22

2

20

++++2222 PrawaRotacja (15)

względem Ojca (18) 9

15151515

1812

11111111 16

6

72

9

15151515

19

2220

18

1612

11111111

LewaRotacja (15)

względem Ojca (9) SSSS 0000

Algorytmy i Struktury Danych – wykład 80 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

ANEX 9.1. Binary Tree Interface

newTree() inicjalizacja drzewa emptyTree() testowanie czy drzewo puste createTreeNode() nowy węzeł drzewa getData() zwraca klucz węzła drzewa setData() wstawia klucz do węzła drzewa

getLeft() zwraca lewe poddrzewo getRight() zwraca prawe poddrzewo setLeft() wstawia lewe poddrzewo do nowego setRight() wstawia prawe poddrzewo do nowego

struct TreeNode TreeNode *left; typData Data; TreeNode *right; ; typedef TreeNode *Tree;

Tree newTree(Tree T) // return new empty Tree T return (T = NULL);

Boolean emptyTree(Tree T) // return TRUE if empty, return (T == NULL);

TreeNode *createTreeNode(typData e) // return new tnode containing typData e TreeNode *node; node = new(TreeNode);

if (node == NULL) puts("Error in new"); exit(1);

node->left = NULL; node->right = NULL; node->data = e; return node;

Element getData(Tree T) // return data contained at current Tree node

if (emptyTree(T)) puts("empty Tree "); exit(1);

return T->data;

Tree setData(typData e, Tree T) // return Tree T with root data set to typData e if (emptyTree(T)) puts("empty Tree "); exit(1);

T->data = e; return T;

Tree getLeft(Tree T) //return left child Tree of Tree T

if (emptyTree(t)) puts("empty Tree "); exit(1);

return T->left;

Tree getRight(Tree T) // return reft child Tree of Tree T if (emptyTree(t)) puts("empty Tree ");exit(1);

return T->right;

Tree setLeft(Tree newT, Tree T) // set left child Tree of Tree T to Tree newT

T->left = newT; return T;

Tree setRight(Tree newT, Tree T) // set right child Tree of Tree T to Tree newT

T->right = newT; return T;

Tree InsertTree(typData e, Tree T) if (emptyTree(T)) return createTreeNode(e);

if (e < getData(T)) T = setLeft(InsertTree(e, getLeft(T)), T);

else if (e > getData(T)) T = setRight(InsertTree(e, getRight(T)),T);

return T;

Page 42: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 81 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

void BreadthFirst(Tree T) typeQueue Qt; // zmienna robocza-kolejka InitQ(&Qt);

if (T)

Enq(T, &Qt);

while (!EmptyQ(Qt))

T = Deq(&Qt);

printf("%3d",T->data);

if (T->left) Enq(T->left, &Qt);

if (T->right) Enq(T->right, &Qt); puts("");

................... int main() // P8.4a Tree Tree root = NULL; int liczba; puts("Wprowadz wartosci:");

while(scanf("%d",&liczba)) root = InsertTree(liczba, root);

puts("breadthFirst"); breadthFirst(root);

puts("InOrder"); InOrder(root);

printf("\nElement do usuniecia: "); fflush(stdin); scanf("%d",&liczba); deleteTree(liczba, root); puts("Po usunieciu(InOrder)");

InOrder(root); return 0;

Wprowadz wartosci: 11 32 45 65 67 654 1 2 3 q breadthFirst 11 1 32 2 45 3 65 67 654 InOrder 1 2 3 11 32 45 65 67 654

Element do usuniecia: 11

Po usunieciu (InOrder) 1 2 3 32 45 65 67 654

void InOrder(Tree t) if (! emptyTree(t))

InOrder(getLeft(t));

printf("%3d", getData(t)); InOrder(getRight(t));

Tree deleteTree(typData e, Tree t) Tree wrk; int newe;

if (EmptyT(t)) puts(” Brak drzewa”); exit();

if (e == getData(t))

if (EmptyT(getLeft(t)) && EmptyT(getRight(t))) wrk=t; delete wrk; t=newTree(t); else if (EmptyT(getLeft(t)) && !EmptyT(getRight(t))) wrk=t; t=getRight(t); delete wrk; else if (EmptyT(getRight(t)) && !EmptyT(getLeft(t))) wrk=t; t=getLeft(t); delete wrk; else t = setRight(Successor(&newe, getRight(t)), t); t = setData(newe,t); else if (e < getData(t)) t = setLeft(deleteTree(e, getLeft(t)), t);

else t = setRight(deleteTree(e, getRight(t)), t);

return t;

Algorytmy i Struktury Danych – wykład 82 Instytut Aparatów Elektrycznych Algrm9A Dr J. Dokimuk

ANEX 9.2.

(a) Pierwsze wołanie wkłada na stos adres powrotu do InOrder() <adr> oraz wartość key=15. Drzewo nie jest puste, następuje realizacja instrukcji if .

Funkcja InOrder() wywoływana jest dla node.key =4. (b) Na stos trafia adres powrotu <102> oraz wartość key=4. Węzeł nie jest pusty, zatem InOrder() wywoływana jest dla lewego syna node.key=1 . (c) Na stos trafia adres powrotu <102> oraz wartość zmiennej key=1.

InOrder() jest wywoływana dla lewego syna node.key =1. (d) Na stos trafia adres<102> oraz wartość NULL .

NULL natychmiast kończy wywołanie InOrder(). Automatycznie zdejmowany jest ze stosu rekord wywołania.

(e) System odtwarza wartość zmiennej node.key =1. Wykonuje instrukcję pod adresem<102>, co odpowiada wydrukowaniu wartości 1. Wartość node.key =1 i adres <102> pozostają na stosie, gdyŜ przetwarzanie węzła nie zostało zakończone.

(f) Rozpoczyna się wykonywanie instrukcji po adresem <103>, co odpowiada wywołaniu funkcji InOrder() dla prawego syna node.key =1. Zatem na stos trafia adres <104> oraz bieŜąca wartość node =NULL. NULL kończy działanie InOrder(), usuwając rekord wywołania.

(g) System odtwarza poprzednią wartość zmiennej node.key =1 i wykonuje instrukcję <104>. Jest to koniec wywołania InOrder().

(h) System odtwarza ze stosu wartość node.key =4. Wykonuje instrukcję <102>, co daje wydruk wartości 4. Następnie wywołuje InOrder() dla prawego syna węzła – wartość NULL.

Struktura stosu podczas realizacji funkcji InOrder() [2]

NULL NULL <102> <104> NULL 1 1 1 1 1 4 <102> <102> <102> <102> <102> <104> 4 4 4 4 4 4 4 4 4 20 <102> <102> <102> <102> <102> <102> <102> <102> <102> <104> 15 15 15 15 15 15 15 15 15 15 15 15

<adr> <adr> <adr> <adr> <adr> <adr> <adr> <adr> <adr> <adr> <adr> <adr>

a b c d e f g h i j k l

NULL NULL <102> <104>

16 16 16 16 <102> <102> <102> <102>

20 20 20 20 <102> <102> <102> <102>

15 15 15 15 <adr> <adr> <adr> <adr> m n o p

InOrder( typeTree node) if (node ) InOrder(node->left); <101>

Disp(node); <102> InOrder(node->right); <103> <104>

15InOrder

204

1 25

3

2

1

5

64

L-v-R

16

Page 43: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 83 Instytut Aparatów Elektrycznych Algrm9B Dr J. Dokimuk

9.3. Kopiec (Heap)

Kopiec jest drzewem binarnym o nast ępujących własno ściach: 1. wartość klucza w kaŜdym węźle jest nie mniejsza niŜ klucze u jego potomkach, 2. wszystkie liście drzewa leŜą na co najwyŜej dwóch sąsiednich poziomach, 3. liście na ostatnim poziomie szczelnie wypełniają lewą część drzewa.

Kopiec tworzony jest na bazie pełnego drzewa binarnego, stąd jego wysokość jest rzędu O(lg(n) ). Kopiec moŜe reprezentować tablica K[1…n], w której kaŜdy węzeł drzewa odpowiada elementowi tablicy o parametrach:

K[i] ≥ K[ 2i ] oraz K[i] ≥ K[ 2i + 1], gdzie 0 ≤ i < n/2.

Korzeniem drzewa jest K[1]. Górne ograniczenie indeksu i wynika z definicji (2).

Atrybutami tablicy K reprezentującej kopiec są:

n = length(K) –długość tablicy

nK –liczba elementów kopca w tablicy K. śaden element tablicy K[1…n], występujący po indeksie nK nie jest elementem kopca K.

Dla i–tego węzła kopca moŜna obliczyć indeksy ojca oraz lewego i prawego syna.

Węzeł ma własność kopca jeŜeli dla kaŜdego węzła i , który nie jest korzeniem zachodzi: K[Ojciec(i)] ≥ K[Potomek(i)] .

Wstawianie elementu do kopca

• dodaj nowy liść z kluczem w do drzewa,

• przesuwaj klucz w w górę po ścieŜce, tak długo dopóki nie dojdzie do korzenia lub nie natrafi na ojca o wartości nie mniejszej od w,

• przechodząc przez kolejne węzły, zamień nowy węzeł z jego ojcem.

Ojciec(i) Lewy(i) Prawy(i)

return i/2 return 2i return 2i + 1

i/2 2i + 1 2i + 2

InsertHeap(w, K) nK nK + 1 // miejsce na wstawienie K[nK] w i nK

while i > 1 and w > K[Ojciec(i)] do Swap(K[i] ,K[Ojciec(i)] ) i Ojciec(i)

iiii

2222iiii 2222iiii++++1111

LLLL PPPP

dla 60 dla 58 dla 54

K[i] 60 58 54 52 51 53 47 46 48 45 i 1 2 3 4 5 6 7 8 9 10

Algorytmy i Struktury Danych - wykład 84 Instytut Aparatów Elektrycznych Algrm9B Dr J. Dokimuk

Usuwanie elementu z kopca

Usuwanie elementu z kopca, to usunięcie elementu o maksymalnym kluczu.

Usuwany jest korzeń i w jego miejsce wstawiany jest ostatni liść. Powoduje to destrukcję kopca. W celu przywrócenia własności kopca naleŜy dokonać przestawień idąc od korzenia w dół.

Przywracanie własności kopca

Po operacji na kopcu często w jakimś węźle wartość klucza ojca jest mniejsza od kluczy jego synów.

Drzewo binarne utraciło własność kopca. Funkcja HeapDown przywraca ją, powodując spłynięcie klucza K[i] w dół kopca,

tak aby poddrzewo zaczepione w węźle i stało się kopcem.

W kaŜdym kroku działania HeapDown() poszukiwana jest wartość maksymalna pośród dzieci węzła i.

Indeks wartości maksymalnej pamiętany jest w zmiennej Max. JeŜeli K[i] jest największe to jest OK.

JeŜeli nie to zamienia się K[i] z K[Max]. Teraz bada się czy poddrzewo doczepione do K[Max] spełnia własność kopca.

JeŜeli nie to funkcja HeapDown() wykonywana jest rekurencyjnie na tym poddrzewie.

HeapDown() działa poprawnie przy załoŜeniu, Ŝe poddrzewa podczepione do synów

węzła i są kopcami.

Czas działania dla kopca n–węzłowego T(n) ≤ T(2n/3) + O(1) = O(lg(n)),

gdzie O(1) –czas poprawiania w węźle i relacji miedzy ojcem(i) i jego synami oraz rekurencyjne wywoływania.

DeleteHeap(K) if nK < 1 then ”Kopiec pusty”

max K[1]

K[1] K[nK]

nK nK - 1

HeapDown(1, K)

return max

HeapDown(i, K)

l Left(i); r Right(i)

if l ≤ nk and K[l] > K[i] then Max l else Max i

if r ≤ nk and K[r] > K[Max] then Max r

if Max ≠ i then Swap(K[i], K[Max] )

HeapDown(Max, K)

Page 44: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 85 Instytut Aparatów Elektrycznych Algrm9B Dr J. Dokimuk

21 22 23 24 25 26 27 28 29 33 1 2 3 4 5 6 7 8 9 10

Budowanie kopca

Tablicę K[1…n] niebędącą kopcem moŜna przekształcić w kopiec, wykorzystując funkcję HeapDown.

Podstawą działania jest fakt, Ŝe wszystkie elementy podtablicy K[(n/2 + 1) …n] są liśćmi drzewa, a tym samym 1–elementowym kopcem.

Funkcja BuildH, przechodząc przez węzły wewnętrzne, wywołuje funkcję HeapDown.

Zachodzi wstępujące (bottom-up) przekształcenie tablicy K.

Wywołanie HeapDown realizowane jest w czasie O(lg(n)). Liczba wywołań moŜe sięgnąć O(n). Pesymistyczny czas budowania kopca moŜna szacować na O(nlg(n)).

Kopiec n–elementowy zawiera co najwyŜej n/2h+1 węzłów o wysokości h Czas działania funkcji HeapDown, wywołanej w węźle o wysokości h wynosi O(h). Asymptotyczny czas działania funkcji BuildH jest klasy O(n).

BuildH(K) n length(K); nK n

for i n/2 downto 1 do HeapDown(i, K)

Algorytmy i Struktury Danych - wykład 86 Instytut Aparatów Elektrycznych Algrm9B Dr J. Dokimuk

Sortowanie przez kopcowanie

Istota algorytmu to sukcesywna zamiana wierzchołka kopca (wartość maksymalna) z jego ostatnim elementem i przywracanie własności kopca nowemu drzewu binarnemu.

Pierwszy krok to budowa kopca na tablicy wejściowej K[1…n]. Aktualny wierzchołek kopca zamieniany jest z ostatnią pozycja tablicy K, oraz zmniejszony zostaje rozmiaru kopca (nk = nk - 1 ).

Zmniejszonej tablicy K[1…nk-1 ] przywraca się własność kopca poprzez wykonanie HeapDown(1, K).

Proces powtarza się aŜ dojdzie się do kopca o rozmiarze 2, wówczas tablica K zawiera posortowane klucze.

Sortowanie przez kopcowanie wykonywane jest w czasie T(n) = O(nlg(n)) Wynika to z czasu budowania kopca na dowolnej tablicy, wynoszący T(n)=O(n) oraz wykony-wanych n – 1 wywołań funkcji HeapDown() , której czas jednego wykonania wynosi O(lg(n)).

Dokładna analiza pozwala na stwierdzenie, Ŝe sortowanie przez kopcowanie wymaga mniej niŜ 2nlg(n) porównań.

Sortowanie przez kopcowanie jest niestabilne.

Zalety sortowania przez kopcowanie: • gwarantuje posortowanie n–elementów w miejscu, • czas sortowania nie zaleŜy od struktury danych wejściowych (brak najgorszego przypadku), • efektywne w przypadku wybierania k najwi ększych elementów w zbiorze n–elementowym.

(dla małych k czas wyboru jest proporcjonalny do n).

HeapSort(K) BuildH(K) for i length(K) downto 2 do Swap(K[1], K[i] ) nK nK – 1 HeapDown(1, K)

Sortowanie przez kopcowanie

(a)10

89

7 5 4

2

6

3

1

2 3

4 6 7

8 9

110

BuildH

5

HeapDown(b1)

(a1)

1

89

7 5 4

2

6

3

1

2 3

4 6 7

8 9

10

5

(b)9

87

3 5 4

2

6

1

1

2 3

4 6 7

8 910

5

(b1) (c)2

87

3 5 4

9

6

1

1

2 3

4 6 7

8

10

5

8

57

3 2 4

9

6

1

1

2 3

4 6 7

8

10

5

(c1)1

57

3 2 4

9

6

8

1

2 3

4 6 7

10

5

Poz. a1, b1, … po wykonaniu przestawienia korzenia z ostatnim elementem;

poz. b, c, ... po wykonaniu HeapDown

(d)

7

56

3 2 4

9

1

8

1

2 3

4 6 7

10

5

(d1)4

56

3 2 7

9

1

8

1

2 3

4 6

10

5

Swap(8,1)

(e)6

54

3 2 7

9

1

8

1

2 3

4 6

10

5

Swap(10,1)

Swap(2,9)

Swap(7,4)

1

nK=nK-1

nK=nK-1 nK=nK-1

nK=nK-1

HeapDown(d1)

HeapDown(c1)

HeapDown(a1)

10 9 8 7 6 5 4 3 2 1

6 4 5 3 1 2 7 8 9 10

Page 45: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 87 Instytut Aparatów Elektrycznych Algrm9B Dr J. Dokimuk

struct Record int key; // priorytet typP AdresProgramu; bla.....bla bla

9.3.1. Przykłady zastosowania kopca

Jednym z istotnych i najbardziej naturalnych zastosowań kopca są kolejki priorytetowe.

Kolejka priorytetowa to struktura danych, słuŜącą do przechowywania zbioru R rekordów. KaŜdy rekord ma przyporządkowany klucz, identyfikujący go jednoznacznie.

Kolejka priorytetowa (ATD) realizuje funkcje: • Umieszcza elementy o podanym priorytecie, w kolejce • Wyszukuje w kolejce elementy o najwyŜszym priorytecie • Usuwa elementy o najwyŜszym priorytecie.

Znajduje zastosowanie w sytuacji, gdy rejestrowane są sukcesywnie napływające zgłoszenia, zaś do obsługi konieczne jest wybranie zgłoszenia o najwyŜszym priorytecie, niezaleŜnie od kolejności, w jakiej napływają.

Po obsłudze zgłoszenia jest ono usuwane z kolejki. Obsługa elementów o równym priorytecie moŜe być dowolna lub chronologiczna.

Funkcje udostępniane przez kolejkę priorytetową: Insert(xr, R) –wstawia nowy rekord do zbioru R Max(R) –zwraca rekord (adres) o największym kluczu. DelMax(R) –zwraca rekord o największym kluczy a

następnie usuwa go ze zbioru R. Empty(R) –zwraca zero gdy kolejka jest pusta

Praca wsadowa

Ustalanie kolejności wykonywania napływających programów w systemie współbieŜnym.

Kolejka priorytetowa przechowuje zadania do wykonania oraz ich priorytety względem siebie. Zakończenie realizacji zadania powoduje pobranie przez funkcje DelMax nowego zadania o

najwyŜszym priorytecie z pośród oczekujących na realizacje. Napływające zadania mogą być dołączone do kolejki w dowolnej chwili przez funkcję Insert.

Symulowanie zdarzeń

Kolejka zawiera zdarzenia do symulacji, których kluczem jest czas ich występowania.

Zdarzenia symulowane są w kolejności ich zajścia.

Wygodnie jest operować funkcją DelMin, która wykorzystywana jest przez stosowny program do wybierania zdarzeń do symulacji, w połączeniu z funkcją Min. Mamy więc do czynienia z kolejką o odwróconym liniowym porządku.

JeŜeli zachodzą nowe zdarzenia to są dodawane do kolejki przez funkcję Insert.

typData *PQ = new typData[maxN]; n = 0; int Empty() return n == 0;

void Insert(typData el) PQ[n++] = el; HeapUp(n-1, PQ);

typData GetMax() swap(PQ[0], PQ[n-1]); HeapDown(0, n-1, PQ); return PQ[--n] ;

void HeapUp(int k, typData X[ ]) // k -start od tego elementu kopca

while (k > 0 && X[k] > X[(k-1)/2])

swap(&X[k], &X[(k-1)/2]);

k = (k - 1)/2;

Page 46: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 88 Instytut Aparatów Elektrycznych Algrm10 Dr J. Dokimuk

10. B–DRZEWA Realizują obsługę duŜych struktur danych, działających na pamięciach masowych (dyskowych).

Jednostką danych na dysku jest blok (cluster), którego lokalizacja na powierzchni moŜe być dowolna.

Aby zapisać/odczytać węzeł (cluster) naleŜy zlokalizować go na powierzchni nośnika, przed wykonaniem operacji.

Czas dostępu do węzła moŜna określić zaleŜnością: czas szukania + bezwładność obrotowa + czas transferu

Algorytmy odsługujące drzewa BST, wydajnie działaj ące w PAO komputera, są absolutnie nieprzydatne w kontekście współpracy z nośnikami zewnętrznymi.

B–drzewa redukują obciąŜenia wynikające z czasu dostępu do danych w strukturach pamięci masowej gdyŜ pozwalają na dopasowanie węzła do rozmiaru bloku danych.

Liczba kluczy w węźle jest zmienna i zaleŜy od rozmiaru klucza, jak teŜ od struktury danych. W węźle mogą być przechowywane tylko klucze lub teŜ dodatkowo całe rekordy danych. Rozmiar bloku zaleŜy od systemu operacyjnego ( 16kB, 32kB, …).

W jednym węźle moŜna przechowywać duŜo informacji.

Rozmiar w ęzła w B–drzewie zazwyczaj określa wielkość bloku danych na dysku, co z kolei limituje liczbę synów przypiętych do jednego węzła, która waha się między 50 ) 1000 (zaleŜy od rozmiaru węzła).

Z reguły korzeń B–drzewa jest stale trzymany w PAO.

Wówczas znalezienie dowolnego klucza(węzła) w drzewie o wysokości h wymaga maksimum h odwołań do operacji dyskowych ; liczba odwołań jest niezaleŜna od rzędu m drzewa.

Mechanizm przetwarzania informacji w strukturach B–drzewa:

JeŜeli poszukiwany obiekt x znajduje si ę w PAO to odwołanie do niego następuje w sposób tradycyjny, w przeciwnym razie zostaje wczytany blok z dysku do PAO.

JeŜeli bloki znajdujące się w PAO są aktualnie niepotrzebne, zostaną automatycznie usuni ęte, po uprzednim zapisaniu na dysk, jeŜeli dokonywane były zmiany w rekordach.

B–drzewo rzędu m to drzewo poszukiwań binarnych rzędu m o własnościach:

1. korzeń ma co najmniej dwa poddrzewa o ile nie jest liściem; 2. węzeł róŜny od korzenia zawiera p-1 kluczy i p wskaźników do poddrzew (p–synów) lub

maksimum 2p-1 kluczy i 2p wskaźników do poddrzew (2p–synów), gdzie m/2 ≤ p ≤ m;

3. klucze w kaŜdym węźle są uporządkowane rosnąco; 4. liście są na jednym poziomie, zawierają p-1 kluczy lub maksimum 2p-1.

B-drzewo jest zawsze przynajmniej w połowie zapełnione i z natury swojej niskie.

Wysokość B–drzewa spełnia zaleŜność: 12

1log ++≤

Np

h , N –całkowita liczba kluczy.

Dla dostatecznie duŜego rzędu m , wysokość B–drzewa jest mała, przy duŜej liczbie kluczy.

Liczba operacji dyskowych dla B–drzewa jest proporcjonalna do jego wysokości.

Struktura węzła B–drzewa:

n –liczba kluczy w węźle, leaf –zm. logiczna o wartości TRUE

jeŜeli węzeł jest liściem , K[m-1] –tablica zawierająca klucze, P[m] –tablica zawierająca wskaźniki

do węzłów (węzeł wewnętrzny).

11 22 33 44 55 p1 p2 p3 p4 p5 p6

B-drzeworzędu m=5

1020 7080

2 4 121416 6163 7274

k<10 10<k<20

m -1 kluczy

root[B] KaŜdy węzeł moŜe generować m potomków

60

key<60 key>60

Algorytmy i Struktury Danych - wykład 89 Instytut Aparatów Elektrycznych Algrm10 Dr J. Dokimuk

10.1. Szukanie w B–drzewie Metodyka poszukiwania klucza w B–drzewie jest podobna jak w BST (z definicji B–drzewa).

W kaŜdym węźle naleŜy dokonać wyboru poddrzewa, w którym będzie kontynuo-wane poszukiwanie.

jeŜeli nx jest aktualną liczbą kluczy w węźle x, to nx + 1 oznacza liczbę moŜliwych poddrzew w węźle.

W drzewie BST wybiera się tylko jedno poddrzewo z dwóch moŜliwych.

DANE: -wskaźnik do węzła x -poszukiwany klucz key, znajdujący się w poddrzewie o korzeniu w x.

WYNIK: para (y, i) zawierająca węzeł y oraz indeks i spełniający: keyi[y] = key lub NULL.

Rozpoczęcie poszukiwania w B–drzewie powoduje wywołanie SearchB(root(B), key)

Pętla while poszukuje w węźle x najmniejszej wartości i dla której key ≤ keyi[x]. JeŜeli nie znajdzie to i = n[x] + 1.

Następnie sprawdza się czy znaleziono poszukiwany klucz. JeŜeli znaleziono to funkcja zwraca parametry węzła,

w przeciwnym razie następuje negatywne zakończenie (gdy węzeł jest liściem) lub pobranie z dysku odpowiedniego syna i ponowne rekurencyjne szukanie.

Czas dostępu do stron dyskowych wynosi: O(h) = O(log p(N)). Czas przeglądania węzła wynosi O(p), gdyŜ n[x] < 2p.

Łączny czas pracy procesora wynosi O(p ⋅⋅⋅⋅h) = O(p ⋅⋅⋅⋅log p(N)) .

10.2. Tworzenie pustego B–drzewa

Utworzyć pusty węzeł–korze ń przy pomocy funkcji CreatB(B). Funkcja InitNode() przydziela pojedynczą stronę na dysku.

Funkcją Insert(key) wypełniać stopniowo drzewo.

Funkcja CreateB działa w czasie O(1) zarówno dla operacji dyskowych jak i CPU.

SearchB(x, key)

i 1 while i ≤ n[x] and key > keyi[x] do i i + 1

if i ≤ n[x] and key = keyi(x) then return (x, i)

if leaf[x] then return NULL else DiskRead(ci[x])

return SearchB(ci[x]), key)

CreateB(B)

x InitNode()

leaf(x) TRUE

n[x] 0

DiskWrite(x)

root[B] x

Page 47: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 90 Instytut Aparatów Elektrycznych Algrm10 Dr J. Dokimuk

10.3. Wstawianie klucza do B–drzewa

10.3.1. Liść nie jest pełny

Wstawianie klucza do B–drzewa rozpoczyna się od umieszczenia go w liściu.

10.3.2. Liść pełny

Tworzony jest nowy liść a klucze dzielone są po połowie między stary i nowy.

a) Nowy klucz wstawiany jest do wła ściwej połowy rozbitego li ścia. b) Ostatni klucz starego liścia przenoszony zostaje do ojca (jeŜeli jest wolne miejsce).

Podobny proces moŜe dotyczyć węzła wewnętrznego, zwiększając liczbę węzłów o jeden.

10.3.3. Ojciec jest pełny

Proces podziału węzła kontynuowany jest aŜ do korzenia.

JeŜeli w korzeniu nie ma miejsca to jest on rozbijany i tworzony jest nowy korzeń.

Następuje dodanie dwóch nowych węzłów do B–drzewa oraz wzrost jego wysokości.

Algorytmy i Struktury Danych - wykład 91 Instytut Aparatów Elektrycznych Algrm10 Dr J. Dokimuk

10.4. Usuwanie klucza z B–drzewa Usuwając naleŜy zwracać uwagę, aby kaŜdy węzeł był wypełniony co najmniej w połowie.

Implikuje to czasami konieczność sklejania węzłów.

10.4.1. Usuwanie klucza z liścia

10.4.1a Usunięcie klucza bez niedoboru JeŜeli po usunięciu klucza k, liść jest wypełniony co najmniej w połowie,

to naleŜy jedynie przesunąć w lewo klucze większe niŜ k .

10.4.1b Usunięcie klucza powoduje niedobór (niedobór to mniej niŜ połowa wypełnienia)

JeŜeli liść ma lewego lub prawego Brata o wypełnieniu większym niŜ połowa, to następuje:

a) przeniesienie klucza od Ojca do liścia,

b) od Brata do Ojca.

10.4.1c Usunięcie klucza w liściu powoduje niedobór, a liczba kluczy u jego Braci jest nie większa niŜ połowa moŜliwości to:

liść i Brat są sklejane ( Brat ulega zniszczeniu) . rozdzielający je klucz u Ojca jest przesuwany do liścia.

JeŜeli teraz u Ojca wystąpi niedobór to jest on taktowany jak liść i czynność powtarza się aŜ do usunięcie niedoboru poprzez scalanie.

Page 48: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 92 Instytut Aparatów Elektrycznych Algrm10 Dr J. Dokimuk

10.4.2. Usuwanie klucza z węzła wewnętrznego

MoŜe implikować konieczność przebudowy drzewa.

Usuwany klucz zastępuje się następnikiem lub poprzednikiem (musi znajdować się w liściu). Następnik/Poprzednik usuwany jest z liścia zgodnie ze wcześniejszą procedurą.

W przypadku usuwania klucza z węzła wewnętrznego, zachodzi czasami potrzeba rekonstrukcja całego drzewa, idąca w górę drzewa, i realizowana jak dla usuwania klucza z liścia (10.4.1b – c).

Często w B–drzewach, większość kluczy znajduje się w liściach a tym samym usuwanie realizowane jest w jednym przebiegu od korzenia do liścia, bez powracania w górę drzewa.

Usuwanie wymaga O(h) operacji dyskowych oraz O(p⋅h) = O(p⋅logp(N)) czasu procesora.

B–drzewo jest wypełnione, co najmniej w 50%, zaś prawie nigdy w 100% pojemności.

Średni poziom wypełnienia przy losowych wstawieniach i usunięciach wynosi ok. 70%.

Algorytmy i Struktury Danych - wykład 93 Instytut Aparatów Elektrycznych Algrm10 Dr J. Dokimuk

ANEX 10.1. ROZBICIE WĘZŁA w B-drzewie

Mechanizm rozbicia pełnego węzła y o 2p - 1 kluczach na dwa w ęzły p - 1 kluczach.

Rozbicie następuje względem klucza środkowego keyp[y], który zostaje przeniesiony do Ojca węzła y.

JeŜeli y nie ma ojca, tworzony jest nowy ojciec–korzeń.

Funkcja SplitB() realizuje proces rozbicia.

x –niepełny węzeł będący Ojcem,

i –wskaźnik i–tego syna węzła x,

y –węzeł dzielony, pełny syn węzła x (y = ci[x]). y = ci[x] y jest i-tym dzieckiem węzła x.

Czas działania procedury SplitB jest rzędu O(p) i determinowany jest działaniem 1-szej pętli for .

SplitB(x, i, y) // Podział węzła B–drzewa

z InitNode() leaf[z] leaf[y] n[z] p – 1 // węzeł z przejmuje p-1 najwiekszych kluczy od y

for j 1 to p – 1 do keyj[z] keyj+p[y]

// węzeł z przejmuje p synów od węzła y if not leaf[y] then for j 1 to p do cj[z] cj+k[y]

n[y] p – 1 // aktualizacja licznika synów węzła y

for j n[x] + 1 downto i+1 do cj+1[x] cj[x] ci+1[x] z

for j n[x] downto i do keyj+1[x] keyj[x] keyi[x] keyp[y]

// węzeł z zostaje synem, klucz środkowy z węzła y przeniesiony został do węzła-ojca x

n[x] n[x] + 1 //aktualizacja licznika kluczy węzła x

// zapis na dysku zmodyfkowanych stron WriteDisk(y); WriteDisk(z); WriteDisk(x)

Page 49: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 89 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

11. GRAFY Graf definiują wierzchołki V (vertices) i krawędzie E (edges), przy czym ścieŜka między dwoma wierzchołkami nie musi być jednoznaczna, zaś sposób rysowania jest nieistotny.

Graf G = (V, E) składa się ze skończonego zbioru wierzchołków oraz zbioru krawędzi. KaŜdą krawędź E stanowi para wierzchołków, zatem E jest zbiorem par (u, v), zaś (u),(v) stanowią elementy zbioru V.

Do oznaczania krawędzi E moŜna uŜywać zapisu (u, v) lub <u, v>. Mówimy, Ŝe wierzchołek v jest sąsiedni (adjacent) do wierzchołka u. Krawędź (u, v) jest incydentna (incident) względem wierzchołków u i v, czyli

krawędź łącząca dwa wierzchołki.

Graf skierowany(digraph): kaŜda krawędź jest uporz ądkowan ą parą wierzchołków.

Krawędź <u, v> jest wychodz ąca z wierzchołka u i wchodz ąca do wierzchołków v. Pętla własna to krawędź łączące wierzchołek z samym sobą.

Graf nieskierowany: kaŜda krawędź jest nieuporz ądkowan ą parą wierzchołków. Nie mogą wystąpić pętle, kaŜda krawędź zawiera dwa róŜne wierzchołki. Krawędzie incydentne z wierzchołkiem 33 : <33, 31> oraz <33, 52>.

Graf skierowany [a] ma parametry:

V = 1, 7, 10, 11, 13, 18 E = <11,13>, <13,18>, <18,1>, <1,7>, <7,1>, <7,7>,

<7,10>, <10,11>, <10,18> .

Graf nieskierowany [c] ma parametry: V = 31, 33, 52, 35, 58, 8 E = (31,35), (31,33), (33,52), (52,35), (35,58), (35,8).

Stopie ń (degree) wierzchołka w gafie nieskierowanym: liczba krawędzi incydentnych do niego. Stopie ń wierzchołka w gafie skierowanym: suma jego stopni wejściowych i wyjściowych .

stopień wejściowy/wyjściowy wierzchołka: liczba krawędzi wchodzących/wychodzących.

Graf rzadki: dla którego |V| >> |E|2 niewielka liczba krawędzi Graf gęsty: gdy |E| jest bliskie |V|2 większość par węzłów połączona jest krawędzią

Graf regularny : kaŜdy wierzchołek ma taki sam stopień. Graf planarny : tak przedstawiony na płaszczyźnie, Ŝe Ŝadne dwie krawędzie nie przecinają się.

k-graf : graf w którym stopień wierzchołka nie moŜe być większy niŜ k. Niezmiennik grafu: ciąg liczb, zaleŜny tylko od struktury grafu a nie od sposobu jego etykieto-

wania (np. liczba wierzchołków, liczba krawędzi). Liczba chromatyczna grafu : najmniejsza liczba kolorów potrzebna do kolorowania wierzchołków

grafu tak, by Ŝadne dwa przyległe wierzchołki nie były tego samego koloru.

ŚcieŜka długości k z wierzchołka u do wierzchołka ur jest ciągiem wierzchołków <v0, v1,…, vk> takich, Ŝe u = v0, u

r = vk oraz (vi-1, vi) ∈ E dla i = 1, 2,…, k.

Długość ścieŜki to liczba krawędzi ścieŜki.

Osiągalny węzeł: ur jest osiągalny z u po ścieŜce p, jeŜeli istnieje ścieŜka p z u do ur. ŚcieŜka prosta: jeŜeli wszystkie jej wierzchołki są róŜne.

PodścieŜka ścieŜki p jest ciągiem kolejnych wierzchołków <vi, vi+1,…, vj> dla 0 ≤ i≤ j≤ k.

ŚcieŜki bezpośrednie (direct paths) : <1,2> <1,3> <2,3> <3,4> <5,1>

ŚcieŜki pośrednie (indirect): <1,4> przez 3 lub przez 2 i 3 <2,4> przez 3 <5,2> przez 1 <5,3> przez 1 <5,4> przez 1 i 3 lub 1,2 i 3.

Algorytmy i Struktury Danych – wykład 90 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

Cykl: droga zaczynająca się i kończąca na tym samym wierzchołku.

Cykl w grafie skierowanym tworzy ścieŜka, jeŜeli v0 = vk oraz graf zawiera co najmniej jedną krawędź.

Cykl jest prosty, jeŜeli v1, v2,…, vk są róŜne. Pętla stanowi cykl o długości 1.

Cykl w grafie nieskierowanym tworzy ścieŜka jeŜeli v0 = vk oraz v1, v2,…, vk są róŜne i k ≥ 2.

Spójny graf skierowany: kaŜde dwa wierzchołki osiągalne są jeden z drugiego (R11.2b). Graf acykliczny: graf nie zawierający cykli. Acykliczny graf skierowany (directed acyclic graph -DAG)-nie posiada cyklu skierowanego.

Spójny graf nieskierowany: kaŜda para wierzchołków połączo-na jest ścieŜką (R11.3a).

KaŜdy wierzchołek dostępny jest z innego.

Pełny graf nieskierowany: kaŜda para wierzchołków połączo-na jest krawędzią.

Multigraf: graf zawierający zdublowane krawędzie, zwane takŜe wielokrotnymi.

Cykl Hamiltona

Cykl w grafie nieskierowanym, który przechodzi przez kaŜdy wierzchołek dokładnie raz.

Algorytm znajdujący cykl Hamiltona w grafie jest złoŜony czasowo, gdyŜ nie istnieje rozwiązanie problemu w czasie wielomianowym.

Cykl Eulera - problem mostów królewieckich Przez Królewiec przepływa rzeka, w rozwidleniu której znajduje się wyspa. Na rzece znajduje się 7 mostów. Szwajcarski matematyk L. Euler udowodnił, Ŝe nie jest moŜliwe przebycie wszystkich mostów dokładnie raz, a to z powodu nieparzystej liczby wejść na mosty.

Cykl Eulera: cykl, który zawiera kaŜdą krawędź dokładnie raz. Droga w cyklu Eulera moŜe przechodzić przez ten sam węzeł wielokrotnie .

Warunek istnienia cyklu Eulera: -graf jest spójny (istnieje droga łącząca kaŜdą parę wierzchołków).

-dla nieskierowanego liczba wychodzących krawędzi z kaŜdego wierzchołka musi być parzysta.

-dla skierowanego do kaŜdego wierzchołka musi wchodzić i wychodzić tyle samo krawędzi.

Cyklu Eulera w grafie moŜna wyznaczyć w czasie liniowym do E.

Graf de Bruijna Graf de Bruijna rzędu n posiada 2n wierzchołków.

Liczba krawędzi wynika z metodyki:

krawędź łącząca wierzchołek i z wierzchołkami (2i) mod 2n oraz wierzchołkami (2i + 1) mod 2n, dla kaŜdego i.

Graf stosowany w problemach wykorzystujących przesuwanie rejestru o stałą długość. Przesuwanie wszystkich bitów o jedną pozycję w lewo, usuwając bit skrajny lewy, i przypisując nowemu bitowi pojawiającemu się na prawej pozycji wartość 0 lub 1.

1

7

2 3

((((aaaa)))) ((((bbbb))))9

5 8 R11.

3

61

7

1111 2

4 3

(b)

1111 2

4 3

cykle R11.2

1

4

(c)(a)

Page 50: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 91 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

11.1. Implementacja Grafu

1. Macierzy sąsiedztwa: macierz kwadratowa o wymiarze określonym liczbą wierzchołków V*V. Element macierzy przyjmuje wartość 1 jeŜeli istnieje krawędź od wierzchołka w kolumnie do wierzchołków wierszu, w przeciwnym razie wartość 0.

ZłoŜoność pamięciowa O(V2).

2. List sąsiedztwa: przechowywane są listy sąsiadów kaŜdego wierzchołka, jako zwykłe listy wiązane, z uŜyciem tablic, bądź łańcuchów tekstowych.

Lista sąsiedztwa jest tablicą list, po jednej dla kaŜdego wierzchołka.

Dla grafu skierowanego G1 suma długości wszystkich list sąsiedztwa wynosi |E|, gdyŜ krawędź (u, v) jest reprezentowana przez wystąpienia v w tablicy zawierającej |N| list.

Dla gafu nieskierowanego macierz sąsiedztwa jest symetryczna względem przekątnej.

Dla grafu nieskierowanego G2 suma długości wszystkich list sąsiedztwa wynosi 2|E|, poniewaŜ wierzchołek u występuje na liście sąsiedztwa v i odwrotnie.

Dla grafu nieskierowanego kaŜda krawędź (u, v) reprezentowana jest dwukrotnie:

- przez wierzchołek v na liście Lista(u), - przez wierzchołek u na liście Lista(v).

Implementację grafu w postaci lisy sąsiedztwa zaleca się dla grafów rzadkich.

Macierz sąsiedztwa udostępnia informację o połączeniu między dwoma wierzchołkami w stałym czasie. Reprezentacja listowa przetwarza wszystkie krawędzie grafu w czasie proporcjonalnym do N + E.

Macierz incydencji Tablica o rozmiarach E*V, składająca się z E wierszy i V kolumn.

Jeśli krawędź wychodzi z danego wierzchołka w odpowiedniej kolumnie wstawiamy -1, jeśli do niego wchodzi to wstawiamy +1,

jeśli wierzchołek nie naleŜy do krawędzi wstawiamy 0, jeśli jest to pętla własna wstawiamy 2.

0 1 2 3 4 5 0 0 1 0 1 0 0 1 0 0 1 0 0 0 2 0 0 0 1 0 0 3 0 0 0 0 1 0 4 0 0 0 0 0 1 5 1 0 0 0 1 1

0: 1,3 1: 0,2,3,4 2: 1 3: 0,1,5 4: 1,5 5: 3,4

0 1 2 3 4 5 0 0 1 0 1 0 0 1 1 0 1 1 1 0 2 0 1 0 0 0 0 3 1 1 0 0 0 1 4 0 1 0 0 0 1 5 0 0 0 1 1 0

0 1 2 3 4 5 0-1 -1 1 0 0 0 0 0-3 -1 0 0 1 0 0 0-5 1 0 0 0 0 -1 1-2 0 -1 1 0 0 0 2-3 0 0 -1 1 0 0 3-4 0 0 0 -1 1 0 4-5 0 0 0 0 -1 1 5-4 0 0 0 0 1 -1 5-5 0 0 0 0 0 2

Algorytmy i Struktury Danych – wykład 92 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

Generowanie grafów losowych Modele grafów losowych:

Wspólnymi danymi wejściowymi jest V – liczba wierzchołków.

G(V, p) : -p prawdopodobieństwo krawędziowe, określone dla kaŜdej krawędzi; określa prawdopodobieństwo, Ŝe dana krawędź znajdzie się w grafie

G(V, k) : -ze zbioru wszystkich moŜliwych krawędzi są losowane kolejne krawędzie, z jednakowym prawdopodobieństwem.

G(V, f) : - f oznacza maksymalny (nie przekraczalny) stopień wierzchołka.

Generowanie grafów obejmuje szereg czynności:

• wybór odpowiedniego generatora.

• wygenerowany graf jest weryfikowany, czy spełnia załoŜenia wybranego modelu, grafy które ich nie spełniają są odrzucane.

• grafy spełniające załoŜenia modelu sprawdza się dodatkowo czy są róŜne, tzn. czy nie ma wśród nich grafów izomorficznych.

Graf izomorficzny: Dwa grafy G1(V1, E1) i G2(V2, E2) są izomorficzne, jeŜeli moŜna przenume-

rować wierzchołki grafu G1 tak, aby stały się wierzchołkami grafu G2, zachowując odpowiednie krawędzie w G1 i G2.

Przykład implementacji tablicowej

#define MAXN 10 struct Graph int TabG[MAXN][MAXN]; ;

Graph NewG(Graph g); Graph JoinG(Graph g, int a, int b); Graph RemoveG(Graph g, int a, int b); int AdjacentG(Graph g, int a, int b);

Graph newG(Graph g) // Initialise and return graph g int x, y; for(x=0; x < MAXN; x++) for(y=0; y < MAXN; y++) g.TabG[x][y] = 0; return g;

Graph JoinG(Graph g, int a, int b) // add edge between nodes a and b

g.TabG[a][b] = 1; return g; Graph RemoveG(Graph g, int a, int b) // remove edge between nodes a and b

g.TabG[a][b] = 0; return g; int AdjacentG(Graph g, int a, int b) // test if edge exists between nodes a and b

return g.TabG[a][b];

Page 51: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 93 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

11.2. Przeszukiwanie wszerz BFS (Breadth First Search) WyróŜnia się wierzchołek początkowy zwany źródłem.

Następnie bada się systematyczne krawędzie grafu w celu odwiedzenia kaŜdego wierzchołka , który jest osiągalny ze źródła.

Wierzchołki w odległości k od źródła odwiedzane są przed wierzchołkami w odległości k+1.

Najpierw dociera się do wszystkich wierzchołków leŜących o jedną krawędź od wierzchołka źródłowego, następnie do wszystkich wierzchołków odległych od niego o dwie krawędzie i tak dalej.

JeŜeli w trakcie przeszukiwania grafu pojawi się węzeł, z którego wychodzi więcej niŜ jedna krawędź, wówczas wybiera się jedną z nich, a pozostałe zostawia się na później.

Krawędzie do późniejszego odwiedzania pamiętane są dzięki uŜyciu kolejki.

Obliczane są odległości od źródła do wszystkich osiągalnych wierzchołków.

Algorytm BFS tworzy drzewo przeszukiwania wszerz o korzeniu w źródle, zawierające wszystkie osiągalne wierzchołki. Dla kaŜdego wierzchołka v osiągalnego ze źródła (S), ścieŜka w drzewie BFS od S do v odpowiada najkrótszej ścieŜce od S do v w grafie G.

Niech Graf będzie reprezentowany przez listy sąsiedztwa pamiętane w tablicy G[u]. Tablica G umoŜliwia dostęp do wszystkich wierzchołków sąsiadujących z u.

Na początku wszystkie wierzchołki są białe, potem zmieniają kolor na szary lub czarny.

Szary: wierzchołek odwiedzony, którego lista sąsiedztwa nie została w całości przejrzana.

W kolejce Q, pamiętane są kolorowane na szaro, odwiedzone wierzchołki.

Czarny: wierzchołek, którego lista sąsiedztwa została w całości odwiedzona. JeŜeli wierzchołek u jest czarny, to wierzchołek v jest albo szary albo czarny.

Wierzchołki szare mogą mieć białych sąsiadów, tworzą granicę między odwiedzanymi i nieodwiedzanymi.

W kolejce umieszcza się źródło S. Z kolejki pobiera się wierzchołek S, odwiedza się go, i umieszcza w kolejce wierzchołki znajdujące się na jego liście sąsiedztwa czyli: e, a.

Z kolejki pobiera się wierzchołek e, odwiedza się go, i umieszcza w kolejce wierzchołki z jego listy sąsiedztwa czyli: b, f (wierzchołek s nie jest pobierany, gdyŜ był juŜ odwiedzony).

Z kolejki pobiera się wierzchołek a, odwiedza się go, i umieszcza w kolejce wierzchołki z jego listy sąsiedztwa czyli: d. itd...

Q=0

R11.9

Algorytmy i Struktury Danych – wykład 94 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

D[u] -odległości (liczba krawędzi) wierzchołka u od źródła S C[u] -kolor wierzchołka. P[u] -zawiera poprzednika (ojca) wierzchołka u; drzewo BFS.

JeŜeli u nie ma poprzednika to P[u] = NULL. Główna pętla algorytmu BFS while wykonywana jest

tak długo jak istnieją szare wierzchołki.

1. Na początku pobierany jest szary wierzchołek u z kolejki Q. 2. W pętli for analizowany jest kaŜdy wierzchołek v z listy

sąsiedztwa u. 2a. JeŜeli v jest biały to nie był odwiedzany, zostaje odwie-

dzony i pokolorowany na szaro zaś D[v] = D[u] + 1.

2b. Wierzchołek u jest pamiętany jako ojciec wierzchołka v (w wektorze P), sam v jest umieszczany w kolejce Q.

Uwaga: wierzchołek v wstawiany jest do kolejki raz, gdyŜ jest jednocześnie kolorowany, zaś ciało instrukcji if wykony-wane jest tylko dla białych wierzchołków.

3. Po zbadaniu wszystkich wierzchołków z listy sąsiedztwa u, sam u jest usuwany z kolejki i kolorowany na czarno.

Uwaga: wierzchołek z początku kolejki jest wierzchołkiem u, którego lista sąsiedztwa jest właśnie przeglądana.

Wynikiem działania algorytmu BFS jest drzewo, które w stadium początkowym składa się z korzenia (wierzchołek źródłowy). Drzewo jest rozbudowywane w trakcie przechodzenia grafu.

Wchodząc od wierzchołka u, w liście sąsiedztwa szukany jest nie odwiedzony (biały) wierzchołek v; wierzchołek ten i krawędź (u, v) dodawane są do drzewa.

Wektor P reprezentuje drzewo BFS, w którym kaŜdy wierzchołek wskazuje na swojego poprzednika (z wyjątkiem korzenia).

Zawarte w nim informacje pozwalają na znalezienie najkrótszej ścieŜki z wierzchołka źródłowego s do docelowego v wg:

ścieŜka z s do P(v) + krawędź (P(v), v).

c P[c] P[P[c]] P[P[P[c]]] c b e s

Czas działania BFS

Wierzchołki kolorowane są na BIAŁO tylko w trakcie inicjalizacji, która kosztuje O(V). KaŜdy wierzchołek wstawiany jest do kolejki i wyjmowany co najwyŜej raz (instrukcja if ).

Operacje wstawiania do kolejki i usuwania są klasy O(1). Łączny czas wykonywania operacji na kolejce wynosi O(V).

Lista sąsiedztwa kaŜdego wierzchołka przeglądana jest co najwyŜej raz, gdyŜ przeglądana jest tylko wtedy, gdy wierzchołek zostanie usunięty z kolejki. Sumaryczny czas przeglądania list sąsiedztwa wynosi O(E). Łączny czas działania procedury BFS wynosi O(V + E).

Najkrótsze ścieŜki łączące kaŜdą parę węzłów grafu NaleŜy wykonać algorytm BFS dla kaŜdego wierzchołka grafu. Dla kaŜdego wierzchołka naleŜy pamiętać długości ścieŜek i reprezentację drzewa.

BFS(G, s) // Breadth First Search for u ∈∈∈∈ G do // inicjalizacja

C[u] BIAŁY D[u]0 P[u] NULL

C[s]Szary; // [1] P[s] NULL; // korzeń nie ma Ojca Qs //inicjalizacja kolejki

while (!Empty(Q)) do

u head[Q] // pobieranie

for v ∈ G[u] do if C[v] = BIAŁY then

C[v] Szary D[v]D[u] + 1 P[v] u Enq(Q, v)

Deq(Q); C[u] Czarny

MinPathG(s, v) if P[v] = NULL then Print(" brak scieŜki s-v ") else if v = s then Print(s) else MinPathG(s, P[v]) Print (v)

v s e a b f d c g P(v) null s s e e a b f

u a: d,s s: e,a b: c,f,e c: b,g d: a e: b,f,s f: g,b,e g: f,c

u s e a b f d c g 0 1

1 D[e]

+1 De +1

Da +1

Db +1

Df +1

D(u)

0 1 1 2 2 2 3 3

s kolejka Q e a a b f b f d f d c d c g c g g

Page 52: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 95 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

11.3. Przeszukiwanie w głąb DFS (Depth First Search)

Wybieramy dowolny wierzchołek u, z którego przechodzi się do wierzchołka v. Bada się rekurencyjnie wszystkich nie zbadanych sąsiadów wierzchołka v. Po zbadaniu sąsiadów v, powraca się do wierzchołka, z którego v został odwiedzony (u).

Kontynuowane jest przeszukiwanie z wierzchołka startowego u we wszystkich kierunkach .

JeŜeli wierzchołek v osiągany jest po raz pierwszy z u to (u, v) wstawiamy do drzewa Tree. W przeciwnym razie (u, v) wstawiana jest do zbioru krawędzi powrotnych.

Wierzchołek nie odwiedzony staje się nowym źródłem i proces powtarza się.

Startujemy oznaczając wszystkie wierzchołki jako nowe.

Wykonujemy VisitDFS1(u1) i z Listy wybieramy v = u2. Wierzchołek u2 jest nowy, dodajemy (u1, u2) do Tree.

Wywołujemy VisitDFS1(u2) i z Listy wybieramy v = u3. Wierzchołek u3 jest nowy, dodajemy (u2, u3) do Tree.

Wywołujemy VisitDFS1(u3) i z Listy wybieramy v = u3. Wierzchołek u1 i u2 był odwiedzony (stary), zatem powrót.

Powrót do VisitDFS1(u2), v = u4 i krawędź (u2, u4) dodawana jest do Tree. Powrót do VisitDFS1(u1) i poszukiwanie rozpoczyna w nowym kierunku v = u5. Do pamiętania węzłów, które mają zostać odwiedzone algorytm rekurencyjny wykorzystuje stos programu.

Krawędź powrotna: krawędź grafu nie zawarta w lesie DFS, mogą jedynie prowadzić od wierzchołka do jednego z przodków w jego drzewie DFS. Pętla traktowana jest jak krawędź powrotna.

Kolejność przeszukiwania wierzchołków

zaleŜy nie tylko od samego grafu ale

takŜe od kolejności umieszczenia

sąsiadów na listach sąsiedztwa.

Podgraf poprzedników nazywany jest lasem przeszukiwań w głąb, złoŜonym z jednego lub kilku drzew przeszukiwań w głąb, gdyŜ przeszukiwanie moŜe być wykonywane z kilku źródeł

Dla grafu skierowanego na wierzchołku u wybieramy tylko krawędzie skierowane od u. Po wyczerpaniu tych krawędzi, powracamy do u mimo, Ŝe mogą istnieć inne krawędzie skierowane do v, które nie są jeszcze przeszukane.

DFS1(G) Tree NULL for u ∈ V do u ”nowy” for u ∈ V do if u = ”nowy” then

VisitDFS1(u)

VisitDFS1(u)

u ”stary” for v∈ Listy[u] do if v = ”nowy” then

Tree (u, v)

VisitDFS1(v)

Drzewo rozpinająceu1

u5

u6

u2

u3

u4 u1(1)

u2

u3 u6u4

u5

(2) (3)(1)

(2)

(3)

Lista u1: u2,u3,u4,u5,u6 u2: u3,u4,u1 u3: u1,u2 u4: u2,u1 u5: u6,u1 u6: u5,u1

Algorytmy i Struktury Danych – wykład 96 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

Algorytm kolorujący wierzchołki na: -Biało w stanie początkowym; -Szaro, gdy są odwiedzane po raz pierwszy; -CzarnCzarnCzarnCzarnoooo gdy lista sąsiedztwa zostanie całkowicie zbadana.

Daje to gwarantuję, Ŝe kaŜdy wierzchołek będzie naleŜał do dokładnie jednego drzewa DFS .

Funkcja DFS2(G) w pierwszej pętli for koloruje wierzchołki na Biało oraz inicjuje zmienne.

Druga pętla for bada kaŜdy wierzchołek i w przypadku wykrycia Białego odwiedza go funkcją VisitDFS2(u).

Wywołanie funkcji VisitDFS2(u) nadaje wierzchołkowi u (nieprzetworzony) statut korzenia w nowym drzewie w lesie przeszukiwań w głąb (wierzchołek u staje się nowym źródłem).

Funkcja VisitDFS2(u) w pętli for bada kaŜdy wierzchołek v sąsiadujący z u; jeŜeli jest Biały to zostaje odwiedzony rekurencyjnie. Po zbadaniu kaŜdej krawędzi wychodzącej z u, sam u jest kolorowany na Czarno .

W wektorze F[u] zapisywany jest czas przetworzenia wierzchołka u.

Zmienna krok jest globalną zmienną całkowitą.

P[v] –pamięta poprzednika wierzchołka nie odwiedzonego v, naleŜącego do listy sąsiedztwa odwiedzonego u. Podgraf poprzedników: GP = ( N, (T(v), v) )

Etykiety czasowe -dodatkowe informacje udostępniane przez algorytm DFS: D[u] -numer kroku obliczeń od momentu gdy u został odwiedzony po raz pierwszy . F[u] -numer kroku zakończenia badania listy sąsiedztwa wierzchołka u ( u na czarno).

Oznaczenia wewnątrz wierzchołków: czas odwiedzenia/czas przetworzenia Czasy odwiedzania i przetworzenia mają strukturę nawiasową w przeszukiwaniu w głąb.

Oznaczmy za pomocą nawiasu otwierającego „(u" chwilę odwiedzenia wierzchołka u a za pomocą nawiasu zamykającego chwilę jego przetworzenia „u)".

Historia odwiedzania i przetwarzania wierzchołków jest poprawnie zbudowanym wyra Ŝeniem nawiasowym - nawiasy są właściwie zagnieŜdŜone.

DFS2(G) for u ∈ V do

C[u] Biały T[u] NULL

krok 0

for u ∈ V do if C[u] = Bialy then VisitDFS2( u)

VisitDFS2(u) C[u] Szary D[u] krok krok + 1 for v ∈ Listy[u] do // badanie z listy sąsiadów u

if C[v] = Biały then P[v] u VisitDFS2(v)

C[u] Czarny F[u] krok krok + 1

G(u) Lista a: b, d b: c c: d d: b e: f, c f: f

v b c d f P(v) a b c e u=a u=b u=c u1=e

Page 53: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 97 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

11.4. Grafy z wagami Graf, w którym do kaŜdej krawędzi przywiązana jest waga, będąca liczbą całkowitą lub rzeczywistą.

Wagi reprezentują zazwyczaj obiekty o określonych cechach fizycznych jak długość, czas przejazdu, koszt i inne, które narastają

liniowo wzdłuŜ ścieŜki grafu, a które staramy się minimalizować. Dla prezentacji grafu wagowego w postaci list sąsiedztwa,

elementy listy stają się strukturami, o co najmniej dwóch polach zawierających numer węzła i wagę krawędzi.

Problem najkrótszej ścieŜki Dany jest waŜony graf skierowany G = (V, E) z funkcją wagową w: ER, przyporządkowującą krawędziom wagi. Dany jest wyró Ŝniony wierzchołek s ∈∈∈∈ V, zwany źródłem.

Dla dowolnego wierzchołka v ∈∈∈∈ V naleŜy znaleźć najkrótszą ścieŜkę z s do v.

Waga ścieŜki p = < v0, v1, ..., vk > -suma wag tworzących ją krawędzi: ∑=

−=k

iii vvwpw

11 ),()(

Wagę najkrótszej ścieŜki z wierzchołka u do wierzchołka v określa zaleŜność:

∞→=

razieprzeciwnymw

vdouzsciezkaistniejejezelivupwminu

p :)()(δ

Najkrótsza ścieŜka z wierzchołka u do v: -kaŜda ścieŜka p z u do v, dla której w(p) = δ(u, v). Warianty problemu najkrótszej ścieŜki dla grafu spójnego:

1. Dla dwóch dowolnych węzłów A i B znaleźć najkrótsza ścieŜkę od A do B. 2. Dla węzła A znaleźć najkrótsze ścieŜki do pozostałych węzłów w grafie. 3. Pomiędzy kaŜdą parą węzłów w grafie znaleźć najkrótszą ścieŜkę.

Mogą pojawić się krawędzie z ujemnymi wagami.

JeŜeli ze źródła s nie jest osiągalny Ŝaden cykl o ujemnej wadze, to waga najkrótszej ścieŜki δ(s, v) jest dobrze zdefiniowana, nawet jeŜeli przyjmuje ujemne wartości.

JeŜeli ze źródła s osiągalny jest cykl o ujemnej wadze to wagi najkrótszych ścieŜek nie są dobrze zdefiniowane. śadna ścieŜka ze źródła s do wierzchołka, przechodząc przez ujemny cykl nie jest najkrótsza (moŜna znaleźć ścieŜkę krótszą, rozszerzając ją o ujemny cykl).

Problem najkrótszej ścieŜki z jednym źródłem rozwiązuje algorytm Dijkstry dla wag nieujemnych , bądź algorytm Bellmama–Forda gdy wagi są ujemne.

Problem minimalnego drzewa rozpinającego w grafie spójnym

Konstrukcja drzewa T łączącego wszystkie wierzchołki, którego łączna waga jest najmniejsza. Łączenie końcówek układów elektronicznych, minimalizujące długość ścieŜek przewodzących. Problem modeluje spójny graf nieskierowany G = (V, E), w którym V jest zbiorem ko ńcówek ,

zaś E jest zbiorem mo Ŝliwych poł ączeń między parami końcówek. Do kaŜdej krawędzi (u, v) przypisana jest waga w(u, v), określająca długość potrzebnego

przewodu do połączenia wierzchołków u i v.

Problem rozwiązuje znalezienie acyklicznego podzbioru T ⊆⊆⊆⊆ E, który łączy wszystkie wierzchołki i którego łączna waga określona zaleŜnością: ∑

=Tvu

vuwTw),(

),()( jest najmniejsza.

Acykliczność podzbioru T łączącego wszystkie wierzchołki grafu sprawia, Ŝe T jest drzewem rozpinającym graf G . Przedstawiony problem rozwiązują algorytmy Kruskala i Prima .

Są to algorytmy realizujące strategię zachłanną, podejmujące decyzje najlepsze w danej chwili. Wybrane strategie zachłanne pozwalają otrzymać drzewo o minimalnej wadze.

Algorytmy i Struktury Danych – wykład 98 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

11.4.1. Algorytm Kruskala

Rozwiązuje problem minimalnego drzewa rozpinającego dla waŜonego grafu nieskierowanego.

Tworzy się rozłączne podzbiory zbioru V (kaŜdy podzbiór zawiera jeden węzeł). Krawędzie badane są zgodnie z niemalejącą wartością ich wag (wartości równe dowolnie). Tworzony jest nowy zbiór F wierzchołków i krawędzi.

Krawędź jest dodawana do F jeŜeli łączy dwa wierzchołki z róŜnych zbiorów rozłącznych, a podzbiory zostają scalone w jeden zbiór.

Krawędź istniej ąca juŜ w nowym podzbiorze F jest opuszczana. Proces kontynuuje się do momentu scalenia wszystkich podzbiorów w jeden zbiór.

ZłoŜoność czasowa (instrukcja porównania) 1. ZłoŜoność czasowa sortowania krawędzi: W(E) ∈ O(E lg(E)).

2. Czas realizacji pętli while to głównie działania na zbiorach rozłącznych; W(E) ∈ O(E lg(E)). W najgorszym przypadku zostaje rozpatrzona kaŜda krawędź, co oznacza wykonanie E przebiegów pętli, która zawiera stałą liczbę wywołań stosownych podprogramów.

3. Czas inicjalizacji V zbiorów rozłącznych moŜe określać zaleŜność: T(V) ∈ O(V) PoniewaŜ E > V-1, sortowanie i wykonywanie operacji na zbiorach rozłącznych jest dominującą

składową czasu inicjalizacji, co oznacza, Ŝe W(E, V) ∈ O(E lg(E)) W najgorszym przypadku kaŜdy węzeł moŜe być połączony z kaŜdym innym: E = 0.5*(V(V-1)) ∈ O(V2). Zatem najgorszy przypadek: W(E, V) ∈ O(V2 lg(V2)) = O(V2 2 lg(V)) = O(V2 lg(V)).

w Krawędź Podzbiory 1 1 (w6, w7) w1, w2, w3, w4, w5,w6, w7, w8, w9 2 2 (w2, w9) w1, w2, w9, w3, w4, w5,w6, w7, w8 3 2 (w5, w6) w1, w2, w9, w3, w4,w5, w6, w7, w8 4 4 (w2, w5) w1, w2, w5, w6, w7w9, w3, w4, w8, 5 4 (w1, w8) w1, w8, w2, w5, w6, w7,w9, w3, w4 6 7 (w2, w3) w1, w8, w2, w3, w5, w6, w7, w9, w4 7 7 (w7, w9) krawędź istnieje - nie jest wstawiana 8 8 (w7, w8) w1, w2, w3, w5, w6, w7, w8, w9, w4 9 8 (w1, w2) krawędź istnieje - nie jest wstawiana

10 9 (w3, w4) w1, w2, w3, w4, w5, w6, w7, w8, w9 11 9 (w6, w9) K O N I E C 12 10 (w4, w5) 13 11 (w1, w7) 14 14 (w3, w5)

F = 0; // inicjalizacja pustego zbioru krawędzi Utwórz V rozłącznych podzbiorów węzłów Sortuj(krawędzie E w porządku niemalejącym) while (liczba krawędzi w F < V - 1)

Wybierz kolejną krawędź if (krawędź łączy 2 węzły ze zbiorów rozłącznych)

Scal (podzbiory) Dodaj (krawędź do F)

F = 0; Wstaw krawędzie do Kolejki priorytetowej

while (liczba krawędzi w F < V - 1) Pobierz krawędź z Kolejki if (krawędź łączy 2 węzły ze zbiorów rozłącznych)

Scal (podzbiory) Dodaj (krawędź do F)

Page 54: algorytmy i struktury danych

Algorytmy i Struktury Danych – wykład 99 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

11.4.2. Algorytm Dijkstry

Dla węzła S znaleźć najkrótsze ścieŜki od S do pozostałych węzłów grafu skierowanego, gdy wagi

wszystkich krawędzi są nieujemne. Algorytm tworzy skierowane drzewo najkrótszych ścieŜek (Shortest Paths Tree – SPT) z korzeniem w źródle S, tak Ŝe kaŜda ścieŜka w drzewie idąca ze źródła jest najkrótsza.

MoŜe być wiele ścieŜek tej samej długości łączącą daną parę węzłów.

Zwalnianie (relaxation) Na początku znane są tylko krawędzie grafu i ich wagi. Przetwarzając uzyskujemy informacje o aktualnie najkrótszych ścieŜkach w grafie. Algorytmy uaktualniają te informacje stopniowo, tworząc nowe wnioski o najkrótszych ścieŜkach na podstawie dotychczas uzyskanych informacji.

W kaŜdym kroku testują moŜliwość znalezienia ścieŜki krótszej od dotychczas znanej. Relaksacja krawędzi: sprawdzanie czy przesuwając się wzdłuŜ innej krawędzi (u, v), znajdziesz nową,

krótszą od dotychczas najkrótszej ścieŜki do węzła v. Pamiętamy najkrótsze znane ścieŜki ze źródła s do wszystkich węzłów (w chwili ti) i pytamy, czy w chwili następnej krawędź (u, v) da jeszcze krótszą ścieŜkę do v. JeŜeli nie to pomijamy ją.

JeŜeli tak to bierzemy krawędź (u, v), uaktualniamy dane, gdyŜ najlepsza droga z s do v wiedzie teraz przez węzeł u.

Najpierw odwiedzamy węzeł najbliŜszy węzła S, potem 2-gi w kolejności i tak dalej (v1, v2,...). Węzeł x1 najbliŜszy S musi być sąsiadem. Węzeł v2 (2-gi) musi być sąsiadem S lub v1.

Kiedy węzeł został juŜ odwiedzony, znamy długość najkrótszej ścieŜki od niego do S. Obwódka -zbiór nie odwiedzonych krawędzi (węzłów), sąsiadujących z węzłami odwiedzonymi. Następny do odwiedzenia wybieramy węzeł z obwódki, którego ścieŜka do S jest najkrótsza.

Grube krawędzie to krawędzie drzewa SPT, zaś szare są krawędziami obwódki. Etap 1: dodanie W1 do drzewa, zaś opuszczających go krawędzi (1, 2) i (1, 6) do obwódki. Etap 2: przesuwamy krawędź (1, 6) z obwódki do drzewa i sprawdzamy krawędzie z węzła W6.

Krawędź (6, 5) dodajemy do obwódki. Krawędź (6, 2) odrzucamy, poniewaŜ nie jest częścią ścieŜki z W1 do W2 krótszej niŜ znana w obwódce ścieŜka (1, 2) (odrzucenie w procesie relaksacji).

Etap 3: przenosimy (1,2) z obwódki do drzewa, dodajemy (2,3) do obwódki i odrzucamy (2,5). Etap 4: przenosimy (6,5) z obwódki do drzewa, dodajemy (5,4) do obwódki.

Zamieniamy (2, 3) na (5, 3), poniewaŜ 1653 jest krótszą ścieŜką niŜ 123. Etap 5, 6: przenosimy z obwódki do drzewa krawędzie (5, 3), a potem (5, 4).

Algorytmy i Struktury Danych – wykład 100 Instytut Aparatów Elektrycznych Algrm11 Dr J. Dokimuk

11.5. Przykłady implementacji

// Przeszukiwanie w gł ąb - DFS;

// "nowy" = 0; "stary" = 1; #include<iostream.h>

const int V = 10; char G[V] = 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J' ; char *ListG[V] = "DBHE", "ACE", "BJG", "HA", "AHIFB",

"E", "CJ", "DAE", "HEJ", "ICG" ; int Stary[V]; // zerowanie (nowy) wszystkich węzłów

inline int Ind(char zn) return zn – 'A'

void Visit(char u) char *jL; Stary[Ind(u)] = 1; cout << u;

for (jL = ListG[Ind(u)]; *jL; jL++)

if (!(Stary[Ind(*jL)])) Visit(*jL);

void DFS() for (int i=0; i < V; i++) //start od G[0] czyli A

if (!(Stary[Ind(G[i])])) Visit(G[i]);

int main() // Graf1DFS DFS(); return 0;

0 A: DBHE

1 B: ACE 2 C: BJG 3 D: HA

4 E: AHIFB

5 F: E 6 G: CJ 7 H: DAE

8 I: HEJ

9 J: ICG

ADHEIJCBGF

Page 55: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 101 Instytut Aparatów Elektrycznych Algrm12 Dr J. Dokimuk

12. KOMPRESJA DANYCH

Pewne litery alfabetu występują w tekstach częściej niŜ inne.

Badania pokazały, Ŝe w języku angielskim najczęściej występują litery: e, t; najrzadziej: q, j. Zgodnie z alfabetem Morse’a: e ≡≡≡≡ . t ≡≡≡≡ - q ≡≡≡≡ --.- j ≡≡≡≡ ---

Morse uzyskał zmniejszenie długości przesyłanych tekstów poprzez przypisanie częściej występującym znakom krótszych ciągów kodowych.

Cel kompresji: zmniejszyć rozmiar bloku danych bez utraty jego zawartości.

Komunikat zawiera tym więcej informacji, im mniejsze jest prawdopodobieństwo jego wystąpienia jest podstawowym załoŜeniem teorii informacji , którą stworzył Claude Shannon w 1948-1949. Niech ΩΩΩΩ będzie skończonym zbiorem wszystkich moŜliwych wysłanych komunikatów.

Odbiorca nie wie, który z moŜliwych komunikatów ze zbioru ΩΩΩΩ = k1, k2, ... , kn otrzyma. Komunikaty o zdarzeniu pewnym (których zajścia jest pewien) nie przekaŜą odbiorcy Ŝadnej informacji, gdyŜ nie zmienią stanu jego wiedzy.

Komunikat, którego prawdopodobieństwo wystąpienia wynosi p zawiera I(p) jednostek informacji.

Entropia H źródła informacji: średnia (waŜona) ilość informacji w komunikatach ze źródła, które nadaje n róŜnych komuni-katów z prawdopodobieństwem wystąpienia pk (k=1,2,...,n).

Słowo kodowe: kod komunikatu. Długo ść słowa kodowego : liczba jego elementów (znaków).

Średnia długość Lave słowa kodowego (waŜona długość ścieŜki): gdzie Ni długości słów kodowych komunikatów źródła.

Algorytm Run Length Encoding - RLE

Efektywny przy kompresji bloków danych, w których powtarza się wiele znaków obok siebie. Niech blok 20 znaków ma postać: AABBBBBCCCBBDDDDDDDD

Blok moŜna skompresować do 13 znaków: AA#5B#3CBB#8D nX oznacza n-krotne wystąpienie znaku X, # jest separatorem pomiędzy danymi niekodowanymi i kodowanymi.

Metoda słownikowa. Kolejno analizowane ciągi symboli koduje się za pomocą pojedynczych odwołań do słownika, w którym przechowywane są często powtarzające się sekwencje znaków. W strumieniowych metodach słownikowych budowane są dwa słowniki:

-jeden wykorzystywany jest dla kodowania często występujących wzorców, -drugi dla pozostałych. Zysk metody zaleŜy od tego, jak duŜo ciągów kodowych

zostanie rozpoznanych jako często występujące.

Algorytm Huffmana BACKABWACKACACKA = 16 znaków ⋅⋅⋅⋅ 8 bitów = 128 bitów

Po kompresji: 10110111000101110100111000110111000 = 35 bitów

Dynamiczny algorytm Huffmana stosowany jest do dekompresji strumienia danych. W miarę napływu informacji, co zadaną liczbę znaków, na budowane drzewo binarne nanoszone są poprawki.

znak Liczba znaków

kod

A 6 0 C 4 11 K 3 100 B 2 1011 W 1 1010

Rozpoczyna się od zebrania statystyk występowania poszczególnych elementów w zbiorze. Przypisuje się kody o zmiennej liczbie bitów (krótszy, gdy znak występuje częściej) poszczególnym elementom na podstawie częstości ich występowania w zbiorze.

Budowane jest drzewo binarne, w którym elementy zbioru umieszczane są w liściach. Kod kaŜdego znaku jest wynikiem przejścia od korzenia drzewa do odpowiedniego liścia.

( )∑=

−=n

kkk pppH

12log)(

( )ppI 2log)( −=

∑=

=n

kkkave NpL

1

p1

6

16p

2

4

16p

3

3

16p

4

2

16p

5

1

16n 5 H p n,( ) 2.11=N

11 N

22 N

33 N

44 N

54

I p1

1.42= I p2

2= I p3

2.42= I p4

3= I p5

4= L ave p n, N,( ) 2.19=

Algorytmy i Struktury Danych - wykład 102 Instytut Aparatów Elektrycznych Algrm12 Dr J. Dokimuk

12.1. Metoda Huffmana RozwaŜmy plik o długości 100000 znaków, zawierający wyłącznie treści składające się z alfabetu ΩΩΩΩ = a, b, c, d, e, f. Częstotliwość wystąpień poszczególnych znaków (w tys.) i przyjęte słowa

kodowe dla kaŜdego znaku zawiera tabela. Kod o zmiennej długości gwarantuje, Ŝe częściej występujące znaki będą

kodowane za pomocą krótszych słów kodowych niŜ znaki o mniejszej częstości występowania.

Po dokonaniu kompresji plik o długości 100000 znaków czyli 800 000 bitów zajmować będzie (45⋅1 + (13 + 12 + 16)⋅3 + (9 + 5 ) ⋅4) ⋅1000 = 224 000 bitów.

Kod prefiksowy: kod kaŜdego znaku nie jest prefiksem innego znaku. Pozwala uzyskać maksymalny stopień kompresji.

Kodowanie prefiksowe wymaga tylko skonkatenowania kodów kolejnych znaków w pliku. Na przykład plik zawierający znaki abcdef reprezentowany będzie za pomocą ciągu bitów: 0•101•100•111•1101•1100 = 010110011111011100, znak „•" oznacza operację konkatenacji.

Dekodowanie dla kodów prefiksowych jest stosunkowo proste. Pierwszy kod znaku jest wyznaczony jednoznacznie, bo Ŝaden kod nie jest prefiksem innego. NaleŜy: -wyznaczyć pierwszy kod w pliku, -przetłumaczyć go na znak,

-usunąć z zakodowanego pliku; -powtórzyć procedurę dekodującą dla reszty pliku.

Ciąg 1111101101010011011100 rozkłada się jednoznacznie na 111-1101-101-0-100-1101-1100, co daje słowo debacef .

Kod prefiksowy reprezentowany jest przez regularne drzewo binarne, którego liście odpowiadają zakodowanym znakom. KaŜdy węzeł wewnętrzny ma dwóch synów.

KaŜdy liść etykietowany jest znakiem i liczbą wystąpień znaku. KaŜdy węzeł wewnętrzny etykietowany jest sumą wag liści w

poddrzewie, którego jest korzeniem.

Słowo kodowe jest ścieŜką od korzenia do znaku przyjmując, Ŝe 0 oznacza „przejście do lewego syna", a 1 „przejście do prawego syna" w drzewie.

a ≡≡≡≡ 0, b ≡≡≡≡ 101, c ≡≡≡≡ 100, d ≡≡≡≡ 111, e ≡≡≡≡ 1101, f ≡≡≡≡ 1100

Tylko regularne drzewo binarne zapewnia optymalny kod. Kod o stałej długości: a ≡ 000, b ≡ 001, c ≡ 010, d ≡ 011, e ≡ 100, f ≡ 101

nie jest optymalny gdyŜ są kody rozpoczynające się od 10..., nie ma zaś od 11....

Dla alfabetu ΩΩΩΩ prefiksowe drzewo kodowe ma |ΩΩΩΩ| liści oraz |ΩΩΩΩ| - 1 węzłów wewnętrznych.

Koszt kodowego drzewa prefiksowego T to liczba bitów potrzebnych do zakodowania pliku, określona jest wzorem B(T)

gdzie Lc –liczba wystąpień znaku c w alfabecie; dc –długość kodu znaku c.

Nadawca i odbiorca muszą uŜywać tego samego sposobu kodowania - tego samego drzewa Huffmana. W jaki sposób nadawca moŜe poinformować odbiorcę, którego kodu uŜył?

1. Nadawca i odbiorca uzgadniają na wstępie wybór konkretnego drzewa Huffmana i obydwaj uŜywają go przy przesyłaniu wszelkich informacji.

2. Nadawca buduje drzewo Huffmana na nowo przy przesyłaniu kaŜdej nowej wiadomości, a wraz z samą wiadomością przesyła tabelę konwersji. Odbiorca wykorzystuje tabelę do rekonstrukcji drzewa Huffmana i dopiero za jego pomocą dokonuje tłumaczenia.

Druga strategia jest bardziej uniwersalna, jej zalety są widoczne przy kodowaniu duŜych plików.

znak Liczba znaków

kod

a 45 0 b 13 101 c 12 100 d 16 111 e 9 1101 f 5 1100

∑ ⋅=Ω∉c cdcLTB )(

p1

0.45 p2

0.13 p3

0.12 p4

0.16 p5

0.09 p6

0.05n 6 N

11 N

23 N

33 N

43 N

54 N

64

I p1

1.15= I p2

2.94= I p3

3.06= I p4

2.64= I p5

3.47= I p6

4.32=

Entropia: H p n,( ) 2.22= Średnia długo ść słowa kodowego: L p n, N,( ) 2.24=

Page 56: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 103 Instytut Aparatów Elektrycznych Algrm12 Dr J. Dokimuk

Budowanie drzewa – algorytm kodowania Huffmana Algorytm buduje drzewo w sposób wstępujący rozpoczynając od zbioru liści |ΩΩΩΩ|. Kolejny etap to |ΩΩΩΩ| - 1 operacji scalania, prowadzących do powstania jednego drzewa.

KaŜdemu znakowi c ∈∈∈∈ ΩΩΩΩ odpowiada jego liczba wystąpień f(c).

Kolejkę priorytetową Q zrealizowano jako kopiec z kluczami f i uŜyto do wyznaczania dwóch obiektów o najmniejszej liczbie wystąpień, które w kolejnym etapie są scalane.

Wynikiem scalenia jest nowy obiekt (węzeł drzewa), którego wartością jest sumą liczby wystąpień jego składowych.

W pętli for cyklicznie usuwane są dwa węzły o najmniejszej liczbie wystąpień, a następnie zastępowane w kolejce Q przez węzeł z, powstały przez ich scalenie.

Węzły x i y stają się odpowiednio lewym i prawym synem węzła z (zamiana lewego i prawego syna daje inny kod o tym samym koszcie).

Po n - 1 scaleniach w kolejce pozostaje korzeń drzewa kodu. ZłoŜoność obliczeniowa wynosi O(nlg(n)) i limitowana jest wykonaniem n-1 operacji na kopcu (kaŜda operacja na kopcu ma złoŜoność O(lg(n)) ). C. Shannon wykazał, Ŝe entropia H źródła informacji daje najlepszą długość kodu. śaden algorytm kompresji nie daje lepszego wyniku niŜ wynika to z entropi E.

Huffman() Utwórz jednowęzłowe drzewo dla kaŜdego symbolu. Uporządkuj wszystkie drzewa ze względu na częstość wystąpień symboli. while zostało więcej niŜ jedno drzewo

weź dwa drzewa T1 i T2 o najmniejszych częstościach f1 i f2 (f1 ≤ f2) występowania symboli i utwórz drzewo o synach T1 i T2 i częstości w korzeniu równej f1 + f2

Oznacz kaŜdą krawędź skierowaną w lewo jako 0, a skierowaną w prawo jako 1. Utwórz kod dla kaŜdego symbolu, przechodząc drzewo od korzenia do liścia odpowiadającego

temu symbolowi i łącząc napotykane zera i jedynki . W korzeniu otrzymanego drzewa częstość wystąpień wynosi 1.

HUFFMAN(ΩΩΩΩ) n |ΩΩΩΩ| Q ΩΩΩΩ // budowanie kopca for i 1 to n-1 do

z CreateNode() x left[z] MIN(Q) y right[z] MIN(Q) f(z) f(x) + f(y) INSERT(Q, z)

return MlN(Q)

Page 57: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 104Instytut Aparatów Elektrycznych Algrm13 Dr J. Dokimuk

13. PROBLEMY NP – ZUPEŁNE Problem plecakowy 0-1, komiwojaŜera, oraz setki innych są trudne w realizacji. Gdyby udało się znaleźć efektywny algorytm dla któregokolwiek z nich to dysponowalibyśmy efektywnymi algorytmami dla wszystkich problemów tej grupy. Takiego algorytmu nie wynaleziono lecz nie udowodniono, Ŝe nie jest to moŜliwe. Jest to tematyka problemów NP-zupełnych.

Algorytm wielomianowy: jego pesymistyczna złoŜoność czasowa ograniczona jest z góry przez funkcję wielomianową, zaleŜną od rozmiaru danych wejściowych n. Istnieje taki wielomian g(n ), Ŝe: W(n) ∈∈∈∈ O(g(n)).

Uwaga: -algorytmy złoŜoności o czasowej 2n, 2n/100,n! nie są algorytmami wielomianowymi. -nlg(n) nie jest wielomianem, jednak spełniona jest nierówność: nlg(n) < n 2, zatem

algorytm o takiej złoŜoności czasowej spełnia kryterium dla algorytmów wielomianowych. Problem trudny: jeŜeli nie moŜna rozwiązać go za pomocą algorytmu wielomianowego.

Trudność jest własnością problemu, nie zaś algorytmów go rozwiązujących. Nie moŜe istnieć Ŝaden rozwiązujący go algorytm wielomianowy.

Wiele algorytmów, o nie wielomianowej pesymistycznej złoŜoność czasowej, charakteryzuje się dobrą efektywnością dla wielu praktycznych danych wejściowych.

Znalezienie algorytmu o wielomianowej złoŜoności czasowej dla pewnych danych wejściowych nie oznacza, Ŝe problem jest łatwy do rozwiązania dla wszystkich danych wejściowych (dla których znalezienie algorytmu wielomianowego moŜe okazać się niemoŜliwe).

Główne kategorie problemów ze względu na ich trudność: 1. dla których wynaleziono algorytmy wielomianowe, 2. których trudność została udowodniona, 3. których trudność nie została udowodniona, jednak nie udało się wynaleźć

algorytmów wielomianowych. Problem optymalizacyjny: jego wynikiem jest optymalne rozwiązanie problemu. Problem decyzyjny: jego wynikiem jest odpowiedź „tak" lub „nie" dla danego problemu.

Problem plecakowy 0-1: znana jest waga i wartość przedmiotów oraz pojemność W plecaka. Optymalizacyjny: wyznaczyć maksymalną wartości przedmiotów, które moŜna upchnąć w plecaku. Decyzyjny: dana jest wartości P, określić, czy istnieje moŜliwość takiego spakowania plecaka, by

łączna waga zapakowanych przedmiotów nie przekraczała W, a łączna wartość tych przedmiotów wynosiła co najmniej P.

Problem komiwojaŜera: trasą w waŜony grafie skierowanym jest droga rozpoczynająca się i kończąca w tym samym wierzchołku, przechodząca raz przez wszystkie pozostałe wierzchołki grafu.

Optymalizacyjny: znaleźć trasę o minimalnej łącznej wadze naleŜących do niej krawędzi. Decyzyjny: dana jest dodatniej liczba całkowita d, określić czy w badanym grafie istnieje trasa o

łącznej wadze naleŜących do niej krawędzi nie większej od d. Gdyby moŜna było znaleźć algorytm wielomianowy dla problemów optymalizacyjnych, mielibyśmy takŜe algorytm wielomianowy dla odpowiedniego problemów decyzyjnych. Rozwiązanie problemu optymalizacyjnego daje rozwiązanie stosownego problemu decyzyjnego.

Zbiór P: zbiór problemów decyzyjnych, które moŜna rozwiązać algorytmami wielomianowymi. Teoretycznie problem plecakowy 0-1 moŜe naleŜeć do zbioru P.

Nikomu nie udało się stworzyć algorytmu wielomianowego rozwiązującego ten problem, lecz nikt jeszcze nie udowodnił, Ŝe nie da się go rozwiązać w czasie wielomianowym.

Z całą pewnością dany problem decyzyjny nie naleŜy do zbioru P jeŜeli udowodnimy, Ŝe opracowanie odpowiedniego algorytmu wielomianowego jest niemoŜliwe.

Algorytm niedeterministyczny: odpowiada tylko za etap "przypuszczania", poniewaŜ nie moŜna dla niego wyznaczyć unikalnej sekwencji kolejnych instrukcji. Podczas realizacji zadań komputer moŜe generować dowolny (losowy) ciąg działań.

Wprowadzenie pojęcia niedeterministycznego jest formalnym zabiegiem, mającym na celu uzyskanie definicji pojęcia sprawdzalności w czasie wielomianowym.

W praktyce nie korzysta się z algorytmów niedeterministycznych do rozwiązywania problemów decyzyjnych.

Mówimy, Ŝe niedeterministyczny algorytm „rozwi ązuje" problem decyzyjny, jeśli: 1. Dla dowolnego przypadku, dla którego odpowiedzią jest „tak ", istnieje pewien ciąg działań,

dla którego na etapie weryfikacji otrzymamy wynik „prawda". 2. Dla dowolnego przypadku, dla którego odpowiedzią jest „nie", nie istnieje Ŝaden ciąg działań,

dla którego na etapie weryfikacji otrzymamy wynik „prawda".

Algorytmy i Struktury Danych - wykład 105Instytut Aparatów Elektrycznych Algrm13 Dr J. Dokimuk

Algorytm wielomianowy niedeterministyczny: algorytm niedeterministyczny (zgadujący), którego etap weryfikacji jest algorytmem wielomianowym.

Zbiór NP (Nondeterministic Polynomial): zbiór wszystkich problemów decyzyjnych, które moŜna rozwiązać za pomocą niedeterministycznych algorytmów wielomianowych.

Problem decyzyjny naleŜy do zbioru NP jeŜeli istnieje dla niego algorytm weryfikuj ący w czasie wielomianowym.

Nie oznacza to, Ŝe musi istnieć algorytm wielomianowy rozwiązujący ten problem decyzyjny. Decyzyjny problem komiwojaŜera naleŜy do zbioru NP, gdyŜ istnieje weryfikujący go algorytm w czasie wielomianowym, mino Ŝe nie wynaleziono takiego algorytmu rozwiązującego ten problem.

Istnieje mnóstwo problemów, dla których nie udało się stworzyć algorytmu rozwiązującego je w czasie wielomianowym, ale udowodniono, Ŝe naleŜą one do zbioru NP, poniewaŜ opracowano dla nich niedeterministyczne algorytmy wielomianowe .

KaŜdy problem naleŜący do zbioru P naleŜy takŜe do zbioru NP. KaŜdy problem naleŜący do zbioru P moŜna rozwiązać za pomocą algorytmu wielomianowego, moŜna więc wygenerować dowolny ciąg na etapie niedeterministycznym i uruchomić algorytm wielomianowy na etapie deterministycznym. Wynikiem weryfikacji dla danego ciągu wejściowego będzie odpowiedź „tak", jeśli jest rozwiązanie dla konkretnego przypadku. Problemy decyzyjne, dla których udowodniono, Ŝe nie naleŜą do zbioru NP, są dokładnie tymi samymi problemami, dla których udowodniono, Ŝe są trudne.

Problem CNF-spełnialności

Zmienna logiczną (boolowską), x jest prawdziwe wtedy i tylko wtedy, gdy x jest fałszywe. Literał jest logiczną zmienną lub negacją logicznej zmiennej.

Klauzula jest sekwencją literałów, oddzielonych za pomocą operatora logicznego OR (v). Koniunkcyjna postać normalna (Conjunctive Normal Form - CNF) logicznego wyraŜenia jest

sekwencją klauzul , oddzielonych za pomocą logicznego operatora AND (∧∧∧∧). Przykład wyraŜenia logicznego w postaci CNF: )()()( 31412321 xxxxxxxx ∨∧∨∨∧∨∨ .

Decyzyjny problem CNF-spełnialności: określić dla logicznego wyraŜenia CNF, czy istnieje takie przyporządkowanie wartości zmiennych (zbiór przyporządkowań wartości „prawda" i

„fałsz" do zmiennych uŜytych w wyraŜeniu), dla którego całe wyraŜenie będzie prawdziwe. Dla wyraŜenia CNF 2121 )( xxxx ∧∧∨ nie istnieją przypisania, dla których będzie ono prawdziwe , dla problemu CNF-spełnialności odpowiedź jest NIE.

Dla wyraŜenia 23221 )()( xxxxx ∧∨∧∨ istnieją przypisania, dla których będzie ono prawdziwe , dla problemu CNF-spełnialności odpowiedź jest TAK. (x1=P, x2=x3=F)

Istnieją algorytmy wielomianowe, które - dla pobranego wyraŜenia logicznego CNF i zbioru przyporządkowań wartości logicznych do zmiennych -sprawdzają, czy wyraŜenie jest logiczne.

Nie istnieje algorytm wielomianowy rozwiązujący ten problem, lecz nie udowodniono, Ŝe tego problemu nie da się rozwiązać w czasie wielomianowym

W roku 1971 S. Cook udowodnił, Ŝe jeśli problem CNF-spełnialności naleŜy do zbioru P , to P=NP.

Rysunek przedstawia zbiór wszystkich problemów decyzyjnych, gdzie P jest podzbiorem zbioru NP. Nie ma jednak pewności, Ŝe tak faktycznie jest.

Niepewność wynika z faktu, Ŝe nikomu nie udało się dowieść, Ŝe istnieje problem w zbiorze NP , który nie naleŜy do zbioru P .

Pytanie czy zbiory P i NP są sobie równe, jest jedną z waŜnych kwestii w informatyce, poniewaŜ duŜa grupa problemów decyzyjnych naleŜy do

zbioru NP. Gdyby okazało się, Ŝe P = NP, moglibyśmy opracować algorytmy wielomianowe dla większości znanych dzisiaj problemów decyzyjnych.

Aby udowodnić, Ŝe P ≠ NP naleŜy znaleźć problem naleŜący do zbioru NP, który nie naleŜałby do zbioru P.

Aby udowodnić, Ŝe P = NP, naleŜy znaleźć algorytm wielomianowy dla kaŜdego z problemów naleŜących do zbioru NP. To zadanie moŜna uprościć, gdyŜ wykazano, Ŝe konieczne jest znalezienie wielomianowego algorytmu tylko dla jednej z ogromnych klas problemów.

PPPP

NPNPNPNP

problemydecyzyjne

Page 58: algorytmy i struktury danych

Algorytmy i Struktury Danych - wykład 106Instytut Aparatów Elektrycznych Algrm13 Dr J. Dokimuk

NaleŜy rozwiązać problem decyzyjny A, dysponując algorytmem rozwiązującym problem B. ZałóŜmy, Ŝe utworzymy algorytm dający taką realizację y problemu B na podstawie kaŜdej moŜliwej realizacji x problemu A, Ŝe algorytm dla realizacji y problemu B zwróci odpowiedź „tak" wtedy i tylko wtedy, gdy odpowiedź dla realizacji x problemu A takŜe brzmi „tak".

Taki algorytm nazywany jest algorytmem transformacji i ma postać funkcji y = Trans(x), odwzorowującej dowolne realizacje problemu A w odpowiednie realizacje problemu B.

Trans jest algorytmem transformacji, odwzorowującym kaŜdą realizację x problemu decyzyjnego A w realizację y problemu decyzyjnego B. W połączeniu z algorytmem dla problemu B daje algorytm rozwiązujący problem decyzyjny A.

Mamy dane wejściowymi x dla problemu A i chcemy odpowiedzi „tak" lub „nie", to zastosujemy algorytm Trans do przekształcenia danych x w dane wejściowe y dla problemu B w taki sposób, Ŝeby odpowiedź do problemu B dla danych y była dokładnie odpowiedzią do problemu A dla danych x.

Problem A jest wielomianowo redukowalny do problemu B, jeśli istnieje wielomianowy algorytm transformacji z problemu decyzyjnego A do problemu decyzyjnego B : A ∝∝∝∝ B.

Jeśli złoŜoność czasowa algorytmu transformacji jest wielomianowa i istnieje wielomianowy algorytm rozwiązujący problem B, to algorytm dla problemu A musi być wielomianowy.

Twierdzenie 2.1. Jeśli problem decyzyjny B naleŜy do zbioru P oraz A ∝∝∝∝ B to problem decyzyjny A takŜe naleŜy do zbioru P .

Problem B nazywamy NP-zupełnym, jeśli spełnione są dwa warunki: 1. Problem B naleŜy do zbioru NP. 2. Dla kaŜdego innego problemu A naleŜącego do zbioru NP prawdziwe jest wyraŜenie: A ∝∝∝∝ B.

Twierdzenie 2.2. (Cooka, 1971r.): Problem CNF-spełnialności jest problemem NP-zupełnym.

Twierdzenie 2.3. Problem C jest problemem NP-zupełnym, jeśli spełnione są dwa warunki: 1. Problem C naleŜy do zbioru NP . (weryfikowany wielomianowo) 2. Dla innego problemu NP-zupełnego B prawdziwe jest wyraŜenie: B ∝∝∝∝ C.

W oparciu o Tw. Cooka i Tw. 2.3 moŜna wykazać, Ŝe wybrany problem decyzyjny jest NP-zupełny, dowodząc jedynie Ŝe:

1. problem ten naleŜy do zbioru NP, 2. CNF-spełnialność ∝∝∝∝ wybrany problem (problem CNF-spełnialności redukuje się do naszego problemu).

Zbiór problemów NP-zupełnych jest podzbiorem zbioru NP, zgodne z definicją.

Problemy decyzyjne nie naleŜące do zbioru NP nie mogą być problemami NP-zupełnymi.

Jeśli P ≠≠≠≠ NP to: P ∩∩∩∩ NP-zupełne = ∅∅∅∅ (brak wspólnych elementów) PowyŜsza zaleŜność wynika z faktu, Ŝe gdyby jakiś problem w zbiorze P był NP-zupełny , to na podstawie Tw.2.1 moglibyśmy uznać, moŜliwość rozwiązania problemu ze zbioru NP w czasie wielomianowym.

Uwaga: nie jest wykluczone, Ŝe P i NP to ten sam zbiór. ____________________________________________________________

Zbiór P problemów decyzyjnych rozwiązywanych w czasie wielomianowym. Zbiór NP problemów decyzyjnych zgadywanych i weryfikowanych w czasie wielomianowym. Zbiór NP-zupełny: wszystko albo nic. Jeśliby dla jakiegokolwiek problemu NP-zupełnego

znaleziono algorytm wielomianowy, byłby to algorytm dla wszystkich z nich. KaŜdy problem NP-zupełny redukuje się w czasie wielomianowym do kaŜdego innego; trudność rozwiązania jednego pociąga za sobą trudność rozwiązania wszystkich.

Aby wykazać, Ŝe problem ΦΦΦΦ jest NP-zupełny , wystarczy udowodnić, Ŝe problem ΦΦΦΦ redukuje się (w czasie wielomianowym) do co najmniej jednego problemu Q NP-zupełnego i Ŝe co najmniej jeden problem R NP-zupełny, redukuje się do ΦΦΦΦ.

Pokazanie, Ŝe jakiś problem jest NP-zupełny, jest sygnalizacją moŜliwych trudności w jego rozwiązaniu.

TransTransTransTransAlgorytm

dlaproblemu B

xxxxyyyy

tak

nie

AAAA

PPPP

NPNPNPNP

NNNNPPPP----zzzz

uuuuppppeeeełłłłnnnn

eeee