Dal abstraktn datov typy Abstraktn datov typy ADT
Další abstraktní datové typy
Abstraktní datové typy (ADT) • abstraktní datový typ – datová struktura + operace nad touto strukturou • jsou většinou dynamické – velikost (počet prvků) se dynamicky mění • vkládání • výběr prvků
ADT • • • spojový seznam fronta zásobník množina strom obecný graf
Fronta • datová struktura typu FIFO – First In - First Out • prvky se odebírají v pořadí, jak byly vkládány
Možné implementace • jako spojový seznam – „neomezená“ velikost fronty (omezená pouze velikostí dostupné paměti) – operace vložení prvku znamená vložení prvku na konec seznamu – operace výběr prvku je výběr z čela fronty • polem pevné délky – fronta s omezenou velikostí – implementuje se jako tzv. kruhová fronta
Implementace pomocí spoj. seznamu • fronta reprezentována ukazatelem na čelo fronty a poslední prvek ve frontě • prvek fronty nese hodnotu a ukazatel na další prvek ve frontě
typedef struct TPrvek { int x; TPrvek *dalsi; } TPrvek; typedef struct { TPrvek *celo; TPrvek *konec; } TFronta;
3 celo konec 5 10 8 konec celo konec NULL vloz(3) vloz(5) vloz(10) vyjmi() – vrátí hodnotu z čela fronty - 3 vloz(8)
Operace nad frontou • void init(TFronta *f), eventuálně TFronta *init() – inicializace, vytvoří prázdnou frontu • int je_prazdna(TFronta *f) – testuje na prázdnou frontu • void vloz(TFronta *f, int prvek) – vloží prvek na konec fronty • int vyjmi(TFronta *f) – vyjme prvek z čela fronty • void zrus(TFronta *f)
int vyjmi(TFronta *f) { int prvek; TPrvek *pom; if (f->celo == NULL) return -1; prvek = f->celo->x; pom = f->celo; f->celo = f->celo->dalsi; free(pom); return prvek; }
• spoléhat se na návratovou hodnotu -1, pokud je fronta prázdná, není nejlepší řešení – nepoznám, zda byla vyjmuta -1 nebo byla prázdná fronta – lépe by bylo např. nastavovat chybovou proměnnou • odstranění problému je ve správném používání knihovních funkcí – před každým voláním funkce vyjmi() aplikace otestuje, zda není fronta prázdná – např. while (!je_prazdna(&f))
Implementace pomocí pole pevné délky • fronta je reprezentována polem a indexem čela a konce fronty • fronta má pevnou délku a je implementována jako kruhová (modulo délka pole) • je nutné implementovat ještě dotaz, zda je fronta plná
Fronta délky n = 3 0 10 3 1 5 2 8 celo: 1 0 konec: 0 2 1 prázdná plná vloz(3) vloz(5) vyjmi() – vrátí hodnotu z čela fronty - 3 vloz(8) vloz(10)
• index celo ukazuje na prvek pole na čele fronty, který bude odebrán • index konec ukazuje na prvek v poli, kam se zapíše nový prvek • pokud se indexy celo a konec rovnají, je fronta buď plná nebo prázdná – podle rovnosti indexů není možné rozlišit stav fronty – musím tedy tyto stavy uchovávat zvlášť – pravidlo: pokud se po přidání prvku indexy rovnají, je fronta plná (obdobně: prázdná)
typedef struct { int *pole; int n; /* velikost pole */ int celo, konec; int plna, prazdna; } TFronta 2; int init(TFronta 2 *f, int vel) { f->prazdna = 1; f->plna = 0; f->n = vel; f->celo = f->konec = 0; f->pole = (int*)malloc(sizeof(int)*vel); if(f->pole!=NULL) return 1; return 0; }
int je_prazdna(TFronta 2 *f) { return f->prazdna; } int vloz(TFronta 2 *f, int prvek) { if (!f->plna) { f->prazdna = 0; f->pole[f->konec]=prvek; f->konec = (f->konec+1)% f->vel; if (f->konec == f->celo) f->plna=1; return 1; } return 0; }
void zrus(TFronta 2 *f) { free(f->pole); } Poznámka: • implementace pomocí pole, které by se zvětšovalo dynamicky při zaplnění fronty, by byla mírně komplikovaná. Proč?
Zásobník (Stack) • datová struktura typu LIFO – Last In - First Out • nejprve se vybírá prvek, který byl vložen na vrchol (top) zásobníku jako poslední • operace: – PUSH (uložení hodnoty na vrchol zásobníku) – POP (odebrání hodnoty z vrcholu zásobníku)
• někdy bývá implementována také operace TOP – zjištění hodnoty na vrcholu zásobníku bez odebrání prvku POP PUSH vrchol
Možné implementace • na principu spojového seznamu – „neomezená“ velikost zásobníku (omezená pouze velikostí dostupné paměti) – operace vložení prvku znamená vložení prvku na konec seznamu – operace výběr prvku je výběr z konce seznamu • polem (pevné nebo proměnné délky) – kruhová implementace není potřebná
Implementace na principu spojového seznamu • zásobník reprezentován ukazatelem na vrchol zásobníku • prvek nese hodnotu a ukazatel na předchozí prvek v zásobníku
typedef struct TPrvek { int x; TPrvek *predch; } TPrvek; typedef struct { TPrvek *vrchol; } TZasob;
2 4 1 vrchol NULL vloz(2) vloz(4) vloz(1) vyjmi() – vyjme a vrátí hodnotu z vrcholu zásobníku - 1 vloz(1)
Operace nad zásobníkem • void init(TZasob *z), eventuálně TZasob *init() – inicializace, vytvoří prázdný zásobník • int je_prazdny(TZasob *z) – test na prázdný zásobník • void push(TZasob *z, int prvek) – vloží prvek na vrchol zásobníku • int pop(TZasob *z) – vyjme prvek z vrcholu zásobníku • void zrus(TZasob *z)
TZasob* init() { TZasob *zas =(TZasob*)malloc(sizeof(TZasob)); zas->vrchol = NULL; return zas; } int pop(TZasob *z) { int prvek; TPrvek *pom; if (z->vrchol != NULL) { prvek = z->vrchol->x; pom = z->vrchol->predch; free(z->vrchol); z->vrchol = pom; return prvek; } return -1; }
Použití zásobníku ( v této implementaci): TZasob *zas; zas = init(); push(zas, 3); push(zas, 4); pop(zas); zrus(zas); free(zas);
Implementace pomocí pole • obdobně jako fronta • implementace je snazší – není třeba implementovat „kruhově“ – stačí index na vrchol • zde je efektivní i implementace s dynamicky se měnící velikostí pole přidávání prvku
Množina • jazyk C neobsahuje datový typ množina Požadavek: • implementovat množinu takovým způsobem, aby operace vložení a vyjmutí prvku, testu, zda je prvek v daném množině, měly operační složitost O(1) – konstantní • prvek je v množině obsažen „pouze jednou“
• běžně se množina reprezentuje bitovým polem • prvek s indexem i je prvkem množiny, je-li bit nahozen (tzv. charakteristická funkce) • tato reprezentace umožňuje snadnou implementaci operací: – vložení prvku nahozením bitu pomocí operace or – vyjmutí prvku nulováním bitu pomocí operace and – test na existenci prvku pomocí operace and – vytvoření doplňku pomocí operace negace – sjednocení pomocí operace or – atd.
• protože je velikost paměti počítače konečná, musí být i univerzum konečné • implementaci množiny si budeme demonstrovat na množině ASCII znaků
• počet ASCII znaků je 256 – bitové pole musí mít 256 bitů, tj. 256/8 = 32 bajtů (slabik) typedef struct { unsigned char pole[32]; } TMnoz. Znaku; Poznámka: – efektivnější by bylo použít pole typu int o velikosti 256/(sizeof(int)*8), z pedagogických důvodů použijeme typ char, kde je velikost pole dána konstantou
Vyprázdnění množiny • operace vyprázdnění množiny představuje vynulování všech prvků pole void vyprazdni(TMnoz. Znaku *m) { memset(m->pole, 0, 32); } Doplněk množiny • doplněk množiny vytvoříme bitovou negací celého pole
void doplnek(TMnoz. Znaku *m) { for(int i=0; i<32; i++) m->pole[i] = ~(m->pole[i]); } Vložení prvku • je třeba nastavit příslušný bit – pomocí operace logického součtu s konstantou • pro výpočet adresy nastavovaného bitu musíme vytvořit mapovací funkci
Příklad mapovací funkce - tabulka kód znaku slabika bit ve slabice 0 0 0 1 7 0 7 8 1 0 9 1 1
• adresa slabiky – celočíselné dělení číslem 8 sl = i / 8 • adresa bitu ve slabice – zbytek po celočíselném dělení číslem 8 bit = i % 8 • s příslušným bitem ve slabice budeme pracovat pomocí masky, kterou získáme operací posuvu konstanty 1 o počet bitů, který je roven adrese bitu ve slabice
void vloz(TMnoz. Znaku *m, unsigned char znak) { int sl = znak / 8; unsigned char maska = 1 << (znak % 8); m->pole[sl] |= maska; } Vyjmutí prvku • při vyjímání prvku je maska negována a provádí se operace logického součinu (and, &)
Průnik dvou množin • operace logického součinu nad bitovými poli obou množin void doplnek(TMnoz. Znaku *m 1, TMnoz. Znaku *m 2, TMnoz. Znaku *m 3) { for(int i=0; i<32; i++) m 3 ->pole[i] = m 1 ->pole[i] & m 2 ->pole[i]; }
Jakou použijeme operaci při implementaci sjednocení dvou množin? logický součet Jak bude vypadat testování přítomnosti prvku v množině? if (m->pole[sl] & maska != 0)
Stromy Strom: • souvislý graf bez kružnic • využití: – počítačová grafika – seznam objektů – efektivní vyhledávání – výpočetní stromy (rozhodování, …)
otec (rodič, předchůdce) kořen vnitřní uzel potomek (syn) list
Binární strom • každý uzel má maximálně dva potomky • uspořádaný binární strom – uzly jsou ohodnoceny prvky (čísly, …) – potomek na levé straně má vždy menší hodnotu nebo rovnu než rodič syn – potomek na pravé straně má vždy větší hodnotu – uspořádané binární stromy se využívají zejména jako vyhledávací stromy; složitost hledání je v průměrném případě log 2 n
Uspořádaný binární strom 10 5 2 12 7 11
• binární strom je nejčastěji reprezentován ukazatelem na kořen (jde o ukazatel na uzel stromu) • uzel je reprezentován strukturou: – prvkem, nesoucí informaci – ukazatelem na levý podstrom – ukazatelem na pravý podstrom • operace nad stromem jsou rekurzivní typedef struct TUzel { int hodnota; TUzel *levy; TUzel *pravy; } TUzel;
• list má ukazatele na levý a pravý podstrom nastaveny na NULL • operace nad stromem – procházení stromu (může být spojeno s nějakou akcí) • do šířky • do hloubky – hledání prvku ve stromě – vložení nového prvku jako nový list – vyjmutí prvku – rušení stromu
Procházení stromu do hloubky 10 5 2 12 7 11
Procházení stromu do šířky 10 5 2 12 7 11
Obecný algoritmus procházení binárního stromu do hloubky void projdi(TUzel *u) { if (u==NULL) return; akce(u->hodnota); projdi(u->levy); projdi(u->pravy); }
Varianty procházení stromu do hloubky • left order – levý podstrom, zpracování uzlu, pravý podstrom • right order – pravý podstrom, zpracování uzlu, levý podstrom • preorder – zpracování uzlu, levý podstrom, pravý podstrom • další permutace, mají-li smysl
Příklad – left order • výpis prvků uspořádaného stromu void vypis(TUzel *u) { if (u==NULL) return; vypis(u->levy); printf(″%d ″, u->hodnota); vypis(u->pravy); }
Příklad – pre order (reprezentace výrazu stromem) * + 2 - 5 11 7
• strom reprezentuje výraz (2+5)*(11 -7) • výpis v pre-order formě (polská notace) * + 2 5 – 11 7 void vypis_pre(TUzel *u) { if (u==NULL) return; printf(″%c ″, u->hodnota); vypis_pre(u->levy); vypis_pre(u->pravy); }
Poznámka: • stromová reprezentace se běžně používá v překladačích programovacích jazyků
Hledání prvku • vrací 1, je-li hodnota nalezena int najdi(TUzel *u, int x) { if (u==NULL) return 0; if (u->hodnota==x) return 1; if (x < u->hodnota) return najdi(u->levy, x); else return najdi(u->pravy, x); }
Vložení nového prvku void pridej(TUzel **u, int x) { if (*u == NULL) { *u = (TUzel*)malloc(sizeof(TUzel)); (*u) -> hodnota = x; (*u) -> levy = NULL; (*u) -> pravy = NULL; } else if (x <= (*u)->hodnota) pridej(&((*u)->levy), x); else pridej(&((*u)->pravy), x); }
Zrušení stromu void zrus(TUzel *u) { if (u==NULL) return; zrus(u->levy); zrus(u->pravy); free(u); }
void main(void) { TUzel *strom = NULL; pridej(&strom, 10); pridej(&strom, 5); pridej(&strom, 7); pridej(&strom, 2); pridej(&strom, 11); najdi(strom, 5); zrus(strom); }
Jaký vznikne strom nyní? void main(void) { TUzel *strom = NULL; pridej(&strom, 10); pridej(&strom, 5); pridej(&strom, 7); pridej(&strom, 2); pridej(&strom, 11); pridej(&strom, 12); }
A nyní? void main(void) { TUzel *strom = NULL; pridej(&strom, 12); pridej(&strom, 11); pridej(&strom, 10); pridej(&strom, 7); pridej(&strom, 5); pridej(&strom, 2); }
Snaha je vytvořit vyvážený strom, kde výška levé a pravé větve se liší maximálně o 1. To musí platit pro libovolný podstrom. Pokud tomu tak není, strom se vyvažuje. 10 7 5 2 12 11
10 12 5 2 7 11
- Slides: 60