Základy algoritmizace I

Smyslem je vytvořit algoritmus, který vyřeší požadovanou úlohu. Dobré je se nejdříve zamyslet nad postupem, použít papír a tužku, a teprve pak začít tvořit program.

Podmínky

Logické operátory a operace == je rovno, != není rovno, < > <= >=, ! negace, & a zároveň, | nebo (pro všechny prvky srovnávaných vektorů), && a || porovnávají jen první prvek vektorů.

Operátory mají různé pořadí vyhodnocování (tzv. precedence operátorů). Podobně, jako * má vyšší přednost než +, tak & má vyšší přednost než |, oba mají ale vyšší přednost než např. ==. Pokud si nejsme jisti, je lepší závorkovat. Zvýší se tím i přehlednost zápisu.

identical() vrátí, zda jsou obsahy parametrů identické, xor() provádí exclusive-or operaci, which(vektor > 3) vrátí indexy prvků, splňujících podmínku (NA přeskočí), což je výhoda oproti pouhému samostatnému vektor > 3. all(vektor > 3) vrátí TRUE/FALSE, zda všechny prvky vektoru splňují podmínku, any(vektor > 3) vrátí, zda alespoň jeden prvek splňuje podmínku. is.element(co, vČem) nebo co %in% vČem vrátí, zda je prvek co obsažen ve vektoru vČem. all(kandidáti %in% vČem) ověří, že všichni kandidáti jsou součástí vČem. match(co, vČem) vrátí index první nalezené pozice.

Nápověda: ?‘if’

x <- 5

if (x == 0) {
    print("x je 0.")
}

print("Pokračujeme.")
## [1] "Pokračujeme."
x <- 0

if (x == 0) {
    print("x je 0.")
}

print("Pokračujeme.")
## [1] "x je 0."
## [1] "Pokračujeme."
a <- 5
b <- 3
if (b == 0) {
    print("Nulou nelze dělit.")
} else {
    a / b
}
## [1] 1.666667

Poznámka: else musí být na stejném řádku, jako znak }. V jiných jazycích je to jinak, ale R je R :-)

a <- 5
b <- 0
if (b == 0) {
    print("Nulou nelze dělit.")
} else {
    a / b
}
## [1] "Nulou nelze dělit."
x <- 5
if (x == 0) {
    print('x je 0')
} else if (x < 10) {
    print('x je menší než 10')
} else if (x < 20) {
    print('x je menší než 20')
} else if (x >= 20 & x <= 30) {
    print('x je mezi 20 a 30 (včetně)')
} else {
    print('x je něco, co neřešíme')
}
## [1] "x je menší než 10"

Najde se první platná podmínka, ostatní se pak již netestují. Přestože je x zároveň menší než 10 i než 20, provede se jen první platná podmínka.

pozdrav <- "ahoj"

if (pozdrav == "ahoj") {       # Porovnávání řetězců
    print("Nazdar.")
} else {
    print("Dobrý den.")
}
## [1] "Nazdar."
x <- 5

hodnota <- if (x > 3) {        # Poněkud nezvyklá, ale užitečná konstrukce.
    10
} else {
    0
}

hodnota
## [1] 10
# Konstrukce ve stylu "Excel". Takhle je to pěkné, ale pro složitější větvení
# by to bylo značně nepřehledné.
x <- 5

hodnota <- ifelse(x > 3, 10, 0)
hodnota
## [1] 10

Cykly

for (i in 1:8) {
    cat("Dobrý den. ")   # i nemusí být vůbec použito
}

cat("Hotovo.\n")
## Dobrý den. Dobrý den. Dobrý den. Dobrý den. Dobrý den. Dobrý den. Dobrý den. Dobrý den.
## Hotovo.
for (i in 1: 5) {
    print(i)    # příkaz se opakuje pro každé nové 'i'
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
print(i)  # v i zůstává poslední hodnota 5 (což není v různých jazycích samozřejmé)
## [1] 5

Pozor na operátor dvojtečka : a jeho prioritu, která je vyšší než aritmetické operace (na rozdíl od např. jazyka Matlab)! Pokud se mají meze cyklu počítat vzorcem, výrazy je nutno závorkovat!

1:5
## [1] 1 2 3 4 5
1-1 : 5-1      # pravděpodobně dost nečekaný výsledek
## [1] -1 -2 -3 -4 -5
(1-1) : (5-1)  # asi to, co jsme měli na mysli
## [1] 0 1 2 3 4

Pokud bychom závorky zapomněli, dostali bychom vskutku neočekávané výsledky, přičemž si chyby občas ani nemusíme všimnout.

for (i in 0: 10-1) {
  print(i)
}
## [1] -1
## [1] 0
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
## [1] 6
## [1] 7
## [1] 8
## [1] 9

Různé způsoby procházení vektoru

jmena <- c("Tomáš", "Petr", "Ivo", "Hana", "Jana", "Tereza")
for (i in 1:length(jmena)) {   # nebezpečné
    jmeno <- jmena[i]
    cat(i, ". prvek je ", jmeno, "\n", sep="")
}
## 1. prvek je Tomáš
## 2. prvek je Petr
## 3. prvek je Ivo
## 4. prvek je Hana
## 5. prvek je Jana
## 6. prvek je Tereza
jmena <- character(0)
for (i in 1:length(jmena)) {   # nebezpečné
    jmeno <- jmena[i]
    cat(i, ". prvek je ", jmeno, "\n", sep="")
}
## 1. prvek je NA
## 0. prvek je
jmena <- c("Tomáš", "Petr", "Ivo", "Hana", "Jana", "Tereza")
for (i in seq(1, length(jmena))) {   # nebezpečné
    jmeno <- jmena[i]
    cat(i, ". prvek je ", jmeno, "\n", sep="")
}
## 1. prvek je Tomáš
## 2. prvek je Petr
## 3. prvek je Ivo
## 4. prvek je Hana
## 5. prvek je Jana
## 6. prvek je Tereza
for (i in seq(1, length(jmena), by = 1)) {   # lepší, ale může skončit s error
    jmeno <- jmena[i]
    cat(i, ". prvek je ", jmeno, "\n", sep="")
}
## 1. prvek je Tomáš
## 2. prvek je Petr
## 3. prvek je Ivo
## 4. prvek je Hana
## 5. prvek je Jana
## 6. prvek je Tereza
library(rPraat)
for (i in seqM(1, length(jmena))) {   # bezpečné
    jmeno <- jmena[i]
    cat(i, ". prvek je ", jmeno, "\n", sep="")
}
## 1. prvek je Tomáš
## 2. prvek je Petr
## 3. prvek je Ivo
## 4. prvek je Hana
## 5. prvek je Jana
## 6. prvek je Tereza
for (i in seq_along(jmena)) {  # v klidu
    jmeno <- jmena[i]
    cat(i, ". prvek je ", jmeno, "\n", sep="")
}
## 1. prvek je Tomáš
## 2. prvek je Petr
## 3. prvek je Ivo
## 4. prvek je Hana
## 5. prvek je Jana
## 6. prvek je Tereza
for (jmeno in jmena) {  # v pohodě, akorát nemáme informaci o pořadí jmeno v jmena
    cat(jmeno, "\n")
}
## Tomáš 
## Petr 
## Ivo 
## Hana 
## Jana 
## Tereza

Problém s automatickým krokem

Problém s automatickým krokem operátoru : je nebezpečný, spousty matematický vzorců jsou psány jako opakování pro rozsah od 0 do N-1, což bychom zapsali 0 : (N-1) Pozor na nutné závorky!

N <- 5
for (i in 0: (N-1)) {
    print(i)
}
## [1] 0
## [1] 1
## [1] 2
## [1] 3
## [1] 4

Ale co když bude N == 0? Pak bychom v našem automatickém algoritmu očekávali, že se for přeskočí, jako tomu je ve většině normálních programovacích jazyků. Ne však v R…

N <- 0
for (i in 0: (N-1)) {
    print(i)
}
## [1] 0
## [1] -1

Řešení pomocí pevného kroku ve funkci seq() sice neudělá tento neočekávaný výsledek, ale skončí s chybou (což je sice nepříjemné - musíme to nějak vyřešit - ale aspoň víme o problému a program si “nedělá, co chce”, jako v předchozí variantě.)

N <- 0
for (i in seq(0, N-1, by = 1)) {
    print(i)
}

Skript by se předčasně ukončil s chybou: Error in seq.default(0, N - 1, by = 1) : wrong sign in ‘by’ argument.

Ideální by tedy bylo mít funkci, která by pro zadaný rozsah hodnot s krokem +1 vyrobila prázdný vektor. Tím by for cykl neproběhl, ale ani neskončil s chybou. Tedy něco ve stylu funkce seq_along(), která pro prázdný vektor vrátí prázdný rozsah indexů:

seq_along(c())
## integer(0)

Takovou funkci nalezneme např. v balíčku library(rPraat) a jmenuje se seqM(), protože se snaží napodobit inteligentní chování matlabovské funkce linspace().

library(rPraat)

N <- 0
for (i in seqM(0, N-1, by = 1)) {
    print(i)
}
print("A pokračujeme...")
## [1] "A pokračujeme..."

Cykly while a repeat

pocet <- 0

while (pocet < 10) {     # opakuj, dokud platí podmínka
    print(pocet)
    pocet <- pocet + 1
}
## [1] 0
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
## [1] 6
## [1] 7
## [1] 8
## [1] 9
x <- 0
repeat {           # opakuj pořád
    x <- x + 1
    
    if (x < 3)
        next       # skoč na repeat
    
    if (x > 9)
        break      # vyskoč ven z tohoto cyklu
    
    print(x)
}
## [1] 3
## [1] 4
## [1] 5
## [1] 6
## [1] 7
## [1] 8
## [1] 9

Pozn. takovýmto strukturám je lépe se vyhýbat, protože se jednoduše může stát chyba v úvaze a cyklus nikdy neskončí. Odborně se tomu říká tzv. zacyklení.

Vnořené bloky

Cykly a další bloky lze navzájem vnořovat. Co bude výsledkem následujících programů?

for (i in 1:3) {
    for (j in 1:5) {
        cat("i = ", i, ", j = ", j, "\n", sep = "")
    }
}
for (i in 1:3) {
    for (j in 1:5) {
        cat("i = ", i, ", j = ", j, "\n", sep = "")
        break
    }
}
for (i in 1:3) {
    for (j in 1:5) {
        cat("i = ", i, ", j = ", j, "\n", sep = "")
    }
    break
}
for (i in 1:8) {
    cat(i, ". ", sep = "")
    for (j in 1:8) {
        cat(j, " ", sep = "")
    }
    cat("\n")
}
## 1. 1 2 3 4 5 6 7 8 
## 2. 1 2 3 4 5 6 7 8 
## 3. 1 2 3 4 5 6 7 8 
## 4. 1 2 3 4 5 6 7 8 
## 5. 1 2 3 4 5 6 7 8 
## 6. 1 2 3 4 5 6 7 8 
## 7. 1 2 3 4 5 6 7 8 
## 8. 1 2 3 4 5 6 7 8

Problémy s desetinnými čísly

V R na ně díky dobře vymyšlené funkci seq() skoro nenarazíme.

for (i in seq(0, 0.3, by = 0.1)) {
    cat(i, "\n")
}
## 0 
## 0.1 
## 0.2 
## 0.3

Ale to neznamená, že by se R problém netýkal…

i <- 0
while (i <= 0.3) {
    cat(i, "\n")
    i <- i + 0.1
}
## 0 
## 0.1 
## 0.2

V jiných jazycích, například v C, je to ale věc, na kterou si musíme dávat velice pozor.

void main() {
    float i;
    for (i=0; i<=1; i+=0.1) {
        printf("%f\n", i);
    }
}

Vstup z klávesnice od uživatele

Nápověda: ?readline

Funguje jen v interaktivním prostředí (skript R, nikoliv v R Markdown, na webové stránce, prezentacích, Shiny aplikacích apod.)

odpoved <- readline("Jste spokojen s tímto kurzem? ")

print(paste0("Řekl jste: ", odpoved))
# defaultní oddělovač (sep) je mezera.

if (tolower(substr(odpoved, 1, 1)) == "n") {
    print("To není možné. Vymýšlíte si!")
} else {
    print("V pořádku.")
}

Tip: Zatímco Ctrl+Shift+Enter spustí celý skript a vypisuje i příkazy (tzv. echo), Ctrl+Shift+S spustí celý skript, ale bez echa.

Typ výstupu je character (řetězec).

vstup <- "123"
vstup + 5    # Error in "123" + 5 : non-numeric argument to binary operator

Převod na číslo: as.numeric(“123”)

vstup <- "123"
cislo <- as.numeric(vstup)
cislo + 5
## [1] 128

Tvorba funkcí

obvodKruhu <- function(polomer) {
    obvod <- 2*pi*polomer
    return(obvod)  # poslední prováděný příkaz musí tzv. vrátit výslednou hodnotu
                   # (což může být teoreticky klidně list nebo data.frame plný mnoha hodnot)
}

obvodKruhu(10)
## [1] 62.83185
obvodObdelniku <- function(a, b) {   # dva parametry nejsou problém
    obvod <- 2*(a+b)
    return(obvod)
}

obvodObdelniku(3, 5)
## [1] 16
obvodObdelniku <- function(a = 2, b = 10) {   # defaultní hodnoty parametrů, uživatel je pak nemusí vyplňovat
    obvod <- 2*(a+b)
    return(obvod)
}

obvodObdelniku(b = 8)
## [1] 20

Funkce může vracet více hodnot prostřednictvím vektoru nebo list.

zpracujObdelnik <- function(a, b) {
    obvod <- 2*(a+b)
    obsah <- a*b
    
    return(c(Obvod = obvod, Obsah = obsah))  # prvky lze dokonce i pojmenovat (to ovšem není nutné)
}

odpoved <- zpracujObdelnik(3, 5)
odpoved["Obvod"] * 3
## Obvod 
##    48

Zaokrouhlování může být problém. Prozkoumáme-li nápovědu funkce round, chování nás může překvapit.

?round

round(0.5)
## [1] 0
round(1.5)
## [1] 2
round(2.5)
## [1] 2
round(3.5)
## [1] 4

Vyplatí se vytvořit si vlastní funkci.

zaokrouhli <- function(cislo) {
    cele <- trunc(cislo + sign(cislo)*0.5)
    return(cele)
}

zaokrouhli(0.5)
## [1] 1
zaokrouhli(1.5)
## [1] 2
zaokrouhli(2.5)
## [1] 3
zaokrouhli(-0.5)
## [1] -1
zaokrouhli(-1.5)
## [1] -2
zaokrouhli(1.4)
## [1] 1
zaokrouhli(-1.4)
## [1] -1

Pozn. trunc odřízne desetinnou část, sign vrátí -1, 0, 1 podle znaménka čísla. Další užitečné funkce: ceiling zaokrouhlí vždy na vyšší číslo, floor na nižší.

ceiling(2.5)
## [1] 3
ceiling(2.1)
## [1] 3
ceiling(-2.9)
## [1] -2

A nešlo by zaokrouhli() implementovat jinak? A co bude rychlejší?

zaokrouhli2 <- function(cislo) {
  if (cislo < 0) {
    posun <- -0.5
  } else {
    posun <- 0.5
  }
  
  cele <- trunc(cislo + posun)
  return(cele)
}


system.time(for (i in 1:10000000) {
  zaokrouhli(2.5)
  }
)
##    user  system elapsed 
##    5.72    0.03    5.80
system.time(for (i in 1:10000000) {
  zaokrouhli2(2.5)
}
)
##    user  system elapsed 
##    6.49    0.00    6.57

Úlohy

  1. Vytvořte funkci, která vypočítá DPH ze zadané částky.
  2. Vytvořte funkci, která zjistí, zda jsou dvě čísla dělitelná beze zbytku.
  3. Vytvořte vlastní funkci, která pomocí for cyklu vypočte faktoriál přirozeného čísla. Pozn. faktoriál 5 = 1 * 2 * 3 * 4 * 5 = 120.
  4. Vytvořte skript, který vypisuje čísla řady 1, 2, 4, 7, 11, 16 atd.
    1. Celkem 20 čísel.
    2. Postupně všechna čísla, která nepřekročí hodnotu 100.

© 25. 11. 2015 Tomáš Bořil,