1. Пригадуємо лічильники й частотні карти
Коли ви вперше чуєте «частотна карта», уяву може намалювати щось на кшталт піратської мапи скарбів — тільки замість хрестика там буде відсоток уживання слова "banana". Насправді все значно простіше: у нас є набір значень — слова, числа, символи, оцінки, — і ми хочемо дізнатися, скільки разів кожне з них трапилося. Це настільки поширена задача, що Swift навіть дав для неї зручний інструмент — той самий dict[key, default: 0].
Уявіть, що у нас є текст, і ми хочемо зрозуміти, які слова в ньому найчастіші. Наївний спосіб — для кожного слова знову й знову проходити весь текст і рахувати повтори. Це працює… але повільно й сумно, бо з’являється вкладений цикл. Набагато краще зробити один прохід і накопичувати лічильники у словнику.
Частотна карта (frequency map) — це просто Dictionary, де:
- ключ (Key) — це «що саме рахуємо» (слово, символ, число),
- значення (Value) — це лічильник Int (скільки разів трапився ключ).
Тобто тип має такий вигляд:
var counts: [String: Int] = [:]
І тепер counts["swift"] — це «скільки разів трапилося слово "swift"».
2. Патерн dict[key, default: 0] += 1
Зовні рядок counts[word, default: 0] += 1 виглядає як магія, але це саме той випадок, коли магія — просто вдало захована рутина.
Словник у Swift зазвичай повертає Optional, коли ви звертаєтеся до counts[word], бо ключа може не бути. Але для підрахунку частот нам хочеться мислити так: якщо ключа немає, то значення нульове. Саме для цього у Swift існує сабскрипт із дефолтом: dict[key, default: someValue].
Мініідея така:
- якщо ключ уже є, ми беремо його поточне значення й збільшуємо його;
- якщо ключа немає, ми тимчасово вважаємо його значення рівним default і збільшуємо його, після чого ключ зʼявиться у словнику вже з новим значенням.
Приклад на іграшкових даних:
import Foundation
var counts: [String: Int] = [:]
counts["apple", default: 0] += 1
counts["apple", default: 0] += 1
counts["banana", default: 0] += 1
print(counts) // ["banana": 1, "apple": 2]
Окрема важлива деталь, яка рятує від несподіваного «чому в мене зʼявилися зайві ключі»: просте читання через default: не додає ключ до словника. У документації до цього API підкреслюється, що let x = dict[key, default: 0] еквівалентне dict[key] ?? 0, і під час читання ключ не «створюється сам собою».
Тобто ось так ключ не зʼявиться:
import Foundation
var counts: [String: Int] = [:]
let x = counts["ghost", default: 0]
print(x) // 0
print(counts) // [:]
Але ось так він зʼявиться, бо ми змінили значення, а отже, словник має зберегти результат:
import Foundation
var counts: [String: Int] = [:]
counts["ghost", default: 0] += 1
print(counts) // ["ghost": 1]
3. Приклад: рахуємо символи в рядку
Почнімо з дуже простої задачі, яка ідеально підходить для частотної карти: порахувати, скільки разів у рядку трапляється кожен символ. Це хороший старт, бо Character чудово підходить як ключ словника.
У «старому стилі» ви б писали if counts[ch] == nil { ... } else { ... }, і досить швидко втомилися б від цього повторюваного шаблону. Swift буквально пропонує вам коротший і безпечніший варіант.
Ось акуратний код:
import Foundation
let text = "banana"
var freq: [Character: Int] = [:]
for ch in text {
freq[ch, default: 0] += 1
}
print(freq["a", default: 0]) // 3
print(freq["b", default: 0]) // 1
Зверніть увагу: ми взагалі не торкаємося Optional руками. І це добре. Чим менше Optional-акробатики ви пишете самі, тим менший шанс, що колись побачите «Fatal error: Unexpectedly found nil…».
4. Нормалізація ключів у рядках
Перш ніж переходити до великого прикладу, варто зробити одну важливу зупинку. Частотна карта чесно рахує ключі. Якщо ви поклали в ключ рядок "Swift", а потім рядок "swift", для словника це два різні ключі. Він не зобовʼязаний розуміти, що за змістом це одне й те саме.
Тому в задачах із текстом майже завжди потрібен крок нормалізації ключа. Це заздалегідь визначене правило: як саме ми приводимо вхідні дані до канонічного вигляду. Найпопулярніше правило — lowercased() і, іноді, trimmingCharacters(in:).
Зробімо маленький приклад: рахуємо слова, але приводимо їх до нижнього регістру.
import Foundation
let words = ["Swift", "is", "swift"]
var counts: [String: Int] = [:]
for w in words {
let key = w.lowercased()
counts[key, default: 0] += 1
}
print(counts) // ["swift": 2, "is": 1]
Тут ви як розробник фіксуєте «контракт»: у нашому лічильнику слова рахуються без урахування регістру. Це не «правильно» чи «неправильно» — це правило системи, і його корисно явно тримати в голові.
5. Мініпроєкт: консольний TextStats
Зараз ми зберемо невеликий консольний інструмент TextStats. Він читатиме рядок зі стандартного вводу, розбиватиме його на слова, нормалізуватиме їх, рахуватиме частоти та друкуватиме результат. Це проста річ, але вона тренує одразу кілька корисних звичок: акуратний ввід, чисту функцію токенізації, чесну роботу зі словником і зрозумілий вивід.
Ми розвиватимемо цей мініпроєкт поступово: спершу навчимося виділяти слова, потім порахуємо частоти, а тоді красиво виведемо топ найчастіших.
Читаємо рядок і отримуємо масив слів
Найпоширеніша помилка початківця на цьому етапі — намагатися «порахувати слова в рядку», не виділивши слова як окремі елементи. Поки у вас усе ще один рядок, будь-яка логіка підрахунку буде або кострубатою, або занадто складною. Тому спершу перетворюємо рядок на масив токенів.
Оскільки ми вже знаємо split(separator:), зробімо максимально просту токенізацію: розділимо за пробілами, приберемо порожні фрагменти, переведемо в нижній регістр.
import Foundation
func tokenize(_ line: String) -> [String] {
let parts = line.split(separator: " ")
return parts.map { part in
part.lowercased()
}
}
Цей варіант навмисно не ідеальний: він не прибирає коми та крапки. Але для старту це нормально: наша мета — опанувати патерн лічильника, а не побудувати ідеальний аналізатор тексту (ідеальні аналізатори зазвичай пишуться сльозами та регулярними виразами… але це вже інша історія).
Швидко перевіримо:
import Foundation
let tokens = tokenize("Swift is swift")
print(tokens) // ["swift", "is", "swift"]
Будуємо частотну карту слів
Тепер найцікавіше: з масиву слів робимо [String: Int].
Зверніть увагу на стиль: окрема функція виконує окрему роботу. Так код легше тестувати в голові, а не лише запуском.
import Foundation
func buildWordFrequency(_ words: [String]) -> [String: Int] {
var freq: [String: Int] = [:]
for w in words {
freq[w, default: 0] += 1
}
return freq
}
І використовуємо:
import Foundation
let words = tokenize("Swift is swift")
let freq = buildWordFrequency(words)
print(freq["swift", default: 0]) // 2
print(freq["is", default: 0]) // 1
Важлива думка: ми не зобовʼязані зберігати у словнику ключі, які трапилися 0 разів. Якщо ключ не траплявся — його просто немає. «Нуль» ми отримуємо через default: під час читання. Це якраз той випадок, коли default: читається майже як звичайна людська фраза: «дай значення, а якщо його немає — вважаймо, що там нуль».
Читаємо ввід користувача й друкуємо частоти
Тепер зберемо найпростіший CLI-скрипт: читаємо рядок, будуємо частотну карту, друкуємо її.
import Foundation
print("Введіть рядок:")
let line = readLine() ?? ""
let words = tokenize(line)
let freq = buildWordFrequency(words)
print(freq) // наприклад: ["swift": 2, "is": 1]
Так, вивід словника не надто «красивий», але поки що це терпимо. Головне — дані пораховані правильно.
Чому «немає ключа» — не те саме, що частота 0
На цьому місці багато хто плутається: «Якщо у слова частота 0, чому б не зберігати 0?». А тому що це зазвичай не потрібно і незручно.
Семантично «ключа немає» означає: «ми ніколи не бачили таке значення». Це трохи сильніша інформація, ніж «частота 0». У частотній карті ми зазвичай зберігаємо тільки те, що траплялося, а «0» використовуємо як зручний дефолт для читання.
У Swift це буквально закріплено зручною рівністю: читання dict[key, default: 0] еквівалентне читанню dict[key] ?? 0.
Подивімося на різницю в поведінці на прикладі:
import Foundation
var freq: [String: Int] = ["swift": 2]
print(freq["java"] as Any) // nil
print(freq["java", default: 0]) // 0
print(freq.keys.contains("java")) // false
Сенс такий:
- freq["java"] дає nil — «ключа немає»,
- freq["java", default: 0] дає 0 — «якщо ключа немає, вважаємо його нульовим»,
- але при цьому ключ як і раніше не зʼявився — ми просто читали.
Це допомагає писати код без зайвих if, водночас зберігаючи коректний зміст даних.
6. Топ‑N найчастіших слів
Коли словник уже побудовано, виникає наступна людська потреба: «Добре, а які слова найчастіші?». Для цього нам треба перетворити словник на масив пар і відсортувати його за значенням.
Ми не будемо робити «ідеальну статистику», але зробимо простий і читабельний вивід. Так, сортування додає вартість, але на малих даних це нормально, а головне — код стає зрозумілим.
Зробімо функцію, яка друкує топ‑N:
import Foundation
func printTopWords(freq: [String: Int], top n: Int) {
let sortedPairs = freq.sorted { a, b in
a.value > b.value
}
for (i, pair) in sortedPairs.prefix(n).enumerated() {
print("\(i + 1). \(pair.key) — \(pair.value)")
}
}
І використаємо її в нашому TextStats:
import Foundation
let line = "Swift is swift and Swift is fun"
let words = tokenize(line)
let freq = buildWordFrequency(words)
printTopWords(freq: freq, top: 3)
// 1. swift — 3
// 2. is — 2
// 3. and — 1 (або fun — 1, порядок серед рівних може відрізнятися)
Тут важливо памʼятати: якщо в двох слів однакова частота, порядок між ними може бути будь-яким. Ми зараз не додаємо «другий ключ сортування» навмисно, щоб не перевантажувати ідею.
7. Частоти пар слів через ключ-кортеж
Іноді хочеться рахувати не окремі слова, а пари сусідніх слів: наприклад, щоб побачити, які поєднання в тексті трапляються часто ("machine learning", "swift is", "is fun"). Це все той самий лічильник, тільки ключ тепер не String, а кортеж із двох рядків.
Перевага в тому, що Swift уміє хешувати кортежі (якщо хешуються їхні елементи), і ми можемо використовувати (String, String) як ключ. Це добре поєднується з тим, що кортежі ми вже знаємо.
Спочатку зберемо пари:
import Foundation
func makeBigrams(_ words: [String]) -> [(String, String)] {
guard words.count >= 2 else { return [] }
var result: [(String, String)] = []
for i in 0..<(words.count - 1) {
result.append((words[i], words[i + 1]))
}
return result
}
Тепер рахуємо частоти біграмів:
import Foundation
func buildBigramFrequency(_ bigrams: [(String, String)]) -> [(String, String): Int] {
var freq: [(String, String): Int] = [:]
for b in bigrams {
freq[b, default: 0] += 1
}
return freq
}
І спробуємо:
import Foundation
let words = tokenize("swift is swift and swift is fun")
let bigrams = makeBigrams(words)
let freq = buildBigramFrequency(bigrams)
print(freq[("swift", "is"), default: 0]) // 2
print(freq[("is", "swift"), default: 0]) // 1
Якщо захочемо знайти «найчастішу пару», це можна зробити через прохід по словнику:
import Foundation
func mostFrequentBigram(_ freq: [(String, String): Int]) -> ((String, String), Int)? {
var best: ((String, String), Int)? = nil
for (key, value) in freq {
if best == nil || value > best!.1 {
best = (key, value)
}
}
return best
}
Використання:
import Foundation
if let best = mostFrequentBigram(freq) {
print("\(best.0.0) \(best.0.1) — \(best.1)")
// swift is — 2
} else {
print("Недостатньо слів для пар.")
}
Тут ми вже бачимо важливий дизайнерський контракт: результат може бути відсутнім, якщо слів менше двох, тому повертаємо Optional. Це набагато чесніше, ніж повертати «порожню пару» на кшталт ("", "").
8. Схема: один прохід даних
Щоб закріпити картинку в голові, корисно уявити алгоритм як потік:
flowchart TD
A[Масив слів] --> B[Беремо черговий елемент x]
B --> C{Чи є ключ x у словнику?}
C -->|так| D[Збільшуємо поточне значення на 1]
C -->|ні| E[Вважаємо default = 0 і отримуємо 0]
E --> F[Записуємо 1]
D --> G[Переходимо до наступного елемента]
F --> G
G -->|доки елементи є| B
G -->|кінець| H[Готова частотна карта]
У реальному коді все це «запаковано» в один рядок freq[x, default: 0] += 1, але під капотом сенс саме такий.
9. Типові помилки під час роботи з частотними картами
Помилка №1: counts[key]! += 1 як стиль «ну воно ж працює».
Такий код справді може працювати… рівно до першого нового ключа, який ще не траплявся. У цей момент counts[key] буде nil, і програма впаде. Патерн dict[key, default: 0] += 1 якраз і зʼявився, щоб не писати зайвих перевірок і не змушувати себе працювати з !.
Помилка №2: плутанина між читанням і записом під час default:.
Іноді очікують, що let x = dict[key, default: 0] автоматично додасть ключ до словника. Але читання з дефолтом не «створює» ключ. Еквівалентність dict[key, default: 0] і dict[key] ?? 0 під час читання — важлива підказка, як це сприймати: дефолт потрібен для зручного значення, а не для мутації.
Помилка №3: відсутність нормалізації ключа, особливо для рядків.
Якщо ви рахуєте слова, але не приводите їх до єдиного вигляду, ви отримаєте окремі лічильники для "Swift", "swift", "SWIFT" і "swift,". Формально словник правий, але результат часто дивує. Тому важливо заздалегідь домовитися про правило: хоча б lowercased(), а далі — залежно від ситуації.
Помилка №4: очікування «красивого порядку» в словнику під час виводу.
Dictionary створено для швидкого доступу за ключем, а не для зберігання елементів у «правильному» порядку. Якщо ви друкуєте словник напряму, порядок ключів не варто сприймати як частину контракту. Для красивого виводу майже завжди треба окремо зібрати пари та відсортувати їх.
Помилка №5: змішування логіки підрахунку і логіки виводу в один комок коду.
Коли підрахунок частот і друк результатів живуть в одному циклі, код швидко перетворюється на «локшину»: складно додати сортування, складно змінити формат, складно перевикористати підрахунок. Набагато спокійніше — і по-дорослому — розділити ці кроки: спочатку будуємо freq, потім окремою функцією виводимо те, що потрібно.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ