🦀 rust.hu
gyakorlat25 perc•modul 2/3 · lecke 5/9

HashMap: szógyakoriság és bejegyzések

Építs szógyakorisági táblát HashMap segítségével, frissíts bejegyzéseket az entry API-val, és készíts rendezett riportot. A feladatok az üres bemenetet, a kulcsok tulajdonjogát és a stabil kimenetet is ellenőrzik.

Cél és előfeltétel

A szövegből kivett szavak akkor válnak igazán hasznossá, ha összesíteni is tudjuk őket. Ebben a leckében gyakorisági táblát építesz, egy kulcshoz tartozó eredményt frissítesz, majd stabil sorrendű riportot készítesz. Ezekre később egy parancssori szövegstatisztikában is szükségünk lesz.

Előfeltétel az A Rust alapjai útvonal és az előző szövegkezelési gyakorlat: ismered az Option típust, a vektorokat és a String–&str különbséget. A példák Rust 2024 editionben, külső csomag nélkül futnak. Mielőtt kódolsz, írd le, hogy nálad mit jelent egy szó. Most minden whitespace által elválasztott token önálló szó; a kisbetűsítés, az írásjelek eltávolítása és a Unicode-normalizálás nem része a számlálásnak.

Kulcs alapján keresünk

A Vec akkor kényelmes, amikor egymás után tárolsz elemeket. A HashMap<K, V> egy kulcshoz egy értéket rendel, ezért például a Rust token előfordulásait közvetlenül a tokennel kérdezheted le. Egy kulcs egyszer szerepelhet a táblában. Ha ugyanazzal a kulccsal újra meghívod az insert metódust, a hozzá tartozó értéket felülírod.

A get eredménye Option<&V>: a hiányzó bejegyzés természetes állapot. Számlálóknál gyakran hasznos a copied().unwrap_or(0), mert a visszakapott referenciaszámból saját számot, hiány esetén nullát készít. Ettől a hiányzó kulcs még nem kerül bele a táblába. A keresést és a beszúrást külön műveletként értelmezzük.

use std::collections::HashMap;

fn main() {
    let mut darabok: HashMap<&str, usize> = HashMap::new();
    for szo in "Rust Rust gyors".split_whitespace() {
        let darab = darabok.entry(szo).or_insert(0);
        *darab += 1;
    }
    assert_eq!(darabok.get("Rust"), Some(&2));
    assert_eq!(darabok.get("hiányzó").copied().unwrap_or(0), 0);
    assert!(!darabok.contains_key("hiányzó"));

    let mut sorok: Vec<_> = darabok.iter().map(|(szo, db)| (*szo, *db)).collect();
    sorok.sort_unstable_by_key(|(szo, _)| *szo);
    for (szo, db) in sorok {
        println!("{szo}: {db}");
    }
}

A kiírás ebben a példában mindig Rust: 2, majd gyors: 1, külön sorban. A táblán való bejárásnak önmagában nincs garantált sorrendje. A rendezés ezért a riport része, nem a HashMap tulajdonsága. A standard könyvtár dokumentációja a tárolási és bejárási műveletek pontos szerződését is leírja.

Az entry egy módosítható helyet ad

A entry(kulcs) megkeresi a kulcshoz tartozó helyet. Az or_insert(0) a meglévő értéket használja, vagy hiány esetén beszúrja a nullát; mindkét esetben módosítható referenciát ad vissza. A *darab += 1 ezért az értéket növeli, nem magát a referenciát változtatja meg. Az or_insert önmagában még nem számlál.

Az Entry API az and_modify lehetőségét is kínálja: egy meglévő bejegyzést külön szabállyal frissíthetsz, miközben az első értéket az or_insert adja. Érdemes ezt a szabályt üzleti mondatként megfogalmazni. A „legjobb pontszámot őrizzük” például egészen más művelet, mint a „minden pontszámot összeadunk”.

Kölcsönzött vagy saját kulcs?

Az első példában &str kulcsok voltak, amelyek a bemeneti szövegre hivatkoztak. Ez jól működik, amíg a szöveg él, és a táblát nem akarod attól függetlenül megtartani. Ha egy függvény saját eredményt ad vissza, a HashMap<String, usize> egyszerűbb szerződés: a kulcsok a tábla tulajdonába kerülnek. A másolás itt tudatos döntés, amely felszabadítja a hívót az eredeti szöveg őrzése alól.

A HashMap nem találja ki a szövegfeldolgozási szabályaidat. A Rust, a rust és a Rust, most három különböző kulcs. Ha később keresési normalizálást szeretnél, azt külön lépésként, külön tesztekkel vezesd be. A Unicode-kisbetűsítés sem azonos az általános graféma- vagy normalizálási műveletekkel. Először legyen pontos és következetes a jelenlegi feladat.

1. feladat: önálló gyakorisági tábla

Írj gyakorisag függvényt, amely &str bemenetből HashMap<String, usize> eredményt készít. Minden whitespace tokent számoljon, az írásjeleket és a betűméretet őrizze meg. Üres vagy csupa whitespace bemenethez üres táblát várunk. Teszteld úgy is, hogy az eredeti String a függvény meghívása után megszűnik, de a tábla tovább használható.

Megoldás
use std::collections::HashMap;

fn gyakorisag(szoveg: &str) -> HashMap<String, usize> {
    let mut eredmeny = HashMap::new();
    for szo in szoveg.split_whitespace() {
        *eredmeny.entry(szo.to_owned()).or_insert(0) += 1;
    }
    eredmeny
}

fn main() {
    let tabla = {
        let bemenet = String::from("Rust Rust rust,\ná");
        gyakorisag(&bemenet)
    };
    assert_eq!(tabla.len(), 3);
    assert_eq!(tabla.get("Rust"), Some(&2));
    assert_eq!(tabla.get("rust,"), Some(&1));
    assert_eq!(tabla.get("á"), Some(&1));
    assert!(gyakorisag("").is_empty());
    assert!(gyakorisag(" \t\n").is_empty());
    assert_eq!(gyakorisag("á\tá").get("á"), Some(&2));
}

A blokk végén a bemenet felszabadul, az eredmény saját kulcsai viszont megmaradnak. A tartalmat kulcsonként ellenőrizzük, a bejárási sorrendtől függetlenül.

2. feladat: személyenként a legjobb pont

Készíts legjobb függvényt, amely egy módosítható táblába játékosnév és u32 pontszám alapján jegyez be eredményt. Új játékosnál a kapott pont legyen az első érték, ismert játékosnál pedig a korábbi és az új érték közül a nagyobb maradjon. A nulla érvényes pontszám. A művelet ne növelje a tábla hosszát ugyanannál a névnél.

Megoldás
use std::collections::HashMap;

fn legjobb(tabla: &mut HashMap<String, u32>, nev: &str, pont: u32) {
    tabla.entry(nev.to_owned())
        .and_modify(|regi| *regi = (*regi).max(pont))
        .or_insert(pont);
}

fn main() {
    let mut tabla = HashMap::new();
    legjobb(&mut tabla, "Réka", 0);
    legjobb(&mut tabla, "Réka", 12);
    legjobb(&mut tabla, "Réka", 4);
    assert_eq!(tabla.get("Réka"), Some(&12));
    assert_eq!(tabla.len(), 1);
    legjobb(&mut tabla, "Áron", 0);
    assert_eq!(tabla.get("Áron"), Some(&0));
    legjobb(&mut tabla, "Réka", u32::MAX);
    assert_eq!(tabla.get("Réka"), Some(&u32::MAX));
}

A maximumképzés nem adja össze a pontszámokat, ezért a legnagyobb lehetséges u32 értékkel is ugyanazt a szabályt alkalmazhatjuk. Ez a teszt a frissítési szándékot is dokumentálja.

3. feladat: stabil riport

Írj riport függvényt, amely szó szerinti kulcssorrendben készít kulcs: darab sorokat, köztük újsorral, a végén extra újsor nélkül. A bemeneti táblát ne fogyaszd el. Az üres eredmény üres String legyen. A Rust alapértelmezett szövegrendezését használjuk; magyar nyelvi ábécérendet nem ígérünk. Két különböző beszúrási sorrendhez is azonos szöveget várunk.

Megoldás
use std::collections::HashMap;

fn riport(tabla: &HashMap<String, usize>) -> String {
    let mut sorok: Vec<_> = tabla.iter().collect();
    sorok.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
    let szovegek: Vec<_> = sorok.into_iter()
        .map(|(szo, db)| format!("{szo}: {db}"))
        .collect();
    szovegek.join("\n")
}

fn main() {
    let egyik = HashMap::from([("rust".to_owned(), 2), ("cargo".to_owned(), 1)]);
    let masik = HashMap::from([("cargo".to_owned(), 1), ("rust".to_owned(), 2)]);
    assert_eq!(riport(&egyik), "cargo: 1\nrust: 2");
    assert_eq!(riport(&egyik), riport(&masik));
    assert_eq!(egyik.len(), 2);
    assert_eq!(riport(&HashMap::new()), "");
}

Elfogadási feltételek

  1. A gyakorisági tábla helyesen kezeli az ismétlődő, ékezetes és whitespace bemeneteket; az eltérő betűméretű vagy írásjeles tokeneket külön tartja.
  2. A legjobb pontszám új kulcsnál beszúr, meglévőnél csak nagyobb értékre változik; a nulla és a u32::MAX tesztje is sikeres.
  3. A riport beszúrási sorrendtől függetlenül ugyanazt a rendezett szöveget adja, és nem fogyasztja el a bemeneti táblát.

Összefoglaló és továbblépés

A kulcs, az érték és a frissítési szabály együtt határozza meg a táblád jelentését. A saját String kulcs az eredményt függetleníti a bemenettől, a rendezett riport pedig ellenőrizhető kimenetet ad. A következő leckében az itt már látott iterátorláncokat bontjuk érthető lépésekre.

gyors önellenőrzés

Próbáld ki, mire emlékszel.

Válaszold meg a kérdéseket, majd ellenőrizd a válaszaidat és olvasd el a magyarázatokat. A válaszok az oldal elhagyásakor törlődnek.

1. Azonos kulccsal kétszer szúrunk be. Mit ír ki a program?
use std::collections::HashMap;

fn main() {
    let mut tabla = HashMap::new();
    tabla.insert("rust", 2);
    tabla.insert("rust", 5);
    println!("{} {}", tabla.len(), tabla.get("rust").unwrap());
}
2. A gyakorisag függvény eredményét az eredeti String megszűnése után is használni szeretnéd. Milyen kulcsot válassz az egyszerű, önálló eredményhez?
3. A teszted a HashMap nyers bejárásából épített szöveg pontos sorrendjét várja. Mi a megfelelő javítás, ha stabil, kulcs szerinti riportot szeretnél?
4. Meglévő kulcsnál mit tesz önmagában az or_insert?
use std::collections::HashMap;

fn main() {
    let mut tabla = HashMap::from([("cargo", 3)]);
    tabla.entry("cargo").or_insert(10);
    println!("{}", tabla.get("cargo").unwrap());
}

0 / 4 kérdés megválaszolva