Podstawowe struktury danych

advertisement
Podstawowe struktury danych
Listy
Lista to skończony ciąg elementów: q=[x1, x2, ... , xn ].
Skrajne elementy x1 i xn 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 =[x1, x2, ... , xn ] i r =[y1, y2, ... , ym ] dla 1 ≤ i ≤ j ≤ n to:
• dostęp do elementu listy - q[i] = xi ;
• podlista - q[i..j] = [xi , xi+1 , ... , xj ] ;
• złożenie - q&r = [x1 , ... , xn , y1, ... ,ym ] ;
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 =[x1, x2, ... , xn ] to:
• tablicowa - q[i] = xi , 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)
B)
usunięcie k-tego elementu to - przesunąć w lewo
elementy tablicy q[k+1]...q[n], n=n-1
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 Key1, Key2, …, Keyn będą n uporządkowanymi kluczami
oraz pi będzie prawdopodobieństwem tego, że Keyi jest
kluczem wyszukiwania. Jeżeli ci oznacza liczbę porównań
koniecznych do znalezienia klucza Keyi w danym drzewie, to:
n
średni czas wyszukiwania = Σci pi
i=1
Jest to wartość która trzeba zminimalizować.
Przykład.
Mamy 5 różnych drzew dla n = 3. Wartości kluczy nie są istotne.
Jeżeli:
p1 = 0.7
,
p2 = 0.2
oraz
p3 = 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 2n-1
Załóżmy, że klucze od Keyi do Keyj są ułożone w drzewie,
które minimalizuje wielkość:
j
Σ cm pm
m=i
gdzie cm jest liczbą porównań wymaganych do zlokalizowania
klucza Keym w drzewie. Drzewo to nazywamy optymalnym.
Wartość optymalną oznaczymy jako A[i][j] oraz A[i][i]=pi
(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 Key1 jest w korzeniu, drzewo 2 w którym
Key2 jest w korzeniu, … , drzewo n w którym Keyn 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 Keym 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 * pm do średniego czasu wyszukiwania.
Średni czas wyszukiwania dla drzewa k wynosi
lub inaczej
n
A[1][k-1] + A[k+1][n] + Σ pm
m=1
Jedno z k drzew musi być optymalne więc średni czas
wyszukiwania optymalnego drzewa określa zależność:
n
A[1][n] = minimum(A[1][k-1] + A[k+1][n]) + Σ pm
m=1
gdzie A[1][0] i A[n+1][n] są z definicji równe 0.
Uogólniamy definicje na klucze od Keyi do Keyj , gdzie i < j
i otrzymujemy:
j
A[i][j] = minimum(A[i][k-1] + A[k+1][j]) + Σ pm i < j
i≤k≤j
m=i
A[i][i] = pi
A[i][i-1] oraz A[j+1][j] są z definicji równe 0.
Wyliczenia prowadzimy podobnie
łańcuchowego mnożenia macierzy.
Algorytm
znajdowania
przeszukiwania binarnego.
jak
w
algorytmie
optymalnego
drzewa
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
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;
j
A[i][j]=minimum(A[i][k-1]+A[k+1][j] +
i ≤ k ≤ j
Σ
pm ;
m=i
R[i][j]= wartość k, która dała minimum;
}
minavg = A[1][n];
}
Złożoność czasową można określić podobnie jak dla mnożenia
łańcuchowego macierzy:
T(n) = n(n-1)(n+1)/6 ∈ Θ( n3 )
Algorytm budowania
binarnego.
optymalnego
drzewa
przeszukiwania
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
p1 = 3/8
p2 = 3/8
p3 = 1/8
p4 = 1/8
Tablice A i R będą wówczas wyglądać:
0 1
2
3
4
1 | 0 3/8 9/8 11/8 7/4
|
2|
0 3/8 5/8 1
|
3|
0
1/8 3/8
|
4|
0
1/8
|
5|
0
A
0
1 |0
|
2|
|
3|
|
4|
|
5|
1
1
2
1
3
2
4
2
0
2
2
2
0
3
3
0
4
0
R
Download