Wykład 6 Dynamiczne struktury danych

advertisement
Wykład 6
Dynamiczne struktury danych
1
Plan wykładu
Ø  Wprowadzenie
Ø  Popularne dynamiczne struktury danych (ADT)
Ø  stosy, kolejki, listy – opis abstrakcyjny
Ø  Listy liniowe
Ø  Implementacja tablicowa stosu i kolejki
Ø  Drzewa
Ø  Możliwe implementacje
2
Wprowadzenie
Ø  Do tej pory najczęściej zajmowaliśmy się jedną strukturą danych –
tablicą. Struktura taka ma charakter statyczny – jej rozmiar jest
niezmienny. Powoduje to konieczność poznania wymaganego rozmiaru
przed rozpoczęciem działań (ewentualnie straty miejsca – deklarujemy
„wystarczająco” dużą tablicę).
Ø  W wielu zadaniach wygodniejsza jest struktura o zmiennym rozmiarze
(w zależności od aktualnych potrzeb) – struktura dynamiczna.
Ø  Potrzebujemy struktury pozwalającej na przechowywanie elementów
niezależnie od ich fizycznego położenia.
logicznie
2 0 5 3 4 1
fizycznie
3
2 4
1 0
5
3
Wprowadzenie
Ø  Przykładowe operacje dla struktur danych:
–  Insert(S, k): wstawianie nowego elementu
–  Delete(S, k): usuwanie elementu
–  Min(S), Max(S): odnajdowanie najmniejszego/największego elementu
–  Successor(S,x), Predecessor(S,x): odnajdowanie następnego/
poprzedniego elementu
Ø  Zwykle przynajmniej jedna z tych operacji jest kosztowna czasowo
(zajmuje czas O(n)). Czy można lepiej?
4
Abstrakcyjne typy danych (Abstract Data Types –ADT )
Ø  Abstrakcyjnym typem danych nazywany formalną specyfikację sposobu
przechowywania obiektów oraz zbiór dobrze opisanych operacji na tych
obiektach.
Ø  Jaka jest różnica pomiędzy strukturą danych a ADT?
à struktura danych (klasa) jest implementacją ADT dla specyficznego
komputera i systemu operacyjnego.
5
Popularne dynamiczne ADT
Ø  Listy łączone
Ø  Stosy, kolejki
Ø  Drzewa – z korzeniem (rooted trees), binarne, BST, czerwonoczarne, AVL itd.
Ø  Kopce i kolejki priorytetowe
Ø  Tablice z haszowaniem
6
Listy
Ø  Lista L jest liniową sekwencją elementów.
Ø  Pierwszy element listy jest nazywany head, ostatni tail. Jeśli obydwa
są równe null, to lista jest pusta
Ø  Każdy element ma poprzednik i następnik (za wyjątkiem head i tail)
Ø  Operacje na liście:
–  Successor(L,x), Predecessor(L,x)
–  List-Insert(L,x)
–  List-Delete(L,x)
–  List-Search(L,k)
2 0 2 3 0 1
head
x
tail
7
Listy łączone
Ø  Rozmieszczenie fizyczne obiektów w pamięci nie musi odpowiadać
ich logicznej kolejności; wykorzystujemy wskaźniki do obiektów (do
następnego/poprzedniego obiektu)
Ø  Manipulując wskaźnikami możemy dodawać, usuwać elementy do
listy bez przemieszczania pozostałych elementów listy
Ø  Lista taka może być pojedynczo lub podwójnie łączona.
head
a1
a2
a3
…
an
tail
null
null
8
Węzły i wskaźniki
Ø  Węzłem nazywać będziemy obiekt
przechowujący daną oraz wskaźnik do
następnej danej i (opcjonalnie – dla listy
podwójnie łączonej) wskaźnik do
poprzedniej danej. Jeśli nie istnieje
następny obiekt to wartość wskaźnika
będzie “null”
Ø  Wskaźnik oznacza adres obiektu w
pamięci
Ø  Węzły zajmują zwykle przestrzeń: Θ(1)
key
data
next
prev
struct node {
key_type key;
data_type data;
struct node *next;
struct node *prev;
}
9
Wstawianie do listy (przykład operacji na liście)
wstawianie nowego węzła q pomiędzy węzły p i r:
p
r
a1
a3
a2
p
q
a1
a2
r
a3
next[q]ß r
next[p] ß q
10
Usuwanie z listy
usuwanie węzła q
p
q
a1
a2
r
a3
p
r
a1
a3
q
next[p]ß r
next[q]ß null
a2
null
11
Operacje na liście łączonej
List-Search(L, k)
1.  x ß head[L]
2. 
while x ≠ null and key[x] ≠ k
3. 
do x ß next[x]
4. 
return x
List-Insert(L, x)
1.  next[x] ß head[L]
2. 
if head[L] ≠ null
3. 
then prev[head[L]] ß x
4.  head[L] ß x
5.  prev[x] ß null
List-Delete(L, x)
1. 
if prev[L] ≠ null
2. 
then next[prev[x]] ß next[x]
3. 
else head[L] ß next[x]
4. 
if next[L] ≠ null
5. 
then prev[next[x]] ß prev[x]
12
Listy podwójnie łączone
x
head
a1
null
a2
a3
a4
tail
null
Listy cykliczne: łączymy element pierwszy z ostatnim
13
Stosy
Ø  Stosem S nazywany liniową sekwencję elementów do której nowy
element x może zostać wstawiony jedynie na początek, analogicznie
element może zostać usunięty jedynie z początku tej sekwencji.
Ø  Stos rządzi się zasadą Last-In-First-Out (LIFO).
Ø  Operacje dla stosu:
–  Stack-Empty(S)
–  Pop(S)
–  Push(S,x)
Push
Pop
2
0
1
5
head
null
14
Kolejki
Ø  Kolejka Q jest to liniowa sekwencja elementów do której nowe
elementy wstawiane są na końcu sekwencji, natomiast elementy
usuwane są z jej początku.
Ø  Zasada First-In-First-Out (FIFO).
Ø  Operacje dla kolejki:
–  Queue-Empty(Q)
–  EnQueue(Q, x)
–  DeQueue(Q)
DeQueue
EnQueue
2 0 2 3 0 1
head
tail
15
Implementacja stosu i kolejki
Ø  Tablicowa
–  Wykorzystujemy tablicę A o n elementach A[i], gdzie n jest maksymalną
ilością elementów stosu/kolejki.
–  Top(A), Head(A) i Tail(A) są indeksami tablicy
–  Operacje na stosie/w kolejce odnoszą się do indeksów tablicy i elementów
tablicy
–  Implementacja tablicowa nie jest efektywna
Ø  Listy łączone
–  Nowe węzły tworzone są w miarę potrzeby
–  Nie musimy znać maksymalnej ilości elementów z góry
–  Operacje są manipulacjami na wskaźnikach
16
Implementacja tablicowa stosu
Push(S, x)
1. 
if top[S] = length[S]
2. 
then error “overflow”
3.  top[S] ß top[S] + 1
4.  S[top[S]] ß x
Pop(S)
1. 
if top[S] = -1
2. 
then error “underflow”
3. 
else top[S] ß top[S] – 1
4. 
return S[top[S] +1]
0
1
2
3
1 5
2
3
4
5
6
top
Kierunek
wstawiania
Stack-Empty(S)
1. 
if top[S] = -1
2. 
then return true
3. 
else return false
17
Implementacja tablicowa kolejki
Dequeue(Q)
1.  x ß Q[head[Q]]
2. 
if head[Q] = length[Q]
3. 
then head[Q] ß 1
4. 
else head[Q] ß (head[Q]+1)mod n
5. 
return x
1 5
tail
2 3 0
head
Enqueue(Q, x)
1.  Q[tail[Q]] ß x
2. 
if tail[Q] = length[Q]
3. 
then tail[Q] ß x
4. 
else tail[Q] ß (tail[Q]+1)mod n
18
Abstrakcyjny typ danych dla kolejki priorytetowej
Ø Kolejka priorytetowa przechowuje dowolne
obiekty
Ø Każdy z obiektów jest parą (klucz, element)
Ø Podstawowe metody dla kolejki priorytetowej:
– insertItem(k, o)
dodaje obiekt o kluczu k i elemencie o
– removeMin()
usuwa element kolejki o najmniejszym kluczu
19
Abstrakcyjny typ danych dla kolejki priorytetowej
Ø Dodatkowe metody
– minKey(k, o)
zwraca (ale nie usuwa) najmniejszą wartość klucza
– minElement()
zwraca (ale nie usuwa) element o najmniejszym kluczu
– size(), isEmpty()
Ø Zastosowania:
–  Algorytmy grafowe
–  Systemy aukcyjne
–  Kodowanie
–  Systemy giełdowe
20
Relacja porządku
Ø Elementy w kolejce priorytetowej pochodzą ze zbioru
uporządkowanego
Ø Dwa rozróżnialne obiekty mogą mieć te samą wartość
klucza
Ø Relacja porządku ≤
– Zwrotna: x ≤ x
– Antysymetryczna: x ≤ y ∧ y ≤ x ⇒ x = y
– Przechodnia: x ≤ y ∧ y ≤ z ⇒ x ≤ z
21
Sortowanie z wykorzystaniem kolejki priorytetowej
Ø  Łatwo wykorzystać kolejkę
priorytetową do sortowania
obiektów:
–  Wstawiamy obiekty do
kolejki priorytetowej –
operacje insertItem(e, e)
dla każdego obiektu e
–  Usuwamy obiekty z kolejki
poprzez sekwencję operacji
removeMin()
Ø  Złożoność obliczeniowa
zależna od sposobu
implementacji kolejki
priorytetowej
Algorithm PQ-Sort(S, C)
Input sequence S, comparator C
for the elements of S
Output sequence S sorted in
increasing order according to C
P ← priority queue with
comparator C
while !S.isEmpty ()
e ← S.remove (S. first ())
P.insertItem(e, e)
while !P.isEmpty()
e ← P.minElement()
P.removeMin()
S.insertLast(e)
22
Implementacja sekwencyjna
Ø Implementacja w postaci
nieposortowanej sekwencji
– Wstawiamy elementy do listy
liniowej w porządku w jakim się
pojawiają
Ø Implementacja w postaci
posortowanej sekwencji
– Wstawiamy elementy do listy
liniowej tak aby pozostawała
ona posortowana
Ø wydajność:
– insertItem zajmuje czas O(1)
(wstawianie na początek listy)
Ø wydajność:
– insertItem zajmuje czas O(n)
(wymaga trawersowania listy)
– removeMin, minKey i minElement
zajmuje czas O(n) ponieważ
wymaga przejścia przez całą listę
w celu wyznaczenia minimalnego
klucza
– removeMin, minKey i
minElement zajmuje czas O(1)
ponieważ element o
minimalnym kluczu znajduje
się na początku listy
23
Selection-Sort
Ø  Sortowanie przez wybór może być rozumiane
jako wariacja PQ-sort z wykorzystaniem
nieposortowanej sekwencji
Ø  Czas działania:
1.  Wstawianie do kolejki to n operacji insertItem co
zabiera czas O(n)
2.  Usuwanie n elementów z kolejki to ciąg operacji
removeMin o czasie:
n + (n -1) + …+ 1
Ø  Daje to łączny czas działania O(n2)
24
Insertion-Sort
Ø 
Ø 
Sortowanie przez wstawianie odpowiada PQsort przy wykorzystaniu implementacji kolejki
priorytetowej poprzez posortowaną sekwencję
elementów
Czas działania:
–  Wstawianie elementów zajmuje odpowiednio
czas proporcjonalny do:
1 + 2 + …+ n
czyli O(n2)
–  Usuwanie elementów to sekwencja n operacji
removeMin co zajmuje czasO(n)
Daje to łączny czas działania O(n2)
25
Drzewa z korzeniem
Ø  Drzewem z korzeniem T nazywamy ADT dla którego elementy są
zorganizowane w strukturę drzewiastą.
Ø  Drzewo składa się z węzłów przechowujących obiekt oraz krawędzi
reprezentujących zależności pomiędzy węzłami.
Ø  W drzewie występują trzy typy węzłów: korzeń (root), węzły
wewnętrzne, liście
Ø  Własności drzew:
–  Istnieje droga z korzenia do każdego węzła (połączenia)
–  Droga taka jest dokładnie jedna (brak cykli)
–  Każdy węzeł z wyjątkiem korzenia posiada rodzica (przodka)
–  Liście nie mają potomków
–  Węzły wewnętrzne mają jednego lub więcej potomków (= 2 à binarne)
26
Drzewa z korzeniem
0
A
B
C
E
K
F
L
M
D
1
G H I
J
N
2
3
27
Terminologia
Ø  Rodzice (przodkowie) i dzieci (potomkowie)
Ø  Rodzeństwo (sibling) – potomkowie tego samego węzła
Ø  Relacja jest dzieckiem/rodzicem.
Ø  Poziom węzła
Ø  Ścieżka (path): sekwencja węzłów n1, n2, … ,nk takich, że ni jest
przodkiem ni+1. Długością ścieżki nazywamy liczbę k.
Ø  Wysokość drzewa: maksymalna długość ścieżki w drzewie od korzenia
do liścia.
Ø  Głębokość węzła: długość ścieżki od korzenia do tego węzła.
28
Drzewa binarne
Ø  Drzewem binarnym T nazywamy drzewo z korzeniem, dla którego
każdy węzeł ma co najwyżej 2 potomków.
A
A
B
D
C
E
G
≠
F
Porządek węzłów
jest istotny!!!
B
D
C
E
F
G
29
Drzewa pełne i drzewa kompletne
Ø  Drzewo binarne jest pełne jeśli każdy węzeł wewnętrzny ma
dokładnie dwóch potomków.
Ø  Drzewo jest kompletne jeśli każdy liść ma tę samą głębokość.
A
B
D
A
C
E
F
B
D
G
pełne
C
E
F
G
kompletne
30
Własności drzew binarnych
Ø  Ilość węzłów na poziomie d w kompletnym drzewie binarnym wynosi 2d
Ø  Ilość węzłów wewnętrznych w takim drzewie:
1+2+4+…+2d–1 = 2d –1 (mniej niż połowa!)
Ø  Ilość wszystkich węzłów:
1+2+4+…+2d = 2d+1 –1
Ø  Jak wysokie może być drzewo binarne o n liściach: (n –1)/2
Ø  Wysokość drzewa:
2d+1 –1= n à log (n+1) –1 ≤ log (n)
31
Tablicowa implementacja drzewa binarnego
1
A
Poziom
0
2
B
5
E
4
D
1
2
A
B
20
3
C
21
3
C
6
F
4
D
1
7
G
5
E
2
6
F
Na każdym poziomie d
mamy 2d elementów
7
G
Kompletne drzewo:
parent(i) = floor(i/2)
left-child(i) = 2i
right-child(i) = 2i +1
22
32
Listowa implementacja drzewa binarnego
root(T)
A
B
D
Każdy węzeł zawiera
Dane oraz 3 wskaźniki:
•  przodek
•  lewy potomek
•  prawy potomek
C
E
F
G
H
data
33
Listowa implementacja drzewa binarnego (najprostsza)
root(T)
A
B
D
Każdy węzeł zawiera
Dane oraz 2 wskaźniki:
•  lewy potomek
•  prawy potomek
C
E
F
G
H
data
34
Listowa implementacja drzewa (n-drzewa)
root(T)
A
B
C
D
E
F
D
G
J
H
I
Każdy węzeł zawiera
Dane oraz 3 wskaźniki:
•  przodek
•  lewy potomek
•  prawe rodzeństwo
K
35
Przykład zastosowania - Algorytm kodowania Huffmana
Ø 
David Huffman (1952) wymyślił sprytną metodę konstrukcji
optymalnego kody prefixowego (prefix-free) o zmiennej długości
słów kodowych
–  Kodowanie opiera się o częstość występowania znaków
Ø 
Optymalny kod jest przedstawiony w postaci drzewa binarnego
–  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
36
Algorytm kodowania Huffmana
Ø 
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 mniejszą częstotliwość znaku:
– 
GET-MIN(Q) zwraca element o najniższej częstości i usuwa go z kolejki
37
Algorytm Huffmana
wejście: alfabet C i częstości f [ ]
wyjście: drzewo kodów optymalnych dla C
HUFFMAN(C, f )
n ← |C|
Q←C
for i ← 1 to n-1
z ← New-Node( )
x ← z.left ← GET-MIN(Q)
y ← z.right ← GET-MIN(Q)
f [z] ← f [x] + f [y]
INSERT(Q, z)
return GET-MIN(Q)
Czas wykonania O(n lg n)
38
Kody Huffmana
Ø Przykład kodowania Huffmana:
– tekst:
Ø Kodowanie Huffmana
– Jest adaptowane dla każdego tekstu
– Składa się z
•  Słownika, mapującego każdą literę
tekstu na ciąg binarny
•  Kod binarny (prefix-free)
Ø Prefix-free
– Korzysta się z łańcuchów o zmiennej
długości s1,s2,...,sm , takich że żaden z
łańcuchów si nie jest prefixem sj
m
a
n
a
m
t
i
a
m
p
i
a
p
t
i
znak
częstość
kod
a
5
10
i
4
01
p
3
111
m
2
000
t
2
001
n
2
110
Zakodowany tekst:
a
p
i
000 10 110 10 000 10 000 10 111 10 001 01 111 01 001 01 111 01
m
a
n
a
m
a
m
a
p
a
t
i
p
i
t
i
p
i
39
Budowanie kodów Huffmana
Ø 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)
–  Oznacz gałęzie 0 i 1
znak
częstość
a
5
i
4
p
3
m
2
t
2
n
2
1
Ø Zbuduj kod z oznaczeń gałęzi
10
znak
kod
a
10
i
01
p
111
m
000
t
001
n
110
1
0
18
8
0
0
1
5
1
3
p
111
0
2
n
110
1
5
4
a
10
2
4
0
2
i
t
m
01
001
000
40
Download