Datové struktury
Úvod
Datový typ je název pro množinu hodnot a množinu operací, které lze nad danými hodnotami provádět. Datový typ určuje, jak se data ukládají do paměti a jak s nimi pracovat.
Základní kategorie datových typů
| Kategorie | Popis | Příklad |
|---|---|---|
| Prázdný | Nemá žádné definované operace ani způsob ukládání dat. | void |
| Jednoduchý | Ukládá data přímo do adresního prostoru. | int, char, boolean |
| Ordinální | Mezi prvky je určené pořadí, lze je porovnávat. | int, enum |
| Neordinální (nominální) | Mezi prvky není určené pořadí; identifikují se názvem. | boolean, výčtové typy bez vnitřního řazení |
| Strukturovaný | Skládá jednoduché datové typy do struktury. | pole, řetězec, struktura (struct) |
| Abstraktní (ADT) | Má pouze definici (rozhraní), ale ne konkrétní implementaci. | zásobník, fronta, seznam, strom |
Abstraktní datový typ (ADT)
Abstraktní datový typ je určen pouze svojí definicí – říká co dělá, ne jak to dělá. Odděluje rozhraní od implementace. ADT se označují jako kontejnery a měly by definovat minimálně tyto operace:
- Vytvoření prázdného kontejneru
- Zjištění počtu prvků
- Přístup k prvkům
- Vložení prvku
- Odstranění prvku
- Vymazání všech prvků
Jednoduché struktury
Pole
Pole (anglicky Array) je strukturovaný datový typ, který seskupuje pevný počet položek stejného typu. Prvky jsou rozlišovány pomocí indexů – jeden index odkazuje právě na jednu položku.
Vlastnosti pole
- Homogenní – všechny prvky mají stejný datový typ.
- Pevná délka – velikost je dána při vytvoření a nelze ji měnit.
- Indexované – k prvkům se přistupuje přes celočíselný index, typicky od \(0\) do \(n-1\).
- Přímý přístup – přístup k libovolnému prvku je \(O(1)\).
Dynamické pole
Dynamické pole je ADT kontejner nad polem, který odstraňuje omezení fixní velikosti. Jednotlivé prvky jsou ukládány ve vnitřním poli:
- Jakmile je kapacita naplněna, alokuje se nové, větší pole (zpravidla dvakrát větší kvůli amortizaci).
- Pokud dlouho zůstává neobsazené, může se alokovat menší pole.
Díky amortizaci je složitost vložení prvku \(O(1)\) – občasné překopírování do většího pole se rozloží mezi mnoho levných vložení.
Textový řetězec
Textový řetězec je strukturovaný datový typ, který bývá chápán jako pole znaků. Většina moderních jazyků ho však implementuje jako samostatný, neměnný (immutable) typ s bohatou sadou operací: spojování, hledání podřetězců, dělení, nahrazování, změna velikosti písmen atd.
Reprezentace řetězce
- V C: pole znaků ukončené nulovým znakem
\0(char[]). - V Javě: objekt
String– neměnný, internovaný (stejné literály sdílí instanci). - Unicode: moderní jazyky podporují znaky mimo ASCII – jeden "znak" může zabírat více bajtů (UTF-8, UTF-16).
Ukazatel
Ukazatel (anglicky Pointer) je abstraktní datový typ, který uchovává adresu v operační paměti. Ukazatel má svůj typ, který odpovídá typu dat uložených na odkazované adrese – int* ukazuje na data typu int.
K čemu jsou ukazatele
- Dynamická alokace paměti –
malloc/freev C,new/deletev C++. - Předávání odkazem – funkce může modifikovat data volajícího.
- Datové struktury – spojové seznamy, stromy, grafy.
- Pole – v C je pole v podstatě konstantní ukazatel na první prvek.
Rizika ukazatelů
- Dangling pointer – ukazatel na již uvolněnou paměť.
- Memory leak – zapomenutí uvolnit dynamicky alokovanou paměť.
- Null pointer dereference – přístup přes
NULL/nullptr→ pád programu.
Výčet
Výčet (anglicky Enum) je ordinální datový typ, který definuje pevnou množinu pojmenovaných konstant. Slouží k omezení hodnot, kterých může proměnná nabývat – např. dny v týdnu, stavy automatu, typy logovacích úrovní.
enum Den {
PONDELI,
UTERY,
STREDA,
CTVRTEK,
PATEK,
SOBOTA,
NEDELE
};
V Javě jsou výčty plnohodnotné třídy – mohou mít atributy, metody a konstruktor:
enum Den {
PONDELI("Pondělí"),
UTERY("Úterý"),
STREDA("Středa");
private final String nazev;
Den(String nazev) { this.nazev = nazev; }
public String getNazev() { return nazev; }
}
Lineární struktury
Lineární datové struktury organizují prvky v jedné linii – každý prvek má (až na krajní) právě jednoho předchůdce a jednoho následníka. Liší se tím, odkud lze přidávat a odebírat.
Seznam
Seznam (anglicky List) je homogenní, lineární a dynamický ADT, který je zobecněním zásobníku a fronty – narozdíl od nich dovoluje přidávat a odebírat prvky na libovolné pozici.
Typy seznamů podle propojení
- Jednosměrný – každý uzel odkazuje jen na následníka.
- Obousměrný – každý uzel odkazuje na předchůdce i následníka.
- Kruhový – konec odkazuje na začátek a začátek na konec.
Jednosměrný kruhový seznam
graph LR
A[Začátek] --> C[1] --> E[2] --> F[...] --> B[Konec]
B --> A
Spojový seznam vs. pole
| Operace | Pole | Spojový seznam |
|---|---|---|
| Přístup podle indexu | \(O(1)\) | \(O(n)\) |
| Vložení na začátek | \(O(n)\) | \(O(1)\) |
| Vložení na konec | \(O(1)\) amort. / \(O(n)\) | \(O(1)\) s tail / \(O(n)\) |
| Mazání uprostřed | \(O(n)\) | \(O(1)\) při znalosti uzlu |
| Paměťová režie | Minimální | Ukazatele navíc |
LIFO – Zásobník (Stack)
Zásobník (anglicky Stack) je homogenní, lineární a dynamický ADT fungující na principu LIFO – Last In, First Out (poslední vložený, první odebraný). Data se ukládají v pořadí vložení, ale čtení probíhá od nejnovějšího po nejstaršímu.
Operace zásobníku
push– vloží prvek na vrchol zásobníku.pop– odebere a vrátí prvek z vrcholu zásobníku.top(peek) – vrátí prvek na vrcholu bez odebrání.is_empty– zjistí, zda je zásobník prázdný.
Zásobník lze implementovat pomocí pole (s proměnnou top ukazující na vrchol) nebo spojového seznamu. Při implementaci polem o pevné délce je nutné hlídat přetečení (overflow).
Použití zásobníku
- Vyhodnocování výrazů – převod infixové notace na postfixovou, výpočet RPN.
- Rekurze – každé rekurzivní volání se ukládá na zásobník volání (call stack).
- Undo/Redo – historie akcí v editorech.
- Backtracking – ukládání cesty při prohledávání do hloubky.
FIFO – Fronta (Queue)
Fronta (anglicky Queue) je homogenní, lineární a dynamický ADT fungující na principu FIFO – First In, First Out (první vložený, první odebraný). Data se ukládají v pořadí vložení a čtení probíhá ve stejném pořadí.
Operace fronty
enqueue(push) – vloží prvek na konec fronty.dequeue(pop) – odebere a vrátí prvek ze začátku fronty.front(peek) – vrátí první prvek bez odebrání.is_empty– zjistí, zda je fronta prázdná.
Frontu lze implementovat pomocí pole či spojového seznamu. Při implementaci polem je vhodné použít kruhový buffer, aby se zabránilo posouvání prvků.
Použití fronty
- Fronty zpráv – RabbitMQ, Kafka (viz distribuované programování).
- BFS – prohledávání do šířky v grafu.
- Plánování procesů – round-robin v operačních systémech.
- Vyrovnávací paměť – buffering mezi producentem a konzumentem.
Kruhový Buffer
Kruhový buffer (anglicky Circular Buffer) je speciální případ fronty implementované pomocí pole, používaný jako vyrovnávací paměť mezi producentem a konzumentem. Díky své implementaci nabízí operace v \(O(1)\) bez nutnosti realokace.
Princip kruhového bufferu
Kruhový buffer tvoří jedno pole a dva ukazatele:
head– ukazuje na první obsazený prvek.tail– ukazuje na první volný prvek.- Při přidání prvku se
tailinkrementuje (v kruhu – po dosažení konce pole se vrací na začátek). - Při odebrání se inkrementuje
heada původní prvek se uvolní.
Plný vs. prázdný buffer
Když head == tail, buffer je buď prázdný, nebo plný. Rozlišení se řeší buď ponecháním jednoho volného místa, nebo pomocnou proměnnou count.
Nelineární struktury
Strom
Strom je abstraktní datová struktura, která reprezentuje souvislý, neorientovaný, acyklický graf. Každý vrchol má \(n\) potomků a každý potomek má právě \(1\) rodiče (kromě kořene, který rodiče nemá).
Vlastnosti stromu
- \(n\)-arita – kolik potomků může mít každý vrchol (binární strom = max. 2).
- Hloubka – počet hladin od kořene k nejvzdálenějšímu listu.
- Šířka – počet vrcholů na dané hierarchické úrovni.
- Pravidelnost – zda mají všechny vrcholy na stejné úrovni stejný počet potomků.
- Vyváženost – zda je strom udržován automaticky vyvážený (např. AVL, Red-Black).
Běžné operace se stromy
- Hledání, přidání a odebrání vrcholu
- Přidání podstromu (roubování – grafting)
- Odebrání podstromu (prořezávání – pruning)
- Hledání kořene
- Dotazování vlastností (hloubka, šířka, výška…)
- Procházení stromu
Procházení stromu
Stromy lze procházet dvěma základními způsoby:
- Do šířky (BFS): po patrech – od kořene přes všechny jeho potomky, pak jejich potomky…
- Do hloubky (DFS): po větvích – tři varianty podle pořadí návštěvy kořene:
- Pre-order: Kořen → levý podstrom → pravý podstrom.
- In-order: Levý podstrom → kořen → pravý podstrom (v BST dává seřazené pořadí).
- Post-order: Levý podstrom → pravý podstrom → kořen.
Binární strom
Binární strom je strom, jehož každý vrchol má maximálně 2 potomky (levý a pravý). Je základem pro binární vyhledávací stromy, haldy a další struktury.
Halda
Halda (anglicky Heap) je případ binárního stromu, který navíc splňuje jednu z podmínek haldy:
- Max-heap: Hodnota potomka je vždy menší než hodnota jeho rodiče. Kořen obsahuje maximum.
- Min-heap: Hodnota potomka je vždy větší než hodnota jeho rodiče. Kořen obsahuje minimum.
Halda se chová jako prioritní fronta – umožňuje rychle získat a odebrat prvek s nejvyšší prioritou.
Binární halda
Binární halda je binární strom, pro který platí:
- Je vyvážený – listy se liší maximálně o jednu úroveň.
- Poslední úroveň se zaplňuje zleva doprava.
Díky těmto vlastnostem lze haldu reprezentovat v poli: pro prvek na indexu \(i\) jsou jeho potomci na \(2i+1\) a \(2i+2\), rodič na \(\lfloor (i-1)/2 \rfloor\).
Operace na haldě
- Vložení (insert): Přidej prvek na konec (první volné místo v poslední úrovni) a nech ho probublat nahoru (bubble-up, sift-up) – porovnávej s rodičem, a pokud je ve špatném vztahu, prohoď je. Opakuj, dokud není vlastnost haldy obnovena.
- Odebrání kořene (extract-min/max): Nahraď kořen posledním prvkem, odstraň poslední prvek, a nech nový kořen probublat dolů (bubble-down, sift-down) – porovnávej s potomky, prohoď s tím ve správném vztahu. Opakuj.
- Složitost: Obě operace \(O(\log n)\) – výška haldy je \(\log n\).
Množina
Množina (anglicky Set) je ADT, který negarantuje pořadí prvků, ale garantuje jejich jedinečnost – každý prvek se v množině vyskytuje nejvýše jednou.
Implementace množiny
- Naivní: Seznam nebo pole, kde se při vkládání kontroluje duplicita průchodem všech prvků. Složitost vložení \(O(n)\).
- Hašovací tabulka (HashSet): Prvky se ukládají podle hashe – vkládání, mazání i vyhledávání v průměru \(O(1)\).
- Binární vyhledávací strom (TreeSet): Prvky jsou seřazené – operace v \(O(\log n)\).
Tabulka
Tabulka (anglicky Map, Dictionary, Associative Array) je ADT, který ukládá dvojice klíč–hodnota. Každý klíč je unikátní a mapuje se právě na jednu hodnotu. Na rozdíl od pole, kde jsou indexy celá čísla, v tabulce může být klíčem libovolný typ (řetězec, objekt…).
Základní operace
put(key, value)– vložení nebo přepsání hodnoty pro daný klíč.get(key)– získání hodnoty podle klíče.remove(key)– odstranění dvojice podle klíče.containsKey(key)– ověření existence klíče.
Implementace tabulky
- Hašovací tabulka (HashMap): Klíč se zahašuje na index do pole. Při kolizi (dva klíče se zahashují na stejný index) se použije spojový seznam (separate chaining) nebo otevřená adresace. Složitost v průměru \(O(1)\).
- Binární vyhledávací strom (TreeMap): Dvojice jsou uložené v BST seřazeném podle klíčů – operace v \(O(\log n)\).
- Trie: Stromová struktura, kde klíč je rozložen po znacích – efektivní pro řetězcové klíče (prefixové vyhledávání, autocomplete).