83
Uniwersytet im. Adama Mickiewicza w Poznaniu Wydział Fizyki, kierunek informatyka stosowana Algorytmy i struktury danych Notatki do wykładów dr inŜ. Pawła Prałata Opracowane przez: Piotr Knychała [email protected] http://www.czbobry.int.pl Kalisz 2004-2005 Ostatnia aktualizacja VIII 2005 Za błędy w treści materiałów nie odpowiadam.

Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

  • Upload
    others

  • View
    7

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Uniwersytet im. Adama Mickiewicza w Poznaniu Wydział Fizyki, kierunek informatyka stosowana

Algorytmy i struktury danych

Notatki do wykładów dr inŜ. Pawła Prałata Opracowane przez: Piotr Knychała

[email protected] http://www.czbobry.int.pl

Kalisz 2004-2005 Ostatnia aktualizacja VIII 2005

Za błędy w treści materiałów nie odpowiadam.

Page 2: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

2

Spis treści:

Efektywność........................................................................................................................................................... 5

Notacja ”wielkie O”........................................................................................................................................... 5

Notacje: Ω i Θ .................................................................................................................................................... 6

Listy........................................................................................................................................................................ 6

Dokładanie elementu do listy .............................................................................................................................7

Wyszukiwanie elementu w liście......................................................................................................................... 7

Usuwanie elementu z listy .................................................................................................................................. 7

Listy cykliczne (rozwiązanie problemu Josephusa)............................................................................................ 8

Stos (LIFO – Last In First Out) ........................................................................................................................... 9

Implementacja stosu z wykorzystaniem tablicy .................................................................................................. 9

Implementacja stosu z wykorzystaniem listy jednokierunkowej ....................................................................... 10

Kolejka (FIFO – First In First Out) .................................................................................................................. 10

Implementacja kolejki z wykorzystaniem tablicy.............................................................................................. 11

Implementacja kolejki z wykorzystaniem listy jednokierunkowej..................................................................... 12

Kolejka priorytetowa (wykorzystanie kopca) ................................................................................................... 12

Kopiec................................................................................................................................................................... 13

Naprawianie kopca .......................................................................................................................................... 14

Tworzenie kopca............................................................................................................................................... 15

Sortowanie danych.............................................................................................................................................. 16

Sortowanie przez wstawianie (ang. insertion sort)........................................................................................... 16

Sortowanie przez wybór (ang. selection sort) .................................................................................................. 17

Sortowanie bąbelkowe - wersja standardowa i ulepszona (ang. bubble exchange sort)..................................17

Sortowanie Shella (ang. shellsort) ................................................................................................................... 18

Sortowanie przez kopcowanie (ang. heapsort)................................................................................................. 19

Sortowanie szybkie – wersja standardowa oraz z losowym elementem dzielącym (ang. quicksort) ................ 20

Sortowanie przez scalanie (ang. mergesort) .................................................................................................... 21

Sortowanie przez zliczanie (ang. counting sort)............................................................................................... 22

Sortowanie stabilne .......................................................................................................................................... 23

Sortowanie pozycyjne (ang. radix sort)............................................................................................................ 23

Sortowanie kubełkowe (ang. bucket sort)......................................................................................................... 24

Algorytmy rekurencyjne .................................................................................................................................... 25

Proste algorytmy rekurencyjne......................................................................................................................... 26 Silnia............................................................................................................................................................ 26 NWD – największy wspólny dzielnik (algorytm Euklidesa)....................................................................... 26 Wypisywanie wyrazu od końca ................................................................................................................... 26

Algorytmy zachłanne (ang. greek algorithm) ................................................................................................... 27 Problem kasjera ........................................................................................................................................... 27 Szukanie minimum funkcji.......................................................................................................................... 27

Page 3: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

3

Strategia ”dziel i rządź”................................................................................................................................... 28 Wyznaczanie wartości maksymalnej ........................................................................................................... 28 Linijka ......................................................................................................................................................... 28 WieŜa Hanoi ................................................................................................................................................ 28 Ciąg liczb Fibonacciego .............................................................................................................................. 29

Programowanie dynamiczne ............................................................................................................................ 29 Ciąg liczb Fibonacciego .............................................................................................................................. 29 Liczba kombinacji – trójkąt Pascala ............................................................................................................ 29 Problem pakowania plecaka (ang. knapsack problem)................................................................................ 30 Rozkład na czynniki pierwsze liczb naturalnych (0-1000).......................................................................... 32

Metoda powrotów – ”prób i błędów” (ang. backtracking)..............................................................................32 Problem skoczka szachowego ..................................................................................................................... 32 Problem ośmiu hetmanów ........................................................................................................................... 33

Coś na deser..................................................................................................................................................... 34 Krzywa Kocha (płatek śniegu) .................................................................................................................... 34 Trójkąt i dywan Sierpińskiego..................................................................................................................... 34 Krzywa Hilberta ..........................................................................................................................................34 Zliczanie białych obszarów i liczby białych pól w kaŜdym obszarze ......................................................... 35

Drzewo binarne – implementacja podstawowych operacji ............................................................................. 36

Dodawanie elementu ........................................................................................................................................ 36

Wyszukiwanie elementu, element minimalny i maksymalny ............................................................................. 37

Usuwanie elementu (3 przypadki) .................................................................................................................... 38

Przechodzenie drzewa w głąb i wszerz............................................................................................................. 40

RównowaŜenie drzewa ..................................................................................................................................... 41 Metoda z wykorzystaniem pomocniczej tablicy.......................................................................................... 41 Algorytm DSW............................................................................................................................................ 42

Tablice haszujące ................................................................................................................................................ 48

Funkcje haszujące – metody ich wyznaczania.................................................................................................. 49

Rozwiązywanie kolizji metodą łańcuchową......................................................................................................50

Adresowanie otwarte........................................................................................................................................ 50

Teoria grafów ...................................................................................................................................................... 53

Podstawowe definicje ....................................................................................................................................... 53

Reprezentacja grafów w komputerze................................................................................................................ 58

Przechodzenie grafu w głąb, wyznaczanie spójnych składowych..................................................................... 59

Przechodzenie grafu wszerz ............................................................................................................................. 60

Sortowanie topologiczne ..................................................................................................................................60

Wyznaczanie silnych spójnych składowych w grafie skierowanym.................................................................. 61

Trochę o łączeniu w zbiory............................................................................................................................... 64

Minimalne drzewo rozpinające ........................................................................................................................ 66 Algorytm Kruskala ...................................................................................................................................... 67 Algorytm Prima ...........................................................................................................................................68

Najkrótsze ścieŜki z jednym źródłem ................................................................................................................ 69 Algorytm Dijkstry ....................................................................................................................................... 69 Algorytm Bellmana – Forda ........................................................................................................................ 73

Maksymalny przepływ ......................................................................................................................................74

Maksymalne skojarzenia w grafie dwudzielnym .............................................................................................. 77

Page 4: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

4

Obliczanie wielomianu chromatycznego.......................................................................................................... 78

Dodatek: Algorytmy wyszukiwania wyrazu wzorcowego w tekście ...................................................81

Metoda „naiwna” ............................................................................................................................................ 81

Metoda Rabina-Karpa......................................................................................................................................81

Metoda Knuta-Morrisa-Pratta ......................................................................................................................... 82

Zakończenie ....................................................................................................................................................... 83

Page 5: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

5

Efektywno ść Początkowi programiści pisząc program cieszą się, gdy się kompiluje, a gdy działa

poprawnie to juŜ w ogóle są w raju. Jednak im bardziej skomplikowane programy piszemy, naleŜy coraz bardziej zwracać uwagę na szybkość ich działania. WaŜne jest to szczególnie wtedy, gdy program ma operować na duŜej liczbie danych. Problemy moŜna rozwiązać na wiele sposobów, chodzi o to, aby wybrać najbardziej efektywny (czyli taki, który najszybciej doprowadzi nas do celu). Właśnie efektywność algorytmów porównuje się obliczając tzw. złoŜoność obliczeniową. Określa ona m.in. ilość czasu i pamięci, jaka jest potrzebna do rozwiązania problemu.

Szybkość wykonywania danego programu zaleŜy od wielu czynników, nie tylko od złoŜoności algorytmu. Na ten czynnik ma takŜe wpływ rodzaj sprzętu, na którym uruchomiony jest program. Znaczenie ma takŜe język, w jakim program został napisany.

Do opisu złoŜoności algorytmu nie stosuje się jednostek czasu, takich jak sekunda czy mikrosekunda, lecz opisuje się ją podając zaleŜność, z jaką zmienia się czas w stosunku do wzrostu liczby danych. Przykładowo: jeŜeli zwiększenie danych do przetworzenia wzrasta 3-krotnie, to czas potrzebny na tą operację teŜ wzrośnie 3-krotnie. MoŜna tą zaleŜność przedstawiać w postaci wzoru t(n). np. t(n)=n (zaleŜność liniowa). Funkcje przedstawiające taką zaleŜność są w rzeczywistości bardziej skomplikowane i bardzo często pomija się w nich składniki nie mające znaczącego wpływu na jej wartość. Wtedy złoŜoność obliczona w takim wzorze jest przybliŜona, ale dla bardzo duŜego n jest i tak dość dokładna. Taki sposób obliczania efektywności algorytmu określa się mianem złoŜoności asymptotycznej.

Szybkość wzrostu poszczególnych składników przykładowej funkcji wyznaczającej złoŜoność asymptotyczną podano w poniŜszej tabeli.

1000log100)( 10

2 +++= nnnnf

n f(n) n2 100n log10n 1000

1 1101 1 100 0 1000 10 2101 100 1000 1 1000 100 21002 10 000 10 000 2 1000 1000 1101003 1 000 000 100 000 3 1000

10 000 101001004 100 000 000 1 000 000 4 1000 100 000 10010001005 10 000 000 000 10 000 000 5 1000

Tabela 1: Efektywność - przykład (źródło: A. Drozdek - "Struktury danych w języku C").

Analizując tabele widzimy, Ŝe dla małych wartości n największy wkład do wyniku funkcji wnosi ostatni składnik (1000). Przy wzroście n do wartości 10 zarówno składnik 100n, jak i 1000 dają taki sam wkład. Dla n równego 100 największy wkład wnosi część n2 oraz 100n. Dla wartości n powyŜej 100 widać wyraźnie, Ŝe kwadratowa szybkość wzrostu składnik n2 powoduje, Ŝe wnosi on największy wkład i wartość funkcji zaleŜy głównie od niego. Zatem dla dostatecznie duŜych n pozostałe składniki moŜna zaniedbać.

Notacja ”wielkie O” Jest to jedna z powszechniej stosowanych notacji opisująca złoŜoność asymptotyczną.

Funkcja f(n) jest O(g(n)), jeśli istnieją liczby dodatnie c i N takie, Ŝe f(n) ≤ cg(n) dla wszystkich n≥N. Co znaczy tyle, Ŝe jeśli mamy daną funkcję:

132)( 2 ++= nnnf to funkcja cg(n) dla wszystkich n≥N opisuje przybliŜona wartość złoŜoności asymptotycznej funkcji f(n). Stałe c i N wyznacza się z równania:

Page 6: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

6

22 132 cnnn =++ Jak widać nie jest wcale łatwo wyznaczyć jednoznaczną wartość c, poniewaŜ zaleŜy ona od jakich n≥N będzie spełniona zaleŜność f(n)≤cg(n). Dodatkowo moŜna przyjąć za g(n) wiele innych funkcji, m.in. n3, n4, itd. Dlatego notację "wielkie O" stosuje się tylko pomijając najmniej znaczące składniki. Dla naszego przykładu wygląda to następująco: f(n) = 2n2+O(n), albo f(n) = O(n2) albo f(n) = O(n3).

Notacje: Ω i Θ Poprzednia notacja ograniczała funkcję od góry. Notacja ”omega” ogranicza ją od dołu.

Funkcja f(n) jest Ω(g(n)), jeśli istnieją liczby dodatnie c i N takie, Ŝe f(n) >= cg(n) dla wszystkich n≥N.

Podobnie jak w poprzedniej notacji mamy do czynienia z taką samą zasadą, lecz wyznaczającą funkcję ograniczającą f(n) od dołu. Pojawiają się tu te same problemy istnienia nieskończenie wielu takich funkcji oraz zaleŜności stałej c od N. Łącząc te dwie notacje uzyskujemy notację ”teta”. Funkcja f(n) jest Θ(g(n)), jeśli istnieją liczby dodanie c1,c1 i N takie, Ŝe c1g(n)≤ f(n) ≤ c2g(n) dla wszystkich n≥N. Opierając się na notacji "wielkiego O" nie zawsze uzyskuję się dobre rezultaty. ZaleŜność zachodzi dla wszystkich n z wyjątkiem pewniej skończonej liczby n, które jej nie spełniają. MoŜna ograniczać tą ilość n zwiększając c, ale wtedy obliczona złoŜoność staje się mniej dokładna. Algorytmy moŜna podzielić na klasy, które określają szybkość wzrost t w zaleŜności od n.

Nazwa klasy ZłoŜoność klasy stały- czas nie zaleŜy od n (jest const) Θ(1) logarytmiczny Θ(ln n) liniowy Θ(n) kwadratowy Θ(n2) sześcienny Θ(n3) wykładniczy Θ(2n)

Tabela 2: Klasy złoŜoności algorytmów.

Listy W przypadku tablicy mieliśmy sytuację, w której kolejne elementy tablicy były przechowywane w pamięci jeden po drugim. Dynamiczne przydzielanie pamięci umoŜliwia nam tworzenia dynamicznych wiązanych struktur danych. Charakteryzują się one tym, Ŝe kaŜdy element oprócz swoich danych zawiera takŜe informację, gdzie znajduje się kolejny element w strukturze. Przykładem takiej struktury danych są listy. Listy to struktura danych słuŜąca do przechowywania z góry nieznanej ilości informacji tego samego typu. Skład się ona z węzłów, które zawierają dane przechowywane w liście oraz wskaźnik do kolejnego elementu. Listę rozpoczyna wskaźnik do początku, a ostatni element wskazuje na NULL.

Page 7: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

7

Nazwa Opis

jednokierunkowe KaŜdy element zawiera wskaźnik do następnego elementu. Listę moŜemy przechodzić tylko w jednym kierunku.

dwukierunkowe KaŜdy element zawiera wskaźnik do następnego i poprzedniego elementu. Dzięki temu listę moŜemy przechodzić od początku do końca i odwrotnie.

z wartownikiem Na początku listy znajduje się dodatkowy element (wartownik) nie przechowujący konkretnych danych, ale usprawniający usuwanie.

bez wartownika Lista nie posiadająca dodatkowego elementu na początku (wartownika). posortowane Elementy listy są ułoŜone w sposób uporządkowany.

nie posortowane Przypadkowe rozmieszczenie elementów na liście.

Tabela 3: Rodzaje list.

Dokładanie elementu do listy Dokładanie elementów na końcu listy nie jest dobrym rozwiązaniem poniewaŜ aby to

zrobić naleŜało by przejść całą listę, co spowalnia proces. Dlatego najlepiej dokładać element na początku listy. Wyjątkiem jest sytuacja, gdy mamy do czynienia z listą posortowaną. Wtedy naleŜy poszukać miejsca, w którym naleŜy wstawić element. Jeśli nasza lista jest posortowana rosnąco, to przechodzimy ją od początku dopóty, dopóki wstawiany element jest większy od aktualnie odwiedzanego na liście. Jeśli warunek ten nie zostanie spełniony to naleŜy wstawić nowy element przed aktualnie odwiedzonym na liście.

czerwone łącze usuwamy (przed); niebieskie dokładamy (po);

Wyszukiwanie elementu w li ście MoŜna tu rozwaŜać listy nie posortowane i posortowane. W pierwszym przypadku musimy niestety przejść całą listę od początku do końca poniewaŜ szukany element moŜe znajdować się w dowolnym miejscu listy. Jeśli natomiast nasza lista jest posortowana to poszukujemy elementu tak długo, aŜ go nie znajdziemy lub aktualnie odwiedzany element listy jest większy od szukanego (przypadek dla list posortowanych rosnąco, dla przeciwnej sytuacji element musi być mniejszy).

Usuwanie elementu z listy Problem ten moŜna rozwaŜać dla kilku sytuacji. Na początek zajmijmy się listą nieposortowaną bez wartownika. Zanim rozpoczniemy poszukiwanie elementu do usunięcia sprawdzamy czy lista nie jest pusta. Gdyby się tak zdarzyło, to nie robimy nic. MoŜe się zdarzyć równieŜ sytuacja, w której element do usunięcia znajduje się na początku listy. Wtedy wystarczy ustawić wskaźnik początku na drugi element listy i usunąć pierwszy.

5 1 9 4

2

pocz NULL

Page 8: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

8

czerwone kreski wywalamy (przed); niebieską dokładamy (po);

Ostatni przypadek dotyczy sytuacji, w której usuwany element znajduje się na n-tym miejscu na liście. NaleŜy najpierw go odnaleźć, a następnie usunąć. NaleŜy pamiętać o odpowiednim ustawieniu wskaźnika elementu poprzedzającego usuwany element.

czerwone kreski wywalamy (przed); niebieską dokładamy (po);

Jeśli nasza lista posiada wartownika, to operacja usuwania sprowadza się do jednego przypadku. Jest to ostatni opcja spośród opisanych powyŜej. Zawsze będziemy usuwać element z n-tej pozycji na liście. Skróci do ilość operacji niezbędną do usunięcia elementu (nie trzeba rozwaŜać przypadków).

Warto jeszcze zwrócić uwagę na listy posortowane. Powróćmy do przypadku listy posortowanej rosnąco. Jeśli poszukujemy elementu do usunięcia i w pewnym momencie (przechodząc listę) zorientujemy się, Ŝe aktualnie odwiedzany element jest większy od poszukiwanego, to oznacza to, Ŝe nie ma na liście elementu, który chcemy usunąć. Kończymy dalsze przechodzenie listy.

Listy cykliczne (rozwi ązanie problemu Josephusa) Jest to specyficzny rodzaj listy. Charakterystyczne jest to, Ŝe ostatni element listy wskazuje na pierwszy element na liście, co sprawia, Ŝe lista tworzy strukturę cykliczną. Znany problem, który moŜna rozwiązać przy pomocy listy cyklicznej to problem Josephusa. Mamy n osób, które będą się zabijać nawzajem . Umierać będzie co k-ta osoba. NaleŜy tak dobrać k, aby konkretna osoba przeŜyła (jeśli osoba dobierająca k, zrobi to poprawnie – to przeŜyje ). Umieszczając numery osób na liście cyklicznej, moŜemy uśmiercać co k-ty element dopóty, dopóki nie zostanie nam jeden element na liście. W ten sposób dowiemy się kto został szczęśliwcem.

5

4

9

2

7

0

5 1 9 4

pocz NULL

5 1 9 4

pocz NULL

Page 9: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

9

Stos (LIFO – Last In First Out) Stos to struktura danych, w której ostatni element włoŜony na stos jest zdejmowany

jako pierwszy. MoŜna to porównać do stosu ksiąŜek. JeŜeli na stole leŜy stos połoŜonych jedna na drugiej ksiąŜek to zawsze najlepiej rozpocząć zdejmowanie tych ksiąŜek od góry (wyciąganie ksiąŜki spod spodu nie jest moŜliwe).

Omówimy teraz dwie metody realizacji stosu. Pierwsza, gdy mamy ograniczony rozmiar stosu, wtedy tworzymy tablicę i umieszczamy elementy stosu w tablicy. Drugi, gdy rozmiar stosu jest nieograniczony (właściwie to ogranicza go ilość pamięci). Wtedy tworzymy wskaźnik, który będzie pokazywał na pierwszy element stosu, a kaŜdy kolejny element stosu przechowuje wskaźnik do kolejnego (realizacja tak jak w przypadku listy). W pierwszym przypadku elementy odkładamy do kolejnych komórek tablicy i zdejmujemy od końca tablicy. W drugim, kolejne elementy dokładamy na początku kolejki i zdejmujemy takŜe od początku kolejki.

Implementacja stosu z wykorzystaniem tablicy Pierwszą metodę stosujemy, gdy znamy maksymalny rozmiar stosu. Wtedy tworzymy tablice i umieszczamy elementy stosu w tablicy. Metoda ta z góry ogranicz nam ilość elementów jaką maksymalnie moŜna wrzucić na stos. Elementy odkładamy w kolejnych komórkach tablicy i zdejmujemy w odwrotnej kolejności – wstawiony jako ostatni, będzie zdjęty jako pierwszy. Pseudokod: class stos int rozmiar, ilosc; int * tablica; public: // konstruktor stos(int rozmiar) //ustalenie warto ści pocz ątkowych this->rozmiar = rozmiar; this->ilosc = 0; //przydzielenie pami ęci tablica = new int [rozmiar]; // destruktor ~stos() //zwolnienie pami ęci delete [] tablica;

int empty() //sprawdzenie czy stos jest pusty return (ilosc == 0); int put(int s) //je śłi stos nie jest pełen if (ilosc == rozmiar) return 0; //dodanie elementu tablica[ilosc++] = s; return 1; int get() //je śli stos nie jest pusty if (empty()) return 0; //zdj ęcie elementu return tablica[--ilosc]; ;

1

2

1 1

2

3

1

2

1

Dokładamy elementy na

stos

Zdejmujemy elementy ze

stos

Page 10: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

10

Implementacja stosu z wykorzystaniem listy jednokie runkowej Drugą metodę stosujemy, gdy nie da się przewidzieć ile elementów znajdzie się na stosie (ilość elementów jest oczywiście ograniczona przez ilość dostępnej pamięci). Dla takiego przypadku najlepiej jest wykorzystać listę jednokierunkową, nieuporządkowaną. Elementy będziemy dokładać na początku listy i zdejmować równieŜ od początku listy. Pseudokod: class stos //pojedynczy element stosu class wezel public: int w; wezel *nast; ; wezel *pocz; public: //konstruktor stos() pocz=NULL; //deklaracje ~stos(); void dodaj(int); int usun(); ; //destruktor stos::~stos() wezel *tmp; //je śli nie pusty while (pocz)

tmp=pocz; //przesu ń pocz ątek pocz ątek 1 pocz=pocz->nast; //usu ń element delete tmp; void stos::dodaj(int s) //stwórz nowy element wezel *nowy=new wezel; nowy->w=s; //ustaw wska źniki nowy->nast=pocz; pocz=nowy; int stos::usun() //je śli nie pusty if (!pocz) return 0; int liczba=pocz->w; //ustaw wska źniki wezel *wsk=pocz; pocz=pocz->nast; //zwolnij pami ęć delete wsk; return liczba;

Kolejka (FIFO – First In First Out) Kolejka to struktura danych, w której element, który został wstawiony do kolejki jako

pierwszy, jako pierwszy z niej jest wyciągnięty. Tę strukturę moŜna porównać do kolejki w sklepie. Osoba, która jako pierwsza przyjdzie do sklepu, jako pierwsza zostanie w nim obsłuŜona i pierwsza wyjdzie ze sklepu (pomijamy tu sytuacje ekstremalne :)). Wepchnięcie się do kolejki nie jest najlepszym pomysłem, poniewaŜ moŜe się źle dla nas skończyć.

Podobnie jak w przypadku stosu, moŜliwe są dwa sposoby realizacji kolejki. Pierwszy

w tablicy (co ogranicza rozmiar kolejki). Drugi, bazując na strukturze zwanej listą. Tworzymy wskaźnik do początku kolejki, a kaŜdy kolejny składnik kolejny przechowuje wskaźnik do kolejnego elementu kolejki. Dodatkowo tworzymy wskaźnik, który przechowuje adres końca kolejki. Elementy do kolejki wstawiamy od początku struktury, a zdejmujemy od końca lub na odwrót.

1

1 2

2

1.

2.

3.

4.

5.

Wstawiamy element do

kolejki

Wyci ągamy element z

kolejki

3

3

1 2 3

Page 11: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

11

Implementacja kolejki z wykorzystaniem tablicy Pierwsza metoda wykorzystuje tablice. Jak wspomniano juŜ przy okazji stosu, rozwiązanie takie z góry ogranicza maksymalną ilość elementów w kolejce. Jednak pojawia się tu jeszcze jeden problem. OtóŜ dane dodawane do kolejki umiejscawiamy w kolejnych komórkach tablicy a zdejmujemy je z początku tablicy. MoŜe się zdarzyć sytuacja, w której dojdziemy do końca tablicy i nie będzie moŜliwe wstawienie kolejnych elementów. Jednak jeśli z początku tablicy zostały juŜ zdjęte jakieś elementy, to moŜna rozpocząć uzupełnianie kolejki od początku tablicy. Wyobrazić sobie naleŜy, Ŝe nasza tablica ma kształt pierścienia. Dodając element do kolejki naleŜy pamiętać, Ŝe wyznaczając indeks kolejnego wolnego miejsca w tablicy naleŜy zabezpieczyć się przed wyjściem poza obszar pamięci przypisany do tablicy. Robimy to dzieląc indeks kolejnego wolnego elementu modulo przez rozmiar tablicy.

Pseudokod: class kolejka int *tab; int roz; int ile,pocz,kon; public: //konstruktor kolejka(int roz) this->roz=roz; ile=pocz=kon=0; tab= new int[roz]; //destruktro ~kolejka() delete [] tab; //dekslaracje int push(int s); int pop(); ; //dodawanie elementu int kolejka::push(int s) //je śli kolejka pusta if (!ile) //dodaj element

tab[kon]=s; ile++; //przesu ń indeks kon=(kon+1)%roz; return 1; //je śli brak miejsca if (kon==pocz) return 0; //niepusta, wolna kolejka tab[kon]=s; kon=(kon+1)%roz; ile++; return 1; //usuwanie elementu int kolejka::pop() //je śli pusta if(!ile) return 0; //zdj ęcie elementu int liczba=tab[pocz]; //ustalenie indeksu pocz=(pocz+1)%roz; ile--; return liczba;

1

5

3

7 4

2

0 1

2

3

4 5

6

7

Page 12: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

12

Implementacja kolejki z wykorzystaniem listy jednok ierunkowej Druga metoda polega na wykorzystaniu listy jednokierunkowej, nieposortowanej. Listę taką naleŜy minimalnie zmodyfikować dokładając dodatkowy wskaźnik wskazujący zawsze na ostatni element na liści. Element do kolejki dodajemy na końcu kolejki, a zdejmujemy z początku. Dodawanie elementu do kolejki:

Usuwanie elementu z kolejki:

czerwone kreski wywalamy (przed); niebieską dokładamy (po);

Pseudokod: class kolejka class wezel public: int w; wezel *nast; ; wezel *pocz; wezel *kon; public: kolejka() pocz=kon=NULL; ~kolejka(); int push(int s); int pop(); ; kolejka::~kolejka() wezel *tmp; while (pocz) tmp=pocz; pocz=pocz->nast; delete tmp;

int kolejka::push(int s) wezel *nowy= new wezel; nowy->w=s; if (!kon) pocz=nowy; nowy->nast=NULL; kon=nowy; return 1; kon->nast=nowy; nowy->nast=NULL; kon=nowy; return 1; int kolejka::pop() if (!pocz) return 0; wezel *tmp=pocz; pocz=pocz->nast; int liczba=tmp->w; delete tmp; if(!pocz) kon=NULL; return liczba;

Kolejka priorytetowa (wykorzystanie kopca) Kolejka priorytetowa jest strukturą danych, w której o zdjęciu elementu z kolejki nie decyduje tylko kolejność umieszczenia elementu w kolejce, ale takŜe priorytet. Pierwszy zostanie obsłuŜony ten element, który ma największy priorytet. MoŜliwe są dwie metody realizacji kolejki priorytetowej. W pierwszym przypadku moŜna wykorzystać listę jednokierunkową posortowaną – najlepiej malejąco. Wstawianie elementu do takiej

5 1 9 4

2

pocz NULL

kon

5 1 9 4

pocz NULL

kon

Page 13: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

13

struktury odbywa się na takich zasadach, jak zostały opisane wcześniej (patrz dodawanie elementu do listy posortowanej). Jeśli nasza lista będzie posortowana malejąco względem priorytetów to obsługiwać będziemy zawsze jako pierwszy element na początku listy. Dla takiej struktury złoŜoność operacji wstawiania będzie wynosić O(n), a złoŜoność operacji zdejmowania O(1). Drugim sposobem implementacji kolejki priorytetowej jest wykorzystanie kopca. Aby zaimplementować taką kolejkę naleŜy zapoznać się ze strukturą zwaną kopcem (opis znajduje się w następnym punkcie). Przy wstawianiu elementu do kolejki naleŜy wykonać dwie czynności. Dodać nowy element na pierwszej wolnej pozycji w kopcu i następnie naprawić strukturę. Idę tej operacji przedstawią poniŜszy schemat.

Kopiec Kopiec to struktura drzewiasta posiadająca korzeń, od którego rozchodzi się potomstwo,

które przechowuje zawsze mniejsze wartości niŜ ich ojciec. NajwyŜej w hierarchii kopca znajduje się korzeń, który przechowuje element o największej wartości. PoniŜej na poszczególnych poziomach znajdują się kolejne elementy kopca – oczywiści rozmieszczone zgodnie z zasadą: elementy niŜej połoŜone są mniejsze od swojego rodzica. W kopcu znajdują się takŜe elementy nie posiadające juŜ potomstwa. Określa się je mianem liści. Kopiec moŜemy zaimplementować na dwa sposoby. przechowujących pierwszym przypadku powstaje on z węzłów przechowujących dane oraz dwa wskaźniki do potomstwa (do lewego i prawego syna – patrz rysunek poniŜej). W drugim przypadku wykorzystujemy tablice. Jeśli implementujemy kopiec w tablicy i indeksowanie elementów tablicy rozpoczyna się od 1 (np. Pascal) to wtedy indeks lewego syna obliczamy z wzoru 2i, a prawego z 21+1,

76

20 60

18 9 1 56

Gotowe

60

20 76

18 9 1 56

Wykonujemy tą operację aŜ dojdziemy do korzenia, lub ojciec

będzie większy od syna.

60

20 56

18 9 1 76

Na zielono zaznaczono element

nowo wstawiony. Porównujemy go z ojcem i jeŜeli jest

większy to zamieniamy miejscami.

Page 14: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

14

gdzie i to indeks ojca. Natomiast indeks dziadka obliczymy z wzoru i/2, gdzie i to takŜe indeks ojca. Sytuacja wygląda nieco inaczej, gdy elementy tablicy są indeksowane od zera (np. C/C++). W tym przypadku indeks lewego syna to 2i+1, prawego 2i+2, a indeks dziadka to (i-1)/2, gdzie i to indeks ojca.

Naprawianie kopca Zaczniemy nietypowo, bo nie od tworzenia struktury, lecz od jej naprawiania. A to dlatego, Ŝe do tworzenia kopca będzie potrzebna umiejętność jego naprawienia. JeŜeli zakładamy, Ŝe lewy podkopiec i prawy podkopiec są prawidłowe, natomiast problem stanowi korzeń, który burzy strukturę kopca (nie przechowuje elementu największego). NaleŜy naprawić tak popsutą strukturę. Naprawianie odbywa się w sposób rekurencyjny, począwszy od korzenia. Schemat tej operacji ilustrują poniŜsze rysunki.

1

20 60

18 9 56 33

Z ojca i synów wybieramy element maksymalny, i jeŜeli nie jest nim ojciec to

zamieniamy elementy, i wywołujemy

naprawianie dla fragmentu kopca, którego korzeniem

znów jest niewłaściwy element.

100

20 60

18 9 56 33

11 15 7 8 21

liście

korzeń

Page 15: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

15

Tworzenie kopca Zajmiemy się sytuacją, w której mamy w strukturze kopca (np. w tablicy) umieszczone wartości w sposób przypadkowy. Teraz sytuacje jest nieco trudniejsza, gdyŜ nie tylko element w korzeniu jest nieprawidłowo umieszczony, ale cały kopiec jest źle skonstruowany. NaleŜy więc utworzyć cały kopiec. W tym celu skorzystamy z omówionej przed chwilą metody naprawiania kopca. Wykonujemy operacje naprawiania kopca dla podkopców począwszy od podkopców umieszczonych najniŜej w strukturze. Schemat tej operacji przedstawiają poniŜsze rysunki.

7

10 13

18 49 3

Wykonujemy operację napraw dla tych kopców. Zawsze

zaczynamy od kopców najniŜej połoŜonych i

poruszamy się w górę.

60

20 56

18 9 1 33

Te same czynności wykonujemy dopóty, dopóki rozpatrywany

podkopiec będzie nieprawidłowy lub nie

doszliśmy do najniŜszego elementu

w kopcu.

Kopiec jest juŜ prawidłowy.

60

20 1

18 9 56 33

Wykonujemy ta samą operację ponownie, wywołując funkcję

napraw rekurencyjnie dla podkopca zakreślonego

kreskowaną linią.

Page 16: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

16

Sortowanie danych Jest wiele metod sortowania danych. Tu omawiamy kilka najpopularniejszych. Dobór odpowiedniej metody zaleŜy od wielu czynników. Są to m.in. rodzaj danych, ilość danych, wspólne cechy charakterystyczne danych. Wszystkie metody sortowania opisane w tym opracowaniu są przedstawione na przykładzie sortowania tablicy liczb.

Sortowanie przez wstawianie (ang. insertion sort) Zakładamy, Ŝe pierwszy element tablicy jest posortowany. Bierzemy kolejny,

przenosimy do zmiennej pomocniczej (tmp) i porównujemy elementy juŜ posortowane z tmp. Jeśli te elementy są większe od tego w tmp, to przesuwamy je o jedną pozycję w prawo. Na końcu (po zakończeniu przesuwania) wstawiamy element z tmp do pozostałej komórki tablicy. ZłoŜoność tego algorytmu wynosi O(n2).

Algorytm ten ma swoje zalety, ale takŜe wady. Plusem jest fakt, Ŝe sortuje on tylko tablice wtedy, gdy jest to potrzebne. Jeśli nasza tablica będzie juŜ posortowana (moŜe to być część tablicy), to działanie algorytmu polega jedynie na pobraniu wartości do zmiennej tmp i odstawieniu jej na miejsce. Nie wykonujemy Ŝadnych przesunięć. Minus dotyczy sytuacji, kiedy liczby, które znajdowały się juŜ na odpowiednich pozycjach są i tak przesuwane. W poniŜszym przykładzie ma to miejsce dla liczby 7, która w tablicy wejściowej zajmowała 2 pozycję i pomimo przesunięć, w ostateczności powróciła na 2 pozycję. TakŜe podczas wstawiania liczby do tablicy naleŜy przesunąć wszystkie większe (mniejsze) elementy w prawo. MoŜe się zdarzyć, Ŝe będzie trzeba przesunąć wszystkie elementy w tablicy.

Algorytm ten wykonuje się w n-1 krokach (gdzie n to ilość elementów do posortowania), a w kaŜdym kroku wykonujemy jeszcze pewną ilość przesunięć – zaleŜną od połoŜenia sortowanej liczby. MoŜliwe są dwa skrajne przypadki: tablica całkowicie posortowana i tablica posortowana w odwrotnej kolejności oraz przypadek pośredni, gdy elementy tablicy są rozmieszczone przypadkowo. W pierwszym skrajnym przypadku ilość kroków będzie równa n-1, a ilość przesunięć w kaŜdym kroku 0 (liczba będzie porównana i wstawiona w to samo miejsce). Zatem złoŜoność w tym przypadku wyniesie O(n). W

49

18 13

7 10 3

Gotowe

7

49 13

18 10 3

Wykonujemy operację napraw dla podkopca połoŜonego wyŜej (w tym przypadku jest to

cały kopiec).

Page 17: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

17

drugim skrajnym przypadku ilość korków wyniesie takŜe n-1, natomiast w kaŜdym k-tym kroku będzie wykonane k przesunięć. Zatem ilość przesunięć w n-1 krokach wyniesie:

∑−

=

=−=−+++=1

1

2 )(2

)1()1(...21

n

k

nOnn

nk

Obliczenia dla pośredniego przypadku doprowadziły do wyniku zbliŜonego do złoŜoności O(n2). Zatem taką złoŜoność przyjmuje się dla tego algorytmu.

13 7 10 4 9 tmp

Krok 1 13 10 4 9 7 Krok 2 7 13 4 9 10 Krok 3 7 10 13 9 4 Krok 4 4 7 10 13 9 4 7 9 10 13

Czerwone pola to część posortowana tablicy.

Sortowanie przez wybór (ang. selection sort) Przeszukujemy podtablicę w celu znalezienia minimum, jeśli jest mniejszy od

pierwszego nieposortowanego elementu w tablicy to zamieniamy je miejscami (na początku to będzie pierwszy element tablicy). Elementy zamienione miejscami tworzą część posortowaną tablicy. Czynność tą wykonujemy n-1 razy, gdzie n to ilość liczb w tablicy. ZłoŜoność tego algorytmu takŜe wynosi O(n2).

Musimy więc wykonać n-1 kroków (n to ilość elementów w tablicy), a w kaŜdym kroku wykonujemy przeszukanie podtablicy o (n-1)-k elementach (k to numer kroku). Zatem suma porównań jakie trzeba wykonać będzie wynosić:

∑−

=

=−=++−−+−=−−2

0

2 )(2

)1(1...1)1()1()1(

n

k

nOnn

nnkn s

Daje to złoŜoność algorytmu równą O(n2).

13 7 10 4 9

Krok 0 13 7 10 4 9 Krok 1 4 7 10 13 9 Krok 2 4 7 10 13 9 Krok 3 4 7 9 13 10 4 7 9 10 13 4 7 9 10 13

Czerwone pola to część posortowana tablicy. Niebieskie- element minimalny. Czarna otoczka to obszar, z którego wybieramy minimum.

Sortowanie b ąbelkowe - wersja standardowa i ulepszona (ang. bubble exchange sort)

Bierzemy ostatni element tablicy i porównujemy z poprzednim. Jeśli jest mniejszy to zamieniamy pozycjami i porównujemy ten mniejszy znów z sąsiednim. MoŜna to porównać do bąbelków, które są lŜejsze i idą w górę. Ostatni element tablicy wrzucamy do bąbelka. Jeśli poprzedni jest mniejszy (lŜejszy) to bąbelek zostawia większy element i zabiera ten mniejszy (lŜejszy). Porównuje go z kolejnym sąsiadem i znów to samo... Robimy tak długo, aŜ bąbelek nie dotrze do części posortowanej tablicy. ZłoŜoność tego algorytmu wynosi O(n2).

Page 18: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

18

Kolejne etapy sortowania 13 13 13 13 4 4 4 4 4 4 4 4 7 7 7 4 13 13 13 7 7 7 7 7 10 10 4 7 7 7 7 13 13 9 9 9 4 4 10 10 10 9 9 9 9 13 10 10 9 9 9 9 9 10 10 10 10 10 13 13

Czerwone pola to cześć posortowana tablicy. Niebieskie to bąbelek.

Algorytm ten moŜna nieco ulepszyć. MoŜe się zdarzyć tak, Ŝe elementy tablicy są częściowo posortowane. Taka sytuacja jest przedstawiona w przykładowej tablicy:

1 3 5 8 4 2 Wtedy moŜna tak zaimplementować algorytm, aby został on wcześniej zakończony. Jeśli bąbelek przejdzie z dołu do góry i Ŝadne komórki nie zostaną zamienione miejscami to oznacza to, Ŝe tablica jest juŜ posortowana. Pomimo, Ŝe pozostały nam jeszcze kroki do końca, nie musimy ich juŜ wykonywać. Przy implementacji warto sobie wprowadzić dodatkową zmienną logiczną, która na początku kaŜdego cyklu bąbelka będzie ustawiana na false, a jakakolwiek zamiana miejscami elementów powoduje jej ustawienie na true. Shella ten sposób będziemy mogli kontrolować, czy były dokonywane jakieś zamiany. Jeśli rozpatrzymy ulepszoną wersję algorytmu, to w przypadku, gdy tablica będzie posortowana zostanie wykonane n-1 porównań (n to ilość elementów w tablicy) i Ŝaden element nie zostanie zamieniony. Zatem algorytm będzie miał złoŜoność O(n). W przypadku pesymistycznym, gdy elementy są posortowane odwrotnie niŜ powinny być wykonamy n-1 kroków (n to liczba elementów w tablicy), a w kaŜdym kroku wykonamy (n-1)-k porównań. W ten sposób ogólna liczba porównań wyniesie:

∑−

=

=−=++−−+−=−−1

0

2 )(2

)1(0...1)1()1()1(

n

k

nOnn

nnkn

ZłoŜoność w tym wypadku wynosi O(n2). W sytuacji, gdy elementy w tablicy są rozmieszczone przypadkowo złoŜoność algorytmu takŜe jest zbliŜona do O(n2).

Sortowanie Shella (ang. shellsort) Sortowanie polegające na podzieleniu tablicy na kilka podtablic. Dokonujemy tego

wybierając liczby przeskakując po tablicy (pierwotnej) co h pozycji. KaŜdą z tych h podtablic sortujemy, zmniejszamy h i znów sortujemy kolejne (większe) podtablice. Operację tą wykonujemy aŜ będziemy mieli jedną tablicę do posortowania.

Do posortowania podtablic wykorzystujemy algorytm sortowania przez wstawianie, ewentualnie bąbelkowe. Wynika to z faktu, Ŝe co krok algorytmu nasza tablica będzie „coraz bardziej posortowana”, a algorytmy te są tym bardziej efektywne, im bardziej jest „posortowana” tablica wejściowa. Nie wolno wykorzystywać sortowania przez wybór, gdyŜ wtedy złoŜoność sortowania wzrośnie do O(n2). Przykład:

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

Ustalamy, Ŝe sortujemy co 3 (h=3). Podzielone podtablice zaznaczam na róŜne kolory.

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

Page 19: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

19

Sortujemy liczby w podtablicach. Posortowane podtablice wyglądają następująco.

1 4 1 3 5 2 7 6 2 8 9 4 Sortujemy co h=2.

Sortujemy liczby w podtablicach. Posortowane podtablice wyglądają następująco.

A teraz co h=1. Sortujemy liczby w tablicy. Posortowana tablica wygląda następująco.

Uwaga: Efekt tego sortowania jest widoczny tylko dla większej ilości elementów. Stwierdzono, Ŝe jeŜeli pierwszy krok będzie sortował co (16n/π)1/3, a drugi co 1 to

złoŜoność takiego algorytmu będzie wynosić O(n5/3). Jednak, aby uzyskać złoŜoność O(n5/4) kroki naleŜy dobierać wg zasady Knutha. Ostatni krok jest zawsze równy zero, a kaŜdy poprzedni powstaje z pomnoŜenia go przez 3 i dodania 1. Dodatkowo naleŜy dobrać pierwszy (hk) krok tak, Ŝe krok hk+2 byłby większy lub równy liczbie n (ilość elementów w tablicy).

h1=1 hi=3hi-1+1 hk (początkowe) musi być tak dobrane, aby hk+2 ≥ n, gdzie n to ilość liczb do

posortowania. Ustalanie kroku początkowego obrazuje poniŜszy przykład:

mamy n=1000 liczb. hi będzie wynosić: 1,4,13,40,121,364,1093,…

Pierwszy krok h5 dobieramy zgodnie z podanym warunkiem, a więc będzie to 121 poniewaŜ h7 jest juŜ większe od 1000 (1093). NaleŜy zauwaŜyć, Ŝe minimalna ilość liczb musi być równa przynajmniej 13. Dla mniejszej tablicy stosujemy sortowanie przez wstawianie od razu dla całej tablicy. ZłoŜoność tego algorytmu jest lepsza od O(n2), ale ciągle brakuje jej do osiągnięcia złoŜoności O(nlnn). Sortowanie przez kopcowanie (ang. heapsort) Do tego algorytmu wykorzystywana jest struktura zwana kopcem (omówiona w rozdziale Kopiec). Aby posortować elementy naleŜy wykonać szereg operacji w kolejności przedstawionej na schemacie. Algorytm ten ma złoŜoność rzędu O(nlnn).

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

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

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

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

Page 20: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

20

Schemat 1: Sortowanie przez kopcowanie.

Sortowanie szybkie – wersja standardowa oraz z loso wym elementem dziel ącym (ang. quicksort) Dzielimy daną tablicę liczb na dwie podtablice, gdzie kaŜdy element pierwszej podtablicy będzie nie większy od kaŜdego elementu drugiej. To samo robimy z kaŜdą podtablicą z osobna (rekurencyjnie) aŜ uzyskamy tablice jednoelementowe. Aby było moŜliwe podzielenie tablicy naleŜy wybrać element wg którego będziemy dokonywać podziału. MoŜna wybrać pierwszy element tablicy (tak jak w poniŜszym przykładzie), ale moŜna równieŜ wybrać losowo element z tablicy. Drugi wariant zabezpiecza nas to przed sytuacją, w której nasza tablica do posortowania będzie prawie uporządkowana. Wtedy wybranie losowego elementu spowoduje w miarę równomierny podział na podtablice. Dzięki losowemu składnikowi nie stracimy na efektywności algorytmu. Do podziału na podtablice wykorzystujemy dwa wskaźniki: niebieski i czerwony. Pierwszy przesuwamy w lewo dopóty, dopóki wskazuje na element większy lub równy elementowi dzielącemu. Czerwony wskaźnik przesuwamy w prawo aŜ nie napotka na element większy lub równy elementowi dzielącemu. Jeśli wskaźniki się nie minęły to zamieniamy miejscami elementy na które wskazują wskaźniki i przesuwamy je dalej. Jeśli się miną to tablica zostanie podzielona na dwie części. Dla kaŜdej podtablicy wykonujemy to samo.

Mamy losowy zbiór liczb.

Tworzymy z nich kopiec.

Zamieniamy element znajdujący się w korzeniu z ostatnim elementem w

kopcu. Elementy przenoszone na koniec

kopca tworzą część posortowaną (nie wchodzącą juŜ w skład kopca)

Naprawiamy pozostały kopiec (bez elementów juŜ

posortowanych).

Page 21: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

21

4 5 1 8 7 3 2

2 5 1 8 7 3 4

2 3 1 8 7 5 4

2 3 1 8 7 5 4

1 3 2 4 7 5 8

1 3 2 4 7 5 8

OK OK

2 3 4 7 5

Niebieski poza zakresem

2 3 4 7 5

OK OK OK

5 7

OK OK 1 2 3 4 5 7 8

Najgorsza sytuacja nasz czeka, gdy w kaŜdym przypadku jako element dzielący wybierzemy element najmniejszy (największy) z tablicy. Wtedy złoŜoność takiego algorytmu wyniesie O(n2). Gdy będziemy mieć szczęście to kaŜda podtablica podzieli się zawsze na pól. W tym optymistycznym wariancie złoŜoność wyniesie O(nlnn). W sytuacji pośredniej oczekiwana złoŜoność algorytmu wynosi O(nlnn).

Sortowanie przez scalanie (ang. mergesort) Sortowanie wykorzystujące rekurencję. ZłoŜoność tego algorytmu wynosi O(nlnn).

Dzielimy tablicę z liczbami na 2 podtablice. Te podtablice znów dzielimy na dwie. Czynność tą wykonujemy do uzyskania tablic jednoelementowych. Innymi słowy wykorzystujemy metodę dziel i rządź. Dzielimy problem, którego nie potrafimy rozwiązać na mniejsze. Te małe problemy są juŜ tak proste, Ŝe ich rozwiązanie nie sprawia nam problemów. Po podzieleniu tablicy scalamy te podtablice jednocześnie sortując. Sortowanie odbywa się poprzez zastosowanie pewnego schematu scalania tablic. Mam dwie podtablice: pierwsza ma 0-n elementów i jest indeksowana zmienną a, druga ma 0-m elementów i jest indeksowana zmienną b oraz tablice wynikową (scaloną) o n+m elementach – indeksowaną zmienną c.

Page 22: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

22

Schemat 2: Sortowanie przez scalanie.

Sortowanie przez zliczanie (ang. counting sort)

JeŜeli posiadamy pewne informacje na temat sortowanych elementów, np. wiemy, Ŝe liczby mogące pojawić się w ciągu są ze skończonego zbioru liczb całkowitych, to moŜemy zmniejszyć jeszcze bardziej złoŜoność algorytmu sortowania. RozwaŜmy to na przykładzie:

Mamy tablice nieposortowanych liczb. Wiemy dodatkowo, Ŝe są to liczby naturalne z przedziału <0,3>.

1 2 0 1 2 3 3 1 1

Aby posortować te liczby tworzymy dodatkową tablicę (TabPom) o dokładnie takiej liczbie komórek jak ilość liczb mieszcząca się w przedziale >∈< 3,0k . Dla naszego przykładu będzie to tablica 4 elementowa (w przypadku implementacji w C/C++ indeks tablicy odpowiada liczbie). Teraz naleŜy przebiec tablice wejściową (nieposortowaną) i zliczyć ilość wystąpień poszczególnych liczb w tej tablicy a wynik wstawić do tablicy pomocniczej pod odpowiednim indeksem.

)(][ kIlosckTabPom = W naszym przykładzie TabPom będzie wyglądać następująco:

1 4 2 2 0 1 2 3

Teraz naleŜy uzupełnić tablicę wyjściową liczbami juŜ posortowanymi. Począwszy od początku (od końca dla sortowania malejącego) wstawiamy taką ilość kaŜdej liczby jak jest to zapisane w tablicy TabPom. Otrzymamy tablicę liczb:

0 1 1 1 1 2 2 3 3

Jeśli pod tab1[a] i pod tab2[b] są liczby

Jeśli tab1[a] jest mniejsze od tab2[b]

tabw[c++]=tab1[a++] tabw[c++]=tab2[b++]

T

N

T

N

Skopiuj do tabw elementy pozostałe w tab1 lub tab2.

Page 23: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

23

ZłoŜoność takiego algorytmu sortowania jest równa O(n), co pozwala niezwykle szybko (w porównaniu do innych algorytmów sortowania) posortować dane.

Sortowanie stabilne Sortowanie stabilne to taki rodzaj sortowania, który zachowuje kolejność elementów o tej samej wartości w ciągu wejściowym i wyjściowym. Innymi słowy ma znaczenie, który z elementów o tej samej wartości będzie pierwszy. Dla omówionego wcześniej przypadku zastosujemy sortowanie stabilne. Do cyfr o tej samej wartości dopisano indeksy, które mówią o kolejności wystąpienia tych samych cyfr w tablicy wejściowej.

Przed:

1A 2A 0A 1B 2B 3A 3B 1C 1D Po:

0A 1A 1B 1C 1D 2A 2B 3A 3B Przepis implementacyjny: wyzerowanie komórek tablicy TabPom od pocz ątku tablicy wej ściowej do n-1 elementu TabPom[TabWej[i-ty_element]]++; /*Zamiana ilo ści wyst ąpie ń liczby na pozycje, pod któr ą ma si ę znale źć w TabWyn*/ TabPom[pierwszy_element]--; od drugiego elementu do ostatniego elementu tablicy TabPom TabPom[i-tego_elementu] += TabPom[(i-1)-tego_ele mentu]; od n-1 do 0 TabWyn[TabPom[TabWej[i-ty_element]]]=TabWej[i-ty _element]; TabPom[TabWej[i-ty_element]]--;

Sortowanie pozycyjne (ang. radix sort) Problem sortowania ciągów znaków lub liczb wielocyfrowych moŜna rozwiązać

wykorzystując sortowanie pozycyjne. Sortowanie to odbywa się w grupach od lewej do prawej lub odwrotnie. Jeśli chcemy posortować ciąg znaków (np. nazwiska) sortować będziemy od lewej do prawej, natomiast aby posortować liczby poruszmy się w odwrotnym kierunku.

RozwaŜamy dla przykładu liczby trzy cyfrowych, które naleŜy posortować rosnąco. Wykorzystując stabilne sortowanie przez zliczanie sortujemy względem liczb na poszczególnych pozycjach. Wbrew pozorom sortowanie rozpoczynamy od pozycji najmniej znaczących (jedności) i poruszamy się w „lewo”. W pierwszym kroku sortujemy liczby sugerując się jedynie ostatnia cyfrą, w drugim kroku przedostatnią, a w ostatnim kroku bierzemy pod uwagę jedynie pierwszą cyfrę. W ten sposób nasza tablica liczb zostanie posortowana. Warto równieŜ zauwaŜyć, Ŝe jeśli któraś z liczb ma mniejszą ilość cyfr to naleŜy z przodu uzupełnić ją zerami. Dokładny opis jednej z technik sortowania pozycyjnego opisano w punkcie sortowanie kubełkowe (ang. bucket sort).

Page 24: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

24

Przykład:

Przed sortowaniem

Po

posortowaniu jedności

Po

posortowaniu dziesiątek

Po

posortowaniu setek

436 382 816 049 628 384 327 327 336 436 628 336 816 336 436 382 496 816 336 384 382 796 049 436 049 327 382 496 327 628 384 628 384 049 496 816

ZłoŜoność takiego algorytmu wynosi kn, gdzie k to ilość cyfr w liczbie, co daje w notacji „wielkie O” złoŜoność O(n). Sortowanie kubełkowe (ang. bucket sort) Jest to metoda pozwalająca posortować liczby całkowite. Do tego celu wykorzystamy 10 kubełków ponumerowanych od 0 do 9. Do kaŜdego będziemy wkładać odpowiednio liczby. Na początku umieszczamy liczby w kubełkach sugerując się cyfrą jedności. Przykładowo, jeśli cyfra ta jest równa 0 to liczba taka trafi do kubełka zerowego, gdy 1 to do kubełka pierwszego, itd. WaŜna jest jednak kolejność liczb w kubełku. Muszą on bowiem być tam umieszczane w kolejności wystąpienia w ciągu. Dlatego podczas implementacji kubełka naleŜy skorzystać z kolejki. W ten sposób wejściowa względna kolejność liczb zostanie zachowana. Następnie wyciągamy elementy z kubełka i układamy w ciąg. WaŜne jest aby wyciągać je począwszy od kubełka zerowego aŜ po kubełek dziewiąty. Uzyskany ciąg liczb znów rozrzucamy do kubełków, ale juŜ względem kolejnej cyfry (dziesiątek). Operacje powtarzamy k razy (k to ilość cyfr w najdłuŜszej liczbie). RozwaŜmy ciąg liczb: 436, 124, 068, 348, 741, 041, 002, 675, 943, 179. Krok 1:

0

041 741

1

002

2

943

3

124

4

675

5

436

6

7

348 068

8

179

9

Otrzymamy: 741, 041, 002, 943, 124, 675, 436, 068, 348, 179

Page 25: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

25

Krok 2:

Krok 3:

Najlepiej wykorzystać do zaimplementowania kolejki zrealizowane na tablicy (patrz

punkt Kolejka), gdyŜ przyspiesza ona szybkość wykonywania. W tego typu implementacji nie jest marnowany czas na operacje związane z tworzeniem i destrukcją kolejek – raz utworzone kolejki pozostają. Jedynie są wykonywane operacje związane z przenoszeniem danych między kolejkami. Ta wersja nie sprawdza się w przypadku duŜych ciągów elementów, gdyŜ niezbędne jest k (ilość pozycji w elemencie – np. cyfr) tablic o rozmiarze n (ilość elementów) oraz tablica wejściowa z danymi. ZłoŜoność tego algorytmu jest rzędu O(n).

Algorytmy rekurencyjne Czasami problem moŜna rozłoŜyć na coraz to prostsze, których rozwiązanie nie

wymaga wysiłku lub wymaga bardzo małego wysiłku. Rozwiązujemy te proste problemy wracając się do coraz trudniejszych, aŜ okaŜe się, Ŝe główny (wyjściowy) problem został rozwiązany. Nazywa się to metodą dziel i rządź. Metoda ta, z pozoru moŜe dziwna, często okazuje się bardzo skuteczna, a niekiedy nawet jedyna.

068 041 002

0

179 124

1

248

2

3

436

4

5

675

6

741

7

8

943

9

Otrzymamy: 002, 041, 068, 124, 179, 436, 675, 741, 943

002

0

1

124

2

436

3

348 943 041 741

4

5

068

6

179 675

7

8

9

Otrzymamy: 002, 124, 436, 741, 041, 943, 348, 068, 675, 179

Page 26: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

26

Proste algorytmy rekurencyjne

Silnia Do obliczania silni moŜna wykorzystać rekurencję. Aby obliczyć silnię z n wystarczy obliczyć silnię z n-1 i pomnoŜyć przez n. Następnie do obliczenia silni z n-1 znów rozkładamy to na n-2 silnia razy n-1. W końcu dojedziemy do sytuacji, w której trzeba będzie obliczyć silnie z 1, a to jest właśnie ten prostszy problem, który nie wymaga od nas wysiłku – silnia z 1 jest równa jeden.

n! = (n-1)! * n Przykład: Mamy obliczyć silnię z 5.

• 5!=(5-1)! * 5 • (5-1)! = 4! = (4-1)! * 4

• (4-1)! = 3! = (3-1)! * 3 • (3-1)! = 2! = (2-1)! * 2

• (2-1)! = 1! = 1 Zagłębiamy się tak długo, aŜ dojdziemy do problemu, który potrafimy rozwiązać (na niebiesko). Następnie wracamy się powrotem. Wiedząc ile wynosi 1! umiemy obliczyć 2!. Wiedząc ile wynosi 2! Umiemy obliczyć 3! itd. W ten sposób udało się obliczyć silnie z 5.

NWD – najwi ększy wspólny dzielnik (algorytm Euklidesa) Problem rozwiązujemy podobnie jak w przypadku silni –rekurencyjnie. NaleŜy

skorzystać tutaj z wzoru: NWD(x,y) = NWD(y,x % y) dla x>y NWD(y,x) = NWD(x,y % x) dla y>x

%- dzielenie modulo. Rozkładamy problem na mniejsze według tego wzoru do mementu, aŜ gdy drugi z argumentów osiągnie wartość zero. Przykład: Mamy obliczyć NWD dla 26 i 8.

• Obliczamy NWD dla 8 i (26%8)=2 • Obliczamy NWD dla 2 i (8%2)=0

Gdy druga z liczb będzie równa zero to pierwsza jest NWD. Zatem NWD dla 26 i 8 wynosi 2.

Wypisywanie wyrazu od ko ńca Kolejny problem, który pomoŜe nam rozwiązać rekurencja. Aby rozwiązać to zadanie

rozbijamy je na dwie części: próbujemy wypisać w odwrotnej kolejności wszystko z wyjątkiem pierwszego znaku oraz na końcu wypisujemy wspomniany pierwszy znak wyraŜenia. Jak wypisać wszystko z wyjątkiem pierwszego znaku. Znów robimy tak samo. Przykład: Wypisz od końca słowo: „notatka”

• wypisujemy od końca „otatka” i na końcu „n” • wypisujemy od końca „tatka” i na końcu „o”

• wypisujemy od końca „atka” i na końcu „t” • …

• wypisujemy od końca „a” i na końcu „k”

Page 27: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

27

Rozbijając problem w ten sposób moŜna go w bardzo prosty sposób wykonać. Nie sprawia nam problemu wypisanie pojedynczego znaku od końca, a na takie właśnie części rozłoŜyliśmy nasze zadanie.

Algorytmy zachłanne (ang. greek algorithm) Algorytmy te nie przewidują tego, co będzie „potem”. Interesuje ich tylko „teraz”.

Innymi słowy postępują zachłannie. Takie działanie nie zawsze okazuje się najlepsze, ale bywa teŜ idealne.

Problem kasjera Mając następujący system monetarny: 1, 2, 5, 10, 20, 50 wydajemy resztę w taki

sposób, aby stracić jak najmniej monet. Postępując zgodnie z zasadami algorytmu zachłannego za kaŜdym razem będziemy wydawać monety o największym nominale. Doprowadzi nas to do optymalnego rozwiązania – wydamy najmniejszą ilość monet. Przykład: Wydać 79 złotych.

• wydajemy 50, zostaje 29zł • wydajemy 20, zostaje 9zł (gdyŜ 50 się nie da – nie chcemy być stratni ) • wydajemy 10, zostaje 9zł • wydajemy 5, zostaje 4zł • wydajemy 2, zostaje 2zł • wydajemy 2, zostaje 0zł

Stosując powyŜszą metodę wydaliśmy 6 monet. Odpowiednie dobranie nominałów w systemie monetarnym okazuje się nie bez znaczenia. Zastanówmy się co by było, gdybyśmy posługiwali się systemem monetarnym: 1, 10, 20, 25. W tym przypadku algorytm zachłanny nie zadziała optymalnie. Przykład: Mając system: 1, 10, 20, 25. Wydać 31 złotych.

• wydajemy 25, zostaje 6zł • wydajemy 1, zostaje 5zł • wydajemy 1, zostaje 4zł • wydajemy 1, zostaje 3zł • wydajemy 1, zostaje 2zł • wydajemy 1, zostaje 1zł • wydajemy 1, zostaje 0zł

Wydaliśmy 7 monet, co nie jest minimalną liczbą monet. MoŜna było wydać 20 + 10 + 1 co daje trzy monety. W tym przypadku algorytm zachłanny nie zadziałał optymalnie.

Szukanie minimum funkcji Problem ten moŜna rozwiązać na wiele sposobów. Jeden z nich polega na obliczeniu wartości funkcji dla pewnej liczby (argument od którego rozpoczniemy poszukiwanie) i następnie obliczaniu kolejnych wartości dla argumentów powiększonych o pewien krok. Robimy to dopóty, dopóki wartości funkcji się zmniejszają. Jeśli kolejna wartość będzie większa od poprzedniej, to cofamy się do poprzedniego kroku – to jest nasze minimum lokalne funkcji.

Page 28: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

28

Strategia ”dziel i rz ądź” PoniŜej jeszcze kilka innych algorytmów wykorzystujących metodę dziel i rządź. Wszystkie omówione problemy udało się rozwiązać dzięki rozłoŜeniu ich na mniejsze, prostsze.

Wyznaczanie warto ści maksymalnej NaleŜy znaleźć wartość maksymalną w ciągu liczb. Rozwiązanie problemu

rozpoczynamy od podziału ciągu na 2 równe (ewentualnie zbliŜone – nieparzysta ilość liczb) części. W kaŜdej z nich szukamy minimum i następnie z tych 2 minimów wybieramy jedno (mniejsze – Ŝeby nie było wątpliwości ). Jak znaleźć minimum w podtablicach? Tak samo jak w tablicy głównej. Znów kaŜdą podtablice dzielimy na 2 części. I tak, aŜ do uzyskania tablic jedno elementowych. Znalezienie minimum w podtablicy jedno elementowej nie jest trudne . Wykorzystując rekurencje i dzieląc problem na mniejsze osiągniemy cel.

Linijka Kolejny problem, który przy pomocy rekurencji moŜna łatwo i szybko rozwiązać. Chodzi o to, aby otrzymać efekt jak na poniŜszym rysunku.

Najpierw dzielimy odcinek na pół. Potem kaŜdą z połówek znów dzielimy na pól, przy czym podziałka ma juŜ proporcjonalnie mniejszą długość. To samo robimy z kaŜdą ćwiartką, itd. Operacje wykonujemy rekurencyjnie. NaleŜy ograniczyć wywołania rekurencyjne, aby umoŜliwić zakończenie podziału. Dla powyŜszego rysunku wykonaliśmy podział stopnia czwartego. Długość odcinka dzielącego moŜna uzaleŜnić od kroku (stopnia zagłębienia) rekurencji.

WieŜa Hanoi Problem polega na tym, aby przełoŜyć n krąŜków z słupka A na słupek B w taki

sposób, aby w trakcie tej operacji większy krąŜek nigdy nie został połoŜony na mniejszym. KrąŜki naleŜy przekładać pojedynczo. Operację tę znów sprowadzamy to prostszych problemów. Przenosimy n-1 krąŜków (wszystkie z wyjątkiem ostatniego - największego) na wieŜe B, przekładamy największy krąŜek na wieŜe C. Aby przenieś pozostałe n-1 krąŜków znów dzielimy problem podobnie. Operacje jakie naleŜy wykonać, aby przenieś n krąŜków opisuje poniŜszy pseudo kod. Przepis implementacyjny: przenie ś (n_kr ąŜków, od_wie Ŝy, do_wie Ŝy, tmp_wie Ŝa) je śli s ą kr ąŜki przenie ś(n-1_kr ąŜków,od_wie Ŝy,tmp_wie Ŝa,do_wie Ŝy) przełó Ŝ kr ąŜek od_wie Ŝy do_wie Ŝy przenie ś(n-1_kr ąŜków,tmp_wie Ŝa,do_wie Ŝy,od_wie Ŝa)

Page 29: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

29

Ciąg liczb Fibonacciego Problem polega na obliczeniu n kolejnych liczb tzw. Ciągu Fibonacciego. Jest to ciąg, w którym pierwsza liczba jest równa 0, a druga 1. KaŜda kolejna powstaje z sumy dwóch poprzednich liczb. 0, 1, 1, 2, 3, 5, 8, 13, 21,… Problem ten rozwiązujemy wykorzystując rekurencję. Warto jednak zauwaŜyć, Ŝe obliczając kolejne liczby ciągu powtarzamy niektóre obliczenia. PoniŜszy schemat obrazuje operacje jakie naleŜy wykonać, aby obliczyć 5 liczbę ciągu. Fragmenty zakreślone przerywaną linią oznaczają podwójnie liczone wyraŜenia. Im dalsza liczba ciągu, tym powtarzanych operacji obliczeniowych jest coraz więcej.

Warto by się zastanowić, czy nie da się tego uniknąć. Rozwiązaniem jest zastosowanie programowania dynamicznego. Obliczając kolejne elementy ciągu zapamiętujemy wyniki, aby w przyszłości (gdy znów w kolejnych obliczeniach będą one nam potrzebne) móc z nich skorzystać.

Programowanie dynamiczne

Ciąg liczb Fibonacciego Zastosowanie programowania dynamicznego do rozwiązania tego problemu zostało opisane w punkcie Ciąg liczb Fibonacciego w dziale Strategia „dziel i rządź”.

Liczba kombinacji – trójk ąt Pascala Problem polega na obliczenie liczby kombinacji, czyli ilości moŜliwości wyboru r-elementowych podzbiorów z n-elementowego zbioru. Matematycznie obliczenia takie moŜna wykonać korzystając ze wzoru:

)!(!

!

rnr

n

r

n

−⋅=

5

4 3

3 2 2 1

2 1 1 1 0 0

1 0

A B C

Page 30: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

30

Do rozwiązania tego problemu uŜyjemy jednak innych wzorów i zaleŜności. Przedstawione są one poniŜej.

1=

r

n, jeśli r=0 lub n=r

−−+

−=

1

11

r

n

r

n

r

n, dla 0<r<n

Korzystając z powyŜszych informacji moŜna zaimplementować algorytm pozwalający w sposób rekurencyjny obliczyć liczbę kombinacji n po r. Warto zauwaŜyć, Ŝe takŜe w tym przypadku (podobnie jak miało to miejsce przy obliczaniu liczb Fibonacciego) niektóre obliczenia będą wykonywane kilka razy. Warto więc wykorzystać idee programowania dynamicznego. Obliczone wartości n po r zapamiętujemy w dwuwymiarowej tablicy, w której indeks wierszy odpowiadać będzie wartościom n, a indeks kolumn – wartościom r.

Tablice tą na starcie uzupełniamy informacjami, które juŜ znamy, tzn. wiemy, Ŝe 10

=

n i

1=

n

n. Pozostałe wartości będą uzupełniane w trakcie kolejnych obliczeń.

Problem pakowania plecaka (ang. knapsack problem) Problem polegam na tym, aby spakować plecak x-kilogramowy rzeczami, które mają

swoją wagę i wartość w taki sposób, aby spakowane rzeczy były najbardziej wartościowe. Suma wag wszystkich przedmiotów nie moŜe przekraczać pojemności plecaka. Zakładamy takŜe, Ŝe mamy nieskończenie wiele rzeczy tego samego typu.

Gdybyśmy mogli dzielić pakowane rzeczy to problem byłby banalny. Wystarczyłoby zapakować cały plecak tym samym rodzajem przedmiotów, których stosunek wartości do wagi jest maksymalny. Jednak my nie moŜemy podzielić naszych rzeczy (nie da się spakować jednej nogawki spodni, bo po co nam to ).

RozwaŜmy przykład: Mamy 18 kg plecak i trzy rzeczy:

Pierwsza o wartości 10zł i wadze 10 kg; Druga o wartości 4zł i wadze 5 kg; Trzecia o wartości 3zł i wadze 4 kg.

Dane:

przedmiot p1 przedmiot p2 przedmiot p3 cena (zł) 10 4 3 waga (kg) 10 5 4 cena/waga 1 0.8 0.75

MoŜemy plecak spakować na dwa przykładowe sposoby:

Pierwszy, gdy będziemy pakować zachłannie, tzn. będziemy wkładać do plecaka rzeczy o największym stosunku c/w. wkładamy rzecz p1, pozostaje nam 8 kg wolnego miejsca; wkładamy rzecz p2, pozostaje nam 3 kg wolnego miejsca; nie moŜemy juŜ włoŜyć nic więcej; nasz plecak ma wartość 10 zł + 4 zł = 14 zł;

Drugi sposób, który rozwaŜymy to pakowanie, które nie opiera się na algorytmie zachłannym.

Page 31: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

31

wkładamy rzecz p1, pozostaje nam 8 kg wolnego miejsca; wkładamy rzecz p3, pozostaje nam 4 kg wolnego miejsca; wkładamy rzecz p3, pozostaje nam 0 kg wolnego miejsca; plecak jest pełen; nasz plecak ma wartość 10 zł + 3 zł + 3 zł = 16 zł.

Nie trudno zauwaŜyć, Ŝe lepiej zabrać plecak drugi (ma większą wartość). Jak to rozwiązać algorytmicznie w programie? Najlepiej w tym celu wykorzystać rekurencję oraz programowanie dynamiczne. Zaczynamy od plecaka 1 kg. Taki plecak jest bardzo łatwo spakować. Następnie pakujemy plecak 2kg, 3kg i tak, aŜ dojdziemy do plecaka x kilogramowego. Aby spakować plecak, np. 8kg, moŜemy wybrać rzecz:

5kg i to, co moŜemy spakować do plecaka 3 kilogramowego (8-5=3); 4kg i to, co moŜemy spakować do plecaka 4 kilogramowego (8-4=4);

waga plecaka co wkładamy (w kg) wartość plecaka (zł) 1 -------------- 2 -------------- 3 -------------- 4 4 (3zł) 3

5 (4zł) 4 5

4 (3zł) + 1. 3 5 (4zł) + 1. 4

6 4 (3zł) + 2. 3 5 (4zł) + 2. 4

7 4 (3zł) + 3. 3 5 (4zl) + 3. 4

8 4 (3zł) + 4. 6 5 (4zł) + 4. 7

9 4 (3zł) + 5. 7 10 (10zł) 10 5 (4zł) + 5. 8 10 4 (3zł) + 6. 7 10 (10zł) + 1. 10 5 (4zł) + 6. 8 11 4 (3zł) + 7. 7 10 (10zł) + 2. 10 5 (4zł) + 7. 8 12 4 (3zł) + 8. 9 10 (10zł) + 3. 10 5 (4zł) + 8. 10 13 4 (3zł) + 9. 10 10 (10zł) + 4. 13 5 (4zł) + 9. 11 14 4 (3zł) + 10. 13 10 (10zł) + 5. 14 5 (4zł) + 10. 14 15 4 (3zł) + 11. 13 10 (10zł) + 6. 14 5 (4zł) + 11. 14 16 4 (3zł) + 12. 13 10 (10zł) + 7. 14 17 5 (4zł) + 12. 14

Page 32: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

32

4 (3zł) + 13. 13 10 (10zł) + 8. 16 5 (4zł) + 13. 14 18 4 (3zł) + 14. 16

Na czerwono oznaczyłem pojemność plecaka, którego opcje pakowania trzeba wybrać dodatkowo(np. plecak 5kg to 4kg i to co wkładamy do 1kg plecaka). Gdy dla danego plecaka mamy kilka opcji do wyboru to wybieramy lepszą, opcje na niebieskim tle.

Rozkład na czynniki pierwsze liczb naturalnych (0-1 000) Problem rozkładu liczb naturalnych z przedziału od 0 do 1000 na czynniki pierwsze rozwiązujemy takŜe korzystając z rekurencji i programowania dynamicznego. Rozkładając kolejne liczby naturalne zapamiętujemy wynik tak, aby później móc z niego korzystać. Do implementacji moŜna wykorzystać tablice 1001 elementową (od 0 do 1000) wskaźników do list przechowujących czynniki pierwsze, na które rozkładamy daną liczbę. KaŜda lista będzie tak naprawdę przechowywała jedną liczbę pierwszą, która wskazywać będzie na odpowiedni indeks innej liczby w tablicy, jako na kolejne elementy listy.

Przykładowo, liczbę 12 rozkładamy na 2 razy to na, co rozłoŜyliśmy liczbę 6. Sześć to: 2 razy to, na co rozłoŜyliśmy 3. PoniewaŜ liczba trzy jest liczbą pierwszą, więc to juŜ jest koniec listy.

Metoda powrotów – ”prób i bł ędów” (ang. backtracking) Aby rozwiązać problem w trakcie jego rozwiązywania dokonujemy pewnych decyzji. Wybieramy pewną drogę, którą będziemy dalej się poruszać. MoŜe się zdarzyć sytuacja, w której nasz wybór będzie zły i doprowadzi nas to do braku rozwiązania – nie osiągniemy celu. Wtedy naleŜy cofnąć ostatnią decyzję i wybrać inną drogę, która być moŜe doprowadzi nas do celu. Taka metoda rozwiązywania problemu określana jest mianem metody powrotów. Aby móc dokonać wspomnianych powrotów naleŜy zapamiętywać ruchy (decyzje), których dokonaliśmy wcześniej, aby móc do nich powrócić. Rozwiązać to moŜna odkładając informacje o kolejnych naszych posunięciach na stos. MoŜna wykorzystać takŜe implementację powrotów wykorzystującą rekurencję.

Problem skoczka szachowego Zadanie polega na przeskoczeniu skoczkiem szachowym wszystkich pól szachownicy. Zakładamy, Ŝe na kaŜdym polu moŜemy stanąć tylko raz. Skoczek szachowy moŜe ośmiu danego miejsca wykonać skok w jednym z ośmiu kierunków. MoŜliwości ruchu skoczka pokazano na poniŜszym rysunku.

7 6 8 5 start 1 4 2 3

Zatem skacząc skoczkiem po planszy sprawdzamy, czy juŜ tu byliśmy, jeśli nie to stawiamy skoczka w tym miejscu, jeśli tak, to naleŜy wycofać się z poprzedniego kroku ośmiu wybrać inną moŜliwość. Podczas implementacji naleŜy wykorzystać rekurencję. Szachownicę moŜemy zaimplementować jako tabelę ośmiu wymiarach n+4 na n+4 (gdzie n to wymiary planszy szachownicy). KaŜde pole będzie przechowywać wartość 0 – gdy jeszcze na nim nie stanęliśmy i 1- gdy juŜ postawiliśmy tu skoczka. Dodatkowe cztery wiersze i kolumny to tzw. Otoczka zabezpieczająca. Poruszając się po wierszach i kolumnach tablicy naleŜy zadbać o to, aby nie wyjść poza obszar pamięci przydzielony dla tej tablicy. Jednym z

Page 33: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

33

rozwiązań jest sprawdzanie przed kaŜdym skokiem, czy ma to miejsce. Drugim (które tu zostało opisane) polega na otoczeniu naszej „szachownicy” otoczką o grubości dwóch wierszy (kolumn). Komórki tej otoczki będą juŜ na starcie przechowywały wartości jeden, co uniemoŜliwi skok na te pola. PoniŜej przedstawiono zawartość tabeli dla szachownicy 4 na 4 przed rozpoczęciem skoków.

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Przepis implementacyjny: skocz_na_pozycje(x,y) je śłi jeszcze nie tu nie byłe ś inkrementuj ilo ść odwiedzonych pól; zaznacz swoj ą obecno ść wstawiaj ąc do komórki 1; je śli ilo ść odwiedzonych pól jest równa ilo ści pol na szachownicy znaleziono rozwi ązanie; koniec; //rekurencyjne wywołanie funkcji skocz_na_pozycje (x-1,y-2); skocz_na_pozycje (x+1,y+2); skocz_na_pozycje (x-1,y+2); skocz_na_pozycje (x+1,y-2); skocz_na_pozycje (x-2,y-1); skocz_na_pozycje (x-2,y+1); skocz_na_pozycje (x+2,y-1); skocz_na_pozycje (x+2,y+1); //gdy rekurencja powróci zmniejsz ilo ść odwiedzonych pół; wyma Ŝ swoj ą obecno śc (wstaw do komórki 0)

Problem o śmiu hetmanów Zadanie polega na rozstawieniu na szachownicy 8 hetmanów w taki sposób, aby Ŝaden z nich nie znajdował się w tej samej kolumnie, wierszu i na tej samej przekątnej co inny hetman. Jeśli przyjmiemy, Ŝe kaŜdy hetman będzie stał w innej kolumnie, to dla kaŜdego z nich mamy 8 moŜliwości ustawienia (poniewaŜ mamy 8 wierszy do wyboru). Rozpoczynamy stawiając pierwszego hetmana w pierwszym wierszu, następnie przechodzimy do drugiej kolumny i próbujemy wstawić drugiego hetmana. Nie moŜna tego zrobić w pierwszym ani drugim wierszu, więc stawiamy go w trzecim. To samo robimy z kaŜdym kolejnym hetmanem. Jeśli okaŜe się, Ŝe któregoś z nich nie da się ustawić w Ŝadnym wierszu, musimy wycofać się z ostatniego ruchu – musimy powrócić. Zdejmujemy poprzednio postawionego hetmana i szukamy dla niego kolejnego moŜliwego wiersza. PoniŜszy pseudo kod pozwala ustawić n hetmanów na planszy o wymiarach n na n.

Page 34: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

34

Przepis implementacyjny: //Funkcj ę wywołamy w p ętli od i=0 do n-1, gdzie n oznacza rozmiar planszy. wstaw_hetmana(w i-tej kolumnie) od j=0 do j=n-1 je śli mo Ŝna wstawi ć hetmana na pozycji (j,i) połó Ŝ tam hetmana; wywołaj funkcj ę wstaw_hetmana(i + 1); zdejmij hetmana(gdy rekurencja „powróci”);

Coś na deser Problemów, których rozwiązanie moŜna znaleźć rekurencyjnie jest wiele. Oprócz omawianych powyŜej są takŜe takie, które umoŜliwiają uzyskanie wielu ciekawych dla oka i nie tylko efektów. PoniŜej opisano w skrócie na czym polegają te najbardziej popularne.

Krzywa Kocha (płatek śniegu) Problem polega na narysowaniu krzywej, która powstaje według pewniej zasady. Na początku mam odcinek. Dzielimy go na trzy części i w miejscu środkowej części rysujemy trójkąt równoboczny bez podstawy. Następnie z kaŜdym z czterech odcinków robimy to samo. Potem znów kaŜdy z odcinków dzielimy według tej zasady. Takie operacje podziału moŜemy wykonywać n-krotnie, co doprowadzi nas do uzyskania całkiem ciekawych efektów wizualnych. Podziału tego moŜemy dokonać takŜe dla kaŜdego z boków dowolnej figury. Uzyskane efekty będą jeszcze bardziej zachwycające.

Trójk ąt i dywan Sierpi ńskiego Problem polega na podziale trójkąta równobocznego na mniejsze, jednakowe trójkąty. Dodatkowo trójkąty narysowane „do góry nogami” będą namalowane w innym kolorze. W ten sposób uzyskamy trójkąt wypełniony pewnego rodzaju mozaiką. Operacje rozpoczynamy od podziału wyjściowego trójkąta na cztery mniejsze. W tym celu dzielimy kaŜdy z boków trójkąta na pół i łączymy te punkty środkowe boków. Dla kaŜdego z powstałych w ten sposób czterech trójkątów znów wykonujemy te samą operację. Czynność powtarzamy n razy.

Krzywa Hilberta Problem polega na narysowaniu krzywej (właściwie łamanej) powstającej według określonego wzoru. Wyjściowym elementem jest łamana o poniŜszym kształcie.

Page 35: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

35

Łącząc odpowiednio takie elementy ze sobą uzyskamy krzywą o specyficznym kształcie. MoŜna tego rodzaju krzywą wykorzystać np. do pseudolosowego przebiegania po pewnym obszarze jak to ma miejsce w przypadku rozsiewania błędów podczas redukcji obrazu do 256 kolorów. PoniŜej pokazano przykłady krzywych róŜnego rzędu (po lewej- 2 rzędu, po prawej 4-rzędu).

Zliczanie białych obszarów i liczby białych pól w k aŜdym obszarze Problem polega na zliczeniu ilości zakreślonych pól na planszy oraz ilości zakreślonych obszarów – grup połączonych ze sobą pól. Niech nasza plansza będzie przedstawiona w postaci tablicy n na n oraz 0 oznacza nie zakreślone pole, 1- zakreślone, a 2- zliczone. Dodatkowo tablica musi mieć tzw. otoczkę , czyli dodatkowy wiersz(kolumnę) na górze (po lewej) i na dole (po prawej). Otoczka ta uniemoŜliwi wyjście poza obszar pamięci przeznaczony dla tablicy z planszą. Problem takŜe rozwiązujemy rekurencyjnie a pseudokod (oraz przykładową planszę) pozwalający tego dokonać przedstawiono poniŜej.

2 2 2 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 2 2 0 1 1 1 0 0 1 0 2 2 0 0 0 1 0 0 1 0 2 2 0 0 0 0 0 0 1 0 2 2 0 1 1 1 0 0 1 0 2 2 0 0 0 1 0 0 1 0 2 2 0 0 1 1 0 0 0 0 2 2 0 0 0 0 0 0 0 0 2 2 2 2 2 2 2 2 2 2 2

Page 36: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

36

Przepis implementacyjny: //Deklarujemy funkcj ę ile(i,j), gdzie i/j to nr wiersza/kolumny. od i=1 do i=n-2 od j=1 do j=n-2; wywołujemy funkcj ę ile(i,j) je śli nie byli śmy jeszcze na tym polu skre ślamy obszar (wstawiamy 2) aby zaznaczy ć swoj ą obecno ść; je śli nasze pole nie jest obszarem zakre ślonym to zwracamy 0 je śłi pole jest obszarem zakre ślonym to zwracamy 1 + ile(i-1,j) + ile(i+1,j) + ile (i,j-1) + ile(i,j+1);

Drzewo binarne – implementacja podstawowych operacj i Drzewo to struktura umoŜliwiająca przechowywanie danych zbudowana z węzłów oraz wskaźników do potomków. Drzewa rysuje się w sposób odwrotny, niŜ rosną one w przyrodzie . Na szczycie struktury znajduje się korzeń – węzeł nie mający ojca, a na samym dole umiejscowione są liście- nie posiadające synów. Odmianą drzew są drzewa binarne. Są to struktury, w których kaŜdy węzeł posiada dwóch synów – lewego i prawego. Rozmieszczenie elementów w drzewie nie jest przypadkowe. Wszystkie elementy znajdujące się w lewym poddrzewie są mniejsze od swojego ojca, natomiast elementy prawego poddrzewa są większe od ojca. Reguła ta obowiązuje wszystkie poddrzewa. Przykładowo: wszystkie elementy lewego poddrzewa piętnastki są mniejsze od niej, to samo ma miejsce dla lewego poddrzewa siódemki oraz 4. Te same reguły obowiązują takŜe dla wszystkich prawych poddrzew – wartości w nich przechowywane muszą być większe od ojca.

Dodawanie elementu Aby wstawić nowy element do drzewa naleŜy począwszy od korzenia porównywać dany element z węzłem i jeŜeli jest on mniejszy od wartości przechowywanej w tym węźle to poruszać się w lewo po drzewie, w przeciwnym wypadku poruszać się w prawo. Wędrujemy tak długo, aŜ dojdziemy do miejsca, w którym napotkany wskaźnik do potomka w węźle będzie wskazywał na NULL. Wtedy wstawiamy nowy węzeł w to miejsce, a wspomniany wskaźnik ustawiamy tak, aby wskazywał na nowy węzeł.

15

7 30

4 13 25 34

28 2

Page 37: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

37

Przykład: do poniŜszego drzewa wstawiamy liczbę 23. Niebieską strzałką zaznaczono kierunek poruszania się po drzewie.

Wyszukiwanie elementu, element minimalny i maksymal ny Przeszukiwanie drzewa binarnego jest bardzo podobne do wstawiania. Rozpoczynając od korzenia porównujemy wartość napotkanego węzła z szukaną liczbą. Gdy to nie jest szukany element to, jeśli szukana liczba jest mniejsza od tej przechowywanej w węźle to idziemy w lewo, w przeciwnym wypadku poruszamy się w prawo. Pomijanie podczas przeszukiwania niektórych poddrzew moŜliwe jest dzięki wspomnianemu juŜ wcześniej specjalnemu rozmieszczeniu elementów w drzewie. JeŜeli przejdziemy całe drzewo (natkniemy się na NULL) i Ŝadna napotkana wartość nie odpowiadała naszej liczbie, to oznacza to, Ŝe liczby nie ma w szukanym węźle. Przykład: przeszukujemy drzewo dwa razy. Za pierwszym razem szukamy liczby 13, za drugim razem sprawdzamy czy jest w nim liczba 22. Niebieska strzałka to kierunek poruszania się po drzewie w poszukiwaniu 13, czerwona- w poszukiwaniu 22. Ja się okaŜe, liczby 22 nie będzie w drzewie.

JeŜeli chcemy znaleźć element minimalny (maksymalny) w drzewie to poruszamy się maksymalnie w lewo (prawo). Na poniŜszym rysunku niebieska strzałka to poszukiwanie minimum, czerwona- maksimum. Specyficzne rozmieszczenie elementów w drzewie sprawia, Ŝe element minimalny znajduje się zawsze w najbardziej „wysuniętym na lewo” węźle, a element maksymalny w najbardziej „wysuniętym na prawo” węźle drzewa.

15

7 30

4 13 25 34

28 2 23

15

7 30

4 13 25 34

28 2 23

Page 38: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

38

Usuwanie elementu (3 przypadki) NaleŜy tutaj omówić trzy moŜliwe przypadki, jakie mogą wystąpić podczas

usuwania elementu z drzewa binarnego. Pierwszy, to taki, kiedy usuwany węzeł nie ma potomstwa. Jest to najprostsza sytuacja. NaleŜy usunąć ten element (zwolnić pamięć) i zadbać o to, aby jego rodzic wskazywał teraz na Null. Przykład: usuwamy węzeł przechowujący wartość 2.

Kolejna sytuacja to taka, w której usuwany węzeł posiada jednego potomka. Tutaj naleŜy zadbać (oprócz zwolnienia pamięci) o to, aby rodzic usuwanego elementu wskazywał teraz zamiast na usuwany element na jego potomka.

15

7 30

4 13 25 34

28 2 23

15

7 30

4 13 25 34

28 2 23

Page 39: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

39

Przykład: usuwamy węzeł przechowujący wartość 30.

Ostatnia z moŜliwych sytuacji, to ta, w której usuwany węzeł posiada aŜ dwóch potomków. Aby usunąć taki węzeł naleŜy zamienić wartość z tego węzła z wartością minimalną w prawym poddrzewie usuwanego węzła (na rysunku otoczone przerywaną linią) lub z wartością maksymalną w lewym poddrzewie. Następnie usuwamy element minimalny w prawym poddrzewie, ewentualnie maksymalny w lewym poddrzewie. Ta operacja juŜ została opisana w dwóch poprzednich przypadkach. Po takiej zamianie element minimalny (maksymalny) nie będzie miał potomstwa lub co najwyŜej będzie miał jedynie prawego (lewego) syna. Przykład: usuwamy węzeł przechowujący wartość 7. Najpierw szukamy elementu minimalnego w prawym poddrzewie.

15

7 30

4 13 25

28 14 10

15

7 30

4 13 25

28 23

Page 40: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

40

Następnie zamieniamy usuwany element z tym znalezionym.

Teraz usuwamy element minimalny (siódemkę) w prawym poddrzewie.

Przechodzenie drzewa w gł ąb i wszerz Aby wypisać elementy znajdujące się w drzewie moŜna skorzystać z jednej z dwóch metod przechodzenia drzewa: w głąb lub wszerz.

Pierwsza z nich to metoda zrealizowana rekurencyjnie. Najpierw zostanie wypisane lewe poddrzewo, a następnie prawe. Istnieje sześć metod realizacji tego wypisywania. RóŜnią się one kolejnością wykonywanych operacji. JeŜeli wypisywanie to jest realizowane rekurencyjnie to naleŜy wykonać trzy operacje: wypisać węzeł, wywołać funkcję dla lewego potomka, wywołać funkcję dla prawego potomka. Kolejność wykonania tych operacji będzie decydowała o sposobie wypisania danych.

Sześć metod wypisywania (W- wypisz węzeł, P- wywołaj funkcję dla prawego syna, L- dla lewego syna):

o WLP o WPL o LWP o PWL o LPW o PLW

15

10 30

4 13 25

28 14 7

15

10 30

4 13 25

28 14 7

Page 41: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

41

Warto zwrócić uwagę na dwie metody: LWP i PWL. Pierwszy z nich wypisze wartości rosnąco, drugi malejąco. Jest to zgodne z zasadami, z jakimi wywoływana jest rekurencja (patrz rekurencja).

Stosując drugą metodę elementy będą wypisywane poziomami. Najpierw zostaną wypisane elementy z najwyŜszego poziomu (korzeń), potem kolejnego, a na końcu liście (ostatni poziom). Wykorzystujemy do tego celu kolejkę. Na początku wrzucamy do kolejki korzeń. Następnie wyciągamy korzeń (wypisujemy go) z kolejki wstawiamy do niej potomstwo korzenia. Czynność wyciągania węzła z kolejki i wrzucania jego potomstwa powtarzamy dopóty, dopóki w kolejce coś się znajduje. Jeśli wyciągany element nie posiada potomstwa to do kolejki nic nie zostanie dodane.

Równowa Ŝenie drzewa Często bywa tak, Ŝe drzewo binarne jest niezrównowaŜone. Drzewo zrównowaŜone

to takie, które ma wypełnione wszystkie poziomy maksymalnie z wyjątkiem być moŜe ostatniego poziomu. Tam elementy są rozlokowane od lewej strony, ale niekoniecznie wypełniają cały poziom. Przykład takiego drzewa ilustruje poniŜszy rysunek.

Drzewo zrównowaŜone umoŜliwia znacznie szybsze jego przeszukiwanie. W najgorszym przypadku nasze drzewo mogło by mieć kształt listy. Wtedy, aby przeszukać taką strukturę naleŜy sprawdzić wszystkie n węzłów drzewa. W najlepszej sytuacji, gdy drzewo będzie

idealnie zrównowaŜone wystarczy sprawdzić )1(log2 +n węzłów. Nie trudno

zauwaŜyć, Ŝe róŜnic w ilości porównań jest znaczna. Dla przykładu podam, Ŝe gdy nasze drzewo miało by 10 000 elementów to w najgorszym przypadku naleŜało by sprawdzić właśnie 10 000 węzłów, a w najlepszym jedynie 14.

Metoda z wykorzystaniem pomocniczej tablicy To pierwsza z moŜliwych metod równowaŜenia drzewa binarnego. Najpierw wykorzystując metodę przechodzenia drzewa w głąb zrzucamy wszystkie elementy drzewa do tablicy. Co waŜne musi to być metoda LWP (elementy muszą być posortowane rosnąco). Następnie, wykorzystując rekurencję, dodajemy elementy do drzewa. Robimy to w taki sposób, Ŝe dodajemy element środkowy tablicy, a następnie wywołujemy funkcje dodaj dla lewej części tablicy i prawej części. Nasza funkcja będzie miała dwa argumenty – lewy i prawy indeks tablicy. Pierwsze wywołanie funkcji nastąpi dla całej tablicy.

15

7 30

4 13 25 34

5 2

Page 42: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

42

Przepis implementacyjny: wstaw_element(lewy,prawy) je śli lewy > prawego koniec; dodajemy element (lewy+prawy)/2 do drzewa; wywołujemy funkcj ę dla lewej podtablicy(lewy,(lewy+prawy)/2-1); wywołujemy funkcj ę dla prawej podtablicy((lewy+prawy)/2+1,prawy);

Przykład: Mamy przykładową tablicę liczb zrzuconych z drzewa i dodajemy je w kolejności: 4, 2 ,1, 3, 6, 5, 7.

Algorytm DSW Metoda ta nie wymaga uŜycia dodatkowej tablicy. Polega ona na obracaniu drzewa w prawo (lewo) aŜ do uzyskania listy, a następnie obracaniu go w lewo (prawo), aŜ do uzyskania drzewa zrównowaŜonego. Podczas tych operacji obowiązują pewne zasady, które zostały podane przy opisie poniŜszych przykładów. Przykład 1: Nasze wyjściowe drzewo niezrównowaŜone wygląda następująco:

Najpierw wykonujemy obroty w prawo (zgodnie ze wskazówkami zegara) aŜ do uzyskania listy. Lewy syn korzenia staje się korzeniem, a prawe potomstwo tego syna staje się lewym potomstwem dawnego korzenia, który wędruje w dół. Dokonujemy tego tak długo, aŜ korzeń nie będzie miał lewego syna, a następnie to samo robimy dla wszystkich prawych potomków.

10

5 20

3 7

8

4

2 6

1 3 5 7

1 2 3 4 5 6 7

Page 43: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

43

Pierwszy obrót dla korzenia.

Drugi obrót dla korzenia.

Korzeń nie ma juŜ lewego syna, wykonujemy obrót dla pierwszego prawego potomka, który posiada lewego syna (jest to 10). Prawe poddrzewo węzła, który idzie w górę (siódemki) jest przyczepiane jako lewe poddrzewo węzła, który idzie w dół (dziesiątki).

Kolejne obroty dla kolejnych prawych potomków, którzy posiadają jeszcze lewe poddrzewa (dla 10).

10

5

20

3

7

8

10

5

20

3

7

8

10

5

20

3

7

8

8

5

20

3

7

10

Page 44: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

44

Uzyskaliśmy listę, kaŜdy element wskazuje na kolejny. Teraz wykonujemy obroty w lewo (przeciwnie do wskazówek zegara).

Obroty te wykonujemy wg określonych zasad. Począwszy od korzenia, co drugi element staje się lewym synem swojego prawego potomka. Dodatkowo naleŜy pamiętać, o ilości wykonywanych obrotów. Najpierw wykonujemy ich tyle, ile wynosi róŜnica pomiędzy ilością elementów w naszym drzewie (n), a ilością elementów w najbliŜszym drzewie pełnym o liczbie elementów mniejszej od naszego drzewa (m). Drzewa pełne to takie, które mają wszystkie poziomy zapełnione maksymalnie. Ilość węzłów w takim drzewie to 2k - 1,

gdzie k to liczba poziomów (+∈ Ck ). JeŜeli nasze drzewo jest drzewem niepełnym o

liczbie węzłów równej n to ilość węzłów w najbliŜszym pełnym drzewie (mającym mniej

węzłów niŜ n) będzie obliczana z wzoru: 12 )1(log2 −= +nm . A więc najpierw wykonujemy t = n - m obrotów a następnie wykonujemy ich w kaŜdym cyklu t/2 obrotów dopóty, dopóki t>0.

Przykładowo, drzewo, które ma 2 poziomy moŜe maksymalnie mieć 3 węzłów (22 – 1). JeŜeli nasze drzewo ma 6 elementów to najbliŜsze pełne drzewo (o liczbie elementów nie większej od naszego drzewa) ma 3 elementy. Dlatego najpierw wykonujemy 6-3 obrotów węzłów nadmiarowych (tych, które znajdą się na ostatnim poziomie). W naszym przypadku obracamy 3, 7 i 10.

Teraz wykonujemy kolejne obroty począwszy od 3/2=1, dla co drugiego węzła począwszy od korzenia (czyli dla 5).

Na tym kończymy gdyŜ ilość obrotów (1/2 = 0) jest równa zero. Nasze drzewo zostało zrównowaŜone.

8

5 20

3 7 10

8

5

20

3

7

10

Page 45: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

45

Przykład 2:

2

1

3

7 9

6

4

5

8

2

1

3

7 9

6

4

5

8

2

1

3

7

6

4

8

9

5

Page 46: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

46

2

1

3

7 9

6

4

5 8

2

1

3

7 9

6

4

5

8

Page 47: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

47

Teraz następują obroty w lewo.

1

2

3

4

5

6

7

8

9

1

2

3

4

5

6

7

8

9

Page 48: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

48

Gotowe

Tablice haszuj ące Przechowywanie sporych ilości danych to częste zadanie dla komputerów. Przeszukiwanie duŜych zbiorów musi przebiegać sprawnie i szybko. Omówione wcześniej struktury danych umoŜliwiały sekwencyjne przeszukiwanie danych, jak miało to miejsce w przypadku list. Aby odnaleźć na liście dany element naleŜało przebiec całą jej zawartość po

1

2

4

5

6

7

8

9

3

1

2

4

5

6

7

8

9

3

1

2

4

5

6

7

8

9

3

Page 49: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

49

kolei i porównywać kaŜdą wartość z szukaną. ZłoŜoność takiego wyszukiwania wynosiła O(n). Nieco lepiej było w przypadku drzew binarnych, gdzie wyszukanie elementu miało złoŜoność O(ln n). Inną metodę składowania danych wykorzystano w przypadku tablic haszujących. Tutaj klucz (identyfikator rekordu danych) jest jedyną wskazówką informującą o połoŜeniu danego rekordu w tablicy. Klucz daje nam moŜliwość bezpośredniego dostania się do danych przez nas szukanych. ZłoŜoność takiej operacji w najlepszym przypadku jest rzędu O(1). Miejsce umiejscowienia elementu w tablicy wyznaczamy na podstawie funkcji haszującej (funkcji mieszającej ang. hash function), której argumentem jest klucz. Klucz ten moŜe być ciągiem znaków, liczbą lub rekordem. WaŜne jest, aby funkcja haszująca potrafiła obliczyć na podstawie tego klucza indeks w tablicy, pod którym dany element jest, bądź teŜ ma zostać wstawiony. Idealna funkcja haszująca potrafi przekształcić kaŜdy argument wejściowy w inny wynik – w inny indeks w tablicy. Zawsze dąŜy się do tego, aby funkcja haszująca była idealna. Dzięki temu nie występują kolizje, a właściwe występują bardzo rzadko. Z kolizją mamy do czynienia wtedy, gdy więcej niŜ jeden klucz zostały przekształcone przez funkcję mieszającą na ten sam indeks. Wtedy co najmniej dwa elementy powinny znajdować się w tym samym miejscu w tablicy. Jest to bardzo często nie moŜliwe dlatego istnieją metody rozwiązywania kolizji. Kilka przykładowych zostało omówiony poniŜej. Aby móc stworzyć doskonałą funkcję mieszającą tablica powinna zawierać przynajmniej tyle miejsc ile równych elementów moŜe wystąpić. Dla przykładu rozwaŜmy problem przechowywania danych osobowych. KaŜdego człowieka moŜna zidentyfikować po numerze PESEL. Wiemy, Ŝe jest to liczba 11 cyfrowa, zatem teoretycznie mieści się ona w przedziale od 0 do 1011-1. Jeśli chcielibyśmy przechowywać dane osób z danego obszaru – powiedzmy 105 elementów, to od razu wiadomo, Ŝe elementów do zapamiętania będzie znacznie mniej niŜ 1011. Jednak, aby nasza funkcja mieszają był idealna to tablica powinna mieścić 1011 elementów. Przy załoŜeniu, Ŝe wykorzystamy zaledwie znikomy procent komórek takie rozwiązanie jest złe. MoŜliwe jest takie dobranie funkcji haszującej, która co prawda nie wyeliminuje kolizji, jednak znacząco pozwoli ograniczyć rozmiar naszej tablicy. Dobranie rozmiaru tablicy do ilości elementów jakie będziemy w niej przechowywać zaleŜy od wielu czynników. Powiedzmy, Ŝe zazwyczaj ilość dostępnych komórek powinna być trzy razy większa od ilości elementów, które będziemy przechowywać. Dla naszego przykładu była by to tablica 3* 105. Jak wyznaczyć funkcję haszującą, która pozwoli przekształcić argument wejściowy w indeks w tablicy. Metoda jest kilka Najpopularniejsze z nich zostaną omówione poniŜej.

Funkcje haszuj ące – metody ich wyznaczania Pierwsza metoda wyznaczania funkcji haszującej polega na podzieleniu modulo klucza przez liczbę D będącą rozmiarem tablicy i wzięciu reszty z tego dzielenia. Niestety często zdarza się, Ŝe w takim przypadku wystąpi znaczna ilość kolizji. MoŜna ten efekt ograniczyć dobierając odpowiednio liczbę D. Najlepiej, jeśli będzie to liczba pierwsza. MoŜe to nie być liczba pierwsza, jednak powinna być tak dobrana, aby jej czynniki pierwsze (uzyskane po rozkładzie na czynniki pierwsze) był duŜe. Druga metoda polega na składaniu elementów klucza. Najpierw klucz dzieli się na klika części, a następnie łączy się te elementy poddając je róŜnego rodzaju operacjom. Przykładowo mając numer telefonu 506-123-678 moŜemy podzielić go na trzy części 506, 123 i 678 i zsumować te elementy uzyskując 1307. Teraz moŜemy otrzymaną liczbę podzielić modulo przez rozmiar tablicy D i wziąć resztę z dzielenia jako indeks. Podział klucza wejściowego moŜemy dokonywać na róŜny sposób, składając go później takŜe róŜnymi metodami. Innym sposobem składania klucza polega na podzieleniu go na kilka części. W naszym przypadku pozostaniemy przy 506, 123, 678. Następnie Parzyste części klucza zapisujemy od końca. 506, 321, 678. W kolejnym kroku sumujemy te elementy i uzyskujemy

Page 50: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

50

1505, które dzielimy modulo przez rozmiar D i jako indeks bierzemy resztę z dzielenia. Warto zaznaczyć, Ŝe aby przyspieszyć operacje przetwarzania klucza wszystkie działania wykonujemy na bitach wykorzystując podstawowe operacje na bitach. Dla ciągów znaków moŜna wykorzystać operacje składania znaków poprze operacje logiczną OR (np. ‘n’ OR ‘a’ OR ‘z’ OR ‘w’ OR ‘a’). Wadą tego rozwiązania jest wynik, który zawsze mieści się w przedziale 0-127. MoŜna takŜe wykonywać operacje logiczną na zbiorach znaków. Przykładowo ciąg nazwa moŜna przekształcić: „na” XOR „zw”.

Rozwi ązywanie kolizji metod ą łańcuchow ą To najprostsza z metod rozwiązywania kolizji. Tablica przechowująca dane tu przechowuje wskaźniki do list. Problem rozwiązano w taki sposób, Ŝe elementy, które funkcja haszująca przydzieliła pod ten sam indeks znajdują się na liście, która jest „przyczepiona” pod tym indeksem. W ten sposób listy wydłuŜają się wraz ze wzrostem ilości kolizji na danej pozycji. Jeśli ilość kolizji spowoduje znaczny wzrost długości list to efektywność takiego algorytmu zmniejszy się. Problem ten moŜna zminimalizować stosują listy posortowane (patrz punkt listy). Inną wadą tego rozwiązania jest dodatkowa pamięć potrzebna na przechowywanie wskaźników do listy oraz wskaźników do następnego elementu. Przy n elementach wskaźników tych będzie n + D.

Adresowanie otwarte Kiedy funkcja haszująca zwróci indeks juŜ zajęty przez inny element powstaje kolizja. Jedną z metod rozwiązywania tego typu problemów jest adresowanie otwarte. JeŜeli zwrócony indeks jest juŜ zajęty znajduje się kolejny wolny indeks w tablicy. Istnieją róŜne metody wyszukiwania kolejnych wolnych indeksów. Tablice przeszukuje się dopóty, dopóki nie znajdzie się wolnej komórki lub trafi się do komórki w tablicy juŜ sprawdzonej albo tablica będzie pełna.

Najprostszą metodą adresowania otwartego jest adresowanie liniowe. Metoda ta polega na wstawianiu elementu w pierwszą wolną pozycję tablicy (po wyznaczonej przez funkcję h), jeŜeli pozycja wyznaczona przez funkcję haszującą h(klucz) jest zajęta. Najpierw wyznaczamy indeks zgodnie z funkcją haszującą, a jeŜeli jest on zajęty to sekwencyjnie przeszukujemy kolejne komórki, aŜ trafimy na wolne miejsce. NaleŜy pamiętać, aby zabezpieczyć się przed wyjściem poza obszar pamięci przydzielony dla tablicy. W tym celu wystarczy wartość wyznaczonego indeksu dzielić modulo przez rozmiar tablicy D.

indeks = [h(klucz) + krok] mod D, gdzie 0≥krok

elem NULL elem 0

1

2

3

4

5

NULL

elem NULL

elem NULL elem

NULL

elem NULL

Page 51: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

51

Metoda ta rozwiązuje problem kolizji, jednak moŜe powodować częste grupowanie się zbiorów elementów. Ma to niekorzystne znaczenie podczas wyszukiwania elementów – uniemoŜliwia szybkie zakończenie tej operacji. Kolejna odmiana adresowania otwartego to adresowanie kwadratowe. W tym przypadku indeks kolejnej komórki do której naleŜy przejść w przypadku kolizji wyznacza funkcja kwadratowa.

indeks = [h(klucz) + c1 * krok + c2 * krok2] mod D c1,c2 to liczby dobrane tak, aby zapobiec zapętleniu się funkcji. Najlepiej, gdy są to liczby pierwsze. Rozwiązanie to zapobiega powstawaniu duŜych zbiorów liczb z sąsiednich indeksów. Ostatnia odmiana to adresowanie podwójne. W tym przypadku indeks początkowy wyznacza funkcja haszująca, a w przypadku kolizji kolejny indeks jest wyznaczany na podstawie drugiej funkcji, której argumentem jest takŜe klucz.

indeks = [h(klucz) + krok * f(klucz)] mod D Druga funkcja jest wyznaczana przy pomocy tych samych metod co pierwsza. MoŜna skorzystać z podziału modulo. Dzielimy klucz takŜe przez liczbę pierwszą, jednak o mniejszej wartości niŜ w funkcji h. Poszukiwanie elementu w tablicy haszującej polega na wyznaczeniu indeksu szukanego elementu na podstawie jego klucza i sprawdzeniu czy dany element tam jest. Jeśli nie to zgodnie z ideą adresowania otwartego moŜe on znajdować się w kolejnych komórkach. Sprawdzamy kolejne komórki wyznaczone na podstawie przyjętej metody (liniowej, kwadratowej, podwójnej). Poszukiwanie kontynuujemy dopóty, dopóki nie natrafimy na komórkę pustą lub trafimy w miejsce juŜ sprawdzone.

Usuwanie elementu wykonujemy w dwóch etapach: najpierw odnajdujemy dany element, a następnie usuwamy go z tablicy. Tu pojawia się jednak pewien problem. Nie moŜna tak po prostu usunąć elementu (np. wyzerować komórkę), gdyŜ moŜe się zdarzyć tak, Ŝe podczas kolejnego wyszukiwania nie zadziała opisany algorytm. Przykład: Mamy tablice haszującą. Nasza funkcja haszująca będzie wyznaczała indeks biorąc resztę z dzielenia klucza przez 11. Wypełniamy ją kolejno liczbami: 14, 15, 26 (kolizja z 15 – wstawiamy na kolejnej pozycji), 37 (kolizja z 15 i 26 – wstawiamy na pierwszej wolnej pozycji), 18, 30.

… 3 14 4 15 5 26 6 37 7 18 8 30 9 10 …

Page 52: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

52

Teraz usuwamy liczbę 26 „wymazując” tylko komórkę.

… 3 14 4 15 5 6 37 7 18 8 30 9 10 …

W następnym kroku szukamy liczby 37. Dzielimy ją modulo przez 11 i otrzymujemy resztę 4. Zaglądamy pod indeks 4 – nie ma jej tam. PoniewaŜ stosujemy adresowanie liniowe zaglądamy do następnej komórki pod indeks 5. Tam takŜe nie ma liczby 37. W kolejnej komórce tablicy nie ma juŜ nic (wymazaliśmy stamtąd 26). Zgodnie z ideą przeszukiwania opisaną powyŜej kończymy stwierdzając, Ŝe liczby 37 nie ma w tablicy. Jak się jednak okazuje wniosek ten jest nieprawdziwy. Tu ujawnia się problem związany z usuwaniem danych. Nie moŜna po prostu „wymazać” komórki. NaleŜy zaznaczyć, Ŝe komórka ta była zajmowana. Oznaczać to będzie, Ŝe podczas przeszukiwania będziemy ją traktować tak jakby coś tam było, a podczas wstawiania uznamy, Ŝe jest ona pusta i moŜna w niej umieścić element. Spróbujmy teraz jeszcze raz usunąć liczbę 26, ale tym razem wstawimy do tej komórki X – jako symbol wcześniejszego wypełnienia komórki.

… 3 14 4 15 5 X 6 37 7 18 8 30 9 10 …

Teraz szukając liczby 37 w 2 kroku trafiamy do komórki o indeksie 5, stwierdzamy, Ŝe to nie jest liczba 37 i idziemy dalej – nie przerywając poszukiwań. Doprowadzi nas to w kolejnym kroku do komórki o indeksie 6, gdzie znajduje się szukany element.

RozwaŜmy teraz wstawianie liczby 25 do tablicy. Funkcja haszująca zwraca nam indeks 3. Jest on jednak zajęty, więc próbujemy wstawić liczbę pod 4 – to takŜe jest niemoŜliwe. Pod indeksem 5 udaje nam się wstawić dany element. Pomimo, Ŝe komórka nie jest pusta (zawiera X) to podczas wstawiania traktujemy ten znak jakby komórka nie zawierała elementu.

Page 53: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

53

… 3 14 4 15 5 25 6 37 7 18 8 30 9 10 …

Teoria grafów

Podstawowe definicje Graf to zbiór wierzchołków i krawędzi, które łączą te wierzchołki. Jest to struktura o wiele bardziej elastyczna od drzew czy list. Nie istnieją tu tak precyzyjne restrykcje, co do połączeń poszczególnych węzłów. Grafy moŜna klasyfikować ze względu na wiele własności. Pierwsza z nich dotyczy kierunku krawędzi. MoŜemy wyróŜnić grafy skierowane i niekierowane. W pierwszym przypadku krawędzie mają określony kierunek, w którym moŜna się poruszać po danej krawędzi. W drugim przypadku kaŜda z krawędzi umoŜliwia ruch w obu kierunkach.

Inna klasyfikacja grafów to podział grafów na ogólne i proste. Pierwsza grupa cechuje się tym, Ŝe uwzględnia pewne moŜliwości w tworzeniu grafów, które nie występują w grafach prostych. Pierwsza z tych moŜliwości to istnienie krawędzi równoległych – czyli przynajmniej dwóch krawędzi łączących te same węzły. W grafie skierowanym dwie krawędzi łączące te same wierzchołki, ale mające przeciwne kierunki nie są krawędziami równoległymi.

Graf nieskierowany Graf skierowany

Page 54: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

54

Drugą z moŜliwości jest istnienie pętli własnych. Jest to krawędź łącząca ze sobą ten sam wierzchołek.

Istnieją grafy, w których nie występują Ŝadne krawędzie – o takich grafach mówimy grafy puste. MoŜliwa jest takŜe sytuacja, w której graf nie będzie miał nie tylko krawędzi, ale takŜe węzłów. Wtedy nazywać go będziemy grafem zerowym.

Skoro istnieją grafy puste, to takŜe moŜna mówić o istnieniu grafów pełnych. Z grafem takim będziemy mieć do czynienia wtedy, gdy ilość krawędzi w takim grafie będzie maksymalna. PoniŜszy wzór pozwala obliczyć maksymalną ilość krawędzi w grafie o n wierzchołkach.

2

)1(

2

−=

nnn

Z wierzchołkami grafów takŜe jest związanych kilka pojęć. MoŜna mówić o wierzchołku izolowanym – czyli takim, który nie jest połączony krawędzią z Ŝadnym innym wierzchołkiem tego grafu.

Grafy pełne

Graf pusty

pętla własna

Graf nieskierowany Graf skierowany

krawędzie równoległa to nie są krawędzie równoległa

Page 55: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

55

Stopień wierzchołka, czyli ilość sąsiadów, który posiada dany wierzchołek. W przypadku grafów nieskierowanych podaje się ilość krawędzie powiązanych z wierzchołkiem. W grafach skierowanych wyróŜniamy stopień wejściowy i wyjściowy wierzchołka. Pierwszy określa ilość krawędzi wchodzących do wierzchołka, a drugi – wychodzących z wierzchołka.

Izomorfizm to sytuacja, w której dwa grafy mają taką samą liczbę wierzchołków tych samych stopni oraz taką samą liczbę krawędzie mających te same wagi. Innymi słowy dwa grafy mogą być inaczej rozrysowane, ich wierzchołki mogą nazywać się inaczej, jednak w obu grafach będzie ta sama liczba wierzchołków o danym stopniu i taka sama ilość krawędzi o danej wadze. Przykładowo, jeśli w grafie G są dwa wierzchołki o stopniu 3, jeden o stopniu 5 i trzy o stopniu 2 to w grafie G’ musi istnieć dokładnie taki sam układ. PoniŜej przedstawiono dwie pary grafów. Pierwsza to grafy izomorficzne, natomiast grafy w drugiej parze nie są izomorficzne.

graf G graf G’

Graf nieskierowany Graf skierowany

Stopień wierzchołka wynosi 3.

Stopień wychodzący wierzchołka wynosi 1,

a wchodzący 2, natomiast stopień

ogólny 3

wierzchołek izolowany

Page 56: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

56

Cykl grafu to droga przejścia z wierzchołka do tego samego wierzchołka. Krawędzie, którymi się poruszamy tworzą cykl.

Spójność grafu to cecha, dzięki której z dowolnego wierzchołka w grafie idzie dojść do wszystkich pozostałych wierzchołków w tym grafie. Graf taki nazywamy grafem spójnym. Grafy niespójne zawsze dzielą się na składowe grafy spójne.

Podgrafem nazywamy graf będący częścią innego grafu.

JeŜeli dany zbiór wierzchołków da się podzielić na dwa podzbiory i wierzchołki jednego podzbioru są połączone tylko z wierzchołkami z drugiego podzbioru to mamy do czynienia z grafem dwudzielnym. Zastosowań takiego rozwiązania jest wiele, np. jeden podzbiór to mogą być zadania do wykonania, a drugi to procesory. KaŜdy z procesorów moŜe wykonywać określone zadania. Graf taki moŜe m.in. posłuŜyć do obliczania maksymalnych skojarzeń (zostanie to omówione później). PoniŜej znajdują się przykłady grafów dwudzielnych.

Graf Podgraf grafu po lewej stronie

Graf spójny

Graf niespójny

spójne składowe

cykl

Page 57: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

57

Graf z wagami to graf, w którym kaŜda z krawędzi ma odpowiednią wagę (priorytet). Przykładowo, jeŜeli graf przedstawia miasta połączone ze sobą, to wagą krawędzi moŜe być długość drogi łączącej te dwa punkty.

NaleŜy zauwaŜyć równieŜ, Ŝe drzewa to teŜ grafy, który nie mają cykli. Mówiąc drzewo nie mam na myśli drzewa binarnego, ale zwykłe. Spójne drzewo posiada dokładnie n-1 krawędzi, gdzie n to ilość wierzchołków.

Graf nieskierowany z wagami

Graf skierowany z wagami

2

4 9

7 1

3 3

3

3 4

9 9

6 1

1

3

5

2

4

6

1

3

5

2 4

6

Te grafy są równowaŜne

Brak połączeń wewnątrz danego

podzbioru.

Page 58: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

58

Reprezentacja grafów w komputerze Pierwsza z metod reprezentacji grafu w komputerze wykorzystuje listy i nazywana jest listą sąsiedztwa. W reprezentacji tej kaŜdy z wierzchołków ma przydzielony numer. W pamięci komputera tworzymy tablice o rozmiarze równym ilości wierzchołków. KaŜdy i-ty element tej tablicy wskazuje na listę, na której przechowujemy numery wierzchołków z którymi połączony jest wierzchołek i. Ten sposób przechowywania grafu w pamięci jest dobry dla grafów o małej liczbie krawędzi, gdyŜ wtedy listy będą stosunkowo krótkie. W przypadku grafów nieskierowanych na listach wystąpią powtórzenia związane z dwukierunkowością kaŜdej z krawędzi. PoniŜej znajduje się graf (po lewej) oraz jego reprezentacja z wykorzystaniem list (po prawej).

Druga metoda reprezentacji grafu w komputerze wykorzystuje macierz określaną macierzą sąsiedztwa. Dla grafu o n wierzchołkach tworzymy macierz n na n i wpisujemy jedynki w miejsca, gdzie występuje połączenie. Wypełniania macierzy moŜemy dokonywać wierszami (ewentualnie kolumnami). W kaŜdym i-tym wierszu wstawiamy jedynkę w j-tej kolumnie jeśli i-ty wierzchołek ma połączenie z j-tym wierzchołkiem. Oczywiście w przypadku grafów nieskierowanych macierz ta będzie symetryczna. PoniŜej znajduje się przykład macierzy sąsiedztwa dla grafu przedstawionego powyŜej.

0

1

2

3

4

5

3 1 2

0 3

3 0

0 2 1 4

4 3

4

1

0

3

2

5

4

a

b

c

d

e

f

g

f

e

g

c

a

b

d

f

e

g

c

a b d

Oba drzewa są równowaŜne – są to grafy

Page 59: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

59

0 1 2 3 4 5 0 0 1 1 1 0 0 1 1 0 0 1 0 0 2 1 0 0 1 0 0 3 1 1 1 0 1 0 4 0 0 0 1 0 1 5 0 0 0 0 1 0

Rozmiar macierzy zaleŜy tutaj od ilości krawędzi. Przy grafach gęstych zuŜywamy mniej pamięci niŜ w przypadku metody list sąsiedztwa. Aby zapamiętać informację o jednej krawędzi wystarczy nam jeden bit pamięci. Dlatego podczas implementacji tej metody reprezentacji grafu warto skorzystać (zwłaszcza przy duŜych grafach) z operacji na bitach. Przykładowo w 8-elementowej tablicy unsigned char (typ zajmujący w języku C 1 bajt) moŜna przechowywać macierz sąsiedztwa grafu mającego 8 wierzchołków. Trzecia metoda reprezentacji grafu wykorzystuje macierzą incydencji. Jest to macierz o wymiarach n (wierszy) na m (kolumn), gdzie n to ilość wierzchołków w grafie, a m – ilość krawędzi. W macierzy takiej dla i-tego wiersza (wierzchołka) wstawiamy jedynkę w tej kolumnie, która reprezentuje krawędź powiązaną z i-tym wierzchołkiem. KaŜda z kolumn zawiera jedynie dwie jedynki, gdyŜ jedna krawędź moŜe łączyć ze sobą jedynie dwa wierzchołki. Przykład macierzy incydencji dla przedstawionego powyŜej grafu został zaprezentowany poniŜej.

a b c d e f g 0 1 1 1 0 0 0 0 1 0 0 1 0 1 0 0 2 1 0 0 1 0 0 0 3 0 1 0 1 1 1 0 4 0 0 0 0 0 1 1 5 0 0 0 0 0 0 1

Przechodzenie grafu w gł ąb, wyznaczanie spójnych składowych Pierwsza z metod nazywa się w głąb (DFS ang. depth-first search). Podczas

przechodzenia grafu tą metodą rozpoczynamy od dowolnego wierzchołka i odchodzimy od niego najdalej jak się da, a następnie wracamy z powrotem. W kolejnym kroku wybieramy następny, jeszcze nieodwiedzony wierzchołek i znów idziemy najdalej jak to moŜliwe.

Wygląda to podobnie jak w przypadku drzew, ale naleŜy zaznaczać swoją obecność w wierzchołku, w którym się jest, aby nie trafić tam ponownie, co zapobiegnie zapętleniu. MoŜna to rozwiązać poprzez wprowadzenie dodatkowej tablicy o rozmiarze równym ilości wierzchołków. Zero w takiej tablicy oznacza, Ŝe nie byliśmy tam jeszcze, jeden- odwiedziliśmy, a dwa- przetworzyliśmy. Przetworzenie i-tego wierzchołka oznacza stan, w którym wszyscy sąsiedzi tego wierzchołka zostali juŜ odwiedzeni. W momencie wejścia do wierzchołka zaznaczamy jego odwiedzenie. Do przechodzenie w głąb równieŜ wykorzystujemy rekurencję, wywołując funkcję dla wszystkich sąsiadów i-tego (nieodwiedzonego jeszcze) wierzchołka. W momencie gdy powrócimy do i-tego wierzchołka i nie da się nigdzie juŜ iść, wierzchołek taki uznajemy za przetworzony. Przykład przechodzenia grafu w głąb pokazano poniŜej. Przy kaŜdym z wierzchołków podano kolejność odwiedzin oraz (po znaku ‘/’) kolejność przetworzenia.

Page 60: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

60

Metoda przechodzenia w głąb umoŜliwia obliczenie ilości spójnych składowych w

grafie. Wywołując funkcję odwiedzin dla dowolnego wierzchołka startowego, odwiedzimy tylko te wierzchołki, do których da się dotrzeć. Utworzą one pierwszą spójną składową. Jeśli ilość wierzchołków w tej spójnej składowej jest mniejsza od całkowitej liczby wierzchołków, to graf jest niespójny i posiada więcej spójnych składowych.

Przechodzenie grafu wszerz Druga metoda nazywa się wszerz (BFS ang. breadth-first search). Podczas przechodzenia grafu tą metodą rozpoczynamy takŜe od dowolnego, i-tego wierzchołka i odwiedzamy najpierw wszystkie wierzchołki leŜące najbliŜej i-tego wierzchołka, czyli te w odległości jednej krawędzi. Następnie odwiedzamy te leŜące w odległości dwóch krawędzi itd.

Podobnie jak w przypadku drzew wykorzystujemy do tego celu kolejkę. Wrzucamy i-ty wierzchołek (niewrzucony jeszcze) do kolejki i jeśli coś jest w kolejce to zdejmujemy element i wrzucamy wszystkich sąsiadów tego zdejmowanego wierzchołka (oczywiście tylko tych, których jeszcze nie wrzucaliśmy). Pamiętać naleŜy o zaznaczaniu swojej obecności w wierzchołku (wrzucenia wierzchołka do kolejki).

Przechodząc graf wszerz odwiedzimy tylko te wierzchołki, do których moŜemy dojść z wierzchołka startowego. UmoŜliwi nam to sprawdzenie spójności. JeŜeli ilość odwiedzonych wierzchołków jest mniejsza od ilości wierzchołków w drzewie to graf jest niespójny. PoniŜej narysowano przykładowy graf. Przy kaŜdym z wierzchołków napisano kolejność jego wypisania (zdjęcia z kolejki).

Sortowanie topologiczne W grafach nie mających cykli moŜemy odwiedzać węzły w takiej kolejności, aby dla i-tego węzła wszyscy jego najbliŜsi sąsiedzi zostali odwiedzeni, zanim on sam zostanie odwiedzony. W takim przypadku węzły będą odwiedzane zgodnie z kolejnością topologiczną. Algorytm umoŜliwiający takie odwiedzanie nazywa się sortowaniem

1 6

4

2

3

5

a

b

c d

e

f

1/6 6/1

5/2

2/5

3/4

4/3

a

b

c d

e

f

Page 61: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

61

topologicznym. Grafy umoŜliwiające sortowanie topologiczne węzłów określane są mianem grafów zaleŜności. Przykład wykorzystania grafów zaleŜności i sortowania topologicznego przedstawiono na poniŜszym rysunku (poniŜsze dwa grafy są izomorficzne). Problem polega na znalezieniu kolejności, w jakiej naleŜy się rano ubierać. Aby się tego dowiedzieć naleŜy przejść poniŜszy graf w głąb, a elementy przetworzone wypisać w kolejności malejących czasów przetworzenia. MoŜna wrzucać je na stos, a następnie je stamtąd zdjąć. W kaŜdym z węzłów podano kolejność odwiedzenia oraz (po znaku ‘/’) kolejność przetworzenia. Rozpoczynamy od „slipów” .

Wierzchołki przetworzono w następującej kolejności: marynarka, pasek, krawat, koszula, buty, spodnie, slipy, skarpety, zegarek. Zatem aby udało nam się ubrać, musimy zakładać te rzeczy w odwrotnej kolejności: zegarek, skarpety, slipy, spodnie, buty, koszule, krawat, pasek, marynarka.

Wyznaczanie silnych spójnych składowych w grafie sk ierowanym Jak juŜ wcześniej wspomniano grafy skierowane to takie grafy, w których waŜny jest kierunek ścieŜki łączącej dwa węzły. Efektem tego są sytuacje, w których moŜna przejść z punktu A do B, ale nie moŜna z B do A. Sytuację moŜna przyrównać do dróg w mieście. Niektóre są dwukierunkowe, ale są teŜ takie, którymi moŜna poruszać się tylko w jednym kierunku. Przykładowy schemat grafu skierowanego przedstawiony jest poniŜej.

slipy 1/7

spodnie 2/6

pasek 3/2

buty 5/5

koszula 6/4

krawat 7/3

marynarka 4/1

skarpety 8/8

zegarek 9/9

slipy 1/7

spodnie 2/6

pasek 3/2

buty 5/5

koszula 6/4

krawat 7/3

marynarka 4/1

skarpety 8/8

zegarek 9/9

Page 62: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

62

Spójność to sytuacja, w której z dowolnego wierzchołka grafu moŜemy dojść do

kaŜdego innego wierzchołka (istnieje droga do wszystkich pozostałych wierzchołków). W przypadku grafów skierowanych sytuacja jest nieco bardziej skomplikowana. Tutaj mamy do czynienia z dwoma rodzajami spójności: słabą i silną.

Sprawdzanie spójności słabej (szukanie spójnych składowych) odbywa się identycznie jak w przypadku grafu nie skierowanego. Najpierw graf skierowany „zamieniamy” na nie skierowany i szukamy spójnych składowych. Innymi słowy kaŜdą krawędź traktujemy jako dwukierunkową. Przedstawiony powyŜej graf będzie spójnych gdyŜ ma tylko jedną spójną składową.

W przypadku spójności silnej waŜny jest juŜ kierunek krawędzi. Definicja tej spójności jest taka: graf skierowany spójny to taki, w którym z kaŜdego wierzchołka idzie dojść do wszystkich pozostałych wierzchołków w grafie. Spróbujmy intuicyjnie znaleźć silne spójne składowe w naszym przykładowym grafie. Rozpoczynając od wierzchołka A (moŜliwy jest dowolny wierzchołek) stwierdzamy, Ŝe bez problemów moŜna poruszać się pomiędzy A, B i E. Do wierzchołków C, D, F, G oraz H takŜe dotrzemy jednak z tych wierzchołków nie moŜna powrócić juŜ do A, B lub E. Zatem wierzchołki A, B, oraz E tworzą pierwszą silną spójną składową tego grafu. Kolejne silne spójne składowe zakreślono kolorowymi kreskami.

W przypadku tak małego grafu nie było trudno odszukać silne spójne składowe. Jednak gdybyśmy mieli to zrobić w duŜo większym grafie pojawiłyby się problemy.

Aby znaleźć silne spójne składowe w dowolnym grafie naleŜy zastosować algorytm., którego kolejne kroki podano poniŜej. Korzystając z tego algorytmu znajdziemy jeszcze raz silne spójne składowe w naszym grafie.

A B C D

H G F E

A B C D

H G F E

Krawędź jednokierunkowa (tylko z A do B)

Krawędź dwukierunkowa, ale nie równoległa.

Page 63: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

63

Najpierw przechodzimy graf w głąb i zapisujemy czasy przetworzenia poszczególnych wierzchołków. Na poniŜszym rysunku przy kaŜdym z wierzchołków napisano numery kolejnych kroków przechodzenia grafu. Na kolorowym tle podano krok, w którym wierzchołek został przetworzony. Zaczynamy od dowolnego wierzchołka (np. C).

Przeszliśmy graf w głąb. Rozpoczęliśmy od C i poruszaliśmy się tak długo, aŜ powróciliśmy do C i juŜ nigdzie nie szło iść. następnie wzięliśmy kolejny nieodwiedzony wierzchołek (B) i przechodziliśmy graf dalej. W ten sposób mam spisane czasy przetworzeń poszczególnych wierzchołków.

Kolejny krok naszego przepisu to transpozycja grafu. Polega to za zamienieniu kierunków krawędzi. Innymi słowy, jeŜeli krawędź prowadziła z A do B to teraz poprowadzi z B do A. PoniŜej przedstawiono nasz graf po transpozycji.

Jednym ze sposobów przechowywanie grafu w pamięci była macierz sąsiedztwa. Dla pierwotnego naszego grafu (tego przed transpozycją) wyglądała ona następująco.

A B C D E F G H A 0 1 0 0 0 0 0 0 B 0 0 1 0 1 1 0 0 C 0 0 0 1 0 0 1 0 D 0 0 1 0 0 0 0 1 E 1 0 0 0 0 1 0 0 F 0 0 0 0 0 0 1 0 G 0 0 0 0 0 1 0 1 H 0 0 0 0 0 0 0 1

Natomiast macierz sąsiedztwa grafu transponowanego to po prostu transponowana macierz grafu pierwotnego. Transpozycja macierzy polega na zamianie wiersze z kolumnami. Po transponowaniu wygląda tak:

A B C D E F G H A 0 0 0 0 1 0 0 0 B 1 0 0 0 0 0 0 0 C 0 1 0 1 0 0 0 0 D 0 0 1 0 0 0 0 0 E 0 1 0 0 0 0 0 0 F 0 1 0 0 1 0 1 0 G 0 0 1 0 0 1 0 0 H 0 0 0 1 0 0 1 1

A B C D

H G F E

14/15 12/17 1/6/11 2/5

3/4 7/10 8/9 13/16

A B C D

H G F E

14/15 12/17 1/6/11 2/5

3/4 7/10 8/9 13/16

Page 64: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

64

Ostatni krok naszego przepisu to znów przejście naszego transponowanego grafu w głąb, ale tutaj waŜna jest kolejność wierzchołków od których zaczynamy. Zawsze przechodzenie odbywa się według malejących czasów przetworzenia z poprzedniego przejścia. Rozpoczynamy od wierzchołka, który przetworzyliśmy jako ostatni, czyli od B. Gdy juŜ nie da się iść dalej, rozpoczynamy przejście od kolejnego wierzchołka, który jeszcze nie odwiedziliśmy. NaleŜy pamiętać, Ŝe wybieramy wierzchołek nieodwiedzony o największym czasie przetworzenia.

Poszczególne kroki przejść rozpisano w róŜnym kolorze. Najpierw znaleźliśmy ABE, potem CD, EG i na końcu H. Zatem graf ten ma cztery silne spójne składowe- zgodnie z tym, co rozrysowaliśmy wcześniej intuicyjnie.

Troch ę o łączeniu w zbiory Mając daną pewną ilość elementów moŜna je połączyć w zbiory. Przykładowo

uczniów jednej klasy wrzucamy do zbioru IA, a drugiej do zbioru IB. Oczywiście musi istnieć pewien łącznik, dzięki któremu kaŜdy będzie mógł stwierdzić do którego zbioru naleŜy. MoŜna przykładowo uczniom klasy IA powiedzieć, Ŝe ich przewodniczącym jest uczeń X, a uczniom IB, Ŝe przewodniczącym jest uczeń Y. W ten sposób kaŜda grupa będzie znała swojego lidera, po którym będzie kojarzona z danym zbiorem. Podobnie moŜna zrobić z dowolnymi elementami. Przykładowo, mamy ciąg liczby od 0 do 6. KaŜda z nich tworzy jednoelementowy zbiór. My chcemy te liczby połączyć w większe zbiory.

Powiedzmy, Ŝe chcemy na początek połączyć 0 i 5 w jeden zbiór. Wybieramy lidera dla tego zbioru. Niech będzie to 5. Lidera moŜna wybierać według róŜnych zasad, w zaleŜności od rodzaju elementów jakie grupujemy. Przykładowo, dla cyfr moŜe to być największa liczba ze zbioru.

0 5

1

2

4

3

6

A B C D

H G F E

2/5 1/6 7/10 8/9

15/16 11/14 12/13 3/4

Page 65: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

65

Jednym ze sposobów implementacji łączenia w zbiory moŜe być zastosowanie pomocniczej tablicy. Jest to bardzo dobra metoda w przypadku łączenia liczb naturalnych. Tworzymy tablicę o takim zakresie, jaki obejmują nasze liczby. U nas będzie to tablica od zera do sześciu. Pod odpowiednim indeksem danej liczby będziemy przechowywali informację o liderze zbioru, do którego naleŜy dana liczba. Nasz tablica na początku (gdy będziemy mieć zbiory jednoelementowe) wyglądałaby następująco:

0 1 2 3 4 5 6 0 1 2 3 4 5 6

Teraz wykonajmy kilka przykładowych operacji łączenia zbiorów.

1) Połączmy 0 z 5, a jako lidera wybierzmy największą liczbę – czyli 5.

0 1 2 3 4 5 6 5 1 2 3 4 5 6

2) W kolejnym kroku połączymy 2 z 3 – liderem zostaje 3.

0 1 2 3 4 5 6 5 1 3 3 4 5 6

3) Teraz połączymy zbiór 0,5 ze zbiorem jednoelementowym 6 – lider to 6.

0 1 2 3 4 5 6 6 1 3 3 4 6 6

4) Kolejna operacja to połączenie zbioru 2,3 z 4 – liderem jest 4

0 1 2 3 4 5 6 6 1 4 4 4 6 6

Po wykonaniu powyŜszych operacji otrzymaliśmy zbiory, których graficzną postać przedstawiono poniŜej.

0 5

1

2

4

3

6

Page 66: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

66

W tej metodzie implementacji, aby dowiedzieć się do jakiego zbioru naleŜy i-ty element wystarczy sprawdzić co znajduje się pod i-tym indeksem w tablicy.

Innym sposobem implementacji zbiorów jest zastosowanie list. Elementy z danego zbioru tworzą listy, w których kaŜdy węzeł oprócz wskaźnika następny wskazującego na następny obiekt zawiera takŜe wskaźnik lider wskazujący na lidera zbioru. Przedstawiony powyŜej przykład zbiorów wyglądał by następująco.

Podczas operacji łączenia zbiorów A i B naleŜy wskaźnik następny ze zbioru A, który wskazywał na Null skierować na początek zbioru B. NaleŜy takŜe wszystkie wskaźniki lider w zbiorze B skierować na lidera w zbiorze A. MoŜna takŜe ustalić nowego wspólnego lidera dla nowo powstałego zbioru i ustawić wszystkie wskaźniki lider na nowego lidera. Dla przykładu połączymy zbiór 1 z 4,2,3.

Minimalne drzewo rozpinaj ące Minimalne drzewo rozpinające to zbiór krawędzi łączących wszystkie wierzchołki grafu, których suma (biorąc pod uwagę wagi krawędzi) jest minimalna. Graf wyznaczony przez krawędzie minimalnego drzewa rozpinającego nie posiada cykli, a zatem, jak sama

5

4 3

6 0

2

1

5

1

4 3

6 0

2

0 5

1 2

4

3

6

Page 67: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

67

nazwa wskazuje jest to drzewo. Istnieją przykłady grafów, które posiadają więcej niŜ tylko jedno minimalne drzewo rozpinające. PoniŜej omówiono dwa algorytmy pozwalające wyznaczyć minimalne drzewo rozpinające w dowolnym grafie. Algorytmy zostały omówione na przykładzie problemu, polegającego na wybraniu z zaproponowanych do pobudowania dróg tych, których całkowity koszt będzie minimalny. Koszt budowy danej drogi to waga krawędzi. PoniŜej przedstawiono graf oraz jego macierz sąsiedztwa dla omówionego problemu.

A B C D E F G H I A 0 4 0 0 0 0 0 8 0 B 4 0 8 0 0 0 0 11 0 C 0 8 0 7 0 4 0 0 2 D 0 0 7 0 9 14 0 0 0 E 0 0 0 9 0 10 0 0 0 F 0 0 4 14 10 0 2 0 0 G 0 0 0 0 0 2 0 1 6 H 8 11 0 0 0 0 1 0 7 I 0 0 2 0 0 0 6 7 0

Algorytm Kruskala Algorytm Kruskala jest algorytmem zachłannym pozwalający rozwiązać powyŜsze

zadanie. Algorytm zachłanny to taki, który nie martwi się tym, co będzie później, wybiera najlepsze rozwiązanie w danym momencie.

Aby znaleźć minimalne drzewo rozpinające postępujemy w następujący sposób. Sortujemy rosnąco wagi i rozpoczynamy od najmniejszej. Bierzemy krawędź o najmniejszej wadze i patrzymy, czy warto wybudować tę drogę. To czy warto określamy w następujący sposób. Dana krawędź łączy dwa miasta, jeŜeli są one w tym samym zbiorze (mają tego samego lidera), to nie warto budować tej drogi. W przeciwnym wypadku budujemy drogę i łączymy zbiory, do których naleŜały wspomniane miasta. Na początku wszystkie miasta będą tworzyły zbiory jednoelementowe, gdyŜ nie będą jeszcze połączone. Stopniowo będziemy scalać miasta. Kolejne kroki dla omawianego grafu będą następujące:

A B C D

E

F G

H

I

4 8 7

9

10

14 4

2

6

1

7

11 8

2

Page 68: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

68

Stosując algorytm Kruskala otrzymaliśmy dla naszego grafu poniŜsze minimalne drzewo rozpinające. Suma wag wszystkich krawędzi wynosi 37. Jak się okaŜe za chwile (dla algorytmu Prima) dla tego grafu istnieją takŜe inne minimalne drzewa rozpinające. Końcowa postać tego drzewa zaleŜy od decyzji, jakie podejmiemy rozpatrując kolejne krawędzie. JeŜeli istnieją krawędzie o takich samych wagach, to kolejność wybór zadecyduje o postaci końcowej drzewa. Zawsze jednak suma wag w kaŜdej wersji minimalnego drzewa rozpinającego musi być taka sama – musi być minimalna.

Algorytm Prima Algorytm Prima to druga z metod szukania minimalnego drzewa rozpinającego.

Rozpoczynamy analizę z dowolnego wierzchołka i wypisujemy moŜliwe połączenia bezpośrednie z innymi węzłami. Następnie wybieramy krawędź o najmniejszej wadze. Połączy ona nasze miasto z innym. Następnie wypisujemy moŜliwe połączenia dla tego nowego (dołączonego) miasta. Tutaj jest jednak pewna zasada. JeŜeli wcześniej wypisane połączenie z jakimś miastem jest bardziej atrakcyjne niŜ to, które wypisujemy obecnie to pozostawiamy poprzednie. Po wypisaniu tych moŜliwości znów wybieramy krawędź o minimalnej wadze. Czynności te wykonujemy dopóki nie zostaną połączone wszystkie miasta.

Dokładną zasadę dla podanego grafu przedstawia poniŜsza tabela. Analizę rozpoczynamy z miasta D i w kolumnach wypisujemy moŜliwe połączenia z rozpatrywanego miasta. Jeśli rozpatrujemy i-ty węzeł i nie ma on połączenia z węzłem w j-tym wierszu to przepisujemy wartość z poprzedniej kolumny. Jeśli natomiast istniej

A B C D

E

F G

H

I

4 8 7

9 4

2 1

2

1. droga H-G, warto ją pobudować – wierzchołki nie są w tym samym zbiorze; 2. droga G-F, warto ją pobudować – wierzchołki nie są w tym samym zbiorze; 3. droga I-C, warto ją pobudować – wierzchołki nie są w tym samym zbiorze; 4. droga A-B, warto ją pobudować – wierzchołki nie są w tym samym zbiorze; 5. droga C-F, warto ją pobudować – wierzchołki nie są w tym samym zbiorze; 6. droga I-G, nie budujemy gdyŜ te miasta są juŜ połączone – są w tym samym

zbiorze; 7. droga C-D, warto ją pobudować – wierzchołki nie są w tym samym zbiorze; 8. droga H-I, nie budujemy gdyŜ te miasta są juŜ połączone – są w tym samym

zbiorze; 9. droga B-C, warto ją pobudować – wierzchołki nie są w tym samym zbiorze; 10. droga A-H, nie budujemy gdyŜ te miasta są juŜ połączone – są w tym samym

zbiorze; 11. droga D-E, warto ją pobudować – wierzchołki nie są w tym samym zbiorze; 12. pozostałych dróg nie budujemy gdyŜ miasta są juŜ połączone – wszystkie są w

jednym zbiorze.

Page 69: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

69

połączenie, to sprawdzamy, czy jest atrakcyjniejsze od wartości z poprzedniej kolumny. Jeśli tak, to wpisujemy, w przeciwnym wypadku przepisujemy wartość z poprzedniej kolumny.

D C I F G H A B A * * * * * 8 - - B * 8 8(C) 8(C) 8(C) 8(C)<11 4 - C 7 - - - - - - - D - - - - - - - - E 9 9(D) 9(D) 9(D)<10 9(D) 9(D) 9(D) 9(D) F 14 4 4(C) - - - - - G * * 6 2 - - - - H * * 7 7(I) 1 - - - I * 2 - - - - - -

*gwiazdka oznacza niemoŜliwość bezpośredniego połączenia tych miast, a ‘–‘ stawiamy w miejscu, gdzie połączenie juŜ istnieje, połączenie miasta z tym samym miastem (np. D z D) istnieje od początku, wybraną krawędź zaznaczono na niebiesko.

Najkrótsze ścieŜki z jednym źródłem Problem polega na ustaleniu najkrótszej drogi (suma wag krawędzi, po których

będziemy się poruszać ma być minimalna) z dowolnie wybranego wierzchołka do wszystkich pozostałych. Problem ten będziemy rozwaŜać na przykładzie grafu skierowanego z wagami, chociaŜ moŜna to samo zrobić na grafie nieskierowanym z wagami. Do rozwiązania tego problemu moŜemy wykorzystać jeden z dwóch algorytmów opisanych poniŜej.

Algorytm Dijkstry Pierwszy krok to wybór wierzchołka z którego będziemy startować. Następnie

wykonujemy tzw. relaksację, czyli przejście po wszystkich sąsiadach i wyszukanie najlepszego połączenia.

Najlepiej będzie rozwaŜyć to na konkretnym przykładzie. Analizując problem będziemy opierać się na kilku zasadach. JeŜeli uda nam się ustalić juŜ krawędź, którą docieramy do danego wierzchołka to w tym wierzchołku zapamiętamy takŜe, z którego wierzchołka dotarliśmy do niego. W wierzchołku zapamiętujemy takŜe sumę wag krawędzi, którymi dotarliśmy do tego wierzchołka z wierzchołka startowego.

Oto przykładowy graf, w którym będziemy szukać najkrótszych połączeń.

A B C D

E

F G

H

I

4

8

7

9 4

2 1

2

Page 70: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

70

Przyjmijmy, Ŝe chcemy znaleźć najkrótsze drogi z wierzchołka A (chociaŜ moŜe to być dowolnie inny wierzchołek) do dowolnego wierzchołka w grafie. Nad A wpisujemy 0, gdyŜ z wierzchołka A do A trzeba pokonać drogę równą 0, nad pozostałymi wierzchołkami wpisujemy wartość bardzo duŜą (np. ∞). Ustalamy, Ŝe skoro z A startujemy to drogę do A juŜ znamy (oznaczamy to niebieskim kolorem).

Teraz sprawdzamy wszystkie moŜliwe połączenia od wierzchołka A do kaŜdego innego wierzchołka, do którego nie znamy jeszcze drogi. z A do A JuŜ mamy połączenie. z A do B Jest takie połączenie o wadze 10. z A do C Nie ma połączenia, więc droga, którą naleŜy przebyć pozostaje ∞. z A do D Jest takie połączenie o wadze 5. z A do E Nie ma połączenia, więc droga, którą naleŜy przebyć pozostaje ∞.

Teraz z pośród grupy wierzchołków do których jeszcze nie ustaliliśmy drogi wybieramy wartość najmniejszą (z tych wartości zapisanych obok wierzchołków). U nas jest to krawędź o wadze 5, prowadząca do wierzchołka D, wiedzie on z wierzchołka A. Znamy juŜ drogę do D.

A

B C

E D

10 0

5

2 3

1

9

2

4 6

7

Kolor oznacza, Ŝe znamy juŜ drogę do tego wierzchołka.

10 ∞

∞ 5

A

B C

E D

10 0/N

5

2 3

1

9

2

4 6

7

waga

Suma wag, którą trzeba pokonać aby tu dotrzeć oraz nazwa wierzchołka z którego

przyszliśmy.

∞ ∞

∞ ∞

Page 71: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

71

W następnym kroku analizujemy połączenia wychodzące z D. Tu jednak obowiązuje pewna zasad, o której jeszcze nie wspomniałem. JeŜeli połączenie z D do i-tego wierzchołka będzie miało większą sumę wag, niŜ ta, która juŜ jest wpisana w i-tym wierzchołku, to nie zamieniamy tej wartości. W przeciwnym wypadku podmieniamy wartość. z D do A JuŜ mamy połączenie. z D do B 5 (droga z A do D) + 3 (z D do B) daje 8. To jest mniejsze od 10, więc wstawiamy 8. z D do C 5 + 9 daje 14, co jest mniejsze od ∞. z D do D JuŜ mamy połączenie. z D do E 5 + 2 daje 7 co jest mniejsze od ∞.

Teraz znów wybieramy minimum z pośród nie ustalonych jeszcze połączeń. Będzie to 7, więc idziemy z D do E.

Teraz przeglądamy połączenia z E: z E do A JuŜ mamy połączenie.

z E do B 5 (droga z A do D) + 3 (D do B) daje 8. To pozostawiamy, gdyŜ połączenie z E do B jest równe ∞, a to więcej niŜ 8.

z E do C 7 + 6= 13 co jest mniejsze od 14, więc zamieniamy na 13 z E do D JuŜ mamy połączenie. z E do E JuŜ mamy połączenie.

A

B C

E D

10 0

5

2 3

1

9

2

4 6

7

8 13

7/D 5/A

A

B C

E D

10 0

5

2 3

1

9

2

4 6

7

8 14

7 5/A

Page 72: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

72

Minimum jest równe 8, więc idziemy z D do B.

Analizujemy połączenia z B: z B do A JuŜ mamy połączenie. z B do B JuŜ mamy połaczenie. z B do C 8 + 1 jest mniejsze od 13, więc zamieniamy na 9 z B do D JuŜ mamy połączenie. z B do E JuŜ mamy połączenie.

Jest tylko jeden element, więc on będzie minimum.

Na powyŜszym grafie mamy gotowe rozwiązanie dla kaŜdego wierzchołka. Przykładowo: aby dowiedzieć się jak dotrzeć z A do C, naleŜy sprawdzić skąd dotarto do C – nad wierzchołkiem pisze, Ŝe z B. Teraz sprawdzamy skąd doszliśmy do B – z D. Z kolei do D doszliśmy z A. A więc droga z A do C biegnie przez D oraz B i jest to najkrótsza moŜliwa droga o sumie wag równej 9.

Niestety algorytm ten moŜe nie zadziałać poprawnie w przypadku, gdy w grafie są ujemne wagi. To samo w sobie nie jest groźne. Problem pojawia się, gdy w grafie występują cykle, których suma wag jest ujemna. Wtedy algorytm się zapętli. Przykładowy fragment grafu mamy narysowany poniŜej. Poszukując rozwiązania zgodnie z algorytmem Dijkstry z wierzchołka A pójdziemy do B. W następnym kroku z B przejdziemy do A. Potem stwierdzimy, Ŝe idąc znów do B nasza droga się skróci. W efekcie suma wag będzie

A

B C

E D

10 0

5

2 3

1

9

2

4 6

7

8/D 9/B

7/D 5/A

A

B C

E D

10 0

5

2 3

1

9

2

4 6

7

8/D 9

7/D 5/A

Page 73: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

73

wyglądał następująco 2-8+2-8+…+2-8. Będzie w kaŜdym przejściu cyklu malała, do czego algorytm dąŜy. Spowoduje to jednak zapętlenie.

Algorytm Bellmana – Forda Algorytm Bellmana - Forda zabezpiecza nas przed wspomnianymi juŜ ujemnymi

cyklami w grafie. Kolejne kroki algorytmu są następujące: Najpierw ustalamy, Ŝe droga z dowolnie wybranego wierzchołka do wszystkich

pozostałych jest bardzo długa- wstawiamy ∞ w miejsce, gdzie pamiętamy długość drogi.

Następnie w pętli n-1 razy (gdzie n to ilość węzłów) robimy relaksację. W ostatnim kroku naleŜy sprawdzić, czy wystąpiły cykle ujemne, jeśli nie to

algorytm zadziałał poprawnie. RozwaŜmy przykładowy graf. Poszukujemy najkrótszych ścieŜek z wierzchołka A do wszystkich pozostałych wierzchołków. Na poniŜszym rysunku wstawiono juŜ początkowe odległości do wszystkich wierzchołków.

RozwaŜania tego algorytmu będą zapisywane w tabeli. Wartości w i-tej kolumnie,

będą informowały nas jaką drogę trzeba przebyć do i-tego wierzchołka i skąd przyszliśmy. Poszczególne wiersze reprezentują kolejne kroki algorytmu. Przy kaŜdej odległości w indeksie dolnym podano z którego wierzchołka dotarliśmy. Symbol nieskończoności oznacza brak bezpośredniego sąsiedztwa.

W zerowym kroku zawsze wpisujemy do tabeli wartości wag z macierzy incydencji. Jeśli chcemy znaleźć połączenia z wierzchołka A, to wpisujemy odległości wierzchołka A do wszystkich pozostałych.

A

B C

E D

6 0

7

8

-3

9

7

2

5

-2

-4

∞ ∞

B

A

2 -8

Page 74: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

74

krok wierz. A wierz. B wierz. C wierz. D wierz. E 0 0A 6A ∞ 7A ∞ Teraz rozwaŜamy wszystkie krawędzie wchodzące do kaŜdego z wierzchołków i sprawdzamy czy uzyskana

droga będzie krótsza od tej juŜ danej.

∞E + 2 > 0 nie zamieniamy

0A + 6 ∞C - 2

nie zamieniamy

6B + 5 7D -3 ∞E + 7

zamieniamy

0A + 7 6B + 8

nie zamieniamy

7D + 9 6B – 4

zamieniamy 1

0A 6A 4D 7A 2B

2E + 2 nie zamieniamy

0A + 6 4C – 2

zamieniamy

6B + 5 7D – 3 2E + 7

nie zamieniamy

0A + 7 6B + 8

nie zamieniamy

7D + 9 6B -4

nie zamieniamy 2

0A 2C 4D 7A 2B

2E + 2 nie zamieniamy

0A + 6 4C -2

nie zamieniamy

2B + 5 7D -3 2E + 7

nie zamieniamy

0A + 7 2B + 8

nie zamieniamy

7D + 9 2B – 4

zamieniamy

3

0A 2C 4D 7A -2B

4 -2E + 2

nie zamieniamy

0A + 6 4C -2

nie zamieniamy

2B + 5 7D -3 -2E + 7

nie zamieniamy

0A + 7 2B + 8

nie zamieniamy

7D + 9 2B – 4

nie zamieniamy

W kroku pierwszym do wierzchołka A prowadzi krawędź z E. Z poprzedniego kroku

wiemy, Ŝe, aby dotrzeć do E naleŜy przebyć drogę nieskończenie długą (∞E). Zatem suma tego co naleŜy przebyć do E oraz odległości A od E jest większa od obecnej wartości dla A. W tej sytuacji nie podmieniamy tych wartości. Do wierzchołka B prowadzą dwie drogi, z A oraz C. Obliczmy sumę dla obu przypadków (0A + 6 oraz ∞C – 2). śadna z nich nie jest mniejsza od istniejącej więc znów nie podmieniamy wartości. Kolejny wierzchołek to C. Tutaj prowadzą krawędzie z B, D oraz E. Po obliczeniu sum okazuje się, Ŝe droga z wierzchołka D będzie krótsza od tej, którą obecnie mam ustaloną – zatem podmieniamy wartości. Te same czynności wykonujemy dla wszystkich pozostałych wierzchołków n-1 razy (gdzie n to ilość wierzchołków). Po wykonaniu n-1 kroków naleŜy jeszcze sprawić, czy nasz graf nie ma cykli ujemnych. PoniewaŜ tym celu wykonujemy jeszcze jeden dodatkowy krok, po którym sprawdzamy, czy coś się zmieniło w naszych ustaleniach. Jeśli nie, to oznacza to, Ŝe w grafie nie ma cykli ujemnych, PoniewaŜ znalezione minimalne ścieŜki są poprawne.

Dla przykładu odczytajmy z tabeli najkrótszą ścieŜkę z wierzchołka A do E. Rozpoczynamy od końca. Najpierw sprawdzamy skąd dotarliśmy do E – był to wierzchołek B (informacja o tym jest zapisana w dolnym indeksie). Z kolei do B dotarliśmy z C. Do wierzchołka C przyszliśmy z D, a do D z A. Droga jaką naleŜy przebyć z A do E jest podana bezpośrednio w kolumnie E i wynosi -2.

Maksymalny przepływ Aby określić trasy, którymi moŜna będzie przesyłać dane, rzeczy lub cokolwiek

innego, w jak największej ilości naleŜy obliczyć maksymalną przepustowość. Pozwoli ona optymalnie pokierować naszymi danymi, aby jak najwięcej dotarło z punktu A do B.

Przykładem zastosowania tego algorytmu moŜe być sieć komputerowa. MoŜna ją przedstawić za pomocą grafu skierowanego, w którym wagi oznaczają ilość bajtów, które moŜna przesłać daną krawędzią (określają przepustowość). Przykład takiej sieci podano na poniŜszym rysunku. Spróbujemy dla tego grafu określić maksymalną przepustowość przesyłu danych z wierzchołka 0 do 5. Na początku przechodzimy graf szukając dowolnej drogi z punktu 0 do 5. MoŜe to być np. droga 0->1->2->5 zaznaczona na niebiesko.

Page 75: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

75

Maksymalna ilość danych jakie moŜna naraz przesłać tą trasą wynosi 12, gdyŜ tyle wynosi waga krawędzi mającej najmniejszą przepustowość.

Rysujemy graf, na który będziemy zaznaczać wykorzystane juŜ drogi – nazwijmy go grafem maksymalnej przepustowości. Pierwsza z nich wiedzie przez wierzchołek 1 oraz 2 i ma przepustowość 12.

Teraz tworzymy tzw. graf moŜliwości. Powstaje on wg pewnej zasady. Krawędzie nie wykorzystane nie zmieniają się, natomiast w miejsce krawędzi wykorzystanych rysujemy krawędzie o pozostałej do wykorzystania przepustowości oraz krawędzie nawrotów (na czerwono). Przykładowo: krawędź 0->1 miała przepustowość 16, my wykorzystaliśmy 12, więc pozostało 4. Krawędź nawrotów to krawędź, która powoduje tzw. nawracanie danych, gdyby się okazało to konieczne. Krawędzie nawrotów to krawędzie narysowane na powyŜszym grafie, ale z przeciwnymi zwrotami. Oto nasz graf moŜliwości:

PowyŜsze czynności (przechodzenie grafu, dorysowanie wybranej drogi do grafu

maksymalnej przepustowości i tworzenie grafu moŜliwości) wykonujemy dopóty, dopóki da się przejść z wierzchołka 0 do 5. W naszym przykładzie udał się znaleźć kolejną drogę, która wiedzie z 0 przez 3 oraz 4 do 5. Jej maksymalna przepustowość wynosi 4. Dorysowujemy ją do grafu maksymalnej przepustowości.

0

1 2

4 3

12

13

10 9

14

7

12

5

12

4

4

4 8

0

1 2

4 3

12

12

5

12

0

1 2

4 3

16

13

10 9

14

7

12

5

20

4

4

Page 76: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

76

Tworzymy graf moŜliwości:

Przechodzimy graf i wybieramy drogę 0->3->4->2->5 (o maksymalnej przepustowości równej 7). Dorysowujemy ją do grafu maksymalnej przepustowości.

Tworzymy graf moŜliwości:

Teraz po przejściu grafu okazuje się, Ŝe nie ma juŜ drogi z wierzchołka 0 do 5, a więc ustaliliśmy drogi dla przesyłania danych. Maksymalnie naraz moŜemy wysyłać 23 jednostki danych, co obrazuje poniŜszy graf maksymalnej przepustowości.

0

1 2

4 3

12

2

10 9

3

7

12

5

12+7=19

4

4

4 1

4+7=11 4+7=11

0

1 2

3

12

12

5

12+7=19

4

4+7=11 4

7

4+7=11

0

1 2

4 3

12

9

10 9

10

7

12

5

12

4

4

4 8

4 4

0

1 2

3

12

12

5

12

4

4 4

4

Page 77: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

77

Oczywiście pozostaje pytanie, w jaki sposób przechodzić dany graf: w głąb czy wszerz? Pierwszy ze sposobów określa się mianem algorytmu Forda-Fulkersona, drugi – metodą Edmunda-Karpa. Warto jednak zaznaczyć, Ŝe przechodzenie grafu wszerz moŜe okazać się efektywniejsze w przypadku niektórych grafów, w których wartości wag róŜnią się znacząco.

Maksymalne skojarzenia w grafie dwudzielnym JeŜeli dany zbiór wierzchołków da się podzielić na dwa podzbiory i wierzchołki

jednego podzbioru są połączone tylko z wierzchołkami z drugiego podzbioru to mamy do czynienia z grafem dwudzielnym. Taką definicję podałem juŜ wcześniej. Teraz chciałbym wspomnieć nieco o poszukiwaniu maksymalnych skojarzeń oraz maksymalnej przepustowości w grafie dwudzielnym.

Przykładowo mamy elektrownie (kaŜda produkuje pewną ilość energii) i mamy odbiorców. NaleŜy tak poprowadzić sieć, aby zaspokoić potrzeby wszystkich odbiorców najlepiej jak się da. Aby rozwiązać ten problem naleŜy znaleźć maksymalną przepustowość takiej sieci energetycznej.

Do rozwiązania zadania moŜemy wykorzystać algorytm omówiony w poprzednim punkcie. Wystarczy wprowadzić dodatkowo tzw. super wejście i super wyjście, które będą łączyły się z wejściami i wyjściami krawędziami o ∞ przepustowości (pozostałe krawędzie mogą mieć dowolną wagę). Przykład takiego rozwiązania pokazano na powyŜszym rysunku, dodatkowe krawędzie narysowano na czerwono. Teraz postępujemy tak, jak zostało to omówione przepustowość punkcie poprzednim – wykorzystując podany tam algorytm.

Drugie z zadań, to znalezienie maksymalnego skojarzenia. Chodzi o to, aby jak stworzyć jak najwięcej par węzłów z jednej i drugiej grupy, ale tak, aby kaŜdy z węzłów był

WYJŚCIA

WEJŚCIA

Super wejście

Super wyjście

0

1 2

3

12

12

5

12+7=19

4

4+7=11 4

7

4+7=11

Page 78: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

78

skojarzony tylko raz. Sytuację w takim grafie rozwiązujemy identycznie, jak w powyŜszym przykładzie, poprzez wprowadzenie super wejścia i super wyjścia. RóŜnica dotyczy jedynie wag w grafie. Tu wszystkie wagi (zarówno krawędzi zwykłych, jak i krawędzi łączących super wejście i wyjście) mają wartości jednakowe, np. 1. W kolejnych krokach postępujemy tak jak w poprzednim punkcie.

Obliczanie wielomianu chromatycznego Omawiany tutaj problem ma szeroki zakres zastosowań. Przykładem moŜe być, chociaŜby problem pokolorowania mapy politycznej w taki sposób, aby sąsiedzi nie byli tego samego koloru, a takŜe, aby zuŜyć jak najmniejszą ilość kolorów. Innym problemem jest kolorowanie wierzchołków grafu. I właśnie na tym zagadnieniu się skupimy. Chodzi o to, Ŝeby tak pokolorować wierzchołki grafu, aby sąsiedzi byli pomalowani na inny kolor. Weźmy sobie przykładowy, prosty graf.

JeŜeli załoŜymy, Ŝe mamy do dyspozycji n kolorów, to rozpoczynając np. od wierzchołka 2, moŜemy go pokolorować na n sposobów. Jednak kolejny wierzchołek (np. 3) moŜemy pokolorować juŜ na n-1 sposobów, gdyŜ nie wolno wykorzystać tego samego koloru, co dla wierzchołka 2. Natomiast ostatni wierzchołek (1) moŜemy pokolorować na n-2 sposobów, poniewaŜ nie wolno nam wykorzystać kolorów uŜytych dla 2 i 3. Ilość sposobów na jaki moŜna pokolorować cały graf to iloczyn moŜliwości dla kaŜdego wierzchołka.

)2)(1()( −−= xxxxχ Wielomian ten nazywamy wielomianem chromatycznym.

RozwaŜmy jeszcze klika przykładów. Obok kaŜdego z wierzchołków napisałem na ile sposobów moŜna go pomalować. Dla uniknięcia zamieszania rozpoczynam malowanie od pierwszego, a kończę na ostatnim wierzchołku.

1

2

3

Super wejście

Super wyjście

Page 79: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

79

22 )2)(1()( −−= xxxxχ )2()1()( 2 −−= xxxxχ

Ogólny wzór wielomianu chromatycznego przedstawia się zatem następująco:

)()\()( eGeGG −−= χχχ gdzie:

G- to graf dla którego obliczamy wielomian; G\e- to ten sam graf bez usuniętej krawędzi; g-e- to ten sam graf, ale ze sklejonymi wierzchołkami, które usunęliśmy. Znaczenie tego zapisu najlepiej wyjaśnić na przykładzie. Oto równanie grafów:

A teraz obliczmy wielomian dla grafu G. Stosujemy tutaj rekurencję. Po rozłoŜeniu tego grafu (jw.) rozkładamy oba te grafy tak długo, aŜ nie będą miały one krawędzi. Warto od razu zauwaŜyć, Ŝe niektóre konfiguracje grafu będziemy rozkładać kilka razy, dlatego wypada w implementacji zastosować programowanie dynamiczne. Na czerwono zaznaczam krawędzie do usunięcia w kolejnych krokach.

e

graf G graf G bez krawędzi e graf G ze sklejonymi wierzchołkami

1 (n)

2 (n)

5 (n-2)

3 (n-1)

4 (n-2)

1 (n)

4 (n-2)

2 (n-1)

3 (n-1)

Page 80: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

80

Przypominam, Ŝe nasz wielomian wcześniej dla identycznego grafu wyniósł:

xxxxxxxx 254)2()1()( 2342 −+−=−−=χ

Z powyŜszego rysunku idzie równieŜ odczytać ten sam wielomian. Ilość wierzchołków w kaŜdym z czynników sumy to wykładnik potęgi do której podnosimy x. dla naszego przykładu wielomian będzie wyglądał następująco:

=−+−++−+−+−− xxxxxxxxxxxx 2223232334

xxxx 254 234 −+−

Page 81: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

81

Dodatek: Algorytmy wyszukiwania wyrazu wzorcowego w tekście

Metoda „naiwna” Pierwsza z metoda polega na porównywaniu pierwszego znaków wzorca z pierwszym znakiem tekstu, gdy jest zgodny to porównujemy kolejne znaki. JeŜeli któryś nie będzie zgodny, to przerywamy porównywanie i rozpoczynamy znów od początku wzorca, ale tym razem porównujemy z kolejnymi znakami tekstu (1 znak wzorca z 2 znakiem tekstu, itd.). Najlepiej pokazać to na przykładzie. Mamy dany tekst: abcaacaabcbacab oraz wzorzec aab. Na czerwono zaznaczam zgodne znaki w porównaniu. a b c a a c a a b c b A c a b a a b a b c a a c a a b c b A c a b a a b a b c a a c a a b c b A c a b a a b a b c a a c a a b c b A c a b a a b a b c a a c a a b c b A c a b a a b a b c a a c a a b c b A c a b a a b a b c a a c a a b c b A c a b a a b

Metoda ta nie jest zbyt efektywna, poniewaŜ w przypadku długiego wzorca, gdy napotykamy na prawie dobry ciąg, ale róŜniący się jedynie końcówką, to cała nasza praca związana z porównaniem tego tekstu idzie na marne, przesuwamy wzorzec jedynie o jedną pozycję i musimy zaczynać porównywanie od nowa.

Metoda Rabina-Karpa Jest to o wiele bardziej efektywna metoda. Polega na porównywaniu nie pojedynczych

znaków, lecz całego ciągu. KaŜdy znak jest reprezentowany przez jakąś liczbę (przykładem moŜe być kod ASCII). JeŜeli potraktujemy porównywany ciąg znaków jak liczbę zapisaną w pewnym systemie liczbowym, to przyspieszy nam to algorytm. W naszym przykładzie będziemy rozpatrywać tekst składający się z trzech znaków, a zatem będziemy działać na kodzie trójkowym. Przyjmijmy, Ŝe a to 0, b=1, c=2. Zatem przykładowy ciąg znaków aabc moŜemy potraktować jako liczbę (0012)3 w kodzie trójkowym.

100123

3 )5(32313030)0012( =⋅+⋅+⋅+⋅=

Obliczanie takiego wielomianu nie jest zbyt dobre dla kompilatora. On strasznie nie lubi podnosić do potęgi. Dlatego najlepiej zastosować schemat Kornera dla obliczenia takiej wartości. Idea tego schematu jest następująca:

nnnnn axxaxaxaxaxaxaxa ++++=++++ −− ...)))(((... 210

022

110

Taki sposób obliczania wielomianu znacznie ułatwia pracę komputerowi. Rozpatrzmy jakiś przykład. Mamy dany tekst: abcabbcbbcab oraz wzorzec: bbcb. PoniewaŜ wzorzec jest cztero

Page 82: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

82

cyfrowy, więc będziemy porównywać liczby czterocyfrowe zapisane w systemie trójkowym. Na początku obliczamy wartość wzorca- zamieniamy ją na system dziesiętny:

4313)23)131((1121 =+⋅+⋅+⋅=

A teraz to samo robimy dla pierwszych czterech (poniewaŜ taka jest długość wzorca) znaków tekstu:

1503)23)130((0120 =+⋅+⋅+⋅=

Dla uproszczenia zdefiniujmy sobie liczbę 15 jako wartość. Teraz porównujemy wartość z 43 jeŜeli są równe to oznacza, Ŝe to jest nasz szukany wzorzec, jeŜeli nie to naleŜy sprawdzić z następnym ciągiem (z ciągiem bcab). Tu jednak pewna uwaga. Nie trzeba obliczać jego wartości z schematu Kornera, jest prostszy wzór:

][)][( dlugoscitekstpodstawapodstawaitekstwartosc dlugosc ++⋅⋅−

gdzie: wartość- to zmienna wartość, czyli poprzedni ciąg znaków; tekst- to tablica, w której przechowujemy przeszukiwany tekst;

i- to indeks w tej tablicy, od którego zaczyna się analizowany tekst; postawa- to podstawa systemu kodowania, który stosujemy (w naszym przykładzie jest to 3); długość- to długość wzorca (ilość znaków).

13)27015()1201( +⋅⋅−=bcab

A zatem nasze porównania będą wyglądać następująco:

Wartość wzorzec wynik abca (15) bbcb (43) róŜne bcab (46) bbcb (43) róŜne cabb (58) bbcb (43) róŜne abbc (14) bbcb (43) róŜne bbcb (43) bbcb (43) równe

Jedynym problemem jaki moŜe się pojawić w przypadku tego algorytmu, to problem zbyt długiego słowa. W przypadku alfabetu mamy do czynienia z dość znaczną ilość znaków, a co za tym idzie dość duŜymi liczbami. Jednak wtedy moŜna zastosować dzielenie tych liczb modulo. Prawdopodobieństwo, Ŝe uzyskamy te same wyniki przy dzieleniu dwóch tak duŜych liczb jest praktycznie zbliŜone do zera.

Metoda Knuta-Morrisa-Pratta Jest to udoskonalona metoda „naiwna”. Udoskonalenie polega na wprowadzeniu funkcji prefiksowej, dzięki której zapamiętujemy dotychczasowe porównania i w wypadku, gdy nasz porównywany wzorzec nie będzie zgodny pod koniec jego porównywania, to nie przesuwamy go o jedną pozycję, ale o tyle, ile mówi nam funkcja prefiksowa. RozwaŜę tutaj jedynie przykład pokazujący idę tego algorytmu, nie skupię się na sposobie wyznaczania funkcji prefiksowej, która jest inna dla kaŜdego wzorca. Mamy tekst: abcaabcbab oraz wzorzec: bcb.

Page 83: Algorytmy i struktury danychsirius.cs.put.poznan.pl/~inf84803/ksiazki... · 2009-09-23 · Algorytmy i struktury danych 5 Efektywno ść Początkowi programiści pisząc program cieszą

Algorytmy i struktury danych

83

a b c a A b c b a b c b a b c b c b

Znaki są niezgodne więc przesuwamy wzorzec o 1. a b c a a b c b a b c b a b c b c b 1 i 2 znak jest OK, ale 3 juŜ nie. Jednak nie przesuwamy wzorca o 1 gdyŜ 1 jego znak na pewno nie będzie

zgodny z 3 i 4 w tekście (stwierdzenie na podstawie funkcji prefiksowej). a b c a a b c b a b c b a b c b c b

1 znak się nie zgadza więc przesuwamy wzorzec o 1. a b c a a b c b a b c b a b c b c b

Znaleźliśmy wzorzec.

Zakończenie Notatki te powstały w ramach przygotować do egzaminu z przedmiotu „Algorytmy i struktury danych”. Mogą zawierać błędy, za które nie biorę Ŝadnej odpowiedzialności. Będę wdzięczny za wszelkie uwagi mogące pomóc mi poprawić ewentualne niedociągnięcia.