« poprzedni punkt | następny punkt » |
Kolejki, podobnie jak stosy, służą do reprezentowania skończonych ciągów i dopuszczają wykonywanie na nich prostych operacji wkładanie usuwania, badania, czy kolejka jest pusta. Organizacja kolejki przypomina organizację kolejki klientów przed okienkiem na poczcie: obsługiwana jest zwykle najpierw pierwsza osoba z kolejki, a nowoprzybyły klient ustawia się na jej końcu. Zasadę rządzącą kolejką przyjęło się określać z angielskiego FIFO: first in - first out.
Standardową strukturą kolejek nazywamy strukturę
Q(E) = < E + Q, in out, first, empty, = >,
in(e, q) =df (e1,e2,...,en,e),
out(q) =df (e2,...,en), gdy n>1 i nieokreślone w pp.
first(q) =df e1, gdy n>1 i nieokreślone w pp.
empty(q) =df true wttw q jest ciągiem pustym, tzn. n=0,
s = s' wttwdf n = m oraz ei = e'i dla i=1,...,n.
Zasada wykonywania operacji w kolejce jest inna niż w stosach: operacje usuwania elementu z kolejki i wkładania nowego elementu do kolejki, odbywają się na różnych jej końcach. Kolejność elementów w kolejce jest związana z kolejnością ich wstawiania do kolejki.
Podobnie jak stosy, kolejki można w komputerze reprezentować na różne sposoby. Omówimy tu dwie z nich: Kolejkę w postaci tablicy i kolejkę w postaci listy.
Implementacja I
Jako pierwszą omówimy implementację kolejki z użyciem tablicy. Kolejka jest reprezentowana przez tablicę TAB, zawierającą elementy składowe kolejki i dwa wskaźniki head i tail, wskazujące pozycje początku i końca kolejki. Jest to więc obiekt o trzech atrybutach TAB, head, tail, jak na rysunku 6.3.
head | tail | |||||||||
¯ | ¯ | |||||||||
TAB: | 1 | 2 | 3 | ... | i | ... | n | |||
e1 | e2 | e3 | ... | ei | ||||||
Rysunek 6.3 |
Operację wkładania do kolejki realizujemy przez wpisanie nowego elementu na pozycję tail+1. Usunięcie elementu z kolejki, to po prostu przesunięcie wskaźnika head o jedną pozycję w prawo. Oczywisty kłopot powstaje wtedy, gdy do kolejki zawierającej n elementów chcemy dodać jeszcze jeden element. Możemy go rozwiązać tak jak w przypadku stosów: przepisać zawartość tablicy do tablicy większej. W kolejkach mamy jednak jeszcze jeden problem. Usuwanie elementów z początku tablicy powoduje powstanie "dziury", którą moglibyśmy wykorzystać, o ile tablicę będziemy wypełniać cyklicznie: jeśli pozycja n-ta jest zapełniona to następny element wkładamy na pozycję pierwszą, o ile jest ona wolna. Na rysunku 6.4 pokazano kolejne stany kolejki w wyniku wykonania ciągu operacji wkładania i usuwania.
head | tail | ||||||||||||||||
¯ | ¯ | ||||||||||||||||
TAB: | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ||
a | b | c | d | e | f | g | h | i | j | k | l | ||||||
stan początkowy kolejki q | |||||||||||||||||
head | tail | ||||||||||||||||
¯ | ¯ | ||||||||||||||||
TAB: | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ||
d | e | f | g | h | i | j | k | l | m | ||||||||
stan po wykonaniu operacji: in(m,q); out(q); out(q); out(q); | |||||||||||||||||
tail | head | ||||||||||||||||
¯ | ¯ | ||||||||||||||||
TAB: | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ||
p | d | e | f | g | h | i | j | k | l | m | n | o | |||||
stan po wykonaniu operacji: in(n,q); in(o,q); in(p,q); | |||||||||||||||||
Rysunek 6.4 | |||||||||||||||||
Przyjmujemy, że kolejka jest pusta, jeśli head = 0. Operację first można zrealizować wykonując jedną instrukcję przypisania {if (q.head¹0) then return q.TAB[head] fi}. Oczywiście wynik nie będzie poprawny, gdy kolejka jest pusta, dlatego zwykle obsługujemy ten błąd. Operację wkładania elementu można zrealizować następująco:
in(x,q){ if (q.head = 0) then
q.head :=1; q.tail :=1
else
if not ((q.tail mod n)+1 = q.head ) then
q.tail := (q.tail mod n) + 1
fi ;
fi;
q.TAB[q.tail] := x;
return q
}.
Jeżeli kolejka q jest pusta, to w wyniku wykonania operacji in(x,q) zarówno atrybut head, jak i tail, wskazują pierwszy element tablicy TAB. W przeciwnym przypadku musimy zbadać, czy jest wolne miejsce w tablicy. Jeśli tak, to zwiększamy wartość wskaźnika tail o jeden i umieszczamy tam x. Tak zrealizowana operacja wkładania będzie funkcjonować poprawnie przy założeniu, że liczba elementów w kolejce nigdy nie przekroczy n.
Realizacja usuwania jest też prosta.
out(q){ if (q.head > 0) then
if q.head = q.tail then
q.tail :=0; q.head := 0
else
q.head := (q.head mod n) +1;
fi fi;
return q
}.
Zauważmy jeszcze, że koszt operacji in, out, first jest stały, przy założeniu, że tablica reprezentująca kolejkę ma dostatecznie duży rozmiar, tak by nigdy nie było w kolejce więcej niż n elementów.
Implementacja II
W tej implementacji będziemy myśleli o kolejce jako o obiekcie typu queue, mającym dwa atrybuty head i tail, wskazujące odpowiednio pierwszy i ostatni element kolejki. head i tail będą same obiektami typu LINK, o dwóch atrybutach val i next. Wartością atrybutu val jest przechowywany w kolejce element, a next jest referencją do obiektu typu LINK. Na rysunku 6.5 przedstawiono przykładową kolejkę, złożoną z n obiektów typu LINK. Elementami tej kolejki są e1,...,en. Atrybut head wskazuje pierwszy element kolejki, a atrybut tail - ostatni. Przy takiej realizacji kolejki, nie ma problemów z przepełnieniem: nowy element dowiązujemy do końca kolejki, a usuwamy element wskazany przez head.
e1 | next | ® | e2 | next | ® | ... | ® | en-1 | next | ® | en | next | ® |
null |
|
|
| | |||||||||||||||
head | tail | |||||||||||||||
Rysunek 6.5 |
Kolejka jest pusta, jeśli wartością head jest null. Realizacja operacji jest niezwykle prosta:
first(q){if (q.head ¹ null) then return q.head.val fi }
in(x,q){pom := new LINK(); pom.val := x;
if (q.head ¹ null) then
q.tail.next := pom
else
q.head := pom
fi;
q.tail := pom;
return q
}out(q){if (q.head ¹ null) then q := q.head.next fi}
empty (q){ return (q.head = null) }.
Operacje first i out są operacjami częściowymi, tak jak to sygnalizowaliśmy w strukturze standardowej.
Zastanówmy się, jakie wspólne cechy mają te dwie implementacje struktury kolejek? Jakie własności charakteryzują kolejki?
Specyfikacja kolejek
Rozważmy następujący zestaw formuł (Q1)-(Q7), w których q jest zmienną typu kolejka, a x jest zmienną typu element kolejki.
(Q1) empty(q) ® out(in(x,q)) = q
(Q2) empty(q) ® first(in(x,q)) = x
(Q3) Ø empty(in(x,q))
(Q4) Ø empty(q) ® first(in(x,q)) = first(q)
(Q5) Ø empty(q) ® in(x,out(q)) = out(in(x,q))
(Q6) każda kolejka jest skończona
(Q7) kolejki q i q' są równe wttw dla każdego i, albo first(out i (q)) = first(out i (q'))
Zauważmy najpierw, że niezależnie od tego, czy rozważamy strukturę standardową, czy jedną z opisanych implementacji kolejek, wszystkie te formuły są prawdziwe. Rzeczywiście, pierwsze dwie opisują sytuację, gdy kolejka jest pusta: włożenie i usunięcie jednego elementu pozostawia znów kolejkę pustą, a po włożeniu elementu, to on jest tym, który zwraca funkcja first. W przypadku, gdy kolejka jest niepusta, operacja in nie zmienia wartości first, a ponadto operacje in i out są przemienne.
Postulaty (Q6) i (Q7) mają nieco inny charakter i nie mogą być zapisane jako zwykłe formuły logiki klasycznej. Podobnie jak w przypadku stosów możemy je zapisać jako własności pewnych algorytmów. Niezapętlanie się programu
{while not empty(q) do q := out(q) od}
oznacza, że kolejka q jest skończona. A program
{bool := true;
while (not empty(q) and not empty(q') and bool) do
bool := (first(q) = first(q));
q := out(q); q':= out(q');
od
}
o ile kończy się spełnieniem warunku (bool and empty(q) and empty(q')) stwierdza równość dwóch kolejek q i q'.
Można udowodnić, że każda struktura danych, w której formuły (Q1)-(Q7) są
prawdziwe jest izomorficzna z pewnym modelem standardowym kolejek. Wynika
stąd, że postulaty (Q1)-(Q7) możemy przyjąć jako definicję abstrakcyjnej
struktury kolejek. Nieważne jak naprawdę reprezentowana jest kolejka i jak
zrealizowane są jej operacje. Wystarczy wiedzieć, że ma własności opisane w
specyfikacji, by móc się nią poprawnie posłużyć.
Pytanie 3: Do początkowo pustej kolejki q, włożono kolejno elementy a,
b, c, d, e. Ile operacji first, in, out trzeba wykonać, aby utworzyć kolejkę, w której kolejność elementów jest odwrotna
niż w kolejce q?
« poprzedni punkt | następny punkt » |