Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Лекции / Лекция_7_алгоритмы_сортировки_продолжение_ipynb_Colab

.pdf
Скачиваний:
0
Добавлен:
28.06.2026
Размер:
1.15 Mб
Скачать

Лекция 7. Продвинутые алгоритмы сортировки. Выход за пределы сравнений

1 блок. Разделяй и властвуй

1.1 Сортировка слиянием. Рекурсивный алгоритм

Сведение о алгоритме

Сложность по времени в наихудшем случае Требует дополнительно памяти в размере

Описание алгоритма

1.Создается дополнительная последовательность размер которой равен сортируемой последовательности. Перейти в 2.

2.Последовательность разбивается на две части и для каждой из частей рекурсивно запускается функция сортировки сначала для левой подпоследовательности, потом для правой. После чего проводят слияние отсортированных подпоследовательностей. Условием выхода из рекурсии является размер подпоследовательности равный нулю.

Графическая иллюстрация работы алгоритма

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

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

Пошаговый алгоритм

1.Разделение

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

[0, 5, -2, 7, 3] → левая [0, 5, -2] , правая [7, 3] [0, 5, -2] → левая [0, 5] , правая [-2] [0, 5] → левая [0] , правая [5]

2.Слияние (рекурсивный возврат)

После разделения начинается слияние отсортированных частей в правильном порядке.

Слияние [0] и [5] → [0, 5]

Слияние [0, 5] и [-2] → [-2, 0, 5]

(сравниваем: -2 < 0 → берём -2, затем 0 < 5 → берём 0, затем 5)

Слияние [7] и [3] → [3, 7]

(сравниваем: 3 < 7 → берём 3, затем 7)

3.Финальное слияние

Слияние [-2, 0, 5] и [3, 7] :

Сравниваем -2 и 3 → берём -2 Сравниваем 0 и 3 → берём 0 Сравниваем 5 и 3 → берём 3 Сравниваем 5 и 7 → берём 5 Остаётся 7 → берём 7

Результат: [-2, 0, 3, 5, 7]

Реализация алгоритма на Python

Функция слияния подпоследовательностей

def merge(sequence, support, ls, le, rs, re):

"""

 

 

 

Слияниедвухотсортированныхчастей

 

последовательности

ls-le:леваячасть(отlsдоle

 

включительно)

rs-re:праваячасть(отrsдоre

 

включительно)

"""

 

 

 

#Копируемсливаемыйдиапазонвовспомогательный

массив

for i

in range(ls,re+

1):

 

support[i]=sequence[i]

 

 

l=ls

#Указательнаначалолевойчасти

r=rs

#Указательнаначалоправойчасти

#Слияниевыбираем: меньшийэлементиздвухчастей

 

for i

in range(ls,re+

1):

 

if l>le:

 

#Еслилеваячастьзакончилась

sequence[i]=support[r]

 

#Берёмэлементизправой

r+=

1

 

 

elif r>re:

 

#Еслиправаячастьзакончилась

sequence[i]=support[l]

 

#Берёмэлементизлевой

l+=

1

 

 

elif support[l]<support[r]:

#Еслилевыйэлементменьшеправого

sequence[i]=support[l]

 

#Берёмлевый

l+=

1

 

 

else:

 

#Еслиправыйэлементменьшеилиравен

sequence[i]=support[r]

 

#Берёмправый

r+=

1

 

 

return None

Реализация алгоритма сортировки

def merge_sort(sequence, support=None, start_index=None, stop_index=None):

"""

 

 

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

 

sequence-сортируемаяпоследовательность

 

support-вспомогательныймассивдляслияния

 

start_-indexлеваяграницасортируемого

подмассива

stop_index-праваяграницасортируемого

подмассива

"""

 

 

#Инициализациявспомогательногомассива(первый

вызов)

if support is None:

 

support=sequence[:]

#Копируемисходнуюпоследовательность

#Инициализациялевойграницы(первызовй)

 

if start_index

is None:

 

start_=index

0

 

#Инициализправойцияграницы(первызовй)

 

if stop_index

is None:

 

stop_index=

len(sequence)-

1

#Базовыйслучайесли:границысомкнулисьвыходим,

 

изрекурсии

if stop_index<=start_index:

 

 

return None

 

 

#Находимсерединуподмассива

 

 

mid=start_index+(stop_index-start_index)

//

2

#Рекурсивносортируемлевуюполовину

 

 

merge_sort(sequence,support,start_index,mid

)

 

#Рекурсивносортируемправуюполовину

 

 

merge_sort(sequence,support,mid+

1,stop_index)

 

#Сливаемдвеотсортированныеполовины

 

 

merge(seqsupport,ence,start_index,mid,mid

+

1,stop_index)

 

 

 

1.2 Сортировка слиянием. Итерационный алгоритм

Сведение о алгоритме

Сложность по времени в наихудшем случае

Требует дополнительно памяти в размере

Краткие сведения о алгоритме и авторе

Алгоритм был разработан в 1945 г. Джоном Фон Нейманом. Этот алгоритм использует подход «разделяй и властвуй» и может быть использован для сортировки структур данных, доступ к элементам которых можно получать только последовательно (например внешние файлы).

Джон фон Нейман (1903-1957)венгеро-американский математик, физик и педагог еврейского происхождения, сделавший важный вклад в квантовую физику, квантовую логику, функциональный анализ, теорию множеств, информатику, экономику и другие отрасли науки. Наиболее известен как человек, с именем которого связывают архитектуру большинства современных компьютеров (так называемая архитектура фон Неймана).

Описание алгоритма

1.Создается дополнительная последовательность размер которой равен сортируемой последовательности. Перейти в 2.

2.Устанавливается начальный размер сливаемых последовательностей равный 1. Выполняем попарное слияние соседних

подпоследовательностей указанного размера начиная с начала последовательности. В случае если для подпоследовательности нет пары, то слияние производить не нужно (для последовательности нечетной длины). Перейти к

3.

3.Увеличить значение размера в два раза. Если размер больше длины последовательности закончить алгоритм. В

противном случае перейти к 2.

Графическая иллюстрация работы алгоритма

В начале алгоритма производят попарное слияние подпоследовательностей длинной 1. По сути это означает упорядочивание пары соседних элементов. Для последнего элемента пары нет, поэтому и слияние для него не производится.

После чего производим попарное слияние подпоследовательностей размером 2. После чего слияние подпоследовательностей длинной 4, таких подпоследовательностей 2 (вторая из одного элемента). После этого оканчиваем алгоритм.

Пошаговый алгоритм

Шаг 1. Создаём вспомогательный массив support того же размера, что и исходный.

Шаг 2. Устанавливаем размер сливаемых подпоследовательностей size = 1 .

Проход 1 (size = 1)

Сливаем соседние подпоследовательности размером 1:

Слияние [0] и [5] → [0, 5] Слияние [-2] и [7] → [-2, 7]

Элемент [3] остаётся без пары → [3]

Результат после прохода: [0, 5, -2, 7, 3]

Шаг 3. Увеличиваем size в 2 раза: size = 2 .

Проход 2 (size = 2)

Сливаем соседние подпоследовательности размером 2:

Слияние [0, 5] и [-2, 7] :

Сравниваем 0 и -2берём -2 Сравниваем 0 и 7 → берём 0 Сравниваем 5 и 7 → берём 5 Остаётся 7 → берём 7

Результат: [-2, 0, 5, 7]

Элемент [3] остаётся без пары → [3]

Результат после прохода: [-2, 0, 5, 7, 3]

Шаг 4. Увеличиваем size в 2 раза: size = 4 .

Проход 3 (size = 4)

Сливаем соседние подпоследовательности размером 4:

Слияние [-2, 0, 5, 7] и [3] :

Сравниваем -2 и 3 → берём -2 Сравниваем 0 и 3 → берём 0

Сравниваем 5 и 3 → берём 3

Сравниваем 5 и ∞ → берём 5

Сравниваем 7 и ∞ → берём 7

Результат: [-2, 0, 3, 5, 7]

Шаг 5. size = 8 > длины последовательности (5). Алгоритм завершён.

Итоговый результат: [-2, 0, 3, 5, 7]

Реализация алгоритма на Python

Функция слияния подпоследовательностей

def merge(sequence, support, ls, le, rs, re):

"""

 

 

 

Слияниедвухотсортированныхчастей

 

последовательности

ls-le:леваячасть(отlsдоle

 

включительно)

rs-re:праваячасть(отrsдоre

 

включительно)

"""

 

 

 

#Копируемсливаемыйдиапазонвовспомогательный

массив

for i

in range(ls,re+

1):

 

support[i]=sequence[i]

 

 

l=ls

#Указательнаначалолевойчасти

r=rs

#Указательнаначалоправойчасти

#Слияниевыбираем: меньшийэлементиздвухчастей

 

for i

in range(ls,re+

1):

 

if l>le:

 

#Еслилеваячастьзакончилась

sequence[i]=support[r]

 

#Берёмэлементизправой

r+=

1

 

 

elif r>re:

 

#Еслиправаячастьзакончилась

sequence[i]=support[l]

 

#Берёмэлементизлевой

l+=

1

 

 

elif support[l]<support[r]:

#Еслилевыйэлементменьшеправого

sequence[i]=support[l]

 

#Берёмлевый

l+=

1

 

 

else:

 

#Еслиправыйэлементменьшеилиравен

sequence[i]=support[r]

 

#Берёмправый

r+=

1

 

 

return None

Реализация алгоритма сортировки

def merge_sort(sequence):

 

 

 

 

"""

 

 

 

 

 

Итерационнаясортировкаслиянием(восходящая)

 

 

 

"""

 

 

 

 

 

 

#Создаёмвспомогательныймассивкоп,исходнойю

 

 

последовательности

support=sequence[:]

 

 

 

 

n=

len(support)

#Длинапоследовательности

 

size=

1

 

#Начальныйразмерсливаемых

подпоследовательностей

while size<n:

 

 

 

 

j=

 

0

#Начинаемсначалапоследовательности

 

#Проходимпопоследовательностишагомsize*2

 

 

 

 

while j<n-size:

 

 

 

 

 

 

#Леваячастьотj:доj+size-1

 

 

 

 

ls=j

 

 

 

 

 

 

le=j+size-

1

 

 

 

 

 

#Праваячастьотj+size: доj+2*size(невыходя-1

 

заграницы)

 

rs=j+size

 

 

 

 

 

re=

min(j+ 2 *size-

1,n-

1)

 

 

 

#Сливаемдвеподпоследовательности

 

 

 

merge(seqsupport,ence,ls,le,rs,

 

 

e)

 

 

 

#Переходимкследующейпаре

 

 

подпоследовательностей

 

j+=

2 *size

 

 

 

 

 

#Увеличиваемразмерсливаемых

 

подпоследовательностей2раза

size=size*

2

 

 

 

 

return None

1.3 Быстрая сортировка. Разбиение Хоара

Сведение о алгоритме и его авторе

Быстрая сортировка была разработана английским ученым Тони Хоаром в 1960 году. Этот алгоритм также как и алгоритм сортировки слиянием использует подход «разделяй и властвуй».

Сэр Чарльз Энтони Ричард Хоар (1934 г. р.) английский ученый работающий в области информатики и вычислительной техники известен своими работами в области разработки алгоритмов и средств для определения корректности программ.

Описание алгоритма

1.Выбрать в последовательности элемент, называемый опорным. Этот элемент выбирается произвольным образом (автор алгоритма предлагает выбирать первый элемент последовательности).

2.В последовательности сравнить все остальные элементы с опорным и переставить их в последовательности так, чтобы разбить ее на две непрерывных области, следующих друг за другом: «элементы меньшие опорного» и «большие». Рекурсивно вызвать пункт 1 сначала для подпоследовательности где хранятся элементы меньше опорного, после для подпоследовательности с элементами больше опорного. Условие прекращения рекурсии это размер просматриваемой подпоследовательности равный 1.

Алгоритм

1.

В качестве опорного элемента выбирается первый элемент последовательности (supportElement). Объявляется две

 

переменных для хранения индексов (в дальнейшем

и ). Переходим к пункту 2.

 

 

2.

Начиная от начала последовательности проводиться поиск элемента для которого

.

 

Начиная от конца последовательности проводится поиск элемента для которого

 

.

 

Выполняем проверку если

то переходим к пункту 3.

на единицу. Уменьшаем

на единицу и

 

В противном случае проводим обмен элементов на

и индексе. Увеличиваем

 

продолжаем поиск.

 

 

 

 

3.

Проводим обмен первого элемента последовательности и элемента на индексе

. Возвращаем значение индекса и

 

заканчиваем текущее разбиение.

 

 

 

Пошаговый алгоритм

Начальная последовательность: [0, 5, -2, 7, 3]

Шаг 1. Разбиение всей последовательности

Опорный элемент: pivot = 0 (первый элемент)

i = 0 , j = 4 (индексы от начала и конца)

Поиск:

Ищем i где sequence[i] > pivot : i = 0 → sequence[0] = 0 (не >) i = 1 → 5 > 0 → i = 1

Ищем j где sequence[j] < pivot : j = 4 → 3 < 0 ? нет

j = 3 → 7 < 0 ? нет

j = 2 → -2 < 0 ? да → j = 2

Проверяем i >= j ? 1 >= 2 ? нет меняем местами sequence[1] и sequence[2]

Результат обмена: [0, -2, 5, 7, 3]

i = 2 , j = 1

Проверяем i >= j ? 2 >= 1 ? да выходим из цикла

Меняем местами опорный элемент (индекс 0) с элементом на индексе j = 1

Результат: [-2, 0, 5, 7, 3]

Возвращаем индекс j = 1 опорный элемент 0 находится на своей позиции.

Шаг 2. Рекурсивная сортировка левой части

Левая часть: [-2] (индексы 0..0)

Размер = 1 → рекурсия завершается.

Шаг 3. Рекурсивная сортировка правой части

Правая часть: [5, 7, 3] (индексы 2..4)

Разбиение подпоследовательности [5, 7, 3]

Опорный элемент: pivot = 5

i = 2 , j = 4 (относительно всей последовательности)

Поиск:

Ищем i где sequence[i] > pivot : i = 2 → 5 > 5 ? нет

i = 3 → 7 > 5 ? да → i = 3

Ищем j где sequence[j] < pivot : j = 4 → 3 < 5 ? да → j = 4

Проверяем i >= j ? 3 >= 4 ? нет меняем местами sequence[3] и sequence[4]

Результат обмена: [-2, 0, 5, 3, 7]

i = 4 , j = 3

Проверяем i >= j ? 4 >= 3 ? да выходим из цикла

Меняем местами опорный элемент (индекс 2) с элементом на индексе j = 3

Результат: [-2, 0, 3, 5, 7]

Возвращаем индекс j = 3 опорный элемент 5 на своей позиции.

Шаг 4. Рекурсивная сортировка левой части правой подпоследовательности

Левая часть: [3] (индексы 2..2)

Размер = 1 → рекурсия завершается.

Шаг 5. Рекурсивная сортировка правой части правой подпоследовательности

Правая часть: [7] (индексы 4..4)

Размер = 1 → рекурсия завершается.

Итоговый результат: [-2, 0, 3, 5, 7]

Реализация алгоритма на Python

Функция для разбиения подпоследовательностей предложенная Хоаром

def partition(sequence, lo_index, hi_index):

"""

РазбиениеХоара lo_index-леваяграницаподпоследовательности

hi_index-праваяграница подпоследовательности

"""

#Опорныйэлемент-первыйэлемент подпоследовательности support_=elsequence[loment_index]

#Указателиначинаем:соследующегопослеопорного

испоследнего

i=lo_index+

1

 

 

j=hi_index

 

 

 

while True:

 

 

#Двигаемiвправопока,элементменьшеопорного

 

while i<hi_index

and sequence[i]<support_element:

i+=

1

 

 

#Двигаемjвлевопока,элементбольшеопорного

 

while sequence[j]>support_element:

 

j-=

1

 

 

#Еслиуказатпересеклисьвыходимиз,цикла

 

if i>=j:

 

 

 

break

 

 

#Меняемместамиэлементынапозицияхi j

 

sequence[i],sequence[j]=sequence[j],

quence[i]

#Сдвигаемуказатели

 

 

i+=

1

 

 

j-=

1

 

 

#Ставимопорныйэлементнасвоёместо(меняемс

элеменатомпозицииj)

sequence[losequence[j]index],=sequence[j],

sequence[lo_index]

return j

#Возвращаеминдексопорногоэлемента

 

Реализация алгоритма быстрой сортировки

def quick_sort(sequence, lo_index=None, hi_index=None):

"""

Рекурсивнаябыстраясортировка sequence-сортируемаяпоследовательность lo_index-леваяграницаподпоследовательности hi_index-праваяграница подпоследовательности

"""

#Инициализациялевойграницы(первызовй) if lo_index is None:

lo_index= 0

#Инициализправойцияграницы(первызовй)

 

if hi_index

is None:

 

hi_index=

len(sequence)-

1

#Базовыйслучайесли:границысомкнулисьвыходим,

изрекурсии

if lo_index>=hi_index:

 

return None

 

#ВыполняемразбиениеХоараполучаем, индекс

опорногоэлемента

pivot_index=partition(sequence,lo index,hi

_index)

#Рекурсивносортируемлевуючасть(элементы

меньшеопорного)

quick_sort(sequence,lo index,pivot_index-

1)

#Рекурсивносортируемправуючасть(элементы

большеопорного)

quick_sort(sequence,pivot index+

1,hi_index)

Быстрая сортировка. Разбиение Ломуто

Сведение о алгоритме

Сложность по времени в наихудшем случае

Алгоритм

1.

В качестве опорного элемента выбирается первый элемент последовательности (supportElement). Объявляется две

 

 

переменных для хранения индексов (в дальнейшем и ). Значение равно первому индексу в подпоследовательности.

 

Переходим к пункту 2.

 

2.

Начиная от начала последовательности проводится поиск элемента для которого

. Если

 

такой элемент найден, то увеличиваем значение на одну единицу, производим обмен элементов которые стоят на

и

 

индексе.

 

3.

Проводим обмен первого элемента последовательности и элемента на индексе . Возвращаем значение индекса и

 

заканчиваем текущее разбиение.

 

Графическое пояснение алгоритма разбиения подпоследовательностей Ломуто

1.Выбираем в качестве опорного первый элемент.

2.Начиная с начала последовательности ищем элемент который меньше опорного (i=2), увеличиваем j на единицу, меняем их местами.

3.Меняем местами j и опорный элемент.

4.Заканчиваем, возвращаем j=1.

Пошаговый алгорит

Исходная последовательность: [0, 5, -2, 7, 3]

Шаг 1. Инициализация

Опорный элемент ( supportElement ): 0 (первый элемент)

j = lo_index = 0 (индекс, куда будут ставиться элементы меньше опорного)

Проходим i от lo_index + 1 до hi_index

Шаг 2. Проход по элементам (i от 1 до 4)

i = 1: sequence[1] = 5

 

0 ? нет ничего не делаем

5 < supportElement ?

5 <

i = 2: sequence[2] = -2

< 0 ? да

-2 < supportElement ?

-2

j = 0 + 1 = 1

Меняем местами sequence[i] и sequence[j] , т.е. sequence[2] и sequence[1]

Результат: [0, -2, 5, 7, 3]

i = 3: sequence[3] = 7

 

0 ? нет ничего не делаем

7

< supportElement ?

7

<

i = 4: sequence[4] = 3

 

0 ? нет ничего не делаем

3

< supportElement ?

3

<

Шаг 3. Установка опорного элемента на своё место

Меняем местами sequence[lo_index] (опорный элемент supportElement , индекс 0) и sequence[j] (индекс 1)

Результат: [-2, 0, 5, 7, 3]

Шаг 4. Завершение разбиения

Возвращаем j = 1 — индекс опорного элемента.

Теперь:

Левая часть: [-2] (индексы от lo_index до j - 1 ) Правая часть: [5, 7, 3] (индексы от j + 1 до hi_index )

Дальнейшая сортировка правой части

Правая часть [5, 7, 3] (индексы 2..4)

Опорный элемент ( supportElement ): 5

j = lo_index = 2

 

 

 

Проход (i от 3 до 4):

 

 

 

i = 3: sequence[3] = 7

 

 

? нет ничего не делаем

7 < supportElement ?

7

< 5

i = 4: sequence[4] = 3

 

 

? да

3 < supportElement ?

3

< 5

j = 2 + 1 = 3

Меняем местами sequence[i] и sequence[j] , т.е. sequence[4] и sequence[3]

Результат: [-2, 0, 5, 3, 7]

Установка опорного: меняем местами sequence[lo_index] (индекс 2) и sequence[j] (индекс 3)

Результат: [-2, 0, 3, 5, 7]

Возвращаем j = 3

Дальнейшая сортировка левой и правой частей от нового опорного

Левая часть от 5 : [3] (индексы 2..2) — размер 1, рекурсия завершается Правая часть от 5 : [7] (индексы 4..4) — размер 1, рекурсия завершается