Что такое хеш-таблица?

В этом уроке ты
  • Узнаешь, зачем нужна хеш-таблица.
  • Научишься использовать её в своём языке программирования.
  • Поймёшь, как часто хеш-таблица встречается на собеседованиях.
  • Разберёшься, какие операции доступны в хеш-таблице и насколько они быстрые.
Как часто хеш-таблица встречается на собеседованиях

Каждый третий разработчик на собеседовании сталкивается с задачей, где нужна хеш-таблица.

Такой интерес к ней вызван широкой областью применения и эффективностью при решении задач поиска, вставки и удаления.

Кроме практических задач, нередко можно услышать вопрос: «Расскажи, как хеш-таблица работает внутри». Чуть позже мы обязательно вернёмся к этому важному вопросу.

Хеш-таблица в твоём языке программирования

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

Чтобы не растеряться на собеседовании, давай разберём основные операции в твоём языке.

const hashTable = {};

// Вставка/обновление
hashTable[1] = 3;

// Поиск
const val = hashTable[1];
console.log(val);

// Удаление ключа "1"
delete hashTable[1];

// Узнать размер
console.log(Object.keys(hashTable).length);
Главное, что нужно знать о хеш-таблице

Поиск в хеш-таблице работает быстро, как и вставка, удаление и получение значений по ключу.

Однако в редких случаях сложность может стать O(n) из-за особенностей реализации. Почему это происходит, разберём позже. Важно запомнить, что в среднем все операции выполняются за O(1):

Операция Время (в среднем) Время (худший случай)
Вставка O(1) O(n)
Поиск значения по ключу O(1) O(n)
Удаление по ключу O(1) O(n)
Поиск ключа в хеш-таблице O(1) O(n)
Сделать хеш-таблицу из массива O(n) O(n)
Сравнение хеш-таблиц O(n) O(n)

Главный вывод: все операции хеш-таблицы в среднем работают за O(1), и именно так их следует оценивать при анализе сложности кода.

Представь, что каждая 100 000-я операция занимает O(n), а остальные 99 999 выполняются за O(1). Это примерная модель, но смысл передаёт хорошо.

Пример задачи с собеседования

Представь, что ты проходишь собеседование. На выполнение первой задачи у тебя есть примерно 20 минут. Вперёд!

Условие

 Дан массив целых чисел nums и число target. Нужно вернуть индексы двух чисел из массива nums, которые в сумме дают target. Гарантируется, что есть только один ответ, и индексы должны быть в порядке возрастания.  

Пример
Ввод: nums = [1,4,3,-6,2,5], target = -1
Вывод: [3,5]
Объяснение: -6 + 5 = -1. "-6" имеет индекс 3, а "5" индекс 5
В чем сложность задачи

Если ты только начинаешь программировать и не решал задач на хеш-таблицы, твоё первое решение может быть неоптимальным:

using System;

class Solution
{
    public int[] TwoSum(int[] nums, int target)
    {
        // Перебираем все возможные пары чисел
        for (int i = 0; i < nums.Length; i++)
        {
            for (int j = i + 1; j < nums.Length; j++)
            {
                if (nums[i] + nums[j] == target)
                {
                    return new int[] { i, j };
                }
            }
        }
        return Array.Empty<int>(); // Возвращаем пустой массив, если пара не найдена
    }
}

Здесь идёт полный перебор всех пар чисел, что занимает O(n²) времени. Это слишком долго для данной задачи, поэтому интервьюер попросит тебя найти более оптимальное решение.  

Оптимальное решение

Для оптимального решения используем хеш-таблицу.

using System;
using System.Collections.Generic;

class Solution
{
    public int[] TwoSum(int[] nums, int target)
    {
        // ключ - число, значение - индекс этого числа в массиве
        Dictionary<int, int> usedNums = new Dictionary<int, int>();

        for (int idx = 0; idx < nums.Length; idx++)
        {
            int secondNum = nums[idx];
            // firstNum - число, которое должно было встретиться ранее,
            // чтобы сумма с secondNum дала target
            int firstNum = target - secondNum;

            // Если firstNum уже встречалось, то возвращаем индексы
            if (usedNums.ContainsKey(firstNum))
            {
                return new int[] { usedNums[firstNum], idx };
            }

            // Добавляем текущее число в словарь
            usedNums[secondNum] = idx;
        }

        return Array.Empty<int>(); // Возвращаем пустой массив, если пара не найдена
    }
}

Идея в том, чтобы в качестве ключа использовать элемент массива, а в качестве значения — его индекс. Далее надо перебрать все числа в массиве, предполагая, что текущее число — одно из искомых. Второе число определяем как first_num = target - second_num. Если first_num уже есть в хеш-таблице, возвращаем результат. Иначе добавляем текущее число в хеш-таблицу.  

Так хеш-таблица хранит числа, которые мы уже прошли, и позволяет быстро проверить, встречали ли мы ранее число, которое в сумме с текущим даёт target.  

Зачем нужны массивы, когда есть хеш-таблица

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

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

Заключение

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