awesome-everything EN
↑ Обратно к восхождению

Алгоритмы с нуля

Подсчёт частоты

Суть Подсчитай, сколько раз встречается каждое значение, используя хеш-карту: один проход O(n) строит полный подсчёт. Решай анаграммы, находи дубликаты, группируй по частоте.
◷ 18 min

У тебя есть строка “mississippi” и ты хочешь знать: сколько раз встречается каждая буква? Один подход: для каждой буквы отсканируй всю строку и подсчитай. Это O(n²) — n букв, каждая требует сканирования. Но вот ключевая идея: отсканируй строку один раз, и по дороге заполняй подсчёт каждой буквы в хеш-карте. Когда достигнешь конца, твоя хеш-карта содержит полный подсчёт частоты. Один проход, O(n) времени. Это приём подсчёта частоты, и это самый частый способ использовать хеш-карты.

Цель

После этого урока ты можешь построить карту частоты за один проход O(n), использовать карту частоты чтобы решить «это анаграмма?», найти самый частый элемент в массиве, обнаружить неповторяющиеся элементы, понять, почему фиксированный алфавит позволяет таблицу частоты на базе массива, и узнать, что подсчёт частоты заменяет подход «сканируй и подсчитай для каждого элемента» за O(n²).

Идея

Проблема, которую мы решаем:

Тебя просят: подсчитай, сколько раз встречается каждая буква в “hello”. Один способ:

function countNaive(str) {
  // Для каждой буквы отсканируй строку и подсчитай
  for (let i = 0; i < str.length; i++) {
    let count = 0;
    for (let j = 0; j < str.length; j++) {
      if (str[i] === str[j]) count++;
    }
    console.log(str[i] + ": " + count);  // h: 1, e: 1, l: 2, l: 2, o: 1
  }
}

Это O(n²): для каждой из n букв ты сканируешь всю строку. Для больших входов это расточительно.

Идея подсчёта частоты:

Вместо этого отсканируй строку один раз. Для каждой буквы, которую ты встречаешь, обновляй её подсчёт в хеш-карте:

function countFreq(str) {
  const freq = new Map();  // карта частоты: буква → подсчёт
  
  for (let char of str) {
    // Инкрементируй подсчёт для этого символа
    freq.set(char, (freq.get(char) ?? 0) + 1);
  }
  
  // freq теперь содержит: h→1, e→1, l→2, o→1
  return freq;
}

Один цикл, O(n) времени. Одна хеш-карта, O(k) памяти, где k — количество уникальных букв.

Почему это работает:

  • Первый проход: посети каждый элемент один раз. Добавь в карту если новый, инкрементируй если видел раньше.
  • O(1) обновления: freq.set() и freq.get() это O(1) в среднем.
  • Результат: после одного прохода, карта полная.

Хеш-карта против массива для маленьких алфавитов:

Если ключи это маленький фиксированный диапазон (например, строчные буквы a–z), ты можешь использовать обычный массив вместо хеш-карты:

function countFreqArray(str) {
  const freq = new Array(26).fill(0);  // 26 ячеек для a-z
  
  for (let char of str) {
    const index = char.charCodeAt(0) - 'a'.charCodeAt(0);  // 'a'→0, 'b'→1, и т.д.
    freq[index]++;
  }
  
  // freq[0] это подсчёт 'a', freq[1] это подсчёт 'b', и т.д.
  return freq;
}

Доступ к массиву это O(1) и немного быстрее чем операции хеш-карты (нет издержек хеширования). Оба работают; массив оптимизирован когда ключи маленькие и предсказуемые.

Общие применения карт частоты:

  1. Анаграммы: две строки это анаграммы если они имеют одинаковые частоты символов.
  2. Дубликаты и неповторяющиеся: найди элементы, что встречаются ровно один раз или больше чем один.
  3. Самый/наименее частый: отслеживай элемент с самым высоким или низким подсчётом.
  4. Группировка по частоте: организуй элементы по частоте.
Код

1. Построение карты частоты:

function frequency(arr) {
  const freq = new Map();
  for (let item of arr) {
    freq.set(item, (freq.get(item) ?? 0) + 1);
  }
  return freq;
}

const counts = frequency([5, 2, 5, 9, 5, 3, 2]);
// counts: 5→3, 2→2, 9→1, 3→1
console.log(counts.get(5));  // 3

2. Проверка, являются ли две строки анаграммами:

Две строки это анаграммы если они содержат одинаковые символы с одинаковыми частотами.

function isAnagram(s, t) {
  if (s.length !== t.length) return false;
  
  const freq = new Map();
  
  // Подсчитай символы в s
  for (let char of s) {
    freq.set(char, (freq.get(char) ?? 0) + 1);
  }
  
  // Декрементируй для символов в t
  for (let char of t) {
    if (!freq.has(char)) return false;
    freq.set(char, freq.get(char) - 1);
    if (freq.get(char) < 0) return false;
  }
  
  // Все подсчёты должны быть 0
  for (let count of freq.values()) {
    if (count !== 0) return false;
  }
  
  return true;
}

console.log(isAnagram("listen", "silent"));  // true
console.log(isAnagram("hello", "world"));    // false

3. Поиск первого неповторяющегося символа:

function firstNonRepeating(str) {
  const freq = new Map();
  
  // Подсчитай все символы
  for (let char of str) {
    freq.set(char, (freq.get(char) ?? 0) + 1);
  }
  
  // Найди первый символ с подсчётом 1
  for (let char of str) {
    if (freq.get(char) === 1) {
      return char;
    }
  }
  
  return null;  // Нет неповторяющегося символа
}

console.log(firstNonRepeating("abacabad"));  // 'c'
console.log(firstNonRepeating("aabbcc"));    // null

Попробуй примеры:

Вывод
 
Пошаговый разбор

Давайте проследим построение карты частоты на строке “aabab”:

1 function frequency(str) {
2 const freq = new Map();
3 for (let char of str) {
4 freq.set(char, (freq.get(char) ?? 0) + 1);
5 }
6 return freq;
7 }

Заметь: мы посетили каждый символ ровно один раз. Каждый set и get это O(1). Полное время: O(n).

Сложность

Подсчёт частоты:

ОперацияВремяПамять
Построить карту частотыO(n)O(k)
Проверить анаграммуO(n)O(k)
Найти первый неповторяющийсяO(n)O(k)
Найти самый частый элементO(n)O(k)

Где n это размер входа (длина строки или размер массива) и k это количество уникальных значений.

Сравнение: наивный O(n²) против подсчёта частоты O(n):

  • Наивный подход: для каждого элемента отсканируй весь вход чтобы подсчитать его. n × n = O(n²).
  • Подсчёт частоты: один проход чтобы построить карту, потом используй карту. O(n) + O(n) = O(n).

Для n = 1000, наивный это 1,000,000 операций. Подсчёт частоты это 2,000. Разница растёт быстро.

Память для варианта на базе массива:

Если алфавит фиксирован (например, строчные буквы, 26 значений), память это O(26) = O(1) константа. Время подсчёта частоты это всё ещё O(n), но память это O(1) вместо O(k).

Практика 0 / 5

Почему подсчёт частоты подсчитывает вхождения каждого элемента за O(n) время вместо O(n²)?

Две строки это анаграммы если они содержат одинаковые символы с одинаковыми частотами. Какой подход эффективнее?

В строке длины 100 с 8 уникальными символами, сколько памяти использует карта частоты?

При подсчёте частоты символов в строке из строчных букв только (a–z), какая структура данных самая быстрая?

Чтобы найти первый неповторяющийся символ в строке, нужно ли построить карту частоты полностью сначала, или можешь проверять во время построения?

lesson.inset.misconception

«Подсчёт частоты это только для строк.» Неправда. Подсчёт частоты работает на любом массиве сравнимых значений: целые числа, объекты (если hashable), пары, и т.д. Приём одинаков: один проход, подсчитай в хеш-карте, используй результат.

Граничные случаи

Пустой или однобуквенный вход: Пустая строка не имеет символов, поэтому карта частоты пуста. Строка с одним символом имеет одну запись: символ с подсчётом 1. Всегда тестируй граничные случаи.

Проверь себя
Викторина

Почему приём подсчёта частоты это O(n) время, даже если кажется что «я должен подсчитать каждый элемент» звучит как O(n²)?

Итог

Подсчёт частоты это практика подсчёта того, сколько раз встречается каждое значение в наборе данных, используя хеш-карту.

Ключевые факты:

  1. Алгоритм за один проход: итерируй вход один раз. Для каждого элемента инкрементируй его подсчёт в карте используя map.set(key, (map.get(key) ?? 0) + 1).
  2. Сложность времени: O(n), где n это размер входа.
  3. Сложность памяти: O(k), где k это количество уникальных значений.
  4. Общие применения:
    • Проверь, являются ли две строки анаграммами (одинаковые частоты символов).
    • Найди первый неповторяющийся элемент.
    • Обнаруживай элементы, что встречаются ровно один раз или больше чем один.
    • Найди самый или наименее частый элемент.
  5. Оптимизация на базе массива: если ключи это маленький фиксированный диапазон (например, строчные буквы a–z), используй массив вместо хеш-карты для O(1) памяти и немного более быстрых операций.
  6. Заменяет O(n²): наивный подход сканирования для каждого элемента это O(n²). Подсчёт частоты решает ту же задачу за O(n).

У тебя теперь есть самый частый приём хеш-карты. Дальше, ты изучишь разрешение коллизий и узнаешь как хеш-функции распределяют ключи.

Продолжить восхождение ↑Приём «видел»
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.