Паттерн KV-VK

В этом уроке ты
  • Научишься применять новый паттерн на примере задачи по поиску топ k частых элементов.
Пример задачи с собеседования

Представь, что ты на собеседовании, и у тебя есть 30 минут, чтобы решить задачу.

Условие

Дан массив целых чисел nums и число k. Необходимо вернуть k наиболее часто встречающихся элементов.   

Ответ можно вернуть в любом порядке.

Пример
Ввод: nums = [5,7,5,6,6,5], k = 2
Вывод: [5,6]
Объяснение: элемент 5 встречается 3 раза, элемент 6 встречается 2 раза, а элемент 7 всего один, поэтому мы возвращаем 5 и 6, так как они топ 2 самых встречающихся элементов.
В чем сложность задачи?

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

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

//Время: O(nlogn), где n — размер входного массива.
//Память: O(n), где n — размер входного массива.
function topKFrequent(nums, k) {
    // Подсчитываем частоту каждого элемента
    const frequencyMap = new Map();
    for (const num of nums) {
        frequencyMap.set(num, (frequencyMap.get(num) || 0) + 1);
    }

    // Преобразуем записи из Map в массив
    const entries = Array.from(frequencyMap.entries());

    // Сортируем массив по убыванию частоты
    entries.sort((a, b) => b[1] - a[1]);

    // Извлекаем первые k элементов
    const result = [];
    for (let i = 0; i < k; i++) {
        result.push(entries[i][0]);
    }

    return result;
}

Однако такое решение может не устроить интервьюера, поскольку его временная сложность составляет O(nlogn) из-за использования сортировки. Он может попросить найти более оптимальный вариант.  

Как решить эффективно?

Сначала мы так же, как и раньше, создаем хеш-таблицу и подсчитываем, сколько раз встречается каждое число. Но дальше вместо сортировки мы используем список списков, где индексом будет частота появления числа, а значением — список всех чисел с этой частотой.

После заполнения этого списка мы просто идем с конца (от самых больших частот к меньшим) и выбираем k чисел. Такой подход позволяет обойтись без лишней сортировки, снижая сложность до O(N).

using System;
using System.Collections.Generic;
using System.Linq;

public class Solution {
    public List<int> TopKFrequent(List<int> nums, int k) {
        // ключ - число, значение - сколько раз встретилось
        Dictionary<int, int> count = new Dictionary<int, int>();
        foreach (int num in nums) {
            if (count.ContainsKey(num)) {
                count[num]++;
            } else {
                count[num] = 1;
            }
        }

        // индекс списка - сколько раз встретилось число
        // значение - список чисел, которые встретились столько раз
        List<List<int>> frequencyList = new List<List<int>>(new List<int>[nums.Count + 1]);
        for (int i = 0; i <= nums.Count; i++) {
            frequencyList[i] = new List<int>();
        }
        foreach (var entry in count) {
            int num = entry.Key;
            int freq = entry.Value;
            frequencyList[freq].Add(num);
        }

        // Собираем top k элементов начиная с конца списка
        List<int> result = new List<int>();
        for (int i = frequencyList.Count - 1; i >= 0 && k > 0; i--) {
            foreach (int num in frequencyList[i]) {
                result.Add(num);
                k--;
                if (k == 0) break;
            }
        }
        
        return result;
    }
}
Оценка по времени и памяти
  • Время: Сначала мы проходимся по nums в цикле, потом создаем пустые списки O(n) и переносим элементы из count в frequencyList O(n). И в конце мы проходимся по frequencyList с конца в начало O(n). Итого, время работы алгоритма — O(n).
  • Память:  Мы используем хеш-таблицу для хранения n уникальных чисел, что требует O(n) памяти. Затем мы создаем список frequencyList, в который переносим все n чисел из nums. Важно отметить, что в каждом вложенном списке число хранится только один раз, а не дублируется. Таким образом, общее количество элементов во всех подсписках не превышает n, потому что каждое число встречается ровно столько раз, сколько оно есть в nums, но при этом присутствует только в одном из подсписков. Мы не создаем новых данных, а лишь перераспределяем n элементов, поэтому frequencyList занимает O(n). Результирующий список имеет размер O(k), где k <= n. Итого: O(n)+O(n)+O(k)=O(n).

Итог:

  • Время: O(n), где n - размер nums.
  • Память: O(n),где n - размер nums.
Что дальше

Скорее переходи к решению задач, чтобы применить полученные знания на практике!