Abstraktn datov typy Abstraktn datov typy ADT abstraktn
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 budeme probírat samostatně
Spojový seznam • určen pro dynamický seznam (lexikon), kde není znám počet prvků seznamu a jejich počet se často mění (vkládání, mazání) – např. v MS Windows je takto uložen seznam ovladačů sdílejících jeden vektor přerušení
Spojový seznam • používá se také pro uložení obecného grafu konec zacatek 3 1 3
• implementace v C: typedef struct TPolozka { int prvek; TPolozka *dalsi; pokud se při deklaraci struktury odkazujeme na ni, } TPolozka; napíšeme jméno ještě před „{“ typedef struct { TPolozka *zacatek; TPolozka *konec; } TSeznam;
TSeznam sez 1; void Init(TSeznam *seznam) { seznam -> zacatek = NULL; seznam -> konec = NULL; }
Vložení na konec seznamu 1) dynamicky vytvořím novou položku (ukazatel na další položku nastavím na NULL) konec zacatek 3 1 3 5 nova
Vložení na konec seznamu 2) ukazatel dalsi u posledního prvku seznamu nastavím na novou položku konec zacatek 3 1 3 5 nova
Vložení na konec seznamu 3) posunu ukazatel na konec seznamu konec zacatek 3 1 3 5 nova Pozor, vkládám-li do prázdného seznamu
void Vloz_na_konec(TSeznam *seznam, int cislo) { TPolozka *nova; nova = (Tpolozka*)malloc(sizeof(Tpolozka)); nova -> prvek = cislo; nova -> dalsi=NULL; if (seznam -> zacatek != NULL) { seznam -> konec -> dalsi = nova; seznam -> konec = nova; } else { seznam -> konec = nova; seznam -> zacatek = nova; } }
Vložení do seznamu za prvek 1) dynamicky vytvořím novou položku prvek zacatek 3 1 5 nova konec 3
Vložení do seznamu za prvek 2) prováži nový prvek se seznamem prvek zacatek 3 1 5 nova konec 3
Obousměrný spojový seznam • položka obsahuje ukazatel na předchůdce i následníka zacatek 3 konec 1 3
Úkol • naimplementujte spojový seznam (jednosměrně nebo obousměrně vázaný), do kterého se ukládají jména a telefonní čísla • naimplementujte tyto procedury a funkce: • • inicializace prázdného seznamu test, zda je seznam prázdný vložení záznamu na konec seznamu vložení záznamu za prvek (parametrem je ukazatel na prvek, za který se vkládá, a nové jméno a telefonní číslo)
• hledání záznamu (podle jména i telef. čísla) vrací ukazatel na záznam, pokud není v seznamu, vrací NULL • zjištění ukazatele na první záznam • zjištění ukazatele na následující záznam • výmaz prvku (parametrem je ukazatel na existující prvek, který se má vymazat) • zrušení celého seznamu • napište jednoduchou konzolovou aplikaci pro otestování implementace seznamu; data zadávejte z klávesnice – nemusíte využít všechny funkce
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;
celo konec NULL
celo konec NULL vloz(3)
3 celo konec vloz(3)
3 celo konec vloz(3) vloz(5)
3 5 celo konec vloz(3) vloz(5)
3 5 celo konec vloz(3) vloz(5) vloz(10)
3 celo vloz(3) vloz(5) vloz(10) 5 10 konec
3 celo vloz(3) vloz(5) vloz(10) 5 10 konec
3 celo 5 10 konec vloz(3) vloz(5) vloz(10) vyjmi() – vrátí hodnotu z čela fronty - 3
5 10 celo konec vloz(3) vloz(5) vloz(10) vyjmi() – vrátí hodnotu z čela fronty - 3
5 10 celo konec vloz(3) vloz(5) vloz(10) vyjmi() – vrátí hodnotu z čela fronty - 3 vloz(8)
5 10 8 celo konec 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 1 2 celo: 0 konec: 0 prázdná plná
Fronta délky n = 3 0 1 2 celo: 0 konec: 0 prázdná vloz(3) plná
Fronta délky n = 3 0 3 1 2 celo: 0 konec: 1 prázdná vloz(3) plná
Fronta délky n = 3 0 3 1 2 celo: 0 konec: 1 prázdná vloz(3) vloz(5) plná
Fronta délky n = 3 0 3 1 5 2 celo: 0 konec: 2 prázdná vloz(3) vloz(5) plná
Fronta délky n = 3 0 3 1 5 2 celo: 0 konec: 2 prázdná plná vloz(3) vloz(5) vyjmi() – vrátí hodnotu z čela fronty - 3
Fronta délky n = 3 0 3 1 5 2 celo: 1 konec: 2 prázdná plná vloz(3) vloz(5) vyjmi() – vrátí hodnotu z čela fronty - 3
Fronta délky n = 3 0 3 1 5 2 celo: 1 konec: 2 prázdná plná vloz(3) vloz(5) vyjmi() – vrátí hodnotu z čela fronty - 3 vloz(8)
Fronta délky n = 3 0 3 1 5 2 8 celo: 1 konec: 0 prázdná plná vloz(3) vloz(5) vyjmi() – vrátí hodnotu z čela fronty - 3 vloz(8)
Fronta délky n = 3 0 3 1 5 2 8 celo: 1 konec: 0 prázdná plná vloz(3) vloz(5) vyjmi() – vrátí hodnotu z čela fronty - 3 vloz(8) vloz(10)
Fronta délky n = 3 0 10 1 5 2 8 celo: 1 konec: 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č?
K čemu se dá fronta jako ADT využít: • fronta událostí v diskrétních simulačních systémech – dopravy – počítačových sítí – číslicových systémů – systémů hromadné obsluhy - fronta zákazníků • algoritmy procházení grafu do šířky
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
K čemu se dá zásobník jako ADT využít: • obrácení pořadí příchozích dat • ukládání dat při eliminaci rekurze • algoritmy procházení grafu do hloubky
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)
• pokud je univerzum velké nebo nelze prvkům přiřadit indexy pole, pak je nutné množinu implementovat méně efektivně binárním stromem se operací hledání se složitostí log 2 n, kde n je počet uzlů stromu (aktuální počet prvků množiny) – musí být definováno uspořádání na univerzu, tj. je nutné umět porovnat dva prvky a rozlišit, který je menší, jinak je nutné použít neuspořádané pole prvků nebo spojový seznam
- Slides: 75