0Pricing
Coding Interview Prep · Урок

Перестановки и сочетания

Перечислите все перестановки списка с повторяющимися элементами и без них, а также сгенерируйте все сочетания по k элементов и варианты задачи о сумме сочетаний

«Перестановки и сочетания» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.

Перестановки и сочетания

Перестановки — это расположения, в которых порядок имеет значение: [1,2,3] и [3,2,1] различаются. Число перестановок из n элементов равно n!. Сочетания — это выбор элементов, в котором порядок не имеет значения: выбор {1,2} совпадает с выбором {2,1}. Число сочетаний размера k из n элементов равно C(n,k) = n! / (k! × (n-k)!). Оба шаблона крайне важны в задачах на собеседованиях, связанных с подсчётом, перечислением и выбором.

import math

# Permutations
n = 4
print(f'Permutations of {n} items: {math.factorial(n)}')
# 4! = 24

# Combinations
for k in range(n+1):
    print(f'C({n},{k}) = {math.comb(n,k)}')
# C(4,0)=1, C(4,1)=4, C(4,2)=6, C(4,3)=4, C(4,4)=1
# Sum = 2^4 = 16 (total subsets)

Генерация всех перестановок

Используйте логический массив used, чтобы отслеживать элементы, входящие в текущий путь. На каждом шаге пробуйте каждый ещё не использованный элемент. После исследования ветви снова помечайте выбранный элемент как неиспользованный. В отличие от подмножеств, индекс start здесь не нужен, поскольку перестановки используют элементы в любом порядке. Рекурсия завершается, когда выполняется условие len(path) == n.

def permutations(nums):
    result = []
    used = [False] * len(nums)
    def backtrack(path):
        if len(path) == len(nums):
            result.append(list(path))
            return
        for i, num in enumerate(nums):
            if not used[i]:
                used[i] = True         # CHOOSE
                path.append(num)
                backtrack(path)        # EXPLORE
                path.pop()             # UNCHOOSE
                used[i] = False
    backtrack([])
    return result

print(permutations([1, 2, 3]))
# [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Перестановки на основе обмена

Другой вариант: обменивать элемент на позиции start с каждым элементом от start до n-1, выполнять рекурсивный вызов, а затем отменять обмен. Этот подход изменяет массив непосредственно, без массива used. Ключевая идея состоит в том, что на каждом уровне всё слева от start уже зафиксировано, а мы выбираем, какой элемент поместить на позицию start. Такой подход немного экономнее расходует память и лежит в основе алгоритма Хипа.

def permutations_swap(nums):
    result = []
    def backtrack(start):
        if start == len(nums):
            result.append(list(nums))
            return
        for i in range(start, len(nums)):
            nums[start], nums[i] = nums[i], nums[start]  # CHOOSE (swap)
            backtrack(start + 1)                          # EXPLORE
            nums[start], nums[i] = nums[i], nums[start]  # UNCHOOSE (swap back)
    backtrack(0)
    return result

print(permutations_swap([1, 2, 3]))
# Same 6 permutations, different order

Перестановки II: обработка дубликатов

Если входные данные содержат дубликаты (например, [1, 1, 2]), подход с массивом used создаёт повторяющиеся перестановки. Исправление: отсортируйте массив, а затем пропускайте дубликат, если предыдущий такой же элемент не был использован в этом рекурсивном вызове. Условие: if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continue. Это гарантирует, что дубликаты всегда выбираются слева направо.

def permutations_unique(nums):
    nums.sort()
    result = []
    used = [False] * len(nums)
    def backtrack(path):
        if len(path) == len(nums):
            result.append(list(path))
            return
        for i in range(len(nums)):
            if used[i]: continue
            # Skip if this num is a duplicate and the previous dup was not used
            if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
                continue
            used[i] = True
            path.append(nums[i])
            backtrack(path)
            path.pop()
            used[i] = False
    backtrack([])
    return result

print(permutations_unique([1, 1, 2]))
# [[1,1,2],[1,2,1],[2,1,1]] — 3, not 6

Следующая перестановка (лексикографический порядок)

Следующая перестановка (LeetCode 31) преобразует массив непосредственно в следующую перестановку, большую в лексикографическом порядке. Алгоритм: (1) найдите крайнюю справа позицию i, для которой nums[i] < nums[i+1]. (2) найдите крайнюю справа позицию j, для которой nums[j] > nums[i]. (3) обменяйте местами nums[i] и nums[j]. (4) разверните суффикс после индекса i. Если такой позиции i не существует, разверните весь массив — это возвращает его к наименьшей перестановке.

def next_permutation(nums):
    n = len(nums)
    # Step 1: find rightmost i where nums[i] < nums[i+1]
    i = n - 2
    while i >= 0 and nums[i] >= nums[i+1]:
        i -= 1
    if i >= 0:
        # Step 2: find rightmost j where nums[j] > nums[i]
        j = n - 1
        while nums[j] <= nums[i]:
            j -= 1
        # Step 3: swap
        nums[i], nums[j] = nums[j], nums[i]
    # Step 4: reverse suffix after i
    nums[i+1:] = nums[i+1:][::-1]
    return nums

print(next_permutation([1, 2, 3]))  # [1,3,2]
print(next_permutation([3, 2, 1]))  # [1,2,3] (wraps)
print(next_permutation([1, 1, 5]))  # [1,5,1]

Поиск с возвратом для сочетаний размера k

Сгенерируйте все сочетания из k элементов среди n (LeetCode 77). Используйте начальный индекс, как в задачах о подмножествах, чтобы не выбирать элементы повторно и сохранять порядок. Отсекайте ветвь, когда остаётся меньше чем k - len(path) элементов: if len(nums) - i + 1 < k - len(path): break. Это эквивалентно рассмотренному ранее combine(n, k), но применяется к реальному массиву.

def combinations(nums, k):
    result = []
    def backtrack(start, path):
        if len(path) == k:
            result.append(list(path))
            return
        for i in range(start, len(nums)):
            # Pruning: not enough elements left
            if len(nums) - i < k - len(path):
                break
            path.append(nums[i])
            backtrack(i + 1, path)
            path.pop()
    backtrack(0, [])
    return result

print(combinations([1,2,3,4,5], 3))
# 10 combinations: C(5,3)
import math
print(math.comb(5,3))  # 10

Сумма сочетаний: неограниченное повторное использование

Сумма сочетаний (LeetCode 39) позволяет использовать каждое число неограниченное количество раз. Отличие от обычных сочетаний состоит в следующем: вместо перехода от start к i+1 передавайте i (тот же индекс), чтобы разрешить повторное использование текущего элемента. Отсечение выполняется так: если оставшаяся целевая сумма становится равной 0, сохраните путь; если она становится отрицательной, остановитесь. Сортировка позволяет досрочно завершить работу, когда все оставшиеся кандидаты превышают оставшуюся целевую сумму.

def combination_sum(candidates, target):
    candidates.sort()
    result = []
    def backtrack(start, path, remaining):
        if remaining == 0:
            result.append(list(path))
            return
        for i in range(start, len(candidates)):
            c = candidates[i]
            if c > remaining: break  # all remaining are too big
            path.append(c)
            backtrack(i, path, remaining - c)  # reuse allowed: pass i, not i+1
            path.pop()
    backtrack(0, [], target)
    return result

print(combination_sum([2, 3, 6, 7], 7))
# [[2,2,3],[7]]

Сумма сочетаний II: без повторного использования, с дубликатами

Сумма сочетаний II (LeetCode 40) использует каждое число не более одного раза, но входные данные могут содержать дубликаты. Здесь объединяются два приёма: увеличивайте start до i+1 (без повторного использования) и после сортировки пропускайте дубликаты на одном уровне (if i > start and nums[i] == nums[i-1]: continue). Это сочетает обработку дубликатов из задачи о подмножествах II с ограничением на отсутствие повторного использования из задачи о сочетаниях.

def combination_sum_ii(candidates, target):
    candidates.sort()
    result = []
    def backtrack(start, path, remaining):
        if remaining == 0:
            result.append(list(path))
            return
        for i in range(start, len(candidates)):
            if candidates[i] > remaining: break
            # Skip duplicates at same level
            if i > start and candidates[i] == candidates[i-1]:
                continue
            path.append(candidates[i])
            backtrack(i + 1, path, remaining - candidates[i])  # no reuse: i+1
            path.pop()
    backtrack(0, [], target)
    return result

print(combination_sum_ii([10,1,2,7,6,1,5], 8))
# [[1,1,6],[1,2,5],[1,7],[2,6]]

Буквенные сочетания телефонного номера

Буквенные сочетания (LeetCode 17) сопоставляют каждой цифре буквы на клавиатуре телефона и генерируют все возможные буквенные сочетания для заданной строки цифр. Это задача на поиск с возвратом: на каждой позиции выбирается одна буква из соответствия данной цифре, после чего выполняется рекурсивный вызов. Для строки длины n, где каждая цифра в среднем соответствует k буквам, временная сложность равна O(kⁿ).

def letter_combinations(digits):
    if not digits: return []
    phone = {
        '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
        '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
    }
    result = []
    def backtrack(index, path):
        if index == len(digits):
            result.append(''.join(path))
            return
        for letter in phone[digits[index]]:
            path.append(letter)
            backtrack(index + 1, path)
            path.pop()
    backtrack(0, [])
    return result

print(letter_combinations('23'))
# ['ad','ae','af','bd','be','bf','cd','ce','cf']

Сравнение перестановок и сочетаний

Ключевые структурные различия: Перестановки — начальный индекс не используется; для предотвращения повторного выбора применяется массив used или обмен элементов; на каждом уровне дерево имеет n вариантов, всего оно содержит n! листьев. Сочетания — используется начальный индекс, чтобы обеспечить порядок; дерево содержит C(n,k) листьев. Сумма сочетаний — индекс start не увеличивается, чтобы разрешить повторное использование; ветви отсекаются по целевой сумме. Если сопоставить новую задачу с одной из этих трёх форм, вы сразу получите подходящий шаблон.

# Pattern summary:
# Permutations: for i in range(n); if not used[i]; no start advancement
# Combinations: for i in range(start, n); advance start → i+1
# Combo Sum (reuse): for i in range(start, n); advance start → i (same)

# Quick reference:
import math
n = 5
print(f'Perm({n})   = n! = {math.factorial(n)}')
print(f'Comb({n},2) = C(n,k) = {math.comb(n,2)}')
print(f'Comb({n},3) = {math.comb(n,3)}')
# Also: subsets = sum(C(n,k) for k=0..n) = 2^n
print(f'Subsets({n}) = 2^n = {2**n}')

Сложность и советы для собеседования

Временная сложность перечисления: перестановки — O(n × n!), сочетания — O(k × C(n,k)), сумма сочетаний — O(n^(T/min_val)). Пространственная сложность равна O(n) для глубины рекурсии плюс O(результат) для сохранения результатов. Важные советы: (1) всегда уточняйте, имеет ли порядок значение (перестановка или сочетание); (2) упоминайте обработку дубликатов до того, как вас об этом спросят; (3) всегда явно называйте условие отсечения; (4) для больших n отмечайте, что сам результат имеет экспоненциальный размер, поэтому для этой задачи алгоритм оптимален.

import math

# Complexity for n=10
n = 10
print(f'Permutations(10): {math.factorial(n):,} results')
print(f'Combinations(10,5): {math.comb(n,5):,} results')
print(f'Subsets(10): {2**n:,} results')

# For interview: state which pattern
# 'This is a combinations problem because order doesnt matter'
# 'I will use a start index to avoid revisiting elements'
# 'Pruning: when sum exceeds target, break (after sorting)'

Проверка знаний

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

Итоги урока

В этом уроке вы узнали, что перестановки используют массив used и не используют начальный индекс, создавая n! расположений, сочетания используют начальный индекс, который увеличивается для предотвращения повторного выбора, создавая C(n,k) вариантов, а дубликаты в обеих задачах обрабатываются сортировкой и пропуском повторяющихся значений на одном уровне рекурсии. Далее мы применим поиск с возвратом к задаче о N ферзях и рассмотрим распространение ограничений.

Часто задаваемые вопросы

Урок «Перестановки и сочетания» бесплатный?

Да — полный текст урока «Перестановки и сочетания» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Перестановки и сочетания»?

Перечислите все перестановки списка с повторяющимися элементами и без них, а также сгенерируйте все сочетания по k элементов и варианты задачи о сумме сочетаний Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Coding Interview Prep?

Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Перестановки и сочетания»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Coding Interview Prep?

Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Шаблон поиска с возвратом: выбрать, исследовать, отменить выбор
  2. Подмножества и множество всех подмножеств
  3. Перестановки и сочетания
  4. Задача о N ферзях и распространение ограничений
← Назад к Coding Interview Prep