Algoritmy a datové struktury pro začátečníky: třídění. Merge-Sort

Sloučit řazení

(Viz strana 430 od F. M. Carrana a J. J. Pritcharda)

Dva důležité třídicí algoritmy rozděl a panuj, slučovací třídění a rychlé třídění, mají elegantní rekurzivní implementaci a jsou extrémně efektivní. V této části se podíváme na slučovací třídicí pole, ale kapitola 14 ukáže, že tento algoritmus lze zobecnit na externí soubory. Při formulování algoritmu použijeme zápis pro segment pole theArray.

Algoritmus řazení sloučení je rekurzivní. Jeho účinnost nezávisí na pořadí prvků v původním poli. Řekněme, že jsme pole rozdělili na polovinu, rekurzivně uspořádali obě poloviny a pak je spojili do jediného celku, jak je znázorněno na obr. 9.8. Obrázek ukazuje, že části pole<1, 4, 8>A<2, 3>sloučeny do pole<1, 2, 3, 4, 8>. Při slučování se prvky umístěné v různých částech pole porovnávají ve dvojicích mezi sebou a menší prvek je odeslán do dočasného pole. Tento proces pokračuje, dokud není použita jedna ze dvou částí pole. Nyní stačí zkopírovat zbývající prvky do dočasného pole. Nakonec se obsah dočasného pole zkopíruje zpět do původního pole.

Rýže. 9.8. Sloučit řazení pomocí pomocného pole

Ačkoli výsledkem sloučení je uspořádané pole, zůstává nejasné, jak se řazení provádí v předchozích krocích. Slučovací řazení se provádí rekurzivně. Jeho pseudokód vypadá takto:

mergesort(inout theArray:ItemArray,

na prvním: celé číslo, na posledním: celé číslo)

// Objednává segment Array,

// 1) řazení první poloviny pole;

// 2) řazení druhé poloviny pole;

// 3) spojení dvou uspořádaných polovin pole.

Pokud (nejprve< last)

mid = (první + poslední)/2 // Určete střed

// Seřadit segment theArray mergesort(theArray, first, mid)

// Seřadit segment theArray mergesort(theArray, mid + 1, last)

// Zkombinujte uspořádané segmenty pole

// a theArray

sloučit (pole, první, střední, poslední)

) // Konec příkazu if

// Pokud je první >= poslední, operace jsou dokončeny

Je jasné, že hlavní operace tohoto algoritmu se provádějí během fáze slučování, a přesto proč výsledkem je uspořádané pole? Rekurzivní volání pokračují v rozdělování částí pole na polovinu, dokud nezůstane pouze jeden prvek. Je zřejmé, že pole sestávající z jednoho prvku je uspořádáno. Algoritmus pak kombinuje fragmenty pole, dokud se nevytvoří jediné uspořádané pole. Rekurzivní volání funkcí mergesort a výsledky fúzí jsou znázorněny na obr. 9.9 na příkladu pole sestávajícího ze šesti celých čísel.


Rýže. 9.9. Sloučit jakési pole šesti celých čísel

Níže je funkce C++, která implementuje algoritmus řazení sloučení. Chcete-li seřadit pole theArray, sestávající z nprvků, provede se rekurzivní volánímergesort(theArray, 0, n-1).

const int MAX_SIZE = maximální-počet-prvků-pole, -

prázdnota merge(DataType theArray, int první, int střední int poslední)

// Kombinuje dva uspořádané segmenty Array a

//Pole do jednoho uspořádaného pole.

// Předpoklad: první<= mid <= last. Оба подмассива

// theArray a theArray jsou seřazeny

// vzestupně.

// Post-stav: segment Array je uspořádán.

// Poznámka k implementaci: funkce sloučí dvě

// podpole do dočasného pole a poté jej zkopíruje

// obsah v původním poli theArray.

// _________________________________________________________

Analýza. Protože hlavní operace v tomto algoritmu se provádějí ve fázi sloučení, začněme analýzu s ním. V každém kroku jsou podpole sloučeny theArray A theArray. Na Obr. Obrázek 9.10 ukazuje příklad, ve kterém je vyžadován maximální počet srovnání. Pokud je celkový počet prvků sloučených segmentů pole roven p, pak při jejich slučování bude potřeba provést n-1 srovnání. (Například na Obr. 9.10 ukazuje pole sestávající ze šesti prvků, proto se provede pět porovnání.) Navíc se po porovnání provede kopie n prvky dočasného pole do původního pole. V každém kroku sloučení se tedy provede 3*n-1 hlavních operací.



Ve funkci mergesort provedou se dvě rekurzivní volání. Jak je znázorněno na Obr. 9.11, pokud původní volání funkce mergesort patří do úrovně nula, pak na úrovni jedna proběhnou dvě rekurzivní volání. Každé z těchto volání pak generuje další dvě rekurzivní volání druhé úrovně a tak dále. Kolik úrovní rekurze bude existovat? Zkusme je spočítat.

Každé volání funkce mergesort rozdělí pole na polovinu. V první fázi je původní pole rozděleno na dvě části. Při dalším volání rekurzivní funkce mergesort každá z těchto částí je opět rozdělena na polovinu a tvoří čtyři části původního pole. Při dalším rekurzivním volání je každá z těchto čtyř částí opět rozdělena na polovinu, čímž se vytvoří osm částí pole a tak dále. Rekurzivní volání pokračují, dokud části pole neobsahují pouze jeden prvek, jinými slovy, dokud není původní pole rozděleno na n dílů, což odpovídá počtu jeho prvků, je-li počet n je mocnina dvou (n=2 m), hloubka rekurze je k=log 2 n Například, jak je znázorněno na Obr. 9.11, pokud původní pole obsahuje osm prvků (8=2 3), pak je hloubka rekurze 3. Pokud počet n není mocnina dvou, hloubka rekurze je 1+ log 2 n (zaokrouhlená hodnota).

Původní volání funkce mergesort(úroveň 0) přistupuje k funkci spojit pouze jednou. Pak funkce spojit provádí fúzi n prvků, provádějících 3*n-1 operací. Na první úrovni rekurze jsou provedena dvě volání funkcí mergesort a tedy funkce spojit. Každé z těchto dvou volání slučuje n/2 prvků a vyžaduje 3*(n/2)-1 operace. Na této úrovni se tedy provedou 2*(3*(n/2)-1)=3*n-2 operací. Na m-té úrovni se provádějí rekurze 2 t sloučit volání funkcí. Každé z těchto volání vede ke sloučení p/2 t prvků a celkový počet operací je 3*(n/2 m)-2. Celkem 2 m volání rekurzivní funkce spojit generuje 3*n-2 m operací. Operace O(l) se tedy provádějí na každé úrovni rekurze. Protože počet úrovní rekurze je roven log2n nebo log 2 n+l, v nejhorších a průměrných případech funkce mergesort má složitost O(n*log2n). Podívejte se na obr. 9.3 a ještě jednou se ujistěte, že množství O(n*log 2 n) roste mnohem pomaleji než množství O(n g).



Přestože je algoritmus řazení sloučení extrémně rychlý, má jednu nevýhodu. K provedení operace

Sloučit nařídil podpole pole a pole

Je potřeba pomocné pole skládající se z n prvků. Pokud je dostupná paměť omezená, tento požadavek nemusí být přijatelný.


Přepážka znázorněná na Obr. 9.12, se vyznačuje tím, že všechny prvky sady Si = pole méně podpůrného prvku p, a hodně S 2 = pole sestává z prvků větších nebo rovných nosnému. Ačkoli tato vlastnost neznamená, že je pole seřazeno, znamená to mimořádně užitečnou skutečnost: pokud je pole uspořádáno, prvky na pozicích od první na pivotlndex-l, zůstávají na svých místech, i když se jejich vzájemné pozice mohou měnit. Podobné tvrzení platí pro prvky umístěné na pozicích od pivotlndex+l na poslední. Referenční prvek ve výsledném uspořádaném poli zůstane na svém místě.

na jeho místě

Toto rozdělení pole určuje rekurzivní povahu algoritmu. Rozdělení pole vzhledem k referenčnímu prvku r generuje dvě menší třídicí úlohy – setřídění levé (S 1) a pravé (S 2) části pole. Po vyřešení těchto dvou problémů získáme řešení původního problému. Jinými slovy, rozdělení pole před rekurzivními voláními ponechá kotevní prvek na místě a zajistí, že levá a pravá část pole jsou v pořádku. Algoritmus rychlého řazení je navíc konečný: velikosti levého a pravého segmentu pole jsou menší než velikost původního pole a každý krok rekurze nás přibližuje k základu, když pole sestává z jednoho prvku. Vyplývá to ze skutečnosti, že referenční prvek p nepatří do žádného z polí S l a S2.

Pseudokód pro algoritmus quicksort vypadá takto:

quicksort (v poli.- ItemArray,

in first:integer, in last:integer) // Objednává pole

-li (první< last)

Vyberte referenční prvek p z pole theArray Rozdělte pole theArray relativně

referenční prvek p // Oddíl vypadá jako pole

// Uspořádání pole SI

quicksort(theArray, first, pivotlndex-l)

// Objednávka pole S2

quicksort(theArray, pivotlndex+l, last) ) // Konec příkazu if // if first >= last, nedělat nic


Jak vybrat podpůrný prvek? Pokud jsou prvky pole zapsány v náhodném pořadí, můžete například jako referenci vybrat libovolný prvek theArray.(Postup pro výběr referenčního prvku bude podrobněji probrán později.) Při dělení pole je vhodné umístit referenční prvek do buňky theArray, bez ohledu na to, který prvek je vybrán jako referenční.

Část pole, která obsahuje prvky, které ještě nebyly rozděleny do segmentů S1 a S2, se nazývá nedefinovaná. Zvažte tedy pole znázorněné na obr. 9.14. Nejprve indexy, lastS1, firstNeznámý a naposledy rozdělte pole na tři části. Vztahy mezi referenčním prvkem a prvky neurčité části theArray neznámý.

Puc. 9.14. Invariant rozdělovacího algoritmu

Při rozdělování pole musí být splněna následující podmínka.

Prvky množiny S1 musí být menší než nosný prvek a prvky množiny S 2- větší nebo rovno tomu.

Tento příkaz je invariantem rozdělovacího algoritmu. Aby se jeho invariant provedl na začátku algoritmu, je nutné inicializovat indexy pole tak, aby bylo celé pole kromě referenčního prvku považováno za nedefinované.

první prvníNeznámý poslední

Puc. 9.15. Počáteční stav pole


V každém kroku rozdělovacího algoritmu se kontroluje jeden prvek z nedefinované části. Podle své hodnoty se umisťuje do množiny S1 resp S2. V každém kroku se tedy velikost nejisté části zmenšuje o jednu. Algoritmus se zastaví, když se velikost nedefinované části stane nulovou, tj. podmínka splněna prvníNeznámý > poslední.

Podívejme se na pseudokód tohoto algoritmu.

rozdělit (inout theArray:ItemArray,

in first:integer, in last:integer,

out pivotlndex:integer) // Rozdělí pole

// Inicializace

Vyberte podpůrný prvek a vyměňte jeho místa

s prvkem Array
p = theArray // str
- podpůrný prvek

// Nastaví prázdné množiny S1 a S 2 a inicializuje nedefinovanou // část pole pomocí segmentu

// theArray lastSl= první prvníNeznámý = první + 1

// Definice sad Sj a S 2 while (prvníNeznámý<= last)

// Vypočítejte index prvku nejvíce vlevo // nedefinované části pole-li (pole< р)

Umístěte prvek Array do Sijiný

Umístěte prvek Array do S 2 ) // Konec příkazu while

// Mezi sady umístěte podpůrný prvek S 2 a S 2 // a zapamatujte si jeho nový index

Vyměňte pole a pivotlindex pole = lastSl

Algoritmus je poměrně jednoduchý, ale operace pohybu vyžaduje objasnění. Zvažte dvě možné akce, které je třeba provést při každé iteraci smyčky zatímco.

Umístěte prvek theArray do množiny S t . Množina S1 a neurčitá část zpravidla nesousedí. Obvykle se mezi nimi nachází množina S 2. Tato operace však může být provedena efektivněji. Živel theArray lze zaměnit s prvním prvkem množiny S 2, tzn. s prvkem theArray, jak je znázorněno na Obr. 9.16. Jak být prvkem množiny S2, který byl umístěn v cele theArray? Pokud zvýšíte index první Neznámý o jeden se tento prvek stane nejvíce vpravo v množině S 2 . Tedy přesunout prvek theArray do pole S1, musíte provést následující kroky.

Vyměňte prvky pole

a Array Increment index lastSl o jeden Increment index firstNeznámý o jeden

Tato strategie zůstává platná, i když je množina S 2 prázdná. V tomto případě hodnota posledníSl+l rovna indexu první Neznámý a prvek prostě zůstane na svém místě.

Umístěte prvek theArray do množiny S 2 . Tuto operaci lze snadno provést. Připomeňme, že index prvku nejvíce vpravo množiny S 2 je roven firstNeznámý-1, těch. množina S 2 a neznámá část sousedí (obr. 9.17). Tedy přesunout prvek theArray do množiny S 2, stačí zvýšit index první Neznámý o jedničku, rozšíření množiny S 2 doprava. V tomto případě není invariant porušen.

Po přenesení všech prvků z nedefinované části do sad S 1 a S 2 zbývá vyřešit poslední problém. Mezi sady S1 a S2 musíte umístit podpěrný prvek. Vezměte prosím na vědomí, že prvek theArray je



Rýže. 9.17. Přesunutí prvku Array na hodnotu S2 po zvýšení prvního Neznámého indexu o jednu

je prvek sady zcela vpravo S1. Pokud jej prohodíte s podpůrným prvkem, bude na správném místě. Proto provozovatel

pivotlndex = lastSl

umožňuje určit index referenčního prvku. Tento index lze použít jako hranici mezi množinami Sj a 5r. Výsledky sledování algoritmu pro rozdělení pole sestávajícího ze šesti celých čísel, kdy prvním prvkem je reference, jsou znázorněny na Obr. 9.18.

Než začneme implementovat algoritmus rychlého řazení, zkontrolujme si správnost rozdělovacího algoritmu pomocí jeho invariantů. Invariant cyklu zahrnutý v algoritmu má následující tvar.

Všechny prvky sady S 2 (theArray) je menší než referenční a všechny prvky množiny S 2 (theArray) jsou větší nebo rovné referenčnímu

Připomeňme, že k určení správnosti algoritmu pomocí jeho invariantů je třeba provést čtyři kroky.

1. Invariant musí být pravdivý od samého začátku, před provedením cyklu. V rozdělovacím algoritmu je referenčním prvkem theArray, neznámá část - segment pole theArray, a množiny S1 a S 2 prázdný. Je zřejmé, že za těchto podmínek je invariant pravdivý.

2. Iterace smyčky nesmí porušit invariant. V rozdělovacím algoritmu každá iterace smyčky přenese jeden prvek z neznámé části do množiny S1 nebo S2 v závislosti na jeho hodnotě ve srovnání s referencí. Takže pokud byl invariant pravdivý před přenosem, musí zůstat pravdivý i po přenosu.

3. Invariant musí určit správnost algoritmu. Jinými slovy, správnost algoritmu musí vyplývat z pravdivosti invariantu. Algoritmus rozdělení se zastaví, když se nedefinovaná oblast vyprázdní. V tomto případě musí každý prvek segmentu Array patřit buď do množiny S1, nebo do množiny S2. V každém případě ze správnosti invariantu vyplývá, že
algoritmus dosáhl svého cíle.

4. Cyklus musí být konečný. Jinými slovy, musíte ukázat, že se smyčka dokončí po konečném počtu iterací. V rozdělovacím algoritmu se velikost nejisté části při každé iteraci sníží o jednu. Proto se po konečném počtu iterací nedefinovaná část vyprázdní a smyčka skončí.



Rýže. 9.18. První rozdělení pole, když je prvním prvkem pivot


Před voláním funkce quicksort se pole rozdělí na části S1 a S2. Algoritmus pak seřadí segmenty SI a S2 nezávisle na sobě, protože jakýkoli prvek v segmentu SI je vlevo od jakéhokoli prvku v segmentu S2. Na druhé straně ve funkci mergeSort není před rekurzivními voláními provedena žádná práce. Algoritmus objednává každou část pole, přičemž neustále bere v úvahu vztahy mezi prvky obou částí. Z tohoto důvodu musí algoritmus po provedení rekurzivních volání sloučit dvě poloviny pole.

Analýza. Hlavní práce v algoritmu quicksort se provádí ve fázi rozdělení pole. Při analýze každého prvku patřícího do nedefinované části je nutné porovnat prvek Array s referenčním a umístit jej buď do segmentu S1 nebo do segmentu S2. Jeden ze segmentů SI nebo S2 může být prázdný; pokud je například podpůrný prvek nejmenším prvkem segmentu, sada S1 zůstane prázdná. K tomu dochází v nejhorším případě, protože velikost segmentu S2 se při každém volání funkce quicksort zmenší pouze o jednu. V této situaci bude tedy proveden maximální počet rekurzivních volání funkce quicksort.

Až příště zavoláte quicksort rekurzivně, oddíl se podívá na n-1 prvků. K jejich distribuci do segmentů bude zapotřebí n-2 srovnání. Protože velikost segmentu uvažovaného funkcí quicksort se na každé úrovni rekurze zmenšuje pouze o jednu, bude existovat n-1 úrovní rekurze. Funkce rychlého třídění tedy provádí srovnání 1 + 2 + ...+ (n-1) = n * (n-1)/2. Připomeňme však, že při přenosu prvku do množiny S2 není nutné prvky přeskupovat. Chcete-li to provést, stačí změnit index firstUnknown.

Podobně, pokud je množina S2 ponechána prázdná při každém rekurzivním volání, bude vyžadováno n* (n-1)/2 srovnání. Navíc v tomto případě, aby bylo možné přenést každý prvek z neznámé části do množiny S1, bude nutné prvky přeskupit. Bude tedy potřeba *(n-1)/2 permutací. (Připomeňme, že každá permutace se provádí pomocí tří operací přiřazení.) V nejhorším případě je tedy složitost algoritmu rychlého třídění O(n2).

Pro kontrast na Obr. Obrázek 9.20 ukazuje příklad, kde se množiny S1 a S2 skládají ze stejného počtu prvků. V průměrném případě, kdy se množiny S1 a S2 skládají ze stejného - nebo přibližně stejného - počtu prvků zapsaných v náhodném pořadí, bude zapotřebí méně rekurzivních volání rychlého třídění. Stejně jako v analýze algoritmu mergeSort je snadné ukázat, že hloubka rekurze v algoritmu quicksort je rovna log2n nebo log2n+l. Každé volání rychlého třídění provede m porovnání a nejvýše m permutací, kde m je počet prvků v dílčím poli, které mají být seřazeny.



Rychlé řazení: nejhorší případ O(n 2), průměrný případ O(n*logn).

Tedy s velkými poli algoritmus

quicksort je výrazně rychlejší než insertionSort, i když v nejhorším případě mají oba zhruba stejný výkon.

Algoritmus quicksort se často používá k třídění velkých polí. Důvodem jeho popularity je jeho výjimečný výkon, a to i přes odrazující nejhorší odhady. Faktem je, že tato možnost je extrémně vzácná a v praxi algoritmus quicksort funguje skvěle s relativně velkými poli.

Díky významnému rozdílu mezi průměrným a nejhorším odhadem složitosti se Quicksort odlišuje od ostatních algoritmů probíraných v této kapitole. Pokud je pořadí prvků v původním poli „náhodné“, algoritmus rychlého třídění funguje minimálně stejně dobře jako jakýkoli jiný algoritmus, který používá porovnávání prvků. Pokud je původní pole zcela neuspořádané, algoritmus rychlého třídění funguje nejlépe.

Algoritmus mergeSort má přibližně stejnou účinnost. V některých případech je rychlejší algoritmus quicksort, v jiných je rychlejší algoritmus mergeSort. Ačkoli odhad složitosti nejhoršího případu od mergeSort je řádově stejný jako odhad složitosti průměrného případu u quicksortu, quicksort je ve většině případů o něco rychlejší. V nejhorším případě je však výkon algoritmu rychlého třídění mnohem nižší.

Pro zjednodušení kódu a zlepšení čitelnosti zavedeme metodu Swap, která bude prohazovat hodnoty v poli podle indexu.

Void Swap(T items, int left, int right) ( if (left != right) ( T temp = items; items = items; items = temp; ) )

Bublinové řazení

Bublinové třídění je nejjednodušší třídicí algoritmus. Několikrát prochází polem a v každém kroku přesune největší neseřazenou hodnotu na konec pole.

Máme například pole celých čísel:

Při prvním průchodu polem porovnáme hodnoty 3 a 7. Protože 7 je větší než 3, necháme je tak, jak jsou. Potom porovnáme 7 a 4. 4 je menší než 7, takže je prohodíme a posuneme sedm o jednu pozici blíže ke konci pole. Nyní to vypadá takto:

Tento proces se opakuje, dokud sedmička nedosáhne téměř konce pole. Nakonec je porovnán s prvkem 8, který je větší, což znamená, že nedochází k žádné výměně. Poté, co pole jednou projdeme, vypadá takto:

Protože byla provedena alespoň jedna výměna hodnoty, musíme pole projít znovu. V důsledku tohoto průchodu přesuneme číslo 6 na místo.

A opět byla provedena alespoň jedna výměna, což znamená, že znovu procházíme polem.

Při dalším průchodu nedojde k žádné výměně, což znamená, že naše pole je roztříděno a algoritmus dokončil svou práci.

Public void Sort(T items) ( bool swapped; do ( swapped = false; for (int i = 1; i)< items.Length; i++) { if (items.CompareTo(items[i]) >0) ( Swap(items, i - 1, i); swapped = true; ) ) ) while (swapped != false); )

Řazení vložení

Vložení řazení funguje tak, že prochází polem a přesouvá požadovanou hodnotu na začátek pole. Po zpracování další pozice víme, že všechny pozice před ní jsou seřazeny, ale po ní již ne.

Důležitý bod: řazení vložení zpracovává prvky pole v pořadí. Protože algoritmus iteruje prvky zleva doprava, víme, že vše nalevo od aktuálního indexu je již seřazeno. Tento obrázek ukazuje, jak seřazená část pole roste s každým průchodem:

Postupně roste setříděná část pole a nakonec se pole seřadí.

Podívejme se na konkrétní příklad. Zde je naše netříděné pole, které budeme používat:

Algoritmus začíná na indexu 0 a hodnotě 3. Protože se jedná o první index, pole až do něj včetně je považováno za seřazené.

V této fázi se třídí prvky s indexy 0..1, ale o prvcích s indexy 2..n není nic známo.

Dále se kontroluje hodnota 4 Protože je menší než sedm, musíme ji přesunout na správnou pozici v seřazené části pole. Otázkou zůstává: jak to definovat? To se provádí metodou FindInsertionIndex. Porovnává hodnotu (4), která mu byla předána, s každou hodnotou v seřazené části, dokud nenajde místo pro vložení.

Našli jsme tedy index 1 (mezi hodnotami 3 a 7). Metoda Insert provede vložení odstraněním vložené hodnoty z pole a posunutím všech hodnot počínaje indexem vložení doprava. Nyní pole vypadá takto:

Nyní je setříděna část pole počínaje prvkem nula a končící prvkem s indexem 2. Další průchod začíná na indexu 3 a hodnotě 4. Jak algoritmus funguje, pokračujeme v takových vkládáních.

Když nejsou k dispozici žádné další vkládání, pole se považuje za zcela seřazené a algoritmus je dokončen.

Public void Sort(T items) ( int sortedRangeEndIndex = 1; while (sortedRangeEndIndex< items.Length) { if (items.CompareTo(items) < 0) { int insertIndex = FindInsertionIndex(items, items); Insert(items, insertIndex, sortedRangeEndIndex); } sortedRangeEndIndex++; } } private int FindInsertionIndex(T items, T valueToInsert) { for (int index = 0; index < items.Length; index++) { if (items.CompareTo(valueToInsert) >0) ( return index; ) ) throw new InvalidOperationException("Index vložení nebyl nalezen"); ) private void Insert(T itemArray, int indexInsertingAt, int indexInsertingFrom) ( // itemArray = 0 1 2 4 5 6 3 7 // insertingAt = 3 // insertingFrom = 6 // // Akce: // 1: Uložit aktuální index v temp // 2: Nahradit indexInsertingAt za indexInsertingFrom // 3: Nahradit indexInsertingAt za indexInsertingFrom na pozici +1 // Posunout prvky o jeden doleva // 4: Zapsat teplotu na pozici v poli + 1. // Krok 1. T temp = itemArray; // Krok 2. itemArray = itemArray // Krok 3. for (int current = indexInsertingFrom; current > indexInsertingAt; current--) ( itemArray = itemArray; ) // Krok 4. itemArray = temp;

Řazení podle výběru

Výběrové řazení je hybrid mezi bublinovým a vloženým řazením. Stejně jako bublinové třídění prochází tento algoritmus polem znovu a znovu a posouvá jednu hodnotu na správnou pozici. Na rozdíl od bublinového řazení však vybírá nejmenší neseřazenou hodnotu místo největší. Stejně jako u vložení řazení je uspořádaná část pole umístěna na začátku, zatímco v bublinovém řazení je na konci.

Podívejme se, jak funguje třídění výběru na našem netříděném poli.

Při prvním průchodu algoritmus používá metodu FindIndexOfSmallestFromIndex k nalezení nejmenší hodnoty v poli a přesunutí ji na začátek.

U takto malého pole můžeme okamžitě říct, že nejmenší hodnota je 3 a už je ve správné pozici. V této fázi víme, že první pozice v poli (index 0) je nejmenší hodnota, proto je začátek pole již seřazen. Začneme tedy druhý průchod – tentokrát pomocí indexů od 1 do n – 1.

Při druhém průchodu určíme, že nejmenší hodnota je 4. Prohodíme ji s druhým prvkem, sedmičkou, načež se 4 umístí na správnou pozici.

Nyní nesetříděná část pole začíná na indexu 2. S každým průchodem algoritmu roste o jeden prvek. Pokud jsme v žádném průchodu neprovedli jedinou výměnu, znamená to, že pole je seřazeno.

Po dalších dvou průchodech algoritmus dokončí svou práci:

Public void Sort (T items) ( int sortedRangeEnd = 0; while (sortedRangeEnd< items.Length) { int nextIndex = FindIndexOfSmallestFromIndex(items, sortedRangeEnd); Swap(items, sortedRangeEnd, nextIndex); sortedRangeEnd++; } } private int FindIndexOfSmallestFromIndex(T items, int sortedRangeEnd) { T currentSmallest = items; int currentSmallestIndex = sortedRangeEnd; for (int i = sortedRangeEnd + 1; i < items.Length; i++) { if (currentSmallest.CompareTo(items[i]) >0) ( currentSmallest = items[i]; currentSmallestIndex = i; ) ) return currentSmallestIndex; )

Sloučit řazení

Rozděl a panuj

Doposud jsme se zabývali lineárními algoritmy. Používají málo paměti navíc, ale mají kvadratickou složitost. Pomocí slučovacího řazení jako příkladu se podíváme na algoritmus rozděl a panuj. (rozděl a panuj).

Tento typ algoritmu funguje tak, že rozděluje velký problém na menší, snáze řešitelné. Používáme je každý den. Příkladem takového algoritmu je například vyhledávání v telefonním seznamu.

Pokud chcete najít osobu jménem Petrov, nebudete hledat začínající písmenem A a obracet jednu stránku po druhé. Knihu nejspíš otevřete někde uprostřed. Pokud stisknete T, vrátíte se o několik stránek zpět, možná příliš mnoho na O. Pak se posunete vpřed. Takže listováním tam a zpět stále méně a méně stránkami nakonec najdete tu, kterou potřebujete.

Jak efektivní jsou tyto algoritmy?

Řekněme, že v telefonním seznamu je 1000 stránek. Pokud ji otevřete do poloviny, vyhodíte 500 stránek, které neobsahují hledanou osobu. Pokud se nedostanete na správnou stránku, vyberete pravou nebo levou stranu a opět ponecháte polovinu dostupných možností. Nyní máte k prohlédnutí 250 stránek. Tímto způsobem znovu a znovu rozdělíme náš problém na polovinu a můžeme najít osobu v telefonním seznamu na pouhých 10 pohledů. To představuje 1 % z celkového počtu stránek, které bychom museli prohlížet při lineárním vyhledávání.

Sloučit řazení

Při slučovacím řazení rozdělíme pole na polovinu, dokud nebude každá sekce dlouhá jeden prvek. Poté jsou tyto sekce vráceny na své místo (sloučeny) ve správném pořadí.

Podívejme se na takové pole:

Rozdělme to napůl:

A každou část rozdělíme na polovinu, dokud nezůstanou části s jedním prvkem:

Nyní, když jsme pole rozdělili na nejkratší možné části, sloučíme je ve správném pořadí.

Nejprve získáme skupiny dvou seřazených prvků, poté je „shromáždíme“ do skupin po čtyřech prvcích a nakonec vše shromáždíme do setříděného pole.

Aby algoritmus fungoval, musíme implementovat následující operace:

  1. Operace pro rekurzivní rozdělení pole do skupin (metoda Sort).
  2. Sloučení ve správném pořadí (metoda sloučení).

Stojí za zmínku, že na rozdíl od lineárních třídicích algoritmů slučovací třídění rozdělí a sloučí pole bez ohledu na to, zda bylo původně seřazeno nebo ne. Proto, i když v nejhorším případě bude fungovat rychleji než lineárně, v nejlepším případě bude jeho výkon nižší než lineární. Proto sloučení řazení není nejlepším řešením, když potřebujete seřadit částečně uspořádané pole.

Public void Sort(T items) ( if (items.Length<= 1) { return; } int leftSize = items.Length / 2; int rightSize = items.Length - leftSize; T left = new T; T right = new T; Array.Copy(items, 0, left, 0, leftSize); Array.Copy(items, leftSize, right, 0, rightSize); Sort(left); Sort(right); Merge(items, left, right); } private void Merge(T items, T left, T right) { int leftIndex = 0; int rightIndex = 0; int targetIndex = 0; int remaining = left.Length + right.Length; while(remaining >0) ( if (leftIndex >= left.Length) ( items = right; ) else if (rightIndex >= right.Length) ( items = left; ) else if (left.CompareTo(right)< 0) { items = left; } else { items = right; } targetIndex++; remaining--; } }

Rychlé řazení

Quicksort je další algoritmus rozděl a panuj. Funguje to rekurzivním opakováním následujících kroků:

  1. Vyberte index klíče a pomocí něj rozdělte pole na dvě části. To lze provést různými způsoby, ale v tomto článku používáme náhodné číslo.
  2. Přesuňte všechny prvky větší než klíč na pravou stranu pole a všechny prvky menší než klíč doleva. Klíčový prvek je nyní ve správné poloze – je větší než jakýkoli prvek vlevo a menší než kterýkoli prvek vpravo.
  3. Opakujeme první dva kroky, dokud není pole zcela seřazeno.

Podívejme se na algoritmus v akci na následujícím poli:

Nejprve náhodně vybereme klíčový prvek:

Int pivotIndex = _pivotRng.Next(left, right);

Nyní, když známe index klíče (4), vezmeme hodnotu umístěnou na tomto indexu (6) a zabalíme hodnoty do pole tak, aby všechna čísla větší nebo rovna klíči byla na pravé straně a čísla menší než klíč jsou vlevo. Všimněte si, že index klíčového prvku se může během procesu přenosu hodnot změnit (to brzy uvidíme).

Přesouvání hodnot se provádí pomocí metody oddílů.

V tomto okamžiku víme, že hodnota 6 je ve správné poloze. Nyní tento proces zopakujeme pro pravou a levou stranu pole.

Zůstane nám jedna neseřazená hodnota, a protože víme, že vše ostatní již bylo seřazeno, algoritmus se ukončí.

Random _pivotRng = new Random(); public void Sort(T items) ( quicksort(items, 0, items.Length - 1); ) private void quicksort(T items, int left, int right) ( if (left< right) { int pivotIndex = _pivotRng.Next(left, right); int newPivot = partition(items, left, right, pivotIndex); quicksort(items, left, newPivot - 1); quicksort(items, newPivot + 1, right); } } private int partition(T items, int left, int right, int pivotIndex) { T pivotValue = items; Swap(items, pivotIndex, right); int storeIndex = left; for (int i = left; i < right; i++) { if (items[i].CompareTo(pivotValue) < 0) { Swap(items, i, storeIndex); storeIndex += 1; } } Swap(items, storeIndex, right); return storeIndex; }

Závěr

Tímto končíme naši sérii článků o algoritmech a datových strukturách pro začátečníky. Během této doby jsme se podívali na propojené seznamy, dynamická pole, binární vyhledávací stromy a sady s příklady kódu v C#.

Algoritmus sloučit řazení navrhl praotec moderních počítačů John von Neumann. Samotná metoda je stabilní, to znamená, že nemění prvky stejné hodnoty v seznamu.

Merge sort je založen na principu rozděl a panuj. Seznam je rozdělen na stejné nebo téměř stejné části, z nichž každá je řazena samostatně. Poté se již objednané díly spojí dohromady. Tento proces lze podrobně popsat takto:

1. pole je rekurzivně rozděleno na polovinu a každá polovina je rozdělena, dokud se velikost dalšího podpole nerovná jedné;

2. Dále je provedena operace algoritmu zvaná slučování. Dvě jednotková pole se sloučí do společného výsledného pole a z každého se vybere menší prvek (seřazený vzestupně) a zapíše se do volné levé buňky výsledného pole. Poté se ze dvou výsledných polí sestaví třetí společné tříděné pole a tak dále. Pokud jedno z polí dojde, prvky druhého se přidají do sestaveného pole;

3. na konci operace sloučení jsou prvky přepsány z výsledného pole do původního.

Podprogram MergeSort rekurzivně rozděluje a třídí pole a Spojit je zodpovědný za jeho splynutí. Takto můžete napsat pseudokód hlavního podprogramu:

Podprogram MergeSort(A, první, poslední)

A– pole

první, poslední– čísla prvního a posledního prvku

Li první<posledníŽe

Volání MergeSort(A, první, (první+poslední)/2) //třídění levé strany

Volání MergeSort(A, (první+poslední)/2+1, poslední) //třídění pravé strany

Volání Spojit(A, první, poslední) //sloučení dvou částí

Tento podprogram se provede pouze v případě, že číslo prvního prvku je menší než číslo posledního. Jak již bylo zmíněno, z podprogramu MergeSort se nazývá podprogram Spojit, který provede operaci sloučení. Přejděme k tomu druhému.

Práce Spojit spočívá ve vytvoření uspořádaného výsledného pole sloučením dvou rovněž seřazených polí menších velikostí. Zde je pseudokód pro tuto rutinu:

Podprogram Spojit(A, první, poslední)

start, finále– čísla prvních prvků levé a pravé části

mas- pole, střední- ukládá číslo prostředního prvku

střední=(první+poslední)/2 //výpočet prostředního prvku

start=první//začátek levé strany

finále=střední+1 //začátek pravé strany

Cyklus j=první na poslední spustit //provést od začátku do konce

Pokud (( start<=střední) A (( finále>poslední) nebo ( A[start]<A[finále]))) To

mas[j]=A[start]

zvýšení start od 1


mas[j]=A[finále]

zvýšení finále od 1

Cyklus j=první na poslední spustit //vrátit výsledek do seznamu

A[j]=mas[j]

Analyzujme slučovací třídicí algoritmus pomocí následujícího příkladu (obr. 6.10). Existuje neuspořádaná posloupnost čísel: 2, 6, 7, 1, 3, 5, 0, 4. Po rozdělení této posloupnosti do polí jednotek bude proces slučování řazení (vzestupně) vypadat takto:

Obrázek 6.10 – Příklad řazení sloučení

Pole bylo rozděleno do jednotlivých polí, která algoritmus spojuje do párů, dokud není získáno jediné pole, jehož všechny prvky jsou na svých pozicích.

Programový kód v C++:

void Merge(int *A, int první, int poslední) //funkce, která sloučí pole

int střední, začátek, konec, j;

int *mas=new int;

prostřední=(první+poslední)/2; //výpočet prostředního prvku

start=první; //začátek levé strany

final=middle+1; //začátek pravé strany

for(j=první; j<=last; j++) //выполнять от начала до конца

pokud ((začátek<=middle) && ((final>poslední) || (A

hmotnost[j]=A;

hmotnost[j]=A;

pro (j=první; j<=last; j++) A[j]=mas[j]; //возвращение результата в список

void MergeSort(int *A, int první, int poslední) //rekurzivní postup řazení

pokud (nejprve

MergeSort(A, první, (první+poslední)/2); //třídění levé strany

MergeSort(A, (první+poslední)/2+1, poslední); //třídění pravé strany

Sloučit(A, první, poslední); //sloučení dvou částí

void main() //funkce main

cout<<"Размер массива >";cin>>n;

pro (i=1; i<=n; i++)

cout< ";

MergeSort(A, 1, n); //zavoláme proceduru řazení

cout<<"Упорядоченный массив: "; //вывод упорядоченного массива

pro (i=1; i<=n; i++) cout<

Programový kód v Pascalu:

typ pole=pole celého čísla;

var n, i: celé číslo;

procedura Merge(var A: massiv; první, poslední: celé číslo); (postup, který sloučí pole)

var střední, začátek, konec, j: celé číslo;

uprostřed:=(první+poslední) div 2; (výpočet středního prvku)

start:=první; (začátek levé strany)

final:=middle+1; (začátek pravé strany)

for j:=od prvního k poslednímu (provést od začátku do konce)

pokud (začátek<=middle) and ((final>poslední) nebo (A

hmotnost[j]:=A;

hmotnost[j]:=A;

for j:=první před posledním proveďte A[j]:=mas[j]; (vrácení výsledku do pole)

procedure MergeSort(var A: massiv; first, last: integer); (rekurzivní postup řazení)

pokud první

MergeSort(A, první, (první+poslední) div 2); (seřadit levou stranu)

MergeSort(A, (první+poslední) div 2+1, poslední); (řazení na pravé straně)

Sloučit(A, první, poslední); (sloučení dvou částí)

začít (hlavní blok programu)

write("Velikost pole > ");

pro i:=1 až n do

write(i, " prvek > ");

MergeSort(A, 1, n); (volání procedury řazení)

write("Seřazené pole: "); (výstup tříděného pole)

for i:=1 to n do write(A[i], " ");

Nevýhodou slučovacího řazení je, že využívá paměť navíc. Ale když musíte pracovat se soubory nebo seznamy, ke kterým se přistupuje pouze sekvenčně, pak je velmi vhodné použít tuto konkrétní metodu. Mezi výhody algoritmu patří také jeho stabilita a dobrá rychlost provozu. Ó(n*log n).

  • algoritmy,
  • Programování
  • Kdysi to někdo řekl

    ...každý vědec, který nedokázal vysvětlit osmiletému dítěti, co dělá, byl šarlatán.

    Ukázalo se, že to byl Kurt Vonnegut.

    Nesnažil jsem se toto tvrzení dokázat. Snažil jsem se vyvrátit svou hloupost.

    Řekněme, že máme dvě pole čísel seřazená vzestupně.

    Int a1 = nový int (21, 23, 24, 40, 75, 76, 78, 77, 900, 2100, 2200, 2300, 2400, 2500); int a2 = nový int (10, 11, 41, 50, 65, 86, 98, 101, 190, 1100, 1200, 3000, 5000);
    Je nutné je sloučit do jednoho uspořádaného pole.

    Int a3 = new int;
    Toto je úkol pro slučovací řazení.

    Co je to? Na internetu je odpověď, je tam popis algoritmu, ale nerozuměl jsem tomu na jedno posezení a rozhodl jsem se na to přijít sám. Chcete-li to provést, musíte pochopit základní princip algoritmu, abyste mohli znovu vytvořit algoritmus z paměti ve vztahu k vašemu problému.

    Začněme pro zdraví

    Pojďme postupně a použijeme to, co je na povrchu: z každého pole postupně vezmeme jeden prvek, porovnáme je a „sloučíme“ do jednoho pole. Jako první umístíme menší prvek, jako druhý pak větší prvek. Pak po prvním průchodu je vše v pořádku:

    10, 21
    A po druhém průchodu to není tak dobré:

    10, 21, 11, 23
    Je jasné, že musíme porovnat prvky s těmi již přidanými.

    Začněme znovu

    Mějme určitou dočasnou vyrovnávací paměť prvků porovnávanou v každém kroku. Po prvním průchodu bude obsahovat 21 a 10. Po porovnání přesuneme menší prvek 10 z bufferu do výsledného pole a ponecháme větší prvek 21, protože nevíme, co za ním bude.

    Po druhém průchodu bude buffer obsahovat 21, 23 a 11. Co s nimi dělat, není jasné, můžete porovnávat více než dva prvky, ale není to tak snadné.

    Domluvme se pak, že do tohoto bufferu vezmeme jeden prvek z každého pole. Protože je snazší porovnat dva prvky mezi sebou a obecně máme dvě entity - dvě pole. Potom po druhém průchodu bude ve vyrovnávací paměti 21 a 11, protože „zástupce“ prvního pole je již ve vyrovnávací paměti - je to 21. Porovnáme je a pošleme menší do výsledného pole. Poté po druhém průchodu budeme mít ve výsledném poli:

    10, 11
    A ve vyrovnávací paměti - 21.

    Při třetím průchodu vezmeme 41 z druhého pole do vyrovnávací paměti, protože „zástupce“ prvního pole zůstává ve vyrovnávací paměti. Porovnáme 21 a 41 a nakonec odstraníme 21 z vyrovnávací paměti.

    Po třetím průchodu budeme mít ve výsledném poli:

    10, 11, 21
    Při čtvrtém průchodu porovnáme dvě hodnoty z vyrovnávací paměti - 41 a 23. Výsledné pole bude obsahovat:

    10, 11, 21, 23
    To znamená, že až nyní - při čtvrtém, a nikoli při druhém průchodu - se výsledek ukázal jako správný. Ukazuje se, že ve smyčce si musíte pamatovat aktuální index pro každé pole a samotná smyčka může být stejně dlouhá jako součet délek polí.

    Blížíme se ke konci, ale najednou

    Co uděláme, když se výsledné pole skládá z:

    10, 11, 21, 23, 24, 40, 41, 50, 65, 75, 76, 78, 77, 86, 98, 101, 190, 900, 1100, 1200, 2100, 2200, 2300, 2400, 2500,
    Vyrovnávací paměť bude obsahovat 3000 z druhého pole a v prvním - dojdou všechny prvky? Protože jsou naše pole setříděná, jednoduše vezmeme 3000 z vyrovnávací paměti a zbývajících 5000. To znamená, že musíme zkontrolovat každý index, abychom zjistili, zda jsme nepřekročili počet prvků v každém z polí.

    Pojďme si úkol zkomplikovat

    Co když máme nesetříděná pole? Obvykle jde o seřazení jednoho pole. Pak lze také použít řazení sloučení.

    Nechť první pole (například z něj vezme několik prvků) má následující uspořádání prvků:

    2100, 23, 40, 24, 2, 1.
    Budeme to třídit. Protože je snazší porovnávat dva prvky najednou, rozdělme pole rovnoměrně na dva:

    2150, 23, 40
    A
    24, 2, 1.
    Získáte tři prvky. Mnoho! Rozdělme každé pole rovnoměrně, dostaneme čtyři pole:

    2100, 23 40 24, 2 1
    Pojďme nyní seřadit každé z polí jednoduchým porovnáním prvního a druhého prvku (kde existují):

    23, 2100 40 2, 24 1
    A sloučíme to zpět podle předchozího algoritmu - přes buffer. Po prvním sloučení získáme dvě pole:

    23, 40, 2100 1, 2, 24
    A znovu sloučíme - do jednoho pole:

    1, 2, 23, 24, 40, 2100
    Takto sloučíme seřazené pole.

    Sečteno a podtrženo

    Slučovací řazení tedy zahrnuje rovnoměrné dělení pole, dokud jedno pole nevytvoří několik malých – ne více než dva prvky. Tyto dva prvky lze snadno vzájemně porovnávat a uspořádat v závislosti na požadavku: vzestupně nebo sestupně.

    Po rozdělení následuje zpětné sloučení, ve kterém je v jednom časovém okamžiku (nebo v průběhu cyklu) vybrán jeden prvek z každého pole a vzájemně porovnáván. Nejmenší (nebo největší) prvek je odeslán do výsledného pole, zbývající prvek zůstává relevantní pro porovnání s prvkem z jiného pole v dalším kroku.

    Pojďme to vyjádřit v kódu (Java)

    Příklad řazení ve vzestupném pořadí dvou seřazených polí:

    Int a1 = nový int (21, 23, 24, 40, 75, 76, 78, 77, 900, 2100, 2200, 2300, 2400, 2500); int a2 = nový int (10, 11, 41, 50, 65, 86, 98, 101, 190, 1100, 1200, 3000, 5000);< a2[j]) { int a = a1[i]; a3[k] = a; i++; } else { int b = a2[j]; a3[k] = b; j++; } }
    int a3 = nový int;

    int i=0, j=0;
    for (int k=0; k
    a1.length-1) ( int a = a2[j]; a3[k] = a; j++; ) jinak if (j > a2.length-1) ( int a = a1[i]; a3[k] = a; i++) jinak if (a1[i];

    Zde:

    A1 a a2 – pole, která je třeba sloučit;

    a3 – výsledné pole;

    i a j jsou indexy pro pole al a a2, v tomto pořadí, které ukazují na aktuální prvky v každém kroku a tvoří stejnou vyrovnávací paměť.<= lo) return; int mid = lo + (hi - lo) / 2; SortUnsorted(a, lo, mid); SortUnsorted(a, mid + 1, hi); int buf = Arrays.copyOf(a, a.length); for (int k = lo; k <= hi; k++) buf[k] = a[k]; int i = lo, j = mid + 1; for (int k = lo; k <= hi; k++) { if (i >První dvě podmínky kontrolují, že indexy nepřesahují počet prvků v polích. Třetí a čtvrtá podmínka zajistí, že nejmenší prvek z prvního a druhého pole se přesune do pole.< buf[i]) { a[k] = buf[j]; j++; } else { a[k] = buf[i]; i++; } } }
    int a3 = nový int;

    Funkce sloučení řazení
    Naformátujme výše uvedený kód jako rekurzivní funkci, která oddělí pole co nejdéle, s parametry odpovídajícími celému poli při prvním volání, jeho polovině při druhém a třetím volání atd.
    Private void SortUnsorted(int a, int lo, int hi) ( if (hi



    
    Nahoru