Полный перебор

В этом уроке ты
  • Решишь одну из самых популярных задач на тему “полного перебора”.
  • Научишься писать как рекурсивные, так и итеративные решения.
  • Оценишь сложность задачи по времени и памяти в нотации Big O.
Задача с собеседования

Дана строка s, содержащая цифры от 2 до 9. Нужно вернуть все возможные комбинации букв, которые могли получиться при нажатии на эти цифры на телефоне, в порядке, соответствующем цифрам в строке s. Ответ можно вернуть в любом порядке.

Пример
Ввод: s = "24"
Вывод: ["ag","ah","ai","bg","bh","bi","cg","ch","ci"]
Рекурсивное решение

Можно пойти по простому и понятному пути с использованием рекурсии для решения задачи и получить следующее решение:

public class Solution {
    private static void BruteForce(int index, string s, List<char> currentCombination, List<string> allCombinations) {
        // Если дошли до конца строки, добавляем текущую комбинацию в результат
        if (index == s.Length) {
            allCombinations.Add(new string(currentCombination.ToArray()));
            return;
        }

        var phoneMap = new Dictionary<char, string> {
            {'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"},
            {'6', "mno"}, {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"}
        };
        // Получаем текущую цифру
        char digit = s[index];

        // Перебираем все буквы, соответствующие текущей цифре
        foreach (char letter in phoneMap[digit]) {
            // Добавляем букву в текущую комбинацию
            currentCombination.Add(letter);
            // Рекурсивно перебираем оставшиеся цифры
            BruteForce(index + 1, s, currentCombination, allCombinations);
            // Убираем букву, чтобы попробовать следующую
            currentCombination.RemoveAt(currentCombination.Count - 1);
        }
    }

    public static List<string> GenerateCombinations(string s) {
        // Если строка пустая, возвращаем пустой список
        if (string.IsNullOrEmpty(s)) {
            return new List<string>();
        }

        // Результирующий список всех комбинаций
        var result = new List<string>();
        // Запускаем рекурсивный перебор
        BruteForce(0, s, new List<char>(), result);
        return result;
    }
}

Особенность рекурсии: при очень длинных строках (более ~1000 символов) может произойти переполнение стека, так как многие языки программирования имеют ограничение на глубину рекурсии по умолчанию в районе ~1000 рекурсивных вызовов.

Итеративное решение

public class Solution
{
    public static List<string> GenerateCombinations(string s)
    {
        if (string.IsNullOrEmpty(s))
        {
            return new List<string>();
        }

        var phoneMap = new Dictionary<char, string>
        {
            {'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"},
            {'6', "mno"}, {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"}
        };

        var combinationsQueue = new Queue<string>();
        combinationsQueue.Enqueue("");

        // Пока у первого элемента в очереди длина меньше длины входящей строки
        while (combinationsQueue.Peek().Length < s.Length)
        {
            string prefix = combinationsQueue.Dequeue();
            char digit = s[prefix.Length];

            // Дописываем все возможные буквы к prefix
            foreach (char letter in phoneMap[digit])
            {
                combinationsQueue.Enqueue(prefix + letter);
            }
        }
        return new List<string>(combinationsQueue);
    }
}

Это решение итеративное, поэтому точно не превысит ограничение на глубину стека. Часто итеративные решения работают быстрее на практике, хотя асимптотика та же, что и у рекурсивного метода.

Оценка сложности
  • Время: O(n*4^n). При худшем сценарии каждая цифра может соответствовать 4 буквам, значит всего 4^n комбинаций. Каждая комбинация имеет длину n.
  • Память: O(n*4^n). Для хранения всех возможных комбинаций нам понадобится столько же места, сколько занимает 4^n строк длины n.
Рекурсивное vs итеративное решение
Рекурсивное Итеративное
Ограничено глубиной стека Не имеет ограничений на глубину стека
На практике работает медленнее Зачастую быстрее

На собеседованиях можно писать оба варианта, но итеративное ценится выше!

Что дальше

Скорее переходи к практике и постарайся решить задачи как итеративно, так и рекурсивно, чтобы отработать оба подхода.