background image

Generalnie mo

 

że być trochę błędów ale

zgrubsza je poprawi

 

łem, skanowalem od strony 12

Przyk

 

ład. Dane n = l oraz z = 1000. Wylosowano następujące liczby: 15, 8, 155,

27, 310, 911, 544. Zawartość tablicy po wykonaniu procedury generacja(n, z, a):

2.1.2.    Wyznaczanie najmniejszego elementu tablicy

Przeglądanie informacji zapisanej w tablicy jednowymiarowej w celu znalezi

nią elementu o najmniejszej wartości jest operacją o złożoności  liniowej  i zaws.
dotyczy całej długości tablicy. Operacja ta może być rozszerzona o wyprowadzan
informacji o miejscu w tablicy (czyli o indeksie), w którym znajduje się eleme

Algorytm wyznaczania najmniejszego elementu tablicy

Dane: n - długość tablicy, a - tablica o długości n.

W y ni k.  Wartość najmniejszego elementu tablicy a.

Metoda: Niech oznacza najmniejszy element tablicy. Przyjmij początkowo,
pierwszy element tablicy jest poszukiwanym najmniejszym elementem. Następ
dla każdego / od 2 do porównuj a{i\ x. Jeśli a[i] jest mniejsze od x, to popi
wartość x. Złożoność: O(ń}.

Implementacja

const nmax = 100;
type ind = L.nmax;

t = array[ind] of integer;

function min(n:integer; var art
var i:integer; x:integer; begin

x:=a[ll;
for i:—2 to n do

if a[i] < x then x:=a[i];

min:=x end; { min }

i n t e g e r ;

Przykład. Po wywołaniu funkcji .y:=min(w, d) dla danych z poprzedniego przykł;
du zmienna będzie miała wartość 8.

background image

13

2.1.3.    Sortowanie tablicy

Sortowanie informacji występuje bardzo często jako element bardziej rozbudo-

wanych zadań. Opracowano wiele różnych metod sortowania tablicy o złożoności
obliczeniowej od O(n

2

do O(nlogn). Opis kilku najważniejszych można znaleźć

w książce [7]. Metodę wykorzystaną w algorytmie sortowania tablicy zastosujemy
również do sortowania listy jednokierunkowej.

Algorytm sortowania tablicy

Dane: n - długość tablicy, a - tablica o długości n.
Wynik: a - posortowana tablica.
Metoda: Wykorzystamy metodę sortowania ciągów zwaną prostym wybieraniem.
W tablicy początkowo nie posortowanej stopniowo powstaje część posortowana
(lewa, czyli o indeksach począwszy od 1) oraz zmniejsza się część nie posortowa-
na. Niech / będzie indeksem początkowego elementu nie posortowanej części ta-
blicy. Dla każdego i' o d  l do n - znajdź  najlepszy element (najmniejszy, jeśli
sortujemy rosnąco) w nie posortowanej części tablicy, zamień go z elementem a[/].
Złożoność: O(n

2

).

Implementacja

c o n s t     n m a x     = 1 0 0 ; t y p e
i n d   =    l.. n m a x ;

t   =     a r r a y [ i n d ]       o f     i n t e g e r ;      {    a     }

p r o c e d u r e     s o r t ( n : i n t e g e r ;       v a r     a : t ) ;
v a r     i , j , k : i n t e g e r ;       n a j : i n t e g e r ;   b e g i n

fo r   i:=l   t o    n   -   1    d o

b e g i n

naj:=a[i]; k:=i; for
j:=i + 1 to n do if
a[j] naj then

begin naj:=a[j]; k:=j; end; { popraw }

if i o k then

end
;

begin
a[k] :=a[i]; a[i]
:=naj; end; end; {
i } { sort }

{ zamie

ń }

background image

14

2.1.4.    Obliczanie wartości sumy elementów tablicy

Jest to prosta operacja polegająca na stopniowym obliczaniu wyniku przez do-

dawanie wartości  kolejnego  elementu  tablicy do wartości sumy  obliczonej  w  po-
przednim  kroku  algorytmu.  Ważna  jest  właściwa  inicjalizacja  obliczenia,  czyli
przypisanie wynikowi wartości początkowej równej zero.  Zauważmy, że w przy-
padku obliczania iloczynu elementów tablicy wartością początkową wyniku byłaby
liczba 1.

Algorytm obliczania wartości sumy elementów tablicy jednowymiarowej

Dane: n - długość tablicy, a - tablica o długości n.

Wynik: s - suma elementów tablicy.

Metoda: Niech oznacza sumę elementów tablicy.
1.  Przygotuj s, czyli podstaw zero do s.
2.  
Przeglądaj tablicę od pierwszego do ostatniego elementu i wartość kolejnego

elementu dodawaj do poprzednio obliczonej wartości sumy s.

Złożoność: O(ri).

Implementacja

c o nst   n m a x   =   10 0;
t y p e   i n d   =    L. n m a x ;

t   =   a r r a y[ i n d ]    of  i nt e g er ;     {    a    }

f u n c t i o n     s u m a ( n : i n t e g e r ;       v a r     a : t ) : i n t e g e r ;
v a r     i : i n t e g e r ;       s : i n t e g e r ;   b e g i n

s : = 0 ;

f o r     i : = l       t o     n   d o     s : = s     +     a [ i ] ;

s u m a : = s;  end;

{   suma   }

2.1.5.    Wyszukiwanie informacji w tablicy

Ważną operacją słownikową jest wyszukiwanie informacji w strukturze danych

czyli operacja member. Jeżeli struktura jest bardzo duża i operacja jest wykonywa-
na często, to optymalizacja wyszukiwania ma istotny wpływ na jakość całego pro
jektu. Istotną cechą dobrego wyszukiwania jest zakończenie działania, gdy poszu-
kiwana  informacja  zostanie  znaleziona.  Przeglądanie  struktury  jednowymiarowe
(np. tablicy) od początku do końca ma miejsce tylko wówczas, gdy nie ma poszu
kiwanej  informacji  w  tej  strukturze  lub  gdy  zostanie  ona  znaleziona  w  ostatnin
elemencie. Odmianą operacji member jest nie tylko odpowiadanie na pytanie „cz;
jest?", ale również „gdzie jest?" lub „ile razy występuje?".

background image

15

Algorytm wyszukiwania informacji w tablicy jednowymiarowej

Dane: x - informacja (np. liczba całkowita), n - długość tablicy, a - tablica.

Wynik: Informacja o tym, czy x jest elementem tablicy.

Metoda: Dla wartości indeksów tablicy od l do co najwyżej porównaj wartość
kolejnego elementu tablicy z wartością x. Jeśli zostanie znalezione w tablicy, to
zakończ wyszukiwanie.

Złożoność: O(n).

Implementacja

const nmax = 100;
type ind = 1.. nmax;

t = array[ind] of integer;

function member(x: integer; n: integer; var a: t):
Boolean; var i: integer; jest: Boolean; begin

jest: =false;
i: =l;

w h i l e    (i    < =    n)    a n d  n ot   j est   d o

i f     a [ i ]       =     x     t h e n     j e s t :   = t r u e     e l s e     i :   = i    +       1 ;

m e m b er :  = j es t ;

end; {  member   }

2. 2. Tablice dwuwymiarowe i tablice rekordów

2. 2. 1. Generacja grafu losowego G(n, p)

Graf jest określany jako para G = (V, E), gdzie F jest zbiorem wierzchołków a

E- zbiorem krawędzi grafu.  Zwykle  przyjmuje  się  F= {l, 2,...,  n} i  wtedy  zbiór
zawiera pewną liczbę par wierzchołków (i, j). Wierzchołki  grafu, między  który-
mi  istnieje  krawędź,  nazywamy  przyległymi.  Jeżeli  krawędzie  mają  wyróżniony
kierunek, to graf  nazywamy  grafem  skierowanym  (ang.  directed  graph  lub  krótko
digraph). Będziemy rozważać przede wszystkim grafy proste, czyli nie skierowane,
bez krawędzi wielokrotnych oraz bez tzw. pętli własnych (i, i).

Macierz  przyległości  grafu  jest  macierzą  kwadratową  o  elementach  O   i   1 ,

zawierającą  informację  o  krawędziach  grafu.  Jeśli  element  tej  macierzy  o
indeksach  i,  j  ma  wartość  l,  to  w  grafie  istnieje  krawędź  (i,  f).  Największą  liczbę
krawędzi na wierzchołkach, czyli \E\ = n(n - l)/2, ma graf K

n

nazywany grafem

pełnym (ang. complete graph).

Graf losowy jest to graf, którego krawędzie zostały wylosowane zgodnie z okre-

ślonym modelem. Jednym ze znanych modeli grafów losowych jest model G(n, p),

background image

16

w którym rozważa się niezależnie każdą krawędź grafu pełnego K

n

 i z prawdopo-

dobieństwem wybiera ją do grafu losowego G o wierzchołkach. Wartość ocze-
kiwana liczby krawędzi grafu G jest równa pn(n - l)/2.

Reprezentacją  macierzy  przyległości  grafu  G  w  strukturze  danych  jest  tablica

kwadratowa A o elementach typu logicznego („false" i „true") lub typu całkowite-
go  (O  i  l,  odpowiednio).  Jeśli  w  grafie  G  istnieje  krawędź  (i,j),  to  element  i-tego
wiersza oraz j-tej kolumny w ma wartość „true", a także j-tego wiersza oraz i-tej
kolumny, ponieważ G jest grafem prostym. Z tego samego względu na przekątnej
głównej tablicy powinny się znajdować wartości „false".

Algorytm generacji grafu losowego = G(«, p)

Dane: n - liczba wierzchołków grafu, p - prawdopodobieństwo krawędziowe.
Wynik: A - macierz przyległości grafu G reprezentowana w tablicy kwadratowej.
Metoda: Każda krawędź ze zbioru o liczności n(n - l)/2 ma być niezależnie wy-
brana do grafu G (na wierzchołkach) z prawdopodobieństwem p.

1.  Przygotuj tablicę (wystarczy przygotować jej główną przekątną).
2.  Dla każdego elementu tablicy o indeksach i, j (l <= i <=j <= n):

a.

wylosuj liczbę z przedziału (O, 1),

b. jeśli ta liczba jest nie większa niż p, to dodaj krawędź (i, j) do grafu G,
c.

uzupełnij informację o krawędzi (j, i) w tablicy A.

Złożoność: O(n

2

).

Implementacja

Boolean

Rys. 2.2. Tablica kwadratowa o elementach typu logicznego do reprezentacji grafu

background image

17

   

const nmax = 100;
type ind = l..nmax;

t2 = array[ind,ind] of Boolean;  { A }

procedure Gnp(n:integer; p:real; var A:t2);
var i,j:integer; x:Boolean; begin

for i:=l to n do A[i,i]:=false; { przek

ątna główna }

for i:=l to n - 1 do for j:= i + 1 to n  do begin

x:= random <= p; A[i,j]:=x;
A[j,i]:=x; end; end; { Gnp }

Przykład. Wylosowano następujący graf przy n = 4, p = 0.5

G:      l

2

2.2.2.    Generacja grafu losowego G(n, K)

Drugim modelem grafów losowych, wykorzystywanym w projektowaniu  inży-

nierskim, jest model G(n, k), w którym dana jest liczba wierzchołków grafu i liczba
jego krawędzi. Jest to model sekwencyjny, czyli do grafu, który nie ma jeszcze ani
jednej krawędzi, dodaje się pierwszą wylosowaną krawędź, potem drugą i tak da-
lej,  aż  do  uzyskania  grafu  o  k  krawędziach.  Aby  poprawnie  wygenerować  graf
losowy G =  G(n, k),  należy spełnić w każdym  kroku algorytmu  warunek  jednako-
wego prawdopodobieństwa wylosowania każdej jeszcze nie wybranej krawędzi.

Zwykle generujemy więcej niż jeden graf losowy, musimy więc zwrócić uwagę,

że operacja przygotowania tablicy krawędzi (czyli init) jest wykonywana tylko
raz - przed generacją pierwszego grafu.

background image

18

Algorytm generacji grafu losowego G = G(n, k)

Dane: n  -  liczba  wierzchołków,  k  -  liczba  krawędzi  grafu, E  -  tablica  wszystkich
krawędzi na wierzchołkach.

Wynik: Graf G jest reprezentowany  w tablicy E  (k elementów  o  najwyższych  in-
deksach, czyli z prawej części tablicy).

Metoda:  Początkowo  w  tablicy  E  znajdują  się  wszystkie  krawędzie  na  n  wierz-
chołkach  w  dowolnym  porządku.  Niech  h  oznacza  ostatni  (czyli  największy)  in-
deks w roboczej części tablicy E, czyli tej części, z której będziemy  losować  kra-
wędzie grafu G.

1.  Podstaw równe n (n - l)/2.

2.  Powtórz k razy następujące operacje:

a.

losuj z jednakowym prawdopodobieństwem jeden z indeksów tablicy E
z przedziału [l, h],

b. zamień wylosowany element z elementem E[h] oraz

c.

zmniejsz h o 1.

Złożoność: O(n

2

).

Implementacja

Rys. 2.4. Tablica rekordów o dwóch polach typu całkowitego do generacji grafu G = G(n, k)

const nmax = 100; kmax = nmax*(nmax - 1) div 2;
type ind = 1.nmax;

t = array[ind] of integer;
r = record a,b:integer end;
tl = array[indl] of r;

{ E }

procedure init(n:integer; var E:t1; var total:integer) ;
var i,j,h:integer; begin

h:=0;

{ przygotowanie indeksu }

background image

19

f o r     i : = l    t o     n    -     1     d o   for

j : =i   +   1   to   n  do  b e g i n

h: = h   +   1;

{   powiększenie   indeksu   }

w i t h   E[ h]    d o   b e g i n  a: = i;    b: = j    e n d ;      {   krawędź   }

e n d ;

t o t a l : = h ;

{   liczba  wszystkich  krawędzi,   n(n - l ) / 2      }

en d;    {   init   }

p r o c e d u r e     G n k ( n , k , t o t a l : i n t e g e r ;       v a r    E : t 1 ) ;
v a r     i , h , z : i n t e g e r ;       x :   r ;
b e g i n

h : = t o t a l ;   f o r    i : = l
t o      k   d o   b e g i n

z : = r a n d o m( h )     + 1 ;      {   l o s o w a n i e   i n d e k s u    t a b l i c y  E      }
x: = E [ z];    E [ z ] : = E [ h ] ;     E[ h]:= x;

{   za miana   }

h:=h  -   1;         {   zmniejszenie  przedziału  losowania   } e n d ;

end;    {   Gnk   }

2.2.3.    Transformacja E-->A reprezentacji grafu

Jeżeli po wygenerowaniu grafu losowego G = G(n, k) w tablicy wykonujemy

dalsze  operacje,  do  których  realizacji  wygodniejszą  strukturą  danych  jest  tablica
kwadratowa, to należy przekształcić pierwotną reprezentację grafu w reprezenta-
cję docelową. W miarę poznawania nowych struktur danych należy ćwiczyć się
w umiejętności przekształcania informacji z jednej struktury danych w inną.

Algorytm transformacji E —>A reprezentacji grafu

Dane: n - liczba wierzchołków, k - liczba krawędzi, E - reprezentacja grafu loso-
wego G = G(n, k).
Wynik: A - reprezentacja grafu G w tablicy kwadratowej.
Metoda:
1.  Oblicz total = n(n - l)/2 - największy indeks tablicy E.
2.  
Przygotuj tablicę (wszystkie elementy).
3.  Wybieraj informację o krawędziach grafu G = G(n, k) z tablicy E (k elemen

tów o indeksach od total do total - k + 1) i przepisuj do tablicy A.

Złożoność: O(n

2

).

background image

Rys. 2.5. Struktury danych do transformacji E —> reprezentacji grafu G = G(n, k)

const nmax = 100; kmax = nmax*(nmax-1) div 2;
type ind = 1..nmax; ind1 = l..kmax; r =
record a,b:integer end;

tl = array[ind1] of r;

{ E }

t2 = array[ind,ind] of Boolean;

{ A }

procedure transEA(n,k:integer; var E:tl; var A:t2);
var i,j,h,total:integer;  begin

total:=n*(n - 1) div 2;
for i:=l to n do

for j:=l to n do A[i,j]:=false;

for h:=total downto total - k + 1 do    { kraw

ędzie }

begin

with E[h] do begin i:=a; j:=b end;
A[i,j]:=true; A[j,i]:=true; end; end; {
transEA }

background image

21

2.3.   Zbiory i tablice rekordów zawierających zbiory

2.3.1.    Tworzenie zbioru losowych liczb całkowitych

Zbiory są strukturą stosowaną w przypadku przetwarzania danych z ograniczo-

nego zakresu wartości, np. liczb całkowitych z przedziału  od  l do 256.  W  języku
Pascal  elementami  zbiorów  mogą  być  tylko  zmienne  okrojonego  typu  prostego.
Operacjami  standardowymi  są:  suma,  różnica  i  iloczyn  zbiorów  (o  symbolach
operacji odpowiednio +, -, *) oraz relacja należenia do zbioru (słowo  kluczowe
in). Wyrażenia typu zbiorowego umieszcza się w nawiasach kwadratowych. Dal-
sze szczegóły dotyczące typu zbiorowego w języku Pascal znajdują się w książce
[3]. Poniższy algorytm tworzy zbiór (o liczności n) liczb  całkowitych  z prze-
działu od l do z.

Algorytm tworzenia zbioru losowych liczb całkowitych

Dane: n - liczność zbioru, z - zakres losowania.
Wynik: w - zbiór losowych liczb całkowitych z przedziału [l, z].

Metoda:

1.  Przygotuj zbiór.
2.  Powtórz razy losowanie liczby z przedziału od l do oraz dodawanie jej do

zbioru.

Złożoność: O(n).

Implementacja

const nmax = 100; type
elem = 1.nmax;

zbiór = set of elem;

procedure konsr(n,z:integer; var w:zbior);
var i:integer; x:elem; begin w: = [];

for i:=l to n do

begin

x:= random(z) + 1; w:=w +
[x] end; end; { konstr }

background image

22

2.3.2.    Wyprowadzanie zawartości zbioru

Wyprowadzanie zawartości zbioru można zaprojektować jako sprawdzanie ca-

łego zakresu wartości jego elementów zgodnie ze zdefiniowanym  typem. Lepsze
rozwiązanie przedstawia podany niżej algorytm, w którym zastosowano przegląda-
nie  zbioru  w  i  odejmowanie  od  niego  kolejnych  znalezionych  elementów.  W  im-
plementacji  tego  algorytmu  zbiór  w  jest  przekazywany  do  podprogramu  „przez
wartość", a więc można zmieniać jego zawartość, nie niszcząc wartości zmiennej
globalnej.

Algorytm wyprowadzania zawartości zbioru

Dane: w - zbiór liczb całkowitych, first - liczba całkowita.
Wynik: Wyprowadzenie do pliku wyjściowego (wydrukowanie) elementów zbioru
nie mniejszych niż first.
Metoda: Dopóki zbiór nie jest pusty, wyprowadzaj kolejne elementy zbioru,
odejmując każdorazowo ten element od zbioru.

Złożoność: O(n).

Implementacja

c o n s t   n m a x    =    1 0 0 ; t y p e
e l e m   =       l. . n m a x ;

z b i ó r     =     s e t   o f     e l e m ;

p r o c e d u r e     d r u k z b i o r ( f i r s t : i n t e g e r ;       w : z b i o r ) ;
v a r     x : e l e m ;   b e g i n

x : = f i r s t ;   w hile  w
< >   []    do  b e g i n

if   x   i n   w   t he n

b e g i n

w r i t e ( o u t ,      x :   3 ,       ' , ' ) ;
w : = w  -   [x]  e n d ;

x : = x     +     1 ;  e n d ;  e n d;

{    dr u k z b i or    }

2.3.3.    Transformacja A-->S reprezentacji grafu

W przypadku wykonywania niektórych operacji na grafach, np. rozdzielania je-

go reprezentacji według składowych grafu, stosuje się strukturę tablicową, która

background image

23

zawiera informację o wierzchołkach przyległych, czyli sąsiadach kolejnych wierz-
chołków  grafu.  Jeśli  graf  G  został  wygenerowany  w  tablicy  kwadratowej  A,  to
transformację  jego  reprezentacji  w  tablicę  sąsiadów  S  przedstawia  poniższy  algo-
rytm.

Algorytm transformacji -> reprezentacji grafu

Dane:  n - liczba wierzchołków grafu, A - macierz przyległości grafu G.
Wynik: S - reprezentacja grafu G w tablicy sąsiadów.

Metoda:

1.  Przygotuj tablicę sąsiadów S.
2.  
Dla każdego elementu A[i, j], l <= i < j <= n , sprawdź, czy wierzchołki i,

j
grafu są przyległe. Jeśli tak, to aktualizuj elementy S[i] oraz S[j] w następujący
sposób:
a. dodaj do zbioru sąsiadów wierzchołka i,
b. dodaj i do zbioru sąsiadów wierzchołka j
c. powiększ o l odpowiednie liczniki l.

Złożoność: O(n

2

).

Implementacja

Rys. 2.6. Struktury danych w implementacji algorytmu transformacji A—> reprezentacji

grafu G

background image

24

const nmax = 100;
type ind = l..nmax;

elem = 1..nmax;

zbior = set of elem;

rS = record w:zbior;

l:integer; end;

t2 = array[ind,ind] of Boolean;     { A }
tS = array[ind] of rS;

{ S }

procedure transAS(n:integer; var A:t2; var S

var i,j:integer;

begin

for i:=l to n do with S[i] do begin w:=[];
for i:=l to n - 1 do for j:=i + 1 to n do
if A[i,j] then begin

with S[i] do begin w:=w + [j]; 1:=1 + with
S[j] do begin w:=w + [i]; 1:=1 + end; end; {
transAS }

2.4.   Przetwarzanie informacji o grafach w strukturach tablicowych

2.4.1.    Testowanie spójności grafu

Odwiedzanie  wszystkich  wierzchołków  grafu  jest  podstawą  większości  algo

rytmów. Dwie główne metody przeglądania grafu  to: metoda BFS (Breadth  Fir.
Search), 
czyli przeglądania wszerz, i metoda DFS (Depth First Search), czyli prze
glądania  w  głąb.  W  metodzie  BFS  przegląda  się  systematycznie  wszystkich  m
odwiedzonych sąsiadów kolejnego wierzchołka. W metodzie DFS odwiedza się
sąsiada danego wierzchołka, następnie jego nie odwiedzonego sąsiada i tak dale
aż  do  odwiedzenia  wszystkich  wierzchołków,  do  których  można  dojść  z
danego

1;
1;

e n d;
e n d;

t S )  ;

l:=0;   e n d;

Przykład. Reprezentacja grafu G w tablicy S

background image

25

wierzchołka. Jeśli graf nie jest spójny, to aby odwiedzić wszystkie jego wierzchoł-
ki,  należy wielokrotnie wybierać wierzchołki początkowe. Szczegółowy  opis  tych
metod znajduje się w książce [5].

Algorytm testowania spójności grafu przegląda wierzchołki, rozpoczynając tyl-

ko raz - od danego wierzchołka. Jeśli odwiedzi wszystkie wierzchołki, to graf jest
spójny (czyli ma jedną składową), w przeciwnym przypadku graf nie jest spójny.

Rozszerzeniami algorytmu testowania spójności są: zliczanie składowych spój-

ności oraz rozdzielanie reprezentacji grafu według jego składowych.

Algorytm testowania spójności grafu

Dane: n - liczba wierzchołków, A - macierz przyległości grafu G.

Wynik: Informacja o tym, czy graf G jest spójny.

Metoda: Wykorzystuje się przeglądanie grafu rekurencyjną metodą DFS.

1.  Odwiedzamy wierzchołek v = 1.
2.  Szukamy wierzchołka j - pierwszego nie odwiedzonego sąsiada wierzchołka v.
3.  Odwiedzamy wierzchołek j.
4.  Podobnie jak z wierzchołkiem v postępujemy z wierzchołkiem j.
5.  Jeżeli odwiedzimy   w ten sposób   wszystkie wierzchołki grafu, to graf jest

spójny.

Złożoność: O(n

2

).

Implementacja

Rys. 2.8. Struktury danych zastosowane w implementacji algorytmu testowania spójności grafu:
tablica kwadratowa oraz wektor x, w którym przechowuje się informację o tym, które wierzchołki

grafu zostały odwiedzone

background image

26

const nmax = 100;
type ind = 1.nmax;

t2 = array[ind,ind] of Boolean;  { A } t3 =
array[ind] of Boolean;      { x } procedure
DFS(n,v:integer; var A:t2; var x:t3); var
j:integer; begin

for j : =1 to n do

if A[v,j] and not x[j] then

begin

x[j]:=true; DFS(n, j, A,
x) end; end; { DFS }

function spójny(n:integer; var A:t2):Boolean;

var i:integer; x:t3; b:Boolean;

begin

for i:=l to n do x[i]:=false;
x[1]:=true;
DFS(n, 1, A, x);

b:=true;

i: =1 ;

while (i <= n) and b do

begin b:=x[i]; i:=i + 1; end;

spójny:=b;

end; { spójny }

2.4.2.    Wyznaczanie liczby trójkątów w grafie

Liczba cykli o danej długości na różnych zbiorach krawędzi jest często poszu

kiwaną  własnością  grafu.  Przegląd  algorytmów  zliczania  cykli  rozpoczynamy  od
najłatwiejszego z nich, czyli od wyznaczania liczby cykli o długości równej 3.

Algorytm wyznaczania liczby trójkątów w grafie

Dane:   n - liczba wierzchołków, A - macierz przyległości grafu G.

Wynik: x - liczba trójkątów w grafie G.
Metoda: Początkowo liczba trójkątów jest równa zeru. Przeglądaj krawędzie graft
i dla każdej z nich sprawdź, czy jej końce mają wspólnego sąsiada. Jeśli tak, te
zwiększ liczbę trójkątów w grafie o jeden.

Złożoność: O(n

3

).

background image

27

Implementacja

co nst  n ma x  =  100;
ty p e   i n d   =   l.. n m a x ;

t 2     =     a r r a y [ i n d , i n d ]       o f  B o o l e a n ;

{   A   }

f u n c t i o n     t r i a n g l e s ( n : i n t e g e r ;       v a r   A :   t 2 )   : i n t e g e r ;
v a r     i , j , h : i n t e g e r ;       x : i n t e g e r ;

begin

x:=0;

f or    i : = l     t o    n    -   2

d o   f or     j: = i    +     1   t o    n
d o

i f   A [ i , j ]       t h e n

  i f  A [ i , h ]      a n d   A [ j, h ]

t h e n   x : = x     + 1 ;
tr i a n g l e s   :  = x

e n d;    {   tria n gl es    }

2.4.3.    Wyznaczanie liczby czworokątów w grafie

Przedstawiamy dwa algorytmy zliczania cykli o długości równej 4. Pierwszy

z nich jest naturalnym rozszerzeniem algorytmu zliczania cykli o długości równej 3
i dlatego jego złożoność jest O(n

4

). W drugim  algorytmie wykorzystano  inny po-

mysł - zmniejszono złożoność obliczeniową do O(n

3

).

Algorytm wyznaczania liczby czworokątów w grafie

Dane:   n - liczba wierzchołków, A - macierz przyległości grafu G.
Wynik: x - liczba czworokątów (cykli o długości 4) w grafie G.
Metoda:

1.  Podstaw równe zeru.
2.  Dla każdej krawędzi (i, j) grafu G:

a. sprawdź, czy wśród wierzchołków od j + l do jest taki wierzchołek k,

który jest sąsiadem wierzchołka ;', oraz czy wśród wierzchołków od i do n
jest taki czwarty wierzchołek h, różny od trzech pozostałych, który jest
jednocześnie sąsiadem wierzchołka i wierzchołka j;

b. jeśli tak, to zwiększ o l.

Złożoność: O(n\

Implementacja

c onst  n ma x  =   100; t y p e
i n d    =    l.. n m a x ;

t 2   =  arr a y[in d, i n d]    of   B o ol ea n;       {    A   }

background image

28

function cykl4(n:integer; var A:t2):integer;

{ Miros

ław Kupczyk, Inf III, 1996 }

var i,j,k,h,x:integer; begin x: =0 ; if n >= 4 then

for i:= 1 to n - 3 do

for j:=i + 1 to n - 1 do

if A[i,j] then

for k:=j + 1 to n do

if A[i,k] then

for h:=i + 1 to n do

if (h <> j) and (h O k) then

if A[j,h] and A[k,h] then x:=x + 1;

cykl4:=x; end; { cykl4 }

Drugi algorytm wyznaczania liczby czworokątów w grafie

Dane: n - liczba wierzchołków grafu, A - macierz przyległości grafu G.
Wynik: x - 
liczba cykli o długości 4.

Metoda: Podstaw równe zeru. Dla każdej pary wierzchołków grafu oblicz c
liczbę wspólnych sąsiadów. Jeżeli jest większe od l, to powiększ x o c(c - l)/2
liczbę cykli o długości 4 dla tej pary wierzchołków.
Złożoność: O(n

3

).

Implementacja

f u n c t i o n     c y k l 4 T F ( n : i n t e g e r ;       v a r   A : t 2 ) : l o n g i n t ;

{   T o ma sz   Fitzer ma nn,   EiT   Ir.,   1998  )

v a r     i , j , k : i n t e g e r ;       c , x : l o n g i n t ;   b e g i n x : = 0 ; if  n   > =   4
t he n

f o r  i: = 1    t o   n   -   l   d o  fo r

j: = i   +   l   t o   n  d o   b e g i n
c: = 0 ;  fo r    k: =i   +   l   to   n  d o

i f  A [ i, k ]     a n d  A [ j, k ]     t h e n     c : = c     +      1 ;  i f  c

> =    2   t h e n  x: = x    +   c * ( c  -    1)    di v    2;  end;

c y kl 4 T F : = x ;   end;

{   cykl4TF   }

background image

29

2.4.4.

Wyznaczanie liczby cykli o danej długości

Problem  zliczania  cykli  o  dowolnej  długości  ma  złożoność  wykładniczą  Do

rozwiązywania  tego  problemu  proponujemy  algorytm  Cycles  (autor  M  Kupczyk,
ulepszenia  K  Zwierzynski,  1998)  W  implementacji  algorytmu  wykorzystano
procedurę  rekurencyjną  OnCycle,  która  jest  realizacją  metody  DFS  przeglądania
grafu, wzbogaconej o elementy metody (Branch and Bound) ograniczania rozmiaru
drzewa  obliczeń  W  tym  przypadku  ograniczanie  to  polega  na  usuwaniu  z  grafu
zbędnych  krawędzi,  czyli  takich,  które  nie  będą  mogły  być  wykorzystane  do  bu-
dowy następnych ścieżek

Podczas  wykonywania algorytmu reprezentacja grafu w  tablicy  A  ulega  zmia-

nie, więc aby nie zniszczyć pierwotnej informacji o grafie G, tablica ta jest przeka-
zywana do podprogramu Cycles „przez wartość"

Algorytm wykorzystuje następujące struktury danych - tablicę kwadratową

do  reprezentacji  grafu  oraz  trzy  wektory  o  długości  n  Jeden  z  nich  -  x  o  warto-
ściach typu  logicznego  - przechowuje  (jak  w  realizacji  DFS)  informację  o  tym,
które  wierzchołki  zostały  już  odwiedzone  Dwa  pozostałe  wektory  mają  elementy
typu całkowitego Deg - ciąg stopni  wierzchołków  grafu, m - aktualna ścieżka
w grafie

Algorytm wyznaczania liczby cykli o danej długości

Dane   n - liczba wierzchołków grafu, c - długość cyklu, A - macierz przyległosci
grafu G
Wynik h - liczba cykli o długości c w grafie G

Metoda

1  Podstaw równe O
2  Dla każdego wierzchołka l należacego [l, n - c + 1] wykonaj następujące

operacje
a    wybierz jako pierwszy wierzchołek ścieżki w grafie G,
b wykorzystując metodę przeglądania grafu w głąb (DFS), konstruuj ścieżki

o  długości  c  +  l  rozpoczynające  się  od  wierzchołka  l,  nie  zawierające
wierzchołków o numerach mniejszych od i,

c   jeżeli numer ostatniego wierzchołka ścieżki jest równy numerowi jej
pierwszego wierzchołka, czyli jeżeli został znaleziony nowy cykl o długości c, to
zwiększ h o Złożoność 0((n-c+ l)!)

Implementacja

Struktury danych przedstawiono na rys 2 9

background image

30

Boolean

integer

Boolean

Rys. 2.9. Struktury danych wykorzystane w implementacji algorytmu zliczania cykli o danej d ługośi

const nmax = 100;

type ind = l..nmax;

t  =  array  [ind]  of  integer;  {  m,  Deg  }
t2  =  array  [ind,ind]  of  Boolean;  {  A  }
tx = array [ind] of Boolean;

{ x }

function Cycles(n, crinteger; A:t2):integer;

var i,j:integer; m,Deg:t; x:tx; h:integer;

procedure OnCycle(d, w, r:integer);
var v,i,j:integer;

begin

if d = c + 1 then

begin if w = m[l] then h:=h + 1 end
else begin

if not x[w] then

begin

if Deg[w] = 1 then

for v:=r to n do
begin

A[w,v]:=false;
A[v,w]:=false end
else begin 
m[d]:=w;
x[w]:=true;

background image

f o r   v : = r     t o     n   d o
if  A [ w , v ]     t h e n    O n C y c l e( d    +     1 ,    v,    r );  i f
d     =     2    t h e n  b e g i n

i : = m [ 1 ] ;   j : = m [ 2 ] ;   A [ i , j ] : = f a l s e ;
A [   j , i ] : = f a l s e ;   D e g [ i ] : = D e g [ i ]       -
1 ;   D e g [ j]   : = D e g [ j ]     -     1 ;  e n d;    {   if   d
}  m [ d ] : = - ! ;   x [ w ] : = f a l s e ;   e n d;    {
else   }  e n d;     {   if   n ot    }  e n d;    {   else
}  end;   {    O n C y cle     }

be gin   {    C ycles   }

h : = 0 ;

if  c   > =    3   t he n

b e g i n

f o r   i:  = 1    t o    n  d o

b e g i n

x [ i ] : = f a l s e ;
m [ i ] : = - 1 ;
D e g [ i ] : = 0   e n d ;

f o r   i: = l   t o    n   -    1    d o

f o r   j: =i    +    1   t o    n  d o
i f  A   [ i, j   ]      t h e n   b e g i n

D e g [ i ] : = D e g [ i]       +     1 ;
D e g [ j] : = D e g [ j]      +       1 ;
e n d ;

f o r     i : = l   t o     n     -      c     +     1     d o   O n C y c l e ( 1 ,       i ,     i ) ;

e n d;

C y c l e s: = h ;   end;

{   Cycles   }

31

background image

3.   PLIKI

3.1.   Pliki liczb całkowitych

3.1.1.   Konstruowanie pliku

Plik jest strukturą danych wykorzystywaną do przechowywania ciągów infor-

macji o nieznanej długości. Nową informację dopisujemy na końcu pliku. Zawar-
tość  pliku  można  pobierać  tylko  od  początku  pliku.  Standardowymi  operacjami
plikowymi  (zrealizowanymi  jako  procedury)  są  w  języku  Pascal:  rewrite(p)
przygotowanie pliku  p  do zapisu,  write(p, x) -  dołączenie x  na  końcu  pliku  p,  n
set(p) 
- przygotowanie pliku do odczytu, read(p,  x) - pobranie x z aktualnego
miejsca  w  pliku  p  oraz  close(p)  -  zamknięcie  pliku  p.  Funkcja  standardowa
eof(p) o wartościach logicznych służy do wykrywania sytuacji  końca pliku (jej
wartoś jest wtedy równa „true").

W implementacji operacji plikowych należy pamiętać o tym, że parametry typ

plikowego  zawsze  przekazuje  się  „przez  zmienną".  Dalsze  wiadomości  o  plikac
sekwencyjnych i o pozostałych operacjach standardowych można znaleźć w książ
kach [3, 7, rozdz.l]. Tutaj przedstawimy najpierw podstawowe operacje na plikac
liczb  całkowitych,  a  następnie  wykorzystanie  plików  w  operacjach  odsiewani
grafów.

Algorytm konstruowania pliku losowych liczb całkowitych

Dane:  n - długość pliku, z - zakres losowania.

Wynik: p - plik losowych liczb całkowitych z przedziału [l, z].

Metoda:

1.  Przygotuj plik do zapisu.
2.  Powtórz razy losowanie liczby całkowitej oraz zapisanie tej liczby w pliku f
3.  Zamknij utworzony plik p.
Złożoność: O(n).

Implementacja

type plik = file of integer;   procedure
konstr (n, z : integer; var p:plik) ; var i:
integer; x: integer!     begin

rewrite(p);

for i:=l to do

begin x:=random(z) + 1; write(p, x); end;

close(p); end; {

konstr }

background image

33

3.1.2. Wyprowadzanie pliku

Jeżeli chcemy wyprowadzić informacje zgromadzone w pliku roboczym do

pliku wynikowego, to musimy wykonać poniższą operację.

Algorytm wyprowadzania pliku

Dane: p - plik liczb całkowitych.
Wynik: zawartość pliku p wyprowadzona do pliku wynikowego.
Metoda:
1.  Przygotuj plik p do odczytu.
2.  Przeglądaj plik p do końca i każdy element przepisz do pliku wynikowego.
3.  Zamknij plik p.

Złożoność: O(n), gdzie jest długością pliku.

Implementacja

t y p e   p l i k   =     f i l e     o f     i n t e g e r ;

p r o c e d u r e   d r u k p l i k ( v a r   p : p l i k ;       v a r     ou t :t e x t ) ;
v a r   x : i n t e g e r ;
begi n

b e g i n   r e a d ( p ,      x ) ;      w r i t e ( o u t ,      x ) ;      e n d ;

c l o s e ( p ) ;   end;   {   drukplik   }

3.1.3.  Poszukiwanie informacji w pliku

Operacja member poszukiwania  informacji  w  pliku  jest  wykonywana  podobnie

jak w tablicy jednowymiarowej. Algorytm podany niżej można rozszerzyć o znale-
zienie wszystkich wystąpień danej informacji w pliku.

Algorytm poszukiwania informacji w pliku

Dane:  x  -  liczba  całkowita,  p  -  plik  liczb  całkowitych.
Wynik: Informacja o tym, czy x jest elementem pliku/?.
Metoda:
1.  Przygotuj plik do odczytu.
2.  Przeglądaj plik p i wartość każdego elementu porównuj z wartością x, dopóki

plik p się nie skończy lub nie zostanie znaleziony element x.

3.  Zamknij plik/?.
Złożoność: O(n), gdzie jest długością pliku.

background image

34

Implementacja

type plik = file of integer;
function member(x:integer; var p:plik):Boolean;

var i:integer; y:integer; jest:Boolean;

begin

reset(p);
jest:=false;

while not eof(p) and not jest do

begin

read(p, y) ;
if x = y then jest:=true;

end;

close(p);
member:=jest;
end; { member }

3.1.4.  Łączenie plików posortowanych

Przegląd  metod  sortowania  plików  przedstawiono  w  książce  [7,  rozdz.  2].

ograniczymy się do omówienia algorytmu merge łączenia plików posortowan;
w jeden plik posortowany. W implementacji tego algorytmu wykorzystano prc
dury standardowe get(p) put(p) używane do wymiany informacji między zmi
na buforowąp\ pliku p a tym plikiem (szczegóły w książkach [3, 7]).

Algorytm łączenia plików posortowanych

Dane: f, g - pliki liczb całkowitych posortowane niemalejąco.
Wynik: h - posortowany plik zawierający informację z plików/ g.
Metoda:
1.  Przygotuj pliki/ do odczytu, plik do zapisu.
2.  Przepisuj do pliku mniejszą liczbę z pliku/lub oraz pobieraj kolejną lic

z odpowiedniego pliku aż do końca jednego z plików/lub g.

3.  Przepisz pozostałą informację z drugiego pliku do pliku h.
4.  Zamknij pliki/ oraz h.
Złożoność: O(n), gdzie jest łączną długością plików/oraz g.

Implementacja

t y p e   p l i k   =     f i l e   o f     i n t e g e r ;

p r o c e d u r e   m e r g e ( v a r     f , g : p l i k ;       v a r   h :p l i k) ;
v a r   b : B o o l e a n ;

b e g i n

background image

35

reset (f); reset (g); rewrite (h) ;
b:=eof (f) or eof (g)

/

while not b do

begin

if f ^ < g^

then

begin h^ : =f ^; get(f); b:=eof(f);

else

,__

begin h^ :=g ^; get(g); b:eof(g);

put(h) ;

end;

while not eof (f) do begin h^:=f^; put(h)
while not eof (g) do begin h^:=g^; put(h)

   

3.2.   Pliki grafów. Przesiewanie

3.2.1.  Testowanie izomorfizmu

Metody sita lub tzw. przesiewanie są jedną z technik wyodrębniania obiektów o

potrzebnych własnościach ze  zbioru  wszystkich  generowanych  obiektów.  Znanym
przykładem jest algorytm poszukiwania liczb pierwszych metodą sita Eratostenesa.
Warunkiem wykorzystania tych metod do obiektów złożonych, jakimi są grafy,
jest dysponowanie metodą porównywania struktur i  oceny,  czy  dane  dwa  grafy
mają tę samą strukturę,  czyli czy są izomorficzne. Izomorfizm  jest relacją  między
dwoma  grafami.  Dwa  grafy  są  izomorficzne,  jeśli  istnieje  odwzorowanie  zbioru
wierzchołków pierwszego grafu w zbiór wierzchołków drugiego grafu, zachowują-
ce przyległość wierzchołków.

Wiadomo, że w ogólnym przypadku problem testowania izomorfizmu grafów

ma złożoność wykładniczą. Nie zostały określone proste warunki wystarczające
i konieczne do tego, aby dwa grafy były izomorficzne. Jednakże można wymienić
wiele niezmienników grafu (tzw. inwariantów), które można sprawdzić, zanim się
przystąpi do zasadniczego testowania. Są to przede wszystkim:  liczba wierzchoł-
ków,  liczba krawędzi, posortowany  ciąg stopni  wierzchołków,  liczba  składowych
spójności. Jeśli algorytm wyznaczenia wartości niezmiennika (np. liczba cykli
o długości  3)  ma złożoność większą  niż O(n\  to  na  ogół  wykorzystanie  takiego
niezmiennika nie jest celowe.

Istnieje wiele różnych metod rozwiązywania problemu  testowania  izomorfizmu

(por. rozdz. 11.7 w książce [2]). Jedną z nich jest wyznaczenie ciągu kanoniczne-
go, 
który jednoznacznie określa strukturę grafu (por. [4]). Jest to taki ciąg binarny
o długości n(n -  l)/2, odpowiadający wyborowi  krawędzi grafu (ułożonych w po-
rządku leksykograficznym), którego wartość liczbowa jest największa spośród

end

end;

get(f) end; get(g) end; close(f); close(g); close(h);
end; { merge }

background image

36

wartości wszystkich możliwych ciągów dla danej struktury. Na przykład, ciąg ka
noniczny  grafu  G  =  P

4

  (ścieżka  o  czterech  wierzchołkach)  ma  postać:  ll001i

czyli  odpowiada wyborowi  krawędzi:  (l,  2), (l,  3)  oraz  (2,  4).  Jeżeli  ciągi  kani
niczne grafów są równe, to grafy są izomorficzne.

Inną  metodą  jest  przeglądanie  grafów  (np.  metodą  DFS);  jeśli  dla  każdej

wierzchołka pierwszego grafu znajdziemy odpowiadający mu wierzchołek w dru
gim  grafie,  to  grafy  te  są  izomorficzne.  Oczywiście,  najpierw  trzeba  sprawdzić,
czy grafy  te  mają  jednakową  liczbę  wierzchołków  i  taki  sam  posortowany  ciąg
stopni  wierzchołków.  Tę  metodę  zastosowano  w  algorytmie  iso6  (funkcja
EqualGraph autor: Jędrzej Miądowicz Inf. I r., 1994). Jeżeli chcemy sprawdzić,
czy  grafy  G  L  są  izomorficzne,  to  umieszczamy  każdy  z  nich  w  strukturze
danych typu TGrap i wywołujemy funkcję EqualGraphs(n, G, H).

3.2.2.  Transformacja E —Gr reprezentacji grafu

Jeżeli grafy są generowane w tablicy krawędzi (np. gdy są to grafy  losów

G(n, k)), to aby można było wykorzystać algorytm iso 6 testowania izomorfizm
należy przekształcić reprezentację grafu z tablicy w rekord typu TGraph (zdef
niowany w  module  iso6). Rekord  ten  oprócz  reprezentacji  macierzy  przyległośi
grafu w polu (tablica kwadratowa o wartościach logicznych)  ma  jeszcze  dw
pola (tablice jednowymiarowe o wartościach całkowitych): Deg - do przechowy
wania ciągu stopni wierzchołków i DegSort - posortowanego ciągu stopni.

Implementacja tego przekształcenia składa się z dwóch operacji:

1.  przepisania krawędzi grafu z E do pola Gr oraz.
2.  wyznaczenia wartości pól Deg DegSort w Gr z informacji w polu A.
Takie rozwiązanie jest bardziej elastyczne, jeśli uwzględnimy fakt, że grafy k

sowę mogą być generowane w różnych strukturach i wtedy używamy innej proce
dury tylko do przeniesienia informacji o krawędziach grafu do pola rekordu.

Algorytm transformacji E —Gr reprezentacji grafu

Dane:  n - liczba wierzchołków, k - liczba krawędzi, E - reprezentacja grafu G.

Wynik: Gr - reprezentacja grafu w rekordzie typu TGraph.
Metoda:

1.  Przepisz informację o krawędziach grafu G z tablicy do pola rekordu Gr.
2.  
Oblicz ciąg stopni wierzchołków grafu G i zapisz go w polach Deg DegSort.
3.  Posortuj ciąg stopni malejąco (informację zawartą w polu DegSort).
Złożoność: O(n

2

).

Implementacja
Struktury danych przedstawiono na rys. 3.1.

background image

Rys. 3.1. Struktury danych w implementacji algorytmu transformacji  -> Gr reprezentacji

grafu G = G(n, k)

const nmax = 100; kmax = nmax*(nmax-1) div 2;
type ind = l..nmax; indl = l..kmax;

r = record a,b:integer end;

{ Gnk }

tl = array[indl] of r;

{ E }

t2 = array [ind,ind] of Boolean;

{ A }

t3 = array [ind] of byte;

TGraph = record A:t2; Deg,DegSort:t3; end;

procedure transEGr(n,k,total:integer; var E:tl;

var Gr:TGraph);

var i, j,h:integer; begin

with Gr do begin

for i:=l to n do for j:=l to n do A[i,j]:=false;
for h:=total downto total - k + 1 do begin

with E[h] do begin i:=a; j:=b; end;
A[i,j]:=true; A[ j,i] :=true; end; end; end; {
transEGr }

37

background image

38

procedure jeden(n:integer; var Gr:TGraph);

var i,j,s:integer;

procedure sort(n:integer; var a:t3);

var i,j,k,naj:integer;

begin

for i:=1 to n - 1 do

begin

naj:=a[i]; k:=i; for

j : =i + 1 to n do

if a[j] naj then begin naj:=a[j]; k:=j;end;

a[k]:=a[i]; a[i]:=naj; end;

end; { sort }

begin { jeden }

with Gr do begin

for i:=l to n do

begin s: =0 ;

f o r    j: = l    t o   n   d o    i f  A [ i,j]    t h e n     s   : =     s    +      1 ;
D e g [ i ] : = s ;       D e g S o r t [ i ] : = s ;   e n d ;

s o r t ( n ,       D e g S o r t ) ;   e nd;

en d;    {   j ede n   }

3.2.3.   Konstruowanie pliku grafów samodopełniających

Każdemu grafowi G o wierzchołkach i krawędziach odpowiada graf G

c

n;

zywany dopełnieniem (ang. complement) grafu G, zawierający  te  krawędzie gra:
pełnego, które nie wystąpiły w grafie G. Graf nazywamy samodopełniającym lv
krótko  sc  grafem  (ang.  self-complementary),  jeżeli  jest  izomorficzny  ze  swoi
dopełnieniem.  Liczba  krawędzi  takiego  grafu  jest  równa  n(n  -  l)/4.  Wynika  sfc
warunek  na  liczbę  wierzchołków  n,  który  musi  być  spełniony,  aby  można  by
skonstruować  sc  graf:  reszta  z  dzielenia  liczby  wierzchołków  przez  4  musi  b]
równa O lub l, czyli = s O, l (mod 4).

Trzy najmniejsze sc grafy przedstawiono na rysunku 3.2. W celu skonstruowa

nią wszystkich 10 grafów tej klasy o 8 wierzchołkach lub wszystkich 36 grafów o
wierzchołkach  wystarczy  powtórzyć  odpowiednią  liczbę  razy  następujące
operacji

1.  generację grafu losowego G = G(n, k),
2.  
wyznaczenie G

c

,

3.  sprawdzenie, czy grafy G i G

c

 są izomorficzne, i jeśli tak, to zapamiętan

jednego z nich w pliku sc grafów.

background image

39

Niektóre struktury mogą wielokrotnie wystąpić w tym  pliku.  Następną  operacją

może być sprawdzenie:

1.  ile różnych struktur zostało wygenerowanych,
2.  czy uzyskano wszystkie sc grafy o wierzchołkach, oraz
3.  jaka była liczba wystąpień każdej struktury podczas generacji.
Istnieje również dokładna metoda (G. Ringel, H. Sachs) konstruowania sc  gra-

fów,  w  której  wykorzystuje  się  kolorowanie  krawędzi  grafu  pełnego  K

n

  dwoma

kolorami. W wyniku implementacji tej metody wygenerowano (por. [1]) wszystkie
sc grafy o 12 i 13 wierzchołkach (odpowiednio 720 oraz 5600 grafów).

Rys. 3.2. Trzy najmniejsze sc grafy, = 4 oraz = 5

Algorytm konstruowania pliku sc grafów

Dane: n - liczba wierzchołków, n = O, l (mod 4), rep - rozmiar próby.
Wynik: W- plik wygenerowanych sc grafów, g - długość pliku W.
Metoda:
1.  Wyznacz liczbę krawędzi sc grafu n*(n - 1) div 4.
2.  Przygotuj plik do zapisu, tablicę krawędzi oraz licznik grafów.
3.  Wykonaj rep razy następujące operacje:

a. wygeneruj graf losowy Gl = G(n, k),
b. przepisz G l do rekordu typu TGraph,
c. wyznacz G2 - dopełnienie grafu Gl w rekordzie typu TGraph,
d. jeśli Gl i G2 są izomorficzne, to dopisz Gl do pliku oraz powiększ o je

den wartość licznika grafów.

4. Zamknij plik W.
Złożoność: zależy od złożoności testowania izomorfizmu grafów.

Implementacja

const nmax  =   100;   kmax  =  nmax* (nmax-1)   div  2 ;
t yp e    i n d     =      l. . n ma x;      i n d 1     =      1. . k ma x;

r     =     r e c o r d   a , b : i n t e g e r     e n d ;
t l   =     a r r a y       [ i n d l ]       o f     r ;

{   E   }

t 2    =   arr a y[ i n d, i n d ]     of    B o olea n;          {    A    }
1 3     =    a r r a y [ i n d ]     of  b yt e;

{   Deg,   DegS ort   }

T G r a p h    =  r e c or d  A :t 2;    D e g, D e g S or t :t 3  e n d;
p l i k    =     f i l e    o f     T G r a p h ;

background image

40

procedure complement(n:integer; var Gl, G2:TGraph);
var i,j:integer;     { G2 jest dope

łnieniem grafu Gl }

begin

G2:=G1; with
G2 do

for i:=l to n - 1 do

for j:=i + 1 to n do
begin

A[i,j]:=not A[i,j]; A[j,i]:=not A[j,i] end;

end; { complement }

procedure konstr(n,k,rep:integer; var W:plik;

var g:integer);

var i,h,total:integer; E:tl; Gl,G2rTGraph;
begin

rewrite(W); init(n,
E, total);
i:=0;

{ licznik wygenerowanych sc grafów }

for h:=l to rep do

begin

Gnk(n, k, total, E);
transEGr(n, k, total, E, Gl) ;

jeden(n, Gl);

complement(n, Gl, G2);
jeden(n, G2) ;
if EqualGraphs(n, Gl, G2)then

begin i:=i + 1; write(W, G1); end; end;

g:=i; close(W); end; { konstr }

3.2.4.   Przesiewanie

Jeżeli po uzyskaniu pliku grafów chcemy utworzyć plik zawierający tylko różi

grafy, tzn. parami nieizomorficzne, to wykonujemy operację przesiewania.

Algorytm przesiewania

Dane: n - liczba wierzchołków, W- plik grafów, g - długość pliku W.

Wynik: Z - plik różnych grafów, sc - długość pliku Z.

Metoda:

l.    Przepisz pierwszy graf z pliku do pliku Z, ustaw licznik elementów pliku Z

background image

41

2.  Pobieraj kolejne grafy z pliku i każdy z nich porównuj ze wszystkimi gra

fami w pliku (operacja member z testowaniem izomorfizmu grafów).

3.  Jeśli nie ma takiego grafu w pliku Z, to dopisz ten graf do Z i powiększ o l

licznik elementów pliku Z.

Złożoność: Zależy od długości pliku W oraz od złożoności testowania izomorfizmu
grafów.

Implementacja

t y p e   p l i k   =   f i l e     o f   T G r a p h ;

p r o c e d u r e   p r z e s i e w a n i e ( n , g : i n t e g e r ;       v a r  W , Z : p l i k ;

v a r       s c : i n t e g e r ) ;

var i,j,h:integer; jest:Boolean; Gl, G2:TGraph;
begin

reset(W);
rewrite(Z) ;
read(W, Gl);

{ pierwszy graf z W do Gl }

'write (Z, Gl) ;

{ przepisz Gl do pliku Z }

close(Z) ;
j:=l;

{ aktualna d

ługość pliku Z }

for i:=2 to g do

{ przegl

ądaj plik W }

begin

read(W, Gl);   { pobierz kolejny graf z W do Gl }
reset(Z);         { przygotuj plik Z do odczytu }
{ porównaj Gl z kolejnymi G2 z pliku Z: }
jest:=false; h:=l;

while (h <= j) and not jest do

begin

read(Z, G2);
if EqualGraphs(n, Gl, G2) then jest:=true

else h:=h + 1;

end;

if not jest then { gdy Gl nie ma w Z, to dopisz }

begin
write (Z, Gl);
j:=j + 1; close
(Z); end;

end; { i }

sc:=j;

{liczba ro

żnych sc grafów w pliku Z }

close(W);

{ zamkniecie pliku W }

end; { przesiewanie }

background image

42

3.2.5.  Generacja grafu losowego G(n,f)

W projektowaniu inżynierskim mamy często do czynienia z grafami, w których

liczba  sąsiadów  każdego  wierzchołka  jest  ograniczona  z  góry  przez  wartość  par
metru f Nazywamy je grafami z ograniczonym stopniem wierzchołków lub krótl
f-grafami.  W  tej  klasie  grafów  wyróżnia  się  maksymalne  f-grafy  (ze  względu  i
liczbę  krawędzi).  Można  je  podzielić  według  wartości  m  -  liczby  wierzchołkó
nienasyconych  (tzn.  o  stopniu  mniejszym  niż  f).  Na  rysunku  3.3  zamieszczeń
wszystkie  maksymalne  2-grafy  o  liczbie  wierzchołków  n  =  4,  5  z  m
wierzchołkami nienasyconymi (m = O, l, 2).

m=0

m= l

m=2

Rys. 3.3. Przykłady maksymalnych/-grafów (f= 2, n =4, 5) z m wierzchołkami nienasyconymi

(w = 0,1,2)

Praktyczne znaczenie mają/grafy, w których 2 </< - l.  Zwykle  generujem

losowe/-grafy, wykorzystując model sekwencyjny G(n,f), w którym maksymalny
f-graf  powstaje  stopniowo  z  grafu  pustego  o  n  wierzchołkach  przez  losowanie
kollejnych krawędzi i dodawanie ich do grafu. Muszą być przy tym spełnione
następujące warunki:

1.  każda krawędź jest losowana z jednakowym prawdopodobieństwem ze

zbioru
wszystkich dostępnych krawędzi,

2.  wylosowaną krawędź można dodać do grafu, jeżeli jej wierzchołki mają

stopień mniejszy niż f.

Pierwszy  z  powyższych  warunków  jest  taki  sam  jak  w  modelu  G(n,  k).  Kc

nieczność spełnienia drugiego warunku sprawia, że nie  każdą  wylosowaną  krawęd
można dodać do grafu. Musimy  więc zaznaczyć, które krawędzie  należą do grafi
W implementacji algorytmu generacji grafu G = G(n,J) wykorzystuje się tablicę.
rekordów  o  trzech  polach,  z  których  dwa  przechowują  krawędź,  a  trzecie  (poi
code typu logicznego) służy do odróżniania krawędzi. Na początku generacji graf
wszystkie krawędzie są oznaczone jako „false". Dodanie  krawędzi do grafu wiąż
się  z  podstawieniem  wartości  „true"  do  pola  code.  Po  zakończeniu  generacji  kra
wędzie maksymalnego f-grafu są rozproszone w całej tablicy E.

background image

43

Algorytm generacji grafu losowego G = G(n, f)

Dane: n - liczba wierzchołków, /- maksymalny stopień wierzchołka, E - tablica
zawierająca wszystkie krawędzie na wierzchołkach.
Wynik: E - reprezentacja grafu G = G(n, f) w tablicy rekordów.
Metoda: Niech będzie indeksem końca roboczej części tablicy E.
1.  Przygotuj tablicę do losowania krawędzi grafu G.
2.  Przygotuj tablicę c, w której przechowuje się ciąg stopni wierzchołków grafu.
3.  Podstaw/z =n(n-l)/2.
4.  Powtarzaj następujące operacje, dopóki jest dodatnie:

a. losuj z - indeks elementu z przedziału [l, h],
b. jeśli wylosowana  krawędź E[z] może być dodana do grafu G, czyli jeśli

stopnie jej wierzchołków są mniejsze niż f to oznacz ją jako dobrą i zaktu
alizuj c,

c. zamień miejscami elementy E[z] E[h] tablicy E oraz zmniejsz h o 1.

Złożoność: O(n

2

).

Implementacja

background image

44

const nmax = 100; kmax = nmax*(nmax-1) div 2;
type ind = 1..nmax; ind1 = 1..kmax;

rE = record a,b:byte; code:Boolean end;
t = array[ind] of integer;

{ c }

tE = array[ind1] of rE;

{ E }

procedure Gnf(n,f,total:integer; var E:tE; var c:t;

var k:integer);

var i,h,y,z:integer; x:r;  begin

for i:=l to n do c[i]:=0;
for i:=l to total do E[i].code:=false;
y:=0;

{ licznik kraw

ędzi }

for h:=total downto 1 do

begin

z:=random(h) + 1;

{ losowanie indeksu )

with E[z] do

{ sprawdzenie )

if (c[a] < f) and (c[b] < f) then

begin

code:=true; y:=y
+ 1; c[a]:=c[a] +
1; c[b]:=c[b] +
1; end;

x:=E[z]; E[z]:=E[h]; E[h]:=x;         { zamiana end;

k:=y; end; { Gnf }

Grafy, w których wszystkie wierzchołki są nasycone, czyli są stopnia f nazy-
wamy regularnymi. 
Liczba ich krawędzi jest równa nf/2. Jest to ważna klasa
grafów Do generacji losowych grafów f-regularnych o 
wierzchołkach
wystarczy wykorzystać algorytm generacji maksymalnego f-grafu losowego
G = G(n, 
i odsiewać grafy o liczbie krawędzi k = n f/2.

background image

4.   STRUKTURY LISTOWE

4.1.   Lista jednokierunkowa

4.1.1.   Dopisywanie informacji na początku listy

Lista jest dynamiczną strukturą danych składającą się z  elementów tego samego

typu,  podobnie  jak  tablica  i  plik,  ale  jest  tworzona  i  usuwana  z  pamięci  podczas
wykonywania  programu.  Inną  cechą  różniącą  listę  od  tamtych  struktur  jest  możli-
wość  dopisywania  do  niej  nowych  elementów  w  dowolnie  wybranym  miejscu,  bez
potrzeby  przepisywania  informacji.  Każdy  z  elementów  listy  jednokierunkowej
zawiera nie tylko informację  do  przetwarzania,  ale  również  wskazuje  miejsce,
w  którym  znajduje  się  kolejna  informacja.  Aby  mieć  dostęp  do  listy,  wystarczy
znać  wskaźnik  jej  początku,  czyli  głowę  listy.  Najprostszym  algorytmem  dopisy-
wania do listy jest algorytm  dopisywania  nowego  elementu  na początku poprzed-
nio utworzonej listy.

W  implementacji  tego  algorytmu  została  użyta  standardowa  procedura  języka

Pascal,  new(p),  która  przydziela  nowy  obszar  w  pamięci  rekordowi  reprezentują-
cemu element listy, przy czym zmienna wskaźnikowa ma wartość adresu począt-
ku  tego  obszaru  w  pamięci.  Zmienna  wskazywana  przez  zmienną  wskaźnikową  p
nazywa się/? t (por. [3]).

Algorytm dopisywania informacji na początku listy

Dane: x - informacja (np. liczba całkowita), h - głowa listy. Wynik: h - głowa
listy rozszerzonej o jeden element na początku. Metoda: Wygeneruj nowy
element, wpisz do niego r, przyłącz element do początku listy oraz zaktualizuj h.
Złożoność: 0(1).

Implementacja

type ref = ^elem;

elem = record a:integer; next:ref; end;

procedure push(x:integer; var h:ref);

var p:ref;

begin

new(p);
with p^ do begin a:=x; next:=^h; end;

h:=p end; {

push }

background image

46

4.1.2.   Konstruowanie listy z dopisywaniem na początku

Do sprawdzania poprawności  zaprojektowanych  algorytmów  działających  na  li-

stach  jednokierunkowych  wykorzystuje  się  algorytm  konstruowania  listy  elemen-
tów  o  wartościach  losowych.  Postąpimy  tu  podobnie  jak  w  przypadku  poprzednio
omawianych struktur danych i utworzymy  listę liczb całkowitych. Metoda  konstru-
owania  listy  jest  jednak  ogólna  i  stosujemy  ją  do  elementów  dowolnego  typu,  np.
grafów  losowych.  Proszę  zwrócić  uwagę,  że  algorytm  konstruowania  listy  z  dopi-
sywaniem  na  początku  odwraca  porządek  wygenerowanego  ciągu,  czyli  pierwszy
element ciągu znajduje się na końcu listy.

Algorytm konstruowania listy z dopisywaniem na początku

Dane: n - długość listy, z - zakres losowania.

Wynik: h - głowa listy.

Metoda: Przygotuj listę, a następnie powtórz razy losowanie liczby z przedziału
[l, z] i dopisanie jej na początku listy.

Złożoność: O(n).

Implementacja

a    next

Rys. 4.1. Lista jednokierunkowa przechowująca liczby całkowite

type ref = ^elem;

elem - record a:integer; next:ref; end;

procedure konstrlista(n,z:integer; var h:ref);
var i: integer; x :integer; begin

h:=nil;

{ przygotuj list

ę }

for i:=l to n do

begin

x:=random{z) + 1;

{ losuj liczb

ę }

push(x, h);

{ dopisz do pocz

ątku listy }

end; end; {

konstrlista }

background image

47

4.1.3.   Wyprowadzanie informacji z listy

Do  przeglądania  listy  nie  jest  potrzebna  informacja  o  jej  długości.  Wystarczy

rozpocząć  przeglądanie  od  pierwszego  elementu  i  przesuwać  wskaźnik  na  kolejny
element  dopóki  będzie  on  miał  wartość  różną  od  wskaźnika  pustego  (nil).  W  im-
plementacji  poniższego  algorytmu  wykorzystano  licznik  wyprowadzonych  ele-
mentów listy, aby drukować po 10 elementów w jednej linii wyników.

Algorytm wyprowadzania informacji z listy

Dane: h - głowa listy.

Wynik: Informacja z listy przepisana do pliku wyjściowego out.
Metoda: Przeglądaj listę i przepisuj do pliku wyjściowego informację z kolejnych

elementów listy.
Uwaga: obsługę pliku out zaprojektowano poza tym algorytmem.
Złożoność: O(ń), gdzie jest długością listy.

Implementacja

type ref = ^elem;

elem = record a:integer; next:ref; end;

procedure druklista(h:ref); var i:integer;
begin

writeln(out, 'lista:');

i:=0;        { licznik wydrukowanych elementów listy }
while h O nil do begin

with h^ do

begin

write (out, a: 6); i:=i -f 1; if mod
10 = 0 then writeln{out); end;

h:=h^.next;

end;

writeln(out); end;

{ druklista }

4.1.4.   Poszukiwanie informacji na liście

Operacja member jest analogiczna do tych, które projektowaliśmy dla tablic

i plików, różni się tylko sposobem przeglądania struktury danych.

background image

48

Algorytm poszukiwania informacji na liście jednokierunkowej

Dane:x- poszukiwana informacja (np. liczba całkowita), -głowa listy.
Wynik: Informacja o tym, czy x jest na liście h.
Metoda:   Przeszukujemy  listę,  porównując  informację  w  kolejnym  elemencie
z wartością x. Operacja kończy się, gdy znajdziemy albo gdy sprawdzimy ostatni
element listy.

Złożoność: O(n), gdzie n jest długością listy.

Implementacja

type ref = ^elem;

elem = record a:integer; next:ref; end;

function member(x:integer; h:ref):Boolean;
var jest:Boolean; begin

jest:^false;

while (h <> nil) and not jest do

if h^.a = x then jest:=true else h:=h^.next;

member:=jest;

end; { member }

4.1.5.   Sortowanie listy z zamianą informacji

Wykorzystamy  tę  samą  metodę  sortowania,  którą  zastosowaliśmy  do  sortowa-

nia tablic. Jest ona łatwa do zrealizowania, ma jednak tę wadę, że wymaga zamiany
informacji.  W  przypadku  tablic  jest  to  postępowanie  naturalne.  Aby  zmienić  po-
rządek  występowania  elementów  na liście,  można  zmienić  odpowiednie  wskaźniki.
Zachęcamy  do  zaprojektowania  algorytmu  sortowania  listy  z  zamianą  wskaźni-
ków.  
Warto  porównać  średnią  złożoność  czasową  tych  dwóch  algorytmów
i  sprawdzić,  jaki  powinien  być  rozmiar  pola  informacyjnego  elementu  listy,  aby
było opłacalne stosowanie algorytmu sortowania z zamianą wskaźników.

Algorytm sortowania listy z zamianą informacji

Dane: h - głowa listy.

Wynik: Posortowana lista o głowie h.

Metoda: Niech będzie początkiem nie posortowanej części listy. Na początku
sortowania jest to głowa listy. Wykonuj podane niżej operacje, dopóki nie posor-

towana część listy nie jest pusta.

1.  Wybierz najlepszy element w nie posortowanej części listy (np. największy).
2.  Zamień miejscami tę informację z informacją znajdującą się w elemencie

wskazywanym przez i przesuń wskaźnik na następny element listy.

Złożoność: O(n

2

).

background image

49

Implementacja

type ref = ^elem;

elem = record a:integer; next:ref;  end;

procedure sortlista(h:ref);    { z zamiana informacji }
var p,r;ref; naj:integer; begin

while h <> nil do

begin

naj:=h^.a; r:=h;

{ malej

ąco }

p:=h^.next;      { od nast

ępnego do końca listy }

while p O nil do begin

if p^.a naj then

begin naj:=p^.a; r:=p; end;

p:=p^.next;
end; { p }
if r <> h then

{ zamiana }

begin r^.a:=h^.a;  h^.a:=naj end;

h:=h^.next; { skroc nie posortowana cze

ść listy } end;

{ h } end; { sortlista }

4.1.6.   Usuwanie pierwszego elementu listy

Jeśli usuwamy  informacje  z  listy,  to  musimy  zwolnić  przydzieloną  (za  pomocą

operacji new(p}} pamięć. Służy  do tego standardowa  operacja  dispose(p).  Dobrym
zwyczajem  jest  podstawianie  pustego  wskaźnika  nil  do  pola  wskaźnikowego  ele-
mentu usuwanego z listy przed użyciem operacji dispose.

Algorytm usuwania pierwszego elementu listy

Dane: h - głowa listy.

Wynik: x - informacja z pierwszego elementu danej listy, - głowa skróconej
listy.

Metoda:

1.  Jeżeli lista nie jest pusta, to wyprowadź informację z pierwszego elementu.
2.  Zapamiętaj początek danej listy i ustaw nowy początek listy.
3.  Zwolnij obszar pamięci zajęty przez pierwszy element danej listy.
Złożoność: O(l).

background image

50 Implementacja

type ref = ^elem;

elem = record aiinteger; next:ref; end;

procedure pop(var h:ref; var x:integer) ; var
p:ref; begin

if h <> nil then

begin

x:=h^.a; p:
=h ;

h : = h ^ . n e x t ;
p ^ . n e x t : = n i l ;
d i s p o s e ( p ) ;   e n d ;  end;
{   po p   }

4.1.7.   Dopisywanie elementu na końcu listy

Jeżeli chcemy skonstruować listę zawierającą elementy danego ciągu informacji

w  nie  zmienionym porządku, to  wykorzystujemy  algorytm  dopisywania  elementu
na końcu listy, tradycyjnie nazywany into. W tym przypadku nic wystarczy wskaź-
nik  początku  listy,  ale  potrzebny  jest  również  wskaźnik  aktualnego  końca  listy
(ang. sentinel).

Algorytm dopisywania elementu na końcu listy

Dane: x - informacja (np. liczba całkowita), h - głowa listy, s - koniec listy.

Wynik: h, s - głowa i koniec przedłużonej listy.
Metoda:

1.  Wygeneruj nowy element, zapisz w nim informację x.
2.  
Jeśli lista nie była pusta, przyłącz nowy element do końca listy; w przeciwnym

przypadku element ten będzie pierwszym elementem listy.

3.  Zaktualizuj s.

Złożoność: O(n), gdzie jest długością listy.

Implementacja

type ref = ^elem;

elem = record a:integer; next:ref; end;

procedure into(x:integer; var h, s:ref); var
p:ref; begin

new(p);

background image

w i t h   p- ^    d o  b e g i n    a: = x ;     n e xt : = nil;   e n d ; if     h
< >       n i l     t h e n       
s ^ . n e x t ; = p      e l s e      h : = p ;   s:  = p ; e n d ;
{   int o   }

4.1.8.   Wstawianie informacji na listę posortowaną rosnąco

Jeżeli chcemy połączyć operację tworzenia i sortowania listy, to możemy wyko-

rzystać  podany  niżej  algorytm.  W  implementacji  tego  algorytmu  zastosowano  pro-
cedurę rekurencyjną.

Algorytm wstawiania informacji na listę posortowaną rosnąco

Dane: informacja, h - głowa listy posortowanej rosnąco.

Wynik: - głowa listy posortowanej rosnąco, zawierającej nowy element.

Metoda: Jeśli lista jest pusta lub wartość jest mniejsza od wartości zapisanej

w aktualnym elemencie listy, to wykonaj operację push', w przeciwnym przypadku
wykonaj operację insert dla kolejnego elementu listy.
Złożoność: O(n), gdzie jest długością listy.

Implementacja

t y p e  r ef   =   ^ el e m;

e l e m     =     r e c o r d     a : i n t e g e r ;       n e x t : r e f ;       e nd ;

p r o c e d u r e       i n s e r t (

K

: i n t e g e r ;       v a r     h : r e f) ;

b e g i n

if    ( h    =    nil)    o r     ( x    <    h ^ . a )    t h e n  p u s h (x ,     h )

else insert(x, h^.next)

end; { insert }

4.1.9.   Konstruowanie listy grafów

Przedstawiamy  algorytm  tworzenia  listy  grafów,  gdy  na  liście  są  umieszczane

wszystkie wygenerowane grafy losowe G(n, f) o  maksymalnej  liczbie  krawędzi
i dodatkowo zbierana jest informacja  o liczbie  wystąpień  grafów z wierzchołka-
mi  nienasyconymi.  Struktura  danych,  w  której  przechowuje  się  grafy,  może  być
wybrana dowolnie, ale tu przewidziano dalsze rozszerzenia przetwarzania listy, np.
o odsiewanie grafów, i z tego względu zastosowano strukturę, w której działa algo-
rytm  testowania  izomorfizmu  grafów.  Wykorzystano,  znane  z  poprzednich  roz-
działów, operacje tworzenia tablicy E, generacji grafu losowego G(n, J) oraz trans-
formacji E —Gr reprezentacji grafu.

background image

52

Algorytm konstruowania listy grafów losowych G(n,f)

Dane: n - liczba wierzchołków,/- maksymalny stopień, rep - rozmiar próbki.
Wynik: h - głowa listy grafów, Q

m

 - licznik grafów z wierzchołkami nienasy-

conymi (m = O, l, ...,f). Metoda:
1.  Przygotuj licznik Q.
2.  
Przygotuj głowę listy grafów.
3.  Utwórz tablicę krawędzi E.
4.  Powtórz rep razy następujące operacje:

a.

wygeneruj graf losowy G = G(n, f),

b.

oblicz m - liczbę wierzchołków nienasyconych w G,

c.

zwiększ Q

m

o1,

d.

przepisz graf G z tablicy do rekordu typu TGraph,

e.

dopisz graf do początku listy.

Złożoność: O(n

2

).

Implementacja

const  nmax   =   13;   kmax   =   nmax*(n max   -   1)   di v   2;
t y p e   i n d   =    1. n m a x ;     i n dl    =    L. k m a x ;

r = record a,b:byte; code:Boolean  end;
t = array[ind] of integer;

{ c }

t1 = array[indl] of r;

{ E }

tQ = array[0..nmax] of integer;

{ Q }

refl = ^eleml;
elem1 = record G:TGraph; next:refl;  end;

procedure pushl(x:TGraph; var h:refl);
var p:ref1;
begin

new(p);

with p^ do begin G:=x; next:=h; end;
h:=p

end; { pushl }

procedure konstrflista(n,f,rep:integer; var h:ref1;

var Q:tQ);

var i,j,m,total:integer; E:tl; Gl:TGraph;
begin

for m:=0 to f do Q[m]:=0;         { przygotowanie Q }
h:=nil;

{ przygotowanie listy }

init(n, E, total);

{ utworzenie tablicy E }

for i:=1 to rep do

begin

background image

53

Gnf(n, f, total, E, c, k);      { generacja Gnf }

m:=0;

for j:=1 to do

if c[j] < f then m:=m + 1;

Q[m]:=Q[m] + 1;

{ aktualizacja Q }

transEGr(n, total, E, Gl);

{ A w G1 }

jeden(n, G1);

{ Deg i DegSort w G1 }

pushl(Gl, h);

{ Gl na pocz

ątek listy }

end; { i } end; {

konstrflista }

4.2.   Tablica list

4.2.1.  Konstruowanie tablicy list identyfikatorów

Tablica list i lista list są przykładami realizacji magazynu informacji o struktu-

rze skorowidza. Dostęp do  informacji  jest tu  łatwiejszy niż w  przypadku  zwykłej
tablicy, pliku lub listy jednopoziomowej, ponieważ informacja  jest sklasyfikowana
zgodnie z pewnym kryterium i umieszczona na oddzielnych listach.

Rozpatrzmy na początek tworzenie skorowidza w postaci tablicy  list identyfi-

katorów. Niech identyfikatorem będzie ciąg liter i cyfr rozpoczynający się od litery
(zgodnie z gramatyką  języka Pascal). Każda  lista  przechowuje  identyfikatory  roz-
poczynające się od tej samej litery.

W  implementacji  algorytmu  zastosowano  tablicę  indeksowaną  literami,  która

przechowuje początki odpowiednich list. Założono, że identyfikatory znajdują się
w pliku input, każdy w oddzielnym wierszu.

Algorytm konstruowania tablicy list identyfikatorów

Dane: n - liczba identyfikatorów c-znakowych oraz ciąg identyfikatorów. Wynik:
B - tablica list identyfikatorów rozpoczynających się od tej samej litery. Metoda:
1.  Przygotuj tablicę B, w której przechowuje się początki list.
2.  Pobierz kolejny identyfikator x.
3.  Sprawdź, czy x znajduje się na liście identyfikatorów zaczynających się od tej

samej litery.

4. Jeśli nie, to dopisz do tej listy (np. na początku listy).
Złożoność: O(n)

background image

Rys. 4.2. Przykład skorowidza jako tablicy list identyfikatorów

const cmax =15;        { max d

ługość identyfikatora }

type t = array[l..cmax] of char;

ref = ^elem;
elem = record a:t; next:ref end;

tB = array['a'..'z'] of ref;

{ B }

procedure czyt(c:integer; var x:t);  { identyfikator }
var i: integer; begin

for i:=l to c do read(x[i]); readln;

end; { czyt }

function member(x:t; h:ref):Boolean;

{ ... }

end;{ member }

procedure push(x:t; var h:ref);

{ ... }

end; { push }

54

Implementacja

background image

55

procedure konstrB (n:integer; var B:tB); var
ch:char; i:integer; p:ref; x:t;  begin

for ch:='a' to 'z' do B[ch]:=nil;
for i:=1 to do begin

czyt(c, x);
p:=B[x[l]] ;

if not member(x, p) then push(x, p); end; {

i } end; { konstrB }

4.2.2.   Konstruowanie tablicy list grafów

W  podobny  sposób  można  skonstruować  skorowidz  zawierający  grafy  losowe,

np. G(n, p), podzielone na klasy według liczby krawędzi i przesiane ze względu na
izomorfizm.  Tablica  B  jest  indeksowana  liczbami  całkowitymi  o  wartościach  od-
powiadających liczbie krawędzi grafów G(n,p), czyli od O do n(n - l)/2.

Algorytm konstruowania tablicy list grafów

Dane: n - liczba wierzchołków, p - prawdopodobieństwo krawędziowe, rep -
rozmiar próbki.
Wynik: B - tablica list grafów z krawędziami, k = O, l,..., n(n - l)/2.
Metoda:

1.  Przygotuj tablicę B.
2.  
Powtórz rep razy następujące operacje:

a. wygeneruj graf G = G(n, p) i oblicz k - liczbę krawędzi grafu,
b. przepisz graf G z tablicy do rekordu typu TGraph,
c. 
jeżeli G nie jest elementem listy o głowie B[k], to dopisz G do tej listy.

Złożoność: Zależy od i złożoności algorytmu testowania izomorfizmu grafów.

Implementacja

const nmax = 100; kmax = nmax*(nmax-1) div 2;
type ind = l..nmax;

indl=l..kmax;

t2 = array[ind,ind] of Boolean;

{ A }

t3 = array[ind] of byte;

{ Deg, DegSort }

TGraph = record A:t2; Deg,DegSort:t3 end;
ref = ^elem;
elem = record Gr:TGraph; next:ref; end;
tB = array[indl] of ref;

{ B }

background image

56

procedure Gnp(n:integer;p:real;var A:t2;var krinteger);

{ -. . } end;
{ Gnp }

procedure transAGr(n:integer; var A:t2; var Gr:Tgraph);

{ ... }

end; {transAGr }

procedure jeden(n:integer; var Gr:TGraph);
{ ... } end; {jeden }

function memberGr(n:integer; x:TGraph; h:ref):Boolean;

var jest:Boolean;
begin

jest:=false;

while (h <> nil) and not jest do

if EqualGraphs(n, x, h

A

.Gr) {funkcja z modu

łu iso }

then jest:=true else h:=h^.next; member:=jest; end; {
memberGr }

procedure push(xrTGraph; var h:ref);
var p:ref;

begin

new(p); with p^ do begin Gr:=x; next:=h; end;
h: =p; end; {

push }

procedure konstrB(n,rep:integer; p:real; var B:tB);
var i,k:integer; A:t2; Gl:TGraph;
begin

for i:=0 to n*(n - 1) div 2 do B[i]:=nil;
for i:=l to rep do

begin

Gnp (n, p, A, k) ;
transAGr(n, A, Gl);
jeden(n, Gl);

if not member (n, Gl, B[k]) then push(Gl, B[k])

end; { i } end; { konstrB }

background image

57

4.3. Lista wielopoziomowa

4.3.1.   Konstruowanie listy list identyfikatorów

Skorowidz  zrealizowany jako  lista list zawiera wskaźniki  początków  tylko  nie-

pustych list, zapamiętane na odrębnej  liście. Rozważmy  operację tworzenia takiego
skorowidza identyfikatorów.

Algorytm konstruowania listy list identyfikatorów

Dane: Plik identyfikatorów c-znakowych.

Wynik: Lista list zawierających identyfikatory rozpoczynające się od tej samej
litery.

Metoda: Niech oznacza początek listy pierwszych liter identyfikatorów.
1.  Przygotuj H.
2.  
Przeglądając plik z danymi, wykonuj następujące operacje:

a.

pobierz x - kolejny identyfikator,

b.

sprawdź, gdzie na liście znajduje się litera równa pierwszej literze w x,

c.

jeśli tej litery nie ma na liście H, to dopisz ją do tej listy, utwórz, pierwszy
element odpowiadającej jej listy identyfikatorów i umieść w nim x;

d.

w przeciwnym przypadku sprawdź, czy x znajduje się na wskazanej liście
identyfikatorów; jeśli go nie ma, to dopisz x do tej listy.

Złożoność: O(n), gdzie jest długością pliku.

Implementacja

const cmax = 1 5 ;          { max d

ługość identyfikatora }

type ind = l..cmax;

t = array[ind] of char;

{ identyfikator }

ref = ^elem;
elem = record a:t; next:ref; end; { lista ident. }
refp = ^r;
r = record

{ element listy pionowej }

a:char; next:ref; nextp:refp; end; procedure push(x:t; var
h:ref);  { dla identyfikatora }

{ ... }

end; { push }

function member(x:t; h:ref):Boolean;     { dla ident. }

{ .. . }

end; { member }

background image

58

procedure pushp(x:char; var h:refp);
var p:re fp;
begin

new(p);
with p^ do

begin a:=x; next:=nil; nextp:=h; end;

h:=p end; {

pushp }

procedure memberp(x:char; h:refp; var gdzie:refp);
begin

gdzie:=nil;
while (h <> nil) and (gdzie = nil) do

if h^.a = x then gdzie:=h

else h:=h^.nextp;

end; { memberp }

procedure skorowidzl(n,c:integer; var H:refp);
var i:integer; x:t; gdzie:refp;
begin

H:=nil;
for i:=1 to n do

begin

czyt(c, x);        { pobranie identyfikatora x }
memberp(x[1], H, gdzie);    { gdzie jest x[l]? }
if gdzie = nil then begin

pushp(x[1], H);
push(x, H^.next);
end else

if not member(x, gdzie^.next) then

push(x, gdzie^.next) end;

end; { skorowidzl }

Przykład  struktury  skorowidza  zrealizowanego  jako  lista  list  identyfikatorów

przedstawiono na rysunku 4.3.

Jeżeli chcemy utworzyć taki skorowidz dla informacji innego typu, np. dla gra-

fów podzielonych na klasy abstrakcji  według  wartości  wybranego  niezmiennika,  to
musimy  podać  odpowiednie  definicje  struktur  danych  do  reprezentacji  elementów
listy poziomej (grafów) i listy pionowej (wartości niezmiennika).

background image

59

Rys. 4.3. Przykład struktury skorowidza zrealizowanego jako lista list identyfikatorów

4.3.2.   Generacja grafu losowego G(n, c)

Przedstawiamy  jeszcze  jeden  algorytm  generacji  grafów  losowych,  w  którym

dana  jest  liczba  wierzchołków  oraz  c  -  ciąg  stopni  wierzchołków,  a  grafy  są  loso-
wane  z  jednakowym  prawdopodobieństwem  ze  zbioru  wszystkich  takich  grafów
(oznaczonych). Jest to algorytm Rangraph  opublikowany przez N.C. Wormalda
w  1984  roku.  Algorytm  ten  jest  trudniejszy  w  realizacji  od  poprzednio  przedsta-
wionych algorytmów  generacji  grafów  losowych, a jego złożoność jest ponadkwa-
dratowa,  zwłaszcza  w  przypadku  generacji  grafów  regularnych  o  dużym  stopniu
wierzchołków.

Przed  wykorzystaniem  tego  algorytmu  należy  sprawdzić,  czy  c  jest  ciągiem

graficznym, a więc czy istnieją grafy o takim ciągu stopni (por. [1]).

Główną strukturą danych, w której konstruuje się graf G = G(n, c), jest tablica L

o  elementach  typu  całkowitego  i  długości  równej  podwojonej  liczbie  krawędzi
grafu. Przed rozpoczęciem właściwej generacji wprowadza się do tablicy numery
/  wierzchołków  grafu,  l  <  /  <  n,  każdy  z  nich  c

t

  razy.  W  implementacji  algorytmu

wykorzystano  dwie  procedury:  initL  oraz  Gnc.  Gdy  generuje  się  wiele  grafów  o
tym samym ciągu stopni, procedurę initL wykonuje się tylko raz, dla pierwszego

background image

60

grafu.  Do  przechowywania  informacji  o  wygenerowanych  krawędziach  wykorzy-
stano  znaną  tablicę  kwadratową  A  (lokalną  w  procedurze  Gnć).  Rozwiązanie  to
można  ulepszyć,  używając  oszczędniejszej  struktury  danych.  Zmienna  last  prze-
chowuje informację o indeksie ostatniego elementu tablicy L.

Algorytm generacji grafu losowego G = G(n, c)

Dane: n - liczba wierzchołków grafu, c - ciąg stopni wierzchołków.
Wynik: L - tablica krawędzi losowego grafu G = G(n, c). Metoda:
1.  Przygotuj tablicę zawierającą c[i] wystąpień wierzchołka /, = l, 2, .... n.
2.  
Podstaw zbiór pusty jako początkową wartość zbioru krawędzi grafu G.
3.  Losuj sąsiadów dla kolejnych wierzchołków z tablicy (rozpoczynając od

ostatniego elementu tej tablicy) w następujący sposób:

a.

jeżeli dla wierzchołka / zostanie wylosowany sąsiad o tym samym numerze
(pętla własna w grafie) lub ponownie zostanie wylosowana   para  wierz
chołków /, (krawędź równoległa), to powtórz losowanie krawędzi od po
czątku, ale dla ostatnio uzyskanego stanu tablicy L;

b.

w przeciwnym przypadku wylosowany element tablicy zamień miejsca
mi z elementem poprzedzającym ten, dla którego wykonano losowanie,
i dodaj nową krawędź do grafu G (zaktualizuj zbiór krawędzi).

Złożoność: O(n

5

).

Implementacja

Rys. 4.4. Struktury danych do generacji grafu losowego G = G(n, c)

const nmax = 100; kmax = nmax*(nmax - 1) di v 2;
type ind = L.nmax;

indl = 1..kmax;
tL = array[indl] of integer;

{ L }

t2 = array[ind,ind] of Boolean;

{ A }

t=array[ind] of integer;

{ c }

background image

61

procedure initL(n:integer; var c:t; var L:tL;

var last:integer); var

i,j,h:integer; begin h:=0; for i:=l to n do

for j:=l to c[i] do begin h:=h + 1; L[h]:=i; end;

last:=h end; { initL }

procedure Gnc(n, last:integer; var c:t; var L:tL);
var i,j,h,z:integer; A:t2; good:Boolean;  begin

good:=false; while
not 
good do begin

good:=true;
for i:=1 to n do

for j:=l to n do A[i,j]:=false;

h:=last;
while (h >= 1) and good do

begin

i:=L[h];

{ wierzcho

łek }

z:=random(h - 1) + 1;      { indeks s

ąsiada }

j:=L[z];

if i = j then good:=false    { p

ętla własna }

else begin

if A[i,j] then good:=false  { nie nowa }

else begin

A[i,j]:=true; A[j,i]:=true;
L[z]:=L[h-l]; L[h-l]:=j; h:=h - 2;
end;

end; { if } end; {

while h} end; { while
not } end; { Gnc }

4.3.3.   Konstruowanie listy list grafów

Jeżeli struktura danych przechowująca wiele grafów powinna uwzględniać po-

dział  grafów  na  klasy  według  wartości  niezmiennika  z  szerokiego  zakresu,  ale
przyjmującego tylko niektóre wartości, to warto wykorzystać listę do przechowy-

background image

62

wania wartości niezmiennika i grupować reprezentacje grafów na odpowiednich
listach, tak jak w poprzednim algorytmie zaprojektowanym dla identyfikatorów.

Wybierzmy liczbę trójkątów jako niezmiennik losowych grafów regularnych

generowanych za pomocą algorytmu G(n, c).

Algorytm konstruowania listy list grafów

Dane: n - liczba wierzchołków, r - stopień wierzchołka, rep - rozmiar próbki.
Wyniki: H - lista list nieizomorficznych grafów regularnych z podziałem na listy
według liczby trójkątów w tych grafach. Metoda:
1.  Przygotuj H.
2.  
Powtórz rep razy następujące operacje:

a. wygeneruj graf losowy G = G(«, c) i oblicz c

3

 - liczbę trójkątów w G,

b. sprawdź, gdzie na liście wartości niezmiennika znajduje się wartość c

3

,

c. jeśli nie ma jej na tej liście, to ją dopisz oraz dopisz G do wskazanej listy

grafów (G będzie pierwszym elementem tej listy),

d. jeśli 

CT

znajduje się na liście H, to sprawdź, czy graf G jest izomorficzny z

jakimś elementem listy i jeśli odpowiedź jest „nie", to dopisz G do tej listy.

Złożoność: zależy od liczby wierzchołków «, stopnia wierzchołków oraz od zło-

żoności algorytmu testowania izomorfizmu grafów.

4.4. Tablica mieszająca

4.4.1.   Konstruowanie tablicy mieszającej identyfikatorów

Tablice mieszające wykorzystuje się w sytuacjach,  gdy  szybkie wyszukiwanie

zgromadzonej informacji jest ważną cechą projektowanego algorytmu. Informacje
są wprowadzane do elementu  tablicy  o  indeksie  obliczonym  jako  wartość  funkcji
mieszającej.  
Argumentem  tej  funkcji  jest  wprowadzana  informacja,  np.  identyfi-
kator lub graf. Jeśli element tablicy o tym indeksie jest wolny, to informacja może
być tam umieszczona; jeśli jest zajęty przez inną informację, należy obliczyć war-
tość  indeksu  wtórnego,  najlepiej  stosując  dodawanie  kwadratu  przyrostu.  Dalsze
szczegóły dotyczące tablic mieszających można znaleźć w książce [7, rozdz. 4.6].

W implementacji algorytmu  konstruowania tablicy  mieszającej  identyfikatorów

przewidziano  ograniczoną  długość  identyfikatora.  Długie  identyfikatory  można
podzielić  na  części  i  przechowywać  na  liście  wskazywanej  przez  pierwszą  część,
znajdującą się w tablicy. Pole a rekordu będącego elementem tablicy jest wykorzy-
stywane do przechowywania informacji o tym, czy element jest wolny czy zajęty.
W przypadku  identyfikatorów  jest  to często  liczba  całkowita  oznaczająca  kolejny
numer identyfikatora.

background image

63

Algorytm konstruowania tablicy mieszającej identyfikatorów

Dane:   - długość ciągu identyfikatorów, ciąg identyfikatorów c-znakowych.
Wynik: H - słownik identyfikatorów w tablicy mieszającej. Metoda: Założenia:
długość tablicy musi być liczbą pierwszą, tablica zawiera dodatkową informację
o tym, czy element tablicy jest wolny; w przypadku kolizji poszukuje się
indeksu wtórnego metodą kwadratową.
1.  Przygotuj tablicę (wszystkie elementy są oznaczone jako wolne, np. przez

wstawienie liczby równej hmax).

2.  Powtórz razy następujące operacje:

a. pobierz identyfikator z ciągu wejściowego, oblicz indeks pierwotny;
b. jeżeli to miejsce jest wolne, wstaw oraz oznacz miejsce jako zajęte (np.

kolejny numer wstawianego identyfikatora);

c. jeżeli miejsce jest zajęte przez inną informację, to szukaj indeksu wtórnego

i wstaw x w to miejsce tablicy.

Złożoność: O(cn).

Implementacja

Rys. 4.5. Przykład struktury tablicy mieszającej

background image

64

const cmax = 15; hmax = 501; nmax = 400; {max. wypeln.}
type ind = 0..hmax - 1;

t = array[1..cmax] of char; rH =
record a:integer; b:t; end; tH =
array[ind] of rH;

function hash(c:integer; var x:t):integer;
var h,i:integer; begin h:=l;
for i:=l to c do h:=(h * ord(x[i])) mod hmax;
hash:=h end; { hash } procedure insert(x:t;
j:integer; var nr:integer;

var H:tH);

begin

with H[j] do begin a:=nr; b:=x; end;
nr:=nr + 1; end; { insert }
procedure konstr(n,c:integer; var H:tH );
var i:integer; free:Boolean;  begin

nr:=l; for i:=0 to hmax - 1 do H[i].a:=hmax;
for i:=l to do begin

czyt(c, x); {pobranie identyfikatora }
j:=hash(c, x);{ obliczenie indeksu pierwotnego }
if H[j].a = hmax   then insert(x, j, nr, H)  else

if H[j].b <> then { indeks wtórny }

begin

m:=0; free:=false;
while not free do
begin

m:=m + 1; j:=(j + sqr(m)) mod hmax;
if H[j].a = hmax  then begin

free:=true; insert(x, j,
nr, H) end

else if H[j].b = x then free:=true

end;

end; { else } end;

{ i } end; { konstr }

background image

65

4.4.2.   Wyszukiwanie informacji w tablicy mieszającej

Podany niżej algorytm służy  do  udzielenia  odpowiedzi  na  pytanie, gdzie znaj-

duje się informacja w tablicy mieszającej. W implementacji tego algorytmu wyni-
kiem jest indeks równy zeru, jeśli nie znaleziono danej informacji.

Algorytm wyszukiwania informacji w tablicy mieszającej

Dane: x - identyfikator c-znakowy, r - liczba powtórzeń, H - tablica mieszająca.

Wynik: Informacja o tym, gdzie x znajduje się w tablicy H.

Metoda:

1.  Oblicz indeks pierwotny.
2.  Sprawdź, czy znajduje się w tym miejscu tablicy H. Jeśli nie, to obliczaj in

deks wtórny (ale nie więcej niż razy) i sprawdzaj, czy znajduje się w tym
miejscu.

3.  Jeśli nie, to wynikiem jest indeks o wartości O, co oznacza, że nie znaleziono x

w tablicy H.

Złożoność: O(r).

Implementacja

function search(r,c:integer; x:t; var H:tH):integer;

var j,m:integer; jest:Boolean;

begin

j:=hash(c, x) ;
jest:=false;
m:=0;

while (m <= r) and not jest do

if H[j].b = x then jest:=true
else begin

m:=m + l;

j:=(j + sqr(m)) mod hmax;

end;

if jest then search:=j else search:=0;

end; {search }

background image

5.   STRUKTURY DRZEWIASTE

5.1.   Drzewa binarne

5.1.1.   Konstruowanie losowego drzewa binarnego

Przegląd  struktur  drzewiastych  rozpoczynamy  od  przedstawienia  podstawo-

wych operacji na  drzewach  binarnych,  czyli takich,  w których  każdy  wierzchołek
ma  co  najwyżej  dwa  następniki',  lewy  i  prawy.  Jeden  z  wierzchołków  jest  wyróż-
niony,  nazywamy  go  korzeniem.  Wierzchołki  nie  mające  następników  nazywamy
wierzchołkami  wiszącymi  lub  liśćmi.  Pozostałe  wierzchołki  nazywamy  wewnętrz-
nymi. 
Wszystkie wierzchołki znajdujące się na lewo od  korzenia  należą  do lewego
poddrzewa, a wierzchołki znajdujące się na prawo od korzenia - do prawego pod-
drzewa.  Poza  tym  dla  każdego  wierzchołka  wyróżnia  się  jego  lewe  i  prawe  pod-
drzewo.

Mówimy, że wierzchołek znajduje się na poziomie k drzewa binarnego, jeśli je-

go odległość od korzenia jest równa k. Przyjmuje się, że poziom  korzenia jest rów-
ny  1.  Wysokością  drzewa  jest  odległość  najdalszego  liścia  od  korzenia.  Drzewo
przedstawione na rysunku 5. l ma wysokość równą 3.

lewe poddrzewo

prawe poddrzewo

Rys. 5.1. Struktura drzewa binarnego

background image

67

Przedstawimy kilka rodzajów drzew binarnych: losowe drzewo binarne, drzewo

binarne poszukiwań, drzewo dokładnie wyważone, drzewo Wirtha oraz kopiec.

Podobnie jak w przypadku tablic, plików i list, przedstawiamy najpierw metodę

konstruowania losowego drzewa binarnego RBT (ang. Random Binary Tree)

0 danej liczbie wierzchołków. Losowany jest nie tylko ciąg liczb całkowitych
wprowadzanych do drzewa (z zakresu od l do z), ale również podczas przeglądania
już zbudowanego drzewa w celu znalezienia lokalizacji nowego elementu (liścia)
losuje się kierunek dalszego przeglądania i z prawdopodobieństwem/) wybiera się
lewe poddrzewo (czyli prawe poddrzewo z prawdopodobieństwem l -p).

W  implementacji  każdego  drzewa  binarnego  wykorzystuje  się  rekordy  o  co

najmniej trzech polach: jednym - informacyjnym (pole np. typu całkowitego)
1dwóch wskazujących na lewe i prawe poddrzewo (pola left right typu wskaźni
kowego).

Drzewo  BRT przedstawione  na rysunku  5.2 wygenerowano  dla  następujących

danych: n = 6, z = 100, p = 0.5. O wygenerowanym ciągu liczb losowych można
powiedzieć, że pierwszym elementem była liczba 10, która znajduje się w korzeniu
drzewa, oraz że co najmniej jedna z liczb znajdujących się w liściach drzewa była
ostatnim elementem ciągu.

Strukturę  losowego  drzewa  binarnego  wykorzystuje  się  przede  wszystkim  do

testowania algorytmów działających na drzewach binarnych, np. do przeglądania,
obliczania wysokości, testowania izomorfizmu dwóch drzew.

Rys. 5.2. Przykład struktury losowego drzewa binarnego RBT

background image

68

Algorytm konstruowania losowego drzewa binarnego

Dane: n - długość ciągu liczb, - prawdopodobieństwo lewostronne, z - zakres

losowania.

Wynik: t - korzeń losowego drzewa.

Metoda:

1.  Przygotuj drzewo.
2.  Powtórz razy losowanie liczby całkowitej x z przedziału [l, z] i wstawianie x

do drzewa za pomocą rekurencyjnej operacji insert w następujący sposób:
a. jeśli drzewo (poddrzewo) jest puste, to wygeneruj nowy element i umieść

w nim x oraz informację o pustym lewym i prawym następniku;

b. w przeciwnym przypadku losuj  liczbę z przedziału (O, 1) i jeżeli jest ona

mniejsza lub równa wartości prawdopodobieństwa/?, to szukaj miejsca dla
w lewym poddrzewie, a jeśli jest większa od p, to w prawym poddrzewie.

Złożoność: O(n).

Implementacja

type ref = ^elem;

elem = record a:integer; left, right:ref end;

procedure  insertRBT(x:integer;  p:real;  var  t:ref);
begin

if t = nil then

begin

new(t) ;
with t^ do begin a:=x; left:=nil; right:=nil end

end

else

if random <= p then insertRBT (x, p, t^. left)

else insertRBT(x, p, t^.right);

end; { insertRBT }

procedure konstrRBT(n, z:integer; p:real; var trref);
var i, x:integer; begin t: =nil ;

for i:=1 to n do

begin

x:=random(z) + 1;
insertRBT(x, p, t) end;
end; { konstrRBT }

background image

69

5.1.2.   Przeglądanie drzewa binarnego

Systematyczne przeglądanie drzewa binarnego  można wykonać w jednym

z  trzech  naturalnych  porządków  odwiedzania  wierzchołków  i  ich  następników
(oraz  w  trzech  dodatkowych,  w  których  zamieniono  kolejność  „lewy,  prawy"  na
odwrotną).  Są  to  następujące  porządki:  preorder,  inorder  i  postorder,  różnią  się
one  kolejnością odwiedzania wierzchołka w stosunku do  jego  następników.  Wyko-
rzystuje  się  je  np.  w  algorytmach  wyprowadzania  struktury  zbudowanego  drzewa
binarnego.  Dalsze  informacje  o  przeglądaniu  drzew  można  znaleźć  w  książce
[7,  rozdz.  4.4.2].  Rysunek  5.3  przedstawia  kolejność  przeglądania  drzewa  o  8
wierzchołkach z zastosowaniem wymienionych trzech porządków.

preorder

inorder

postorder

Rys. 5.3. Przykład przeglądania drzewa binarnego w trzech porządkach (liczby wskazują kolejność

odwiedzania wierzchołków)

Algorytm przeglądania drzewa binarnego

Dane:   t - korzeń dowolnego drzewa binarnego.

Wynik: Ciąg informacji z kolejnych wierzchołków drzewa w odpowiednim po-
rządku. Metoda:
1.  preorder. odwiedź korzeń t, odwiedź jego lewe poddrzewo w ten sam sposób,

odwiedź jego prawe poddrzewo w ten sam sposób,

2.  inorder: odwiedź lewe poddrzewo wierzchołka t, następnie wierzchołek / oraz

prawe poddrzewo wierzchołka t,

3.  postorder: odwiedź lewe poddrzewo wierzchołka t, potem jego prawe pod

drzewo oraz wierzchołek t.

Złożoność: O(n), gdzie jest liczbą elementów drzewa.

background image

70 Implementacja

t y p e    r ef   =   

^

ele m;

e l e m     =       r e c o r d     a : i n t e g e r ;       l e f t,   r i g h t :  re f    e n d ;

p r o c e d u r e     p r e o r d e r   ( t :  r e f   ) ;  b e g i n

if   t    < >     nil   t h e n

b e g i n
w r i t e    ( o u t ,   t

^

. a,    '    ' ) ;

p r e o r d e r   ( t

^

.   l e f t )   ;   p r e o r d e r

( t

^

  .   r i g h t )   ;  e n d ;

e n d ;     {    pr e or d er    }  p r o c e d u r e
i n o r d e r   ( t : r e f   ) ;  b e g i n

i f    t    O      n i l    t h e n

b e g i n
i n o r d e r   ( t

^

.   l e f t )   ;   w r i t e

( o u t,    t

^

. a ,     '    '  )  ;  i n o r d e r   ( t

^

. r i g h t   )   ;  e n d ;

e n d;     {   i n or d er    }  p r o c e d u r e
p o s t o r d e r   (   t : r e f  ) ;  b e g i n

if   t    < >     nil   t h e n

b e g i n

p o s t o r d e r   ( t

^

  .   l e f t )   ;   p o s t o r d e r  ( t

^

. ri g h t  ) ;  w r it e     ( o u t ,     t

^

. a ,    '    ' ) ;

e n d ;  e n d ;     {    p o st or d e r    }

5.1.3.   Wyprowadzanie informacji z drzewa binarnego

Przedstawiamy  kilka  algorytmów  wyprowadzania  struktury  zbudowanego

drzewa wraz z zapisaną w nim informacją. Pierwszy tych algorytmów przegląda
drzewo według  preorder, ujmuje  poddrzewa w  nawiasy  kwadratowe,  a  lewe  pod-
drzewo oddziela od prawego znakiem „ ; ". Drugi algorytm przegląda drzewo we-
dług  inorder,  ujmuje  poddrzewa  w  nawiasy  okrągłe,  a  puste  poddrzewa  oznacza
symbolem gwiazdki „*". Algorytmy te są łatwe do zaprojektowania, ale odczytanie
struktury  zbudowanego  drzewa  jest  kłopotliwe  nawet  w  przypadku  drzew  o  10
wierzchołkach.

background image

71

Trzeci  algorytm,  zaproponowany  przez  Wirtha  (por.  [7],  str.  209),  ale  ze  zmie-

nioną  kolejnością  odwiedzania na „prawy  - lewy",  wyprowadza drzewo  nie
w postaci ciągu znaków, ale z wykorzystaniem drugiego wymiaru na płaszczyźnie
-  rozmieszcza  informację  pobraną  z  wierzchołków  tak,  aby  po  dorysowaniu  kra-
wędzi powstał  odwrócony  o 90° rysunek  drzewa  binarnego.  Przykłady  wykonania
tych trzech algorytmów przedstawiono na rysunku 5.4.

Rys. 5.4. Przykłady wyprowadzania informacji z drzewa binarnego

Algorytm wyprowadzania informacji z drzewa binarnego

Dane:   t - korzeń dowolnego drzewa binarnego.

Wynik: Informacja przechowywana w wierzchołkach drzewa oraz struktura drze-
wa, zgodnie z wybranym algorytmem wyprowadzania: tekstowo (druki, druki) lub
w postaci półgraficznej (druk3). Metoda:
1.  druki: odwiedzanie wierzchołków wpreorder, lewe i prawe poddrzewa ujęte

w nawiasy [, ] i oddzielone znakiem „ ; ".

2.  druki', odwiedzanie wierzchołków w inorder, lewe i prawe poddrzewa ujęte

w nawiasy (,), a puste poddrzewa oznaczone jako „*".

background image

72

3.  druki:  odwiedzanie  wierzchołków  w  odwrotnym  inorder  (najpierw  prawe

poddrzewo)  i  drukowanie  informacji  z  odstępami.  Jeśli  obrócimy  wyprowa-
dzoną  strukturę  o  90°  i  uzupełnimy  krawędziami,  to  otrzymamy  czytelną  po-
stać struktury drzewa.

Złożoność: O{n).

Implementacja

type ref = ^elem;

elem = record a:integer; left, right:ref end;

procedure druki(t:ref);

{ w preorder }

begin

if t <> nil then

with t^ do begin

write(out,' ',a,' '); write(out,'[');
drukl(left); write(out,';');
drukl(right); write(out,']') end;

end; { drukl }

procedure druk2(t:ref);

{ w inorder }

begin

if t <> nil then

with t^ do begin

write(out,'(');

druk2(left); write(out,' ',a,' ')/
druk2(right); write(out,')'); end

else write(out, '*');

end; { druk2 }

procedure druk3(t:ref; h:integer);     { wg N. Wirtha }
var i:integer;

{ h - wci

ęcie }

begin

if t o nil then

with t^ do begin

druk3(right, h + 3); for i:=1 to h do
write(out,

1

 '); writeln(out,a);

druk3(left, h + 3); end; end; { druk3
}

background image

73

5.1.4.    Poszukiwanie informacji w drzewie

Operacja poszukiwania informacji w dowolnym drzewie binarnym wymaga

w najgorszym przypadku odwiedzenia wszystkich wierzchołków drzewa.

Algorytm poszukiwania informacji w drzewie

Dane: x - poszukiwana informacja (np. liczba całkowita), - korzeń drzewa.
Wynik: Informacja o tym, czy znajduje się w drzewie.
Metoda:
1.  Jeśli drzewo nie jest puste, to porównaj x z informacją w korzeniu drzewa. Jeśli

są równe, to znaleziono x.

2.  "W przeciwnym przypadku przeszukaj lewe poddrzewo i jeśli nie znajdziesz

w nim x, to przeszukaj prawe poddrzewo.

Złożoność: O(n).

Implementacja

type ref = ^elem;

elem = record a:integer; left, right:ref end;

procedure member(x:integer; trref; var jest:Boolean);
begin

if t O nil then

with t~ do

if x = a then jest:=true

else begin

member(x, left, jest);

if not jest then member(x, right, jest)

end

else jest:=false

end; { member }

5.1.5.    Obliczanie wysokości drzewa

Wysokość drzewa binarnego jako struktury danych powinna być jak  najmniej-

sza. Najgorszy przypadek zachodzi, gdy w każdym elemencie drzewa brak jednego
następnika (drzewo jest listą) - wtedy  jego wysokość  jest  równa  liczbie  elemen-
tów.

Przedstawiamy  dwie  realizacje  algorytmu  obliczania  wysokości  drzewa,  które

wyznaczają liczbę poziomów, i w związku z tym, aby otrzymać informację o wy-
sokości, należy od liczby poziomów odjąć l. W pierwszej realizacji wykorzystano
pomocniczą operację wyznaczania większej z dwóch liczb, tak aby operację obli-

background image

74

czenia wysokości można było zaprojektować wprost z definicji wysokości drzewa.
Druga realizacja jest funkcją rekurencyjną.

Algorytm obliczania wysokości drzewa binarnego

Dane:   t - korzeń dowolnego drzewa binarnego.

Wynik: Wysokość drzewa.

Metoda (rekurencyjną): Jeżeli drzewo jest puste, to jego wysokość jest równa zeru.

W przeciwnym przypadku oblicz wysokość lewego i prawego poddrzewa, wybierz

większą z nich i powiększ o l.
Złożoność: O(n), gdzie jest liczbą elementów drzewa.

Implementacja

type ref = ^elem;

elem = record a:integer; left, right:ref end; {

funkcje height i heightl wyznaczaj

ą liczbę poziomów:}

function wi

ększy(a,b:integer):integer;

{ Tomasz Obrebski, Inf., 1989 }

begin

if a > b then wi

ększy:=a else większy:=b

end; { wi

ększy } function

height(t:ref):integer; begin

if t <> nil then

height:=wiekszy(height(t^.left), height(t^.right)) + 1
else height:=0 end; { height }

function heightl(t:ref):integer;

{ Andrzej Urbanski, 1988}

var x,y:integer; begin

if t <> nil then

with t

A

 do begin

x:=heightl(left);
y:=heightl(right);
if x > y then heightl:=x + 1

else heightl:=y + 1;

end

else height1:=0;

end; { heightl }

background image

75

5.1.6.  Poszukiwanie największego elementu w drzewie

Jeżeli  należy  znaleźć  największy  (lub  najmniejszy)  element  w  drzewie  binar-

nym, to postępowanie jest podobne do przyjętego w odniesieniu do tablic. Za każ-
dym  razem  należy  przejrzeć  całą  strukturę,  ale  za  najgorszy  przypadek  należy
uznać ten, w którym  każdy  odwiedzany  wierzchołek  zawiera  informację  lepszą,
tzn. liczbę o większej wartości, od dotychczas znalezionej.

Algorytm poszukiwania największego elementu w drzewie binarnym

Dane:   t - korzeń drzewa binarnego przechowującego liczby całkowite.
Wynik: Wartość największego elementu w drzewie.
Metoda: Jeżeli drzewo nie jest puste, to zakładamy początkowo, że m - najwięk-
szy element - znajduje się w korzeniu drzewa. Następnie przeszukujemy drzewo
preorder, aktualizując każdorazowo wartość m, jeśli w odwiedzanym wierzchoł-
ku znajdziemy wartość większą niż m. Złożoność: O(n), gdzie «jest liczbą
elementów drzewa.

Implementacja

type ref = ^elem;

elem = record a:integer; left, right:ref end;

procedure szukaj(t:ref; var m:integer); { w preorder }
begin

if t <> nil then

begin

if (t^.a > m) then m:=t^.a;
szukaj(t^.left, m);
szukaj(t^.right, m); end

end; { szukaj } function
max(t:ref):integer; var
naj:integer; begin

if t O nil then

begin

naj:=t^.a;
szukaj(t, naj)
end;

max:=naj;

end; { max }

background image

76

5.1.7.  Testowanie izomorfizmu drzew binarnych

Dwa drzewa binarne uważamy za izomorficzne, jeżeli po ewentualnej zamianie

lewych  lub prawych  następników  mają  tę  samą  strukturę.  Na  rysunku  5.5  przed-
stawiono ilustrację tej relacji (oznaczonej symbolem = ).

Rys. 5.5. Dwa przykłady izomorficznych drzew binarnych

Algorytm testowania izomorfizmu drzew binarnych

Dane: tl, (2 - korzenie dwóch drzew binarnych.

Wynik: Informacja o tym, czy drzewa tl t2 są izomorficzne.
Metoda (rekurencyjna): Jeżeli tl t2 są puste, to odpowiedź jest „tak". Jeśli jedno
z nich jest puste, to odpowiedź jest „nie". Jeżeli żadne z drzew nie jest puste i lewe
poddrzewa są izomorficzne, to porównaj prawe poddrzewa; jeśli lewe poddrzewa
nie są izomorficzne, to porównaj prawe poddrzewo tl z lewym poddrzewem tl
i jeśli są izomorficzne, to sprawdź, czy lewe poddrzewo tl i prawe poddrzewo t2
są izomorficzne.

Złożoność: O(n), gdzie jest liczbą elementów drzewa.

Implementacja

type ref = ^elem;

elem = record a:integer; left, right:ref end;

function izo(tl, t2:ref):Boolean;

begin

{ Robert Jamro

ży, Inf., 1990 }

if (tl = nil) or (t2 = nil) then

izo:=tl = t2 else if
izo(t1^.left, t2^.left)

then izo:=izo(tl^.right, t2^. right)
else if izo(tl^.right, t2^.1eft)

then izo:=izo (tl^.left, t2^. right) else izo:=false;
end; { izo }

background image

77

5.2.   Binarne drzewo poszukiwań

5.2.1.   Konstruowanie losowego drzewa BST

Binarne drzewo poszukiwań BST (ang. Binary Search Tree) jest ważną struktu-

rą danych ze  względu  na  wewnętrzne uporządkowanie informacji. Jeżeli  rozważa-
my drzewo BST przechowujące liczby całkowite, to w lewym poddrzewie  każdego
wierzchołka znajdują się liczby mniejsze od znajdującej się w danym wierzchołku,
a  w  jego  prawym  poddrzewie  -  większe.  W  tak  określonym  drzewie  BST  każdy
wierzchołek  przechowuje  inną  liczbę  i  jeżeli  w  ciągu  wejściowym,  z  którego  two-
rzymy  drzewo  BST,  występują  powtórzenia  informacji,  to  tylko  pierwsze  wystą-
pienie będzie wprowadzone do tego drzewa. Należy o tym pamiętać, jeżeli chcemy
zbudować losowe drzewo o dokładnie wierzchołkach.

Wymienimy tu dwie istotne zalety tej struktury danych. Pierwsza z nich to fakt,

że operacje poszukiwania w BST wymagają przejrzenia tylko jednej ścieżki. Druga
wynika z łatwości sortowania - aby  otrzymać ciąg posortowany,  wystarczy  odwie-
dzić  wierzchołki  w  odpowiednim porządku (jeśli ciąg  ma  być  posortowany  rosną-
co, to wierzchołki są odwiedzane według inorder).

W implementacji  algorytmu  przedstawiono  przykład  drzewa  BST  wygenerowa-

nego  dla  następujących  danych:  n  =  7, z  =  100  (por. rysunek  5.6).  Podobnie  jak
przy  generacji  drzewa  RBT,  o  ciągu  wylosowanych  liczb  możemy  powiedzieć
tylko  tyle,  że  jego  pierwszym  elementem  była  liczba  27,  a  ostatnim  jedna  z  liczb
umieszczonych w liściu drzewa.

Algorytm konstruowania losowego drzewa BST

Dane: n - długość ciągu liczb, z - zakres losowania.
Wynik: t - korzeń drzewa BST.
Metoda:
1.  Przygotuj drzewo.
2.  Powtórz razy losowanie liczby z przedziału [l, z] i wstawianie do drzewa

BST za pomocą operacji insert w następujący sposób:

a.

jeśli wskazuje na puste poddrzewo, to wygeneruj nowy element wskazy
wany przez i umieść w nim informację oraz informację o pustym lewym
i prawym następniku,

b. w przeciwnym przypadku szukaj w drzewie BST miejsca, w którym można

dopisać x, czyli jeśli wartość x jest mniejsza od wartości znalezionej w ko
lejnym wierzchołku drzewa, to szukaj w lewym poddrzewie, a jeśli nie -
szukaj w prawym poddrzewie.

Złożoność: O(ri).

background image

Rys. 5.6. Przykład struktury drzewa BST

type ref = ^elem;

elem = record a:integer; left, right:ref; end;

procedure insert(x:integer; var trref); begin

if t = nil then

begin new(t) ;

with t^ do begin a:=x; left:=nil; right:=nil end;

end else if x <> t^.a then

if x < t ^.a then insert(x, t^.left)

else insert(x, t^.right);

end; { insert }

procedure konstrl(n, z:integer; var t:ref);
var i,x:integer; begin t:=nil;

for i:=l to n do

begin

x:=random(z) + 1;
insert(x, t); end; end; {
konstrl }

78

Implementacja

background image

79

5.2.2.   Poszukiwanie informacji w drzewie BST

Przedstawiamy  dwie  realizacje  algorytmu  poszukiwania  informacji  w  drzewie

BST  -  iteracyjną  i  rekurencyjną.  Poszukiwania  ograniczają  się  do  odwiedzenia
tylko jednej ścieżki  drzewa od  korzenia do znalezionego  wierzchołka lub do końca
ścieżki (gdy nie  ma w  drzewie poszukiwanej  informacji). Najgorszy przypadek to
drzewo o jednej ścieżce o długości n, w którym nie ma poszukiwanej informacji.

Algorytm poszukiwania informacji w drzewie BST

Dane:   x - informacja (np. liczba całkowita), t - korzeń drzewa BST. Wynik:
Informacja o tym, czy x jest elementem drzewa o korzeniu t. Metoda: Przeglądaj
kolejne wierzchołki, rozpoczynając od korzenia drzewa. Jeżeli wartość jest
mniejsza od informacji znalezionej w wierzchołku, to dalsze poszukiwania
prowadź w lewym poddrzewie, w przeciwnym przypadku - w prawym
poddrzewie. Zakończ poszukiwanie, jeśli znajdziesz lub jeśli wierzchołek jest
liściem. Złożoność: O(n).

Implementacja

type ref = 

A

elem;

elem = record a:integer; left, right:ref  end;

procedure member(x:integer; trref;  var jest:Boolean);

{ iteracyjn

ą }

begin

jest:=false;

while (t O nil) and not  jest do if

x = t^.a then jest:=true else

if x < t^.a then  t:=t

A

.left

else t:=t

A

.right;

end; { member }

procedure memberl(x:integer; trref; var jest:Boolean);

{ rekurencyjn

ą }

begin

jest:=false;
if t O nil then

if x = t^.a then jest:=true

else

if x < t~.a then memberl(x, t^.left, jest)

else memberl(x, t^.right, jest);

end; { memberl }

background image

80

5.2.3.  Wyszukiwanie największego elementu w drzewie BST

Największy element w drzewie BST znajduje się w prawym poddrzewie na

skrajnej prawej ścieżce. Przedstawiamy dwie realizacje: iteracyjną i rekurencyjną.

Algorytm wyszukiwania największego elementu w drzewie BST

Dane: t - korzeń drzewa BST. Wynik: m -
największy element w drzewie.
Metoda: Przechodzimy od korzenia po skrajnej prawej ścieżce i na jej końcu znaj-
dujemy element o największej wartości. Złożoność: O(n), gdzie jest liczbą
elementów drzewa. Implementacja

type ref = ^elem;

elem = record a:integer; left, right:ref end;

procedure max(t:ref; var m:integer);     { iteracyjn

ą }

begin

if  t  O  n i l  th en

while f. right <> nil do t:=t^.right;

m:=t^. a; end; { max }

procedure maxl(t:ref; var m:integer);  { rekurencyjn

ą }

begin

if f*. right = nil then m:=t^.a

else max1(t^.right, m)

end; { maxl }

5.2.4. Usuwanie informacji z drzewa BST

Jeżeli  chcemy  usunąć  zbędną  informację  z  drzewa  BST,  to  operację  tę  należy

zaprojektować tak, aby  nie  zniszczyć  wewnętrznego  uporządkowania  elementów
w tym drzewie. Jeśli usuwamy informację, która znajduje się w liściu drzewa lub
w  wierzchołku,  który  ma  tylko  jeden  następnik,  to  operacja  polega  na  usunięciu
liścia  lub  ominięciu  wierzchołka.  Gdy  usuwany  wierzchołek  ma  dwa  następniki,
wybieramy  jedno  z  dwóch  rozwiązań.  W  pierwszym  najpierw  wyszukujemy
w drzewie informację, którą można umieścić zamiast usuwanej. Będzie to informa-
cja znajdująca się w jednym z liści: w lewym poddrzewie na skrajnej prawej ścieżce
lub  w prawym  poddrzewie  na  skrajnej  lewej  ścieżce  (nie  jest  istotne,  którą  z
nich wybierzemy). Po przeniesieniu tej informacji na miejsce, gdzie znajdowała się
informacja do usunięcia, usuwamy zbędny już liść.

Drugie  rozwiązanie  polega  na  przyłączeniu  prawego  poddrzewa  usuwanego

wierzchołka  do  największego  elementu  w  jego  lewym  poddrzewie,  a  następnie
usunięciu tego wierzchołka (ma on już teraz tylko lewe poddrzewo).

background image

81

Należy  pamiętać  o  zwolnieniu  pamięci  zajmowanej  dotychczas  przez  usuwany

wierzchołek.

Na  rysunku  5.7  pokazano  usuwanie  wierzchołka  z  drzewa  BST  z  wykorzysta-

niem  pierwszej  z  opisanych  metod  (usuwa się  wierzchołek  zawierający  liczbę  5  -
informację  tę  zastępujemy  informacją  ze  skrajnej  prawej  ścieżki  w  lewym  pod-
drzewie, czyli liczbą 3, wierzchołek, który zawierał liczbę 3, usuwamy z drzewa).

Rys. 5.7. Przykład usuwania informacji z drzewa BST pierwszą metodą

Pierwszy algorytm usuwania informacji z drzewa BST

Dane:  x - informacja (np. liczba całkowita), t - korzeń drzewa BST.
Wynik: t - korzeń drzewa BST po usunięciu x.
Metoda:

1. Jeżeli w drzewie znajduje się wierzchołek zawierający x, to sprawdź liczbę

jego poddrzew:

a.

jeżeli ma co najwyżej jedno poddrzewo, to usuń (jeśli jest liściem) lub
omiń (jeśli ma jedno poddrzewo) ten wierzchołek;

b. jeżeli ma dwa poddrzewa, to wyszukaj największy element w jego lewym

poddrzewie (łub najmniejszy w prawym poddrzewie), wstaw tę informację
zamiast x i usuń znaleziony wierzchołek.

2.

Zwolnij pamięć, którą zajmował usunięty wierzchołek.

Złożoność: O(n), gdzie jest liczbą elementów drzewa

background image

82

Implementacja

type ref = ^elem;

elem = record a: integer; left, right: ref end;

procedure delete (x : integer; var t:ref);

{ wg N. Wirtha [7], str.224 }

var p:ref;

procedure zamiana (var r:ref);
begin

if r^. right <> nil then zamiana (r" .right )

else begin

p^ . a:=r^ . a;
p:=r;

end; end; {

zamiana }

begin { delete }

if t <> nil then

if x < t^.a then delete (x, t'.left)

else

if x > t^.a then delete (x, t" . right)

else { x = t^.a } begin { usuwanie }

p:=t;

{ pomocniczy wska

źnik }

if p^.left = nil then t:=t^.right

else

if p^. right = nil then t:=t^.left

else zamiana (p^ . left ) ; dispose (p) ;

end; { usuwanie } end; { delete }

Drugi algorytm usuwania informacji z drzewa BST

Dane: x - informacja (np. liczba całkowita), t - korzeń drzewa BST.
Wynik: t - korzeń drzewa BST po usunięciu x.
Metoda:

1.   Jeżeli w drzewie znajduje się wierzchołek zawierający x, to sprawdź liczbę

jego poddrzew:

a.   jeżeli ma co najwyżej jedno poddrzewo, to usuń (jeśli jest liściem) lub

omiń (jeśli ma jedno poddrzewo) ten wierzchołek;

background image

83

b.  jeżeli  ma  dwa  poddrzewa,  to  wyszukaj  największy  element  w  jego  lewym

poddrzewie  i  dołącz  do  niego  prawe  poddrzewo  x;  omiń  wierzchołek  x,
który ma teraz tylko jedno (lewe) poddrzewo.

2.   Zwolnij pamięć, którą zajmował usunięty wierzchołek.

Złożoność: O(ri), gdzie jest liczbą elementów drzewa.

Implementacja

type ref = ^elem;

elem = record a:integer; left, right:ref; end;

procedure szukajprawego(p:ref; var r:ref);
begin

if p^.right <> nil

then szukajprawego(p^.right, r)
else r:=p; end; {

szukajprawego }

procedure deleteM(x:integer; var t:ref);

{ Leszek Masadynski, 1988 }

var p:ref; begin

if t o nil then if

t^.a = x then

if t^.left = nil then t:=t

A

.right

else begin

szukajprawego(t

A

.left, p);

p^.right:=t~.right; p:=t;

t:=t^.left;
dispose(p) end else

if t^.a > x then deleteM(x, t^.left)

else deleteM(x, t^.right)

end; { deleteM } ______________________________________ 

Na rysunku 5.8 pokazano usuwanie wierzchołka z drzewa BST z wykorzysta-

niem drugiej metody.

background image

84

Rys. 5.8. Przykład usuwania wierzchołka z drzewa BST drugą metodą

5.2.5.  Reorganizacja drzewa BST

Jeżeli  informacja przechowywana  w  drzewie BST  ma dodatkowy atrybut i  wy-

magamy,  aby  dostęp  do  informacji  był  tym  łatwiejszy,  im  większa  jest  wartość
atrybutu,  to  należy  zreorganizować  dane  drzewo  BST.  Rysunek  5.9  przedstawia
przykład drzewa BST przed reorganizacją i po niej.

W  rozwiązaniu  tego  zadania  można  zaproponować  utworzenie  nowego  drzewa

przez  pobranie  z  pierwszego  kolejno  elementów  o  największej  wartości  atrybutu,
tak aby znalazły się one jak najbliżej korzenia nowego drzewa BST.

W implementacji przewidziano trzy procedury pomocnicze:

1.  insert — wstawianie wierzchołka do drzewa BST,
2.  konstrBST - konstruowanie drzewa z danych w pliku input oraz
3.  max - poszukiwanie największego  elementu w dowolnym drzewie binar

nym.

Po utworzeniu nowego  drzewa  należy  zwolnić  pamięć  zajmowaną  przez  pierw-

sze drzewo.

Algorytm reorganizacji drzewa BST

Dane: ciąg par liczb całkowitych, n - długość ciągu.

Wynik: nowe - korzeń nowego drzewa BST, po reorganizacji.

background image

Rys. 5.9. Przykład reorganizacji drzewa BST

Metoda:

1.  Utwórz pierwsze drzewo BST ciągu par liczb całkowitych według pierwsze

go elementu pary.

2.  Przygotuj nowe drzewo BST.

85

background image

86

3.  Poszukuj wierzchołków o kolejnej największej wartości atrybutu w pierwszym

drzewie BST i wykorzystując operację insert, wstawiaj je do nowego (zreorga
nizowanego) drzewa BST według pierwszego elementu pary.

4.  Po wykorzystaniu kolejnego wierzchołka należy usunąć z niego informację

o atrybucie (w pierwszym drzewie), aby wierzchołek ten nie był ponownie
wybrany.

Złożoność: O(n), gdzie jest długością ciągu.

Implementacja

type ref = ^elem;

elem = record a,b:integer; left, right:ref end;

procedure insert(x,y:integer; var trref); begin

if t = nil then

begin new(t);
with t^ do

begin a:=x; b:=y; left:=nil; right:=nil end

end else if x <> t^.a then

if x < t^.a then insert(x, y, t

A

.left)

else insert(x, y, t

A

.right)

end; { insert }

procedure konstrBST(n:integer; var t:ref);
var i:integer; begin

t: =nil ;
for i:=1 to n do

begin read(x, y); insert(x, y, t) end;

end; { konstrBST }

procedure max(t:ref; var mrinteger; var rrref);
begin

if t <> nil then

begin

if t^.b > m then begin m:=t^.b; r:=t end;
max(t^.left, m, r) ; max(t^.right, m, r) end; end; {
max }

procedure reorg(n:integer; var nowe:ref);
var m:integer; t,r:ref;

background image

87

begin

konstrBST(n, t); nowe:=nil;  m:=-l;
repeat

max (t, m, r) ;

r^.b:=-l;

if  m <> -l then insert(r^.a, m, nowe);

until m = -1; end; { reorg }

5.3. Inne drzewa binarne

5.3.1.   Konstruowanie drzewa dokładnie wyważonego

Drzewo  dokładnie  wyważone  ma  najmniejszą  możliwą  wysokość  przy  danej

liczbie  wierzchołków.  Na  każdym  poziomie  liczba  wierzchołków  w  jego
lewym i prawym poddrzewie różni się co najwyżej o jeden.

Na  rysunku  5.10  przedstawiono  dwa  drzewa  dokładnie  wyważone  na  4  i  10

wierzchołkach.  Liczby  wskazują  kolejność  wstawiania  wierzchołków  do  drzewa.
Zauważmy, że wszystkie poziomy, z wyjątkiem ostatniego, są dokładnie zapełnio-
ne  wierzchołkami.  Natomiast  na  ostatnim  poziomie  mogą  nie  występować  prawe
następniki wierzchołków poprzedniego poziomu.

W  implementacji  wykorzystano  funkcję  (por.  [7]),  która  generuje  elementy

struktury  wskazywane  przez  lokalne  wskaźniki  i  dołącza  je  do  drzewa  podczas
powrotu z rekurencji.

Rys. 5.10. Dwa drzewa dokładnie wyważone na « wierzchołkach (n = 4 i 10). Liczby wskazuj ą kolej-

ność wstawiania wierzchołków do drzewa

background image

88

Algorytm konstruowania drzewa dokładnie wyważonego

Dane:   n- liczba wierzchołków drzewa, z - zakres losowania elementów ciągu.

Wynik: drzewo dokładnie wyważone (binarne).

Metoda:

1.  Przeznacz j eden wierzchołek na korzeń.
2.  Zbuduj lewe poddrzewo z   nl = n div 2 wierzchołkami w niniejszy sposób.
3.  Zbuduj prawe poddrzewo z   np = n-nl- l wierzchołkami.
Złożoność: O(n).

Rys. 5.11. Struktura danych i przykład drzewa dokładnie wyważonego (n = 4, z = 200)

type ref = ^elem;

elem = record a:integer; left, right:ref; end;

function DDW(n, z:integer):ref;       { dane losowane }
var p:ref; x, nl, np:integer; begin

if n > 0 then

begin

nl:=n div 2; np:=n - nl - 1;
x:=random(z) + 1; write(out, x, ' ');
new(p); with p^ do begin

a:=x; left:=DDW(nl, z) ; right:=DDW(np, z);

end;

DDWl:=p;

end

else DDW:=nil;

end; { DDW }

background image

89

5.3.2.   Konstruowanie drzewa Wirtha

Przedstawimy prosty algorytm tworzenia drzewa binarnego z danych  uporząd-

kowanych  w  preorder.  Algorytm  ten  zaproponował  N.  Wirth  i  dlatego  drzewo
generowane za pomocą tego algorytmu nazywamy drzewem Wirtha.

Jeżeli dany ciąg informacji jest uporządkowany w preorder (z wykorzystaniem

wyróżnionego symbolu,  np.  '*',  na  oznaczenie  braku  następnika),  to  w  najprost-
szym  przypadku  informację  mogą  stanowić  symbole  będące  literami,  tak  jak  w
implementacji algorytmu przedstawionego niżej. W przypadku ogólnym mogą to
być  np.  dowolne  identyfikatory.  Innym  zastosowaniem  tego  algorytmu  jest  kon-
struowanie  losowych  drzew  binarnych  wykorzystywanych  do  testowania  algoryt-
mów przetwarzania struktur drzewiastych.

W  implementacji  algorytmu  Wirtha,  w  odróżnieniu  od  omówionych  już  algo-

rytmów konstruowania drzew binarnych, mamy tylko jedną procedurę rekurencyj-
ną  enter.  Działanie  tej  procedury  kończy  się,  gdy  ostatnio  pobrany  znak  będzie
przyjęty  jako  symbol  braku prawego  następnika  na skrajnej prawej ścieżce  budo-
wanego  drzewa.  Na  rysunku  5.12  podano  dwa  przykłady  drzewa  Wirtha  -  na
trzech i pięciu wierzchołkach (informację stanowią litery alfabetu łacińskiego).

a*bc***

a

*bc**cd*e***

Rys. 5.12. Dwa przykłady drzewa Wirtha wygenerowanego z danego ciągu znaków

Algorytm konstruowania drzewa Wirtha

Dane: ciąg liter i znaków '*' -wpreorder.
Wynik: drzewo Wirtha (binarne).
Metoda: Pobieraj kolejne elementy ciągu znaków. Jeżeli pobrano '*', to wskazy-
wane poddrzewo jest puste; w przeciwnym przypadku wygeneruj nowy wierzcho-

background image

90

łęk, wstaw do niego pobraną informację i zbuduj w ten sam sposób jego lewe,
a następnie prawe poddrzewo.
Złożoność: O(n), gdzie jest liczbą wierzchołków drzewa (liczbą liter w ciągu
wejściowym).

Implementacja

type ref=^elem;

elem=record a:char; left, right:ref end;

procedure enter(var t:ref); var chrchar; begin

read(ch); write(ch); if
ch = '*' then t:=nil else
begin

new (t); with
t

A

 do

b e g i n     a : = c h ;       e n t e r ( l e f t ) ;       e n t e r ( r i g h t );      e n d ;  e n d ;

e n d;    {   ent er   }

5.3.3.   Kopce. Sortowanie przez kopcowanie

Drzewo  binarne  jest  kopcem  (ang.  heap),  jeżeli  w  każdym  wierzchołku

przechowuje  informację  o  wartości  nie  mniejszej  niż  ta,  która  znajduje  się  w  jego
lewym  i  prawym  następniku.  Tak  więc,  jeżeli  w  kopcu  przechowywane  są  liczby
całkowite,  to  w  jego  korzeniu  znajduje  się  największa  liczba  spośród  wszystkich
występujących w tym kopcu.

Kopiec  jest  strukturą  danych  z  bezpośrednim  dostępem  do  największego

elementu,  czyli  jest  realizacją  kolejki  priorytetowej.  Pobieranie  informacji  z  tej
kolejki  polega  na  pobraniu  elementu  wskazywanego  przez  korzeń  kopca,
a  dokładanie  nowego  elementu  trzeba  tak  zaprojektować,  aby  nie  zniszczyć
struktury kopca.

Algorytm  sortowania  przez  kopcowanie  (por.  [7],  rozdz.  2.2.5)  przetwarza

elementy tablicy  jednowymiarowej  tak,  jakby  były  elementami  drzewa  binarnego.
Pierwszy  element  tablicy  jest  korzeniem  tego  drzewa,  a  wszystkie  pozostałe
znajdują  się  na  kolejnych  poziomach.  Jeżeli  długość  ciągu  w  tablicy  jest  równa
całkowitej  potędze  liczby  2,  to  drzewo  binarne  jest  pełne,  w  przeciwnym
przypadku tylko ostatni poziom nie ma kompletu wierzchołków.

Jeżeli sortujemy ciąg o długości w tablicy a, to następnikami elementu a, są

w drzewie elementy o indeksach 2i (lewy następnik) oraz 2i + l (prawy). Metoda

background image

91

sortowania  przez  kopcowanie  jest  bardzo  prosta.  Najpierw  trzeba  utworzyć
pierwszy  kopiec  (czyli  ustawić  elementy  tablicy  tak,  aby  dla  każdego  /  wartość  a,
była  nie  mniejsza  niż  wartości  jego  następników).  Następnie  pierwszy  element
kopca (czyli a\) zamienia  się  miejsca mi  z  element em  ostatnim  (czyli  a„)
i  zmniejsza  długość  nie  posortowanego  ciągu  o  1.  Strukturę,  która  powstała  po
zamianie  elementów  tablicy,  należy  poprawić,  aby  znów  powstał  kopiec.  Dalsze
postępowanie jest podobne, czyli zamienia się wartości znajdujące się w a\ oraz na
końcu nie posortowanej części tablicy, czyli  a„-\. Po zakończeniu  operacji tablica
jest posortowana rosnąco.

Przedstawiamy dwie realizacje sortowania przez kopcowanie - z iteracyjnym

i rekurencyjnym poprawianiem kopca.

Na  rysunkach  5.13  i  5.14  przedstawiono  przykład  czterech  początkowych

kroków sortowania w tablicy ciągu  o długości 8 oraz odpowiadające  im struktury
drzewa binarnego.

   

Rys. 5.13. Stany tablicy w początkowych krokach sortowania, struktura drzewa i pierwszy kopiec

(2)

pierwszy kopiec

background image

92

(3)

(4)

   

   

Rys. 5.14. Drzewo po pierwszej zamianie i drugi kopiec

Algorytm sortowania przez kopcowanie (iteracyjny)

Dane:
Wynik:
Metoda:
l .   Utwórz pierwszy kopiec w tablicy a. 2.
Powtórz n -l razy następujące operacje:

a.   zamień informację a[l] oraz a[i], zmniejsz wartość indeksu / o l,

utwórz nowy kopiec w nie posortowanej części tablicy: #[!],.., «[/].

Implementacja

const nmax = 100;
type ind = l..nmax;

t = array[ind] of integer;

{ a }

procedure heapsort(nr integer; var art);
var i,x r integer;

procedure popraw(j, last r integer);     { iteracyjna }

var k,xr integer; b:Boolean;

begin

x:=a[j] ;
b:=false;

k:=2   *   j;

drugi kopiec

:  n - długość ciągu liczb, a - tablica zawierająca ciąg liczb. ik:
a - tablica zawierająca posortowany (niemalejąco) ciąg liczb

a.

zame    normacę a     oraz  a, zmnesz  war o     n

b.

utwórz nowy kopiec w nie posortowanej części tablicy:

Złożoność: O(n log n).

background image

93

while (k <= last) and not b do

begin

if k + 1 <= r then

if a[k + 1] > a[k] then k:=k + 1;

if a[k] > x

then begin a[j] :=a[k]; j:=k; k:=2 * j ; end
else b:=true

{ wyj

ście z pętli }

end;

a[j]:=x

end; { popraw }

begin { heapsort }

for i:=n div 2 downto 1 do popraw(i,n);
for i:=n downto 2 do begin

x:=a[i]; a[i]:=a[l]; a[l]:=x;
popraw(1, i - l); end; end; {
heapsort }

Algorytm sortowania przez kopcowanie (rekurencyjny)

Dane:  n - długość ciągu liczb, a - tablica zawierająca ciąg liczb.
Wynik: a - tablica posortowana (niemalejąco).
Metoda:
1.  Utwórz pierwszy kopiec.
2.  Powtórz n - l razy:

a.

zamianę miejscami korzenia kopca («[!]) i ostatniego elementu kopca oraz

b. utworzenie nowego kopca (algorytm rekurencyjny heapify) z ciągu krót

szego o l element.

Złożoność: O(n log n).

Implementacja

const nmax = 100;
type ind = 1..nmax;

t = array[ind] of integer;

{ a }

procedure heapsortl(n:integer; var a:t);
var i,x:integer;

procedure heapify(j,last:integer);   { rekurencyjna}
var k,x:integer;
begin

if j <= last div 2 then

background image

94

b e g i n

k:=2    *   j;

i f    k    +       l    < =     l a s t     t h e n

if   a [ k   +    1]    >    a [ k]   t he n   k: = k    +    1 ;

if   a[ k]     >   a [ j ]    t he n

b e g i n  x : = a [j ];    a [ j ] : = a [ k ] ;     a [ k]: = x ;    en d ;

h e a p i f y ( k ,         l a s t ) ;   e n d;

end;    {   hea pify   }

b e g i n

f o r     i: = n   d i v     2       d o w n t o       1     d o     h e a p i f y ( i ,n ) ;  f o r
i:= n   d o w nt o    2    do  b e g i n

x : = a [ i ] ;       a [ i ] : = a [ 1 ] ;       a [ 1 ] : = x ;
h e a p i f y( l ,     i    -    1 ) ;   end;  e n d;    {
h e a p s ortl   }

5.4.   Drzewa wielokierunkowe

5.4.1.   Konstruowanie losowego drzewa wielokierunkowego

Rozważmy  metodę  tworzenia  struktury  drzewiastej,  w  której  wierzchołki  mogą

mieć  dowolną  liczbę  następników  (ograniczoną  tylko  przez  liczbę  wierzchołków
drzewa).  Podobnie  jak  w  przypadku  wszystkich  uprzednio  rozważanych  struktur,
przedstawimy metodę konstruowania losowego drzewa wielokierunkowego.

W  metodzie  wykorzystamy  model  losowego  drzewa  rekurencyjnego  RRT

(ang.  Random  Recursive  Tree)  wprowadzony  i  badany  teoretycznie  przez  Jerzego
Szymańskiego.

Metoda generowania drzewa RRT  o  n  wierzchołkach  jest  prosta.  Wierzchołek  l

jest korzeniem  drzewa. Dla  każdego  następnego  wierzchołka  losujemy  (z  jednako-
wym  prawdopodobieństwem)  numer  wierzchołka,  który  będzie  jego  poprzedni-
kiem.  Tak  więc  poprzednikiem  wierzchołka  2  jest  zawsze  wierzchołek  l,  ale  dla
następnych  wierzchołków  wybór  jest  coraz  większy.  Wyniki  losowania  przecho-
wujemy w ciągu a, (/ = l, 2, ..., n). W podobny sposób generujemy drzewo RRT
o  zadanym  ciągu  stopni  wierzchołków:  po  wylosowaniu  poprzednika  sprawdzamy,
czy  może  być  on  zaakceptowany  (jeśli  nie,  losujemy  ponownie).  Na  rysunku  5.15
przedstawiono  przykład  losowego  drzewa  wielokierunkowego  o  pięciu  wierzchoł-
kach.

background image

95

Algorytm konstruowania drzewa wielokierunkowego

Dane: n - liczba wierzchołków.

Wynik: t - korzeń losowego drzewa wielokierunkowego.

Metoda:

1.

Wygeneruj drzewo w tablicy w następujący sposób:

a.

wierzchołek l nie ma poprzednika,

b.

losuj poprzedniki dla wierzchołków od 2 do n (spośród liczb od l do z - l
w każdym kroku i z jednakowym prawdopodobieństwem).

2.

Wyniki zapisz w tablicy list L.

Złożoność: O(n).

t

Rys. 5.15. Przykład drzewa wielokierunkowego, = 5

Implementacja

const nmax=100;
type ind=l..nmax;
ref 

=^

elem;

elem=record a: integer; x: Boolean; son:ref;

next:ref end;
ta=array [ind] of integer;

{ a }

tL=array [ind] of ref;

}

procedure konstrRRT (n: integer; var t:ref);

{ K. Zwierzy

ński, Inf. 1998 }

var a:ta; L:tL; i : integer;

procedure generacja (n: integer; var a:ta);
var i : integer;
begin

a[l]:=0; for i:=2 to n do a [i] :=random(i-l) + 1;

end; { generacja }

background image

96

procedure initL(n:integer; var L:TL);
var i:integer;
begin

for i:=l to n do
begin new(L[i]) ;

with L[i]^ do begin a:=i;son:=nil;next:=nil end;

end;

end; { initL }

begin { konstrRRT }

generacja(n, a); initL(n, L) ;
for i:=n downto 2 do begin

L[i] ^.next:=L[a[i]]^.son;
L[a[i]]^.son:=L[i]; end;

t:=L[l];

{ korze

ń drzewa wielokierunkowego }

end; { konstrRRT }

Na  rysunku  5.16  przedstawiono  strukturę  danych  -  element  listy  następników

każdego wierzchołka  oraz drzewo z  poprzedniego  rysunku  w  strukturze  danych.
Aby  ułatwić  dostęp  do  każdego  wierzchołka,  przewidziano  tablicę  L  zawierającą
wskaźniki wierzchołków.

Rys. 5.16. Przykład struktury danych dla drzewa wielokierunkowego z poprzedniego rysunku