Хеш-таблицы в задачах

⏱ 45 мин · 🧩 заданий: 5

Хеш-таблица (dict, set) даёт поиск, вставку и удаление в среднем за O(1). Это самый частый способ превратить квадратичное решение в линейное: вместо того чтобы искать по списку, мы помним увиденное.

Шаблон 1: «уже видели?»

def contains_duplicate(nums):
    seen = set()
    for x in nums:
        if x in seen:
            return True
        seen.add(x)
    return False

print(contains_duplicate([1, 2, 3, 1]), contains_duplicate([1, 2, 3]))
print(len(set([1, 2, 3, 1])) != len([1, 2, 3, 1]))    # то же одной строкой

Шаблон 2: значение → индекс (Two Sum)

Классика собеседований. Для каждого x нужно найти target - x среди предыдущих элементов:

def two_sum(nums, target):
    index = {}                                    # значение → индекс
    for i, x in enumerate(nums):
        need = target - x
        print(f"  i={i}, x={x}, ищем {need}, словарь={index}")
        if need in index:
            return [index[need], i]
        index[x] = i

print(two_sum([2, 7, 11, 15], 9))
print(two_sum([3, 2, 4], 6))

Сравните: перебор пар — O(n²) времени и O(1) памяти; словарь — O(n) времени и O(n) памяти. Это типичный компромисс время/память.

Шаблон 3: подсчёт частот

from collections import Counter

def is_anagram(a, b):
    return Counter(a) == Counter(b)              # O(n); sorted(a) == sorted(b) — O(n log n)

print(is_anagram("listen", "silent"), is_anagram("rat", "car"))

def can_build(note, magazine):
    """Можно ли составить записку из букв журнала (каждая буква — один раз)."""
    need, have = Counter(note), Counter(magazine)
    return all(have[ch] >= cnt for ch, cnt in need.items())   # или: not (need - have)

print(can_build("aab", "baa"), can_build("aa", "ab"))

def top_k_frequent(nums, k):
    return [x for x, _ in Counter(nums).most_common(k)]

print(top_k_frequent([1, 1, 1, 2, 2, 3], 2))

Шаблон 4: группировка по ключу

Главное — придумать ключ, одинаковый для элементов одной группы:

from collections import defaultdict

def group_anagrams(words):
    groups = defaultdict(list)
    for w in words:
        key = "".join(sorted(w))                 # у анаграмм одинаковый отсортированный вид
        groups[key].append(w)
    return list(groups.values())

print(group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"]))

def group_by_shift(words):
    """Строки, получаемые друг из друга сдвигом букв: abc, bcd, xyz."""
    groups = defaultdict(list)
    for w in words:
        key = tuple((ord(c) - ord(w[0])) % 26 for c in w)    # разности между буквами
        groups[key].append(w)
    return list(groups.values())

print(group_by_shift(["abc", "bcd", "acef", "xyz", "az", "ba", "a", "z"]))
Задача Ключ
анаграммы "".join(sorted(w)) или tuple(Counter(w).items()) отсортированный / массив из 26 счётчиков
точки на одной прямой наклон как несократимая дробь (dy // g, dx // g)
изоморфные строки шаблон: индексы первых вхождений символов
дубликаты файлов хеш содержимого

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

Шаблон 5: префиксы + словарь

Подмассивы с заданной суммой (уже встречали в уроке про окно): храним, сколько раз встречалась каждая префиксная сумма.

def longest_balanced(bits):
    """Самый длинный подмассив с равным числом 0 и 1."""
    first_index = {0: -1}                       # баланс → первая позиция
    balance = best = 0
    for i, b in enumerate(bits):
        balance += 1 if b == 1 else -1
        if balance in first_index:
            best = max(best, i - first_index[balance])
        else:
            first_index[balance] = i
    return best

print(longest_balanced([0, 1, 0, 0, 1, 1, 0]))

Самая длинная последовательность подряд

Красивый приём: начинаем счёт только с начала цепочки — числа x, для которого x - 1 нет в множестве. Тогда каждый элемент просматривается не больше двух раз → O(n).

def longest_consecutive(nums):
    values = set(nums)
    best = 0
    for x in values:
        if x - 1 not in values:                 # начало цепочки
            length = 1
            while x + length in values:
                length += 1
            best = max(best, length)
    return best

print(longest_consecutive([100, 4, 200, 1, 3, 2]))

LRU-кеш

Частая задача на проектирование: кеш на capacity элементов, при переполнении вытесняется давно не использованный. get и put — за O(1). Решение — хеш-таблица + двусвязный список, в Python — OrderedDict:

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.data = OrderedDict()

    def get(self, key):
        if key not in self.data:
            return -1
        self.data.move_to_end(key)              # недавно использован
        return self.data[key]

    def put(self, key, value):
        self.data[key] = value
        self.data.move_to_end(key)
        if len(self.data) > self.capacity:
            self.data.popitem(last=False)       # вытеснить самый старый

cache = LRUCache(2)
cache.put(1, "a"); cache.put(2, "b")
print(cache.get(1))          # a — теперь 1 свежий
cache.put(3, "c")            # вытеснит 2
print(cache.get(2), cache.get(3), list(cache.data))

На собеседовании могут попросить реализовать двусвязный список вручную — урок «Связные списки».

Когда хеш-таблица — сигнал

Если в решении есть «для каждого элемента найди другой элемент / проверь, встречался ли» — почти наверняка нужен set или dict. Если нужен порядок или «ближайший больший/меньший» — хеш-таблица не поможет: смотрите на сортировку, кучу, стек или бинарный поиск.

Итог

  • Хеш-таблица превращает поиск в O(1) и многие O(n²) решения — в O(n) ценой O(n) памяти.
  • Шаблоны: «уже видели» (set), значение → индекс (dict), частоты (Counter), группировка по ключу (defaultdict), префиксы + словарь.
  • Главное в группировке — придумать хешируемый ключ.
  • LRU-кеш — OrderedDict или хеш-таблица + двусвязный список.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Two Sum. Дан список nums (не отсортирован) и target. Верните индексы [i, j], i < j, двух чисел с суммой target (гарантируется, что ответ ровно один). Решение за O(n) со словарём.

Какие тесты проверяют код (2)
assert two_sum([2, 7, 11, 15], 9) == [0, 1] and two_sum([3, 2, 4], 6) == [1, 2] and two_sum([3, 3], 6) == [0, 1]
import time
nums = list(range(1, 3_001)) + [100_000]
start = time.perf_counter()
assert two_sum(nums, 103_000) == [2_999, 3_000]
assert time.perf_counter() - start < 0.05, "Нужно решение за O(n)"

Задание 2 Программа на Python

Группировка анаграмм. Сгруппируйте слова, состоящие из одних и тех же букв. Верните список групп; внутри группы слова в исходном порядке, группы — в порядке первого появления. ["eat", "tea", "tan", "ate", "nat", "bat"] → [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]].

Какие тесты проверяют код (2)
assert group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"]) == [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]
assert group_anagrams([]) == [] and group_anagrams(["a"]) == [["a"]] and group_anagrams(["", ""]) == [["", ""]]
Задание 3 Программа на Python

Самая длинная последовательность подряд идущих чисел. Верните длину самой длинной цепочки последовательных целых чисел, элементы которой есть в списке (порядок в списке не важен). [100, 4, 200, 1, 3, 2] → 4 (1, 2, 3, 4). Требуется O(n), без сортировки.

Какие тесты проверяют код (3)
assert longest_consecutive([100, 4, 200, 1, 3, 2]) == 4 and longest_consecutive([]) == 0
assert longest_consecutive([0, 3, 7, 2, 5, 8, 4, 6, 0, 1]) == 9 and longest_consecutive([1, 2, 0, 1]) == 3
import time
nums = list(range(200_000, 0, -1))
start = time.perf_counter()
assert longest_consecutive(nums) == 200_000
assert time.perf_counter() - start < 1, "Слишком медленно — начинайте счёт только с начала цепочки"

Задание 4 Программа на Python

Первый неповторяющийся символ. Верните индекс первого символа строки, который встречается в ней ровно один раз, или -1. Два прохода и Counter.

Какие тесты проверяют код (2)
assert first_unique("leetcode") == 0 and first_unique("loveleetcode") == 2 and first_unique("aabb") == -1
import time
s = "ab" * 15_000 + "c"
start = time.perf_counter()
assert first_unique(s) == 30_000
assert time.perf_counter() - start < 0.05, "s.count в цикле — O(n²); посчитайте частоты один раз"

Задание 5 Один вариант ответа

Почему в Two Sum со словарём проверку target - x in index делают до добавления x?

Войдите, чтобы отмечать пройденные уроки.