DobSort Úvod do programování Michal Krátký 1,Jiří Dvorský 1 1 Katedra informatiky VŠB Technická univerzita Ostrava Úvod do programování, 2004/2005 V roce 1980 navrhl Dobosiewicz variantu (tzv. DobSort), která se ukázala být neočekávaně efektivní. Idea je následující: zvolíme posloupnost kroků i = t, t 1,,1, h i+1 > h i, h 1 = 1, t > 1 délky t = O(log n). V i-tém chodu třídíme stejně jako při bublinkovém třídění (bez jakýchkoliv vylepšení) prvky posloupnosti ležící ve vzdálenosti h i, pro i = t, t 1,,2. Po i 1 chodech se třídění ukončí bublinkovém třídění. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 1/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 2/36 DobSort Implementace 1/3 public class DobSort int mdata[]; public DobSort(int size) mdata = new int[size]; for (int i = 0 ; i < size ; i++) mdata[i] = (int)(math.random() * size); c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 3/36 DobSort Implementace 2/3 public void Sort() int i, j, h, t; for(h = 1; h <= mdata.length; h=3*h+1); do h = h / 3; j = 0; while (j < mdata.length-h) if (mdata[j] > mdata[j+h]) t = mdata[j]; mdata[j] = mdata[j + h]; mdata[j + h] = t; j++; while (h!= 1); c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 4/36 DobSort Implementace 3/3 boolean finishf = true; while (finishf) finishf = false; for(i = 0; i < mdata.length-1; i++) if (mdata[i] > mdata[i+1])) t = mdata[i]; mdata[i] = mdata[i+1]; mdata[i+1] = t; finishf = true; c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 5/36 DobSort - Příklad 12, 13, 11, 4, 7, 0, 7, 14, 14, 12, 0, 10, 14, 8, 7 8, 13, 11, 4, 7, 0, 7, 14, 14, 12, 0, 10, 14, 12, 7//h=13,j=0 8, 7, 11, 4, 7, 0, 7, 14, 14, 12, 0, 10, 14, 12, 13 //h=13,j=1 //h=4 7, 7, 11, 4, 8, 0,7,14,14,12,0,10,14,12,13//j=0 7, 0, 11, 4, 8, 7, 7,14,14,12,0,10,14,12,13//j=1 0, 7, 7,4,8,7,0,10,14,12,11,14,14,12,13//h=1,j=0 0, 7, 4, 7, 8,7,0,10,14,12,11,14,14,12,13//h=1,j=2 0, 7, 4, 7, 7, 0, 8, 10, 12, 11, 14, 14, 12, 13, 14 // BubleSort 0, 0, 4, 7, 7, 7, 8, 10, 11, 12, 12, 13, 14, 14, 14 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 6/36
DobSort Výsledky ukázaly, že pro n 10000 byl algoritmus prakticky stejně rychlý jako Quicksort, a přibližně dvakrát rychlejší než Shellsort. U tohoto algoritmu však zatím nebyla provedena podrobnější analýza složitosti. Při třídění rozdělováním se v souladu s metodou divide-et-impera (rozděl a panuj) nejprve tříděná množina rozdělí na dvě disjunktní podmnožiny podle možnosti přibližně stejné velikosti tak, že všechny prvky jedné množiny jsou menší než prvky druhé množiny. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 7/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 8/36 QuickSort Implementace 1/3 Každá množina se potom rekurzivně dotřídí. Závěrečný syntetizační krok je triviální spočívá v konkatenaci setříděných posloupností. Rozdělení množiny v prvním kroku se realizuje tak, že se zvolí jeden prvek z množiny zvaný pivot a jedna podmnožina potom obsahuje všechny prvky menší než pivot, adruhá všechny ostatní. Tento na první pohled přirozený způsob třídění objevil v roce 1962 C. A. R. Hoare a nazval jej QuickSort. public class QuickSort int mdata[]; public QuickSort(int size) mdata = new int[size]; for (int i = 0 ; i < size ; i++) mdata[i] = (int)(math.random() * size); c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 9/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 10/36 QuickSort Implementace 2/3 public void Sort() QSort(0, mdata.length-1); private void QSort(int l, int r) int i, j, t, v; i = l; j = r; v = mdata[(l+r)/2]; do while (mdata[i] < v) i += 1; c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 11/36 QuickSort Implementace 3/3 while (v < mdata[j]) j -= 1; if (i <= j) t = mdata[i]; mdata[i] = mdata[j]; mdata[j] = t; i++; j--; while (i <= j); if (l < j) QSort(l, j); if (i < r) QSort(i, r); c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 12/36
QuickSort - Příklad Nerekurzivní QuickSort 7, 7, 2, 7, 11, 2, 4, 8, 14, 10, 8, 2, 7, 3, 4 7, 7,2,7,11,2,4,8, 14, 10, 8, 2, 7, 3, 4 // v=8, l=0, r=14 7, 7, 2, 7, 4, 2, 4, 3, 7, 2, 8,10, 14, 8, 11 // i=11, j=9 7, 7,2,7,4, 2,4,3,7,2, 8, 10, 14, 8, 11 // v=4, l=0, r=9 2, 3, 2, 4, 2, 4,7, 7, 7, 7, 8, 10, 14, 8, 11 // i=5, j=4 2, 3,2, 4,2, 4, 7, 7, 7, 7, 8, 10, 14, 8, 11 // v=2, l=0, r=4 2, 2, 3, 4, 2, 4, 7, 7, 7, 7, 8, 10, 14, 8, 11 // i=2, j=1 2, 2, 2, 3, 4, 4, 7, 7, 7, 7, 8, 8, 10, 11, 14 QuickSort se nejčastěji uvádí v rekurzivní variantě. Lze ovšem napsat nerekurzivní (iterační) verzi, transformací na iteraci s pomocnou pamětí. V průběhu iterace dělíme pole na dva úseky, ale v následujícím okamžiku jsme schopni zpracovat jen jednu část pole a druhou bude nutné uložit do pomocné paměti. Je zřejmé, že úseky by se měly z pomocné paměti vybírat v opačném pořadí, než tam byly uloženy. Z těchto úvah plyne, že pomocná pamět se chová jako zásobník. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 13/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 14/36 Nerekurzivní QuickSort - Velikost zásobníku QuickSort - Porovnání Nejhorší případ nastane, pokud pravý úsek, odkládaný do zásobníku, bude tvořen jediným prvkem. Potom rozsah zásobníku bude n, což je nepřijatelné. Zlepšení dosáhneme tím, že do zásobníku budeme odkládat delší úsek tříděného pole a pokračujeme úsekem kratším. V tomto případě bude velikost zásobníku ohraničena log n. Praktické testy ukázaly, že QuickSort je velice rychlý při třídění rozsáhlých polí, ale zaostává oproti přirozeným algoritmům třídění (InsertSort, SelectSort) při třídění malých polí. Do jistého počtu prvků má QuickSort větší režii (třídí pomaleji) než např. InsertSort. Tato hranice byla experimentálně stanovena na asi 12 prvků. Vyplatilo by se tedy QuickSortem třídit rozsáhlé pole, ale jakmile úseky na než se pole dělí budou kratší než zvolená mez (řekněme zmíněných 12 prvků), tento krátký úsek dotřídit InsertSortem. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 15/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 16/36 QuickSort - Analýza QuickSort vyžaduje přibližně 2n log n porovnání v průměrném případě. Poznamenejme, že 2n ln n 1, 38n log n, tudíž průměrný počet porovnání je pouze o 38% vyšší než počet porovnání v nejlepším případě. HeapSort zlepšuje algoritmus třídění výběrem. Spočívá v nalezení efektivnějšího způsobu výběru i-tého největšího prvku. Je zřejmé, že pro i = 1 (pro nalezení maximálního prvku) vždy potřebujeme n 1 porovnání. Pro další kroky i > 1 je však možné zapamatovat si jistým způsobem výsledky předcházejících porovnání, a později je využít. Tomuto účelu nejlépe vyhovuje datová struktura halda. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 17/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 18/36
Halda (Heap) Halda Halda reprezentující posloupnost S mohutnosti n, je úplný binární strom výšky h 1sn vrcholy, a následujícími vlastnostmi: všechny listy se nachází ve vzdálenosti h nebo h 1od kořene; všechny listy na úrovni h jsou vlevo od listů na úrovni h 1; každému vrcholu tohoto stromu je přiřazen jeden prvek posloupnosti S tak, že všem jeho potomkům jsou přiřazeny menší prvky. Halda se dá výhodně reprezentovat v poli tak, že do pole postupně zapíšeme všechny prvky přiřazené jednotlivým poschodím, zleva doprava, od kořene směrem k listům. Při této reprezentaci zřejmě platí, že prvek ležící na i-té pozici má levého potomka na pozici 2i, pravého potomka na pozici 2i + 1. Z podmínek haldy potom plyne, že maximální prvek je vždy v kořenu haldy to znamená na první pozici pole. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 19/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 20/36 HeapSort Implementace 1/3 public class HeapSort int mdata[]; public HeapSort(int size) mdata = new int[size]; for (int i = 0 ; i < size ; i++) mdata[i] = (int)(math.random() * size); c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 21/36 HeapSort Implementace 2/3 private void DownHeap(int a[], int k, int l) int j, v = a[k]; while (k < l/2) j = k + k; if (j < (l-1)) if (a[j] < a[j+1]) j += 1; if (v >= a[j]) break; a[k] = a[j]; k = j; ; a[k] = v; c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 22/36 HeapSort Implementace 3/3 HeapSort - Příklad 1/2 public void HSort() int i, t; for(i = mdata.length/2; i >= 0; i--) DownHeap(mData, i, mdata.length); i = mdata.length-1; do t = mdata[0]; mdata[0] = mdata[i]; mdata[i] = t; i -= 1; DownHeap(mData, 0, i+1); while (i > 0); c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 23/36 7, 12, 6, 12, 9, 12, 3, 4, 8, 4, 4, 5, 13, 10, 10 7, 12, 6, 12, 9, 12, 3, 4, 8, 4, 4, 5, 13, 10, 10 // k = 7, l = 15, v = 4 k=6,l=15,v=3 7, 12, 6, 12, 9, 12, 3, 4,8,4,4,5,13,10,10 7, 12, 6, 12, 9, 12, 13, 4,8,4,4,5,3, 10,10//k=6,j=12 k=5,l=15,v=12 7, 12, 6, 12, 9, 12, 13, 4, 8, 4, 4, 5, 3, 10, 10 k=3,l=15,v=12 7, 12, 6, 12, 9,12,13, 4,8,4,4,5,3,10,10 7, 12, 6, 13, 9,12,12, 4,8,4,4,5,3,10,10//k=3,j=6 13, 12, 9, 12, 8, 6, 12, 4, 7, 4, 4, 5, 3, 10, 10 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 24/36
HeapSort - Příklad 2/2 HeapSort - Analýza 1/2 k=0,l=14,v=10 10, 12, 9, 12, 8, 6, 12, 4, 7, 4, 4, 5, 3, 10, 13 12, 12, 9, 12, 8, 6, 12, 4, 7, 4, 4, 5, 3, 10, 13 //k=0,j=1 12, 12, 9, 12, 8, 6, 12, 4, 7, 4, 4, 5, 3, 10, 13 //k=1,j=3 12, 12, 9, 12, 8, 6, 12, 4, 7, 4, 4, 5, 3, 10, 13 //k=3,j=6 12, 12, 9, 12, 8, 6, 10, 4,7,4,4,5,3,10,13 k=0,l=13,v=10 10, 12, 9, 12, 8, 6, 10, 4, 7, 4, 4, 5, 3, 12, 13 12, 12, 9, 12, 8, 6, 10, 4, 7, 4, 4, 5, 3, 12, 13 //k=0,j=1 12, 12, 9, 12, 8, 6, 10, 4, 7, 4, 4, 5, 3, 12, 13 //k=1,j=3 12, 12, 9, 10, 8, 6, 10, 4, 7, 4, 4, 5, 3, 12, 13 3, 4, 4, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 12, 13 Všechny základní operace s haldou vložení, zrušení, výměna (pomocná funkce DownHeap) prvků vyžadují méně než 2 log n porovnání. Všechny tyto operace vyžadují průchod haldou od jejího kořene k listům, což představuje ne více než log n uzlů pro haldu s n prvky. Násobící faktor 2 pochází právě od funkce DownHeap, která ve svém cyklu provádí dvě porovnání. Konstrukci haldy zdola nahoru lze provést v lineárním čase. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 25/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 26/36 HeapSort - Analýza 2/2 Třídění slučováním (MergeSort) Ke tvrzení nás opravňuje fakt, že většina zpracovávaných hald je velice malá. Například k vybudování haldy ze 127 prvků, je funkce DownHeap vyvolána 64 krát pro haldu velikosti 1, 32 krát pro haldu velikosti 3, 16 krát pro haldu velikosti 7, 8 krát po 15 prvcích, 4 krát pro haldu o 31 prvcích, dvakrát pro haldu velikosti 63 prvků a naposledy pro haldu o 127 prvcích. Dohromady 64 0 + 32 1 + 16 2 + 8 3 + 4 4 + 2 5 + 1 6 = 120 vyvolání v nejhorším případě. HeapSort má časovou složitost O(n log n). Třídění slučováním představuje efektivní třídící techniku vpřípadě sekvenčního přístupu k tříděným datům. Na rozdíl od již uvedených metod vnitřního třídění se složitostí O(n 2 ), třídění slučováním má časovou složitost O(n log n). Vpřípadě, že máme dostatek místa pro uložení dvou setříděných posloupností položek, je vhodné použít MergeSort. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 27/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 28/36 Princip slučování 1/3 Princip slučování 2/3 Nejprve si ukážeme princip slučování. Předpokládejme, že chceme spojit dvě posloupnosti, přičemž každá z nich je již sama o sobě setříděná. Výsledkem má být posloupnost, která bude obsahovat všechny prvky setříděné dohromady. Mějme tedy dvě vzestupně setříděné posloupnosti A a B. Chceme vytvořit vzestupně setříděnou posloupnost C. Načteme prvek a z posloupnosti A a prvek b z posloupnosti B. Pokud a b do C zapíšeme a a znovu načteme nový prvek z posloupnosti A. Jinak do C zapíšeme hodnotu b anačteme nový prvek z posloupnosti B. Popsané kroky opakujeme tak dlouho, dokud nevyčerpáme všechny prvky z jedné posloupnosti. Zbylé prvky z neprázdné posloupnosti zapíšeme do C. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 29/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 30/36
Princip slučování 3/3 Třídění pomocí slučování Základní idea třídění pomocí slučováním spočívá v dělení původní posloupnosti na dvě části (nejlépe o polovičním počtu prvků), jejich setřídění a poté použití metody slučování. Výsledkem je setříděná posloupnost o stejném počtu prvků jakobylvpůvodní posloupnosti. Rekurzivně pak obě posloupnosti setřídíme rozdělením na dvě části a slučováním setříděných posloupností. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 31/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 32/36 Třídění pomocí slučování Třídění pomocí slučování Rozdělování posloupnosti na části skončí pokud bude posloupnost setříděná. Nejmenší setříděnou posloupností je posloupnost jednoprvková. Dělení se obvykle ukončuje při takovém počtu prvků, který je možné setřídit některou metodou vnitřního třídění. Každé rekurentní volání znamená rozdělení posloupnosti na dvě části a návrat zpět znamená slučování rozdělených (již setříděných) částí do jedné pomocí metody slučování. c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 33/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 34/36 Třídění pomocí slučování - Analýza MergeSort - Sekvenčního zpracování dat Počet přesunů při třídění slučováním je určen počtem fází a počtem prvků na vstupu. Počet fází je roven počtu dělení vstupní posloupnosti na nejmenší část, která je vstupem pro fázi slučování. M = n log n Třídění slučováním patří mezi metody s časovou složitostí O(n log 2 n). c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 35/36 c 2005 Michal Krátký, Jiří Dvorský Úvod do programování 36/36