Strategia "dziel i zwyciężaj" W tej metodzie problem dzielony jest na kilka mniejszych podproblemów podobnych do początkowego problemu. Problemy te rozwiązywane są rekurencyjnie, a następnie rozwiązania wszystkich podproblemów są łączone w celu utworzenia rozwiązania całego problemu. W strategii dziel i zwyciężaj każdy poziom rekurencji składa się z następujących trzech etapów: Dziel: Dzielimy problem na podproblemy. Zwyciężaj: Rozwiązujemy podproblemy rekurencyjnie, chyba że są one małego rozmiaru i już nie wymagają zastosowania rekursji używamy wtedy bezpośrednich metod. Połącz: Łączymy rozwiązania podproblemów, aby otrzymać rozwiązanie całego problemu. Wyszukiwanie binarne Wyszukiwanie binarne znajduje element x w posortowanej (w porządku niemalejącym) tablicy poprzez porównanie w pierwszej kolejności x ze środkowym elementem tablicy. Jeżeli są one sobie równe to algorytm kończy działanie. Jeżeli nie, to tablica jest dzielona na dwie podtablice, z których jedna zawiera wszystkie elementy znajdujące się na lewo od elementu środkowego, a druga wszystkie elementy znajdujące się na prawo od tego elementu. Jeżeli x jest mniejsze od elementu środkowego, to ta sama procedura zostaje zastosowana względem lewej podtablicy. W przeciwnym wypadku względem prawej podtablicy. Oznacza to, że x jest porównywane ze środkowym elementem odpowiedniej podtablicy. Jeżeli są sobie równe to algorytm kończy działanie. Jeżeli nie, podtablica jest dzielona na dwie części. Procedura jest powtarzana do momentu znalezienia x lub wykazania, że x nie występuje w tablicy. Kolejne etapy postępowanie: Jeżeli x jest równe elementowi środkowemu, kończymy. W przeciwnym razie: Dzielimy tablicę na dwie podtablice o rozmiarze mniej więcej dwa razy mniejszym od oryginalnej. Jeżeli x jest mniejsze od elementu środkowego, wybieramy lewą podtablicę. Jeżeli x jest większe od elementu środkowego, wybieramy prawą podtablicę. Zwyciężamy (rozwiązujemy) podtablicę poprzez określenie, czy x do niej należy. O ile podtablica nie posiada dostatecznie małych rozmiarów, należy wykorzystać rekurencję. Otrzymujemy rozwiązanie problemu tablicy na podstawie rozwiązania problemu podtablicy.
Przykład Wyszukiwanie binarne dla x=18 oraz poniższej 13 elementowej tablicy liczb. 10 12 13 14 18 20 25 27 30 35 40 45 47 Wybieramy lewą podtablicę, ponieważ x<25 Porównujemy x z 25 10 12 13 14 18 20 Porównujemy x z 13 Wybieramy prawą podtablicę, ponieważ x>13 14 18 20 Porównujemy x z 18 Określamy, że x występuje, ponieważ x=18 int binarne( int tab[],int poczatek, int koniec, int x){ if (poczatek > koniec) return -1; else { int srodek=(poczatek+koniec)/2; if (x == tab[srodek]) return srodek; else if (x < tab[srodek]) return binarne(tab,0,srodek-1,x); else return binarne(tab,srodek+1,koniec,x);
Porównanie wyszukiwania sekwencyjnego z wyszukiwaniem binarnym Jeżeli tablica zawiera 32 elementy i x nie występuje w tablicy, to algorytm sekwencyjny porówna x ze wszystkimi elementami zanim określi, że element ten nie występuje w tablicy. Zatem wyszukiwanie sekwencyjne dokonuje n porównań w celu określenia, że x nie występuje w tablicy o rozmiarze n. Oznacza to, że jeżeli x występuje w tablicy, liczba porównań jest nie większa niż n. Natomiast algorytm wyszukiwania binarnego w tym przypadku wykona tylko 6 porównań. tab[16] tab[24] tab[28] tab[30] tab[31] tab[32] Zauważmy, że. Zatem jest to maksymalna liczba porównań wykonywanych przez algorytm wyszukiwania binarnego. Załóżmy, że podwojono rozmiar tablicy tak, że zawiera teraz 64 elementy. Algorytm wykonuje tylko o jedno porównanie więcej, ponieważ pierwsze porównanie podzieli tablicę na pół dając w rezultacie podtablicę o rozmiarze 32. Zatem jeśli x jest większe od wszystkich elementów występujących w tablicy o rozmiarze 64 algorytm wykona 7 porównań,. Ogólnie za każdym razem gdy podwoimy rozmiar tablicy dodajemy tylko jedno porównanie. Dlatego, jeśli n jest potęgą liczby 2, a x jest większe od wszystkich elementów występujących w tablic o rozmiarze n to liczba porównań wykonywanych przez algorytm wynosi. rozmiar tablicy algorytm sekwencyjny algorytm binarny 128 128 8 1024 1024 11 1048576 1048576 21 4294967296 4294967296 33 Zatem T(n)= Jeśli T(n) jest potęgą liczby 2 to T(n)= T(n)=. +1. Natomiast jeśli n nie jest potęgą liczby 2 to Sortowanie przez scalanie W tym algorytmie również stosujemy metodę dziel i zwyciężaj. Dziel: Dzielimy n-elementowy ciąg na dwa podciągi po n/2 elementów każdy. Zwyciężaj: Sortujemy otrzymane podciągi, używając rekurencyjnie sortowania przez scalanie. Połącz: Łączymy posortowane podciągi w jeden posortowany ciąg.
Przykład 12 9 11 6 9 12 3 9 12 7 14 6 11 2 6 11 10 5 3 7 9 12 14 2 5 6 10 11 2 3 5 6 7 9 10 11 12 14 Podstawową operacją algorytmu jest scalanie dwóch posortowanych ciągów dokonywane w kroku "połącz". W celu dokonania scalania korzysta się z pomocniczej funkcji void Scalaj (int tab[], int początek, int srodek, int koniec), która łączy dwa posortowane ciągi zgodnie z zasadą: Dopóki jeden z ciągów nie jest pusty to porównuj pierwsze elementy obu ciągów i mniejszy wstaw do ciągu wynikowego. Pozostałe elementy niepustego ciągu dołącz do ciągu wynikowego.
Przykład scalenia dwóch tablic T1 i T2 do jednej tablicy Tab. T1 T2 Tab 10 12 20 27 13 15 22 25 10 10 12 20 27 13 15 22 25 10 12 10 12 20 27 13 15 22 25 10 12 13 10 12 20 27 13 15 22 25 10 12 15 10 12 20 27 13 15 22 25 10 12 15 20 10 12 20 27 13 15 22 25 10 12 15 20 22 10 12 20 27 13 15 22 25 10 12 15 20 22 25 10 12 20 27 13 15 22 25 10 12 15 20 22 25 27 Z funkcji Scalaj korzysta się w algorytmie sortowania przez scalanie. void merge_sort(int tablica[], int poczatek, int koniec){ int srodek; if (poczatek < koniec){ srodek = (poczatek + koniec) / 2; merge_sort(tablica, poczatek, srodek); merge_sort(tablica, srodek + 1, koniec); Scalaj(tablica, poczatek, srodek, koniec); Wyznaczmy teraz złożoność obliczeniową algorytmu czyli T(n). Jeżeli tablica jest jednoelementowa to jest spełniony warunek końcowy i nie jest wykonywane żadne scalenie. Zatem T(1)=0 (0 operacji). Gdy ciąg nie jest jednoelementowy: dziel - wyznaczenie środka - O(1), zwyciężaj - rozwiązanie dwóch problemów o rozmiarze -2T, scal - liczba porównań zależy od wartości elementów - Ɵ(n), Zatem
Sortowanie szybkie - quicksort Zaimplementuj algorytm sortowania szybkiego QuickSort oraz określ jego złożoność obliczeniową. Sortowanie szybkie przypomina sortowanie przez scalanie o tyle, że cały proces polega na dzieleniu tablicy na dwie części, a następnie rekurencyjnym sortowaniu każdej z nich. Jednak w przypadku sortowaniu szybkiego tablica jest dzielona przez umieszczenie wszystkich elementów mniejszych niż pewien element odniesienia przed tym elementem oraz wszystkich elementów które są od niego większe za nim. Elementem odniesienia (ang. pivot ) może być dowolny element i dla wygody można użyć elementu pierwszego. Przykład (podtablice są umieszczone w ramkach, natomiast punkty odniesienia są bez ramek) 15 22 13 27 12 10 20 25 15 10 13 12 22 27 20 25 10 13 12 15 20 22 27 25 10 12 13 Algorytm sortowania szybkiego 15 20 22 25 27 void quicksort(int tab[],int poczatek, int koniec){ int punkt_podzialu; if(poczatek<koniec){ podzial(tab,poczatek,koniec,punkt_podzialu); quicksort(tab,poczatek,punkt_podzialu-1); quicksort(tab,punkt_podzialu+1,koniec); return;
W powyższym algorytmie występuje funkcja podzial, która dokonuje podziału tablicy. Funkcja ta działa poprzez sprawdzanie każdego elementu w tablicy po kolei. Kiedy zostanie znaleziony element o wartości mniejszej niż element odniesienia, zostaje on przeniesiony na lewą stronę tablicy. Poniższa tabela pokazuje sposób działania tej funkcji (elementy porównywane są pogrubione, natomiast elementy zamienione są podkreślone) i j tab[0] tab[1] tab[2] tab[3] tab[4] tab[5] tab[6] tab[7] - - 15 22 13 27 12 10 20 25 1 0 15 22 13 27 12 10 20 25 2 1 15 22 13 27 12 10 20 25 3 1 15 13 22 27 12 10 20 25 4 2 15 13 22 27 12 10 20 25 5 3 15 13 12 27 22 10 20 25 6 3 15 13 12 10 22 27 20 25 7 3 15 13 12 10 22 27 20 25-3 13 12 15 22 27 20 25