84
KLOG Jonathan L. Verner Úvod do programování část II: Algoritmy skripta

à vod do programovánà (Ä Ã¡st II: Algoritmyweb.ff.cuni.cz/~vernjaff/cdn/alg110006/prednasky.pdf · byla podporenaˇ projektem OPVK CZ.1.07/2.2.00/28.0216 Logika: ... Skripta

  • Upload
    letuyen

  • View
    216

  • Download
    1

Embed Size (px)

Citation preview

  • The development of mathematics in the direction of greater exactness has as is well known led to large tracts of it becoming formalized, so that proofs can be carried out accorda few mechanical rules. The most comprehensive formal systeyet set up are, on the one hand, the system of Principia Mathematica (PM)2 and, on the other, the axiom system for setheory of Zermelo-Fraenkel (later extended by J. v. Neumann)These two systems are so extensive that all methods of proof uin mathematics today have been formalized in them, i.e. redua few axioms and rules of inference. It may therefore be surmthat these axioms and rules of inference are also sufficient to all mathematical questions which can in any way at all be expressed formally in the systems concerned. It is shown belothis is not the case, and that in both the systems mentioned thare in fact relatively simple problems in the theory of ordinary

    KLOG

    V

    OD

    DO

    PR

    OG

    RA

    MO

    V

    N

    Jonathan L. Verner

    vod do programovnst II: Algoritmy

    J.L.

    VER

    NER

    L-systm nebo tak Lindenmayerv systm je varianta formln gramatiky, vyvinut pro modelovn rstu rostlin. L-systm popisuje pravidla pro vvoj rostliny, kter se opakovan aplikuj na vznikajc model. Tato pravidla mohou nap. popisovat, za jakch podmnek se stonek rostliny rozdvoj, zda m vzniknout list nebo zda m st rostliny uhynout. Vsledn model se me nap. vykreslit jako obrzek nebo se z nj vytvo potaov 3D model rostliny. L-systmy se tak daj pout pro generovn rznch kivek, fraktl nebo pro modelovn bunnch organism.

    Wikipedie

    Obrzek na oblce vznikl trojnsobnou aplikac pravidla F=FF-[-F+F+F]+[+F-F-F] na axiom ++F. Psmeno F zna krok dopedu, znak + zna otoen vlevo o 60, znak - otoen vpravo o 16, znak [ zapamatovn bodu a znak ] nvrat k zapamatovanmu bodu.

    L-Systm(obrzek na oblce)

    skripta

  • Jonathan L. Verner, PhD.Katedra LogikyFilozofick fakulta UKPalachovo nmest 2116 18 Praha 1

    Jonathan L. Verner, 2015

    Tato ucebnice slou jako skripta k letnmu semestru prednky vod do programovn, vy-psan pod kdem ALG110006 na Katedre logiky FF UK v letech 20112015. Jejich prpravabyla podporena projektem OPVK CZ.1.07/2.2.00/28.0216 Logika: systmov rmec rozvoje oboruv CR a koncepce logickch propedeutik pro mezioborov studia

  • vod do programovn

    cst II: Algoritmy

  • 4

  • Obsah

    1 Porovnvn algoritmu, Eukleiduv algoritmus 9

    2 Zkladn datov struktury 17

    3 Vyhledvn, trdc algoritmy 33

    4 Analza jazyka 47

    5 Grafov algoritmy 65

    6 Sloitost podruh 77

    Lid 83

    5

  • OBSAH OBSAH

    6

  • vod

    V situaci, kdy je k dispozici mnostv velmi dobrch vodnch i pokrocilch uceb-nic programovn se mue zdt zbytecn pst ucebnici novou. Chtel bych protohned na vod podotknout, e pri psan techto skript jsem nemel ambici napsatnejlep ucebnici; dokonce jsem ani nemel ambici napsat ucebnici originln.Skripta vznikala v podstate jako prprava na letn semestr prednky vod doprogramovn pro prvn rocnk studentu logiky na Filozofick fakulte UK. Jejichhlavnm prnosem je, e pokrvaj vcemne presne prednesenou ltku. Pres-toe jsou skripta clena zejmna na m studenty, doufm, e i prpadnmu jinmuctenri budou k prospechu, vyprovokuj v nem zjem o informatiku a treba ho po-vedou k dal cetbe jiste mnohem lepch textu. Z neprebernho vberu bych zdechtel vyzdvihnout dve knky skvelou ceskou knku P. Tpfera: Algoritmy aprogramovac techniky ([To95]) a klasiku oboru The Art of Computer Programming odD. Knutha.

    Na zver vodu patr podekovn. Chtel bych predne podekovat sv ene Ane zacenn pripomnky (a jej lsku, ale to je jin prbeh. . .), Tomi Lavickovi za upo-zornen na chyby v priloench algoritmech a konecne tak Evropsk unii, kter vrmci projektu OPVK CZ.1.07/2.2.00/28.0216 Logika: systmov rmec rozvoje oboruv CR a koncepce logickch propedeutik pro mezioborov studia podporila psan techtoskript. O tento projekt se na na katedre skvele starali Marta Blkov a Michal Dan-ck. A nakonec bych chtel podekovat tomu, kter ns stvoril a vybavil schopnosttvorit svety virtuln.

    Autor

    7

  • OBSAH OBSAH

    8

  • Kapitola 1

    Porovnvn algoritmu,Eukleiduv algoritmus

    Mm clem v zimnm semestru bylo Vs naucit pst jednoduch programy v Py-thonu a mt nejakou konkrtn predstavu o tom, co to prakticky znamen pro-gramovat. V letnm semestru se podvme na programovn z teoretictejho hle-diska. Nam zkladnm tmatem budou ruzn algoritmy standardn (ale i ne-standardn) postupy reen ruznch problmu, se ktermi se jako programtorimuete potkat.

    Pojdme se nyn spolecne podvat na jednu skupinu takovch problmu. Jedn se oproblmy, kter puvodne pochzej z matematick disciplny zvan Teorie csel. Vpraxi jsou zce provzny s modernmi ifrovacmi metodami (RSA, Elektronickpodpis, ...). Asi nejvce fascinujcm problmem je nsledujc loha:

    1.1 loha. Naleznete rozklad danho csla n na soucin prvocsel.

    s n zce souvis loha, ve kter mme rozhodnout, zda cslo je, ci nen prvocslem:

    1.2 loha. Pro dan cslo n rozhodnete, zda je prvocslem nebo nen.

    K tto loze se mon, pokud zbyde cas, vrtme v nejak pozdej prednce. Dnesse podvme na jednodu lohu, kter bv jednou ze zkladnch operacch pro-vdench pri reen sloitejch problmu v teorii csel.

    1.3 loha. Pro dan csla n,m zjistete jejich nejvetho spolecnho delitele.

    9

  • KAPITOLA 1. POROVNVN ALGORITMU, EUKLEIDUV ALGORITMUS

    Pokud bychom chteli lohu reit na poctaci, pravdepodobne by ns jako prvnnapadlo proste projt vechna csla od 1 do min{n,m} a najt nejvet cslo, kterdel obe csla najednou. V Pythonu by takov algoritmus mohl vypadat treba takto:

    1 def gcdN(n,m):2 ret = 13 for d in range(2,n+1):4 if d % n == 0 and d % m == 0:5 ret = d6 return ret

    Mon, e nekter z Vs se s touto lohou setkali na stredn kole. Tam jste se mohlidozvedet o postupu, kter popsal ji Euklides (300 pr. Kr.): vezmeme vet z oboucsel a spoctme si zbytek po delen menm z obou csel. Pokud je zbytek 0, jsmehotovi (nsd je men z obou csel). V opacnm prpade provedeme tot, tentokrt smenm cslem a spoctanm zbytkem. V Pythonu lze pomoc rekurze zapsat tentopostup nsledovne:

    1 def gcdE(n,m):2 if n > m:3 n,m = m,n4 z = m % n5 if z == 0:6 return n7 return gcdE(z,n)

    Pokud bychom se chteli vyhnout rekurzi, mohli by stejn algoritmus vypadat na-prklad jako vpisu 1.4.

    Algoritmus 1.4 Eukleiduv algoritmus (nerekurzivn verze)

    1 #coding: utf-82 def gcdE_nre(n,m):3 if n > m:4 n,m = m,n5 z = m % n67 while not (z == 0):8 m = n9 n = z

    10 z = m % n1112 return n

    U tohoto algoritmu nemus bt na prvn pohled jasn, jak funguje. Dokonce nen

    10

    #coding: utf-8def gcdE_nre(n,m): if n > m: n,m = m,n z = m % n

    while not (z == 0): m = n n = z z = m % n

    return n

    Jonathan VernerSource code for Eukleiduv algoritmus (nerekurzivn verze)

  • KAPITOLA 1. POROVNVN ALGORITMU, EUKLEIDUV ALGORITMUS

    jasn, zda vubec skonc a pokud ano, zda d sprvnou odpoved. Jednou monost,jak zjistit, jestli algoritmus funguje, je vyzkouet ho na nekolika vstupech a zkontro-lovat odpovedi. To nm vak ned odpoved s naprostou jistotou. Abychom si bylinaprosto jist, musme algoritmus pochopit a presvedcit se, e opravdu funguje. Oto se nyn pokusme.

    Nejprve si ukeme, e algoritmus opravdu skonc. Prvn, rekurzivn verze algo-ritmu, nm dv i nvod: u rekurzivnch algoritmu dokeme, e skonc, pokud sepresvedcme, e skonc v nejakm zkladnm prpade (prpadech) a vechny ostatnvstupy postupne prevede na tento zkladn prpad(y). U naeho algoritmu budezkladnm prpadem situace, kdy n del m (co budeme znacit n|m). Ukeme, erekurzivn voln postupne prevede kad vstup na tuto zkladn situaci. Oznacmesi n0 = n a m0 = m poctecn vstupy a nk,mk hodnoty promennch d, n pri k-tmpruchodu rdkem 7 (t.j. nk amk budou hodnoty parametru pri k-tm (rekurzivnm)voln funkce gcdE).

    Vimneme si, e 0 nk+1 = mk (mod nk) < nk. Tedy posloupnost nk je zdolaomezen nulou a ostre kles. Tud mus bt konecn, tedy program mus skon-cit. Tud algoritmus provede rdek 7 pouze konecnekrt a tedy v konecnm caseskonc.

    Kdy u vme, e algoritmus skonc, musme se jete presvedcit, e d sprvnouodpoved. Opet vyuijeme rekurzivn verzi. Pokud algoritmus skonc v zklad-nm prpade (rdek 6), zjevne d sprvnou odpoved. Pokud jete ukeme, e ina rdku 7 zskme sprvnou odpovet, t.j. e gcdE(n,m) = gcd(z,n), budemehotovi. Posledn rovnost vak plyne z nsledujcch jednoduchch pozorovn:

    Pokud d|n a d|m pro nejak m = kn+ z, pak i d|z.

    Pokud naopak d|n a d|z, pak d del kad m tvaru kn+ z.

    Tedy, je-li m = kn + z pak kad spolecn delitel csel n,m je i spolecnmdelitelem csel n, z a obrcene.

    Tm jsme odpovedeli na zkladn otzku, kterou si pri seznamovn s novm algo-ritmem musme poloit zda pracuje sprvne. Jak jsme videli, i v takto jednodu-chm prpade jde o pomerne komplikovan proces. Mohlo by Vs napadnout, zdaby nelo tento proces nejak automatizovat. To by se hodilo zejmna ve sloitejchprpadech, kdy je treba nejen zkontrolovat sprvnost algoritmu, ale zkontrolovati jeho bezchybnou implementaci. Obecne je odpoved , jak si ukeme v poslednprednce, nanetest zporn. Nicmne v nekterch konkrtnch prpadech to lze.Tmto problmem se zabv oblast informatiky zvan formln verifikace. My se

    11

  • KAPITOLA 1. POROVNVN ALGORITMU, EUKLEIDUV ALGORITMUS

    vak nyn obrtme k dalm otzkm, kter je treba poloit. Uvedli jsme si dva al-goritmy pro stejnou lohu a je prirozen se ptt, zda jsou oba stejne dobr. Tomuse budeme venovat v nsledujcm oddle.

    Porovnvn algoritmu

    Proto, abychom oba ve popsan algoritmy mohli porovnvat, je treba zvolit ne-jak kritrium. Kritri mue bt samozrejme mnoho, nsledujc seznam zdalekanen vycerpvajc:

    rychlost

    pametov nrocnost

    jednoduchost, srozumitelnost

    elegance

    energetick nrocnost (opravdu !)

    V naem kurzu se budeme zamerovat na prvn dva body (a zejmna na prvn bod),ackoliv jsou situace, kdy mnohem duleitejm kritriem mue bt treba jednodu-chost nebo energetick nrocnost.

    Pokusme se tedy oba algoritmy porovnat z hlediska rychlosti. Asi nejjednodumzpusobem, jak to provst, je merit cas T od puten programu do jeho skoncen. Tovak m sv skal:

    tento cas bude zviset na rychlosti danho poctace, jeho zatenosti, operac-nm systmu, prekladaci, interpretu ...

    je to experimentln velicina o kter lze teko matematicky neco dokazovat

    Radeji tedy tuto zkladn ideu trochu upravme vahy abstrahujeme od kon-krtnho poctace a konkrtn implementace. Nebudeme merit cas, ale urcme simnoinu zkladnch operac (prirazen, testovn booleovsk podmnky (if), arit-metick operace) a budeme poctat pocet operac, kter program pri svm behu vy-kon. Oznacme si toto csloOp. Je zjevn, e toto cslo bude zviset na vstupnch da-tech. Pokud budeme poctat nejvetho spolecnho delitele csel 1235468127495138a 2159842611264567486 budeme muset provst vce operac ne pri poctn spolec-nho delitele csel 14 a 35. Tedy Op bude ve skutecnosti funkce vstupnch dat.

    12

  • KAPITOLA 1. POROVNVN ALGORITMU, EUKLEIDUV ALGORITMUS

    Problm s funkc Op je ten, e mnohdy je obtn j vyjdrit nejakm jednoduchmvzoreckem. Casto pro ns proto budou duleit spodn a horn odhady tto funkce.Zkusme nyn spoctat OpN a OpE dvou ve uvedench algoritmu. Zacneme naiv-nm algoritmem. Ten nejprve provede jedno prirazen na rdku 2. Dle se proveden 1-krt cyklus, tedy n 1-krt se bude testovat podmnka na rdku 4. V cstitechto prpadu se navc provede prirazen na rdce 5. Tedy mme

    OpN (n,m) = 1 + (n 1)+?

    Zbv spoctat clen ?, t.j. v kolika prpadech se provede rdek 5. Pokud jsou n,mnesoudeln, neprovede se nikdy. Dolnm odhadem tedy je 0. Pokud by se rdek 5provedl pokad, pak by se provedl n1-krt, co je tedy horn odhad. Presn cslozvis na n am a nelze je jednodue vyjdrit. Mohli bychom se pokusit o statistickouanalzu, nicmne neudelme to (zjemce ji mue nalzet v Taoc P, [TAOCP2]).Mueme tedy ucinit nsledujc zver:

    n = 1 + (n 1) OpN (n,m) 1 + 2(n 1) = 2n 1

    Podvejme se nyn na druh algoritmus. Zde je situace o trochu sloitej. Pri ka-dm voln funkce gcdE dochz ke dvema porovnnm (2. a 5. rdek), jedn arit-metick operaci (4. rdek) a pri plne prvnm voln jete ke dvema prirazenm(3. rdek, uvedomte si, e jsou to prirazen dve a e se tento rdek v dalch vo-lnch nikdy neprovede). Oznacme si tedy pocet voln funkce gcdE cslem c. Pakmueme pst:

    OpE(n,m) = 2 + 3c

    Jak spocst cslo c? Opet budeme pouze odhadovat. Doln odhad je 1 v prpade, en del m. Jednoduch horn odhad dostaneme z vah, kter jsme provdeli pri du-kazu sprvnosti algoritmu. Zjistili jsme, e pri kadm rekurzivnm voln je prvnargument ostre men ne pri predchozm. Funkce gcdE se tedy zavol nejvce n-krt. Tedy mme odhad

    5 = 2 + 3 OpE(n,m) 2 + 3n

    Na zklade tohoto hornho odhadu bychom mohli usoudit, e prvn naivn al-goritmus je ve skutecnosti dokonce lep ne ten sloitej Eukleiduv. Musme sivak uvedomit, e skutecn hodnota OpE(n,m) mue bt (a, jak za chvli uvidme,opravdu je) mnohem men ne ve uveden odhad. K odvozen lepho hornho

    13

  • KAPITOLA 1. POROVNVN ALGORITMU, EUKLEIDUV ALGORITMUS

    odhadu zavedeme nsledujc znacen. Necht nk,mk je hodnota parametru pri k-tm voln funkce gcdE. Pak plat nsledujc:

    mk+1 = nk

    nk+1 mk/2

    Prvn bod je zrejm uvaujme nad druhm. Vyjdeme z toho, e nk+1 = mk mod nk.Je-li mk >= 2nk, pak mk/2 >= nk, zatmco zbytek po delen nk je vdy < nk, tedyv tomto prpade bod plat. Je-li naopak mk (nk, 2nk) [uvedomme si, e mk > nk],pak mk div nk = 1, tedy mk mod nk = mk nk. Nyn opet jednodue nk+1

  • KAPITOLA 1. POROVNVN ALGORITMU, EUKLEIDUV ALGORITMUS

    Protoe veliciny T aOp typicky zvis na vstupnch datech (t.j. jsou funkcemi nejakpromenn n) a pro mal n je tato nerovnost casto nezajmav, budeme se zajmat olimitn formu tto nerovnosti. Zavedme nyn nsledujc znacen. Jsou-li f, g funkcepromenn n budeme pst

    f(n) = O(g(n)) (K,n0)(n > n0)(f(n) Kg(n))

    a

    f(n) = (g(n)) (K,n0)(n > n0)(g(n)/K f(n))

    Prvn definice rk, e pro dostatecne velk n je funkce f shora omezen funkc ga na nejakou multiplikativn konstantu. Podobne druh definice rk, e je tatofunkce omezen zdola a na multiplikativn konstantu. O nejakm algoritmu Apracujcm s daty reprezentovanmi promennou n rekneme, e m sloitostO(g(n)),pokud OpA(n) = O(g(n)) a zroven OpA(n) = (g(n)). Vimneme si, e v tako-vm prpade bude i TA(n) = O(g(n)) a TA(n) = (g(n)), tedy funkce g v tako-vm prpade verne popisuje reln chovn algoritmu. Funkce g, se ktermi se se-tkme nejcasteji, jsou nsledujc: 1, n, log2 n, n, n log2 n, n2, 2n. Temto funkcm od-povd i klasifikace algoritmu na algoritmy v konstantnm case (O(1)), algoritmylogaritmick (O(log2 n)), algoritmy linern (O(n)), algoritmy kvadratick (O(n2))resp. polynomiln (O(P (n)), kde P je nejak polynom) a algoritmy exponencilnO(2n). Algoritmy, kter maj nejve polynomiln sloitost, se obecne povauj zaefektivn (zle samozrejme na konkrtnch okolnostech), zatmco algoritmy expo-nenciln jsou vesmes nepouiteln (uvedomme si, e pokud exponenciln algo-ritmus na nekterm poctaci doke zpracovat data nejve velikosti n, na dvakrtrychlejm poctaci zpracuje data pouze o jedna vet velikosti n+ 1).

    Kdy jsme nyn zavedli klasifikaci algoritmu vidme, e Eukleiduv algoritmus mlogaritmickou sloitost, zatmco naivn algoritmus m sloitost linern.

    15

  • KAPITOLA 1. POROVNVN ALGORITMU, EUKLEIDUV ALGORITMUS

    16

  • Kapitola 2

    Zkladn datov struktury

    V tto kapitole se budeme ble zabvat ruznmi zpusoby, jak v pameti ukldatdata a jak s nimi pracovat. Naucme se pracovat s frontou a zsobnkem a ukemesi zajmavou strukturu, kter se rk halda.

    Fronta

    Zacneme nsledujc, mon trochu roubovanou, situac. Ve vaem meste podnikpopulrn zelinr. Protoe prodv kvalitn ovoce a zeleninu, kter sm nakupujeod farmru, podnikn se mu dar a jeho malou samoobsluhu navtevuje stle vcea vce lid. S tm vak prichz i neprjemn fenomn fronty. Zelinr je nadendo modernch technologi a rozhodne se problm reit za pomoci elektronickhozkaznickho odbavovacho systmu. Predstavuje si to takto: zkaznk si naloveci do koku a pak si u jednoho ze stojanu, roztrouench po obchode, vyzvedneporadov cslo. Se svm kokem se pohodlne usad do k tomu celu pristavenchkresel a cek, ne ho zkaznick systm vyvol. Protoe zelinr poctacum prlinerozum, rozhodl se Vs najmout, abyste mu naprogramovali software, kter budecel systm rdit:

    2.1 Problm. Naprogramujte software pro zkaznick odbavovac systm.

    Ponechme stranou technick detaily tisku csel a podobne a soustredme se na jdroproblmu. Budeme potrebovat dve funkce. Funkci generujListek, kter zkaz-nkovi u stojanu pridel cslo a funkci naRade, kterou bude volat zelinr, kdy bude

    17

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    chtt obslouit dalho zkaznka. V podstate jde o to, naprogramovat takovou elek-tronickou frontu. To mueme jednodue udelat treba takto: budeme si proste v ne-jak promenn, treba last, uchovvat posledn cslo, kter jsme nejakmu zkaz-nku pridelili a v druh promenn current, si budeme uchovvat cslo zkaznka,kter je prve obsluhovn. V Pythonu pak bude program vypadat treba takto:

    1 current=02 last=034 def generujListek():5 global last6 last = last + 17 return last89 def naRade():

    10 global last, current11 if current == last:12 return None13 else:14 current = current + 115 return current

    Predstavte si nyn, e by se poadavky trochu zmenily. Csla jsou prli anonymna zelinr rd lidi oslovuje jmnem. Proto by chtel, aby uivatel do systmu mohlzadat sv jmno. Systm ho pak bude vyvolvat jmnem. N vchoz programjednodue upravme:

    18

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    1 current=02 last=03 jmena=[]45 def generujListek( jmeno ):6 global last,jmena7 last = last + 18 jmena.append(jmeno)9

    10 def naRade():11 global last,jmena, current12 if current == last:13 return None14 else:15 current = current + 116 return jmena[current-1]

    Problm s tmto prstupem je ten, e jmna se nm budou v pameti kupit a kupit akupit . . . Po nejakm case dojde pamet, odbavovac systm spadne a v obchode sestrhne mela. To je neprjemn. Bude proto lep program trochu prepsat. Tentokrtbudeme pouvat pomocnou promennou fronta, kde budeme mt seznam jmen.Kadho novho zkaznka pridme na konec tohoto seznamu a zkaznky, kterbudeme odbavovat, budeme naopak brt ze zactku. Program tedy vypad takto:

    1 fronta=[]23 def generujListek( jmeno ):4 global fronta5 fronta.append(jmeno)67 def naRade():8 global fronta9 if len(fronta) == 0:

    10 return None11 return fronta.pop(0)

    Vimnete si funkce pop, se kterou jsme se jete nesetkali. Voln seznam.pop(n)vrt n-t prvek seznamu seznam a pak ho ze seznamu smae. Kdybychom ji chtelisami naprogramovat, vypadala by treba takto:

    19

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    1 def pop(seznam, index):2 ret = seznam[index]3 del seznam[index]4 return ret

    Zkusme se nyn podvat na sloitost naeho programu. V podstate jde o to, jak jsousloit funkce append a funkce pop.

    Jejich rychlost zvis na tom, jak Python uvnitr realizuje datov typ list. Pythonov-sk list je ve skutecnosti realizovn jako neco, cemu se v jinch programovacch ja-zycch rk pole. Kdy Pythonu reknete, aby vytvoril list o n-prvcch, Python si na tov pameti vyhrad n po sobe jdoucch chlvecku. Kdy chceme pristoupit k i-tmuprvku pole, je treba zjistit, kde v pameti se nachz odpovdajc chlvecek. Ale to jevelmi jednoduch, stac vedet, kde pole zacn a pak k tomuto zactku pricst i-krtvelikost chlvecku. Zkracovn pole (funkce pop(-1), pop(0)) je tak jednodu-ch Python proste posledn (prvn) chlvecek uvoln k jinmu pouit. Trochusloitej je odebrn k-tho prvku, kde 0 < k >> seznam = ['A','B','C',5]>>> seznam.append('Q')

    x x A B C 5

    Q

    x x

    start end

    seznam

    x x A B C 5 x

    A B C 5 Q

    start end

    seznam

    20

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    Python problmy se zvetovnm seznamu cstecne obchz pomoc elegantnhotriku. Kdy toti vytvorte nov pole, Python si ve skutecnosti vyhrad chlveckuvc, aby mel nejakou rezervu na zvetovn. Pridvat nov prvky je pak jednodu-ch do t doby, ne dojde tato rezerva. Kdy mu rezerva dojde, mus chte-nechtevyhradit msto nov a pole koprovat. Nicmne v tuto chvli si opet vytvor rezervua pro jistotu si j vytvor 2-krt tak velkou jako na poctku. Kdy m naopak prvekz konce pole smazat, pouze si poznac, e pole je krat a msto toho, aby mstona konci pole uvolnil pro jin pouit, zvet o nej rezervu. Ackoliv to nemus btjasn na prvn pohled, je tento postup v praxi casto mnohem vhodnej. Pokudnaprklad do pole v prumeru stejne casto prvky pridvme jako odebrme, mlo-kdy nm rezerva dojde a vetina operac bude velmi rychl. Tuto vhodnost lzematematicky analyzovat podobne jako jsme to udelali se sloitost algoritmu pomoc tzv. amortizovan sloitosti (vce viz napr. [Ta85]), nicmne my to nyndelat nebudeme a vrtme se k naemu zelinri.

    Cekat v kresle je sice pohodlnej ne stt ve fronte, nicmne nekter zkaznci bysi klidne priplatili, aby nemuseli cekat vubec. Zelinr proto za Vmi priel s tm,abyste mu systm upravili tak, aby si lid mohli priplatit za prednostn odbaven.Jedno mon reen spocv v tom, e zavedeme fronty dve. Frontu prednostn afrontu obycejnou. Zkaznk si pri volbe csla mue priplatit, aby byl zarazen doprednostn fronty. K pokladne pak budou vyvolvni nejprve lid z fronty pred-nostn a teprve v okamiku, kdy je tato fronta przdn, prijdou na radu lid vefronte obycejn. Implementace je jednoduch:

    1 f_obyc=[]2 f_predn=[]34 def generujListek( jmeno, prednostni = False ):5 global f_obyc, f_predn6 if prednostni:7 f_predn.append(jmeno)8 else:9 f_obyc.append(jmeno)

    1011 def naRade():12 global f_obyc, f_predn13 if len(f_predn) > 0:14 return f_predn.pop(0)15 elif len(f_obyc) > 0:16 return f_obyc.pop(0)17 else:18 return None

    21

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    Reen je elegantn a celkem vyhovujc. Protoe ale nastaven cena prednostnfronty nen prli vysok, mus se i v n casto dlouho cekat. Nekter movitej z-kaznci by si klidne priplatili mnohem vce, jen aby nemuseli cekat. lo by to reitzavedenm superprednostn fronty, prpadne i hyperprednostn atd . . . Vs vaknapadne mnohem lep reen. Fronta bude jen jedna, ale pri vyzvednut lstkubude mt kad monost zaplatit urcitou cstku dle vlastn volby. Zkaznci pakbudou odbavovni v porad podle obnosu, kter zaplatili. Implementace bude ten-tokrt trochu sloitej:

    1 fronta = []23 def generujListek( jmeno, castka ):4 global fronta5 fronta.append( [jmeno, castka] )678 def naRade():9 global fronta

    1011 # Pokud je fronta prazdna, vrat None12 if len(fronta) < 1:13 return None1415 # Najdi zakaznika s maximalni castkou16 # (zakaznik je vzdy dvojice [ ID, castka ] )17 max_jmeno, max_castka = zakaznici[0]18 max_pos = 019 pos = 02021 for (jmeno, castka) in zakaznici:22 if castka > max_castka:23 max_castka = castka24 max_jmeno = jmeno25 max_pos = pos26 pos = pos + 12728 del zakaznici[pos]29 return max_jmeno

    Pojdme se podvat na sloitost tohoto reen. Funkce generujListek provedepouze jednu operaci append na konec pole, tud m konstantn (amortizovanou)sloitost (O(1)), t.j. nezvis na velikosti fronty. Funkce naRade provede nejprve a

    22

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    5 inicializacnch operac (rdky 10 a 1518) a pak mus projt celou frontu (rdek20) a s kadm prvek provst a 2-5 operac (rdky 2125). M tedy v nejhormprpade sloitost 5 + 5n = O(n), kde n je dlka fronty. Vzhledem k tomu, e pron > 1 funkce nikdy neskonc rdkem 11, jednodue zjistme, e v nejlepm prpadem sloitost 5 + 2n = O(n), tedy opet linern v dlce fronty. Ukeme si ponekuddumyslnej reen, kter za cenu mrnho zpomalen funkce generujListek v-razne urychl funkci naRade. Trik bude spocvat v tom, e si budeme zkaznkyukldat takovm zpusobem, abychom velmi rychle umeli vybrat zkaznka s nej-vy zaplacenou cstkou. Budeme k tomu pouvat tzv. haldu (angl. heap), kterje specilnm prpadem stromu. Pojdme se tedy nejprve podvat na (matematick)stromy.

    Stromy

    Matematick pojem stromu je dobre ilustrovn nsledujcm obrzkem znzornu-jcm delen ivch organizmu:

    organizmy

    rostliny

    semenn

    jehlicnany linovce

    kapradorosty

    ivocichov

    bezobratl obratlovci

    ptci

    penkava rozela vlatovka

    savci

    primti

    hominidae

    23

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    Schematicky ho mueme popsat takto: na obrzku je mnoina kategori (obratlovci,savci, . . .), kter se ve stromov terminologii casto rk mnoina uzlu. Mezi nekte-rmi kategoriemi pak vede cra (hrana), znzornujc vztah kategorie-podkategorie(obratlovcisavci, . . .), ve stromov terminologii se tomuto vztahu casto rk vztahrodicdte (motivace pochz z rodokmenu, kter se tak casto znzornuj pomocstromu). Zavedeme si jete nekolik pomocnch pojmu:

    2.2 Definice. koren je uzel, kter je plne na vrcholu, t.j. cry vedou pouze z nej,dn cra nevede do nej. Na naem obrzku je korenem kategorie organizmy.Naopak list je uzel, ze kterho nevede dn cra (napr. vlatovka, ...) a uzly,kter nejsou listy (vcetne korene) budeme souhrnne nazvat vnitrnmi uzly. Pokudm uzel alespon dve deti (t.j. vedou z nej dve cry), rekneme, e se vetv, resp. eje to vetvc uzel. Pokud maj vechny uzly nejve dve deti, rkme, e strom jebinrn (podobne definujeme ternrn strom atd.). Cestou ve stromu rozumme po-sloupnost uzlu, kter jsou postupne mezi sebou pospojovan hranami vedoucmivdy od rodice k dteti. Cestu od korene k nejakmu listu nazveme vetv. Hloub-kou stromu rozumme dlku nejdel vetve. Hladinou ve strome rozumme mnoinuuzlu, kter maj stejnou hloubku. Uzel A je potomkem uzlu B, pokud z B do A vedecesta. V takovm prpade rkme t, e uzel B je predkem uzlu A.Poznmka. Formlne lze tuto situaci modelovat ruznmi zpusoby. Naprklad v teorii mnoin jestrom definovn jako cstecne uspordan mnoina (T,), kde prvky t T odpovdaj uzlum a prokad uzel t T je uspordn zen na mnoinu {s T : s t} jeho predchudcu dobr. Jinzpusob, kter pro ns bude duleitej, reprezentuje strom jako speciln prpad grafu, tzv. orientovangraf bez cyku, anglicky DAG (directed acyclic graph). Grafum se budeme pozdeji venovat podrobneji.

    Pojdme se nyn podvat, jak je mon tuto situaci reprezentovat v Pythonu. Budepro ns duleit, e v kadm uzlu chceme uchovvat nejak data treba pojme-novn toho uzlu. Nejjednodu bude reprezentovat uzel jako dvojici u = (data,deti), kde deti je seznam uzlu, kter jsou detmi uzlu u. Zde je nzorn prklad:

    >>> T = ('Koren',[('List A',[]),('B',[

    ('List C',[]),('List D',[])])

    ])

    Koren

    List A B

    List C List D

    Ukame si na dvou jednoduchch prkladech, jak s takovmi stromy pracovat.Casto bude velmi prirozen pouvat rekurzi. Naprklad procedura, kter vrt se-znam jmen vech listu mue vypadat takto:

    24

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    1 def listy(T):2 ret = []3 jmeno, deti = T4 if len(deti) == 0:5 # T je list (nem dn deti), vrtme jmno T6 return [jmeno]7 else:8 # Projdeme vechny deti a do naeho seznamu pridme listy,9 # kter jsou jejich potomkem

    10 for d in deti:11 ret = ret + listy(d)12 return ret

    Zkusme si nyn nejak strom vytvorit. Budeme chtt naprogramovat funkci, kterdostane jako parametr cslo a vytvor nsledujc strom. V kadm uzlu stromubude uloeno nejak cslo, v koreni bude uloeno cslo dan vstupnm paramet-rem. Deti kadho uzlu budou odpovdat vem delitelum > 1, listy tedy budouodpovdat prvocselnm delitelum. Naprklad pro cslo dvacet budeme cht vytvo-rit nsledujc strom:

    20

    2 4

    2

    5 10

    2 5

    V Pythonu mue nae funkce vypadat treba takto:

    25

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    1 def delitele(n):2 ret = []3 for d in range(2,n/2+1):4 if n % d == 0:5 ret.append(d)6 return ret78 def strom_delitelu(n):9 delit = delitele(n):

    10 deti = []11 for d in delit:12 deti.append(strom_delitelu(d))13 return (n,deti)

    Haldy

    Vratme se nyn k naemu zelinri. Rekli jsme, e budeme chtt ukldat zkaznkytak, abychom umeli velmi rychle vybrat zkaznka s nejvy zaplacenou cstkoua podobne rychle zkaznky i pridvat. Reen spocv v tom, e je budeme ukl-dat do stromov struktury, kter se rk halda. Halda je speciln prpad binrnhostromu, ve kterm je v kadm uzlu mimo ostatnch dat jete navc uloeno nejakcslo. Jakoto strom je to strom, ve kterm se kad uzel, kter nen listem, vetv avechny listy jsou na posledn nebo predposledn hladine s tm, e listy na poslednhladine jsou co nejvce vlevo. Navc pro kad uzel plat, e cslo, kter je v nemuloeno, je men ne csla uloen ve vech jeho potomcch. Prkladem haldy jenaprklad nsledujc strom:

    2

    2010

    1011

    Naopak ani jeden z nsledujcch stromu haldou nen.

    26

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    (A)

    0

    2010

    911

    (B)

    0

    20

    31

    3332

    30

    3735

    10

    911

    1615

    (C)

    0

    10

    911

    Ve stromu (A) je poruena podmna, e uzly jsou men ne jejich potomci (10 6 9),ve stromu (B) je poruena podmnka, e listy na posledn hladine maj bt plnevlevo a ve stromu (C) je poruena podmnka, e kad uzel, kter nen listem, semus vetvit.

    Prvky se do haldy pridvaj tak, e se vytvor nov list a do nej se prvek vlo.Tm mue dojt k poruen podmnky, e uzly jsou men ne potomci. Proto mu-sme zkontrolovat, zda je vloen prvek vet ne jeho rodic. Pokud nen, s rodi-cem ho vymenme a musme dle zkontrolovat rodice s jeho rodicem. Pokud jeve O.K., skoncme, jinak pokracujeme v prohazovan dokud se nedostaneme dokorene, kter dnho rodice nem.

    Prklad pridvn prvku 1 do haldy:

    2

    2010

    1011

    2

    20

    1

    10

    1011

    2

    1

    20

    10

    1011

    1

    2

    20

    10

    1011

    Naopak z hlady se (vrchn) prvek odebr tak, e se vrt hodnota v koreni a na-hrad se hodnotou v nejpravejm listu na posledn hladine a tento list se smae.Tm mue bt opet porueno pravidlo, e uzly jsou men ne nslednci, proto mu-sme zkontrolovat, zda nov hodnota korene je vet ne hodnota obou jeho det.Pokud nen, vymenme tuto hodnotu s hodnotou menho dtete a pokracujeme vkontrolovn tohoto dtete dokud nen ve O.K., nebo dokud se nedostaneme dolistu, kde je ve O.K. z definice.

    27

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    0

    20

    3130

    3332

    10

    10

    3735

    11

    1615

    33

    20

    3130

    32

    10

    10

    3735

    11

    1615

    10

    20

    3130

    32

    33

    10

    3735

    11

    1615

    10

    20

    3130

    32

    10

    33

    3735

    11

    1615

    Jak je sloitost techto operac? Pro pridvn potrebuji

    pridat prvek na konec haldy

    a v nejhorm prpade projt stromem od listu ke koreni a v kadm krokuprovst jedno porovnn a jednu vmenu

    Sloitost pridn v nejhorm prpade tedy bude zviset na vce stromu. Nentek si uvedomit, e pokud je strom haldou a m n uzlu, pak jeho vka budebud log2 n nebo, log2 n + 1. Celkov sloitost tedy bude asymptoticky O(log n) +sloitost pridn prvku na konec haldy. To bude samozrejme zviset na konkrtnmkdu, ale v jakkoliv rozumn verzi to nebude hor neO(log n). Podobne to vyjdepro operaci odebrn prvku. Pokud bychom chteli jen zjistit, jak je nejmen prvek,bude to dokonce O(1).

    Zavedli jsme haldu takovm zpusobem, aby mela v koreni nejmen cslo. Pro nzelinrsk software by se vak hodilo naopak mt v koreni cslo nejvet. Rozmys-lete si, e pokud v halde obrtm podmnku na csla, t.j. pokud budu poadovat,aby cslo uzlu bylo vet, ne cslo vech jeho potomku, bude vechno opet fungovat(po jednoduch prave operac). V nsledujc praktick implementaci v Pythonuuvedeme tuto opacnou verzi haldy.

    Ukeme si nyn prci s haldou v Pythonu. Mohli bychom s n pracovat jako sobycejnm stromem. V takovm prpade by bylo dobr si u kadho uzlu navc

    28

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    pamatovat i jeho rodice. Tedy uzel by mohl bt treba ctverice (data, hodnota,rodic, deti). S takovouto reprezentac by se ale pracovalo trochu neikovne,protoe neumonuje jednodue zjistit nejlevej list na predposledn hladine (resp.na posledn, pokud takov nen). Bude proto lep si umet haldu uchovvat v se-znamu (a v budoucnu se nm to bude hodit).

    Haldu budeme ukldat po jednotlivch hladinch, kad prvek seznamu budedvojice (data, hodnota). Jeho index v poli bude urcovat jeho pozici ve stromu.Na prvnm mste bude koren, na druhm a tretm jeho prpadn dve deti, a tak dle.Protoe ukldan strom je haldou, mueme si z pozice uzlu u jednodue vypoctatpozice jeho det a rodicu. Je-li toti jeho pozice n, pak jeho deti maj pozici 2*n +1 a 2n + 2 a jeho rodic m index (n-1)/2. Uvedomte si, e zde vyuvme faktu,e jde o haldu, u obecnho stromu by to nefungovalo! Elegance tohoto prstupuspocv v tom, e nejlevej list je proste posledn prvek pole. Operace v Pythonupak muou vypadat treba nsledovne jako na vpisu 2.3.

    Zelinr pokracovn

    Rkali jsme, e upravme n zkaznick systm tak, aby jak titen lstku (funkcegenerujListek) tak vyvolvn zkaznku (funkce naRade) pracovaly rychle.Udelme to tak, e vyuijeme ve uveden funkce pro prci s haldou a zkaznkysi budeme ukldat do haldy. Funkce budou tedy vypadat takto:

    1 halda = []2 def generujListek( jmeno, castka ):3 push( halda, jmeno, castka)45 def naRade():6 return pop( halda )

    2.4 Cvicen. N kamard zelinr si upraven zkaznick systm velmi pochva-luje, nicmne viml si jedn neprjemn vlastnosti. Obcas se toti stane, e nejaketriv zkaznk zaplat za odbaven malou cstku a pak se nikdy nedostane naradu, protoe ho vichni ostatn preplat. A tak se mu v obchode hromad neodba-ven etriv zkaznci . . . Podal Vs proto, abyste systm upravili tak, aby bral vpotaz nejen zaplacenou cstku, ale i cas strven ceknm. Na Vs ted je navrhnoutpravu. Kdy se nad tm zamyslte, nebude to vubec sloit . . .

    29

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    Algoritmus 2.3 Operace s haldou

    1 # -*- coding:utf-8 -*-23 # Jednoduch funkce pro poctn pozic det a rodicu4 def left_child_index(n):5 return 2*n + 167 def right_child_index(n):8 return 2*n + 29

    10 def parent_index(n):11 return (n-1)/21213 def last_index(halda):14 return len(halda)-11516 # Projde stromem od uzlu s indexem index ke koreni17 # a zajist, e hodnoty potomku jsou vdy18 # men ne hodnoty rodicu19 def check_up(halda, index):20 p = parent_index(index)21 if halda[p][1] < halda[index][1]:22 halda[p],halda[index] = halda[index],halda[p]23 check_up(halda,p)2425 # Podobne jako predchoz funkce, ale prochz26 # od uzlu s indexem index naopak k listum27 def check_down(halda, index):28 l,r = left_child_index(index), right_child_index(index)29 if halda[l][1] < halda[r][1]:30 c = r31 else:32 c = l33 if halda[index][1] < halda[c][1]:34 halda[index],halda[c] = halda[c],halda[index]35 check_down(halda, c)3637 # Zjist nejvet prvek v halde38 def peek(halda):39 if len(halda) == 0:40 return None41 return halda[0]4243 # Prid prvek do haldy44 def push( halda, data, hodnota ):45 halda.append((data,hodnota))46 check_up(halda, last_index(halda))4748 # Odebere nejvet prvek z haldy49 def pop( halda ):50 if len(halda) == 0:51 return None52 ret = peek(halda)53 halda[0] = halda[-1]54 del halda[-1]55 check_down(halda, 0)56 return ret

    30

    # -*- coding:utf-8 -*-

    # Jednoduch funkce pro potn pozic dt a rodidef left_child_index(n): return 2*n + 1

    def right_child_index(n): return 2*n + 2

    def parent_index(n): return (n-1)/2

    def last_index(halda): return len(halda)-1

    # Projde stromem od uzlu s indexem index ke koeni# a zajist, e hodnoty potomk jsou vdy# men ne hodnoty rodidef check_up(halda, index): p = parent_index(index) if halda[p][1] < halda[index][1]: halda[p],halda[index] = halda[index],halda[p] check_up(halda,p)

    # Podobn jako pedchoz funkce, ale prochz# od uzlu s indexem index naopak k listmdef check_down(halda, index): l,r = left_child_index(index), right_child_index(index) if halda[l][1] < halda[r][1]: c = r else: c = l if halda[index][1] < halda[c][1]: halda[index],halda[c] = halda[c],halda[index] check_down(halda, c)

    # Zjist nejvt prvek v halddef peek(halda): if len(halda) == 0: return None return halda[0]

    # Pid prvek do haldydef push( halda, data, hodnota ): halda.append((data,hodnota)) check_up(halda, last_index(halda))

    # Odebere nejvt prvek z haldydef pop( halda ): if len(halda) == 0: return None ret = peek(halda) halda[0] = halda[-1] del halda[-1] check_down(halda, 0) return ret

    Jonathan VernerSource code for Operace s haldou

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    Zsobnk

    V predchozch oddlech jsme si predstavili dve zkladn datov struktury: frontua haldu. Zajmavch ikovnch zpusobu jak ukldat data je samozrejme mnohemvce (Red-Black Stromy, Hashovac tabulky, ...), ale v tto prednce nm na nenezbude cas. Presto bychom rdi zmnili jete alespon zsobnk (anglicky stack, nebotak LIFO last in, first out). V jistm smyslu se velmi podob fronte: je to obycejnseznam prvku, do kterho umme pridvat prvky (funkce append) a prvky z nejvybrat (funkce pop). Prvky se do zsobnku ukldaj stejne jako do fronty nakonec seznamu. Rozdl spocv ve funkci pop, kter prvky neodebr ze zactku(jako u fronty), ale z konce. V Pythonu se kad seznam standardne chov jakozsobnk:

    1 >>> z = []2 >>> z.append(1)3 >>> z.append(2)4 >>> z.append(3)5 >>> z.pop()6 37 >>>

    2.5 Cvicen. Predstavte si, e mte k dispozici dve promenn typu zsobnk (t.j.mte funkce append a pop, kter se chovaj jak bylo ve popsno). Zkuste po-moc techto dvou zsobnku naprogramovat frontu. (Npoveda: co se stane, kdyz jednoho zsobnku postupne vyberete vechny prvky a dte je na druh zsob-nk?)

    31

  • KAPITOLA 2. ZKLADN DATOV STRUKTURY

    32

  • Kapitola 3

    Vyhledvn, trdc algoritmy

    Vyhledvn

    Nechme zelinre zelinrem a pojdme se spolu podvat na jinou lohu. Tentokrtbudeme chtt napsat spellchecker. Pro zactek bude velmi jednoduch. Dostane tex-tov dokument a slovnk a vype vechna slova, kter se vyskytuj v dokumentua nevyskytuj se ve slovnku. V Pythonu to naprogramujeme velmi jednodue:

    1 # Vrt seznam vech slov nachzejcch se ve stringu doc,2 # kter nejsou v seznamu slovnik. Predpokldme e jednotliv3 # slova ve stringu doc jsou oddelena mezerou.45 def spell_check(slovnik,doc):6 spatna = []7 for slovo in doc.split(' '):8 if slovo not in slovnik:9 spatna.append(slovo)

    10 return spatna

    Toto jednoduch reen vak m sv mouchy. Jak je jeho sloitost? Funkce musnejprve rozdelit retezec doc na jednotliv slova. Python toto provede za ns funkcsplit, ale pri troe dobr vule bychom to zvldli sami a zjistili bychom, e sloitosttto operace je O(N), kde N je dlka retezce. Pokud by ns zajmala sloitost v z-vislosti na poctu slov, stac si uvedomit, e vetina slov je kratch ne nejakch 15znaku a pravdepodobne dn slovo nebude del ne 30 znaku. Z toho plyne, e

    33

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    sloitost bude opet O(N) multiplikativn konstanty v O-notaci zanedbvme.Pak musme projt vechna slova a u kadho otestovat, zda se nachz ve slov-nku. Ve ve uvedenm programu to za ns del opertor in. Ten je v podstate jenzkratkou nsledujcho kdu:

    1 # Vrt true, pokud se slovo vyskytuje v seznamu slovnik2 def contains(slovo, slovnik):3 for s in slovnik:4 if s == slovo:5 return True6 return False

    Z kdu je videt, e sloitost bude zviset na tom, jestli (a kde) se slovo slovo veslovnku slovnik nachz. V idelnm prpade se slovo nachz na prvnm mste to provedeme pouze jednu operaci porovnn. V nejhorm prpade v seznamunebude vubec a pak provedeme tolik operac, kolik je prvku seznamu slovnik.Sloitost v nejhorm prpade tedy bude O(N + N M) = O(N M), kde N jepocet slov ve vstupnm dokumentu a M je pocet slov ve slovnku. Ukeme si, epokud venujeme nejak cas prprave slovnku slovnik, podar se nm navrhnoutfunkci contains tak, aby mela sloitost O(logM), co d pro cel algoritmus slo-itost O(N logM). Trik spocv v tom, e si slovnk predem uspordme podleabecedy. Jak to udelat uvidme v dal csti, ted vak predpokldejme, e ho mmeuspordan. K testu, zda se ve slovnku dan slovo nachz, ted mueme vyutmetodu pulen intervalu vezmeme si slovo uprostred naeho slovnku a porov-nme ho s hledanm slovem. Pokud je v abecede hledan slovo drve, vme, e semus nachzet v prvn polovine slovnku. Pokud je naopak pozdeji, nachz se vdruh polovine. Ten sam postup budeme opakovat znovu, dokud slovo bud ne-najdeme, nebo nezjistme, e ve slovnku nen. Takto popsan algoritmus nejlpezapeme pomoc rekurze:

    Pojdme se nejprve presvedcit, e algoritmus je sprvn. Jak je u rekurzivnch algo-ritmu obvykl, budeme postupovat indukc, v tomto prpade dle n=end-start.

    n=0 Pokud je start == end, pak algoritmus vrt False, co je sprvne, protoeslovnik[start:start] je przdn seznam ve kterm se nic nevyskytuje.

    n=1 Pokud je end-start == 1, pak middle == start a slovnik[start:end]= [slovnik[start]]. Pokud je tedy slovo == slovnik[middle], al-goritmus vrt True, jinak se tam slovo nevyskytuje. Pokud se tam nevysky-tuje, zavol se bud contains(slovo, slovnik, start, start) nebocontains(slovo, slovnik, end,end) co oboj sprvne vrt False.

    34

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    Algoritmus 3.1 Binrn vyhledvn

    1 # Vrt True, pokud se slovo vyskytuje v setrzenm2 # seznamu slovnik[start:end]. Jinak vrt False.3 def contains(slovo, slovnik, start, end):4 if start >= end:5 return False6 middle = start + (end-start)/27 if slovo == slovnik[middle]:8 return True9 elif slovo < slovnik[middle]:

    10 return contains(slovo, slovnik, start, middle)11 elif slovo > slovnik[middle]:12 return contains(slovo, middle+1, end)

    n+1 Indukcn krok. Predpokldejme e pro end-start = n >= 1 algoritmuspracuje a uvaujme end-start = n+1. Je jasn, e pokud se slovo v se-znamu slovnik[start:end] vyskytuje, pak je bud rovno

    slovnik[middle]

    nebo se vyskytuje v

    slovnik[start:middle]

    nebo v

    slovnik[middle+1:end].

    Prvn prpad je jasn a na druh dva mueme pout indukcn predpoklad,protoe jak

    middle-start < end-start

    tak

    end-(middle+1) < end-start

    (prvn prpad plyne z toho, e start-end >= n+1>=2).

    35

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    A jak je sloitost algoritmu? Protoe v kadm kroku provedeme maximlne triporovnn a jedno prirazen a zroven zmenme rozdl end-start na polovinu,je intuitivne vcelku jasn, e sloitost bude 4 logM = O(logM).

    3.2 Cvicen. Indukc dokate, e sloitost ve uveden funkce contains budeopravdu O(logM).

    Mme tedy mnohem efektivnej funkci contains, ale nen to zadarmo. Aby tatofunkce fungovala, je treba mt slovnk setrzen podle abecedy, co bude namdalm tmatem. Predem mueme prozradit, e sloitost trden bude O(M logM).Kdy se vrtme k na funkci spell_check dostaneme sloitostO((M+N) logM).Jak to porovnat s puvodn sloitost O(N M)? V typickm prpade bude slovnkmnohem vet ne seznam slov, tedy M >> N . Pokud naprklad N < logM zjis-tme, e puvodn algoritmus bude lep vzhledem k tomu, e budeme hledat jenpr slov, nevyplat se nm trdit cel slovnk. Na druhou stranu, pokud predpokl-dme, e funkci spell_check budeme vyuvat casto (co je rozumn predpo-klad), setrzen slovnku se nm dlouhodobe vyplat.

    Trdc algoritmy

    Insertion Sort. V predchoz csti jsme narazili na lohu setrdit seznam slov dleabecedy. Pojdme se zamyslet nad tm, jak bychom to mohli udelat. Jako prvn nsasi napadne postupovat tak, jak bychom sami postupovali, pokud bychom melilohu vyreit rucne. Proste bychom si setrzen seznam budovali postupne a vkadm kroku do ji setrzen csti zatrdili dal slovo. Tomuto algoritmu se rkinsertion sort. Zapime si ho v Pythonu:

    Algoritmus 3.3 Insertion Sort

    1 def insertion_sort(seznam):2 last_sorted = 03 while last_sorted < len(seznam)-1:4 j = last_sorted + 15 while j > 0 and seznam[j] < seznam[j-1]:6 seznam[j-1],seznam[j] = seznam[j], seznam[j-1]7 j = j - 18 last_sorted = last_sorted + 1

    V promenn last_sorted si algoritmus udruje index poslednho setrzenhoprvku (t.j. pole seznam[:last_sorted+1] je setrzen). Na zactku je setrzenpouze prvn prvek pole (rdek 2). Dokud nen pole setrzen cel (podmnka nardku 3), provdm nsledujc kroky (rdky 4-8): vezmu prvn nesetrzen prvek

    36

    def insertion_sort(seznam): last_sorted = 0 while last_sorted < len(seznam)-1: j = last_sorted + 1 while j > 0 and seznam[j] < seznam[j-1]: seznam[j-1],seznam[j] = seznam[j], seznam[j-1] j = j - 1 last_sorted = last_sorted + 1

    Jonathan VernerSource code for Insertion Sort

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    (rdek 4), zatrdm ho do pole na sprvn msto (rdky 5-7) a zvetm si indexposlednho setrzenho prvku (rdek 8).

    Jak je sloitost tohoto algoritmu? Cyklus na rdku 3 se provede n-krt, kde n jevelikost vstupnho pole seznam. V tele cyklu pak provedu dve prirazovac operace(rdky 4, 8) a zatrzen (cyklus na rdcch 5-7). Vnitrn zatrizovac cyklus provede vnejhorm prpade 3j operac. Po chvilce poctn (je treba secst radu 1+2+ +n)dostanu celkov doln odhad sloitosti 2n+ 3n(n+ 1)/2 = O(n2).

    3.4 Cvicen. Jak bude sloitost algoritmu, pokud ho pustme na ji setrzen pole?

    Za chvli uvidme, e existuj algoritmy s mnohem lep asymptotickou sloitost.Proc jsme tedy insertion sort vubec zminovali? Krome toho, e je to algoritmusvelmi jednoduch, m nekolik podstatnch vhod. Ackoliv je jeho asymptoticksloitost kvadratick, pro mal vstupn pole (do velikosti cca 100 prvku) je castonejrychlejm algoritmem. Dal vhodou je jeho optimln chovn, pokud ho pus-tme na ji setrzen pole (viz cvicen). Dal monou vhodou je to, e pole zpra-covv postupne algoritmy, kter popeme dle, potrebuj znt pole cel najed-nou.

    Heap sort. Pokud vm prozradm, e optimln asymptotick sloitost trdcchalgoritmu (zaloench na provnvn prvku) je O(n log n) a navc Vm napovm,e lze pout haldu, mon Vs u napadne, jak funguje algoritmus, ktermu serk heap sort. Vzpomente si, e operace pridn prvku do haldy (push) a odebrnnejmenho prvku z haldy (pop) m sloitost O(log n). Co kdybychom postupneproli cel pole a kad prvek pridali na haldu a pak postupne odebrali vdynejmen prvek z haldy? To je princip heap sortu (viz vpis 3.5)

    Algoritmus 3.5 Heap sort

    1 def heap_sort(seznam):2 halda = []3 for p in seznam:4 push(halda, p)56 for i in range(len(seznam)):7 p = pop(halda)8 seznam[i] = p

    Kd uveden ve vpisu 3.5 je pekn (a nen tek spoctat, e m asymptotickou

    37

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    sloitost O(n log n)1), nen vak moc efektivn k setrzen pole si v pameti vy-hrad jete jednou tolik msta na haldu, na kterou postupne pridv prvky. Tentonedostatek vak lze velmi jednodue odstranit. Kdy se nad tm zamyslte a uvedo-mte si, e jednoprvkov pole je haldou, urcite pro Vs nsledujc cvicen nebudeprli obtn:

    3.6 Cvicen. Prepite heap sort (a haldov operace push resp. push) tak, aby algo-ritmus nepotreboval separtn seznam pro haldu.

    Dal algoritmus, kter si ukeme, je asi v praxi nejpouvanejm algoritmem.

    Quicksort je klasickm rekurzivnm algoritmem. Funguje na nsledujcm prin-cipu. Pokud seznam, kter se m setrdit, m pouze jeden prvek, je setrzen aalgoritmus mue skoncit. V opacnm prpade si seznam rozdel na dve csti tak,aby vechny poloky v lev csti byly men ne vechny poloky v prav csti.Pak se rekurzivne zavol a setrd levou a pravou cst. Nsledne mue proste pri-pojit setrzenou pravou cst za setrzenou levou cst a cel seznam bude setrzen,protoe prvky vlevo byly men ne prvky vpravo a obe csti setrzen byly. Zbvpopsat rozdelovn na csti. To se typicky provd tak, e se ze seznamu nhodnezvol jeden prvek, tzv. pivot, a pole se rozdel tak, aby vlevo byly vechny prvkymen nebo rovn pivotu a vpravo vechny prvky vet nebo rovn pivotu. Se-znamem se prochz soucasne zleva i zprava. V okamiku, kdy z jednoho smerunarazme na prvek, kter m bt vzhledem k pivotu v opacn csti, cekme, nek takovmu prvku dorazme i zprava (pokud na dn nenarazme, vme, e jsmehotovi a tento prvek bude prvn prvek, kter patr do prav csti). V okamiku, kdyna takov prvek narazme i zprava, oba prvky vymenme a pokracujeme. Kdy sesetkme, mme pole rozdelen. Schematicky bude quicksort vypadat takto:

    1 def quicksort( seznam ):2 if len(seznam) == 1:3 return seznam4 pivot = zvol_pivota( seznam )5 L, P = rozdel_seznam( seznam, pivot )6 L_sorted = quicksort(L)7 P_sorted = quicksort(P)8 return L_sorted + P_sorted

    1Tak, jak je napsan, m prvn cyklus, kter stav haldu, sloitost O(n logn). D se vak napsat trochuchytreji tak, aby mel sloitost linern (viz napr. [Wi85]). Druh cyklus se takto prepsat ned, sloitosttedy bude stle O(n logn)

    38

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    3.7 Poznmka. Ve skutecnosti by ve uveden schematicky zapsan quicksort bylrelativne pomal, protoe by dochzelo k castmu koprovn z pameti do pametipri vytvren novch pol (L,P, L+P). V relu proto vubec nebudeme vytvret novseznamy a funkce quicksort bude vypadat spe tak, jak je uvedena ve vpisu 3.8.

    Algoritmus 3.8 Quicksort

    1 def quicksort( seznam, start, end ):2 if start >= end:3 # Prazdn a jednoprvkov pole jsou4 # z definice setrzen5 return6 pivot = zvol_pivota( seznam, start, end )7 stred = rozdel_seznam( seznam, start, end, pivot)8 quicksort( seznam, start, stred )9 quicksort( seznam, stred+1, end )

    10 return

    t.j. vechno se bude odehrvat v poli seznam, nic se nebude nikam koprovat apodseznamy budou vdy vymezen dvema indexy, poctecnm (start) a konco-vm (end).

    3.9 Poznmka. Pri rozdelovn pole je treba dt pozor na to, aby se pole opravdurozdelilo, t.j. aby bylo start

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    8 + 8

    4 + 4 + 4 + 4

    2 + 2 + 2 + 2 + 2 + 2 + 2 + 2

    1+1 + 1+1 + 1+1 + 1+1 + 1+1 + 1+1 + 1+1 + 1+1

    n

    log n

    Formlne lze sprvnost odhadu ukzat naprklad indukc dle n (pro jednoduchostuvaujeme pouze pole velikosti n = 2k): Pro pole velikosti 20 je to zrejm. Mm-linyn dno pole velikosti 2k, musm rozdelit pole (sloitost 2k) a dvakrt zavolatquicksort na pole polovicn velikosti. Z indukcnho predpokladu m jedno takovvoln sloitost 2(k1) log 2(k1)) = 2k1(k1), dve voln tedy budou mt sloitost2k(k 1). Celkem tedy 2k + 2k(k 1) = 2kk = O(2kk) = O(n log n).

    Jak bude sloitost quicksortu, pokud se nm pivoty nepodar zvolit optimlne?Predstavme si, e jako pivota volm vdy prvn prvek pole a na vstupu dostanupole ji setrzen. Co se stane je opet dobre videt na obrzku:

    1 + 15

    1 + 14

    1 + 13...

    n

    n

    Na predchozm obrzku jsme meli pln binrn strom, jeho hloubka je log n. Zdenm tento strom zdegeneroval ve strom, jeho hloubka je n. Kdy si spocteme slo-itost, uvidme, e je to O(n2), co je neprjemn (tato situace by mohla nastat i

    40

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    v prpade, e je pole ji setrzen; v takovm prpade m i jednoduch insertionsort sloitost dokonce linern!). Je tedy videt, e vhodn volba pivota mue radi-klne ovlivnit dlku vpoctu. Jak jsme si ukzali ve, optimln volbou by byl me-din pole. Protoe pro volbu medinu existuje algoritmus bec v linernm case(detaily viz [BFPRT73], nebo heslo Selection Algorithm na Wikipedii), mohlibychom jej pout a zajistit si, e sloitost quicksortu bude O(n log n) i v nejhormprpade. Nicmne v praktickch implementacch se ukazuje, e je rychlej jako pi-vota zvolit nejak nhodn prvek pole (nikoliv tedy vdy prvn!). Nen tak sicezajiteno, e algoritmus pobe v O(n log n), ale ve vetine prpadu pobe mno-hem rychleji, ne kdyby pivota vybral sloiteji2.

    Mohli bychom nyn pokracovat zkoumnm dalch trdcch algoritmu a venovattomu jete nekolik kapitol. Tak bychom se mohli zabvat i jinmi parametry trd-cch algoritmu ne jen jejich casovou sloitost mohli bychom naprklad zkoumatjejich stabilitu nebo pametovou nrocnost. Zde to ale delat nebudeme a odkemezjemce do literatury. Msto toho se jete chvli budeme zabvat casovou sloitosta budou ns zajmat doln odhady.

    Doln odhad pro sloitost vyhledvn

    Ukzali jsme si, e algoritmus Quicksort m v optimlnm case sloitost O(n log n).Algoritmus Heap sort m dokonce v nejhorm prpade sloitost O(n log n). Po-dobne treba algoritmus Merge sort (kter jsme si neukazovali) m v nejhormprpade sloitost O(n log n). Nemohli bychom najt nejak jete lep algoritmus?Ukeme si, e pokud jako zkladn operace povolme pouze porovnvat jednot-liv prvky prpadne je prohazovat, lep algoritmy neexistuj. Bude to vak tro-chu sloitej, ne dokazovat, e nejak algoritmus m takovou a takovou sloi-tost. Budeme toti chtt ukzat, e dn trdc algoritmus nemue beet rychlejine O(n log n)3 Jak takovou vec ukzat? Rozhodne nemueme prochzet trdc al-goritmy jeden po druhm a dokazovat, e maj sloitost nejmne O(n log n). Npostup bude nsledujc. Ukeme, e mme-li nejak rychl algoritmus, pak tonemue bt trdc algoritmus nalezneme pole, kter nesetrd sprvne. Pred-stavme si tedy, e mme nejak algoritmus A, kter na vstupu dostane seznamcsel x0, . . . , xn, kter m setrdit. Pro jednoduchost budeme predpokldat, e v-stupem bude posloupnost indexu a = (a0, . . . , an) setrzenho pole, t.j. a0 je index

    2Navc jsou implementace napsny tak, e pokud zjist, e hroz kvadratick sloitost naprkladse pole nekolikrt po sobe rozdelilo velmi nerovnomerne prepnou se treba na heap sort, kter je sicetrochu pomalej, ale m garantovanou sloitost O(n logn)

    3Formlne vzato nsledujc vaha dv doln odhad pro sloitost v nejhorm prpade. Malou pra-vou bychom mohli ukzat, e odhad plat i pro sloitost v prumernm prpade.

    41

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    nejmenho prvku, a1 index druhho nejmenho prvku a tak dle. Rekneme, etento algoritmus m sloitost g(n) < O(n log n). Pri svm behu tedy mue provstnejve g(n) operac porovnn a kad takov porovnn m pouze dva mon v-sledky men, vet (rovnost pro jednoduchost zanedbme). Oznacme-li si tedy tytovsledky jako 0 a 1, dostaneme pro kad beh algoritmu posloupnost nul a jednicekp = (p0, . . . , pg(n)) dlky (nejve) g(n). Mnoinu vech takovch posloupnost bu-deme znacit P (n). Ke kad posloupnosti nm algoritmus d posloupnost indexua, co je prost posloupnost csel od 0 do n. Oznacme si mnoinu vech prostchposloupnost csel od 0 do n symbolemA(n). N algoritmus nm tedy dv funkcif : P (n) A(n). Ukeme, e pro dostatecne velk n bude mnoinaA(n) vet nemnoina P (n). To znamen, e bude existovat posloupnost a = (a0, . . . , an) A(n),kter nem dn vzor nen vstupem algoritmu pro dn vstup. Zvolme tedycsla x0, . . . , xn tak, abychom jejich setrzenm zskali indexy a0, . . . , an. Pustme nane n algoritmus, kter zacne csla porovnvat tm zskme posloupnost p. Pro-toe ale posloupnost a byla volen tak, aby nebyla vstupem algoritmu, algoritmusmus vrtit posloupnost jinou, kter tud nebude sprvne setrzen!

    Zbv tedy ukzat, e pro dostatecne velk n bude mnoina A(n) vet ne mno-ina P (n). Spocst velikost mnoiny P (n) je jednoduch, je to 20+21+ +2g(n)1+2g(n) = 2g(n)+1 1. Velikost mnoiny A(n) je n!. Protoe se vak s faktorily patnepracuje, uvedomme si, e n! > (n/2)n/2 to nm bude stacit. Protoe algoritmusbyl asymptoticky rychlej ne O(n log n), vme, e pro libovoln K, b a dostatecnevelk n bude g(n) < nK log n b. Polome K = 1/4, b = 1 a zvolme si dostatecnevelk n (aby platila nerovnost, alespon > 4). Pak bude platit:

    g(n) < nK log n b

    a navcnK log n max: max = p7 elif p < min: min = p89 # Vyroben prihrdek

    10 poc_prihradek = (max-min)/m11 prihradky = []12 for i in range(poc_prihradek):13 prihradky.append([])1415 # Roztrden do prihrdek16 for p in seznam:17 prih = (p-min)/m18 prihradky[prih].append(p)1920 # Setrzen prihrdek21 for prih in prihradky:22 # Sort je nejak vhodn klasick trdc23 # algoritmus, kter setrd zadan pole24 prih.sort()2526 # "Slit" prihrdek zptky do seznamu27 pos = 028 for prih in prihradky:29 for p in prih:30 seznam[pos] = p31 pos += 1

    43

    def bucket_sort(seznam, m):

    # Nalezen nejmenho a nejvtho prvku min = max = seznam[0] for p in seznam: if p > max: max = p elif p < min: min = p

    # Vyroben pihrdek poc_prihradek = (max-min)/m prihradky = [] for i in range(poc_prihradek): prihradky.append([])

    # Roztdn do pihrdek for p in seznam: prih = (p-min)/m prihradky[prih].append(p)

    # Setzen pihrdek for prih in prihradky: # Sort je njak vhodn klasick tdc # algoritmus, kter setd zadan pole prih.sort()

    # "Slit" pihrdek zptky do seznamu pos = 0 for prih in prihradky: for p in prih: seznam[pos] = p pos += 1

    Jonathan VernerSource code for Bucket Sort

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    Jak bude sloitost? Nalezen minima a maxima a vyroben prihrdek bude mtsloitost O(n). Rozdelen csel do prihrdek m sloitost opet O(n). Prihrdek je(kmax kmin)/m, a kadou musme setrdit. V kad prihrdce je nejve m csel,kter jsme schopni setrdit v case O(m logm). Celkem tedy zabere trzen prihr-dek (rdky 18.22.) cas O((kmax kmin)/m m logm) = O((kmax kmin) logm) =O(kmax kmin)4 Celkov sloitost algoritmu tedy bude O(n) + O(n) + O(kmax kmin) = O(kmax kmin). Pokud bude rozsah monch klcu, t.j. hodnota kmax kmin, roven O(n) bude mt algoritmus linern sloitost!

    3.12 Cvicen. Pri odvozen sloitosti jsme predpokldali, e kad cslo se vyskytnenanejv jednou, z ceho vyplv, e n kmax kmin. Tento predpoklad vak jezbytecn. Nen tek upravit algoritmus tak, aby fungoval i s opakujcmi se klci.Dokonce vlastne mueme vyut prmo ve uveden kd, kter ve skutecnosti je-dinecnost klcu nikde nepouv. Jedinm mstem, kde jsme ji vyuily, byl odhadsloitosti setrzen jedn prihrdky. Zkuste se tedy upravit kd tak, aby setrzenprihrdky trvalo vdy O(m logm) kroku i v situaci kdy jedinecnost klcu nepred-pokldme.

    Mueme se vak dostat do situace, kdy kmax kmin bude prli velk (ve srovnns n) a ve uveden casov odhad zacne bt neprzniv v porovnn s n log n. Ivtomto prpade se vak nemusme vzdt, jen musme bt trochu chytrej. Tutosituaci re nsledujc varianta prihrdkovho trden, kter se rk radix sort

    Radix sort Princip korenovho trden je podobn jako u jednoduchho prihrd-kovho trden, jen se trochu jinak zarazuj poloky do prihrdek. Algoritmus fun-guje tak, e si klc kad poloky rozdel na predem dan pocet cst d a pak po-stupne v d pruchodech poloky setrd. V kadm pruchodu postupuje stejne jakoobycejn prihrdkov trzen s prihrdkama velikosti jedna (tud odpad nutnostv danm kroku prihrdky trdit), ale msto celho klce uvauje jen jeho cst. VPythonu mue vypadat treba jako na vpisu 3.13.

    4Posledn rovnost plyne z toho, e m je nejak predem pevne dan konstanta.

    44

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    Algoritmus 3.13 Radix Sort

    1 # coding: utf-82 def extract_key(start_cifra, end_cifra, klic):3 """Vrt cst klce od start_cifra do end_cifra4 >>> extract_key(1,4,12345)5 2346 >>> extract_key(0,2,123445)7 458 """9 return (klic % (10**end_cifra))/(10**start_cifra)

    1011 def var_buck_sort(seznam, scf, ecf):12 """ Provede bucket sort ale msto vech cifer klce uvauje pouze13 cifry od msta scf do msta ecf. Prihrdky maj velikost 1 tud14 odpad krok trden jednotlivch prihrdek."""1516 # Nalezen nejmenho a nejvetho prvku17 min = max = extract_key(scf,ecf,seznam[0])18 for p in seznam:19 k = extract_key(scf,ecf,p)20 if k > max: max = k21 elif k < min: min = k2223 # Vyroben prihrdek24 poc_prihradek = (max-min)+125 prihradky = []26 for i in range(poc_prihradek):27 prihradky.append([])2829 # Roztrden do prihrdek30 for p in seznam:31 k = extract_key(scf,ecf,p)32 prih = (k-min)33 prihradky[prih].append(p)3435 # "Slit" prihrdek zptky do seznamu36 pos = 037 for prih in prihradky:38 for p in prih:39 seznam[pos] = p40 pos += 14142 from math import log, ceil43 def radix_sort(seznam, d):44 mx = max(seznam)45 delka_klice = int(ceil(log(mx,10)))46 delka_casti = delka_klice/d47 ecf = delka_casti48 while ecf max: max = k elif k < min: min = k

    # Vyroben pihrdek poc_prihradek = (max-min)+1 prihradky = [] for i in range(poc_prihradek): prihradky.append([])

    # Roztdn do pihrdek for p in seznam: k = extract_key(scf,ecf,p) prih = (k-min) prihradky[prih].append(p)

    # "Slit" pihrdek zptky do seznamu pos = 0 for prih in prihradky: for p in prih: seznam[pos] = p pos += 1

    from math import log, ceildef radix_sort(seznam, d): mx = max(seznam) delka_klice = int(ceil(log(mx,10))) delka_casti = delka_klice/d ecf = delka_casti while ecf

  • KAPITOLA 3. VYHLEDVN, TRDC ALGORITMY

    46

  • Kapitola 4

    Analza jazyka

    V tto kapitole se podvme na problm analzy jazyka. Nepujde nm o prirozenjazyky to by presahovalo monosti vodnho kurzu ale o jazyky tzv. formln.Formln jazyky jsou jazyky, kter maj presne specifikovnu syntax, t.j. co je a conen v jazyce prpustn. Co to znamen presne specifikovat? Dohodneme se, e pronae cely to znamen, e je mon o danm retezci automaticky rozhodnout, zdaje v jazyce prpustn ci nikoliv.

    Prirozen jazyky tuto podmnku nesplnuj, protoe prpustnost je nejedno-znacn na prpustnosti nejak vety se casto neshodnou ani lingvist. Jed-noduchm diagonlnm argumentem vak lze zkonstruovat jazyky, kde jeprpustnost dan jednoznacne, nicmne neexistuje algoritmus, kter by o pr-pustnosti rozhodl. Takov jazyky ns vak zajmat nebudou.

    Prkladem formlnho jazyka je treba samotn Python. Dalm prkladem mue bttreba jazyk nejak matematick teorie. Nebo html, jazyk ve kterm se p webovstrnky. Toto vechno jsou relativne komplikovan jazyky, kterm odpovdaj i (re-lativne) komplikovan rozhodovac algoritmy. My si ukeme nekolik jednodu-ch jazyku a algoritmy, kter rozhoduj o prpustnosti pro tyto jazyky. Nejprvese vak budeme zabvat jednodum problmem vyhledvnm v textu. Op-timln algoritmus pro vyhledvn ns toti navede k definici tzv. regulrnch ja-zyku.

    47

  • KAPITOLA 4. ANALZA JAZYKA

    Vyhledvn v textu

    Nsledujc lohu zn kad, kdo v textovm editoru nekdy pouil funkci najdi(find).

    4.1 loha. Naleznete v danm textu (posloupnosti znaku) vechny vskyty neja-kho slova (vety, posloupnosti znaku).

    Prvn reen, kter ns napadne, mue vypadat treba takto:

    Algoritmus 4.2 Naivn Vyhledvn

    1 def find(text, s):2 found=[]3 for tpos in range(len(text)):4 f = True5 for spos in range(len(s)):6 if not text[tpos+spos] == s[spos]:7 f = False8 break9 if f:

    10 found.append(tpos)11 return found

    Vnej cyklus postupne prochz cel text a vnitrn cyklus na kad pozici testuje,zda se tam nevyskytuje hledan posloupnost. Tento vnitrn test provede nejvem operac, kde m je dlka hledanho retezce. Celkov sloitost algoritmu je tedyO(n m), kde n je dlka textu. Na prvn pohled ns napadne, e lpe to snad aninejde. Uvedomme si vak, e algoritmus del spoustu zbytecn prce. Predstavmesi, e hledme retezec aaaaa (s) v retezci aaaazzzz (text). V prvn iteraci zjis-tme, e na pozici 0 se retezec nenachz, protoe pt psmeno je z a ne a. V tuchvli vak u je zbytecn testovat vechny pozice do ptho psmena, protoe nretezec dn z neobsahuje. Ve skutecnosti, kdy testujeme vskyt retezce na pozicitpos pro tpos >= 5, vechny znaky text[tpos],...,text[tpos+4] jsmeu videli, take znalost znaku text[tpos+5] by nm teoreticky mela stacit k roz-hodnut, zda se s na pozici tpos vyskytuje. Jak tuto informaci vyut? Predstavmesi, e kad posloupnost p ctyr znaku m prirazeno nejak cslo np. Pak lze sestrojittabulku, kter nm pro kad znak z, a cslo np urc,

    zda je posloupnost p+z (posloupnost p nsledovan znakem z) je rovn hle-dan posloupnosti

    cslo np[1:]+z posloupnosti p+z bez prvnho znaku

    48

    def find(text, s): found=[] for tpos in range(len(text)): f = True for spos in range(len(s)): if not text[tpos+spos] == s[spos]: f = False break if f: found.append(tpos) return found

    Jonathan VernerSource code for Naivn Vyhledvn

  • KAPITOLA 4. ANALZA JAZYKA

    Pokud bychom takovou tabulku meli, bylo by vyhledvn velmi jednoduch. Al-goritmus by vypadal treba jako na vpisu 4.3 (uvaujeme jednoduch prklad, kdyhledme retezec aab v posloupnosti kter sestv pouze ze znaku a a b).

    Algoritmus 4.3 Vyhledvn za pomoci tabulky

    2 # Ocslovn posloupnost dlky 23 # (uvaujeme pouze retezce ze znaku 'a' a 'b')4 seqnum = {5 'aa':0,6 'ab':1,7 'bb':2,8 'ba':39 }1011 # Tabulka odpovdajc hledn12 # retezce 'aab'13 tabulka = {14 (0,'a'):(False,0),15 (0,'b'):(True,1),16 (1,'a'):(False,3),17 (1,'b'):(False,2),18 (2,'a'):(False,3),19 (2,'b'):(False,2),20 (3,'a'):(False,0),21 (3,'b'):(False,1)22 }2324 def smartfind(text, tabulka, lens):25 """ text ... retezec, ve kterm se vyhledv26 tabulka ... vyhledvac tabulka27 lens ... dlka hledanho retezce """28 found = []29 # precti prvnch lens znaku a zjisti cslo tto30 # posloupnosti31 np = seqnum[text[:lens-1]]3233 for tpos in range(lens,len(text)):34 # dle tabulky urci, zda nsledujc znak (text[tpos])35 # zakoncuje hledanou posloupnost a zroven36 # aktualizuj cslo posloupnosti np37 f, np = tabulka[(np,text[tpos])]38 if f:39 found.append(tpos-lens+1)40 return found

    Je lehk nahldnout, e sloitost je nyn linern v dlce textu, t.j. O(n). Problmje vak se sestrojenm tabulky (a s jej velikost). Mme-li abecedu o k znacch (vprkladu 4.3 je k = 2), a hledme retezec dlky m, pak nae tabulka bude mt veli-kost k km1 a tedy jej vroba bude trvat O(km). Tm bychom vymenili sloitostO(n m) za O(n+ km), co nen prli vhodn.

    Konecn automaty Nae tabulka je ale prli velik. Vimnete si naprklad, ehodnoty odpovdajc np = 1, 2 jsou stejn. To napovd, e by bylo mon uet-rit. Ukeme, e tabulku lze nahradit komplikovanej strukturou, tzv. konecnmautomatem. Jeho vhoda spocv v tom, e vhodn automat lze vytvorit v caseO(m) (msto O(2m)). Co to je konecn automat? Ukeme si to nejprve na prkladejednoduchho konecnho automatu zakdovanho zmku na kole.

    49

    # coding: utf-8# Oslovn posloupnost dlky 2# (uvaujeme pouze etzce ze znak 'a' a 'b')seqnum = { 'aa':0, 'ab':1, 'bb':2, 'ba':3}

    # Tabulka odpovdajc hledn# etzce 'aab'tabulka = { (0,'a'):(False,0), (0,'b'):(True,1), (1,'a'):(False,3), (1,'b'):(False,2), (2,'a'):(False,3), (2,'b'):(False,2), (3,'a'):(False,0), (3,'b'):(False,1)}

    def smartfind(text, tabulka, lens): """ text ... etzec, ve kterm se vyhledv tabulka ... vyhledvac tabulka lens ... dlka hledanho etzce """ found = [] # peti prvnch lens znak a zjisti slo tto # posloupnosti np = seqnum[text[:lens-1]] for tpos in range(lens,len(text)): # dle tabulky uri, zda nsledujc znak (text[tpos]) # zakonuje hledanou posloupnost a zrove # aktualizuj slo posloupnosti np f, np = tabulka[(np,text[tpos])] if f: found.append(tpos-lens+1) return found

    Jonathan VernerSource code for Vyhledvn za pomoci tabulky

  • KAPITOLA 4. ANALZA JAZYKA

    Koncepcne m zmek dva stavy odemceno a zamceno. Zadnm sprvnhokdu ho clovek mue ze stavu zamceno presunout do stavu odemceno. Ve skutec-nosti si vak mueme predstavit, e zmek m mimo dvou zkladnch stavu ruznvnitrn stavy odpovdajc ruznm monm kdovm kombinacm. Jeden z techtovnitrnch stavu stav odpovdajc sprvnmu kdu pak odpovd globlnmustavu odemceno, ostatn odpovdaj stavu zamceno. Nastavenm cslice na jednomz monch mst pak presouvme zmek z jednoho vnitrnho stavu do jinho.

    Obecn definice konecnho automatu je zobecnenm tto konkrtn situace.

    4.4 Definice. Konecn automat je dn mnoinou (vnitrnch) stavu S, mnoinou vstupu(abecedou) , prechodovou funkc f : S S, mnoinou koncovch stavuE S a jednm poctecnm stavem sb S.

    V prpade zmku by mnoina vnitrnch stavu byla mnoina vech 5 cifernch csel,Mnoina vstupu by byla mnoina vech dvojic (pozice, cslice), kde pozice je jednoz psmen A, B, C, D, E. Prechodov funkce pak odpovd zmene stavu pri zadnjedn cslice kdu. Koncov stav je jedin ten kter odpovd sprvnmu kdu.Poctecn stav je nhodn zamcen stav, kter byl nastaven pri zamykn.

    Definujme jete co to znamen vpocet. Intuitivne je to posloupnost stavu a vstupuzacnajc v poctecnm stavu a koncc v nejakm koncovm stavu takov, e pre-chod mezi sousednmi cleny tto posloupnosti je dn prechodovou funkc:

    4.5 Definice. Posloupnost stavu s = s0, . . . , sn a vstupu v = v0, . . . , vn1 jevpoctem automatu A = (SA,A, fA, EA, sbA) pokud s0 = s

    bA, sn EA a pro 1

    i n 1 plat si+1 = f(si, vi).

    Kdy se zamyslte nad algoritmem 4.3 mon Vs napadne, e by se dal popsat jakoprovden vpoctu nejakho konecnho automatu. Tabulka v tomto prpade prostezadv prechodovou funkci. Abychom dostali konecn automat, musme jete do-plnit poctecn stav, dva stavy odpovdajc precten prvnho znaku a koncov stav.

    50

  • KAPITOLA 4. ANALZA JAZYKA

    Vsledkem pak bude automat znzornen na nsledujcm obrzku. Kad ko-lecko odpovd stavu, ipky mezi kolecky odpovdaj prechodum mezi stavy da-nm vstupnm psmenem.

    0

    1

    2

    3

    found

    start

    ab

    a

    b

    a

    b

    a

    a

    b

    a

    b

    b

    b

    aa b

    a

    b

    Jak jsme poznamenali ji drve a je z obrzku videt, e stavy 1 a 2 jsou v podstateekvivalentn a mohli bychom je sloucit dohromady. Ve skutecnosti lze ale cel au-tomat jete podstatne zjednoduit tak, aby mel presne o jeden stav vce ne je dlkahledanho retezce (m). Stavy automatu na obrzku odpovdaj vem monm nej-ve dvouprvkovm posloupnostem psmen a,b. Naprklad stav 0 odpovd po-sloupnosti aa zatmco treba stav 3 odpovd posloupnosti ba. Zkladn mylenkaspocv v tom, e zahodme vechny stavy, kter neodpovdaj dnmu poctec-nmu seku hledanho retezce. Zskme tak automat znzornen nsledujcm ob-rzkem:

    51

  • KAPITOLA 4. ANALZA JAZYKA

    aa aab a a b

    ab

    a

    b

    a

    b

    Zbv jete ukzat, e takov automat umme sestrojit v case O(m). Sestrojit stavya zkladn dopredn ipky spojujc tyto stavy od nejkratho k nejdelmu je jed-noduch. Potrebujeme vak jete dodat ipky jdouc zpet. Rekneme, e jsme vnejakm stavu s kter odpovd retezci r. Abychom mohli sprvne dodat ipkyjdouc zpet, potrebovali bychom znt stav s odpovdajc retezci r[1:] (kadipka jdouc zpet toti bude odpovdat nejak ipce ze stavu s). Nen vak pro-blm si tento stav uloit v nejak pomocn promenn a tuto promennou postupneaktualizovat. Cel algoritmus v Pythonu (vcetne funkce find) lze nalzt na vpisu4.6.

    Pro zjemce dodejme, e existuje jete jin algoritmus, tzv. Boyer-Moore algorit-mus, kter re lohu 4.1 tak v caseO(m+n) ale v praxi je rychlej ne algoritmus4.6.

    Regulrn jazyky

    V predchozm odstavci jsme zavedli pojem konecnho automatu a ukzali, jak lzekonecn automaty vyut pro rychl hledn v textu. Nicmne samotn pojem ko-necnho automatu je zajmav i teoreticky, protoe se d ukzat, e charakterizujetrdu regulrnch jazyku.

    4.7 Definice. Je-li mnoina znaku (abeceda), znacme mnoinu vech (konec-nch) posloupnost prvku mnoiny . Prvkum mnoiny budeme rkat slova.Jazykem v abecede pak rozumme libovolnou podmnoinu L .

    4.8 Definice. Trda regulrnch jazyku nad abecedou je nejmen trda L splnujc:

    (i) Przdn jazyk je prvkem L,

    (ii) Pro kad znak a je {a} L,

    52

  • KAPITOLA 4. ANALZA JAZYKA

    Algoritmus 4.6 Vyhledvn pomoc konecnch automatu

    3 def build_automaton(retezec):4 """ Vrt automat rozpoznvajc retezec retezec.56 Funkce ve skutecnosti vrt seznam stavu, kde stav stav s indexem 0 je poctecn7 a stav posledn stav je koncovm stavem. Kad stav je seznamem ipek, kter z8 nej vedou, t.j. dvojice (znak, index_nsledujcho_stavu_pro_dan_znak)9 """

    10 c_state_num = 0 # index aktulne vytvrenho stavu11 help_state = [] # pomocn stav pro podretezec r[1:]12 c_state = [] # aktulne vytvren stav13 A = [None]*(len(retezec)+1) # Inicializujeme automat14 for c in retezec:15 A[c_state_num] = c_state1617 # Pridme doprednou ipku do aktulnho stavu18 c_state.append((c,c_state_num+1))1920 # Pridme zpetn ipky, t.j. zkoprujeme ipky z pomocnho stavu do aktulnho stavu.21 # Pokud z pomocnho stavu vede ipka oznacen znakem c pak prslune aktualizujeme22 # pomocn stav, jinak nastavme pomocn stav na 023 next_help_state = A[0]24 for sipka in help_state:25 char, target_state_num = sipka26 if char == c:27 next_help_state = A[target_state_num]28 else:29 c_state.append((char,target_state_num))3031 help_state = next_help_state32 c_state_num += 133 c_state = []3435 # Pridme koncov stav36 for sipka in help_state:37 char, target_state_num = sipka38 c_state.append((char,target_state_num))39 A[-1]=c_state40 return A4142 def optimalfind(text, s):43 A = build_automaton(s)44 c_state = A[0] # aktuln stav nastavme na poctecn stav automatu45 found_state_num = len(A)-1 # index koncovho stavu46 found = [] # seznam vskytu4748 # Protoe vskyt retezce s na pozici p najdeme teprve kdy precteme znak49 # na pozici p+len(s), musme len(s) odecst50 pos = -len(s)5152 for c in text:53 pos = pos+154 # Najdi ipku odpovdajc nactenmu psmenu. Pokud takov je, presun se do odpovdajcho55 # stavu, jinak se presun do poctecnho stavu56 move_to_zero = True57 for sipka in c_state:58 char, target_state_num = sipka59 if char == c:60 # pokud jsme v koncovm stavu, mme vskyt61 if target_state_num == found_state_num:62 found.append(pos)63 c_state = A[target_state_num]64 move_to_zero = False65 break66 if move_to_zero:67 c_state = A[0]68 return found

    53

    # coding: utf-8

    def build_automaton(retezec): """ Vrt automat rozpoznvajc etzec retezec. Funkce ve skutenosti vrt seznam stav, kde stav stav s indexem 0 je poten a stav posledn stav je koncovm stavem. Kad stav je seznamem ipek, kter z nj vedou, t.j. dvojice (znak, index_nsledujcho_stavu_pro_dan_znak) """ c_state_num = 0 # index aktuln vytvenho stavu help_state = [] # pomocn stav pro podetzec r[1:] c_state = [] # aktuln vytven stav A = [None]*(len(retezec)+1) # Inicializujeme automat for c in retezec: A[c_state_num] = c_state # Pidme dopednou ipku do aktulnho stavu c_state.append((c,c_state_num+1)) # Pidme zptn ipky, t.j. zkoprujeme ipky z pomocnho stavu do aktulnho stavu. # Pokud z pomocnho stavu vede ipka oznaen znakem c pak pslun aktualizujeme # pomocn stav, jinak nastavme pomocn stav na 0 next_help_state = A[0] for sipka in help_state: char, target_state_num = sipka if char == c: next_help_state = A[target_state_num] else: c_state.append((char,target_state_num)) help_state = next_help_state c_state_num += 1 c_state = [] # Pidme koncov stav for sipka in help_state: char, target_state_num = sipka c_state.append((char,target_state_num)) A[-1]=c_state return A def optimalfind(text, s): A = build_automaton(s) c_state = A[0] # aktuln stav nastavme na poten stav automatu found_state_num = len(A)-1 # index koncovho stavu found = [] # seznam vskyt # Protoe vskyt etzce s na pozici p najdeme teprve kdy peteme znak # na pozici p+len(s), musme len(s) odest pos = -len(s) for c in text: pos = pos+1 # Najdi ipku odpovdajc natenmu psmenu. Pokud takov je, pesu se do odpovdajcho # stavu, jinak se pesu do potenho stavu move_to_zero = True for sipka in c_state: char, target_state_num = sipka if char == c: # pokud jsme v koncovm stavu, mme vskyt if target_state_num == found_state_num: found.append(pos) c_state = A[target_state_num] move_to_zero = False break if move_to_zero: c_state = A[0] return found

    Jonathan VernerSource code for Vyhledvn pomoc konecnch automatu

  • KAPITOLA 4. ANALZA JAZYKA

    (iii) Trda L je uzavren na sjednocen a konkatenaci (jsou-li A,B L, pak jejichkonatenace, znacenAB, je jazyk sestvajc ze slov tvaruwAwB , kdewA Aa wB B).

    (iv) Trda L je uzavren na Kleeneho hvezdicku (je-li A L, pak i jazyk A jeprvkem L, kde A je nejmen nadmnoina A obsahujc przdnou mnoinua uzavren na konkatenaci).

    Tato definice vypad velmi abstraktne. Nicmne je prekvapiv, e m mnoho ruz-nch ekvivalentnch reformulac. Jedna mon definice vyuv konecn automaty:

    4.9 Veta. Jazyk L je regulrn, pokud existuje konecn automat A takov, e L je mno-inou slov w takovch, e existuje posloupnost stavu s, e (s, w) je vpoctem (viz 4.5)automatu A.

    Nabz se otzka, zda je kad jazyk regulrn. Jednoduchm argumentem se mu-eme presvedcit, e nikoliv: vech jazyku je toti nespocetne (kontinuum), ale ko-necnch automatu je jen spocetne (kad konecn automat je konecn objekt). Nenprli tek dokonce prmo sestrojit neregulrn jazyk.

    4.10 Cvicen. Ukate, e jazyk nad abecedou = {(, )} sestvajc ze sprvne uz-vorkovanch vrazu, nemue bt regulrn. (Hint: uvate retezec zacnajc n + 1-otevrenmi zvorkami, kde n je pocet stavu danho automatu.)

    Regulrn vrazy Ukeme si jete jinou ekvivalentn definici regulrnho jazyka,kter je bli puvodn definici a pouv pojem regulrnho vrazu. Tento pojemzavedl americk matematik Kleene (19091994) a umonuje velmi kompaktn po-pis regulrnch jazyku. Kadmu regulrnmu vrazu R odpovd jazyk L(R) dlensledujc definice

    4.11 Definice. Regulrn vraz nad abecedou (neobsahujc znaky , (, ), |, ) je de-finovn rekurzivne:

    , jsou regulrn vrazy, L() = , L() = {}, t.j. przdn jazyk a jazyksestvajc z przdnho slova,

    kad znak a je regulrnm vrazem, L(a) = {a},

    je-li r regulrn vraz, pak i (r) je regulrn vraz a L((r)) = L(r),

    jsou-li r, s dva regulrn vrazy, pak r|s a rs je regulrn vraz a L(r|s) =L(r) L(s),L(rs) = L(r) L(s) (alternace a konkatenace)

    54

  • KAPITOLA 4. ANALZA JAZYKA

    je-li r regulrn vraz, pak r je regulrn vraz a L(r) = L(r) (Kleenehohvezdicka).

    Aby se predelo zbytecnmu mnostv zvorek je stanovena standardn prioritaoperac: Kleeneho hvezdicka m nejvy prioritu, pak konkatenace a nakonec al-ternace.

    4.12 Prklad. Je-li = {a, b, c}, pak r = ac(aa|bb) je regulrn vraz popisujc jazyksestvajc ze slov zacnajcch psmenem a, pokracujcch libovolnm poctem znaku c a paklibovolnm poctem dvojic znaku aa nebo bb.

    Nsledujc veta rk, e regulrn vrazy presne popisuj regulrn jazyky.

    4.13 Veta. Jazyk L nad abecedou je regulrn prve kdy existuje regulrn vraz rtakov, e L = L(r).

    Aritmetick vrazy

    Ve cvicen 4.10 jsme uvedli, e jazyk sestvajc ze slov obsahujcch stejne otevre-nch jako zavrench zvorek nen regulrn. To naznacuje, e mnoho zajmavchjazyku pravdepodobne regulrnch nebude. Typickm reprezentantem je naprkladjazyk sestvajc z aritmetickch vrazu, kterm se nyn budeme podrobneji zab-vat. e je to neregulrn jazyk je videt z toho, e nejen e mus obsahovat stejnpocet otevrench jako zavrench zvorek, tyto zvorky navc mus bt sprvne po-skldan. S aritmetickmi vrazy se kad z vs nejsp setkal ji nekdy v prvntrde zkladn koly a mte intuitivn predstavu o tom, co to aritmetick vrazje. Abychom s nimi mohli pracovat bude uitecn tuto intuitivn predstavu zpres-nit do formln definice. Pro jednoduchost budeme uvaovat pouze aritmetickvrazy, ve kterch se nevyskytuj promenn a jedin povolen operace jsou sc-tn, odctn, nsoben a delen. Formlne tedy budou aritmetick vrazy retezcenad abecedou = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9,+,, , /, (, )}. Nejjednodum aritme-tickm vrazem je nejak libovoln cslo (retezec cslic, t.j. neprzdn prvek jazyka{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}). Tyto vrazy (t.j. csla) budeme nazvat atomickmi vrazy.

    4.14 Definice. Jazyk aritmetickch vrazu je nejmen podmnoina kter obsa-huje vechny atomick vrazy a je uzavrena na nsledujc operaci:

    Jsou-li a, b dva retezce a o {+,, , /} pak prvn operace (OP1) je defino-van takto:

    OP1(a, b, o) = aob

    55

  • KAPITOLA 4. ANALZA JAZYKA

    t.j. vsledkem je retezec sestvajc z konkatenace lev zvorky, retezce a,symbolu op, retezce b a prav zvorky.

    Je-li a retezec, pak druh operace (OP2) je definovan takto:

    OP2(a) = (a)

    t.j. vsledek je konkatenac lev zvorky, retezce a a prav zvorky.

    Stromov reprezentace Kad aritmetick vraz tedy vznikne z atomickch v-razu (csel) postupnm aplikovnm operace OP1 (aritmetick operace) resp. OP2(uzvorkovn). Tuto postupnou vstavbu aritmetickho vrazu si mueme pred-stavit jako (v naem prpade binrn) strom. V koredni stromu je vsledn vraz aposledn proveden operace, kterou byl zskn. Jeho detmi jsou pak ty jednoduvrazy, ze kterch byl pomoc tto operace vytvoren. V listech stromu jsou pak ato-mick vrazy. Nejlpe to je videt na nejakm prklade. Naprklad stavba vrazu(8 + 7) 5 je znzornena nsledujcm stromem:

    ((8+7)*5),OP1*

    5(8+7),OP2

    8+7,OP1+

    78

    Tento strom mueme jete trochu zjednoduit tm, e si v uzlech budeme pamato-vat pouze operace a zahodme vechny neatomick vrazy:

    *

    5()

    +

    78

    56

  • KAPITOLA 4. ANALZA JAZYKA

    Dalho zjednoduen mueme doshnout, pokud nm nezle na rozlien v-razu, kter se li pouze ruznm ekvivalentnm uzvorkovnm. V takovm pr-pade mueme proste zahodit uzly s druhou operac. Zskme tak strom:

    *

    5+

    78

    Je vak dobr si uvedomit, e v tomto prpade ji dolo ke ztrte informace: stejnstrom by nm toti vyel i pro vraz (8+7)*(5). Pro vetinu aplikac je vak duleithodnota vrazu, kterou tato ztrta neovlivn. Uvedomte si, e vraz 8+(7*5), kterm jinou hodnotu, d ve skutecnosti jin strom, ackoliv se od puvodnho vrazuli pouze uzvorkovnm; toto uzvorkovn je toti neekvivalentn.

    Se zvorkami souvis jete jeden problm. Ve zmnen vstavba vrazu nenvdy jednoznacn. Naprklad vrazu 8 + 7 5 odpovdaj nsledujc dva stromy:

    *

    5+

    78

    +

    *

    57

    8

    K tomuto problmu se dostaneme za chvli, a se budeme zabvat konstrukc techtostromu.

    Vyhodnocovn vrazu Predpokldejme vak na chvli, e strom pro dan arit-metick vraz ji mme v Pythonu k dispozici, naprklad pro vraz (8+7)*5 mmetedy strom

    1 >>> T = ['*',['+',[8,None,None],[7,None,None]],[5,None,None]]

    V tuto chvli je relativne jednoduch vraz vyhodnotit. Vyhodnocovn bude prob-hat rekurzivne tak, e spocteme hodnotu kadho uzlu, kter odpovd nejakmu

    57

  • KAPITOLA 4. ANALZA JAZYKA

    podvrazu. Hodnota listu, t.j. atomickch vrazu, je proste dan cslo. Hodnotavnitrnch uzlu se pak spocte aplikac operace uloen v uzlu na hodnoty jeho det.V pythonu to mue vypadat takto:

    1 def eval_tree(T):2 D,L,R = T34 # Hodnota listu je v nem uloen5 if L is None and R is None:6 return D78 # Rekurzivne spocteme hodnotu det9 LVal = eval_tree(L)

    10 RVal = eval_tree(R)1112 # Aplikujme operaci D13 if D == '+':14 return LVal + RVal15 if D == '-':16 return LVal - RVal17 if D == '*':18 return LVal * RVal19 if D == '/':20 return LVal / RVal

    Stromov reprezentace se hod pro dal zpracovn, pro uivatele ale nen prliciteln. Proto je uitecn mt funkci, kter strom prevede zpet na aritmetick vraz.I tuto funkce napeme snadno za pomoci rekurze:

    1 def tree_to_expr_naive(T):2 D,L,R = T34 if L is None and R is None:5 return str(D)67 return tree_to_expr_naive(L) + D + tree_to_expr_naive(R)

    Zde vak narazme na ve zmnen problm souvisejc s tm, e jsme zahodili z-vorky. N strom toti d vraz vraz 8+7*5, kter je mono cst dvemi zpusobyjako 8+(7*5) ci (8+7)*5. Konvence o prioritch operac pak preferuje druhou va-riantu, kter vak odpovd jinmu stromu! Abychom se tto dvojznacnosti vyhli,

    58

  • KAPITOLA 4. ANALZA JAZYKA

    budeme muset na vhodnch mstech vypsat zvorky. Sprvnej tedy bude napsatfunkci takto:

    1 def tree_to_expr_inorder(T):2 D,L,R = T34 if L is None and R is None:5 return str(D)67 ret = '('8 ret = ret + ' ' + tree_to_expr_inorder(L)9 ret = ret + ' ' + D

    10 ret = ret + ' ' + tree_to_expr_inorder(R)11 ret = ret + ' )'12 return ret

    Tato funkce vrt pro puvodn strom vraz ( ( 8 + 7 ) * 5 ). Kdyby dan vrazpsal clovek, pravdepodobne by vynechal vnej zvorky. Prebytecn zvorky bysamozrejme lo odstranit, bylo by to ale prli komplikovan, proto se smrme stm, e nae funkce obcas zvorkuje prli horlive.

    Prefixov a postfixov notace Zastavme se jete u funkce tree_to_expr_inorder.Funkce postupne rekurzivne prochz uzly stromu. Kdy prijde do danho uzlu,nejprve rekurzivne prevede lev podstrom na vraz, k tomuto vrazu prid ope-raci D a pak rekurzivne prevede prav podstrom a pripoj ho nakonec. Co by sestalo, kdybychom prohodili rdky 8 a 9? T.j. pri prchodu do uzlu bychom nejprvevypsali operaci v danm uzlu a teprve pak se zabvali podstromy. Vsledkemby byl nsledujc vraz

    ( * ( + 8 7 ) 5 )

    Nazveme takto zmenenou funkci tree_to_expr_preorder. Ackoliv jej vstupna prvn pohled nemus vypadat prli uitecne, m tento zpis svuj smysl a do-konce i jmno rk se mu prefixov zpis (a odpovdajcmu zpusobu prochzenstromu se rk preorder). V tomto zpise se nejprve zape operace a po n teprvensleduj operandy. Vhoda tohoto zpisu je (ackoliv to mon nen hned videt),e se obejde bez zvorek a tud odpadaj nejednoznacnosti, kter se zvorkamisouvis. Je vak treba dt pozor na to, e narozdl od standardnho tzv. infixovhozpisu, je treba nejak oddelovat csla (atomick vrazy): u vrazu +875 toti nenjasn, zda se maj sctat csla 8 a 75 nebo csla 87 a 5. Dal vhodou tohoto zpisu

    59

  • KAPITOLA 4. ANALZA JAZYKA

    je, e lze jednodue prevst zpet na stromovou reprezentaci:

    1 def parse_prefix(token_list):2 OPs = ['+','-','*','/']3 token = token_list.pop(0)4 if token in OPs:5 L = parse_prefix(token_list)6 R = parse_prefix(token_list)7 else:8 token = int(token)9 L = None

    10 R = None11 return (token,L,R)

    Funkce parse_prefix dostane na vstupu prefixove zapsan vraz jako seznamjednotlivch prvku vrazu (prvek je bud opertor nebo cslo) a vrt stromovoureprezentaci.

    4.15 Cvicen. Indukc ukate, e funkce parse_prefix je sprvne napsan.

    Funkci parse_prefix lze jednodue upravit tak, aby vraz msto preveden nastromovou reprezentaci rovnou vyhodnotila. Tuto variantu si ukeme v nerekur-zivnm proveden, kter pouv zsobnk:

    1 def eval_prefix(token_list):2 OPs = ['+','-','*','/']3 stack = []4 while len(token_list) > 0:5 token = token_list.pop()6 if token in OPs:7 L = stack.pop()8 R = stack.pop()9 if token == '+':

    10 stack.append(L+R)11 elif token == '-'12 stack.append(L-R)13 elif token == '*'14 stack.append(L*R)15 elif token == '/'16 stack.append(L/R)17 else:18 stack.append(int(token))19 return stack.pop()

    60

  • KAPITOLA 4. ANALZA JAZYKA

    Zsobnk ve skutecnosti pouze simuluje pouit rekurze pokud bychom vrazstack.pop nahradili rekurzivnm volnm a vraz stack.append prkazem re-turn, dostali bychom rekurzivn variantu.

    4.16 Cvicen. Pokud ve funkci tree_to_expr_inorder prohodme naopak rdky9 a 10, dostaneme funkci, kterou je vhodn nazvat tree_to_expr_postorder.Tato funkce vrt vraz v tzv. postfixov notaci. Naprogramujte funkce parse_postfixa eval_postfix.

    Zpracovn infixov notace Vratme se nyn k standardn t.j. infixov notaci a po-kusme se navrhnout algoritmus, kter ji prevede na stromovou reprezentaci. Jakjsme ji zmnili, infixov notace m problmy s jednoznacnost jednomu vrazumohou odpovdat ruzn stromy. Tento problm by lo vyreit vhodnou modifi-kac jazyka aritmetickch vrazu, kter by pomoc zvorek zajistila jednoznacnost.Operaci OP2 (zvorkovn) bychom plne vypustili a operaci OP1 bychom modi-fikovali:

    OP1(a, b, o) = (aob)

    Takto modifikovan jazyk je vak pro lidi trochu neikovn vede k nadmernmumnostv zvorek. Msto toho se problm jednoznacnosti obvykle re zavedenmtzv. priority opertoru: opertory s vy prioritou se vyhodnocuj pred opertorys ni prioritou. Toto pravidlo umonuje kadmu vrazu jednoznacne priraditprslun strom. Popeme nyn zpusob, jak prevst infixov zpis na strom pri re-spektovn priority opertoru. Na vstupu predpokldme retezec. V prvnm krokuz tohoto retezce odstranme vnej zvorky. Pokud se ve zbvajcm vrazu nena-chz dn opertory, pak mme atomick vraz odpovdajc jednoprvkovmustromu. V opacnm prpade v retezci nalezneme opertor s nejni prioritou mezitemi, co se nachzej vne vech zvorek. Pokud je takovchto opertoru vce, vez-meme z nich nejpravej. Tento opertor vlome do korene stromu. Zroven nmopertor vraz rozdel na dve csti. Rekurzivnm volnm prevedeme levou i pra-vou cst na stromy, kter napojme na koren. V Pythonu to bude vypadat treba jakona vpisu 4.17.

    Vhoda ve uvedenho algoritmu je jeho jednoduchost; nen vak prli efektivn.Ukeme si proto algoritmus, kter zvldne infixov vraz zpracovat v linernmcase. Pro jednoduchost uvedeme pouze verzi, kter vraz rovnou vyhodnot. N-vrh verze, kter zroven postav i strom, ponechme do cvicen. Idea algoritmuje zaloen na jednoduchm pozorovn. Pokud priorita opertoru ve vyhodnoco-vanm vraze zleva doprava nekles, pak lze vraz jednodue vyhodnotit tak, esi pamatujeme aktuln hodnotu vrazu a prochzme zprava do leva j postupne

    61

  • KAPITOLA 4. ANALZA JAZYKA

    Algoritmus 4.17 Parsovn infixovch vrazu

    2 def count_open_brackets(exp):3 """ Spoct kolika otevrenmi zvorkami zacn seznam @exp """4 ret = 05 for c in exp:6 if c != '(':7 return ret8 ret = ret+19 return ret10 def remove_brackets(exp):11 """ Vrt seznam @exp bez vnejch zvorek. """12 open_brackets = count_open_brackets(exp)13 coun