JavaRush /Курсы /Swift SELF /Частотная карта (frequency map)

Частотная карта (frequency map)

Swift SELF
13 уровень , 4 лекция
Открыта

1. Модель частотной карты

Когда вы только начинаете программировать, кажется, что основные задачи — это «посчитать сумму», «найти максимум», «отсортировать». Но очень быстро реальность приносит задачи другого сорта: «сколько раз встречается слово», «сколько раз пользователь нажал кнопку», «какие товары покупают чаще», «какие ошибки в логах самые популярные».

И тут возникает типичная боль: если делать это «в лоб» через массивы, вы начинаете писать вложенные циклы, проверять каждый элемент по много раз и чувствуете, что компьютер грустит, а вы вместе с ним. Частотная карта (frequency map) решает именно этот класс задач: быстро и просто накапливать статистику по повторениям.

Что такое частотная карта: «элемент → количество»

Частотная карта — это словарь, который хранит для каждого элемента число его появлений. То есть не «кто на каком месте лежит» (как в массиве), а «сколько раз встречался вот этот ключ». В Swift это чаще всего выглядит так:

  • для слов: [String: Int]
  • для чисел: [Int: Int]
  • для символов: [Character: Int]

Главная идея звучит почти смешно просто: ключом выступает сам элемент, а значением — счётчик. В этом и сила: как только вы так начали думать, полкурса задач по статистике и «подсчётам» внезапно становятся однотипными.

Чтобы зафиксировать модель, удобно смотреть на это как на маленькую таблицу:

Данные Частотная карта
["apple", "banana", "apple"]
["apple": 2, "banana": 1]
[10, 10, 7]
[10: 2, 7: 1]

Ключевой момент: ключи в словаре уникальны, поэтому «добавить ещё один 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 и внутри удаляете ключи или добавляете новые, вы разрушаете предсказуемость обхода. В простых задачах это иногда «случайно работает», но это плохая привычка. Если нужно удалить часть ключей, сначала соберите список ключей, а потом удаляйте отдельным проходом (или перестройте словарь заново).

1
Задача
Swift SELF, 13 уровень, 4 лекция
Недоступна
Счётчик повторов
Счётчик повторов
1
Задача
Swift SELF, 13 уровень, 4 лекция
Недоступна
Частоты по порядку
Частоты по порядку
1
Задача
Swift SELF, 13 уровень, 4 лекция
Недоступна
Хит слов дня
Хит слов дня
1
Задача
Swift SELF, 13 уровень, 4 лекция
Недоступна
Позиции в тексте
Позиции в тексте
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ