1. Модель частотної карти
Коли ви тільки починаєте програмувати, може здаватися, що головні задачі — це «підрахувати суму», «знайти максимум» або «відсортувати». Але дуже швидко реальність підкидає задачі іншого типу: «скільки разів трапляється слово», «скільки разів користувач натиснув кнопку», «які товари купують частіше» або «які помилки в логах трапляються найчастіше».
І тут виникає типова проблема: якщо робити це напряму через масиви, ви починаєте писати вкладені цикли, перевіряти кожен елемент по кілька разів і відчувати, що комп’ютер сумує, а ви разом із ним. Частотна карта (frequency map) вирішує саме цей клас задач: швидко й просто накопичувати статистику повторень.
Що таке частотна карта: «елемент → кількість»
Частотна карта — це словник, який зберігає для кожного елемента кількість його появ. Тобто не «хто на якому місці лежить» (як у масиві), а «скільки разів траплявся ось цей ключ». У Swift це найчастіше виглядає так:
- для слів: [String: Int]
- для чисел: [Int: Int]
- для символів: [Character: Int]
Головна ідея до смішного проста: ключем виступає сам елемент, а значенням — лічильник. У цьому й сила: щойно ви починаєте так мислити, значна частина задач зі статистикою та підрахунками стає однотипною.
Щоб закріпити модель, зручно подивитися на неї як на маленьку таблицю:
| Дані | Частотна карта |
|---|---|
|
|
|
|
Ключовий момент: ключі в словнику унікальні, тому «додати ще один apple» означає «збільшити лічильник за ключем apple».
Базовий шаблон: freq[x, default: 0] += 1
Тепер погляньмо на прийом, який робить частотні карти такими зручними. Якби в нас не було default:-сабскрипта, довелося б щоразу перевіряти, чи є ключ, а якщо ні — створювати його. У Swift це можна зробити значно охайніше: через dict[key, default: ...]. Саме цей механізм і придумали для зручного накопичення значень у словнику.
Ось мінімальний приклад частотної карти для масиву рядків:
var freq: [String: Int] = [:]
let words = ["swift", "cli", "swift", "code"]
for w in words {
freq[w, default: 0] += 1
}
print(freq) // наприклад: ["cli": 1, "swift": 2, "code": 1]
Важлива деталь, яка часто дивує новачків: запис freq[w, default: 0] повертає не Optional, а звичайний Int. Саме тому тут можна написати += 1 без розпакування. Ідея проста: «якщо ключа немає — вважати, що там 0».
Якщо вам потрібно порахувати частоти символів у рядку, шаблон той самий:
let text = "how now brown cow"
var letters: [Character: Int] = [:]
for ch in text {
letters[ch, default: 0] += 1
}
print(letters["o", default: 0]) // 4
Ця конструкція добре підкреслює думку: «отримати значення (або 0) і відразу змінити його», не розпаковуючи Optional вручну.
3. Нормалізація ключів
Коли ви рахуєте частоти рядків, дуже легко випадково отримати «різні» ключі, які для людини виглядають однаково. Наприклад, "Swift", "swift" і "swift " для словника — це три різні рядки, а для людини — майже одне й те саме слово.
Тому в реальних задачах майже завжди перед підрахунком виконують нормалізацію: приводять вхідні дані до єдиного вигляду. На цьому етапі достатньо двох простих дій: прибрати пробіли по краях і перевести текст у нижній регістр. Для trimmingCharacters(in:) потрібен Foundation, тож чесно імпортуємо його.
Зробімо маленьку функцію, яка перетворює те, як людина ввела слово, на зручний для підрахунку вигляд:
import Foundation
func normalize(word: Substring) -> String {
let s = String(word)
let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.lowercased()
}
Зверніть увагу: вхідний тип тут — Substring, тому що split(separator:) повертає саме його. Ми перетворюємо значення на String — і вже далі спокійно працюємо з рядком.
Тепер можна спокійно рахувати слова:
import Foundation
let line = " Swift swift CLI "
let parts = line.split(separator: " ")
var freq: [String: Int] = [:]
for p in parts {
let w = normalize(word: p)
if !w.isEmpty {
freq[w, default: 0] += 1
}
}
print(freq) // ["swift": 2, "cli": 1]
Тут ми ще й відсікаємо порожні рядки. Чому це важливо? Тому що split(separator:) при множинних пробілах поводиться зручно, але в реальних введеннях люди здатні дивувати: іноді трапляються табуляції, іноді копіюють текст із «невидимими» пробілами, іноді вводять порожній рядок. Перевірка isEmpty — дешева страховка.
4. Мінізастосунок TextStats
Тепер зберемо все в невеликий консольний застосунок, який ви можете запускати у Web-IDE або локальній IDE: користувач вводить рядок, а застосунок друкує частотну карту.
Ми й далі розвиватимемо один і той самий мінізастосунок — «аналізатор тексту». Сьогодні він ще дуже простий, але вже корисний: покаже, які слова трапляються частіше.
import Foundation
print("Введіть рядок:")
let line = readLine() ?? ""
let parts = line.split(separator: " ")
var freq: [String: Int] = [:]
for p in parts {
let w = String(p).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if !w.isEmpty {
freq[w, default: 0] += 1
}
}
print(freq) // наприклад: ["swift": 2, "is": 1, "fun": 1]
Тут спеціально все зібрано в один блок без додаткових функцій, щоб ви чітко бачили сам патерн частотної карти. На наступних кроках, коли ви почнете активніше рефакторити код, таку логіку буде зручно виносити у функцію. Але поки важливіше закріпити саму ідею.
5. Статистика й агрегати поверх частотної карти
Як витягнути корисну статистику
Частотна карта сама по собі — уже результат. Але зазвичай хочеться чогось більш «людського»: скільки всього унікальних слів, яке слово найчастіше, скільки слів повторилися більше одного разу.
Гарна новина: на початковому рівні все це робиться звичайним циклом for (key, value) in dict. Погана новина: словник не гарантує «красивого порядку», тому виведення може виглядати хаотично. Це нормально: словник не про порядок, а про швидкий доступ.
Порахуємо кількість унікальних слів і загальну кількість слів:
let freq: [String: Int] = ["swift": 2, "cli": 1, "fun": 3]
let uniqueWords = freq.count
var totalWords = 0
for (_, count) in freq {
totalWords += count
}
print(uniqueWords) // 3
print(totalWords) // 6
freq.count — це кількість ключів, тобто кількість унікальних елементів. А сума всіх значень дає загальну кількість елементів у вихідному тексті після нормалізації.
Тепер знайдемо найчастіше слово. Тут важливо бути уважним: словник може бути порожній, наприклад якщо користувач натиснув Enter на порожньому рядку. Тому почнемо з безпечного початкового стану.
let freq: [String: Int] = ["swift": 2, "cli": 1, "fun": 3]
var bestWord = ""
var bestCount = 0
for (word, count) in freq {
if count > bestCount {
bestWord = word
bestCount = count
}
}
print("\(bestWord) = \(bestCount)") // fun = 3
Якщо freq порожній, bestWord так і залишиться порожнім рядком, а bestCount — нулем. Це не ідеальний UX, але на цьому етапі це чесна й безпечна поведінка.
Частотна карта як «універсальний накопичувач»
Важливо побачити ширшу ідею: частотна карта — це окремий випадок загального патерну «словник як накопичувач». У частотній карті накопичення відбувається в Int, але так само можна накопичувати суми, списки чи будь-які інші агрегати, які вам потрібні.
Наприклад, припустімо, що в нас є список покупок у вигляді «категорія товару» та «ціна», і ми хочемо порахувати сумарні витрати за категоріями. Це вже не frequency map, але за механікою — майже те саме.
let categories = ["food", "food", "books", "food"]
let prices = [10, 5, 30, 7]
var sums: [String: Int] = [:]
for i in 0..<categories.count {
sums[categories[i], default: 0] += prices[i]
}
print(sums) // наприклад: ["books": 30, "food": 22]
Той самий патерн: default: 0, а потім +=.
А тепер приклад, де значенням буде масив. Припустімо, хочемо зібрати індекси позицій кожного слова — де воно траплялося. Це корисно, наприклад, щоб потім підсвічувати збіги в тексті. Ми не використовуємо жодних готових API для групування, а робимо все вручну — без скорочень:
let words = ["swift", "cli", "swift", "fun"]
var positions: [String: [Int]] = [:]
for i in 0..<words.count {
positions[words[i], default: []].append(i)
}
print(positions["swift", default: []]) // [0, 2]
Тут default: [] створює «порожній список позицій» для слова, яке трапилося вперше, і далі ми просто виконуємо append.
Якщо після цієї частини вам здається, що словник — це «швейцарський ніж», то ви все правильно відчуваєте. Просто не забувайте: швейцарським ножем можна відкрити пляшку, але ремонтувати двигун ним не варто.
6. Корисні тонкощі: порядок і мутації
Із частотними картами є кілька моментів, які важливо зрозуміти саме зараз, інакше пізніше ви натрапите на дивні помилки й будете підозрювати, що Swift живе своїм життям. Спойлер: Swift не живе своїм життям, це просто ми іноді пишемо так, ніби правила не існують.
По-перше, порядок обходу словника у for (k, v) in dict не варто сприймати як відсортований або стабільний. Якщо вам потрібен порядок, це вже окрема задача, і зазвичай її вирішують не самим словником.
По-друге, словник не можна безпечно змінювати — додавати або видаляти ключі — поки ви обходите його циклом for-in. Це схоже на ситуацію, коли ви переставляєте полиці в шафі, поки сидите всередині й шукаєте шкарпетки. Іноді здається, що так можна, але зазвичай усе закінчується падінням або дивною поведінкою.
По-третє, для частотної карти діє просте правило коректності: лічильник не повинен ставати від’ємним. Якщо ви реалізуєте зменшення частот, доведеться продумати, що робити при нулі: залишати ключ зі значенням 0 чи видаляти ключ. У межах сьогоднішньої лекції ми лише збільшуємо лічильник — це найпростіший і найнадійніший варіант патерну.
Щоб чітко зафіксувати сам алгоритм підрахунку, ось його схема:
flowchart TD
A[Беремо наступний елемент x] --> B{Чи є в словнику ключ x?}
B -- так --> C[Беремо поточне значення]
B -- ні --> D[Вважаємо поточне значення нульовим]
C --> E[Збільшуємо на 1]
D --> E
E --> F[Записуємо значення назад у словник]
F --> G{Елементи закінчилися?}
G -- ні --> A
G -- так --> H[Готова частотна карта]
На практиці dict[x, default: 0] += 1 робить всю цю схему за вас в один рядок. Саме тому цей патерн так люблять: він і короткий, і виразний.
7. Типові помилки під час роботи з частотною картою
Помилка №1: намагатися писати freq[x] += 1.
Новачки часто роблять так за звичкою: «взяв значення, збільшив». Але freq[x] — це Int?, тому що ключа може не бути. Swift не дасть вам додати nil до 1. Правильний шлях — freq[x, default: 0] += 1, тому що він гарантує звичайний Int, навіть якщо ключ трапляється вперше.
Помилка №2: примусове витягування freq[x]! заради «аби працювало».
Іноді хочеться «перемогти компілятор» і написати freq[x]! += 1. Це працює рівно до першого нового ключа, а потім ви отримуєте падіння застосунку. Якщо дані надходять від користувача, файла або мережі, таке падіння буде не «рідкісним винятком», а гарантованою подією в майбутньому.
Помилка №3: не нормалізувати рядки й дивуватися, «чому так багато ключів».
Якщо ви рахуєте слова без lowercased() і без обрізання пробілів, то "Swift", "swift" і "swift " стануть різними ключами. Частотна карта при цьому буде формально правильною, але марною для людини. Нормалізація — це частина задачі, а не «косметика».
Помилка №4: плутати items.count і freq.count.
items.count — це скільки всього елементів у вихідних даних. freq.count — це скільки унікальних ключів вийшло. Коли ви аналізуєте текст, freq.count — це розмір словника, а не довжина рядка й не кількість слів у тексті.
Помилка №5: змінювати словник під час обходу й отримувати дивні результати.
Якщо ви ітеруєтеся за допомогою for (k, v) in freq і всередині видаляєте ключі або додаєте нові, ви руйнуєте передбачуваність обходу. У простих задачах це іноді «випадково працює», але це погана звичка. Якщо потрібно видалити частину ключів, спочатку зберіть список ключів, а потім видаляйте їх окремим проходом — або перебудуйте словник заново.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ