« poprzedni punkt | następny punkt » |
Rozważmy następujący problem. Na początku semestru dziekanat pewnej Uczelni przygotował alfabetycznie uporządkowane listy studentów każdej z grup drugiego roku. Na koniec semestru potrzebny jest jednak protokół egzaminacyjny zawierający nazwiska wszystkich studentów drugiego roku. Jak stworzyć nową, alfabetycznie uporządkowaną listę, tak by nikogo nie pominąć i wykonać jak najmniej pracy?
Sformalizowana wersja tego problemu, to problem scalania:
Dane są dwa ciągi uporządkowane a[1],..., a[n] oraz b[1],..., b[n]. Zbudować ciąg uporządkowany o n+m elementach e[1], ..., e[n+m], złożony z wszystkich elementów obu ciągów.
Przykład 4.1
(a) Niech ciąg a składa się ze słów bit, kit, lit, mit, wit, a ciąg b ze słów kok, lok, rok, sok, tok. W wyniku operacji scalania otrzymamy ciąg bit, kit, kok, lit, lok, mit, rok, sok, tok, wit.
(b) Jeżeli a = (2,5,6,8,9), b = (0,1,3,4,7), to w wyniku scalania otrzymamy ciąg e = (0,1,2,3,4,5,6,7,8,9).J
Specyfikacja algorytmu:
wp = { a[1] £ ...£ a[n], b[1]£ ...£ b[m]}
wk = {e[1] £ e[2] £... £ e[n+m], ciąg e jest permutacją elementów a[1],...,a[n], b[1],...,b[m]}
Każdy algorytm, całkowicie poprawny ze względu na tę specyfikację będziemy
uznawali za rozwiązanie problemu scalania.
Idea rozwiązania:
Wyobraźmy sobie bramkę, przed którą stoją dwie uporządkowane kolejki (tzn. ciąg a i ciąg b). W każdym kroku, przez bramkę przechodzi jeden element: mniejszy z tych, które znajdują się aktualnie na początku kolejek. Jego kolejka przesuwa się tym samym o jedną pozycję do przodu. Większy pozostaje przed bramką tak długo, aż na początku drugiej kolejki pojawi się element od niego większy, lub druga kolejka będzie pusta.
Przykład 4.2
Niech ciągi a i b zawierają odpowiednio liczby 2,3,5,6,8,9 oraz 0,1,4,7. W kolejnych wierszach poniższej tabeli zapisano kolejne kroki algorytmu scalania. Kolorem czerwonym zaznaczono elementy porównywane w danym kroku. Element mniejszy z porównywanych jest w zapisany w ciągu wynikowym.
ciąg a | ciąg b | ciąg wynikowy | |||||||||||||||||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | |||||||||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | ||||||||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | 2 | |||||||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | 2 | 3 | ||||||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | 2 | 3 | 4 | |||||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | 2 | 3 | 4 | 5 | ||||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ||||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |||
2 | 3 | 5 | 6 | 8 | 9 | 0 | 1 | 4 | 7 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
W chwili, gdy porównaliśmy 8 i 7, i siódemka została zapisana w ciągu wynikowym, elementy 8,9 z pierwszego ciągu możemy, bez wykonywania porównań, dopisać na końcu ciągu wynikowego. J
Algorytm
Niech a będzie ciągiem o długości n, a b ciągiem o długości m. W opisanym poniżej algorytmie Merge, oba ciągi wejściowe a i b zostały rozszerzone o jeden element, oznaczony tutaj + ¥. Zakładamy, że jest on większy od wszystkich innych elementów obu ciągów. Taki zabieg pozwoli nam uprościć nieco strukturę algorytmu. Zadbamy jednak o to, by ciąg e otrzymany w wyniku miał dokładnie n+m elementów.
Merge { | |||
a[n+1] := + ¥; b[m+1]:= + ¥; | |||
i := 1; j := 1; k :=1; | |||
while (k £ (n+m)) do | // e[1] £ e[2] £... £ e[k-1], k = i+j-1 | ||
if (a[i] £ b[j]) then | |||
e[k] := a[i]; i := i+1 | // e[1] £ ...£ e[k-1] £ e[k] oraz e[k] = a[i-1], k = i+j-2 | ||
else | |||
e[k] := b[j]; j := j+1 | //e[1] £ ... £ e[k-1] £ e[k] oraz e[k] = b[j-1] , k = i+j-2 | ||
fi; | |||
k := k+1 | // e[1] £...£ e[k-1] k= i+j-1 | ||
od; | |||
return e | // e[1] £...£ e[k-1], k= n+m+1 | ||
} |
Poprawność algorytmu
Zauważmy, że elementy dodatkowe, dopisane do ciągów a i b, nie mogą się znaleźć w ciągu wynikowym, gdyż pętla jest wykonywana tylko (n+m) razy. Gdyby, w którymś momencie i= n+1, tzn. a[i] =+ ¥ , to przy porównywaniu tego elementu z b[j] dla j<n+1, wygra b[j] i to b[j] zostanie zapisane w ciągu wynikowym. Analogicznie w przypadku, gdy j=m+1. Jeśli zaś oba wskaźniki i, j wskazują element specjalny, to znaczy, że wszystkie poprzednie elementy już zostały wpisane do tablicy wynikowej, a więc k musi być w tym momencie równe n+m+1. Warunek pętli "while" nie będzie spełniony i zakończymy wykonywanie algorytmu.
Jest oczywiste, że algorytm zatrzymuje się dla dowolnych danych, gdyż zmienna kontrolująca pętlę zmienia się od 1 do n+m. Trzeba jeszcze pokazać, że otrzymany wynik jest ciągiem uporządkowanym złożonym z elementów danych ciągów.
Przed pierwszym wejściem do pętli "while" jest trywialnie spełniony warunek e[1] £ e[2] £... £ e[k-1]. Załóżmy, że rozpoczynamy pewną iterację pętli "while" i spełnione są własności (*):
e[1] £ e[2] £... £ e[k-1], i £ n+1, j £ m+1, k = (i + j -1), k £ (n+m+1), (*)
(ciąg e[1],...,e[k-1] jest permutacją ciągu a[1],...,a[i-1],b[1],...,b[j-1] ).
Formuły te mówią, że k-1 elementów w ciągu e tworzy ciąg uporządkowany, a ponadto i-1 elementów ciągu a oraz j-1 elementów ciągu b zostało już zapisanych na pozycjach od 1 do k-1 ciągu e.
Po wykonaniu instrukcji warunkowej "if (a[i] £ b[j])..." , na miejscu k-tym w ciągu e pojawi się mniejszy z elementów a[i], b[j] oraz, albo i, albo j wzrośnie o jeden. Ponieważ ciągi a i b są uporządkowane niemalejąco zatem wstawiony element e[k] jest niemniejszy od wszystkich elementów, które znajdują się na wcześniejszych pozycjach w ciągu e. Mamy więc w tablicy e aż k elementów uporządkowanych, oraz i+j-1 = k+1.
e[1] £... £ e[k-1] £ e[k], i £ n+1, j £ m+1, (k+1) = (i + j -1), k £ (n+m+1),
(ciąg e[1],...,e[k] jest permutacją elementów ciągu a[1],...,a[i-1],b[1],...,b[j-1] ).
Po wykonaniu instrukcji przypisania "k:=k+1;" ponownie spełnione są własności (*). Wykazaliśmy tym samym, że jest to niezmiennik pętli. Z twierdzenia o niezmienniku (por. wykład I, p.3), ta sama formuła jest spełniona w chwili wyjścia z pętli. Wtedy jednak k = n+m+1 i wszystkie elementy, zarówno ciągu a, jak i b, znalazły się już w ciągu e oraz e[1] £... £ e[n+m]. Spełniony jest zatem warunek końcowy specyfikacji.
Twierdzenie 4.1 Algorytm Merge jest całkowicie poprawnym rozwiązaniem problemu scalania za względu na specyfikację,
wp = {a[1] £ ... £ a[n], b[1] £ ... £ b[m], n>0, m>0}
wk = {ciąg e[1],...,e[n+m] jest uporządkowaną niemalejąco permutacją elementów a[1],..., a[n], b[1],...,b[m] }
w każdej strukturze danych z liniowym porządkiem £.
Koszt algorytmu
Ponieważ w pętli "while" wykonujemy w każdej iteracji tylko jedno porównanie, a liczba iteracji wynosi dokładnie n+m, zatem koszt algorytmu jest liniowy w stosunku do długości ciągów scalanych i wynosi
T(n) = Q(n+m).
Pytanie 4: Ile porównań elementów musi wykonać algorytm Merge, jeżeli wiadomo, że a[n]<b[1]? (Nie liczymy porównań z elementem specjalnym + ¥.)
« poprzedni punkt | następny punkt » |