« poprzedni punkt | następny punkt » |
Problem, który rozważymy w tym punkcie, to znalezienie elementu najmniejszego i największego w danym skończonym zbiorze, reprezentowanym przez tablicę e o elementach e[1], ..., e[n]. Dla uproszczenia nazywać będziemy ten problem min-max. Zaczniemy od prostego rozwiązania, tzw. algorytmu naiwnego, który w następnych punktach tego wykładu nieco poprawimy. Przyjmiemy, że poszukiwany algorytm będzie używał zmiennej result, reprezentującej obiekt o dwóch atrybutach : min i max, na których zostaną zapamiętane wyniki.
Algorytm rozwiązujący problem min-max, interpretowany w strukturze, w której uniwersum jest zbiorem liniowo uporządkowanym, powinien być poprawny ze względu na następującą specyfikację:
wp = {n>0},
wk = {($ i0,i1 £ n) (e[i0]=result.min Ù e[i1] = result.max), ("j£ n)(e[i] £ result.min Ù result.max £ e[i])}.
Metoda
Najprostszym rozwiązaniem problemu min-max jest znalezienie najpierw elementu największego, a potem elementu najmniejszego przez dwukrotne przeglądanie ciągu. Jeżeli kolejny element jest większy od największego znalezionego do tej pory, to będzie on teraz elementem największym. Podobnie przy wyszukiwaniu elementu najmniejszego: porównujemy kolejne elementy ciągu ze znalezionym elementem najmniejszym i ewentualnie poprawiamy jego wartość, tak by wartością zmiennej result.min był element najmniejszy ze wszystkich przejrzanych do tej pory elementów. Algorytm może być zapisany następująco:
min_max1 |
{ | |
result.max := e[1]; i := 2; | ||
while (i £ n) do | ||
if not (e[i] £ result.max) then | ||
result.max := e[i] fi; | ||
i := i+1 | ||
od ; | // ("i £ n) e[i] £result. max Ù ($i £ n) result.max = e[i] | |
result.min :=e[1]; i := 2; | ||
while (i £ n) do | ||
if not (result.min £ e[i]) then | ||
result.min := e[i] fi; | ||
i := i+1 | ||
od; | //("i £ n) result.min £ e[i] Ù ($i £ n) result.min = e[i] | |
} |
Pełną analizę poprawności tego algorytmu pozostawiamy Czytelnikowi (por. wykład 01). Stwierdzimy teraz tylko, że niezależnie od typu elementów ciągu, jeżeli tylko relacja £ jest liniowym porządkiem, to po wykonaniu algorytmu prawdziwa będzie formuła
(" i£ n) (e[i] £ result.max Ù result.min £ e[i]).
Co więcej, wartościami zmiennych result.min i result.max są elementy rozważanego ciągu. Zatem
Twierdzenie 2.1
Algorytm min_max1 jest całkowicie poprawny w każdej strukturze danych Str, w której określony jest z liniowy porządek £, ze względu na warunek początkowy n>0 i warunek końcowy
("i£ n)(e[i] £ result.max Ù result.min £ e[i])Ù ($i £ n) result.max = e[i]Ù ($i £ n) result.min = e[i].
Zauważmy, że liczba wykonywanych operacji nie zależy od kolejności elementów w ciągu. Przyjmując n jako rozmiar danych i porównywanie jako operację dominującą, z łatwością oszacujemy czasowy koszt algorytmu min_max1: T(n) = 2n - 2.
Ten prosty algorytm nie jest jednak najlepszy. Po pierwsze, zamiast dwukrotnie przeglądać wszystkie elementy, raz w poszukiwaniu elementu największego i drugi raz w poszukiwaniu elementu najmniejszego, można wykonać tylko jedną pętlę.
{ | |
result.max :=e[1]; result.min :=e[1]; i := 2; | |
while (i £ n) do | |
if not (e[i] £ result.max) then result.max := e[i] fi; | |
if not (result.min £ e[i]) then result.min := e[i] fi; | |
i := i+1 | |
od | |
} |
Taki zabieg uprościł nieco strukturę algorytmu, ale nadal wykonujemy 2(n-1) porównań. Teraz jednak łatwo zauważymy, że jeśli jakiś element e[i] jest większy od aktualnego maksimum, to przecież ten element nie może być jednocześnie elementem mniejszym od aktualnego minimum i nie musimy wykonywać drugiego porównania. Zatem algorytm może przyjąć postać:
min_max2{ |
|||
result.max :=e[1]; result.min := e[1]; i := 2; | // result.min £ e[j], e[j] £ result.max dla j=1,...,(i-1) | ||
while (i £ n) do | |||
if not (e[i] £ result.max) then | |||
result.max := e[i] | // e[j]£result.max, dla j=1,...,i | ||
else | |||
if not (result.min £ e[i]) then | |||
result.min := e[i] fi | // result.min £ e[j], dla j=1,...,i | ||
fi; | |||
i := i+1; | // result.min £ e[j], e[j] £ result.max dla j=1,...,(i-1) | ||
od | |||
} |
Teraz jednak łatwo zauważyć, że koszt algorytmu zależy od tego, jakie są wzajemne zależności między elementami ciągu, czyli zależy od kolejności w jakiej zapisane są elementy w ciągu. W najgorszym razie algorytm wykona 2n-2 porównania. Stanie się tak np. dla dowolnego ciągu uporządkowanego malejąco. Dyskusję kosztu średniego algorytmu przedstawimy w następnym punkcie tego wykładu. Teraz zajmiemy się przeanalizowaniem jego poprawności.
Załóżmy, że wykonujemy algorytm min-max2 w strukturze danych STR, w której określona jest relacja porządku liniowego £, a elementy tablicy e[1:n] należą do uniwersum tej struktury. Po wykonaniu pierwszych trzech instrukcji przypisania, oba atrybuty zmiennej result są równe e[1], a i=2. Zatem prawdziwa jest własność
("j< i)(e[j] £ result.max Ù result.min £ e[j]).
Załóżmy teraz, że po pewnej liczbie iteracji pętli "while" spełniona jest formuła g(i) postaci result.min £ e[j], e[j] £ result.max dla j=1,...,(i-1). Prześledźmy co się stanie w następnej iteracji. Jeżeli rozważany element e[i] jest większy od wartości zmiennej max, to, po wykonaniu przypisania result.max := e[i], wartością zmiennej result.max jest największy spotkany do tej pory element. Jeżeli natomiast element e[i] nie jest większy od result.max, to stara wartość zmiennej max wskazuje element największy. Wykonane instrukcje nie zmieniły wartości zmiennej result.min, zatem po wykonaniu pierwszego porównania mamy
e[j] £ result.max dla j=1,...,(i-1),i, oraz result.min £ e[j] dla j=1,...,(i-1).
Drugie porównanie (to po "else") wykonujemy tylko wtedy, gdy rozważany element e[i] był niewiększy od aktualnego maksimum. Jeśli e[i] jest mniejsze od wartości aktualnego minimum, wykonujemy instrukcję przypisania result.min := e[i]. Oczywiście, ani sprawdzenie testu w "if", ani wykonanie tej instrukcji przypisania, nie zmieniają wartości result.max. Po wykonaniu pierwszej instrukcji pętli spełniona jest zatem własność g(i+1) postaci
e[j] £ result.max dla j=1,...,(i-1),i oraz result.min £ e[j] dla j=1,...,(i-1),i.
Wewnątrz pętli pozostała do wykonania instrukcja przypisania i := i+1; Mamy zatem starą wartość zmiennej i zastąpić wartością większą o 1. Zastępując w formule g(i+1), wyrażenie (i+1) przez i, otrzymujemy formułę g(i):
result.min £ e[j], e[j] £ result.max dla j=1,...,(i-1).
Udowodniliśmy więc, że jeśli na początku kolejnej iteracji g(i) jest spełnione, to jest też spełnione po wykonaniu tej iteracji. Formuła g(i) jest niezmiennikiem pętli w tym algorytmie. Zatem po wyjściu z pętli, co jest możliwe dopiero wtedy, gdy i= n+1, również spełniona jest formuła g(i). Na wyjściu prawdziwa jest więc formuła postaci:
(result.min £e[1] £ result.max) Ù (result.min £ e[2] £ result.max ) Ù ... Ù (result.min £e[i-1] £ result.max ) Ù (i = n+1),
a ponieważ wartościami zmiennych result.min, result.max są elementy danego ciągu, to prawdziwy jest warunek końcowy przyjętej specyfikacji algorytmu.
Jest oczywiste, że algorytm min_max2 zatrzymuje się dokładnie po n-1 iteracjach, ponieważ n jest liczbą naturalną, a wartościami zmiennej i, kontrolującej pętlę, są kolejne liczby naturalne zaczynając od 2. Ostatecznie
Twierdzenie 2.2 W każdej strukturze danych STR, w której określony jest z liniowy porządek £, algorytm min_max2 zatrzymuje się dla dowolnego niepustego ciągu, a wartością zmiennej result jest para złożona z elementu największego i najmniejszego tego ciągu, tzn. algorytm min_max2 jest, ze względu na przyjętą specyfikację, całkowicie poprawnym rozwiązaniem problemu min-max.
Pytanie 2: Czy algorytm min_max2 działa poprawnie i jaki będzie wynik
algorytmu, jeśli n=0?
Zobacz odpowiedź
« poprzedni punkt | następny punkt » |