Перестановки и сочетания
Перечислите все перестановки списка с повторяющимися элементами и без них, а также сгенерируйте все сочетания по 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 — локальная установка не требуется.
Все уроки этого курса
- Шаблон поиска с возвратом: выбрать, исследовать, отменить выбор
- Подмножества и множество всех подмножеств
- Перестановки и сочетания
- Задача о N ферзях и распространение ограничений