Анаграммы

В этом уроке ты
  • Решишь задачу с собеседования в крупной IT-компании.
  • Освоишь методы решения задач на тему «Анаграммы».
  • Поймёшь, как выбрать оптимальное решение на собеседовании.
Что такое анаграмма

Анаграмма — это слово или фраза, полученные перестановкой букв другого слова или фразы.  

Например, «СИЛАЧ» — это анаграмма слова «ЧИСЛА». Переставив буквы в слове «ЧИСЛА», можно получить «СИЛАЧ».  

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

Задачи на анаграммы часто встречаются в начале собеседования, как своего рода разминка. Представь, что у тебя есть 20 минут, чтобы решить следующую задачу.

Условие

Даны две строки s и t. Нужно вернуть True, если t является анаграммой s, и False в противном случае.  

Пример 1
Ввод: s = "hello", t = "lolhe"
Вывод: true
Пример 2
Ввод: s = "abc", t = "abcc"
Вывод: false
В чем сложность задачи

Первое, что может прийти на ум, — отсортировать обе строки и сравнить их:

class Solution
{
    public bool IsValidAnagram(string s, string t)
    {      
        // Преобразуем строки в массивы символов, сортируем и сравниваем
        char[] sArray = s.ToCharArray();
        char[] tArray = t.ToCharArray();
        Array.Sort(sArray);
        Array.Sort(tArray);
        return sArray.SequenceEqual(tArray);
    }
}

Это работает, но не оптимально. Сортировка занимает O(n*log(n)) времени, а затраты памяти могут варьироваться от O(1) до O(n) в зависимости от реализации.  

Интервьюер ожидает более эффективного решения с временем O(n).
Оптимальное решение

Две строки являются анаграммами, если количество каждой буквы совпадает.

Например, в словах «ЛАСКА» и «СКАЛА» содержится:

  • 2 буквы «А»
  • по 1 букве «С», «К» и «Л»

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

public class Solution
{
    public static bool IsValidAnagram(string s, string t)
    {
     if (s.Length != t.Length)
        {
            return false;
        }

        var countS = new Dictionary<char, int>();
        var countT = new Dictionary<char, int>();

        foreach (char c in s)
        {
            if (!countS.ContainsKey(c))
                countS[c] = 0;
            countS[c]++;
        }

        foreach (char c in t)
        {
            if (!countT.ContainsKey(c))
                countT[c] = 0;
            countT[c]++;
        }

        foreach (var kvp in countS)
        {
            if (!countT.TryGetValue(kvp.Key, out int count) || count != kvp.Value)
            {
                return false;
            }
        }
        
        return true;
    }
}

Это решение работает за O(n) времени и использует O(n) памяти, что намного оптимальнее предыдущего решения.  

Неожиданный вопрос

Хороший интервьюер после решения может спросить: «Можешь решить задачу без словарей, если строки содержат только строчные латинские буквы?»

Да, без переписывания решения тут не обойтись!
Улучшаем решение

Если строки содержат только 26 строчных латинских букв, можно использовать массив фиксированного размера для подсчёта букв.

Остаётся понять, как сопоставить букву 'a' с индексом 0 в массиве count, букву 'b' с индексом 1 и т.д. Тут пригодится таблица ASCII.  

ASCII таблица — это таблица, где каждому символу присвоено числовое значение. Полную таблицу можно найти в интернете, но нас интересует крайний столбец справа.

Значения увеличиваются на 1 от «a» до «z», что как раз нам и нужно для сопоставления буквы с индексом. Теперь осталось разобраться, как из символа получить число и наоборот:

# ord - переводит символ в число
print(ord("a")) # 97
# chr - переводит число в символ
print(chr(97)) # a
class Solution
{
    public static bool IsValidAnagram(string s, string t)
    {
        if (s.Length != t.Length)
        {
            return false;
        }

        // Массив для подсчета количества букв (26 символов английского алфавита)
        int[] count = new int[26];

        // Увеличиваем счетчик для букв из s
        foreach (char c in s)
        {
            count[c - 'a']++;
        }

        // Уменьшаем счетчик для букв из t
        foreach (char c in t)
        {
            count[c - 'a']--;
        }

        // Если массив состоит только из нулей, значит строки — анаграммы
        foreach (int i in count)
        {
            if (i != 0)
            {
                return false;
            }
        }

        return true;
    }
}

Алгоритм решения задачи:

  • Создаём массив count длиной 26 (по числу букв в алфавите).
  • Индексы в массиве соответствуют буквам (0 для 'a', 1 для 'b' и т.д.).
  • Проходим по первой строке и увеличиваем счётчики.
  • Проходим по второй строке и уменьшаем счётчики.
  • Если все элементы массива равны нулю, строки — анаграммы.
Какое решение выбрать
  • Если известно, что строки содержат ограниченный набор символов (например, только строчные латинские буквы), лучше использовать массив.
  • Если набор символов неизвестен или велик, словарь будет более универсальным.
Важно спросить интервьюера: «Известно ли, какие символы могут быть в строках?» Это покажет твою внимательность и умение выбирать оптимальное решение до написания кода. Такие вопросы идут только в плюс на собеседовании!
Заключение

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