Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование.Python.8-10.docx
Скачиваний:
2
Добавлен:
01.07.2025
Размер:
1.2 Mб
Скачать

Перебор всех перестановок

Напишем алгоритм перебора всех перестановок чисел от 1 до n, то есть последовательности, полученной из списка чисел от 1 до n изменением порядка их следования. Известно, что таких перестановок существует n!, напишем функцию, которая выводит все перестановки в лексикографическом порядке.

Как и ранее, функция получает в качестве параметра значение n (длина перестановки) и prefix - уже построенное начало перестановки. Далее перебираются все числа от 1 до n в порядке возрастания в качестве возможного продолжения перестановки. Если число не содержится в списке prefix, то к списку prefix добавляется следующий элемент последовательности и функция вызывается рекурсивно.

Окончание рекурсии происходит в случае, если список prefix имеет длину n, то есть все числа от 1 до nуже содержатся в списке prefix.

def generate(n, prefix):     if len(prefix) == n:         print(prefix)     else:         for next in range(1, n + 1):             if next not in prefix:                 generate(n, prefix + [next])

Сортировка слиянием

Алгоритм сортировки слиянием основан на идее, что два отсортированных списка можно слить в один отсортированный список за время, равное суммарной длине этих списков.

Для этого сравним первые элементы данных списков. Тот элемент, который меньше, скопируем в конец результирующего списка (который первоначально пуст) и в этом списке перейдем к следующему элементу. Будем повторять этот процесс (выбираем из начала двух списков наименьший элемент, копируем его в результирующий список), пока один из исходных списков не кончится. После этого оставшиеся элементы (один из двух исходных списков будет непуст) скопируем в результирующий список.

Для того, чтобы не удалять начальные элементы из списков, заведем два индекса i и j, указывающие на текущие элементы в каждом списке. Вместо удаления элементов будем передвигать эти индексы. В конце добавим к результирующему списку оставшиеся элементы из двух исходных списков A[i:] + B[j:] (один из этих срезов будет пустым, это не должно нас смущать).

def merge(A, B):     Res = []     i = 0     j = 0     while i < len(A) and j < len(B):         if A[i] <= B[j]:             Res.append(A[i])              i += 1          else:             Res.append(B[j])              j += 1      Res += A[i:] + B[j:]      return Res

Заметим, что сложность работы функции merge — линейная от суммарных длин списков A и B, так как каждый элемент обрабатывается ровно один раз за O(n).

Теперь можно реализовать сортировку слиянием. Это — рекурсивная функция, которая получает на вход исходный список и список, составленный из тех же элементов, но отсортированный. Если длина исходного списка равна 1 или 0, то он уже отсортирован и сортировать его не надо. Если же длина списка больше 1, то разобьем его на две части равной (или почти равной, если длина исходного списка — нечетная). Отсортируем обе эти части, затем сольем их вместе при помощи функции merge.

def MergeSort(A):      if len(A) <= 1:          return A      else:         L = A[:len(A) // 2]          R = A[len(A) // 2:]      return merge(MergeSort(L), MergeSort(R))

Оценим сложность этого алгоритма. Пусть массив содержит n элементов. Тогда за O(n) его можно разделить на две части и после сортировки слить их вместе. Каждая из этих двух частей имеет размер n/2, и за O(n) шагов каждую из них можно поделить на две части размером n/4 и затем после сортировки слить их вместе. Аналогично, четыре части размером n/4 за суммарное O(n) шагов делятся на части размером n/8 и сливаются вместе. Этот процесс «в глубину» продолжается столько раз, сколько раз можно число n делить на 2, до тех пор, пока размер части не станет равен 1, то есть log2n. Итого, общая сложность этого алгоритма равна O(nlog2n).

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

Быстрая сортировка Хоара

Этот алгоритм, чаще называемый просто «быстрая сортировка» (англ. Quicksort), придуман английским ученым Чарльзом Хоаром в 1960 году и использует стратегию «разделяй и властвуй».

Шаги алгоритма таковы:

1. Выбираем в массиве некоторый элемент, который будем называть опорным элементом.  2. Операция разделения массива: реорганизуем массив таким образом, чтобы все элементы, меньшие или равные опорному элементу, оказались слева от него, а все элементы, большие опорного — справа от него. Обычный алгоритм этой операции:

  • Два индекса — l и r, приравниваются к минимальному и максимальному индексу разделяемого массива соответственно.

  • Вычисляется индекс опорного элемента m.

  • Индекс l последовательно увеличивается до m до тех пор, пока l-й элемент не превысит опорный.

  • Индекс r последовательно уменьшается до m до тех пор, пока r-й элемент не окажется меньше либо равен опорному.

  • Если r = l — найдена середина массива — операция разделения закончена, оба индекса указывают на опорный элемент.

  • Если l < r — найденную пару элементов нужно обменять местами и продолжить операцию разделения с тех значений l и r, которые были достигнуты. Следует учесть, что если какая-либо граница (l или r) дошла до опорного элемента, то при обмене значение m изменяется на r-й или l-й элемент соответственно.

3. Рекурсивно упорядочиваем подмассивы, лежащие слева и справа от опорного элемента. 4. Базой рекурсии являются наборы, состоящие из одного или двух элементов. Первый возвращается в исходном виде, во втором, при необходимости, сортировка сводится к перестановке двух элементов. Все такие отрезки уже упорядочены в процессе разделения.

Поскольку в каждой итерации (на каждом следующем уровне рекурсии) длина обрабатываемого отрезка массива уменьшается, по меньшей мере, на единицу, терминальная ветвь рекурсии будет достигнута всегда и обработка гарантированно завершится.