7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na...

21
Zaawansowane algorytmy 1 5-7 projektów, 2 tygodnie na każdy projekt, 0-10 punktów na każdy projekt, 7pkt za projekt wykonany poprawnie, 2 pkt straty za spóźnienie. Minimalna ilość by zaliczyć ćw to (ilość projektów -1)*5+2. Czyli jednego projektu można nie zrobić. Egzamin pisemny. Będzie zerówka na ostatnim lub przedostatnim wykładzie. „Projekty nie polegają na wygooglaniu rozwiązania”. Cormen, Lieserson Wprowadzenie do algorytmów Sedgewick Algorytm w c++ Sedgewick Algorytmy w c++.Grafy W wikipedii angielskiej są ładnie zrobione algorytmy. Plan wykładu: -Przypomnienie -Drzewa binarne i drzewa BST, słowniki, przeszukiwanie danych, drzewa binarne bardzo przyspieszają przeszukiwanie, ale drzewa BST mogą się też degenerować, nie mają cech samonaprawczych w sobie, jeśli mamy złośliwe dane, to doprowadzimy słownik do postaci liniowej, że nic się tam nie będzie dało zrobić. -Drzewa AVL i 2-3-4 (AVL próba by długości ścieżek były równe z dokładnością do 1, ale wtedy czas balansowania jest bardzo duży i słownik trzymany w tej postaci jest duży, bo długo zajmuje organizacja i to może nie być opłacalne, dlatego dojdziemy do drzew czerwono czarnych, one nie są zbalansowane ale są na tyle bliskie zbalansowania, że dobrze się szuka i czas nie jest długi. -Drzewa zbalansowane (czerwono – czarne) w takim drzewie wystarczy by długości ścieżek nie różniły się o więcej niż połowę, czyli maksymalnie dwa razy dłuższe ścieżki i to nam zapewni logarytmiczny czas poszukiwania co będzie dla nas wystarczające. -Będą też B drzewa i też będziemy budować słownik, binarne będą nieefektywne, to będą drzewa 2-3-4 czyli mogą mieć więcej niż dwójkę dzieci, ale jeśli się to fajnie zorganizuje, to będzie dobry czas i niezła implementacja, maksymalnie będą 4 dzieci czyli maksymalnie cztery wskaźniki, pokazane w Sedgewicku czyli zaczniemy od słowników o czasie logarytmicznym a potem przejdziemy do haszowania - Tablice z haszowaniem – drzewa w dobrym wydaniu ma być czas logarytmiczny a tutaj dostaniemy czas stały, czyli teoretycznie nie ważne ile będzie danych, zawsze dostaniemy ten sam czas, ceną jest tutaj pamięć, jeśli mam jej bardzo dużo to i tak nawet jeśli zbiór rośnie to czas NIE rośnie. Można to było spotkać w systemach sieciowych. - Kompresja danych – pokaże nam kilka sposobów pakowania danych, czyli w jaki sposób zrobić z dużego zbioru mały zbiór i pokaże mam tylko metody bezstratne, np. zip,. Rar one odtworzą stan oryginalny, a przykład kompresji stratnej to JPG - kodowanie arytmetyczne czyli w dowolnej liczbie można przetrzymać dowolnie długą informację i do tego słownik, ale tu będzie trudne przechowywanie, bo ta liczba będzie musiała być aż tak dokładna - wyszukiwanie wzorca w tekście są algorytmy siłowe czyli przyporządkowanie znaczek po znaczku, ale są też fajniejsze i tu dużo dziwnych nazw - algorytmy grafowe (Przeszukiwanie wszerz i wgłęb, drzewo rozpinające, znajdowanie najlepszej drogi, znajdowanie najkrótszych ścieżek pomiędzy wszystkimi wierzchołkami), to będzie na sieci, routing itp

Transcript of 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na...

Page 1: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 1

5-7 projektów, 2 tygodnie na każdy projekt, 0-10 punktów na każdy projekt, 7pkt za projekt wykonany poprawnie, 2 pkt straty za spóźnienie. Minimalna ilość by zaliczyć ćw to (ilość projektów -1)*5+2. Czyli jednego projektu można nie zrobić. Egzamin pisemny. Będzie zerówka na ostatnim lub przedostatnim wykładzie. „Projekty nie polegają na wygooglaniu rozwiązania”.

Cormen, Lieserson Wprowadzenie do algorytmówSedgewick Algorytm w c++Sedgewick Algorytmy w c++.GrafyW wikipedii angielskiej są ładnie zrobione algorytmy.

Plan wykładu:

-Przypomnienie

-Drzewa binarne i drzewa BST, słowniki, przeszukiwanie danych, drzewa binarne bardzo przyspieszają przeszukiwanie, ale drzewa BST mogą się też degenerować, nie mają cech samonaprawczych w sobie, jeśli mamy złośliwe dane, to doprowadzimy słownik do postaci liniowej, że nic się tam nie będzie dało zrobić.

-Drzewa AVL i 2-3-4 (AVL próba by długości ścieżek były równe z dokładnością do 1, ale wtedy czas balansowania jest bardzo duży i słownik trzymany w tej postaci jest duży, bo długo zajmuje organizacja i to może nie być opłacalne, dlatego dojdziemy do drzew czerwono czarnych, one nie są zbalansowane ale są na tyle bliskie zbalansowania, że dobrze się szuka i czas nie jest długi.

-Drzewa zbalansowane (czerwono – czarne) w takim drzewie wystarczy by długości ścieżek nie różniły się o więcej niż połowę, czyli maksymalnie dwa razy dłuższe ścieżki i to nam zapewni logarytmiczny czas poszukiwania co będzie dla nas wystarczające.

-Będą też B drzewa i też będziemy budować słownik, binarne będą nieefektywne, to będą drzewa 2-3-4 czyli mogą mieć więcej niż dwójkę dzieci, ale jeśli się to fajnie zorganizuje, to będzie dobry czas i niezła implementacja, maksymalnie będą 4 dzieci czyli maksymalnie cztery wskaźniki, pokazane w Sedgewicku czyli zaczniemy od słowników o czasie logarytmicznym a potem przejdziemy do haszowania

- Tablice z haszowaniem – drzewa w dobrym wydaniu ma być czas logarytmiczny a tutaj dostaniemy czas stały, czyli teoretycznie nie ważne ile będzie danych, zawsze dostaniemy ten sam czas, ceną jest tutaj pamięć, jeśli mam jej bardzo dużo to i tak nawet jeśli zbiór rośnie to czas NIE rośnie. Można to było spotkać w systemach sieciowych.

- Kompresja danych – pokaże nam kilka sposobów pakowania danych, czyli w jaki sposób zrobić z dużego zbioru mały zbiór i pokaże mam tylko metody bezstratne, np. zip,. Rar one odtworzą stan oryginalny, a przykład kompresji stratnej to JPG

- kodowanie arytmetyczne czyli w dowolnej liczbie można przetrzymać dowolnie długą informację i do tego słownik, ale tu będzie trudne przechowywanie, bo ta liczba będzie musiała być aż tak dokładna

- wyszukiwanie wzorca w tekście – są algorytmy siłowe czyli przyporządkowanie znaczek po znaczku, ale są też fajniejsze i tu dużo dziwnych nazw

- algorytmy grafowe (Przeszukiwanie wszerz i wgłęb, drzewo rozpinające, znajdowanie najlepszej drogi, znajdowanie najkrótszych ścieżek pomiędzy wszystkimi wierzchołkami), to będzie na sieci, routing itp

Page 2: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 2

- NP-zupełność – duże problemy, takie których czas rozwiązania jest niesensowny, czyli nie mają rozwiązania w czasie wielomianowym. Może nie zdążymy, a na egz będzie to co zdążymy zrobić na wykładzie.

POWTÓRZENIE

Algorytm – recepta, która mówi w jaki sposób postępować by rozwiązać dany problemDla wielu zadań istnieje wiele rozwiązań danego zadania. Ogólnie mamy to co było w zeszłym semestrze na pierwszym wykładzie.

Program – implementacja algorytmu w jakimś języku programowania

Struktura danych – organizacja danych niezbędna do rozwiązania problemu (metody dostępu)

Ogólne spojrzenie Wykorzystanie komputera:- projektowanie programów (algorytmy, struktury danych)- pisanie programów (kodowanie) tu myślimy o celach implementacyjnych - weryfikacja programów (testowanie)

Cele algorytmiczne: poprawność i efektywnośćCele implementacji: zwięzłość, możliwość powtórnego wykorzystania

Co to znaczy, że algorytm działa dobrze? Np. to że ma działać szybko, ale można też wziąć szybszy komputer, jeśli nie chcemy zmieniać programu, jednak to ma swoje granice, istnieje jakiś najszybszy komputer, a jeśli mamy nadal ten sam komputer to mamy sprawić, by wykonywał on mniej operacji. Drugim kryterium jest pamięć, minimalizowanie zasobów. Ostatnim parametrem minimalizacji to kwestia, że jeśli używamy wielu procesorów to możemy równolegle wykonywać ileś procesów, ileś kawałków zadania, i wtedy możemy minimalizować ilość procesów prowadzonych jednocześnie,. Zwykle jeśli algorytm używa mniej pamięci to jest wolniejszy itp. Czyli taka odwrotna proporcjonalność.

Możliwość powtórnego wykorzystania to jedna z podstawowych cech, bo jednak chodzi o to by wykorzystywać istniejące biblioteki a nie tworzyć je od nowa.

Zwięzłość to chodzi o to by nie było za wiele kodu, jeśli nie ma on sensu.

Algorytmy to rodzaj czarnej skrzynki przetwarzającej dane wejściowe na dane wyjściowe.

Przypomnijmy, że algorytm ma cechy: określoność, skończoność, poprawność, ogólność, dokładność.

Algorytmem nazywamy skończoną sekwencję jednoznacznych instrukcji pozwalających na rozwiązanie problemu, tj. na uzyskanie pożądanego wyjścia dla każdego legalnego wejścia.

Ważne jest porządne wyspecyfikowanie wejścia i wyjścia, czyli powinniśmy jasno umieć powiedzieć, co ma być na wejściu, oraz czy to co jest na wyjściu jest tym co mieliśmy otrzymać.

Sortowanie przez wstawianie (złożoność kwadratowa, czyli nienajlepsza jeśli chodzi o metody sortowania, a taka pożądana to było nlogn, a przy pewnych założeniach da się dojść do liniowości).

Page 3: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 3

Gdy mówimy o wielkości zadania, to nie musi to oznaczać ilości danych na wejściu, bo wystarczy wziąć zadanie, czy dana liczba na wejściu jest liczbą pierwszą np. sprawdzać po kolei i dojść do pierwiastka z tej liczby. Czas wykonania jest wykładniczy. Rozmiarem wejścia nie musi być ilość czy wielkość liczby, ale może być wielkość jej reprezentacji, czyli wtedy istotne jest ile pamięci ona zajmuje, a nie jaka jest jej wartość. Jeśli reprezentacja będzie dwukrotnie większa, to czas wcale nie będzie dwa razy dłuższy i to jest sprawdzalne.

Tak więc liczymy ilość operacji i liczymy operacje dominujące, czyli te które trwają najdłużej, a resztę pomijamy. Np. odczyt z dysku jeśli wykorzystywane są pamięci masowe, a jeśli nie są one wykorzystywane to całe przypisanie itp.

Algorytm przez wstawianie – porównujemy liczby z kolejnymi czyli wsadzamy karty do posortowanego już ciągu i przypominamy ten algorytm bo czasami musi on wykonać dużo pracy czyli wtedy gdy musimy wsadzić liczbę na początek, czyli porównać ją ze wszystkimi, lub mało pracy, gdy wsadzamy na koniec, dlatego można rozpatrywać różne przypadki:- najgorszy to przypadek takich danych gdy już gorzej być nie może czyli w sortowaniu będą to liczby odwrotnie posortowane i to rozpatrywanie ma sens gdy chodzi np. o rozstawianie karetek by zdążyły zawsze dojechać na miejsce itp. Czyli w sumie chodzi o to gdzie czas ma sens.- czasami najgorszy przypadek nas nie interesuje np. ustawianie świateł w mieście, najwyżej czasami ludzie sobie poczekają, ale trudno, chodzi o to że interesuje nas ‘średni’ przypadek, a nie ten najgorszy, czyli ile zwykle się jedzie przez miasto itp. Często są drastyczne różnice między tym średnim i najgorszym przypadkiem, bo najlepszy nam nic nie daje. Przypadek najgorszy jest zwykle dobrze przebadany a przypadek średni często nie jest nawet dobrze zdefiniowany.

Poprawność jest praktyczna i całkowitaZ punktu widzenia teoretycznego musimy mieć poprawność całkowitą, czyli musimy umieć przeprowadzić dowód, że dla wszelkich poprawnych danych da się dowieść że otrzymamy poprawny wynik. Ale często nie da się tego dowodu ładnie przeprowadzić w budownictwie, zadaniach mechanicznych itp. Poprawność praktyczna jest wtedy gdy jeśli mamy poprawne dane i algorytm zaczyna działać to skoro coś wypluwa, to wyprodukuje wynik poprawny, ale nie umiemy dowieść, że dla wszelkich danych będzie dobrze.

Istnieją też niezmienniki.

Były również notacje asymptotyczne.Cel: uproszczenie analizy czasu wykonania, zaniedbywanie szczegółów, które mogą wynikać ze specyficznej implementacji czy sprzętu. Główna idea: jak zwiększa się czaswykonania algorytmu wraz ze wzrostem rozmiaru wejścia (w granicy). Algorytm asymptotycznie lepszy będzie bardziej efektywny dla prawie wszystkich rozmiarów wejść, ewentualnie poza być może bardzo małych, ale ich będzie skończona ilość, poza tym one nas zwykle nie interesują.

Wracamy do notacji O duże czyli ograniczenia z góry, wykorzystywane w analizie najgorszego przypadku. Notacja z dołu to była omega mówiła o najlepszym możliwym zachowaniu się algorytmu. To jest fajne gdy chcemy zbudować algorytm pomiędzy o duże i omega duże. A gdy umiem opisać coś od góry i od dołu naraz to mamy Teta. Różni się to stałymi.

Page 4: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 4

Drzewa poszukiwań binarnych

Najpierw sobie je omówimy, a potem będziemy je poprawiać, by się zachowywały przyzwoicie, to jakieś dwa wykłady w sumie. Najpierw powtarzamy to co było w zeszłym semestrze.

WprowadzeniePoszukujemy dynamicznego ADT, który efektywnie będzie obsługiwał następujące operacje:- wyszukiwania elementu (serach)- znajdowanie minimum/maksimum – czasami się to przydawało np. przy strukturze kopca- znajdowanie poprzednika, następnika – przydaje się przy operacjach wstawiania i usuwania- wstawianie, usuwanie elementu

Wykorzystamy drzewo binarne. Wszystkie operacje powinny zajmować czas Teta(lg_2(n)).- czyli okolice, pomiędzy.Drzewo powinno być zawsze zbalansowany – inaczej czas będzie proporcjonalny do wysokości drzewa (gorszy od O(lg_2(n))).

Struktura drzewa z korzeniem. Każdy węzeł x posiada pola left, right, parent , key.

Własności drzewa BST:Niech x będzie dowolnym węzłem drzewa natomiast niech będzie należał do poddrzewa o korzeniu w x wtedy:- jeżeli należy do lewego poddrzewa to key(y)<=key(x)- jeżeli należy do prawego poddrzewa to key(y)>key(x)

Czyli mamy strukturę gdzie jest dana i dwa wskaźniki na lewo i prawo w drzewie binarnym, dlatego jest łatwe do implementacji. Duplikaty zawsze idą w jedną stronę. Zasada prosta a łatwo się szuka po wartościach klucza w węźle. Definicja drzewa NIE mówi o jednoznaczności, mogą być złożone z tych samych węzłów ale różnych ścieżek a są niby tymi samymi drzewami. Są trzy porządki przechodzenia w drzewach:

In-order – z korzenia idę w lewo potem znów w lewo jak dojdę do końca to wracam troszkę i idę na prawo itp. Czyli do przykładu ze slajdów byśmy mieli kolejność: 2 3 5 5 7 8 , a ad przykład drzewa obok byśmy mieli: 2 3 5 5 7 8 czyli drzewa są różne, złożone z tych samych węzłów i przechodzenie In order otrzymaliśmy to samo i to posortowane. To wynika z zasady budowy drzewa.

Pre-order Najpierw korzeń potem lewe poddrzewo i potem prawe, czyli byśmy mieli 5 3 2 5 7 8 a to drugie by było: 2 3 7 5 5 8 czyli nie mamy ani tego samego ani posterowanego

post-order – najpierw dzieci potem rodzic czyli lewe potem prawe i na koniec korzeń, pierwsze drzewo by było: 2 5 3 8 7 5 a drugie to 5 5 8 7 3 2 i znów mamy co innego i nie ma posortowania.

Poszukiwanie w drzewie BSTRekurencyjne lub iteracyjne UZUPELNIC KOD CormenTree – Serach(x,k)If x = null lub…

Sprawdzam czy szukany klucz jest mniejszy czy większy do danego węzła i jeśli jest mniejszy to idę w lewo, a większy to w prawo no i trzeba kiedyś zakończyć albo gdy nie

Page 5: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 5

ma gdzie szukać, czyli jestem w liściu, albo znalazłam to czego szukałam. Jeśli szukamy 4 to w pewnym byśmy mieli 3 a w prawym 5, bo tyle węzełków musimy przejść i stwierdzić ze nie są równe. Złożoność zależy od wysokości drzewa log(n) lub n – ilość elementów.Złożoność O(h).

Przechodzenie przez wszystkie węzły drzewaNp. chcemy wypisać wszystkie elementy In orderInorder-Tree-Walk(x)If x różny od null thenInorder-tree-walk(left[x})Print key[x]Inorder-tree-walk(right[x])

Czybyśmy chcieli pre-order to byśmy musieli wystarczy zmienić kolejność print i pójścia w lewi, a jak zmienimy kolejność dwóch ostatnich instrukcji to dostaniemy porządek post-order.

Czas wykonania T(0)=Teta(1)T(n)=T(k)+T(n-k-1)+Teta(1)złożoność Teta(n).

Odnajdowanie minimum i maksimumNajmniejszy to ten w relacji ze wszystkimi

Tree-Minimum(x)While left[x] różne od nullDo x:=left[x]Return x

Tree-maksimum(x)While right[x] różny od nullDo x:=right[x]Return x.

Złożoność O(h) w najgorszym razie zajmuje tyle ile trwa najdłuższa ścieżka – liniowa względem ilości elementów, a pry dobrze zbudowanym drzewie dostaniemy logarytm z ilości węzłów.

Odnajdowanie następnikaNastępnikiem x nazywamy najmniejszy element y wśród elementów większych od x. Następnik może zostać odnaleziony bez porównywania kluczy. Jest to:

1. null jeśli x jest największym z węzłów (bo z definicji nie może mieć następnika)2. minimum w prawym poddrzewie t jeśli ono istnieje3. najmniejszy z przodków x dla których lewy potomek jest przodkiem x

Wstawianie elementówWstawianie jest bardzo zbliżone do odnajdowania elementu: i odnajdujemy właściwie miejsce w drzewie w które chcemy wstawić nowy węzeł z. Dodawany węzeł z zawsze staje się liściem. Zakładamy, że początkowo left(z) oraz right(z) mają wartość null. Duplikaty wstawiamy tak samo, bo czemu by nie. Równe idą na lewo wg naszej zasady. Są też inne strategie, ale ta jest najprostsza.

Usuwanie z drzewa BSTUsuwanie elementu jest bardziej skomplikowane niż wstawianie. Można rozważyć trzy potomków usuwania węzła z:

1. z nie ma potomków: usuwamy z i zmieniamy u rodzica wskazanie na null

Page 6: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 6

2. z ma jednego potomka (usuwany węzeł zastępujemy dzieckiem, wystarczy tak skleić, nie ważne czy to dziecko jest lewe czy prawe, czyli jakby zastępujemy ojca wnukiem jak w carskiej armii ;) )

3. z ma 2 potomków (teraz jest już kłopot) Rozwiązanie polega na zastąpieniu węzła jego następnikiem(najmniejszy wśród większych od niego).Założenie: jeśli węzeł ma dwóch potomków, jego następnik ma co najwyżej jednego potomkaDowód: jeśli węzeł ma 2 potomków to jego następnikiem jest minimum z prawego poddrzewa. Minimum nie może posiadać lewego potomka (inaczej nie byłoby to minimum).

Analiza złożonościUsuwanie: dwa pierwsze przypadki wymagają O(1) operacji: tylko zamiana wskaźnikówPrzypadek trzeci wymaga wywołania tree-successor i dlatego wymaga czasu O(h)Stąd wszystkie dynamiczne operacje na drzewie BST zajmują czas O(h) gdzie h jest wysokością drzewa.W najgorszym przypadku wysokość ta wynosi O(n).

Drzewa BST sa wygodne do efektywnego implementowania operacji:Search, Successor, Predecessor, Minimum, Maximum, Insert, Delete w czasie O(h), (gdzie h jest wysokoscia drzewa)_ Jeśli drzewo jest zbalansowane (ma wtedy wysokość h = O(lg n)), operacje te sąnajbardziej efektywne._ Operacje wstawiania i usuwania elementów mogą powodować, że drzewo przestaje być zbalansowane. W najgorszym przypadku drzewo staje się lista liniowa (h = O(n))!

RotacjeBędziemy przebudowywać drzewo, czyli zamieniamy w sumie kolejność węzłów by je wywłaszczyć, manipulujemy trzema węzłami. Przykład na slajdach z 2 pliku. Taka rotacja nam pomaga, gdy chcemy zmniejszyć głębokość, gdy się gdzieś robi za długa ścieżka, to taka rotacja zmniejsza ścieżkę o jeden, (w drugim poddrzewie ją o 1 zwiększy, ale może nam to nie przeszkodzi jeśli i tak w tym poddrzewie będzie znacznie mniejsza ścieżka) to nie załatwia nam problemu, ale jest pewnym krokiem w dobrą stronę.Zachowują własność drzewa BST.Zajmują stały czas O(1) – stałą ilość operacji na wskaźnikachRotacje w lewo i w prawo są symetryczne.

Kawałek pseudokodu, chodzi o inteligentne podwieszanie wskaźników. Nie ma tu zależności od ilości węzłów, to tylko manipulowanie trzema wskaźnikami – dwoma + uwzględnienie rodzica. Był przykład slajd 29.

Wykorzystanie tej rotacji:

Drzewa zbalansowane AVL i 2-3-4

Drzewa są różne, ale my poznamy w sumie trzy rodzaje – te najciekawsze. AVL – próbują byś idealne, próbują być zawsze dobrze wyważone. Wymaga to operacji długotrwałych typu przebudowywanie drzewa przy każdej operacji wstawiania i usuwania, a to powoduje obciążanie węzła – dodaje się licznik, który wskazuje na jego głębokość i na tej podstawie decydujemy, czy coś robić czy nie.

Drzewa 2-3-4 to jakiś podzbiór drzew które nie mają struktury binarnej, odmiana B drzewa, ilość potomstwa maksymalnie 4. Gdybyśmy chcieli mieć drzewo i nie wiadomo ile potomków będzie to byśmy musieli każdemu kazać pamiętać rodzica i jednego z rodzeństwo, tworzyć jakby listę dzieci, a węzeł pamięta tylko najstarsze dziecko. Czyli operacje na takim drzewie byłoby bardziej skomplikowane niż na binarnym. Dlatego my

Page 7: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 7

się decydujemy na coś innego – na drzewa 2-3-4, czasami nazywane drzewami 2-4. Można np. obciążać czterema adresami węzłów każdy nasz węzełek, tak jak teraz obciążamy dwoma.

Są też drzewa czerwono – czarne, tam koszt jest obciążenie tylko jednego bitu –pamiętamy kolor, kwestia stanu, chodzi o to by węzły o tych samych kolorach nie były obok siebie. A to już nam pozwoli na utrzymanie niezłej struktury tego drzewa. Sedgewick wyprowadza drzewa czerwono czarne z drzew 2-3-4 choć można podejść, że to są inne struktury, kwestia podejścia.

Drzewa zbalansowaneStaramy się znaleźć takie metody operowania na drzewie, żeby pozostawało ono zbalansowane._ Jeżeli operacja Insert lub Delete spowoduje utratę zbalansowania drzewa, będziemy przywracać te własność w czasie co najwyżej O(lgn), tak aby nie zwiększać złożoności._ Potrzebujemy zapamiętywać dodatkowe informacje, aby to osiągnąć._ Najbardziej popularne struktury danych dla drzew zbalansowanych to:– Drzewa AVL: równica wysokości dla poddrzew wynosi co najwyżej 1– Drzewa 2-3-4 drzewa zbalansowane, ale nie binarne– Drzewa czerwono-czarne: o wysokości co najwyżej 2(lg n + 1)

Drzewa AVLBędziemy patrzeć na operacje, które są dynamiczne zarówno w tych drzewach jak i w 2-3-4, czyli na operacje wstawiania i usuwania. Oczywiście istotna będzie złożoność obliczeniowa owych operacji. Na razie mamy zagwarantowaną złożoność liniową drzewem BST, bo mówimy, że może być lepiej w BST, ale nie mam gwarancji że będzie. Chcielibyśmy dostać złożoność obliczeniową logarytmiczną i dostaniemy, ale za cenę pilnowania tego drzewa, zawsze jak coś z nim zrobi to muszę od razu poprawiać to drzewko.

Definicja drzewa AVLDrzewem AVL nazywamy drzewo BST takie, że dla każdego z węzłów różnica wysokości jego lewego i prawego poddrzewa wynosi co najwyżej 1. Nie zagwarantuje równości bo wystarczy wziąć jako drzewo węzeł z jednym dzieckiem, czyli równość nie zawsze musi być. W implementacji – dodajemy do węzła info o jego głębokości i ewentualnie się modyfikuje w zależności od tego. Skrót AVL pochodzi od nazwisk twórców Adelson – Velskii oraz Landis.

Twierdzenie – wysokość drzewa AVL przechowującego n węzłów wynosi O(logn). Dowód na wykładach na stronce.

Wstawiamy tak jak do drzewa BST – zawsze dodajemy nowy liść. Łatwo jest modyfikować info o głębokości, bo skoro wstawiamy jak w BST to przechodzimy całą ścieżkę wiec znamy głębokość poddrzewa, łatwo modyfikuję głębokość węzła – tylko patrzę, czy idę w lewo czy w prawo, bo przecie modyfikuję głębokość ‘rodzica’. Jeśli się zepsuje drzewo, w sensie przestanie ono być AVL to odnajdujemy pierwsze miejsce, gdzie jest źle, pierwsze poddrzewo w którym się problem psuje i wtedy naprawiam drzewo w tym miejscu i potem pójdę do góry i najwyżej będę też poprawiać wyżej.

Przebudowa drzewaNiech (a,b,c) będzie pożądaną listą wierzchołków w porządku In orderPrzeprowadzimy rotacje niezbędne do przemieszczenia b na górę poddrzewa.

Jeśli te trzy węzły są wszystkie na jedna stronę to robimy tak by b poszedł na górę, a jeśli tak nie jest to musimy robić dwie rotacje – najpierw prawa c a potem lewa a – patrz slajdy i przykład. Czas jest stały, czyli pożądany. Ale pamiętajmy, że problem się może przenieść poziom wyżej, przenosimy go do korzenia. Przenosimy go wyżej tyle ile jest

Page 8: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 8

najdłuższa ścieżka. To nie popsuje nam czasu działania na tym drzewie. Zawsze po jednej rotacji sprawdzam, czy się problem rozwiązał sprawdzając głębokości poszczególnych węzłów.

Usuwanie z drzewa AVLUsuwamy jak w BST i to znów może nam popsuć kwestię zbalansowania. Znów naprawiamy poprzez rotację. Szukamy pierwszego węzła, który jest niezbilansowany. Zwykle to oznacza zmianę korzenia drzewa. Dodawanie też mogło zmienić korzeń. _ Niech z będzie pierwszym „niezbalansowanym” węzłem, na który natrafiamy idąc od w. Niech y będzie dzieckiem z o większej wysokości, a x jego dzieckiem o większej wysokości (patrz przykład na slajdzie 19)._ Przeprowadzamy przebudowę drzewa w x , tak aby przywrócić zbalansowanie w z (węzły a,b,c maja być w porządku inorder)._ Ponieważ operacja taka może zachwiać balans w węźle powyżej - musimy sprawdzać zbalansowanie drzewa powyżej (aż do korzenia)

Złożoność obliczeniowa operacji na drzewach AVLPojedyncza przebudowa zabiera czas O(1) – jeśli korzystamy ze struktury łączonego drzewa binarnego (tylko 1 lub 2 rotacje)Wyszukiwanie zajmuje O(logn) – wysokość drzewa wynosi O(logn)Wstawianie zajmuje O(log n)– Odnalezienie miejsca O(log n)– Przebudowa drzewa O(log n) – ze względu na wysokość drzewaUsuwanie - O(log n)– Odnalezienie zabiera czas O(log n)– Naprawa drzewa O(log n) – ze względu na wysokość drzewa

Przypomnijmy, że nie zostaliśmy przy drzewku BST dlatego, bo chcemy miećdrzewko prawie zbalansowane, czyli by zapewnić sobie czas operacji logarytmiczny, a nie tak jak było w zdegenerowanym BST tj kwadratowy. Przy rotacji w lewo wysokość lewego poddrzewa się zmniejszy o 1, a prawego poddrzewa się zwiększy o 1 LUB NIE!

Drzewa 2-3-4Określenie: To drzewa zbalansowane, ale nie są oczywiście binarne. Drzewem poszukiwań o wielu drogach (B-drzewem) nazywamy uporządkowane drzewo o następujących cechach:- każdy węzeł wewnętrzny posiada co najmniej 2 potomków i przechowuje d-1 elementów- kluczy, gdzie d jest ilością potomków węzła.Dla każdego węzła o potomkach v1 v2 …vd przechowującego klucze k1k2 … kd−1• Klucze w poddrzewie v1 są mniejsze od k1• Klucze w poddrzewie vi są pomiędzy ki−1 i ki (i = 2, …, d − 1)• Klucze w poddrzewie vd są większe od kd−1- Liście nie przechowują kluczy.Też jest kłopot z duplikatami, trzeba przyjąć, gdzie one mają iść.

Implementacja:Albo zrobimy wiele wskaźników, tj. tyle by wystarczyło, czy ograniczam z góry liczbę dzieci, ale jeśli dopuszczę do miliona dzieci, to trzymanie w każdym węźle miliona wskaźników jest bez sensu. Wtedy węzłem jest lista kluczy, struktura dynamiczna, trzymam w kluczu wskaźnik na pierwszy element, on trzyma wskaźnik na drugi itp. Czyli trzymanie listy to przecież trzymanie pierwszego wskaźnika. Ale co wtedy z dziećmi? Robimy tak, by dziecko pamiętało, kto jest jego rodzicem, taka odwrócona sytuacja, czyli mam strzałeczki w dwie strony. Rodzic pamięta, kto był pierwszym dzieckeim, czyli pamięta najstarsze dziecko, a dziecko pamięta od siebie to o jeden młodsze.

Page 9: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 9

Przechodzenie inOrder w drzewach o wielu gronach- możliwe jest rozszerzenie notacji odwiedzania węzłów inOrder z drzew binarnych na drzewa o wielu drogach- odwiedzamy element (k_i, o_i) w węźle v pomiędzy rekursywnymi odwiedzinami poddrzew v o korzeniu w v_i oraz v_(i+1)- odwiedzane węzły w tak zdefiniowanym porządku inOrder są uporządkowane rosnąco

Poszukiwanie podobne do BST, przykład na slajdach.Przyda się jednak ograniczenie ilości dzieci, bo gdyby ta ilość byłą nieograniczona, to moglibyśmy porównywać szukaną wartość z tymi, które są w węźle w nieskończoność. Nie ważne, czy jest ograniczona piątką, czy milionem, chodzi o samo podejście, że musi to ograniczenie być.Dla każdego węzła wewnętrznego o potomkach v1 v2 …vd o kluczach k1 k2 …kd−1:– k = ki (i = 1, …, d − 1):poszukiwanie zakończone sukcesem– k < k1: poszukiwanie kontynuujemy w poddrzewie v1– ki−1< k < ki (i = 2, …, d − 1): poszukiwanie kontynuujemy w poddrzewie vi– k > kd−1: poszukiwania kontynuujemy w poddrzewie vd

B-drzewa są stosowane w bazach danych. Stosujemy je (a nie BST czy AVL) dlatego, bo jak jest więcej dzieci to jest mniejsza wysokość i tu się wykorzystuje własności funkcji logarytm, że ona jest taka ‘płaska’.

Drzewka 2-3-4 - własnościTo drzewa poszukiwań o wielu drogach o następujących własnościach:- własność ilości potomków – każdy węzeł wewnętrzny ma co najwyżej 4 potomków- własność wysokości – wszystkie liście mają tę samą wysokośćW zależności od ilości potomków węzły wewnętrzne będziemy nazywać 2-węzłami, 3-węzłami, 4-węzłami. Chcemy by te drzewa były dokładnie zbalansowane. Można troszkę przeciążać te drzewka. Przy tych drzewach najlepiej jednak pamiętać wskaźniki na wszystkie dzieci, skoro jest ich mało. W implementacji to drzewko najlpiej dokładnie zbalansowane wcale nie jest takie hop.

Twierdzenie: Drzewo 2-3-4 przechowujące n elementów ma wysokość O(logn) Dowód na slajdach. Wyszukiwanie w drzewie 2-3-4 o n elementach zajmuje czas O(logn).Jak dodajemy elementy np. same rosnące, to będziemy musieli wciąż to drzewko przebudowywać, co czwarty element coś będziemy musieli wykonać.

WstawianieNowy element (k,o) wstawiamy do węzła v – ostatniego wewnętrznego węzła, przez który przechodziliśmy poszukując k. Wtedy nie psujemy własności wysokości drzewa, możemy spowodować przepełnienie węzła (v mogłoby się stać piątym węzłem).Przykład na slajdach. Problem przepełnienia można rozwiązać przez podział węzła v:– niech v1 … v5 będą potomkami v i k1 …k4 będą kluczami w v– węzeł v zastępujemy 2 węzłami v' i v"• v' jest 3-wezłem o kluczach k1 k2 i potomkach v1 v2 v3• v" jest 2-wezłem o kluczach k4 i potomkach v4 v5– klucz k3 jest wstawiany do rodzica u węzła v (to może tworzyć nowy korzeń)_ Przepełnienie może teraz nastąpić w węźle u

Analiza wstawianiaAlgorytm insertItem(k, o)1. Odszukujemy klucz k w celu zlokalizowania węzła do wstawienia wartości v2. Dodajemy nowy element (k, o) w węźle v3. while overflow(v)

if isRoot(v)

Page 10: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 10

twórz nowy pusty korzeń nad vv <- split(v)

Niech T będzie drzewem 2-3-4 o n elementach– T ma wysokość O(log n)– krok 1 zajmuje czas O(log n), ponieważ odwiedzamy O(log n) węzłów– krok 2 zajmuje czas O(1)– krok 3 zabiera O(log n) czasu, ponieważ każde rozdzielanie zabiera O(1) i możemy mieć O(log n) takich operacji_ Stad wstawianie do drzewa 2-3-4zajmuje czas O(log n)

Jak będziemy dzielić i sytuacja problemowa nam się przeniesie do korzenia, to dzielimy korzeń, czyli stworzymy nowy korzeń i z niego dwoje dzieci. Czyli mamy logarytmiczny czas znalezienia problemu, potem stały czas zabawy wskaźnikami i znów logarytmiczny czas naprawy.

UsuwanieRozważania na temat usuwania można sprowadzić tylko do przypadku usuwania wartości z węzła posiadającego jedynie potomków – liście, wtedy się popsuje zbalansowanie.W przeciwnym razie (węzły wewnętrzne) zastępujemy element kolejnym w porządku Inorder (następnikiem, jego lewe dziecko musi być liściem).Przykład na slajdachUsunięcie elementu z węzła v może spowodować niedobór – v może stać się 1-węzłem (0 klucz, 1 potomek)Dla obsługi tej sytuacji należy połączyć v z rodzicem u, rozpatrzmy dwa przypadkiPrzypadek 1Sąsiedni brat v jest 2-węzłem- operacja łączenia: łączymy v z bratem w i przenosimy element z u do połączonego węzła v- po połączeniu, niedobór może nastąpić w węźle powyżej uPrzypadek 2Sąsiedni brat w węzła v jest 3-węzłem lub 4węzłemPrzenosimy potomka w do v; element z u do v; element z w do uPo przeniesieniu nie występują przepełnienia!! O tyle jeśli się da doprowadzić do drugiego przypadku to z niego koniecznie korzystamy!Przykład na slajdach.

Analiza usuwaniaNiech T będzie drzewem 2-3-4 o n elementach. Wysokość T wynosi O(logn)Operacja usuwania- odwiedzamy O(logn) węzłów aby odszukać węzeł z którego usuwamy element- obsługa niedoborów może prowadzić do wykonania serii O(logn) łączeń węzłów (przyp) oraz jednego przesuwania (przyp 2)- każde łączenie zajmuje O(1)Stąd operacja usuwania w drzewie 2-3-4 zajmuje czas O(log n).

Gdybym miała drzewko korzeń a, z dziećmi b,c i usuwam c, to zmieniam całość tj zmniejszam ilość poziomów i będę miała tylko korzeń w którym będzie a,b.

B-drzewa są wbrew pozorom jednak dość nieefektywne, dlatego szukamy fajniejszych struktur i w ten sposób doszliśmy do…

O drzewach czerwono czarnych będzie innym razem (czas balansowania w AVL jest logarytmiczny a to dość sporo, dlatego w RB będzie fajniej, bo będzie to struktura binarna, tyle że jeszcze z tymi kolorkami). W przeciętnym słowniku koszt operacji to O(n) – lista liniowa

Page 11: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 11

Lepszy słownik to O(ln n) – drzewo Pamięć jest przy dużych obiektach ta sama, operacje są trochę bardziej skomplikowane, jednak tanim kosztem można przejść między tymi wersjami. Ale da się jeszcze taniej –by czas poszukiwania był w idealnym układzie stały niezależnie od wielkości zbioru. Operacje dynamiczne też będziemy wykonywać w czasie niezależnym od rozmiaru zbioru, co przy dużej bazie danych ma sens. Czyli zejdziemy na czas stały O(1), co przeczy intuicji, ale jest możliwe.

Haszowanie Cormen rozdział 12Tablice z adresowaniem bezpośrednim – pokażemy że to w ogóle możliwe, adresowanie kluczem, czyli w tablicy indeksami będą wartości kluczy. Czyli odnajduję miejsce w tablicy o danym kluczu i sprawdzam czy jest to miejsce puste czy nie, jeśli tak to wstawiam tam element. - Problem jest tutaj taki, że rozmiar tablicy jest tak duży jak rozmiar kluczy, czyli np. mam miliard kluczy a zajmuję tylko 100 miejsc. - Drugi problem – nie da się wsadzić tu duplikatów, stąd próbujemy pokombinować i dochodzimy do kolejnego rozwiązania:

Tablice z haszowaniem (Adresowanie otwarte, haszowanie łańcuchowe) – bierzemy proste funkcje, które się dobrze liczy, by była jak najprostsza i ona będzie odrzuca ileś rzeczy tak że otrzymamy możliwości wstawiania do dużo mniejszej tablicy, będzie nam w miarę równomiernie rozrzucać te klucze, nie jest różnowartościowa, wiec nie da się jej odwrócić. A co jeśli dwa różne klucze zostaną przeprowadzone na to samo miejsce? Pierwsze podejście do adresowanie otwarte – wsadzamy element w następne wolne miejsce. Czyli jak chcę zobaczyć czy jakiś klucz jest w tablicy, to patrzę w które miejsce mnie wrzuciła funkcja haszująca, jeśli nie mam tego klucza to szukam dalej i postępuję według jakiegoś podejścia. Haszowanie łańcuchowe to możliwość wsadzania iluś elementów do jednego miejsca, jeśli jest już miejsce zajęte, to buduję w nim listę liniową, czyli mam tablicę list.

Funkcje haszujące (mieszające) – tu będzie właśnie problem by dobrać te funkcje, zwykle są to funkcje w których się coś mnoży i bierze modulo rozmiar tablicyHaszowanie uniwersalneIdealne Haszowanie

Nie zakładamy uporządkowania kluczy. Drzewa poszukiwań BST wymagają relacji porządku i dają czas O(lg n), a tablice z haszowaniem pozwalają na dowolne klucze i dają średni czas O(1) – statystyczny przypadek przy odpowiedniej pamięci, jednak jeśli tablica jest za mała, a kluczy jest dużo to nie otrzymam tego czasu, wiec jednak muszę ileś pamięci mieć, niekoniecznie tak dużo jak w BST, ale nadal…. W tych tablicach jednak przez to się niefajnie znajduje najmniejszą lub największą wartość właśnie dlatego, bo nie mamy porządku, klucze są rozrzucane niezależnie od ich wartości, czyli jak mamy jakiekolwiek zadanie związane z porządkiem to Haszowanie jest złe.

Tablice z adresowaniem bezpośrednim- Korzystamy z tablicy o rozmiarze zgodnym z możliwym zakresem kluczy- Wykorzystujemy klucz k jako indeks tablicy A (k->A[k])- Rozmiar tablicy z haszowaniem jest proporcjonalny do zakresu kluczy, nie do ilości elementów- Potrzeba bardzo dużo pamięciMożna np. trzymać 1 jeśli element jest ,a 0 jeśli go nie ma. Jak usuwamy, to ustawiamy A[k] = 0, czyli operacje są proste i działają ;)To troszkę podobne do sortowania przez zliczanie.

Oto co próbujemy z tym fantem zrobić:

Page 12: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 12

Zwykle jeśli mam milion kluczy to tablicę biorę dwa miliony i wstawiając elementy wciąż sprawdzam, czy nie zapełniłam już połowy, jeśli tak to rozszerzam. Rozmiar tablicy z haszowaniem jest proporcjonalny do ilości elementów , nie do ich zakresu.

Do tej pory mieliśmy funkcje identycznościową, a teraz obliczamy indeks jako wartość pewnej funkcji haszowania h(k), czyli buduje funkcję, która działa z mojego zbioru indeksów w pewien mniejszy zbiór (np. 0..n-1), dlatego nie jest ona różnowartościowa. Zatem jeśli trafię z dwoma kluczami w to samo miejsce to albo przesuwam, albo buduję listę. Przykład to tablice rejestracyjne czy loginy i hasła użytkowników systemu.

Trzeba dobrać tak funkcje, by nie powielała informacji zawartej w kluczu, w sensie zła jest funkcja haszująca taka, że jak ma adres IP to bierze tylko pierwszy oktet, a resztę ucina, wtedy skoro jest tylko jedne oktet pierwszy dla Afryki, to całą Afrykę byśmy wsadzili do jednego pola. Ale jeśli weźmiemy już modulo coś, to sytuacja się zmieni.

Formalnie:Niech U będzie zbiorem wszystkich możliwych kluczy rozmiarze |U|, K – aktualny zbiór kluczy o rozmiarze n, T tablica z haszowaniem o rozmiarze O(m), m <= |U|._ Niech h(k) będzie funkcją haszującą: h(k): U->[0..m–1] mapującą klucze z U na indeksy tablicy T. wartość h(k) można obliczyć w czasie O(|k|) = O(1)._ Elementy tablicy T[i] sa dostępne w czasie O(1). T[i] = null jest wejściem pustym._ Dla uproszczenia można przyjąć U = {0, …, N –1}.

Patrzmy jaki podzbiór kluczy nas interesuje i do niego dobierajmy funkcję haszująca. Na sam koniec zawsze musimy pamiętać zrobić modulo rozmiar tablicy, by trafić w tablicę. Krytycznie źle dobrana funkcja – wszystkie klucze trafiają w jedno miejsce tablicy. Wtedy się czas zdegeneruje do liniowego.

Adresowanie bezpośrednie jako HaszowanieTablica z haszowaniem ma rozmiar m T[0..m-1]Ilość wykorzystywanych kluczy n jest bliska m, lub m jest mała ( w porównaniu z dostępnymi zasobami, pamięcią)Klucz k staje się indeksem T, tj. funkcja mieszająca jest następująca: h(k) = k._ Nie ma kolizji, czas dostępu wynosi O(1), w najgorszym wypadku._ Nie ma potrzeby przechowywania klucza – tylko dane._ Problemy: Metoda ta wymaga dużo pamięci i jest niemożliwa do zrealizowania dla dużych m

Tablica z haszowaniem- Możemy używać funkcji mieszającej wiele do jednego h(k) do mapowania kluczy k na indeksy T- Zbiór wykorzystywanych kluczy K może być dużo mniejszy od przestrzeni kluczy U, tj. m « |U|._ Rozwiązywanie kolizji kiedy h(k1) = h(k2) dwoma metodami:I. Adresowanie otwarteII. Łańcuchy

Współczynnik zapełnieniaZałożenie o równomiernym haszowaniu: dla każdego klucza wszystkich m! permutacji zbioru {0..m-1} jest jednakowo prawdopodobnych jako ciąg kolejnych próbkowań (ciąg kontrolny)

Definicja:Współczynnikiem zapełnienia a dla tablicy z haszowaniem T z m pozycjami określamy stosunek n/m gdzie n jest ilością przechowywanych elementów. Jest to średnia liczba elementów przechowywanych w jednej pozycji tablicy.

Page 13: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 13

Haszowanie przez adresowanie otwarteWszystkie klucze k , mapowane w tą samą pozycję T[h(k_i)] są wstawiane bezpośrednio do tablicy w pierwsze wolne miejsce.Poszczególne pozycje tablicy mogą zawierać elementy albo wartości null.Funkcja haszująca powinna być zmodyfikowana (dwuargumentowa, gdzie drugi argument jest numerem próby)H(k,i): U x [0..m-1] ->[0..m-1]Sekwencja kolejnych miejsc w tablicy dla elementu h(k,0), h(k,1),…Chodzi o to by to rzadko liczyć, by nie była tu zależność od ilości elementów, by te łańcuszki były krótkie. Przy dobrze dobranej funkcji i odpowiedniej wielkości tablicy przy którejś próbie będzie dobrze. Czyli chodzi o to by kolejne próby teoretycznie zapełniły wszystkie miejsca w tablicy, a nie o sytuację, że np. zapełniam tylko miejsca parzyste.

Operacje potrzebne dla adresowania otwartegoInsert: testujemy kolejne miejsca w tablicy, aż do odnalezienia wolnego. Sekwencja kolejnych miejsc do sprawdzenia zależy od wstawianego klucza. Jeśli nie odnajdujemy wolnego miejsca po m próbach, oznacza to, że tablica jest pełna.Search: testujemy te sama sekwencje pozycji co przy wstawianiu, albo do momentu odnalezienia poszukiwanego klucza (sukces), albo natrafienia na wolna pozycje (porażka).Usuwanie:Usuwanie jest kłopotem tym razem, bo jeśli pozbawimy ciągłości sekwencję próbkową to będzie źle…Bo jeśli mamy tak po prostu klucze k1, k2, k3, k4, k5 i usuniemy po prostuk3 to stracimy k4 i k5, a to dlatego, bo gdybyśmy wtedy chcieli znaleźć k4, to liczę h(h4,0) i porównuję klucze, skoro nie jest null, to liczę następną próbę, czyli wchodzę w drugie miejsce tablicy h(h4,1) znów zajęte, ale nie to o co nam chodzi, a przy kolejnym próbkowaniu trafię na null czyli nie znajdę h4. Można więc przeliczyć wszystkie następne hasze, ale to za czasochłonne, za to można ustawić znacznik, że jakiegoś klucza nie ma, to tylko znacznik, który mówi, ze klucza nie ma, ale nie zostaje przerwana sekwencja próbkowania. Przykład tego w bazach danych.

Złożoność : zależna od długość sekwencji próbkowania.

Strategie próbkowania- adresowanie liniowe (to, że w kolejnej próbie biorę pod uwagę kolejne miejsce)- adresowanie kwadratowe (czyli w kolejne próbie idę do kwadratu…, czyli się wciąż oddalam coraz dalej, jak funkcja kwadratowa)- Haszowanie dwukrotne

Adresowanie linioweFunkcja haszująca h(k,i) = (h’(k)+i)mod m Gdzie h’(k) jest zwykłą funkcją haszująca niezależną od numeru próby.Dla klucza k sekwencja próbkowania jest wtedy następująca:T[h’(k)], T[h’(k)+1], … T[m–1 ], T[0], T[1] … T[h’(k)–1]

Problem: tendencja do grupowania zajętych pozycji (czyli się łączą takie bloczki ze sobą…). Długie spójne ciągi zajętych pozycji szybko się powiększają, co spowalnia operacje poszukiwania.

Wykorzystywana przy kluczach gdzie jest niedużą dynamika.

Adresowanie kwadratoweFunkcja haszującah(k, i) = (h’(k) + c1i + c2i^2) mod mgdzie c1 i c2 stałe różne od 0, h’(k) zwykła funkcja haszująca (niezależna od numeru

Page 14: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 14

próby).W przeciwieństwie do adresowania liniowego, kolejne rozpatrywane pozycje są oddalone od początkowej o wielkość zależną od kwadratu numeru próby i.

Zwykle dodaje się tylko kwadrat numeru próby, wtedy nie powstaną nam te bloczki.W przeciwieństwie do adresowana liniowego, kolejne rozpatrywane pozycje są oddalone od początkowej o wielkość zależną od kwadratu numeru próby i.Prowadzi to do mniej groźnego zjawiska grupowania - określanego jako grupowanie wtórne: klucze o tej same pozycji początkowo dają takie same sekwencje.

Haszowanie podwójneFunkcja haszującah(k, i) = (h1(k) + ih2(k)) mod mgdzie h1(k) i h2(k) dwie funkcje haszujące.Pierwsza sprawdzana pozycja to T[h1(k)]. Kolejne próby są oddalone od początkowej o h2(k) mod m.Wartość h2(k) powinna być względnie pierwsza z rozmiarem tablicy m, aby mieć gwarancje że cała tablica zostanie przeszukana.Przykłady funkcji:h1(k) = k mod mh2(k) = 1 + (k mod m’) gdzie m’ < m (np. m-1, m-2)Generuje Teta(m^2) istotnie różnych sekwencji (dla liniowego i kwadratowego Teta(m) )

Przesuwamy się o wielokrotność drugiej funkcji haszującej a nie o numer próby.

Analiza adresowania otwartegoTwierdzenie: Jeśli współczynnik zapełnienia tablicy z haszowaniem wynosi a=n/m <1, to oczekiwana liczba porównań kluczy w czasie wyszukiwania elementu (przy spełnieniu założeniu o równomiernym haszowaniu)- co najwyżej 1/(1-a) dla nieudanego poszukiwania- co najwyżej 1/a ln 1/(1-a) dla udanego poszukiwaniaJeśli a jest stałe, to czas poszukiwania wynosi O(1).

BY oczyścić tablicę, to trzeba znów przeliczyć wszystkie wartości haszy.

Rozwiązywanie kolizji metodą łańcuchową- Wszystkie klucze k, które trafiają w tę samą pozycję umieszcza się w liście liniowej…- elementy tablicy przechowują wskaźniki do list Lj- wstawianie: nowy klucz wstawiany jest na początek listy Lj (Czas O(1)) – tak jak zawsze na programowaniu, że tworzę nowy węzełek i przepinam jeden wskaźnik, czyli mamy tablicę kluczy- poszukiwanie / usuwanie – przeszukujemy listę w poszukiwaniu klucza (czas proporcjonalny do ilości elementów najdłuższej listy)Nadal chodzi o to by te listy nie były za długie – czyli o dobry dobór funkcji haszujacej. To przypomina sortowanie kubełkowe.

„Trzeba ją inteligentnie wykorzystać”

Jak mam adresowanie otwarte intów i sizeof(int) = 4 to biorę 4* 2^32 to wyjdzie chyba koło 4 giga, a przy long int który ma 8 bajtów to…

Od tego miejsca czyste przepisanie ze slajdów, brak omówienia na wykładzie…

Proste równomierne haszownie oznacza, że losowo wybrany element z jednakowym prawdopodobieństwem trafia na każdą z m pozycji, niezależnie od tego gdzie trafiają inne elementy.

Page 15: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 15

Twierdzenie: w tablicy haszowania wykorzystującej łańcuchową metodę rozwiązywania kolizji, przy założeniu prostego, równomiernego haszowania, średni czas działania procedury wyszukiwania (zarówno dla sukcesu, jak i pokraki) wynosi Teta(1+alfa), gdzie alfa jest współczynnikiem zapełnienia tablicy.

Dobre funkcje haszujące

Wydajność haszowania zależy w olbrzymiej mierze od zbioru wykorzystywanych kluczy oraz funkcji wykorzystywanej do haszowania._ Dobre funkcje haszujące to takie, które spełniają założenie prostego równomiernego haszowania!_ Zwykle jednak nie można (lub nie jest łatwo) stwierdzić, czy założenie takie jest spełnione. Najczęściej nie znamy rozkładu prawdopodobieństwa występowania kluczy._ Szukając dobrych funkcji najczęściej poszukuje się funkcji działających dobrze „w większości wypadków” -> heurystyczne

Heurystyczne funkcje haszujące

Funkcja powinna dobrze się sprawować dla większości zbiorów kluczy (takich, które naprawdę występują w zadaniu), niekoniecznie dla specjalnie przygotowanych „złośliwych” zbiorów kluczy._ Zbiór wykorzystywanych kluczy K zwykle nie jest losowy, ale posiada pewne cechy (np. wspólne początkowe bity, wielokrotności pewnej liczby, itp.)._ Celem jest znalezienie takiej funkcji haszującej, która tak dzieli cały zbiór potencjalnych kluczy U , że dla zbioru kluczy aktualnych K wydaje się on losowy. Przypadek najgorszy (worst-case) powinien być mało prawdopodobny.

Haszowanie modularne

Niech U = N = {0,1,2, …}, będzie zbiorem liczb naturalnych._ Mapujemy klucz k na pozycje m przez branie reszty z dzielenia k przez m:

h(k) = k mod m_ aby taka metoda działała dobrze należy unikać takich m , które są potęgami 2 (m = 2^p) – ponieważ wtedy wybieramy ostatnie p bitów klucza, ignorując istotna część informacji._ Heurystyka: wybieramy jako m liczbę pierwsza odległa od potęg 2.

PrzykładWeźmy |U| = n = 2000 i załóżmy, _e dopuszczamy maksymalnie 3 kolizje dla klucza._ Jaki powinien być rozmiar tablicy (m)?_ Mamy floor(2000/3) = 666; liczba pierwsza bliska tej wartości, a jednocześnie daleka od potęg 2 to np. 701._ Stad funkcja haszująca może być taka: h(k) = k mod 701_ Wtedy klucze 0, 701, i 1402 są mapowane na 0.

Haszowanie przez mnożenie

_ Mapujemy klucz k na jedna z m pozycji przez pomnożenie go przez stałą a z zakresu 0 < a < 1, dalej wyznaczamy część ułamkowa ka, i mnożymy ja przez m:H( k ) = floor (m(ka − floor(ka))) , 0 < a < 1_ Metoda taka jest mniej wrażliwa na wybór m ponieważ_ „losowe” zachowanie wynika z braku zależności pomiędzy kluczami a stała a._ Heurystyka: wybieramy m jako potęgę 2 i a jako liczbę bliska „złotemu podziałowi” :a = ( sqrt5 −1)/ 2 = 0.6180339887...

Haszowanie uniwersalne_ Idea: dobieramy funkcję haszującą losowo w sposób niezależny od kluczy.

Page 16: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 16

_ Funkcja wybierana jest z rodziny funkcji o pewnych szczególnych własnościach (takich, które „średnio” zachowują się dobrze)._ Gwarantuje to, że nie ma takiego zestawu kluczy, który zawsze prowadzi do najgorszego przypadku._ Pytanie: jak określić taki zbiór funkcji?_ Wybieramy ze skończonego zbioru uniwersalnych funkcji haszujacych.

Cel: chcemy aby zachodziło założenie o prostym równomiernym haszowaniu – tak aby klucze były średnio rozproszone równomiernie.Właściwości prostego, równomiernego haszowania:– Dla dowolnych dwóch kluczy k1 i k2, i dowolnych dwóch pozycji y1 i y2,prawdopodobieństwo tego, _e h(k1) = y1 i h(k2) = y2 wynosi dokładnie 1/m2.– Dla dwóch kluczy k1 i k2, prawdopodobieństwo kolizji, tj. h(k1) = h(k2) wynosi dokładnie 1/m._ Chcemy, aby rodzina funkcji haszujacych H była tak dobrana, że szansa kolizji jest taka sama jak przy prostym, równomiernym haszowaniu.

Definicja: niech H będzie skończoną rodziną funkcji haszujacych, mapujacych zbiór dopuszczalnych kluczy U na zbiór {0,1,…,m –1}.H nazywamy uniwersalna jeżeli dla każdej pary równych kluczy k1 i k2 z U, ilość funkcji haszujacych h z H, dla których h(k1) = h(k2) jest równa co najwyżej |H|/m._ Inaczej mówiąc, jeśli losowo wybierzemy funkcje z takiej rodziny to szansa na kolizje pomiędzy różnymi kluczami k1 i k2 nie jest większa niż 1/m.

Twierdzenie: niech h będzie funkcja haszująca, wybrana losowo z uniwersalnej rodziny funkcji haszujacych. Jeśli zastosujemy ją do haszowania n kluczy w tablice T o rozmiarze m, to oczekiwana długość łańcucha, do którego dotłaczany jest klucz (przy założeniu, że alfa = n/m jest współczynnikiem zapełnienia) wynosi:– jeśli k nie ma w tablicy – co najwyżej alfa– jeśli k jest już w tablicy – co najwyżej 1+alfa

Wniosek: Przy zastosowaniu uniwersalnego haszowania i rozwiązywania kolizji metodąłańcuchową, dla tablicy o rozmiarze m, oczekiwany czas n operacji wstawiania, usuwania i wyszukiwania wynosi Teta(n).

Konstrukcja rodziny uniwersalnejWybieramy liczbę pierwszą taką, że p > m i p jest większe od zakresu kluczy aktualnych K . Niech Zp oznacza zbiór {0,…p –1}, i niech a i b będą dwoma liczbami z Zp._ rozważmy funkcje: ha,b(k) = (ak +b) mod p_ Rodzina wszystkich, takich funkcji jest: Hp,m = {ha,b | a,bÎZp i a != 0 }_ Aby wybrać losowa funkcje z tej rodziny – wybieramy losowo a i b ze zbioru Zp.

Lemat: dla dwóch różnych kluczy k1 i k2, oraz dwóch liczb x1 i x2 z Zp,prawdopodobieństwo, że k1 trafi na pozycje x1 i k2 na pozycje x2 wynosi 1/p2.Dowód na slajdach.Ponieważ_ zakres kluczy może być bardzo duży, zawężamy zbiór funkcji haszujacych do m wartosci: ha,b(k) = ((ak +b) mod p) mod mRodzina: Hp,m = {ha,b: = a, b Î Zp}jest poszukiwana rodzina uniwersalna funkcji haszujacych.

Haszowanie uniwersalne daje czas O(1) w przypadku średnim i to dla dowolnego zbioru aktualnych kluczy, nawet jeśli trafiamy na same „złośliwe” układy kluczy._ Szansa na złe zachowanie się metody jest bardzo mała._ Jednak dla dynamicznych zbiorów kluczy, nie możemy powiedzieć z wyprzedzeniem czy dana funkcja będzie dobra, czy nie…

Page 17: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 17

Haszowanie idealneHaszowanie uniwersalne zapewnia średni czas O(1) dla dowolnego zbioru kluczy._ Czy można zrobić to lepiej? W niektórych przypadkach TAK!_ Idealne haszowanie zapewnia czas O(1) w najgorszym przypadku (worst-case) dla statycznego zbioru kluczy (takiego, w którym klucz raz już zachowany, nie zmienia sie nigdy)._ Przykłady statycznych zbiorów kluczy: słowa kluczowe dla języka programowania, nazwy plików na płycie CD.

Idea: korzystamy z dwupoziomowego schematu haszowania – za każdym razem jest to haszowanie uniwersalne.poziom 1: haszujemy łańcuchowo: n kluczy ze zbioru K jest umieszczanych na m pozycjach w tablicy T z wykorzystaniem funkcji h(k) wybranej z rodziny uniwersalnej.poziom 2: zamiast tworzyć listę liniowa dla kluczy wstawianych na pozycje j, wykorzystujemy druga tablice z haszowaniem Sj skojarzona z funkcja hj(k). Wybieramy hj(k) aby mieć pewność, że nie zachodzą kolizje, Sj ma rozmiar równy kwadratowi ilości nj – kluczy umieszczonych na tej pozycji |Sj| = n_j^2. Przykład na slajdach.

Twierdzenie: jeśli przechowujemy n kluczy w tablicy o rozmiarze m = n^2 przy wykorzystaniu funkcji h(k) losowo wybranej z rodziny uniwersalnej funkcji haszujacych, wtedy prawdopodobieństwo kolizji wynosi < ½ .

Dla dużych n, przechowywanie tablicy o rozmiarze m = n^2 jest kosztowne._ Dla zredukowania potrzebnej pamięci można wykorzystać następujący schemat:– poziom 1: T rozmiaru m = n– poziom 2: Sj rozmiaru mj= n_j^2

_ Jeśli mamy pewność, że dla tablic drugiego poziomu nie ma kolizji, czas dostępu (równie_ worst-case) jest stały._ Pytanie – jaki jest oczekiwany rozmiar potrzebnej pamięci?Rozmiar tablicy pierwszego poziomu - O(n)._ Oczekiwany rozmiar wszystkich tablic drugiego poziomu wynosi: tu szlaczek…_ Wyrażenie w sumie określa łączną ilość kolizji._ Średnio jest to 1/m razy ilość par. Ponieważ_ m = n, daje to co najwyżej n/2._ Stad, oczekiwana rozmiar tablic drugiego poziomu wyniesie mniej niż 2n.

Haszowanie jest uogólnieniem abstrakcyjnego typu danych dla tablicy._ Pozwala na stały czas dostępu i liniowe składowanie dla dynamicznych zbiorów kluczy._ Kolizje rozwiązywane sa poprzez metodę łańcuchową albo otwarte adresowanie._ Haszowanie uniwersalne zapewnia gwarancje oczekiwanego czasu dostępu._ Idealne haszowanie zapewnia gwarancje czasu dostępu w przypadku statycznych zbiorów kluczy._ Haszowanie nie jest dobrym rozwiązaniem przy poszukiwaniach związanych z porządkiem (szukanie maksimum, następnika itp.) – nie ma porządku pomiędzy kluczami w tablicy.

Kompresja danych

Co to jest kompresja danych? Powinny zajmować mniej miejsca niż poprzednio.

Kodowanie to przekształcenie informacji, a kompresja to ściśnięcie informacji, zatem to co innego.

Po co kompresować dane?

Page 18: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 18

Oszczędność przy składowaniu danych – nie zawsze ma miejsce, czasami jak spakujemy coś, to plik ma nadal tą samą długość lub nawet większą, np. nie da się za bardzo skompresować PDFa, a to dlatego, bo PDF, JPG itp. Mają już kompresję same w sobie. By kompresować coś wykorzystujemy strukturę pewnej informacji, po skompresowaniu ta struktura już nie istnieje, zatem program nie skompresuje tego. Bardziej efektywne przesyłanie w sieciach komputerowych

Kompresja tekstuZadanie: mamy tekst z literami z pewnego alfabetu, chcemy zrobić tak by długie łańcuchy stały się krótkimi łańcuchami. Chcemy znaleźć wersję skompresowaną tego tekstu, ale by się dało odtworzyć potem tekst wejściowy. Zatem szukamy odwracalnej funkcji kompresji.Nie da się tak ładnie tego zrobić, bo wszystkie łańcuchy np. do 10znaków nie da się przenieść na łańcuchy np. do 3 znaków, bo przecież w tych do 10znaków znajdują się już te wszystkie do 3 znaków i masa innych. Zatem bez dodatkowych założeń nie da się kompresować. Programy nasze jednak działają, bo wykorzystujemy pewne wartości różnych zbiorów – np. częstość występowania znaków w tekstach.

Zwykle listery w tekście występują ze zróżnicowaną częstością, dla j. angielskiego e 12%, t 10% itp. Znaki specjalne $# występują rzadko. Niektóre znaki występują sporadycznie, są właściwie niewykorzystywane np. początkowe znaki kodu ASCII.

Teksty (pliki) podlegają pewnym regułom:- słowa się powtarzają (występują jedynie słowa z pewnego słownika) – w sensie przechodzimy z poziomu znaków na poziom słów, kodujemy tylko te łańcuchy znaków, które coś znaczą, a nie jakieś trtrt- nie każda kombinacja słów jest możliwa- Zdania mają określoną strukturę (gramatyka)- Programy korzystają z określonych słów (elementy języka programowania)- wzorce są często powielane- obrazy kodowane cyfrowo mają jednolite obszary (bez zmian koloru) – wtedy nie kodujemy każdego piksela z osobna tylko cały obszar

PrzykładPrzyjmijmy, że kod znaku w ASCII zajmuje 1 bajt. Przypuścimy że mamy tekst składający się ze 100 znaków ‘a’ – tekst powinien zajmować 100 bajtów. Przechowując tekst w postaci ‘100a’ możemy dostać dokładnie taką samą informację: rozmiar nowego pliku wyniesie 4bajty. Oszczędność 4/100 -> 96%. Wiadomo że przy zwykłym tekście to nie pójdzie, bo w normalnym tekście nie występują sekwencje tych samych znaków.

Kompresja bezstratna

W 100% odwracalna, poprzez dekompresję można odzyskać oryginalne dane. Poprzedni przykład pokazywał taką kompresję. Musi być ta kompresja stosowana tam, gdzie istotna jest integralność danych. Przykłady oprogramowania: winzip, GIF, bzip. Zwykle właśnie o tą kompresję będzie nam chodziło.

Kompresja stratnaStratna oznacza, że oryginalna informacja nie może być w całości odzyskana. Kompresja ta zmniejsza rozmiar przez permanentne usuniecie pewnej części informacji. Po dekompresji dostajemy jedynie część początkowej informacji (ale użytkownik może tego wcale nie zauważyć, bo jeśli przekłamię ileś pikselów w obrazku, to nikt tego nie zauważy, bo nie widzimy pikseli).Kompresję stratną stosujemy dla plików audio, obrazów, wideo, formaty JPG i mpeg. Mp3 – stratna kompresja.

Page 19: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 19

Kody- Metody reprezentacji informacji- kod dla pojedynczego znaku często nazywany jest słowem kodowym- dla kodów binarnych każdy znak reprezentowany jest przez unikalne binarne słowo kodowe- kody o stałej długości słowa: ASCII, Unicode, długość słowa kodowego dla każdego znaku jest taka sama. – prowadzi to do dużego marnotrawstwa jak już mówiliśmy

Kody stałej długościPrzypuścimy że mamy n znakówJaka jest minimalna ilość bitów dla kodów o stałej długości? Sufit(log_2n).Przykład {a,b,c,d,e} czyli 5 znaków, to sufit z tego logarytmu to 3 bity na znak. Możemy zakodować np. tak: a=000, b=001, c=010, d=011, e=100.

Kody o zmiennej długościDługość słowa kodowego może być różna dla różnych znakówZnaki występujące częściej dostają krótsze kodyZnaki występujące sporadycznie dostają długie kody. Oczywiście trzeba będzie dostarczyć tablicę kodów.Przykład na slajdach.

By się dało rozkodować, to musi być kod prefiksowy, czyli żaden kod nie zawiera się w początku innego kodu, przeanalizować przykład na slajdach. Po prostu jak mam zakodowane słowo, czyli ciąg 0 i 1 to biorę pierwszy znak i patrzę, czy ten znak odpowiada jakiejś literze, jeśli nie to biorę pierwsze dwa itp., jak znajdę że ten kod odpowiada jakiejś literze to znaczy, że to jest ta litera i że tu się ten kod kończy i jadę dalej.

Musimy mieć pewność, że żadne słowo kodowe nie występuje jako prefix innego. Potrzebujemy kodów typu prefiksowego (prefix – free) – ostatni przykład taki byłKody prefix-free pozwalają na jednoznaczne dekodowanie Metoda Huffmana jest przykładem konstrukcji takiego kodu, będzie dokładniej omówiona dalej.

EntropiaEntropia jest jednym z podstawowych pojęć teorii informacjiZa ojca teorii informacji uważa się Claude’a Shanonna który prawdopodobnie po raz pierwszy użył tego terminu w 1945roku w swojej pracy zatytułowanej "A MathematicalTheory of Cryptography". Natomiast w 1948 roku, w kolejnej pracy pt. "AMathematical Theory of Communication" przedstawił najważniejsze zagadnienia związane z ta dziedzina nauki.Entropia pokazuje, że istnieje ograniczenie dla bezstratnej kompresji

Ta granica jest nazywana entropia źródła H.H określa średnią liczbę bitów niezbędną dla zakodowania znaku. Niech n będzie rozmiarem alfabetu, p_i prawdopodobieństwem występowania (częstością) i-tego znaku alfabetu. Wtedy entropia określona jest wzorem:H = suma (od i=1 do n) –p_i log_2p_i

Przykład na slajdach

Algorytm kodowania HuffmanaHuffman wymyślił sprytną metodę konstrukcji optymalnego kody prefixowego (prefix free) o zmiennej długości słów kodowychKodowanie opiera się o częstość występowania znakówOptymalny kod jest przedstawiony w postaci drzewa binarnego

Page 20: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 20

– Każdy węzeł wewnętrzny ma 2 potomków– Jeśli |C| jest rozmiarem alfabetu – to ma ono |C| liści i |C|-1 węzłów wewnętrznych

Budujemy drzewo od liści (bottom up). Zaczynamy od |C| liści. Przeprowadzamy |C|-1 operacji ‘łączenia’ . Niech f[c] oznacza częstość znaku c w kodowanym tekście. Wykorzystamy kolejkę priorytetową Q w której wyższy priorytet oznacza mniejsza częstotliwość znaku:– GET-MIN(Q) zwraca element o najniższej częstości i usuwa go z kolejkiPseudokod na slajdach.Czas wykonania O(nlgn)

Kodowanie Huffmana–Jest adaptowane dla każdego tekstu–Składa sie z• Słownika, mapującego każdą literę tekstu na ciąg binarny• Kod binarny (prefix-free)_Prefix-free –Korzysta się tu z łańcuchów o zmiennej długości s1,s2,...,sm , takich że żaden z łańcuchów s_i nie jest prefixem sj

Budowa kodów HuffmanaBARDZO WAŻNY PRZYKŁAD TEGO NA SLAJDACH! W nim to drzewo było tworzone tak, że na dole najpierw wypisujemy wszystkie literki razem z ich częstościami. Na slajdach zostały one wymienione w takiej kolejności, by się ładnie nam złożyło z nich drzewko. Zawsze łączymy ze sobą dwie najmniejsze liczby i tak powstaje nam w górę drzewko. To kwestia umowna czy na lewo idą 1 czy też 0. Na koniec z drzewka odczytujemy kod jaki ma mieć dana liczba.

Znajdujemy częstości znaków_Tworzymy węzły (wykorzystując częstości)_powtarzaj: Stwórz nowy węzeł z dwóch najrzadziej występujących znaków (połącz drzewa), a potem oznacz gałęzie 0 i 1_Zbuduj kod z oznaczeń gałęzi

Poszukiwanie w kodach HoffmanaNiech u będzie rozmiarem skompresowanego tekstuNiech v będzie rozmiarem wzorca (zakodowanego na podstawie słownika)KMP(będzie o tej metodzie potem z rozwinięciem tego skrótu) może odnaleźć wzorzec w kodach Hoffmana w czasie O(u+v+m)Zakodowanie wzorca zajmie O(v+m) kroków_Budowanie prefixów zajmie czas O(v)_Poszukiwanie na poziomie bitowym zajmie czas O(u+v)_Problem: Algorytm rozważa bity, a można działać na poziomie bajtów

Kodowanie arytmetyczne – wprowadzeniePozwala na mieszanie bitów w sekwencji komunikatu. Pozwala też na redukcję bitów potrzebnych do kodowania do poziomu l < 2 + suma (od i=1 do n) s_i, gdzie n to ilość znaków, a s_i jest ilością wystąpień i-tego znaku. Dla każdego znaku komunikatu przypisujemy podprzedział przedziału [0,1). Dla każdego komunikatu przedział ten nazywa się przedziałem komunikatu. Przykład na slajdach.

Przy dekodowaniu trzeba przekazać jeszcze długość komunikatu byśmy wiedzieli ile razy mamy dekodować.

Wynikowy przedział dla komunikatu nazywany jest przedziałem sekwencji.

Przedział komunikat jest unikalny

Page 21: 7 projektów, 2 tygodnie na każdy projekt, 0 10 punktów na ...kolos.math.uni.lodz.pl/~archive/Algorytmy i struktury danych 2... · Sedgewick Algorytmy w c++.Grafy ... -Przypomnienie-Drzewa

Zaawansowane algorytmy 21

Istotna własność: przedziały wynikowe dla różnych komunikatów o długości n są zawsze rozłączneStad: podanie dowolnej liczby z przedziału komunikatu jednoznacznie identyfikuje ten komunikatDekodowanie jest procesem podobnym do kodowania, odczytujemy znak i zawężamy przedział

Przykład dekodowania na slajdach.

Możemy korzystać z krótszej ułamkowej reprezentacji binarnej dla wartości z przedziału dla komunikatu. np. [0,.33) = .01 [.33,.66) = .1 [.66,1) = .11, trochę więcej na slajdach…Ta kreska nad 01 na slajdach to odpowiednik (01) czyli 01 w okresie.

Ułamki binarne można uważać za reprezentacje przedziałów – slajdy.Będziemy to nazywać przedziałem kodowym.

Lemat: Jeśli zbiór przedziałów kodowych jest parami rozłączny, odpowiadające im kodowanie tworzy kod prefixowy.

Wybór przedziałów kodowaniaAby utworzyć kod prefixowy znajdujemy takie ułamki binarne, dla których przedziały kodowe zawierają się w przedziałach komunikatów.Przykład – slajdy.

Nie ma tu nic od strony 31 na slajdach, bo tego nie było na wykładzie.