Podstawowe struktury danych

Podobne dokumenty
Podstawowe struktury danych

Programowanie dynamiczne, a problemy optymalizacyjne

Podejście zachłanne, a programowanie dynamiczne

Listy, kolejki, stosy

Zofia Kruczkiewicz, Algorytmu i struktury danych, Wykład 14, 1

Ogólne wiadomości o grafach

Wysokość drzewa Głębokość węzła

Wykład 3. Złożoność i realizowalność algorytmów Elementarne struktury danych: stosy, kolejki, listy

Struktury danych i złożoność obliczeniowa Wykład 5. Prof. dr hab. inż. Jan Magott

Drzewa BST i AVL. Drzewa poszukiwań binarnych (BST)

Programowanie dynamiczne

Tadeusz Pankowski

< K (2) = ( Adams, John ), P (2) = adres bloku 2 > < K (1) = ( Aaron, Ed ), P (1) = adres bloku 1 >

Struktury danych: stos, kolejka, lista, drzewo

Dynamiczny przydział pamięci w języku C. Dynamiczne struktury danych. dr inż. Jarosław Forenc. Metoda 1 (wektor N M-elementowy)

Algorytmy i. Wykład 5: Drzewa. Dr inż. Paweł Kasprowski

Porządek symetryczny: right(x)

Wykład 8. Drzewo rozpinające (minimum spanning tree)

Zadanie 1 Przygotuj algorytm programu - sortowanie przez wstawianie.

ALGORYTMY I STRUKTURY DANYCH

Podstawy programowania 2. Temat: Drzewa binarne. Przygotował: mgr inż. Tomasz Michno

Teoretyczne podstawy informatyki

Grafem nazywamy strukturę G = (V, E): V zbiór węzłów lub wierzchołków, Grafy dzielimy na grafy skierowane i nieskierowane:

Drzewo. Drzewo uporządkowane ma ponumerowanych (oznaczonych) następników. Drzewo uporządkowane składa się z węzłów, które zawierają następujące pola:

Wykład 2. Drzewa zbalansowane AVL i 2-3-4

MATEMATYKA DYSKRETNA - MATERIAŁY DO WYKŁADU GRAFY

Drzewa spinające MST dla grafów ważonych Maksymalne drzewo spinające Drzewo Steinera. Wykład 6. Drzewa cz. II

Digraf. 13 maja 2017

Algorytmy grafowe. Wykład 1 Podstawy teorii grafów Reprezentacje grafów. Tomasz Tyksiński CDV

ALGORYTMY I STRUKTURY DANYCH

Wykład X. Programowanie. dr inż. Janusz Słupik. Gliwice, Wydział Matematyki Stosowanej Politechniki Śląskiej. c Copyright 2016 Janusz Słupik

Drzewa binarne. Drzewo binarne to dowolny obiekt powstały zgodnie z regułami: jest drzewem binarnym Jeśli T 0. jest drzewem binarnym Np.

Matematyka dyskretna - 7.Drzewa

Drzewa poszukiwań binarnych

Struktura danych. Sposób uporządkowania informacji w komputerze. Na strukturach danych operują algorytmy. Przykładowe struktury danych:

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Algorytmy i struktury danych Laboratorium 7. 2 Drzewa poszukiwań binarnych

Egzaminy i inne zadania. Semestr II.

Podstawy Informatyki. Metody dostępu do danych

dr inż. Paweł Myszkowski Wykład nr 11 ( )

Algorytmy i struktury danych. Drzewa: BST, kopce. Letnie Warsztaty Matematyczno-Informatyczne

a) 7 b) 19 c) 21 d) 34

Przykłady grafów. Graf prosty, to graf bez pętli i bez krawędzi wielokrotnych.

Algorytmy i Struktury Danych

Programowanie obiektowe

Programowanie obiektowe

Algorytmy i Struktury Danych

Teoretyczne podstawy informatyki

Drzewa poszukiwań binarnych

Algorytmy Równoległe i Rozproszone Część V - Model PRAM II

Sortowanie bąbelkowe

Wykład 6. Drzewa poszukiwań binarnych (BST)

Podstawy Informatyki. Wykład 6. Struktury danych

Algorytmy i złożoności. Wykład 3. Listy jednokierunkowe

7. Teoria drzew - spinanie i przeszukiwanie

Wyszukiwanie w BST Minimalny i maksymalny klucz. Wyszukiwanie w BST Minimalny klucz. Wyszukiwanie w BST - minimalny klucz Wersja rekurencyjna

Algorytmy i struktury danych. Wykład 4 Tablice nieporządkowane i uporządkowane

Matematyczne Podstawy Informatyki

Przykładowe B+ drzewo

Dynamiczne struktury danych

Algorytmiczna teoria grafów

Programowanie dynamiczne i algorytmy zachłanne

Algorytmy i struktury danych. Co dziś? Tytułem przypomnienia metoda dziel i zwyciężaj. Wykład VIII Elementarne techniki algorytmiczne

Egzamin, AISDI, I termin, 18 czerwca 2015 r.

Algorytmy i struktury danych. wykład 5

Przypomnij sobie krótki wstęp do teorii grafów przedstawiony na początku semestru.

Wstęp do programowania. Listy. Piotr Chrząstowski-Wachtel

Algorytmy i złożoności Wykład 5. Haszowanie (hashowanie, mieszanie)

Bazy danych. Andrzej Łachwa, UJ, /15

WSTĘP DO INFORMATYKI. Drzewa i struktury drzewiaste

. Podstawy Programowania 2. Drzewa bst - część druga. Arkadiusz Chrobot. 12 maja 2019

Lista liniowa dwukierunkowa

TEORETYCZNE PODSTAWY INFORMATYKI

Algorytmy równoległe: ocena efektywności prostych algorytmów dla systemów wielokomputerowych

Wykład 2. Drzewa poszukiwań binarnych (BST)

Algorytmy i Struktury Danych.

Reprezentacje grafów nieskierowanych Reprezentacje grafów skierowanych. Wykład 2. Reprezentacja komputerowa grafów

ZASADY PROGRAMOWANIA KOMPUTERÓW ZAP zima 2014/2015. Drzewa BST c.d., równoważenie drzew, kopce.

BAZY DANYCH. Microsoft Access. Adrian Horzyk OPTYMALIZACJA BAZY DANYCH I TWORZENIE INDEKSÓW. Akademia Górniczo-Hutnicza

Podstawowe własności grafów. Wykład 3. Własności grafów

Graf. Definicja marca / 1

Sortowanie. Bartman Jacek Algorytmy i struktury

Drzewa. Jeżeli graf G jest lasem, który ma n wierzchołków i k składowych, to G ma n k krawędzi. Własności drzew

Algorytmy sortujące i wyszukujące

PLAN WYKŁADU BAZY DANYCH INDEKSY - DEFINICJE. Indeksy jednopoziomowe Indeksy wielopoziomowe Indeksy z użyciem B-drzew i B + -drzew

Segmentacja obrazów cyfrowych z zastosowaniem teorii grafów - wstęp. autor: Łukasz Chlebda

E: Rekonstrukcja ewolucji. Algorytmy filogenetyczne

Algorytmy i Struktury Danych.

Minimalne drzewa rozpinające

Matematyczne Podstawy Informatyki

Wykład 5. Sortowanie w czasie liniowologarytmicznym

Każdy węzeł w drzewie posiada 3 pola: klucz, adres prawego potomka i adres lewego potomka. Pola zawierające adresy mogą być puste.

Struktura danych. Sposób uporządkowania informacji w komputerze. Na strukturach danych operują algorytmy. Przykładowe struktury danych:

Algorytmy i str ruktury danych. Metody algorytmiczne. Bartman Jacek

Sortowanie topologiczne skierowanych grafów acyklicznych

Wykład 8. Drzewa AVL i 2-3-4

Wstęp do programowania

ALGORYTMY I STRUKTURY DANYCH

Wstęp do programowania. Drzewa. Piotr Chrząstowski-Wachtel

Sortowanie. Kolejki priorytetowe i algorytm Heapsort Dynamiczny problem sortowania:

Matematyka dyskretna

Transkrypt:

Podstawowe struktury danych Listy Lista to skończony ciąg elementów: q=[x 1, x 2,..., x n ]. Skrajne elementy x 1 i x n nazywamy końcami listy, a wielkość q = n długością (rozmiarem) listy. Szczególnym przypadkiem jest lista pusta: q = [ ]. Podstawowe abstrakcyjne operacje na listach q =[x 1, x 2,..., x n ] i r =[y 1, y 2,..., y m ] dla 1 i j n to: dostęp do elementu listy - q[i] = x i ; podlista - q[i..j] = [x i, x i+1,..., x j ] ; złożenie - q&r = [x 1,..., x n, y 1,...,y m ] ; Na podstawie operacji podstawowych można zdefiniować inne operacje, np. wstawianie elementu x za element x i, na liście q: q[1..i] & [x] & q[i+1.. q ]. W operacjach na listach ograniczamy się zwykle do zmian ich końców: a) front(q) = q[1] - pobierz lewy koniec listy b) push(q,x) = [x]&q - wstaw element na lewy koniec c) pop(q) =q[2.. q ] - usuń bieżący lewy koniec d) rear(q) = q[ q ] - pobierz prawy koniec listy e) inject(q,x) =q&[x] -wstaw element na prawy koniec f) eject(q) =q[1.. q -1 ] - usuń bieżący prawy koniec

W zależności od możliwości wykonania różnych operacji wyróżniemy: kolejkę podwójną - wszystkie sześć operacji stos - tylko operacje front, push, pop kolejkę - tylko operacje front, pop, inject Dwie podstawowe implementacje (reprezentacje) listy q =[x 1, x 2,..., x n ] to: tablicowa - q[i] = x i, gdzie 1 i n, dowiązaniowa W implementacjach pojedynczej liniowej i podwójnej liniowej dowiązanie prowadzące do listy wskazuje na pierwszy element na liście. W implementacji pojedynczej cyklicznej i podwójnej cyklicznej dowiązanie prowadzące do listy wskazuje na element ostatni.

Aby dowiązana struktura nigdy nie była pusta dodaje się na początku listy element pusty zwany GŁOWĄ lub WARTOWNIKIEM listy. Operacje na listach o stałej złożoności czasowej: a) w implementacji pojedynczej liniowej: operacje stosu, wstawianie jednego elementu za drugi, usuwanie następnego elementu, b) w implementacji pojedynczej cyklicznej: te co w a) oraz złożenie i operacje rear i inject, c) w implementacji podwójnej cyklicznej: te co w b) oraz eject, wstawianie jednego elementu przed drugim, wstawianie danego elementu, odwracanie listy. LISTY JEDNOKIERUNKOWE Lista jednokierunkowa jest oszczędną pamięciowo strukturą danych, pozwalającą grupować dowolną - ograniczoną tylko ilością dostępnej pamięci - liczbę elementów: liczb, znaków, rekordów. Do budowy listy używane są dwa typy rekordów: - informacyjny - wskaźniki, dowiązania do początku listy (głowa) i końca listy (ogon) - robocze - pole wartości i wskaźnik do następnego elementu listy Dzięki rekordowi informacyjnemu mamy ciągły dostęp do niektórych operacji, np. dołączanie elementu na koniec listy. Głowa, ogon i następny to wskaźniki, wartość to dowolna wielkość (znanego typu).

Wskaźniki NULL oznaczają adresy pamięci pod którymi nie ma żadnej zmiennej. Przykład listy jednokierunkowej:

Pola głowa i ogon pozwalają na przeglądanie elementów listy i dołączanie nowych elementów. Przykład (pseudokod) przeglądania elementów listy: adres_tmp=info.głowa; while (adres_tmp <> NULL ) { if(adres_tmp.wartość == x) { Wypisz "Znalazłem szukany element" opuść procedurę } else adres_tmp=adres_tmp.następny } Wypisz "Nie znalazłem elementu" Dokładanie nowych elementów (dwa podejścia): 1)potraktowanie listy jak worek nie-uporządkowanych elementów i umieszczanie nowych elementów na początku

1) dokładanie elementów we właściwym ustalonym przez użytkownika porządku (całość listy musi być widziana jako posortowana) Możliwe są trzy przypadki: a) wstawiamy element na początek listy b)wstawiamy element na koniec listy c) wstawiamy element gdzieś w środku W każdym z przypadków musimy zapamietywać dwa wskaźniki - przed który element wstawić i po którym mamy to zrobić.

Podobnie postępujemy przy fuzji (łączeniu) list tak by wypadkowa lista pozostała uporządkowana. Podsumowując wady i zalety list jednokierunkowych: Wady nienaturalny dostęp do elementów niełatwe sortowanie Zalety małe zużycie pamięci elastyczność Lista w której elementy są już na samym początku wstawiane w określonym porządku, służy obok gromadzenia danych, także do ich porządkowania. W sytuacji, gdy jest tylko jedno kryterium sortowania struktura działa bardzo dobrze "sama" dbając o sortowanie.

Dla kilku kryteriów sortowania należy wprowadzić obok listy danych, także kilka list z wskaźnikami do danych - list tych powinno byś tyle ile kryteriów sortowania. Sortowanie w takich wypadku polega na porządkowaniu wskaźników bez ruszania listy danych. Nieposortowaną listę DANE można uporządkować według trzech kryteriów:

- imienia i nazwiska (Adam Fuks, Jan Kowalski, Michał Zaremba) - kodów ( 30, 34, 37) - kwot ( 1200, 2000, 3000 ) Tablicowa implemantacja list jest niezwykle prosta jeśli umówimy się, że i-temu indeksowi tablicy odpowiada i-ty element listy. Wymagana jest dodatkowa informacja wskazująca jak wiele elementów liczy lista (jak duża musi być tablica). Wadą jest marnotrawstwo pamięci bo najczęściej przydzielamy na tablicę większy obszar pamięci niż to zwykle potrzeba. Operacje na listach są w implementacji tablicowej proste: 1) front(q), x=q[1] - pobierz lewy koniec listy 2) push(q,x) - przesuń wszystkie elementy tablicy o jeden w prawo i q[1]=x - wstaw element na lewy koniec 3) pop(q), przesuń wszystkie elementy tablicy poza pierwszym o jeden w lewo - usuń bieżący lewy koniec 4) rear(q), x=q[n] - pobierz prawy koniec listy 5) inject(q,x), q[n+1]=x -wstaw element na prawy koniec 6) eject(q), n=n-1 - usuń bieżący prawy koniec

Dodatkowo: A) usunięcie k-tego elementu to - przesunąć w lewo elementy tablicy q[k+1]...q[n], n=n-1 B) wstawienie elementu na pozycję k to - przesunąć w prawo elementy tablicy q[k]...q[n], n=n+1 LISTY DWUKIERUNKOWE Listy jednokierunkowe są wygodne i zajmują mało pamięci. Operacje na nich zajmują dużo czasu. W liście dwukierunkowej komórka robocza zawiera wskaźniki do elementów: poprzedniego i następnego. pierwsza komórka na liście nie posiada swojego poprzednika (pole poprzedni zawiera NULL wskaźnik pokazujący pusty element pamięci) ostatnia komórka na liście nie posiada swojego następnika (pole następny zawiera NULL wskaźnik pokazujący pusty element pamięci) Lista dwukierunkowa jest kosztowna jeśli chodzi o pamięć, ale wygodna gdy chodzi o szybkość.

Usunięcie elementu listy dwukierunkowej: Lista cykliczna jest zamknięta w pierścień, wskaźnik ostatniego elementu wskazuje na pierwszy element. Elementy pierwszy i ostatni są umowne. STOSY Stos jest struktura danych, do której dostęp jest możliwy tylko od strony tzw. wierzchołka, czyli pierwszego wolnego miejsca znajdującego się na nim. Funkcje odkładania elementu X na stos ( push(x) ) i pobieranie go ze stosu ( pop(x) ) można opisać symbolicznie (wraz z kodem błędu s wprowadzonym przez użytkownika):

Tablicowa implementacja stosu wygląda analogicznie jak dla listy, ale z dostępnymi jedynie operacjami front, push, pop. Grafy Wprowadzenie do teorii grafów. Przykład

Ważony graf skierowany. Kółka wierzchołki grafu. Linie łączące wierzchołki - krawędzie grafu. Wszystkie krawędzie posiadają przypisany kierunek graf skierowany (digraf). W digrafie mogą istnieć dwie krawędzie między dwoma wierzchołkami, każda biegnąca w innym kierunku. Jeżeli krawędzie posiadają związane ze sobą wartości, są one nazywane wagami (nieujemne). Graf jest zwany grafem ważonym. Droga w grafie ważonym to sekwencja wierzchołków, taka że istnieje krawędź z każdego wierzchołka do jego następnika [ν 1, ν 4, ν 3 ] jest drogą, [ν 3, ν 4, ν 1 ] nie jest drogą. Droga jest nazywana prostą, jeżeli nie przechodzi dwa razy przez ten sam wierzchołek. Droga prosta nigdy nie zawiera pod-drogi, która byłaby cykliczna. Długością drogi w grafie skierowanym jest suma wag krawędzi należących do drogi. Droga z wierzchołka do niego samego cykl. Graf zawierający cykl jest grafem cyklicznym, gdy nie zawiera cyklu jest grafem acyklicznym. Najczęstsze zadanie dla grafów to znalezienie najkrótszych dróg z każdego wierzchołka do wszystkich innych wierzchołków. Najkrótsza droga musi być drogą prostą.

Grafy i drzewa Załóżmy, że planista przestrzenny chce połączyć określone miasta drogami w taki sposób, aby było możliwe dojechanie z dowolnego z tych miast do dowolnego innego. Dążymy do zbudowania najkrótszej sieci dróg. Do rozwiązania tego problemu niezbędne jest poznanie zagadnień z zakresu teorii grafów. Przypomnijmy, że graf jest nieskierowany, gdy jego krawędzie nie posiadają kierunku. Mówimy wówczas po prostu, że krawędź jest między dwoma wierzchołkami. Droga w grafie nieskierowanym jest sekwencją wierzchołków, taką że każdy wierzchołek i jego następnik łączy krawędź. Krawędzie nie mają kierunku, więc droga z wierzchołka u do wierzchołka ν istnieje wtedy i tylko wtedy, gdy istnieje droga z ν do u. Graf nieskierowany jest nazywany spójnym, kiedy między każdą parą wierzchołków istnieje droga. Grafy z rysunku poniżej są spójne.

W grafie nieskierowanym droga wiodąca z wierzchołka do niego samego, zawierająca co najmniej 3 wierzchołki, wśród których wszystkie wierzchołki pośrednie są różne, jest nazywana cyklem prostym. Graf nieskierowany nie zawierający żadnych cykli prostych jest określany mianem acyklicznego (grafy (a), (b) są cykliczne). Drzewo jest acyklicznym spójnym grafem nieskierowanym (grafy (c), (d) są drzewami). Funkcjonuje też pojęcie drzewo korzeniowe to drzewo posiadające jeden wierzchołek, określony jako korzeń (jest to inna klasa drzew niż te rozpatrywane tutaj). Szerokie zastosowanie ma problem usuwania krawędzi ze spójnego, ważonego grafu nieskierowanego G w celu utworzenia takiego pod-grafu, że wszystkie wierzchołki pozostają połączone, a suma ich wag jest najmniejsza. Podgraf o minimalnej wadze musi być drzewem, ponieważ gdyby tak nie było, zawierałby cykl prosty, więc usunięcie krawędzi tego cyklu prowadziłoby do grafu o mniejszej wadze. Drzewo rozpinające grafu G to spójny pod-graf, który zawiera wszystkie wierzchołki należące do G i jest drzewem ( (c) i (d) są drzewami rozpinającymi). Spójny pod-graf o minimalnej wadze musi być drzewem rozpinającym, ale nie każde drzewo rozpinające ma minimalną wagę. Algorytm rozwiązujący przedstawiony wcześniej problem musi tworzyć drzewo rozpinające o minimalnej wadze. Takie drzewo nosi nazwę minimalnego drzewa rozpinającego ((d) jest takim drzewem). Znalezienie minimalnego drzewa rozpinającego metodą siłową wymaga czasu gorszego niż wykładniczy. Chcemy

rozwiązać to bardziej wydajnie wykorzystując podejście zachłanne. Definicja Graf nieskierowany G składa się ze skończonego zbioru V wierzchołków oraz zbioru E par wierzchołków ze zbioru V czyli krawędzi grafu. Graf G oznaczamy: G = ( V, E ) Dla grafu (a): V = {ν 1,ν 2,ν 3,ν 4,ν 5 } E = {(ν 1,ν 2 ),(ν 1,ν 3 ),(ν 2,ν 3 ),(ν 2,ν 4 ),(ν 3,ν 4 ),(ν 3,ν 5 ),(ν 4,ν 5 )} Drzewo rozpinające T dla grafu G zawiera te same wierzchołki V, co graf G, jednak zbiór krawędzi drzewa T jest podzbiorem F zbioru E. Drzewo rozpinające możemy oznaczyć jako T = ( V, F ). Problem polega na znalezieniu podzbioru F zbioru E, takiego aby T = ( V, F ) było minimalnym drzewem rozpinającym grafu G. Wysokopoziomowy algorytm zachłanny realizujący to zadanie mógłby wyglądać: F= ; //Inicjalizacja zbioru krawędzi while (realizacja nie została rozwiązana) { wybierz krawedz zgodnie z warunkiem optymalnym lokalnie; // procedura wyboru if(dodanie krawędzi do F nie powoduje powstania cyklu) // spr. wykonalnosci dodaj ją; } if(t=(v,f) jest drzewem rozpinajacym) realizacja jest rozwiazana;

Oczywiście warunek optymalny lokalnie może być inny w różnych problemach i w rożnych algorytmach rozwiązania. Dwa najbardziej znane algorytmy realizujące to zadanie to algorytm Prima i algorytm Kruskala. Drzewa wyszukiwania binarnego ( i ich optymalizacja) Opracowujemy algorytm określania optymalnego sposobu zorganizowania zbioru elementów w postaci drzewa wyszukiwania binarnego. Dla każdego wierzchołka w drzewie binarnym poddrzewo, którego korzeniem jest lewy (prawy) potomek tego wierzchołka, nosi nazwę lewego (prawego) pod-drzewa wierzchołka. Lewe (prawe) pod-drzewo korzenia drzewa nazywamy lewym (prawym) pod-drzewem drzewa. Drzewo wyszukiwania binarnego. Drzewo wyszukiwania binarnego jest binarnym drzewem elementów (kluczy) pochodzących ze zbioru uporządkowanego. Najprostsze drzewo wyszukiwania binarnego spełnia warunki: Każdy wierzchołek zawiera jeden klucz. Każdy klucz w lewym poddrzewie danego wierzchołka jest mniejszy lub równy kluczowi tego wierzchołka.

Klucze znajdujące się w prawym pod-drzewie danego wierzchołka są większe lub równe kluczowi tego wierzchołka. Przykład. Dwa drzewa o tych samych kluczach. W lewym drzewie prawe pod-drzewo wierzchołka Rudolf zawiera klucze (imiona) Tomasz, Urszula, Waldemar wszystkie większe od Rudolf zgodnie z porządkiem alfabetycznym. Zakładamy, że klucze są unikatowe. Głębokość wierzchołka w drzewie jest liczbą krawędzi w unikatowej drodze, wiodącej od korzenia do tego wierzchołka, inaczej zwana poziomem wierzchołka w drzewie. Głębokość drzewa to maksymalna głębokość wszystkich wierzchołków (w przykładzie - drzewo po lewej głębokość 3, po prawej głębokość 2)

Drzewo nazywane jest zrównoważonym, jeżeli głębokość dwóch pod-drzew każdego wierzchołka nigdy nie różni się o więcej niż 1 (w przykładzie lewe drzewo nie jest zrównoważone, prawe jest zrównoważone). Zwykle drzewo wyszukiwania binarnego zawiera pozycje, które są pobierane zgodnie z wartościami kluczy. Celem jest takie zorganizowanie kluczy w drzewie wyszukiwania binarnego, aby średni czas zlokalizowania klucza był minimalny. Drzewo zorganizowane w ten sposób jest nazywane optymalnym. Jeżeli wszystkie klucze charakteryzuje to samo prawdopodobieństwo zostania kluczem wyszukiwania, to drzewo z przykładu (prawe) jest optymalne. Weźmy przypadek, w którym wiadomo, że klucz wyszukiwania występuje w drzewie. Aby zminimalizować średni czas wyszukiwania musimy określić złożoność czasową operacji lokalizowania klucza. Algorytm wyszukiwania klucza w drzewie wyszukiwania binarnego Wykorzystujemy strukturę danych: struct nodetype { keytype key; nodetype* left; nodetype* right; }; typedef nodetype* node_pointer;

Zmienna typu node_pointer jest wskaźnikiem do rekordu typu nodetype. Problem: określić wierzchołek zawierający klucz w drzewie wyszukiwania binarnego, zakładając że taki występuje w drzewie. Dane: wskaźnik tree do drzewa wyszukiwania binarnego oraz klucz keyin. Wynik: wskaźnik p do wierzchołka zawierającego klucz. void search(node_pointer tree, keytype keyin, node_pointer p) { bool found; } p = tree; found = false; while (!found) if (p->key == keyin) found = true; else if (keyin < p->key) p = p->left; else p = p->right; Liczbę porównań wykonywanych przez procedurę search w celu zlokalizowania klucza możemy nazwać czasem wyszukiwania. Chcemy znaleźć drzewo, dla którego średni czas wyszukiwania jest najmniejszy. Zakładając, że w każdym przebiegu pętli while wykonywane jest tylko jedno porównanie możemy napisać : czas wyszukiwania = głębokość(key) + 1 Przykładowo (lewe poddrzewo): czas wyszukiwania = głębokość(urszula) + 1 = 2+1 = 3

Niech Key 1, Key 2,, Key n będą n uporządkowanymi kluczami oraz p i będzie prawdopodobieństwem tego, że Key i jest kluczem wyszukiwania. Jeżeli c i oznacza liczbę porównań koniecznych do znalezienia klucza Key i w danym drzewie, to: średni czas wyszukiwania = Σc i p i i=1 Jest to wartość która trzeba zminimalizować. n Przykład. Mamy 5 różnych drzew dla n = 3. Wartości kluczy nie są istotne.

Jeżeli: p 1 = 0.7, p 2 = 0.2 oraz p 3 = 0.1 to średnie czasy wyszukiwania dla drzew wynoszą : 1) 3*(0.7) + 2*(0.2) + 1*(0.1) = 2.6 2) 2*(0.7) + 3*(0.2) + 1*(0.1) = 2.1 3) 2*(0.7) + 1*(0.2) + 2*(0.1) = 1.8 4) 1*(0.7) + 3*(0.2) + 2*(0.1) = 1.5 5) 1*(0.7) + 2*(0.2) + 3*(0.1) = 1.4 Piąte drzewo jest optymalne. Oczywiście znalezienie optymalnego drzewa wyszukiwania binarnego poprzez rozpatrzenie wszystkich drzew wiąże się z ilością drzew co najmniej wykładniczą w stosunku do n. W drzewie o głębokości n-1 wierzchołek na każdym z n-1 poziomów (oprócz korzenia) może się znajdować na prawo lub lewo. Zatem liczba różnych drzew o głębokości n-1 wynosi 2 n-1 Załóżmy, że klucze od Key i do Key j są ułożone w drzewie, które minimalizuje wielkość: j Σ c m p m m=i gdzie c m jest liczbą porównań wymaganych do zlokalizowania klucza Key m w drzewie. Drzewo to nazywamy optymalnym. Wartość optymalną oznaczymy jako A[i][j] oraz A[i][i]=p i (jeden klucz wymaga jednego porównania). Korzystając z przykładu można pokazać, że w problemie tym zachowana jest zasada optymalności.

Możemy sobie wyobrazić n różnych drzew optymalnych: drzewo 1 w którym Key 1 jest w korzeniu, drzewo 2 w którym Key 2 jest w korzeniu,, drzewo n w którym Key n jest w korzeniu. Dla 1 k n pod-drzewa drzewa k muszą być optymalne, więc czasy wyszukiwania w tych pod-drzewach można opisać: Dla każdego m k wymagana jest o jeden większa liczba porównań w celu zlokalizowania klucza Key m w drzewie k niż w celu zlokalizowania tego klucza w poddrzewie w którym się znajduje. Dodatkowe porównanie jest związane z korzeniem i daje 1 * p m do średniego czasu wyszukiwania. Średni czas wyszukiwania dla drzewa k wynosi lub inaczej n A[1][k-1] + A[k+1][n] + Σ p m m=1

Jedno z k drzew musi być optymalne więc średni czas wyszukiwania optymalnego drzewa określa zależność: A[1][n] = minimum(a[1][k-1] + A[k+1][n]) + Σ p m m=1 gdzie A[1][0] i A[n+1][n] są z definicji równe 0. Uogólniamy definicje na klucze od Key i do Key j, gdzie i < j i otrzymujemy: A[i][j] = minimum(a[i][k-1] + A[k+1][j]) + Σ p m i < j i k j m=i A[i][i] = p i A[i][i-1] oraz A[j+1][j] są z definicji równe 0. Wyliczenia prowadzimy podobnie jak w algorytmie łańcuchowego mnożenia macierzy. Algorytm znajdowania optymalnego drzewa przeszukiwania binarnego. Problem: określenie optymalnego drzewa wyszukiwania binarnego dla zbioru kluczy, z których każdy posiada przypisane prawdopodobieństwo zostania kluczem wyszukiwania. Dane: n-liczba kluczy oraz tablica liczb rzeczywistych p indeksowana od 1 do n, gdzie p[i] jest prawdopodobieństwem wyszukiwania i-tego klucza Wyniki: zmienna minavg, której wartością jest średni czas wyszukiwania optymalnego drzewa wyszukiwania binarnego oraz tablica R, z której można skonstrułować drzewo optymalne.r[i][j] jest indeksem klucza j n

znajdującego się w korzeniu drzewa optymalnego, zawierającego klucze od i-tego do j-tego. void optsearch(int n, const float p[], float minavg, index R[][]) { index i, j, k, diagonal; float A[1..n+1][0..n]; for (i=1; i <= n; i++) { A[i][i-1] = 0; A[i][i] = p[i]; R[i][i] = i; R[i][i-1] = 0; } A[n+1][n] = 0; for(diagonal = 1; diagonal <= n-1; diagonal++) for(i = 1; i <= n - diagonal; i++) //Przekatna 1 { //tuz nad glowna przek j = i + diagonal; } A[i][j]=minimum(A[i][k-1]+A[k+1][j] + Σ p m ; i k j R[i][j]= wartość k, która dała minimum; } minavg = A[1][n]; j m=i Złożoność czasową można określić podobnie jak dla mnożenia łańcuchowego macierzy: T(n) = n(n-1)(n+1)/6 Θ( n 3 )

Algorytm budowania optymalnego drzewa przeszukiwania binarnego. Problem: zbudować optymalne drzewo wyszukiwania binarnego. Dane: n liczba kluczy, tablica Key zawierająca n uporządkowanych kluczy oraz tablica R, utworzona w poprzednim algorytmie. R[i][j] jest indeksem klucza w korzeniu drzewa optymalnego, zawierającego klucze od i-tego do j-tego Wynik: wskaźnik tree do optymalnego drzewa wyszukiwania binarnego, zawierającego n kluczy. node_pointer tree(index i,j) { index k; node_pointer p; } k = R[i][j]; if(k == 0) return NULL; else { p = new nodetype; p->key = Key[k]; p->left = tree(i,k-1); p->right = tree(k+1,j); return p; } Przykład. Załóżmy, że mamy następujące wartości w tablicy Key: Damian Izabela Rudolf Waldemar Key[1] Key[2] Key[3] Key[4] oraz p 1 = 3/8 p 2 = 3/8 p 3 = 1/8 p 4 = 1/8

Tablice A i R będą wówczas wyglądać: 0 1 2 3 4 0 1 2 3 4 1 0 3/8 9/8 11/8 7/4 1 0 1 1 2 2 2 0 3/8 5/8 1 2 0 2 2 2 3 0 1/8 3/8 3 0 3 3 4 0 1/8 4 0 4 5 0 5 0 A R