← Přehled všech hodin

Řadicí algoritmy — od bublin po chytré řazení

Cíl hodiny: Pochopíš, jak se dá seřadit hromada čísel, a uvidíš, že mezi „hloupým" a „chytrým" řazením je propastný rozdíl v množství práce.
Jak stránku používat: vpravo nahoře je tlačítko Prezentace — schová všechno kromě jednoho bloku a přepíná se šipkami. U ukázek a cvičení je kód vlevo editovatelný: uprav ho a klikni na Spustit (nebo Ctrl+Enter). Program se přeloží a spustí přímo v prohlížeči a vpravo se objeví to, co vypsal do konzole. Nic se neinstaluje.
Kam to dnes směřuje:

1. Vysvětlení a souvislosti

Poznámky pro učiteleJak celou hodinu vést1. Vysvětlení a souvislosti

Tenhle blok je jen pro vás — dětem ho nepromítejte.

Fáze Co se děje Čas
Rozjezd Seřaďte živě pět dětí podle výšky. Kolik porovnání to dalo? 10 min
Graf v konzoli Blok 2–3, pole jako sloupce 10 min
Bubble sort Blok 4 + ukázka 2, počitadla na tabuli 20 min
Merge a quick Bloky 5–6 + ukázky 3–4 25 min
Hotové řazení Blok 7 + ukázka 5 10 min
Cvičení Děti pracují samy, vy obcházíte 55 min
Závěr Porovnání počtu porovnání u všech tří algoritmů 10 min
Hlavní pedagogický cíl

Rozjezd bez počítače je tady nejdůležitější část hodiny. Postavte pět dětí do řady a nechte je seřadit se podle výšky tak, že smí porovnávat jen dvojice sousedů a prohazovat je. Třída počítá, kolikrát se porovnávalo.

Pak to samé „chytře": rozdělte je na dvě skupinky, každou seřaďte zvlášť a nakonec skupinky slijte dohromady. Děti samy uvidí, že to bylo rychlejší. Až potom sáhněte na klávesnici.

Cílem není memorovat algoritmy, ale mít v hlavě obrázek: hloupé řazení porovnává pořád dokola totéž, chytré si rozdělí práci.

Tři věci, které dnes NEVYSVĚTLUJTE
  1. Složitost O(n²) a O(n log n). Stačí: „bubble roste s druhou mocninou, chytré algoritmy skoro lineárně". Pracujte s naměřenými počty porovnání.
  2. Stabilitu řazení. Zmiňte jen u rychlíků (viz učitelský blok na konci).
  3. Implementaci Array.Sort (introsort). Stačí „je to chytrá směs quicksortu a dalších algoritmů, napsaná profesionály".

Jak pracovat se smíšenou úrovní. Cvičení 1 zvládne každý. Cvičení 2 a 3 jsou rekurzivní a jsou nejtěžší — slabším stačí doplnit slučování, zbytek jim nechte předepsaný. Cvičení 4 a 5 jsou nejvděčnější, protože tam se konečně používají hotové nástroje.

Blok 1Řadit znamená porovnávat a prohazovat1. Vysvětlení a souvislosti

Každé řazení na světě stojí na dvou operacích:

  1. porovnej dva prvky (pole[0] > pole[1]),
  2. když jsou ve špatném pořadí, prohoď je.

Prohození potřebuje dočasnou proměnnou. Kdybys napsal rovnou pole[0] = pole[1];, původní sedmička se nenávratně ztratí — přepsal bys ji. Představ si dvě sklenice: než přelít, musíš mít třetí prázdnou.

Algoritmy se liší jen v tom, které dvojice a v jakém pořadí porovnávají. Bubble sort porovnává sousedy, merge sort slévá hotové poloviny, quicksort porovnává s vybraným prvkem. Ale pod tím je pořád totéž.

Klikni vlevo na Spustit a objeví se tu výstup programu.
Zkratka pro prohození

C# umí prohodit dvě hodnoty i bez dočasné proměnné pomocí n-tice (tu znáš z minulé hodiny):

int[] pole = { 7, 3 };
(pole[0], pole[1]) = (pole[1], pole[0]);
Console.WriteLine(string.Join(" ", pole));   // 3 7

Vpravo se nejdřív spočítá celá dvojice a teprve pak se rozdá doleva. Je to kratší a hlavně se u toho nedá splést pořadí.

Blok 2Pole jako sloupcový graf1. Vysvětlení a souvislosti

Aby bylo řazení vidět, musí být pole vidět. V konzoli na to máme dva způsoby:

  • Řádek číselstring.Join(" ", pole) slepí prvky mezerami. Hodí se, když chceš vypsat stav po každém průchodu pod sebe.
  • Sloupcový grafnew string('█', hodnota) vyrobí tolik plných čtverečků, jaká je hodnota. Menší číslo = kratší sloupec.

{hodnota,3} znamená „zarovnej číslo na tři znaky doprava", takže svislítka jsou hezky pod sebou i u dvouciferných čísel.

Tohle je konzolová obdoba grafu: jakmile se pole seřadí, sloupce se srovnají do schodů. A to je přesně ten moment, kdy řazení „uvidíš".

Klikni vlevo na Spustit a objeví se tu výstup programu.
Blok 3Bubble sort: hloupý, ale pochopitelný1. Vysvětlení a souvislosti
7 3 9 1 5     porovnej 7 a 3 → prohoď
3 7 9 1 5     porovnej 7 a 9 → nech
3 7 9 1 5     porovnej 9 a 1 → prohoď
3 7 1 9 5     porovnej 9 a 5 → prohoď
3 7 1 5 9     konec 1. průchodu, devítka je doma

Bubble sort (bublinkové řazení) projde pole a porovná každé dva sousedy. Když jsou ve špatném pořadí, prohodí je. A pak to celé opakuje.

Po prvním průchodu je největší číslo úplně vpravo — probublalo tam jako bublina v limonádě. Po druhém průchodu je na místě druhé největší, a tak dál.

Proto se vnitřní cyklus může pokaždé o kousek zkrátit:

int[] pole = { 7, 3, 9, 1, 5 };

for (int i = 0; i < pole.Length - 1; i++)
{
    for (int j = 0; j < pole.Length - 1 - i; j++)
    {
        // tady se porovnají pole[j] a pole[j + 1]
    }
}

To - i znamená „konec pole už je hotový, tam nechoď".

Kolik práce to dá? U pole s 10 prvky asi 45 porovnání. U 100 prvků už skoro 5 000. U 1 000 prvků skoro půl milionu. Deset prvků navíc stojí desetkrát víc práce — a to je ten problém.

Jedno vylepšení, které se vyplatí

Když v celém průchodu nedošlo ani k jednomu prohození, je pole už seřazené a nemá smysl pokračovat:

int[] pole = { 7, 3, 9, 1, 5 };

for (int i = 0; i < pole.Length - 1; i++)
{
    bool prohozeno = false;

    // ...vnitřní cyklus; při každém prohození nastav prohozeno = true;

    if (!prohozeno) break;   // nic se nehýbalo → pole je hotové
}

Na už seřazeném poli pak bubble sort skončí po jediném průchodu. Je to hezká ukázka toho, že i hloupý algoritmus se dá vyladit — ale rychlým se z toho nestane.

Blok 4Merge sort: rozděl a panuj1. Vysvětlení a souvislosti
        [7 3 9 1 5 8]
         /          \
    [7 3 9]       [1 5 8]
     /    \        /    \
   [7]  [3 9]    [1]  [5 8]
          |              |
        [3 9]          [5 8]
     \    /        \    /
    [3 7 9]       [1 5 8]
         \          /
        [1 3 5 7 8 9]     ← slévání zpátky

Merge sort má jediný nápad: dvě seřazené hromádky jdou slít dohromady velice rychle. Stačí se pořád dívat jen na dva vrchní prvky a brát menší z nich.

Algoritmus proto:

  1. rozdělí pole na dvě půlky,
  2. každou půlku seřadí stejným postupem (ano, zavolá sám sebe — rekurze),
  3. a obě seřazené půlky slije do jednoho.

Rozdělování končí u jednoprvkových kousků — jeden prvek je totiž seřazený sám o sobě. Tomu se říká dno rekurze a bez něj by program běžel donekonečna.

Pro pole 1 000 prvků udělá merge sort asi 10 000 porovnání místo půl milionu. Padesátkrát míň práce, a čím větší pole, tím větší náskok.

Jak funguje slévání

Máš [3 7 9] a [1 5 8]. Díváš se na první prvky obou: 3 a 1. Menší je 1 → jde ven a v pravé hromádce se posuneš dál. Pak porovnáš 3 a 5, ven jde 3. A tak dál, dokud jedna hromádka nedojde; zbytek druhé se prostě přilepí.

Pointa: každý prvek se podívá jen jednou. Proto je slévání tak levné.

Blok 5Quicksort: všechno stojí na pivotu1. Vysvětlení a souvislosti
[7 3 9 1 5 8]   pivot = 9
   menší: [7 3 1 5 8]   stejné: [9]   větší: []

[7 3 1 5 8]     pivot = 1
   menší: []    stejné: [1]   větší: [7 3 5 8]

[7 3 5 8]       pivot = 5
   menší: [3]   stejné: [5]   větší: [7 8]

Quicksort si vybere jeden prvek — pivot — a rozdělí podle něj pole na tři hromádky: menší než pivot, rovné pivotu, větší než pivot.

Pak stejným způsobem seřadí hromádku menších a hromádku větších a všechno slepí za sebe: menší + stejné + větší. Žádné slévání se neřeší, protože hromádky už jsou ve správném pořadí.

Volba pivota rozhoduje o všem. Když pivot rozdělí pole zhruba na polovic, je quicksort ještě rychlejší než merge sort. Když padne pokaždé na nejmenší prvek (třeba u už seřazeného pole a pivota zleva), zdegeneruje na bubble sort.

Proto se pivot obvykle bere ze středu nebo náhodně — my použijeme střed:

var data = new List<int> { 7, 3, 9, 1, 5, 8 };
int pivot = data[data.Count / 2];
Console.WriteLine($"Pivot je {pivot}.");
Blok 6Hotové řazení: to, co budeš používat doopravdy1. Vysvětlení a souvislosti

V praxi si řazení nikdy nepíšeš sám. .NET má tři nástroje:

  • Array.Sort(pole) — seřadí pole na místě, původní pořadí je pryč.
  • seznam.Sort() — totéž pro List<T>, taky na místě.
  • .OrderBy(...) — z LINQ; nezmění původní seznam, vrátí nový seřazený. Do závorky píšeš, podle čeho se má řadit.

Zápis j => j.Length se čte „pro každou položku j vezmi její délku". Je to nejkratší způsob, jak říct „řaď podle tohohle".

Chci… Použij
seřadit pole čísel Array.Sort(pole)
seřadit seznam na místě seznam.Sort()
nový seřazený seznam seznam.OrderBy(x => x).ToList()
řadit podle vlastnosti .OrderBy(p => p.cena)
od největšího .OrderByDescending(p => p.cena)
druhé kritérium .OrderBy(a).ThenBy(b)
Klikni vlevo na Spustit a objeví se tu výstup programu.
Proč se to tedy učíme ručně

Ze stejného důvodu, proč se ve škole počítá zpaměti, i když existuje kalkulačka. Až budeš řešit úlohu, na kterou hotový nástroj není, budeš potřebovat umět přemýšlet jako algoritmus — rozdělit problém, porovnat, poskládat zpátky. A hlavně: díky dnešku víš, že Array.Sort není magie, jen dobře napsaný kód.

Poznámky pro učiteleTechnické pozadí — jen pro vás1. Vysvětlení a souvislosti

Co je uvnitř Array.Sort. Od .NET Core 2.1 je to introsort: začne quicksortem, při příliš velké hloubce rekurze přepne na heapsort a malé úseky dodělá insertion sortem. Je to nestabilní řazení.

Stabilita. Array.Sort a List<T>.Sort nejsou stabilní — prvky se stejným klíčem mohou změnit vzájemné pořadí. LINQ OrderBy stabilní je. U inventáře v cvičení 5 to je vidět: při řazení podle vzácnosti zůstane u OrderBy původní pořadí předmětů uvnitř skupiny.

Porovnávání vlastních typů. List<T>.Sort() bez parametru vyžaduje, aby prvky uměly IComparable. U n-tic to funguje (porovnává se položka po položce), u vlastních tříd (16. hodina) ne — tam se předává Comparison<T>, tedy seznam.Sort((a, b) => a.cena.CompareTo(b.cena)).

CompareTo vrací tři stavy: záporné číslo (a je menší), nulu (rovnost), kladné číslo (a je větší). Prohozením a a b otočíte směr řazení. Dětem stačí vzor „menší → a.CompareTo(b), větší → b.CompareTo(a)".

Měření času. Stopwatch v prohlížeči funguje, ale čísla jsou v desítkách milisekund nepřesná (WASM, jiné rozlišení časovače). Proto ve cvičeních měříme hlavně počet porovnání — to je stejné číslo na každém počítači a nedá se zpochybnit.

Pozor na rekurzi v prohlížeči. Merge i quicksort jsou rekurzivní. Na polích do tisíce prvků je to v pohodě; kdo zkusí sto tisíc prvků s pivotem zleva na seřazených datech, může narazit na StackOverflowException.

2. Ukázky pro projekci

Blok 7Ukázka 1 — Pole jako graf a jedno prohození2. Ukázky pro projekci
Studenti zkuste změnit
  • Změň čísla v poli — zkus i nulu a číslo 20.
  • Vyměň znak za # nebo .
  • Prohoď místo prvních dvou prvků poslední dva.
  • Vypiš pole pozpátku (cyklus for od konce).
demo/demo1_graf.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.

Co si z toho odnést: graf je jen new string('█', hodnota) a řazení uvidíš podle toho, jak se sloupce postupně srovnávají do schodů.

Blok 8Ukázka 2 — Bubble sort krok za krokem2. Ukázky pro projekci
Studenti zkuste změnit
  • Zadej pole už seřazené. Kolik prohození proběhne?
  • Zadej pole seřazené pozpátku. To je nejhorší možný případ.
  • Přidej do pole další tři čísla. O kolik vzroste počet porovnání?
  • Vypiš stav pole po každém prohození, ne až po průchodu.
demo/demo2_bubble.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.

Co si z toho odnést: po každém průchodu je vpravo o jedno číslo „doma". A ten sloupeček počtů porovnání je jediné, co dnes opravdu porovnáváme.

Blok 9Ukázka 3 — Merge sort: rozděl a slij2. Ukázky pro projekci
Studenti zkuste změnit
  • Přidej do pole další čtyři čísla. O kolik řádků přibude?
  • Vypiš i to, jak se pole dělí (na začátku metody MergeSort).
  • Zkus pole, kde jsou všechna čísla stejná.
  • Porovnej počet porovnání s bubble sortem na stejných datech.
demo/demo3_merge.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.

Co si z toho odnést: nejvíc odsazené řádky jsou nejmenší hromádky. Postupují zdola nahoru — čím výš, tím větší a hotovější kousky pole.

Blok 10Ukázka 4 — Quicksort: pivot rozděluje2. Ukázky pro projekci
Studenti zkuste změnit
  • Změň pivot na data[0] (první prvek) a pusť to na seřazeném poli.
  • Vypiš u každého kroku i hloubku rekurze.
  • Zkus pole, kde je jedno číslo pětkrát. Co dělá hromádka „stejné"?
  • Porovnej počet porovnání s merge sortem na stejných datech.
demo/demo4_quick.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.

Co si z toho odnést: quicksort nic nesluje — hromádky jsou už ve správném pořadí, stačí je slepit za sebe pomocí AddRange.

Blok 11Ukázka 5 — Hotové řazení a inventář hrdiny2. Ukázky pro projekci
Studenti zkuste změnit
  • Přidej do inventáře svůj předmět a spusť znovu.
  • Seřaď inventář podle názvu místo ceny.
  • Zkus OrderByDescending podle ceny — co se změní?
  • Vypiš jen předměty dražší než 50 zlaťáků (.Where(p => p.cena > 50)).
demo/demo5_inventar.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.

Co si z toho odnést: OrderBy vyrobí nový seřazený seznam, Sort přerovná ten původní. Když se ti pořadí „záhadně" změnilo, sáhl jsi na Sort.

Blok 12Když se něco pokazí2. Ukázky pro projekci
Unhandled exception. System.IndexOutOfRangeException:
  Index was outside the bounds of the array.

Klasika bubble sortu. Vnitřní cyklus sáhne na pole[j + 1], takže j smí dojít nejvýš k pole.Length - 2. Podmínka proto musí být j < pole.Length - 1 - i, ne j < pole.Length.

Když si nejste jistí, vypište si j a j + 1 před porovnáním — hned uvidíte, kde to přeteče.

Druhý zdroj téhle chyby je GetRange v merge sortu: druhá půlka se bere jako data.GetRange(stred, data.Count - stred), tedy „od středu, tolik prvků, kolik zbývá". Napsat tam data.Count je nejčastější překlep hodiny.

Hláška / projev Co to znamená Jak to opravit
IndexOutOfRangeException vnitřní cyklus jde o jeden moc daleko j < pole.Length - 1 - i
ArgumentException u GetRange špatný počet prvků GetRange(stred, data.Count - stred)
StackOverflowException rekurze bez dna if (data.Count <= 1) return data;
pole se neseřadí prohození bez dočasné proměnné (a, b) = (b, a) nebo tři řádky s docasny
pole je seřazené pozpátku obrácené porovnání > řadí vzestupně, < sestupně
OrderBy nic neudělal výsledek se zahodil ulož ho: pole = pole.OrderBy(...).ToList();
List.Sort() hlásí InvalidOperationException prvky se neumí porovnat předej pravidlo: Sort((a, b) => a.cena.CompareTo(b.cena))
quicksort nikdy neskončí pivot zůstává v hromádce „menší" rovné prvky dávej do samostatné hromádky stejne

3. Cvičení

Blok 13Cvičení 1 — Bublinky pod lupou (15 min)3. Cvičení

Zadání:

Napiš bubble sort tak, aby bylo po každém průchodu vidět, co se v poli stalo.

  1. Dokonči metodu Vykresli a vypiš pole před řazením
    💡 Poradit
    Projdi pole cyklem foreach a každou hodnotu vypiš jako sloupec:
    Console.WriteLine($"{hodnota,3} │{new string('█', hodnota)}");
    {hodnota,3} zarovná číslo na tři znaky, aby byla svislítka pod sebou.
    Řádek čísel vyrobíš pomocí string.Join(" ", pole).
  2. Napiš vnitřní cyklus: porovnej každé dva sousedy a prohoď je
    💡 Poradit
    Cyklus musí skončit o jedno dřív, protože saháš i na pole[j + 1]:
    for (int j = 0; j < pole.Length - 1; j++)
    Uvnitř: if (pole[j] > pole[j + 1]) (pole[j], pole[j + 1]) = (pole[j + 1], pole[j]);
  3. Obal ho vnějším cyklem a po každém průchodu vypiš stav pole
    💡 Poradit
    Vnější cyklus: for (int i = 0; i < pole.Length - 1; i++)
    Vnitřní se smí pokaždé zkrátit, konec pole je už hotový:
    for (int j = 0; j < pole.Length - 1 - i; j++)
    Za vnitřním cyklem (ale ještě uvnitř vnějšího) vypiš string.Join(" ", pole).
  4. Přidej počitadla porovnani a prohozeni a vypiš je na konci
    💡 Poradit
    Obě proměnné si připrav před cykly: int porovnani = 0;
    porovnani++; patří hned před if — porovnání proběhne vždycky.
    prohozeni++; patří dovnitř if — prohodí se jen někdy.
  5. Zastav řazení dřív, když se v průchodu nic neprohodilo
    💡 Poradit
    Na začátku každého průchodu nastav bool prohozenoVPruchodu = false;
    Při každém prohození ji přepni na true.
    Na konci průchodu: if (!prohozenoVPruchodu) break;
Úroveň Co má zvládnout
Minimum Body 1–3, pole se seřadí a je vidět každý průchod
Rozšíření Body 4–5, počitadla a předčasné ukončení
Výzva Vypsat graf po každém průchodu, ne jen na konci
cviceni/cviceni1_bublinky.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Pro učitele — na co si dát pozor

Očekávaný výsledek: pole 9 4 7 1 6 3 se seřadí na 1 3 4 6 7 9 za 15 porovnání a 11 prohození, přičemž poslední průchod se už neudělá.

Kde se zaseknou:

  • j < pole.Length. Nejčastější chyba hodiny. Program spadne na IndexOutOfRangeException, protože poslední porovnání sáhne za konec pole. Nechte je to přečíst z hlášky — je naprosto srozumitelná.
  • Prohození bez dočasné proměnné. Kdo napíše pole[j] = pole[j+1]; a pak pole[j+1] = pole[j];, dostane dvě stejná čísla. Ukažte to na dvou sklenicích.
  • Počitadlo na špatném místě. porovnani++ uvnitř if počítá prohození, ne porovnání. Krásný důvod, proč se počitadla kontrolují ručním počítáním na malém poli.
  • Zapomenutý break. Bez něj to funguje, jen se udělá o průchod víc. Ať si vypíšou počet porovnání s ním i bez něj.

Kdo je hotový dřív: ať volá Vykresli po každém průchodu — vznikne animace řazení. A ať zkusí pole seřazené pozpátku: { 9, 7, 6, 4, 3, 1 }.

Otázka na závěr cvičení: „Proč se vnitřní cyklus s každým průchodem zkracuje?" Odpověď: protože největší čísla už probublala na konec a jsou na svém místě.

Řešení 1 — jedna z možných variant
reseni/reseni1_bublinky.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Blok 14Cvičení 2 — Slučuj jako merge sort (20 min)3. Cvičení

Zadání:

Celý merge sort stojí na jedné jediné dovednosti: slít dvě seřazené hromádky. Tu si nejdřív vyrob.

  1. Napiš hlavní cyklus metody Slij: dokud mají obě hromádky prvky, ber menší z vrchu
    💡 Poradit
    Potřebuješ dva ukazatele: int i = 0; do levé a int j = 0; do pravé hromádky.
    while (i < leva.Count && j < prava.Count)
    Uvnitř porovnej vrchy a ten menší přidej do výsledku:
    if (leva[i] <= prava[j]) { vysledek.Add(leva[i]); i++; }
    else { vysledek.Add(prava[j]); j++; }
  2. Dolij zbytek hromádky, která ještě něco má, a Slij vyzkoušej
    💡 Poradit
    Když jedna hromádka dojde, druhá je celá větší — stačí ji přilepit:
    while (i < leva.Count) { vysledek.Add(leva[i]); i++; }
    while (j < prava.Count) { vysledek.Add(prava[j]); j++; }
    Bez těchhle dvou řádků ti na konci budou chybět čísla.
  3. Doplň MergeSort: dno rekurze, rozdělení na půlky a volání sebe sama
    💡 Poradit
    Dno rekurze: if (data.Count <= 1) return data; — jeden prvek je seřazený sám o sobě.
    Rozdělení: int stred = data.Count / 2;
    var leva = MergeSort(data.GetRange(0, stred));
    var prava = MergeSort(data.GetRange(stred, data.Count - stred));
    Druhý parametr GetRange je počet prvků, ne index konce.
  4. Vypisuj každé slévání s odsazením podle hloubky rekurze
    💡 Poradit
    Přidej metodě parametr int hloubka a při rekurzi předávej hloubka + 1.
    Odsazení vyrobíš stejně jako sloupce: string odsazeni = new string(' ', hloubka * 2);
    Vypiš to až po slití: $"{odsazeni}[{string.Join(" ", leva)}] + [{string.Join(" ", prava)}] → [{string.Join(" ", spojene)}]"
  5. Přidej počitadlo porovnání a srovnej ho s bubble sortem na stejných datech
    💡 Poradit
    Proměnnou int porovnani = 0; si dej úplně nahoru, ať na ni obě metody vidí.
    porovnani++; patří dovnitř hlavního cyklu ve Slij, hned před if.
    Na bubble sort si napiš metodu, která vrátí jen počet porovnání — dovnitř si udělej kopii dat: var kopie = new List<int>(vstup);
Úroveň Co má zvládnout
Minimum Body 1–2, Slij správně slije dvě hromádky
Rozšíření Body 3–4, funkční merge sort s výpisem slévání
Výzva Bod 5 a odpověď na otázku, kdo vyhraje na 1 000 číslech
cviceni/cviceni2_merge.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Pro učitele — na co si dát pozor

Očekávaný výsledek: pole se seřadí na 1 2 3 4 6 7 8 9, merge sort udělá 17 porovnání, bubble sort 28. Rozdíl je malý schválně — pointa je v posledních dvou vypsaných řádcích o tisíci číslech.

Kde se zaseknou:

  • Zapomenuté dolití zbytku. Proto má krok 2 vlastní snímek, kde chybějící číslo doslova vidí. Je to nejlepší moment celé hodiny.
  • GetRange(stred, data.Count). Druhý parametr je počet prvků, ne index konce. Hlásí se ArgumentException.
  • Chybějící dno rekurze. StackOverflowException přijde okamžitě. Ptejte se: „Kdy už není co dělit?"
  • Výpis před slitím. Když vypíšou slévání na začátku metody, řádky přijdou v obráceném pořadí a strom nedává smysl.

Kdo je hotový dřív: ať do DATA přidá dalších osm čísel a spočítá, o kolik řádků výpis naroste. Nebo ať Slij upraví tak, aby řadila sestupně.

Otázka na závěr cvičení: „Proč je slévání dvou seřazených hromádek tak levné?" Odpověď: protože se na každý prvek koukneme jen jednou — nikdy se nevracíme.

Řešení 2 — jedna z možných variant
reseni/reseni2_merge.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Blok 15Cvičení 3 — Pivot v akci (20 min)3. Cvičení

Zadání:

Quicksort nic nesluje. Vybere si jedno číslo a podle něj celé pole rozhází na tři hromádky.

  1. V metodě QuickSort vyber pivot ze středu a připrav tři prázdné hromádky
    💡 Poradit
    Pivot bereme ze středu, protože zleva by to na seřazených datech dopadlo špatně:
    int pivot = data[data.Count / 2];
    Hromádky jsou obyčejné seznamy:
    var mensi = new List<int>();, var stejne = new List<int>();, var vetsi = new List<int>();
  2. Rozděl všechny prvky do hromádek a vypiš je
    💡 Poradit
    Projdi data cyklem foreach a každé číslo pošli do správné hromádky:
    if (hodnota < pivot) mensi.Add(hodnota);
    else if (hodnota > pivot) vetsi.Add(hodnota);
    else stejne.Add(hodnota);
    Rovné prvky musí jít do stejne, jinak by se pivot vracel pořád dokola.
  3. Doplň dno rekurze, rekurzivní volání a slep výsledek
    💡 Poradit
    Dno rekurze patří úplně nahoru: if (data.Count <= 1) return data;
    Slepení se dělá metodou AddRange, a to přesně v tomhle pořadí:
    vysledek.AddRange(QuickSort(mensi, hloubka + 1));
    vysledek.AddRange(stejne);
    vysledek.AddRange(QuickSort(vetsi, hloubka + 1));
  4. Přidej odsazení podle hloubky a počitadlo porovnání
    💡 Poradit
    string odsazeni = new string(' ', hloubka * 2); a při rekurzi předávej hloubka + 1.
    porovnani++; dej dovnitř foreach — každý prvek se s pivotem porovnává.
    Proměnnou int porovnani = 0; si připrav nahoře, ať na ni metoda vidí.
  5. Porovnej pivot ze středu a pivot zleva na už seřazeném poli
    💡 Poradit
    Přidej metodě parametr bool pivotZleva a hned za výběrem pivota:
    if (pivotZleva) pivot = data[0];
    Před každým během vynuluj počitadlo: porovnani = 0;
    Pusť obě varianty na new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 } a porovnej čísla i odsazení výpisu.
Úroveň Co má zvládnout
Minimum Body 1–2, rozdělení na tři hromádky se vypíše
Rozšíření Body 3–4, funkční quicksort s odsazeným výpisem
Výzva Bod 5 a vysvětlení, proč pivot zleva na seřazených datech propadne
cviceni/cviceni3_pivot.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Pro učitele — na co si dát pozor

Očekávaný výsledek: na náhodných datech 19 porovnání, na seřazených datech s pivotem ze středu 17 porovnání, ale s pivotem zleva 35 — a výpis se zláme do schodiště. To schodiště je celá pointa cvičení.

Kde se zaseknou:

  • Rovné prvky v hromádce mensi. Pivot se pak dostane do rekurze znovu a program běží donekonečna (v prohlížeči se prostě zasekne). Proto je hromádka stejne povinná.
  • Chybějící dno rekurze. Stejný projev. Ptejte se: „Kdy už není co dělit?"
  • Špatné pořadí AddRange. Když se stejne přilepí až na konec, výsledek je skoro seřazený — a to je zákeřné, protože to na první pohled vypadá dobře.
  • Výpis uvnitř rekurze u velkých polí. Kdo si zvětší data na stovky čísel, utopí se ve výpisu. Ať výpis zakomentuje.

Kdo je hotový dřív: ať spočítá i maximální hloubku rekurze (parametr hloubka si stačí zapamatovat do proměnné maximum). U pivota zleva na seřazených datech vyjde hloubka rovná počtu prvků.

Otázka na závěr cvičení: „Proč je pivot ze středu lepší než pivot zleva?" Odpověď: dělí pole na dvě podobně velké části, takže rekurze je mělká.

Řešení 3 — jedna z možných variant
reseni/reseni3_pivot.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Blok 16Cvičení 4 — Bubble sort versus Array.Sort (15 min)3. Cvičení

Zadání:

Teď to zkus naostro. Tisíc dvě stě čísel — a uvidíš, kde je hranice mezi „hloupým" a „hotovým".

  1. Vyrob pole POCET náhodných čísel pomocí new Random(42) a vypiš prvních deset
    💡 Poradit
    Číslo v závorce je semínko — se stejným semínkem dostaneš pokaždé stejná čísla.
    var generator = new Random(42);
    data[k] = generator.Next(1, 1000); vrátí číslo od 1 do 999.
    Prvních deset vypíšeš přes LINQ: string.Join(" ", data.Take(10))
  2. Napiš metodu BubbleSort, která pole seřadí a vrátí počet porovnání
    💡 Poradit
    Je to přesně ten kód z cvičení 1, jen zabalený do metody s návratovou hodnotou:
    int BubbleSort(int[] data) { ... return porovnani; }
    Pouštěj ho na kopii, ať máš pořád i původní data: int[] kopie = puvodni.ToArray();
  3. Seřaď druhou kopii pomocí Array.Sort a ověř, že výsledky jsou stejné
    💡 Poradit
    Array.Sort(kopieB); seřadí pole rovnou na místě, nic nevrací.
    Shodu ověříš jedním voláním z LINQ:
    bool stejne = kopieA.SequenceEqual(kopieB);
    Kdyby vyšlo false, máš v bubble sortu chybu.
  4. Změř obě řazení pomocí Stopwatch
    💡 Poradit
    Nahoru přidej using System.Diagnostics;
    var stopky = Stopwatch.StartNew();stopky.Stop();
    Výsledek přečteš jako stopky.Elapsed.TotalMilliseconds a zaokrouhlíš:
    $"{stopky.Elapsed.TotalMilliseconds:F1} ms"
  5. Vypiš pomocí LINQ pět největších čísel a pět nejmenších
    💡 Poradit
    puvodni.OrderByDescending(x => x).Take(5) vrátí pět největších.
    puvodni.OrderBy(x => x).Take(5) vrátí pět nejmenších.
    Obojí pak vypiš přes string.Join(" ", ...). Všimni si, že původní pole se nezměnilo.
Úroveň Co má zvládnout
Minimum Body 1–3, obě metody dají stejný výsledek
Rozšíření Bod 4, změřené časy obou řazení
Výzva Bod 5 a tabulka časů pro 300, 600 a 1 200 čísel
cviceni/cviceni4_souboj.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Pro učitele — na co si dát pozor

Očekávaný výsledek: bubble sort udělá kolem 719 000 porovnání, Array.Sort je řádově rychlejší. Přesné časy se liší podle počítače i prohlížeče — proto je důležitější to obrovské číslo porovnání než milisekundy.

Kde se zaseknou:

  • Zapomenutá kopie. Kdo pustí bubble sort přímo na puvodni, má pak Array.Sort seřazená data a kontrola shody je bezcenná. ToArray() je tu klíčové.
  • using System.Diagnostics; uprostřed souboru. Direktivy using musí být úplně nahoře, jinak CS1529.
  • Array.Sort přiřazený do proměnné. int[] x = Array.Sort(pole); se nepřeloží — metoda nic nevrací, řadí na místě.
  • Příliš velké POCET. Kdo si nastaví 50 000, bude v prohlížeči čekat minuty. Doporučená horní hranice je 3 000.

Kdo je hotový dřív: ať udělá tabulku pro 300, 600 a 1 200 čísel. Při zdvojnásobení počtu vyskočí počet porovnání zhruba čtyřikrát — to je ta „druhá mocnina" názorně.

Otázka na závěr cvičení: „Proč měříme počet porovnání, a ne jenom čas?" Odpověď: čas závisí na počítači, počet porovnání je na každém stroji stejný.

Řešení 4 — jedna z možných variant
reseni/reseni4_souboj.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Blok 17Cvičení 5 — CsQuest: chytrý inventář (25 min)3. Cvičení

Zadání:

Hrdina nasbíral hromadu věcí a batoh je v nepořádku. Zařiď, ať si ho umí srovnat podle čehokoliv.

  1. Vytvoř inventář jako seznam n-tic a vypiš ho jako tabulku
    💡 Poradit
    Typ napiš celý: var inventar = new List<(string nazev, int cena, string vzacnost)>();
    Předmět přidáš jako trojici: ("Lektvar zdraví", 25, "běžný")
    V metodě Vypis zarovnej názvy: polozka.nazev.PadRight(22) a cenu {polozka.cena,4}.
  2. Seřaď kopii inventáře bubble sortem podle ceny
    💡 Poradit
    Kopii vyrobíš takhle: var kopie = new List<(string nazev, int cena, string vzacnost)>(inventar);
    Porovnává se jen jedna položka n-tice: if (kopie[j].cena > kopie[j + 1].cena)
    Prohození funguje úplně stejně jako u čísel: (kopie[j], kopie[j + 1]) = (kopie[j + 1], kopie[j]);
  3. To samé jedním řádkem přes OrderByDescending a vypiš tři nejcennější
    💡 Poradit
    inventar.OrderByDescending(p => p.cena).Take(3).ToList()
    Zápis p => p.cena znamená „u každé položky se dívej na její cenu".
    OrderBy původní seznam nemění — vrací nový.
  4. Seřaď podle vzácnosti a uvnitř každé skupiny podle ceny sestupně
    💡 Poradit
    Abeceda by dala špatné pořadí, tak si vzácnostem přiděl čísla:
    var PORADI = new Dictionary<string, int> { ["legendární"] = 0, ["vzácný"] = 1, ["běžný"] = 2 };
    Pak se řadí dvakrát za sebou:
    inventar.OrderBy(p => PORADI[p.vzacnost]).ThenByDescending(p => p.cena).ToList()
  5. Vypiš souhrn: celkovou hodnotu, nejdražší předmět a počty podle vzácnosti
    💡 Poradit
    Součet i maximum umí LINQ: inventar.Sum(p => p.cena) a inventar.Max(p => p.cena).
    Nejdražší předmět dostaneš jako první z seřazeného seznamu:
    var nejdrazsi = inventar.OrderByDescending(p => p.cena).First();
    Počty spočítej cyklem přes PORADI.Keys a inventar.Count(p => p.vzacnost == klic).
Úroveň Co má zvládnout
Minimum Body 1–2, inventář se vypíše a seřadí podle ceny
Rozšíření Body 3–4, LINQ a řazení podle dvou kritérií
Výzva Bod 5 plus filtr „ukaž jen věci dražší než 50 zlaťáků"
cviceni/cviceni5_inventar.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Pro učitele — na co si dát pozor

Očekávaný výsledek: čtyři vypsané tabulky (původní pořadí, podle ceny, tři nejcennější, podle vzácnosti) a souhrn: 7 předmětů, 615 zlaťáků, nejdražší Dračí šupina.

Kde se zaseknou:

  • Zapomenutý .ToList(). OrderBy vrací IEnumerable, ale metoda Vypis chce List. Hláška CS1503 je poměrně dlouhá — ukažte, kde v ní je jádro.
  • Řazení vzácnosti podle abecedy. Vyjde „běžný, legendární, vzácný", což je nesmysl. Odtud je jen krok k pochopení, proč se dělá číselné pořadí.
  • Dlouhý typ n-tice. List<(string nazev, int cena, string vzacnost)> se opakuje pořád dokola. Zmiňte, že v 16. hodině to nahradí jedna třída Predmet a bude z toho List<Predmet>.
  • Změna n-tice na místě. predmet.cena = 0; uvnitř foreach nejde — n-tice v seznamu se musí přepsat celá: seznam[i] = (nazev, novaCena, vzacnost);

Kdo je hotový dřív: ať přidá filtr inventar.Where(p => p.cena > 50).ToList() a ať zkusí seřadit podle poměru cena/vzácnost — tedy vymyslet vlastní pravidlo a obhájit ho.

Otázka na závěr cvičení: „Kdy se vyplatí OrderBy a kdy Sort?" Odpověď: OrderBy když chci původní pořadí zachovat, Sort když ho chci přepsat.

Řešení 5 — jedna z možných variant
reseni/reseni5_inventar.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.

4. Domácí úkol

Blok 18Domácí úkol na příští hodinu4. Domácí úkol

Vyrob program „Žebříček hrdinů". Připrav si pět hrdinů jako n-tice (jméno, úroveň, zlato) a pak:

  • vypiš hrdiny tak, jak jsi je zadal,
  • seřaď je vlastním bubble sortem podle úrovně od nejvyšší,
  • totéž udělej jedním řádkem přes OrderByDescending a ověř, že vyšlo to samé,
  • přidej druhé kritérium: při stejné úrovni rozhoduje zlato (ThenByDescending),
  • a ke každému hrdinovi dokresli sloupec z podle jeho úrovně.

Přines soubor Program.cs nebo snímek obrazovky s výstupem.

Pro učitele

Nejcennější bod je ten třetí — porovnání vlastního řadicího kódu s LINQ. Dítě si má na vlastní oči ověřit, že dvacet řádků bubble sortu a jeden řádek OrderByDescending dají stejný výsledek. To je celá pointa dnešní hodiny.

Na začátku příští hodiny promítněte dva tři žebříčky a zeptejte se, co se stane, když mají dva hrdinové stejnou úroveň. Odtud je krok k ThenByDescending a k pojmu stabilní řazení.

Kdo nemá doma .NET, může úkol udělat rovnou tady v prohlížeči a poslat zkopírovaný výstup.

Zobrazit ukázkové řešení domácího úkolu

Toto je jedna z možných variant, jak mohl úkol dopadnout. Tvoje řešení se může lišit a to je v pořádku.

domaci/domaci_ukol13.cs
Tady se objeví výstupKlikni na Spustit. Kód vlevo můžeš měnit a spouštět opakovaně klávesami Ctrl+Enter.
Kód vlevo jde upravovat. Tab odsazuje o čtyři mezery.
Blok 19Oficiální dokumentace4. Domácí úkol

Všechno, co jsme dnes použili, je popsané na Microsoft Learn. Odkazy jsou anglicky — klidně si je nech přeložit prohlížečem, kód je stejný v každém jazyce.

Téma Odkaz
Array.Sort learn.microsoft.com — Array.Sort Method
List<T>.Sort learn.microsoft.com — List<T>.Sort Method
OrderBy a ThenBy learn.microsoft.com — Enumerable.OrderBy
Řazení v LINQ learn.microsoft.com — Sorting Data (LINQ)
Random learn.microsoft.com — Random Class
Stopwatch learn.microsoft.com — Stopwatch Class
N-tice learn.microsoft.com — Tuple types

5. Umíme informatiku

Blok 20Co dnes umíš navíc5. Umíme informatiku

Dnešek nebyl o tom naučit se nazpaměť tři algoritmy. Byl o jedné myšlence: stejný úkol se dá vyřešit chytře nebo hloupě, a ten rozdíl je vidět v číslech.

  • Umíš porovnat a prohodit dva prvky a víš, proč je potřeba dočasná proměnná.
  • Napsal jsi bubble sort a spočítal jsi, kolik práce ho stojí.
  • Rozumíš principu rozděl a panuj — merge sort rozdělí, seřadí a slije.
  • Víš, co je pivot a proč na jeho volbě u quicksortu záleží.
  • Používáš Array.Sort, List.Sort a LINQ OrderBy a víš, čím se liší.
  • A hlavně: umíš změřit, který postup je lepší, místo abys hádal.

Tohle je přesně to, co dělá informatik. Ne že by znal víc příkazů — ale že se umí zeptat „kolik práce to doopravdy dá?" a odpověď si ověřit.

Příště se podíváme na to, jak si všechny ty seřazené inventáře a žebříčky uložit do souboru, aby nezmizely, když program skončí.