8. Datové struktury¶
Při řešení různorodých problémů se hodí mít možnost uspořádat si data do takového uspořádání (struktury), která nám napomůže ke správnému řešení či zlepší jeho efektivitu, přehlednost atp. Tyto struktury jsou obecně nazývány datovými a informatika jim věnuje nemálo pozornosti.
Datová struktura
Datová struktura je konkrétní způsob uspořádání nějakých dat. Konkrétní datová struktura typicky implementuje nějakou abstraktní datovou strukturu, která popisuje s jakými daty pracujeme a jaké operace s danou strukturou můžeme provádět (e.g. přidání/odebrání prvku atp.)
Pole
Jednou z nejjednodušších struktur, mající fixní počet míst pro data jednoho
datového typu, je vestavěna do jazyků jako C, C++, Java apod. V Pythonu se
místo této struktury používají seznamy, která mají řadu funkcí navíc,
například append
, či možnost mít v seznamu libovolně různorodé datové
typy.
Seznam
Jednou z nejjednodušších datových struktur v Pythonu je seznam (anglicky/slangově “list”), probíraný v kapitole Řetězce a seznamy. Pro zopakování, nejdůležitější operace:
V této kapitole se budeme zabývat jen zásobníky, frontami, slovníky a množinami, ale datových struktur existuje velká řada a během dalšího studia se s nimi určitě potkáte, například se stromy:
(zdroj: XKCD)
8.1. Zásobníky¶
Zásobník (anglicky “stack”), slangově s je speciální variantou seznamu,
umožňující prvky vkládat a odebírat pouze z vrcholu zásobníku, tedy podle
principu LIFO: Last In – First Out. Tyto dvě dodatečné operace se nazývají
pop
a push
. V Pythonu pro ně můžeme použít seznamy, které obě
operace implementují:
8.1.1. Interpretace výrazu v postfixu¶
Při běžném počítání s aritmetickými výrazy 6 + 5
používáme takzvanou
infixovou notaci s operátorem mezi operandy. V tomto příkladě budeme pracovat
s takzvaně reverzní polskou notací, kdy operátor následuje po operandech,
například 6 5 *
.
Vyzkoušejte si převést 7 * 8 + 2 * 6 + 5
do postfixu a následně si ho
zkuste spolu s 5 7 * 3 - 4 +
vyhodnotit.
Napište funkci, která obdrží řetězec v postfixové notaci a ta výraz za pomocí
zásobníku vyhodnotí. Implementuje operace +
, -
, /
a *
v
plovoucí čárce.
Tip: může se vám hodit metoda split
, která je použitelná na řetězcích: "a
b c".split()
.
def evaluate_postfix(expr: str) -> float:
pass
def test_evaluate_postfix() -> None:
assert evaluate_postfix("8 7 * 6 5 + 2 * +")) == 78
assert evaluate_postfix("6 5 -")) == 1
assert evaluate_postfix("15 7 1 1 + - / 3 * 2 1 1 + + -")) == 5
8.1.2. Stejná výška¶
Máte 3 sloupce složené z válců, každý válec má stejný průměr, ale mají různou výšku. Výšku sloupce můžete změnit odebráním válce zvrchu. Válce pouze odebírejte, nepřehazujte je na jiné věže.
Napište funkci equal_height
, která vrátí nejvyšší možnou výšku takovou, že všechny sloupce
mají stejnou výšku. Funkce zároveň upraví seznamy sloupců (viz asserty níže).
Bonusový úkol: upravte pro jakýkoliv počet sloupců
(t1: List[int], t2: List[int], t3: List[int] -> ts: List[List[int]]
).
def equal_height(t1: List[int], t2: List[int], t3: List[int]) -> int:
pass
def test_equal_height() -> None:
t1 = [1, 1, 1, 2, 3]
t2 = [2, 3, 4]
t3 = [1, 4, 1, 1]
assert equal_height(t1, t2, t3) == 5
assert t1 == [1, 1, 1, 2]
assert t2 == [2, 3]
assert t3 == [1, 4]
8.1.3. Další větší než¶
Napište funkci next_greater
, která pro seznam lst
vrátí seznam n-tic
(e, ng)
, kde e
je prvek ze seznamu lst
a ng
je další prvek,
který je větší než e
. Když se v seznamu nenachází větší prvek, použijte
None
.
Ve výsledném seznamu se musí nacházet všechny prvky z předaného seznamu, na pořadí nezáleží.
def next_greater(lst: List[int]) -> List[Tuple[int, Optional[int]]]:
pass
def test_next_greater() -> None:
assert set(next_greater([4, 5, 2, 25])) == {(4, 5), (5, 25), (2, 25), (25, None)}
assert set(next_greater([13, 7, 6, 12])) == {
(13, None),
(7, 12),
(6, 12),
(12, None),
}
assert set(next_greater([11, 13, 21, 3])) == {
(11, 13),
(13, 21),
(21, None),
(3, None),
}
8.2. Fronty¶
Fronta (anglicky/slangově “queue” či “deque”) je struktura, do které lze
prvky přidávat (push
) a odebírat (pop
). Pořadí odebírání se ale
děje podle principu FIFO (First In – First Out), tak jako v klasické frontě
v obchodě. Jedná se tedy opět jen o speciální variantu seznamu, umožňující
vkládat prvky na jeden konec a odebírat z druhého konce. Mohli bychom tedy
klidně opět využít seznamy, tento způsob však není moc efektivní (přidávání
nebo odebírání prvků ze začátku seznamu způsobí, že všechny prvky dál se
musí posunout o 1 pozici). Lepší je tedy použít specializovaný typ deque z
knihovny collections
.
from collections import deque
queue = deque(["Petr", "Zdenek", "Filip"]) # 3 studenti cekaji ve fronte na obed
queue.append("Kuba") # Kuba prisel na konec fronty
print(queue) # deque(["Petr", "Zdenek", "Filip", "Kuba"])
queue.append("Roman") # Roman prisel na konec fronty
print(queue) # deque(["Petr", "Zdenek", "Filip", "Kuba", "Roman"])
student = queue.popleft() # prvni student ve fronte (Petr) dostal obed
print(queue, student) # deque(["Zdenek", "Filip", "Kuba", "Roman"]), student: "Petr"
student = queue.popleft() # dalsi student ve fronte (Zdenek) dostal obed
print(queue, student) # deque(["Filip", "Kuba", "Roman"]), student: "Zdenek"
8.2.1. Má mě rád, nemá mě rád¶
Nejprve si vygenerujeme kytici lineárně uspořádaných n
květin, každou o 1
až 4 okvětních lístcích. Poté vezmeme první květinu a utrhneme z ní právě jeden
lístek a zařadíme ji nakonec. Postup opakujeme dokud nejsou všechny květiny
otrhané. Má vás rád?
from random import randint
def game(n: int) -> None:
pass
print(game(1))
print(game(10))
8.2.2. Horký brambor¶
Horký brambor je jednoduchá dětská hra, kterou hraje n
lidí s jedním horkým
bramborem, který si mezi sebou přehazují. Kdo má brambor vyhodnotí jednoduché
kritérium (jako každý sedmý) a pokud toto kritérium splňuje, pak vypadává ze
hry. Vyhrává poslední hráč.
Naprogramujte tuto hru za použití fronty. Přičemž hraje n
lidí, jejichž
jména dostane program v seznamu a vypadává každý k
-tý hráč. Průběh hry,
vítěze a počet kol průběžně vypisujte.
def hot_potato(namelist: List[str], k: int) -> None:
pass
hot_potato(["Bill","David","Susan","Jane","Kent","Brad","Sam"],7)
# Bill is off!
# Sam is off!
# Kent is off!
# David is off!
# Brad is off!
# Jane is off!
# After 43 rounds Susan is winner!
8.2.3. Fronta v obchodě¶
V obchodě je fronta u samoobslužných pokladen. Vaším úkolem je napsat funkci
queue_time
, která vypočítá nejmenší možný celkový čas nutný k vyřízení celé
fronty.
Funkce dostává parametry customers
a n
. customers
je seznam
kladných čísel, každé číslo reprezentuje zákazníka a jeho hodnota udává čas,
který potřebuje k vyřízení nákupu. n
je kladné číslo, které reprezentuje
počet pokladen.
from collections import deque
def queue_time(customers: Deque[int], n: int) -> int:
pass
def test_queue_time() -> None:
# they have to wait for each other
assert queue_time(deque([5,3,4]), 1) == 12
# because 1st customer spends 10 "times" and others go to second machine
assert queue_time(deque([10,2,3,3]), 2) == 10
assert queue_time(deque([2,3,10]), 2) == 12
8.3. Slovníky¶
Slovník, někdy též asociativní seznam (slangově též “mapa”) mapuje klíče na hodnoty. Je podobný seznamu, ale jeho prvky nejsou indexovány pomocí posloupnosti celých čísel, ale pomocí klíčů. Klíčem může být například číslo, řetězec nebo jiný neměnitelný objekt, tedy seznam klíčem být nemůže. Pod tímto klíčem můžeme přistupovat k uloženým hodnotám, jsou-li takové ve slovníku přítomny, či nové ukládat.
8.3.1. Morseova abeceda¶
Napište funkce pro převod řetězce do a z Morseovy abecedy.
MORSE_CODE = {
"A": ".-", "B": "-...", "C": "-.-.", "D": "-..",
"E": ".", "F": "..-.", "G": "--.", "H": "....",
"I": "..", "J": ".---", "K": "-.-", "L": ".-..",
"M": "--", "N": "-.", "O": "---", "P": ".--.",
"Q": "--.-", "R": ".-.", "S": "...", "T": "-",
"U": "..-", "V": "...-", "W": ".--", "X": "-..-",
"Y": "-.--", "Z": "--..", "1": ".----", "2": "..---",
"3": "...--", "4": "....-", "5": ".....", "6": "-....",
"7": "--...", "8": "---..", "9": "----.", "0": "-----",
}
def encode(value: str) -> str:
pass
def decode(value: str) -> str:
pass
def test_encode() -> None:
assert encode('SOS') == "... --- ..."
assert encode('HELLOWORLD') == ".... . .-.. .-.. --- .-- --- .-. .-.. -.."
def test_decode() -> None:
assert decode('.... . .-.. .-.. --- .-- --- .-. .-.. -..') == "HELLOWORLD"
assert decode(encode('IDENTITY')) == "IDENTITY"
zkuste reprezentovat abecedu jinou datovou strukturou; která by byla vhodná?
zkuste implementovat podporu pro mezery ve vstupu a case-insensitive vstup
8.3.2. Frekvenční analýza písmen¶
Napište funkci freq_analysis(text)
, která spočítá výskyt jednotlivých
písmen (znaků) ve vstupním textu a následně tento seznam vypíše setříděný
sestupně podle počtu výskytů.
dummy = """Monty Python and Monty Python all over here."""
def freq_analysis(text: str) -> None:
pass
freq_analysis(dummy)
8.3.3. Frekvenční analýza slov¶
Napište funkci freq_analysis(text)
, která spočítá výskyt jednotlivých slov
ve vstupním textu a následně tento seznam vypíše setříděný podle abecedy.
Tip: může se vám hodit metoda split
, která je použitelná na řetězcích: "a
b c".split()
.
dummy = """Monty Python and Monty Python all over here."""
def freq_analysis(text: str) -> None:
pass
freq_analysis(dummy)
8.4. Množiny¶
V množině nemají jednotlivé prvky své indexy, ale můžeme prvky přidávat i odebírat a testovat, zda je nějaký prvek v množině. Množina neobsahuje duplicity (když do ní třikrát přidáme číslo 4, pořád ho bude obsahovat pouze jedenkrát). Přestože nemůžeme přistupovat k jednotlivým prvkům pomocí indexů, můžeme projít celou množinu (a provést nějakou operaci pro každý prvek množiny).
Množinové operace na množinách
8.4.1. Kontrola unikátnosti seznamu¶
Napište funkci, která zkontroluje, zda předaný seznam obsahuje jen unikátní položky.
def unique_check(temp: List[int]) -> bool:
pass
def test_unique_check():
assert unique_check([1, 5, 6, 5, 4, 9]) == False
assert unique_check([1, 5, 6, 3, 9]) == True
8.4.2. Kontrola řádku Sudoku¶
Napište funkci, která zkontroluje, zda předaný seznam reprezentuje správný řádek vyplněného Sudoku, tj. obsahuje pouze čísla 1 až 9 a každé z nich právě jedenkrát.
def sudoku_line_check(line: List[int]) -> bool:
pass
def test_sudoku_line_check() -> None:
assert sudoku_line_check([1, 2, 8, 9, 3, 5, 6, 7, 4]) == True
assert sudoku_line_check([1, 2, 8, 9, 3, 5, 7, 4]) == False
assert sudoku_line_check([1, 1, 2, 8, 9, 3, 5, 7, 4]) == False
assert sudoku_line_check([0, 1, 2, 3, 4, 5, 6, 7, 8]) == False
8.5. Procvičování¶
8.5.1. Vyhodnocení logického výrazu v postfixové notaci¶
Funkce vrátí True, pokud je předaný výrok pravdivý, jinak False. Výraz se skládá z následujících znaků:
‘T’: True
‘F’: False
‘N’: negace
‘K’: konjunkce
‘A’: disjunkce
‘C’: implikace
def evaluate_logic_expression(expr: str) -> bool:
pass
def test_evaluate_logic_expression() -> None:
assert evaluate_logic_expression("TFK") == False
assert evaluate_logic_expression("FNTFCNK") == True
8.5.2. CalcuLoS¶
Následující úloha byla adaptována z šifrovací hry InterLoS 2015.
Napište program, který vezme za vstup řetězec složený pouze z písmen
INTERLOS
a transformuje (respektive vyhodnotí) na řetězec složený z písmen
LOS
podle níže popsaných pravidel.
Vyhodnocování probíhá zleva, narazí-li se na nějaké z písmen LOS
(hodnota)
pokračuje se směrem vpravo. Je-li nalezen nějaký znak z INTER
(operátor)
vyhodnotí se za použití argumentů, které ihned následují. Přitom ale může
dojít k situaci, že tyto argumenty nejsou hodnotami, ale operátory, pak se musí
vyhodnotit nejdříve tyto argumenty.
Ukázkové vyhodnocení:
LINLRETOS Nejprve musíme vyhodnotit T na O → O
LINLREOS Pak musíme vyhodnotit E na O a S → SO
LINLRSO Následně musíme vyhodnotit R na S → S S
LINLSSO Poté musíme vyhodnotit N na L a S → S
LISSO A nakonec musíme vyhodnotit I na S a S → O
LOO Výsledek.
Operátor I bere dva argumenty, vrací jednu hodnotu podle následující tabulky:
L |
L |
L |
L |
O |
L |
O |
S |
S |
L |
S |
O |
Operátor N bere dva argumenty, vrací jednu hodnotu podle následující tabulky:
L |
L |
O |
S |
O |
O |
S |
L |
S |
S |
L |
O |
Operátor T bere jeden argument, vrací jednu hodnotu podle následující tabulky:
L |
S |
O |
O |
S |
L |
Operátor E bere dva argumenty, vrací dvě hodnoty: E x y → y x
pro všechny x
a y
z hodnot.
Operátor R bere jeden argument, vrací dvě hodnoty: R x → x x
pro všechny x
z hodnot.
8.5.3. Kontrola uzávorkování¶
Napište funkci, která pro zadaný řetězec složený pouze ze závorek [](){}
ověří, že jde o korektní uzávorkování.
def parenthesis_check(value: str) -> bool:
pass
def test_parenthesis_check() -> None:
assert parenthesis_check('([]({()}))[]{[()]}') == True
assert parenthesis_check('([)]') == False
8.5.4. Placení v kině¶
Před kinem čekají lidi ve frontě na nový film. Každý má jenom jednu 100, 50 nebo 25dolarovou bankovku. Vstupenka stojí 25 dolarů.
Napište funkci can_sell_tickets
, která dostane frontu a vrátí True
,
když je možné prodat všem vstupenky, jinak False
.
from collections import deque
def can_sell_tickets(people: Deque[int]) -> bool:
pass
def test_can_sell_tickets() -> None:
assert can_sell_tickets(deque([25, 25, 50]))
# not enough to give change for 100
assert not can_sell_tickets(deque([25, 100]))
# can't give back 75 exactly
assert not can_sell_tickets(deque([25, 25, 50, 50, 100]))