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:) возвращает именно Substring. Мы превращаем его в 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("Enter a line:")
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[Считаем текущее значение = 0]
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() и без «trim», то "Swift", "swift" и "swift " станут разными ключами. Частотная карта при этом будет формально правильной, но бесполезной для человека. Нормализация — это часть задачи, а не “косметика”.
Ошибка №4: путать items.count и freq.count.
items.count — это сколько всего элементов в исходных данных. freq.count — это сколько уникальных ключей получилось. Когда вы анализируете текст, freq.count — это размер словаря, а не длина строки и не количество слов в тексте.
Ошибка №5: менять словарь во время обхода и получать странности.
Если вы итерируетесь по for (k, v) in freq и внутри удаляете ключи или добавляете новые, вы разрушаете предсказуемость обхода. В простых задачах это иногда «случайно работает», но это плохая привычка. Если нужно удалить часть ключей, сначала соберите список ключей, а потом удаляйте отдельным проходом (или перестройте словарь заново).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ