И. В. Беляева
К. с. Беляев
МЕТОДЫ СОРТИРОВОК
И их РЕАЛИЗАЦИИ
Ульяновск
2006
Федеральное агентство по образова...
5 downloads
165 Views
2MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
И. В. Беляева
К. с. Беляев
МЕТОДЫ СОРТИРОВОК
И их РЕАЛИЗАЦИИ
Ульяновск
2006
Федеральное агентство по образованию Государственное образовательное учреждение высшего профессионального образования
Ульяновский государственный техниический университет
МЕТОДЫ СОРТИРОВОК И ИХ РЕАЛИЗАЦИИ
методические указания к выполнению лабораторных работ по
программированию для студентов направлений вычислительная техника и
23010062 «Информатика и специальности 230 1о 165«Вычислительные
машины, комплексы, системы и сети»
Составители: и. В. Беляева К. С. Беляев
Ульяновск
2006
УДК
681.3(076)
ББК 32.973я7
М54
Рецензент
кандидат технических наук, профессор, декан ФИСТ В. В. Шишкин Одобрено секцией методических пособий научно-методического совета университета
Методы сортировок и их реализации:
методические указания к
М54 выполнению лабораторных работ / сост. и. В. Беляева, К. с. Беляев. Ульяновск: УлГТУ,
2006. - 48
с.
Рассмотрены алгоритмы сортировок и их реализация на языке С++. В данном методическом
указании
освещены
следующие
методы
сортировок:
методом
выбора,
метода пузырька, методом вставок, методом слияния и быстрая сортировка. Подробно рассмотрены вопросы оценки эффективности алгоритмов с использованием нотации О
большое. В методическое указание включены задания для лабораторной работы «Методы сортировок»
.
Методические указания могут быть использованы студентами младших курсов,
изучающими
дисциплины «Информатика»,
«Программирование на языках высокого
уровня» при изучении темы «Методы сортировок» и при выполнении практических
и
лабораторных работ по этой теме. Методические указания также могут использоваться
учащимися школ при изучении соответствующих тем школьного курса «Информатика». Работа подготовлена на кафедре ВТ.
УДК
681.3(076)
ББК 32.973я7
©
и. В. Беляева, К. с. Беляев,
составление,
©
Оформление. УлГТУ,
2006 2006
ОГЛАВЛЕНИЕ
ВВЕдЕНИЕ
4
ИЗМЕРЕНИЕ ЭФФЕКТИВНОСТИ АЛГОРИТМОВ
5
БЫСТРОДЕЙСТВИЕ АЛГОРИТМОВ
6
СТЕПЕНЬ РОСТА ВРЕМЕННЫХ зАТРАТ
7
ОЦЕНКА ПОРЯДКА ВЕЛИЧИНЫ И ОБОЗНАЧЕНИЕ О-БОЛьшОЕ
8
ЭФФЕКТИВНОСТЬ АЛГОРИТМОВ ПОИСКА
14
АЛГОРИТМЫ СОРТИРОВКИ И ИХ ЭФФЕКТИВНОСТЬ
15
СОРТИРОВКА МЕТОДОМ ПУЗЫРЬКА СОРТИРОВКА МЕТОДОМ ВСТАВОК
19
22
СОРТИРОВКА СЛИЯНИЕМ
24
БЫСТРАЯ СОР'ТИРОВКА
30
СРАВНЕНИЕ АЛГОРИТМОВ СОРТИРОВКИ
44
ЛАБОРАТОРНАЯ РАБОТА «МЕТОДЫ СОРТироВоК»
44
ЦЕЛЬ РАБОТы ОБЩЕЕ ЗАДАНJ!IЕ
44
44
ВАРИАНТЫ
45
БИБЛИОГРАФИЧЕСКИЙ сПИСОК
48
3
Введение
Развитие компьютерной техники, позволившее хранить и обрабатывать огромные объемы информации, вызвало необходимость в методах и средствах, обеспечивающих ее сортировку. Критерием выбора того или иного метода сортировки является его быстродействие. Быстродействие метода зависит от самого алгоритма лежащего в его основе, так 1--1 от его реализации.
Причем
зачастую складывается ситуация, что алгоритм, прекрасно показавший себя при сортировке
обработке
данных
данных
сортировок,
одного
размера,
меньшего
необходимо
про игрывает
размера.
иметь
Как
другому
видим,
некоторый
кроме
алгоритму
самих
инструментарий
при
методов
позволяющий
оценивать их эффективность. Цель данных методических указаний
-
изучение алгоритмов сортировок
и оценка их эффективности. Были рассмотрены наиболее популярные методы сортировок: методом выбора, методом вставок, методом пузырька, методом слияния и быстрая сортировка. Авторы старались простым языком донести сущность
высоко
алгоритмов
уровня.
используемые
в
В нем
и
описать
качестве
их
реализацию
языка
конструкции,
реализации
позволили
на языке
был
создать
реализации алгоритмов. В пособии рассматриваются
программирования
выбран простые
язык и
С++
,
элегантные
оценка алгоритмов с
использованием нотации О-большое. Авторы надеются, что данные методические указания помогут студентам
с
наибольшей
эффективностью
освоить
сортировок и их реализация.
4
столь
важную
тему,
как
методы
Измерение эффективности алгоритмов
Сравнение алгоритмов между собой
основная тема компьютерных
-
наук. Измерение эффективности алгоритмов чрезвычайно важно, поскольку
выбор
алгоритма
сильно
влияет
на
работу
приложения.
Эффективность
алгоритмов, положенных в основу программы, определяет ее успех, будь то текстовый процессор, кассовый аппарат, банкомат, видеоигра или что-нибудь еще.
Допустим,
два
алгоритма
решают
одну
и
ту
же
задачу,
например
осуществляют поиск данных. Как их сравнить между собой и решить, какой из них
лучше?
Выясним
какакие
же
факторы,
влияют
на
стоимость
компьютерной программы. Некоторые из этих факторов касаются стоимости работы,
затраченной
программы.
Другие
на
разработку,
факторы
сопровождение
определяют
стоимость
и
ее
использование
выполнения,
т.е.
эффективность, выраженную объемом компьютерного времени, необходимого для выполнения программы.
Анализ алгоритмов
это область компьютерных
(analysis of algorithms) -
наук, изучающая способы сравнения эффективности разных методов решения задач. Обратите внимание, что в этом определении использован термин "метод
решения
задачи",
а
не
"программа" .
Следует
подчеркнуть.,
что
анализ
алгоритмов, как правило, исследует существенные различия эффективности, которые обусловлены собственно методами решения задач, а не остроумными программистскими трюками. Изощренные приемы кодирования, позволяющие
снизить
стоимость
вычислений,
чаще
всего
снижают
читабельность
программы, тем самым повышая затраты на ее сопровождение и модификацию. Сравнение
алгоритмов
должно
быть
сосредоточено
на
их
существенных
различиях, поскольку именно их эффективность является основным фактором,
определяющим общую стоимость решения. Если два алгоритма выполняются несколько
часов,
а
разница
между
временем
их
выполнения
составляет
несколько секунд, их эффективность одинакова. При анализе эффективности одинаково важны как время выполнения алгоритма,
так
и
занимаемая
им
память.
Для
анализа
этих
факторов
используются аналогичные методы.
Как сравнить быстродействие двух алгоритмов, решающих одну и ту же задачу? Для этого их можно запрограммировать на языке С++ и запустить
обе программы. У этого подхода есть три существенных недостатка.
1. Как
запрограммированы
алгоритмы?
Допустим, алгоритм А 1
выполняется быстрее, чем алгоритм А 2 • Это может быть связано с тем,
что
программа,
реализующая
алгоритм
А1
просто
лучше
написана. Следовательно, сравнивая время выполнения программ, вы на самом
алгоритмы.
деле
сравниваете
Реализации
реализации
алгоритмов
5
алгоритмов,
сравнивать
а
не
сами
бессмысленно,
поскольку они очень
сильно зависят от стиля программирования и
не позволяют определить, какой из алгоритмов эффективнее.
2.
На
каком
компьютере
Особенностей
должны
конкретного
сравнить
эффективность
работать
намного
выполняться
компьютера
алгоритмов.
быстрее
другого,
также
Один поэтому
программы?
не
позволяют
компьютер для
выполнения
программ необходимо применять один и тот же компьютер. компьютер
выбрать?
Конкретные
может Какой
операции, составляющие основу
алгоритма А 1 на одном из компьютеров могут выполняться быстрее, чем операции алгоритма А 2 , а на другом компьютере
Сравнение
эффективности
алгоритмов
не
-
должно
наоборот.
зависеть
от
особенностей конкретного компьютера.
3.
Какие данные вводятся в программы? Возможно, наиболее сложной
проблемой
является
выбор
тестовых
данных.
Всегда
существует
опасность, что при выборе конкретной тестовой задачи алгоритмы окажутся эффективнее, чем на самом деле. Например, сравнивая между
собой
последовательный
упорядоченном
массиве,
и
можно
бинарный
предложить
поиск
элемента
алгоритмам
в
найти
наименьший элемент. В этом случае алгоритм последовательного
поиска
сразу
найдет
искомый
элемент.
Следовательно,
анализ
эффективности не должен зависеть от выбора конкретных данных. Чтобы преодолеть эти трудности, специалисты в области компьютерных наук разработали математические методы анализа алгоритмов, не зависящие от их конкретных реализаций, компьютеров и выбора тестовых данных. Как показано в следующем разделе, эти методы начинаются с подсчета основных
операций, выполняемых при решении задачи.
Бытродействиеe алгоритмов Быстродействие
алгоритма
связано
с
количеством
выполняемых
операций, поэтому оценить его эффективность можно путем их простого подсчета. Рассмотрим несколько примеров. Связанный
список, допускающий обход.
списка, на который ссылается указатель
head,
Содержимое
можно вывести на экран с
помощью следующего фрагмента программы.
N ode * сцг == head; while (cur !== NULL { cout «
cur->itetn «
<- 1
endl · < - n
cur == cur->next;
} //
Конец цикла
присваивание
< - п+ 1
<- n
while
6
связанного
сравнений
операций записи присваиваний
В
предположении,
операторы выполняют
что
связанный
список
состоит
n+ 1 присваивание, n+ 1 сравнение
из
п
узлов,
эти
и п операций записи.
Если каждое присваивание, сравнение и операция записи выполняется за а,
Ь и с единиц времени, то на выполнение данного фрагмента программы уйдет
(n+ 1) *(а+с) +n *и>
единиц
времени.
Итак,
догадаться, что вывод на экран содержимого
можно
интуитивно
узлов связанного списка
100
будет выполняться дольше, чем вывод содержимого
1О
узлов.
Вложенные циклы. Рассмотрим алгоритм, содержащий вложенные циклы.
[о«
(! == 1
до
n)
[о»
(j == 1 до ')
[о» (k == 1 до 5)
Задача Т
Если задача Т решается за t единиц времени, то на выполнение наиболее глубоко вложенного цикла по переменной Цикл по переменной j затратит переменной
L( 5
i
5
*t *i
k
уйдет
5
*
t единиц времени.
единиц времени, а внешний цикл по
будет выполняться за
* t * i) == 5 * t * (1 + 2 +... + n) == 5 * t * п * (n + 1)/2
единиц времени.
Степень роста временных затрат Описанные
алгоритма
выше
выражается
примеры
функцией,
демонстрируют,
зависящей
от
что
время
размера
задачи.
измерения размера задачи зависит от конкретного приложения
узлов
связанного
списка,
выполнения
-
Способ
количества
размера массива или количества элементов стека.
Итак, мы приходим к следующим выводам.
Для решения задачи,
затрачивает
n 2/ 5 *п
алгоритм А
i единиц времени.
Для решения задачи,
затрачивает
имеющей размер п, имеющей размер п,
алгоритм В
единиц времени.
Единицы времени, используемые при оценке эффективности этих
алгоритмов, должны быть одинаковыми. Например, утверждение может выглядеть так.
Для решения задачи,
n
имеющей размер
n,
алгоритм А затрачивает
С'/5секунд.
Выше мы уже перечислили трудности, возникающие на этом пути.
На каком компьютере алгоритм будет выполнен за n 2/5 секунд? Какая реализация этого алгоритма выполняется за п: /5 секунд? При каких 2 данных алгоритм выполнится за n /5 секунд? )
7
Что конкретно нужно знать о быстродействии алгоритма? Важнее всего
знать,
насколько
быстро
возрастает
время
его
выполнения
увеличением размера задачи. Степень роста временных затрат
rate)
с
(growth
выражается следующими высказываниями.
Время выполнения алгоритма А прямо пропорционально п Время выполнения алгоритма В прямо пропорционально По
этим
утверждениям
нельзя
определить,
2
.
n
сколько
именно
времени
выполняется алгоритм А или В. Главное, что при решении больших задач
алгоритм В работает намного быстрее. затрачиваемый
алгоритмом
размера задачи,
В,
Иными словами, объем времени,
выраженный
растет медленнее,
функцией,
зависящей
от
чем время выполнения алгоритма А,
поскольку линейная функция растет медленнее квадратичной. Даже если В
5 * n секунд, в то время как алгоритм А выполняется за n /5 секунд, в целом алгоритм В выполняется значительно быстрее алгоритма А. Эта ситуация проиллюстрирована на рис. 1. Таким
действительно
затрачивает 2
образом, выражение Время выполнения алгоритма А прямо пропорционально
n 2 точно характеризует эффективность алгоритма и не зависит от конкретных компьютеров и реализаций. Алгоритм А выполняется за п2/5 секунд
Алгоритм В выполняется за 5*п секунд
n РИСУНОК
1. Время
25
выполиения алгоритмов как функция, зависящая от размера задачи
n
Оценка порядка величины и обозначение О-большое Допустим, выполняется следующее утверждение.
Время выполнения алгоритма А прямо пропорционально функции
f(n}. В таких (order f(n)).
случаях говорят, что алгоритм А имеет порядок
Этот
факт
обозначается
называется сложностью алгоритма
как
О (f(n)).
Функция
(growth-rate function).
f(n} f(n)
Поскольку в
обозначении используется прописная буква О (первая буква слова опаее
(порядок)), оно называется обозначением О-большое
8
(Big-O notation).
Если время решения задачи прямо пропорционально ее размеру п, то
сложность задачи равна О(n) , т.е. имеет порядок
n.
Если время решения
задачи прямо пропорционально квадрату ее размера, Т.е. n 2 задачи равна о(n ) и т.д.
2
то сложность
,
ОСНОВНЬIE ПОНЯТИЯ Определение порядка алгоритма Алгоритм А имеет порядок Дн). Этот факт обозначается как константы
kи
выполняется не более чем за
Условие п
правило,
k
и
k* f(n}
алгоритм А
единиц времени.
определению
n.
если существуют
n ?.nо,
по формализует интуитивное понятие большой задачи. Как
>
этому
переменных
O(f(o)),
по такие, что при решении задачи, имеющей размер
удовлетворяет
большинство
значений
Проиллюстрируем определение несколькими примерами.
• Допустим, что при решении задачи, имеющей размер n, алгоритм 2 выполняется за п -3*п+ 1О секунд. Если существуют такие константы k и ПО, что
3 *n+ 1О для всех п ~ по, 2 то алгоритм имеет порядок п • Фактически если константа k равна 3, а число по равно 2, то 3 * n 2 > n 2 - 3 *n + 1О для всех п> 2, k
* n2 >
n
2
-
как показано на рис.2. Таким образом, при п ~ по дЛЯ выполнения ')
алгоритма потребуется не более
• Ранее
мы
показали,
связанного
что для
k * п:
вывода на экран
списка потребуется
Поскольку неравенство
единиц времени. первых п
(n+ 1) *(a+c)+n*w
2*n 2: n+ 1
элементов
единиц времени.
выполняется для всех
n 2: 1,
имеет
О(п).
Здесь
место неравенство
(2*а+2*с+
w)*n 2: (n+ l)*(a+c)+n* w
Следовательно,
константа
k
равна числу
Требование n времени
сложность
2:
задачи
2 *а+ 2 *c+w,
для всех п
имеет
порядок
а константа по равна
по, в определении величины
будет корректной лишь для
2: 1.
O(f(n))
1.
означает, что оценка
достаточно больших задач.
Иными
словами, если задача имеет относительно небольшие размеры, то оценка
времени ее решения будет слишком заниженной. Например, функция равна О, если число любых значениях
1. Итак, из константы k, следует n
равно
того, что число
равно О при
неправильная оценка времени. Для
выполнения любого алгоритма требуется не нулевое
9
k * log 1
log n
З*п2
п2-З*п+10
о
Рисунок
2. Если
п>
1
2, то 3
* n2
3
2
n
больше, чем
n2 - 3
* п + 1О
количество единиц времени, даже если размер задачи равен
еслиf(n) ==
log n,
Чтобы
функции,
задачу при п ==
подчеркнуть
рассмотрим
таблице (рис.
значения
3,
1 следует
значение
таблицу
и
1.
Следовательно,
рассматривать отдельно.
правильной
график,
оценки
степени
представленные
на рис.
роста
3.
В
а) показаны разные значения аргумента п и приближенные
некоторых
функций,
зависящих
от
п,
в
порядке
увеличения
скорости их роста.
0(1) < 0(10g2n) < О(n) < 0(n*log2n) < о(n 2) < О(n 3) < 0(211) По
этой
таблице
можно
оценить
значений различных функций. (На рис.
относительную скорость
3 ,
б показаны
графики
роста
этих
функций).
1
Константа постоянно
Время
означает,
и, следовательно,
выполнения
algorithm)
что
время
выполнения
алгоритма
не зависит от размера задачи
логарифмического
алгоритма
(logarithmic
медленно возрастает с увеличением размера задачи. Если
размер задачи
возводится в
квадрат, ее
сложность увеличивается
всего в два раза. Позднее мы убедимся, что алгоритм бинарного поиска обладает именно такими свойствами. Напомним, что при бинарном
поиске
массив
делится
пополам,
а
затем
поиск
продолжается в одной из полученных половин массива. Обычно логарифмические алгоритмы решают задачу, сводя ее к задаче меньшего размера. Основание логарифма не влияет на сложность алгоритма, поэтому его можно не указывать.
10
п
Время
выполнения
линейного
алгоритма
прямо
пропорционально размеру задачи.
(linear algorithm)
Если
размер задачи
возводится в квадрат, объем времени увеличивается точно так же
Время
n*log2n
выполнения алгоритма, имеющего сложность
O(n*log2n)
растет быстрее, чем у линейного алгоритма. Такие алгоритмы обычно разбивают задачи на подзадачи и решают их по отдельности. Пример
- сортировка слиянием - рассматривается далее выполнения квадратичного алгоритма (quadratic
такого алгоритма
n
Время
2
аlgогithm)быстро возрастает с увеличением размера задачи. В алгоритмах
такого
типа
часто
используются
два
вложенных
цикла. Такие алгоритмы следует применять лишь для решения
небольших задач.
n
Время выполнения кубического алгоритма (qubic algorithm) еще
3
быстрее возрастает с увеличением размера задачи по сравнению с квадратичным.
цикла,
часто
Алгоритмы,
оказываются
использующие
кубическими.
три
Такие
вложенных
алгоритмы
следует применять лишь для решения небольших задач
2
n
С
увеличением
размера
экспоненциального
алгоритма
резко
поэтому
возрастает,
задачи
время
выполнения
(exponential algorithm) на
практике
такие
обычно
алгоритмы
при меняются редко
Если
сложность
сложность
алгоритма
медленнее,
чем
алгоритма А
В
функция
пропорциональна функции
пропорциональна
f,
то
функции
g,
f(n) ,
которая
совершенно очевидно, что
а
растет
алгоритм В
эффективнее алгоритма А, если размер решаемой задачи достаточно велик. Сложность
алгоритма является
решающим
фактором
при
оценке
его
эффективности. Для упрощения анализа алгоритмов будем использовать некоторые
математическиесвойства обозначения О-большое. При этом следует иметь в
виду, что запись ОИn)) означает «порядка f(n»> или «имеет порядок Символ О
-
f(n»>.
это не функция.
1.При оценке сложности алгоритма можно учитывать только старшую
степень. Например, если алгоритм имеет сложность О(n 3 + 4*n 2 + 3*n), он имеет порядок О(п 3 ) . Из таблицы, показанной на рис. 3, а, видно, что слагаемое п 3 намного больше, чем слагаемые 4*n 2 и 3*n, особенно при 3 больших значениях п, когда порядок функции n + 4 *n 2 + 3 *п совпадает с порядком функции п '. Иначе говоря, эти функции имеют одинаковый 3 2 порядок роста. Итак, даже если сложность алгоритма равна О(n + 4*n + 3*n), можно говорить, что он имеет порядок просто О(n 3 ) . Как правило, алгоритмы имеют сложность О (((n)) , где функцией f(n} является одна из функций, перечисленных на рис. 2.При
оценке
множитель
сложности
при
старшей
3. алгоритма
степени.
11
можно
Например,
если
игнорировать алгоритм
имеет
3
сложность О(5*n ),МОЖНО говорить, что он имеет порядок О(n 3 ) . Это утверждение следует из определения величины O(f(n)), если положить k == 5.
3.0(f(n))+O(g(n))=O(f(n)+g(n)).
Функции,
описывающие
сложность
алгоритма, можно складывать. Например, если алгоритм имеет сложность
о(n 2)+о(n), то говорят, что он имеет сложность о(n В соответствии 2 с лт.Г, это МОЖНО записать просто как о(n ) . Аналогичные правила 2+n).
выполняются для умножения.
Из указанных выше свойств следует, что при оценке эффективности алгоритма нужно оценить лишь порядок его сложности. Точная формула, описывающая сложность алгоритма, зачастую весьма сложна,
а
иногда и
просто невозможна.
Наихудший и средний варианты. При решении конкретных задач одинаковой размерности время
выполнения алгоритма может оказаться
разным. Например, время, необходимое для поиска п элементов, может зависеть от природы самих элементов. Обычно оценивается максимальное
время,
необходимое для
решения задачи размера п,
вариант. Анализ наихудшего варианта оценке
O(f(n)),
т.е.
наихудший
(worts-case analysis)
приводит к
если при решении задачи, имеющей размер п, в наихудшем
случае алгоритм выполняется не более чем за всех
значений
наихудшего
п,
за
варианта
исключением приводит
к
их
k*f(n}
конечного
единиц времени для числа.
пессимистическим
Хотя
оценкам,
анализ это
не
означает, что алгоритм всегда будет работать медленно. Следует иметь в виду, что наихудший вариант на практике встречается редко.
12
а)
n
---------------------------------
111,000
1110,000
11100,000 111,000,000 1
110
11100
1
1
1
1
1
1
1
/Og2n
3
6
9
13
16
19
п
10
102
103
104
105
106
n* /Og2n
30
664
9,965
105
106
107
n2
102
104
106
108
1010
1012
103
106
109
1012
1015
1018
103
1030
10301
103,010
1030,103
10301,030
Функция
In
3 I
12"
I
б)
3
100
п2
n*log2n
75 ro
~
о
о
с,
.о
50
:I:
Q)
1:: Q) ~
о
25 n log2n
1 5
РИСУНОК
3.
Функция
10 n
15
20
Сравнение сложности алгоритмов: а) в табличном виде; б) в графическом виде
f(n)==l
на рисунке не показана., поскольку она не соответствует выбранному
масштабу. Ее график представляет собой линию., проходящую через точку У осих.
13
= 1 параллельно
Анализ среднего варианта( average-case
analysis)
позволяет оценить среднее
время выполнения алгоритма при решении задачи размера n.Говорят, что
среднее время выполнения алгоритма А равно
если при решении
задачи размера п оно не превышает величины
всех значений п, за
O (f(n)) , k* f(n) для
исключением их конечного числа. Как правило, анализ среднего варианта
выполнить намного сложнее, чем анализ наихудшего варианта. Одна из трудностей заключается в
определении вероятностей появления разных
задач одинаковой размерности. Вторая трудность заключается в вычислении
распределений
разных
значений.
Анализ
наихудшего
варианта
легче
поддается вычислениям и поэтому выполняется намного чаще.
Эффективностьалгоритмов поиска в качестве еще одного примера рассмотрим два алгоритма поиска:
последовательныйи бинарный поиск элемента в массиве. Последовательный поиск. При последовательном поиске элемента в массиве,
имеющем
длину
п,
элементы
просматриваются
по
очереди,
начиная с первого, пока не обнаружится искомый, либо не будет достигнут конец массива. В наилучшем случае искомым элементом является первый.
Для его обнаружения понадобится только одно сравнение. Следовательно, в наилучшем случае
0(1).
сложность алгоритма последовательного поиска равна
В наихудшем случае искомый элемент является последним. Для того
чтобы его найти, понадобится п сравнений. Следовательно, в наихудшем случае
сложность
алгоритма
последовательного поиска
равна
О(п).В
среднем случае искомый элемент находится в средней ячейке массива и
обнаруживаетсяпосле
n/2
сравнений.
Бинарный ПОИСК. Является ли бинарный поиск более эффективным, чем
последовательный?
поиска
элемента
в
Алгоритм
упорядоченном
бинарного массиве
и
поиска, основан
предназначен на
для
повторяющемся
делении частей массива пополам. Алгоритм определяет, в какой из двух частей находится элемент, если он действительно хранится в массиве, а
затем повторяет процедуру деления пополам. Итак, в ходе бинарного поиска возникает
несколько
массивов
меньшего
размера,
причем
каждый
раз
размер очередного массива уменьшается вдвое по сравнению с предыдущим.
В ходе очередного разбиения массива алгоритм выполняет сравнения.
Сколько сравнений выполняет алгоритм при поиске элемента в массиве, имеющем длину позиции
n?
искомого
Точный ответ на этот вопрос, разумеется, зависит от элемента
в
массиве.
Однако
можно
вычислить
максимальное количество сравнений, т.е. наихудший вариант. Допустим,
что п == 2
k
,
где k -
некоторое натуральное число. Алгоритм поиска
выполняет следующие шаги.
14
1. Проверяет среднюю ячейку массива, имеющего длину n. 2. Проверяет среднюю ячейку массива, имеющего длину n/2. 3. Проверяет среднюю ячейку массива, имеющего длину n/2 2 и т.д, Чтобы проверить среднюю ячейку массива, сначала нужно поделить массив пополам. После того как массив, состоящий из
n
элементов, поделен
пополам, делится пополам одна из его половин. Эти деления продолжаются
до тех пор, пока 'не останется только один элемент. Для этого потребуется k
выполнить
k разбиений массива. Это возможно, поскольку n/2 ==1. k (Напомним, что n = 2 .) В наихудшем случае алгоритм выполнит k разбиений k и, следовательно, k сравнений. Поскольку п == 2 , получаем, что k== Iog2 n. Что произойдет, если число п не будет степенью двойки? Легко
найти наименьшее число
2k- 1
k, удовлетворяющее
условию
(Например, если n равно 30, то k=5, поскольку 24 Алгоритм по-прежнему
выполнит
по меньшей мере
16 < 30 < 32 < 25.)
=
k
разбиений, пока не
возникнет массив, состоящий из одного элемента. Итак, получаем следующие оценки.
k-l < Iog 2 п < k,
k < 1+log2n< k+ 1,
k~l+ 10g2n. Следовательно,
в
наихудшем
случае
сложность
бинарного
поиска
оценивается величиной бинарного поиска равна O(lOg2n), если ni-2 • в принципе сложность алгоритма в наихудшем случае имеет порядок O(10g2n) для любого k
значения п.
Можно ли утверждать, что бинарный поиск лучше последовательного? Намного
лучше!
последовательного
Например, поиска
Iog21 000000==19,
выполнит
алгоритм бинарного поиска
миллион
не более
-
20.
поэтому
сравнений,
в
алгоритм
то
время
как
Если массивы велики, бинарный
поиск намного эффективнее последовательного. Однако приводит
следует к
иметь
в
дополнительным
виду,
что
затратам,
условие
которые
упорядоченности
могут
стать
массива
существенными.
В следующем разделе мы попробуем их оценить.
Алгоритмы сортировки И их эффективность Сортировка
(sorting) -
это процесс упорядочения набора элементов в
возрастающем или убывающем порядке. Сортировка необходима во многих ситуациях.
Например,
иногда
нужно
упорядочить
данные,
прежде
чем
включить их в отчет. Однако чаще всего сортировка выполняется в качестве первого шага некоторых алгоритмов. Например, поиск данных является одной
из наиболее распространенных задач, выполняемых компьютерами. Если набор
данных
достаточно
велик,
необходимо
например алгоритм бинарного поиска.
15
применять
эффективный
метод,
Однако для применения алгоритма
бинарного поиска необходимо, чтобы массив был упорядочен. Следовательно,
если
исходный
набор
не
был
упорядочен,
сортировка
данных
должна
предшествовать бинарному поиску. Алгоритмы сортировки разделяются на две категории. При выполнении внутренней сортировки
зоп) предполагается, что все данные находятся
(intemal
в оперативной памяти компьютера. Мы будем рассматривать только алгоритмы внутренней сортировки. При выполнении внешней сортировки (extemal воп) данные могут храниться на вспомогательныхзапоминающихустройствах, например на жестком диске.
Упорядочивать можно целые числа, строки символов и даже объекты.
Легко представить себе результаты сортировки набора целых чисел или строк. Однако для набора объектов эта операция непривычна. Если каждый объект содержит только одну переменную-член, то их сортировка ничем не отличается
от сортировки целых чисел. Однако если в объектах содержатся несколько данных-членов,
нужно
указать,
какая
переменная
определяет
порядок
следования объектов. Эта переменная-член называется ключом сортировки (5011
key).
Например,
если
объекты
хранят
информацию
о
людях,
их
можно
упорядочить по имени, возрасту или почтовому индексу. Независимо от выбора ключа сортировки алгоритм сортировки упорядочивает все объекты по значению этой переменноЙ-члена.
Для простоты будем предполагать, что сортировка применяется к числам или символам. Все рассматриваемые алгоритмы ориентируются на
возрастающий порядок. Изменить порядок на убывающий довольно просто. В каждом примере предполагается, что данные хранятся в массиве.
Представьте себе данные, которые можно проверять все сразу. Для их
упорядочения можно было бы выбрать наибольший элемент, поставить его на свое место, затем найти следующий наибольший элемент, поставить его на свое место и. т .д. Карточным игрокам этот процесс напоминает перетасовку
карт в определенном порядке. Этот интуитивный алгоритм формализуется с помощью сортировки методом выбора
(selection sort).
Чтобы упорядочить массив в возрастающем порядке, нужно выбрать наибольший элемент. Поскольку наибольший элемент нужно поставить в самый конец массива, его нужно поменять местами с последним элементом, даже
если
эти
элементы
идентичны.
Теперь,
игнорируя
последний
(наибольший) элемент массива, выполним поиск наибольшего элемента в оставшейся
части
массива
и
поменяем
его
местами
с
предпоследним
элементом исходного массива. Этот процесс продолжается до тех пор, пока не будут найдены и переставлены
n-l
элемент из п элементов массива.
Оставшийся элемент, стоящий первым, не нарушает порядок и поэтому не рассматривается.
На рис.4 показан пример сортировки методом выбора. Среди пяти целых чисел выбирается наибольшее -
с
последним
правильных
элементом местах,
массива
выделены
-
число
числом
полужирным
16
37,
которое меняется местами
13.
(Числа,
шрифтом.
Это
стоящие
на
соглашение
принято для всех остальных рисунков.) Затем среди оставшихся четырех
чисел
снова выбирается
наибольшее
местами с предпоследним элементом следующий выбор
-
число
14-
число
-
-
числом
29, - которое меняется 13. Обратите внимание, что
уже стоит на правильном месте, однако
алгоритм игнорирует этот факт и выполняет фиктивную перестановку числа
14
на одном и том же месте. В принципе намного эффективнее выполнять
фиктивные перестановки, чем каждый раз проверять, нужна перестановка или нет. В заключение выбирается число вторым элементом массива
-
числом
13,
1о.
которое меняется местами со
Теперь массив упорядочен по
возрастанию.
Выбранные элементы закрашены: элементы, стоящие на своих местах,
выделены полужирным шрифтом
Исходный массив
После 1-го обмена:
После 2-го обмена:
После З-го обмена:
После 4-го обмена:
РИСУНОК
4.
Сортировка массива, состоящего из пяти целых чисел, методом выбора
Рассмотрим функцию на языке С++, выполняющую сортировку массива theАrrауметодом выбора.
'JJpeJe.!' тип-элементи-массива DatuTY/)f!"
yoid selectionSon(DataType thеАггщ'f/. ln111)
/T-finoPJz-(j-()-Ч-Zl(i'(1е т эл еме н т ы .Н асс ива п () в О)J")-"u"-"c'-"n'"·1····(1·-·····/1···'["'.n. (.)..,"
// Предусловие: массив tJ](:!i1Гf~ау состоит из н элементис.
// Постусловие: массив t}U!,AIT(~1~ упорядочен по возпаспшниы
// число 11 остается
без изменения.
// Вызываемые фУUКI(IIU: inl1еХ(I!LаГ,f!;еs!, -"н'ар.
//-------------------------------------------------------- // last .: : : индекс последнего элемента в подмассиве.
// подлежащем сортировке.
/? larK(!s! ~:::: индекс найденного наибольшего злементо
jor (intlast .: : n-/: last >.:::: 1; --last) (
t
17
1/Инвариант: массив theI417'ay/7aSl -+- l ..п-Г] упорядочен, // а его размер превышаетразмер массива Ihe1tlJ'~1~UY[().. last} 11;/ Выбираем наибольший элемент в массиве
int lal (gest lf
=
Ihe,A1TayfU.. /ast/
intiех(!/l.lагр:еs{rthеАfтау, /ast-+- J),'
// Меняем местами элементы 11-1еА17 (О ,,/ !агдеs! / и Iht!./11T(~Y //l1S/! SH'op(tJle,A/'TaJ:f1al}!,'cst /, thejl,.r{~~:/ !ast!); 4
}// Конец оператораюк ) // Конец функция sеlесtiоnSогl Функция sеlесfiоn/)огt вьпывает две функиии: indех(!fIаГfZсst и
S1'1Ylp.
;nt Index(!fL(J1If(~esf(COnc5t Datal)'pe lhеАГ/lf l1У[]. ;111 ыге!
// //llаходUПI наибольшии элемент масснии.
// Предусловие: размер массива t}l~.A}T(H: задается аргумента»
// ыге, причем ыге > 1.
!/ Постусловие: возеращает индекс наибольшего элемента массива. j/
Аргументы Н(! изменяются.
//-------------------------------------------------------- ;111
jndех~sоJ~'аг == (); // Индекс наибольшего элемента.
// найденного 00 сих пор.
[о» (пи сипепнпаех : ;: 1; сипенипасх <. ы:«;
t- -сипепнпаех)
{
// Инвариант: tlle~4J7·ay[inllexSof·'aJ"1>
// f he14 rJIf а.у[О ..с ипе 11t 1ndех -!J
index~Sof'ar
:=
сипепнпаех;
} ///{ОllСll операторакт тит
in(lexSoFaJIf; // Индекс наибольшего элементо
/ // Конец функции
index(~flial"ge.\'l
»ои! змар!l)аtат.~lJеЬ х,
[J111a7)Jpe& J)
//-------------------------------------------------------- // ()6Л1ен ()В}'Х энементов.
// Предусловие:аргументы .х
/>/ Постусловие:
и .У элементы, подлежащие обмену.
содержимое ячейки х находится в ячейке J;'. и наоборап
//-------------------------------------------------------- I
l
})а 1а Т)/]) е / е 111 Р
=::
-<-у:.
Х' == ));
\t' :::::-
ге 111/):
} // Конец функции
S)~'{IIJ
Анализ. Как следует из описания алгоритма, сортировка сводится к
сравнениям, обменам и перестановкам элементов. Для начала подсчитаем количество этих операций. Как правило, такие операции более дорогостоящи, чем операции управления счетчиком цикла или манипуляции с
индексами
массива, особенно если в массиве хранятся не числа или символы, а более сложные объекты. Поэтому в
нашем
второстепеннымиоперациями.
18
анализе мы
будем
пренебрегать
Очевидно, цикл
selectionSort выполняется n-l раз. Таким образом, функция selectionSort n-l раз вызывает функции indexOfLargest и swap. При каждом вызове функции indexOfLargest ее цикл выполняется last раз (т.е. size-l раз, где size равно last+l). Итак, при n-l вызове функции indexOf Largest для значений переменной last от n-l до 1 общее количество итераций цикла равняется (n-l )+(n-2)+ ... + 1 == n*(n-l )/2. Поскольку при каждой итерации в функции indexOf Largest выполняется одно сравнение, их общее количество равно n*(n-l )/2. В результате для n-l вызова функции swap выполняется n-l обменов. for
в функции
Для каждого обмена нужно выполнить три присваивания. Следовательно,
общее количество операций присваиванияравно 3*(n-l). В сумме алгоритм сортировки методом выбора выполняет
n*(n-I)/2+З*(n-l) = n /2 + 5*n/2-З основных операций. 2
Применяя
слагаемые
с
свойства
младшими
обозначения
степенями.
В
О-большое,
итоге
можем
получим
отбросить
величину
О(n
/2).
2
Игнорируя множитель 1/2, получаем окончательную оценку о(n ) . Итак, 2 сложность алгоритма сортировки методом выбора равна о(n ) . Хотя
алгоритм
сортировки
методом
выбора
не
зависит
от
первоначального расположения данных, что можно отнести к преимуществам
этого
метода,
его
можно
применять
только
для
небольших
массивов,
?
поскольку величина О(n-) довольно быстро растет. Хотя алгоритм выполняет
о(n ) сравнений, в ходе сортировки осуществляется только О(n) перестановок.
2
Алгоритм
сортировки
методом
выбора
хорош,
представляют собой затратные операции, а сравнения произойти,
когда
каждый
элемент
данных
когда
-
достаточно
перестановки
нет. Это может велик,
а
ключ
сортировки мал. Разумеется, хранение данных в связанном списке позволяет
эффективно выполнять перестановки элементов любого алгоритма.
Сортировка методом пуэырькв Алгоритм
сортировки
методом
пузырька
(bubble sort)
сравнивает
между собой соседние элементы и меняет их местами, если они нарушают порядок. Для этого приходится несколько раз просматривать одни и те же
элементы. Во время первого прохода сравниваются два первых элемента
массива;
если
они
нарушают
порядок,
сравнивается другая пара, т.е. 2-й и
их
меняют
местами.
Затем
3-й элементы. Если они нарушают
порядок, их меняют местами. Просмотр, сравнение и обмен двух элементов выполняется до тех пор, пока не будет достигнут конец массива.
19
а) Проход1:
б) Проход2:
Исходный массив:
РИСУНОК
5. Первые
два прохода при сортировке массива. состоящего из пяти целых чисел, методом пузырька: а) первый проход; б) второй проход
На
рис.
5,
а
показаны
результаты
первого
прохода
алгоритма
сортировки методом пузырька на примере массива, содержащего пять целых
чисел. Сначала сравниваются между собой элементы первой пары
29
и
10.
числа
-
Они нарушают заданный порядок, поэтому их меняют местами.
Затем сравниваются элементы второй пары
-
числа
29
и
14,
поэтому их
также меняют местами. После этого сравниваются элементы третьей пары
числа
29
и
37.
Они не нарушают установленный порядок, поэтому остаются
на своих местах.
пары
-
числа
Хотя
37
В заключение меняются
и
после
местами элементы
последней
13. первого
прохода
массив
остается
неупорядоченным,
наибольший элемент оказывается в конце массива, "всплывая", как пузырек на поверхность
воды.
Во
время второго
прохода нужно
вернуться
к
началу
массива и обработать его точно так же, как и в первый раз, останавливая
обработку на предпоследнем элементе. Таким образом, при втором проходе просматриваются
n-l
элемент
массива.
После
второго
прохода
второй
наибольший элемент окажется на предпоследнемместе, как показано на рис.
б.
Теперь,
5,
игнорируя два последних элемента, которые уже поставлены в
нужном порядке, следует продолжить обработку массива, пока он весь не
будет упорядочен. Несмотря на то что алгоритм сортировки методом пузырька состоит из
n-l
прохода, в некоторых случаях удается обойтись меньшим количеством
шагов. Таким образом, процесс можно прекратить, если в ходе проверки не
выполнено ни
bubbleSort,
одной перестановки, В
приведенной ниже
функции
написанной на языке С++, дЛЯ сигнализации о перестановке
используется булева переменная
sorted.
Функция
bubbleSort
использует
функцию swap, описанную ранее.
\'oid buhh1е/)()/'''( Пала Ту1)(; /11(;/~1 кк ау 1_1. j /1/ 11) //--------------------------------------------------------
20
//
Упорядочиеает элементы массиеа в воэрастающсм
порядке.
// Предуслов ие: масс нв tllе~,41/"Г'а~\' с остоит 1/3 /1 элем ент ов.
// Постусловие: массив fJle..:41/'r(~yупорядочен 11()
в о 3 .ра с т (,11-/1,' Т()
;
ч исло /1 остается без изм енения.
1/ Вызываемая фун кция: .S'}1lt11J.
//
//-------------------------------------------------------- { Ьооl хопеа -== false; // Есл и выполняетсн перестаковка.
// принимает значениекнзею»нп! ракк = 1: (ТJ{lSS <.. п} -+" +1J(1..чн)
l
'копеи;
&&
r
// Инвариант:массив tll€i4,"rоР/J-z+I-/Jl.[s,\'.. п-Г] упорядочен.
/ / (1
его раз..мер больше размера массива lhe;-l,',<{гу{О .. п-ракз]
зопег!
: : : true,- // Массивупорядочен
.for (;111 таех : : :
О; таех
<:: п-разк;
.
+' таех)
/
(
// J[нвариант: размер массива IhеL"1Гllf(l) :j// . indi!x-lj
,// не превышает размера массива 11-1С)l,"jtOо}У'Тпаех.
ии пехнпаех =- index -+ /,' !f>(thе~4гга}!{ill{lе.\, :> f/l(!А4г,.а.\/{nеХfl-"1(lе~);])
{ // Ilереставляем элементЬ/ .\');1 J ( ;[j } ( t-hej'l rl"(I}'{,il'1d(?~Y}. 'h(!",/1 r}'(/)'[ пехлГпаех]') .,.
S()//4tcli : : : : falA.'ie.· // Приэнак персстановки
}
I} (:/ K()llel-( () ператора /,1 f{()HC11 ()}7С!){1т()!){] ,!()//4
11Диагностическое
if'
утвержден ие:
theArr(lJ,'/().. п-раь:~;,s-Il- меньше t heA /""ra.y / п-разз 1
/
1/
K{)fICl/ оператора
}//Z<'""()/lel.j
функции
Анализ.
размер массива J"1(1(~(:1~(---)a /~/
.!()r
bllhlJleS()rl
Как
указывалось
выше,
количество
исполь.зовании метода пузырька не превышает выполняется
n-l
//
!)t;13J\4(?/.)(/
сравнением не
проходе выполняется
больше
и
не
при
При первом проходе
перестановок. При
втором
n-2 перестановок. В общем, при l-M проходе выполняется n-l сравнений и не больше n-l
n-2
сравнения
n-l
n-l.
проходов
больше
перестановок. Следовательно, в худшем случае при сортировке методом
пузырька будет выполнено
(n-I)+(n-2)+ ... +1 == n*(n-I)/2 сравнений
и
перестановке
столько
же
перестановок.
выполняется
три
Напомним,
присваивания.
что
Таким
при
образом,
каждой
общее
количество основных операций в худшем случае равно 2 2* n* (n-l) = 2 n - 2*n.
*
Следовательно, в
худшем
случае
сложность алгоритм
сортировки
2
методом пузырька равна о(n ) . Лучшим считается вариант, когда исходные данные уже упорядочены.
В этом случае алгоритм сортировки методом пузырька сделает только один
проход, выполнив n-l сравнений, и ни одной перестановки.
21
Сортировка методом вставок
Представьте себе колоду карт, из которой каждый раз вынимается и вставляется на указанное место одна карта. Такой способ упорядочения называется сортировкой методом вставок В
данном
случае
массив
(insertion sort).
делится
неупорядоченную, как показано на рис.
на
две
части:
упорядоченную
и
6.
Вначале весь массив неупорядочен. На каждом шаге метода вставок из
неупорядоченной
части
извлекается
первый
элемент,
вставляется в нужное место упорядоченной части. переместить
элемент
из
theArray{O}
который
затем
Первый шаг тривиален:
неупорядоченной
части
в
упорядоченную. Для этого даже не нужно переставлять элементы массива.
Следовательно,
theArray[O}
этот
уже
шаг
можно
считая,
что
элемент
принадлежит упорядоченной части, анеупорядоченной
частью массива является отрезок в
пропустить,
theArray[l ... n-l}.
упорядоченной части расположены в
Тот факт, что элементы
порядке возрастания, является
инвариантом алгоритма. Поскольку на каждом шаге размер упорядоченной части
увеличивается
соответственно, на
на
единицу,
а
размер
единицу уменьшается, в
неупорядоченной
части,
момент окончания алгоритма
весь массив окажется упорядоченным. Упорядоченная часть
Неупорядоченнаячасть
=
..
л--_
~_----~----_'" r_---
[ I ] ...
о
[I]
п-l После
РИСУНОК
6.
Сортировка
На рис.
7
i
итераций
методом
вставок
разбивает массив на две части
показаны результаты сортировки массива, состоящего из
пяти целых чисел, методом вставок. В исходном положении упорядоченная
часть массива состоит из единственного элемента к
неупорядоченной
части
относятся
все
theArray[O},
остальные
равного
элементы
Извлечем из неупорядоченной части массива ее первый элемент
-
и
вставим
понадобится вставляемого
в
соответствующее
сдвинуть числа.
элементы Снова
место
упорядоченной
массива, извлечем
чтобы
неупорядоченной части массива ее первый элемент
вновь
-
в соответствующее место упорядоченной части и т.д.
22
части.
освободить
из
-
число
29,
а
массива. число Для
место
1О
этого
для
образованной
14 -
и вставим
Исходный массив:
Скопировать
Сдвинуть
1О
29
Вставить 10;Скопировать
Сдвинуть
14
29
Вставить 14;Скопировать
37
Скопировать
Упорядоченный массив:
Рисунок
7.
37,
оставить
на месте
13
Сдвинуть
37,29, 14
Вставить
13
Сортировка массива, состоящего из пяти целых чисел, методо . и вставок
Рассмотрим функцию на языке С++, выполняющую сортировку массива,
состоящего
из
n
элементов, методом вставок.
void il·1/j'erti()flS(),.{(DatcIT.J.pe thl::/l,"r{I)'/j,
[п! 1'1)
//-------------------------------------------------------- // Упорядочиваетэлементы массива в возраспшющемпорядке.
// Предусловие:массив thеАгrа)' состоит из п элементоа.
// Постусловие:массив lhe.4rJf(lJ/ упорядочен по возрастанию:
// число п остается без изменения.
/ / --------------------------------------------------------- /
I
// ипкопеа :::::: индекс первого элемента неупорядоченной части; //70С = индекс ячейки упорядоченной части. в которую
// производитсявставка; // пехшет == следующий элемент псупорядоченпои части. // 13 исходном положении упорядоченная часть состоит // uз единственногоэлемента thе-,L11;4ГС~1/i(~l,
// неупорядоченнойчастью . массива
является отрезок
// Ihе~4ГГ{IJ}[I..n-l/. В обще. н случае упорядоченной частью //массива является отрезок tJle~4 пау]') .. ипзопеа-Г],
// а неупорядоченной
/or (int
ипзопеа ~
отрезок the./l1J']4(1)'[llJlsorle(/. n-I
1: ипзопеа «:
п. --т---+-unsО,..lеd)
23
J
f
l
Находим индекс нужной ячейки
// tll(!,,4гrо.у[О..un..зопеа]
(1ос)
в отрезке
для вставки Э.7С ..мента
// the/lr,.a}'[unsorledj. являющегося первым элементом // пеупорядоченнойчасти; при необходимостивыполняем // сдвиг элементов. освобождаяместо для вставки. Еляа'Гуре пехшет -= f}le~4"/Y1))/unso,.tedl~" [о«
(:(loc >-
О)
int /ос
=
ипзопеа:
(111е.Агrll):[юс-! J~> пехшет) :--lo(~
&&
// (7()(114-~?(Je)\4 отрезок tlll!/lГГ(lJ'[!()('-lJ// вправо tllеАГ"СlуI1()(~./ -==
tJlcA J'*гау!1()с~-Г],
/гДиигностическоеутверждение: llчеiiка'//tI1еАГ1·(,))[I()с~.1 содержит элемент пехлИет
//
J
Вст авляем э.н е./\'1е: i т п е.Х t 1t е 111
11
-{.'
Z~
/' // n(JflCZf 1(ик.. па J()r
Анализ. Внешний цикл в функции
insertionSort
выполняется
n-l
раз. Этот цикл содержит внутренний цикл, который выполняется не больше чем
unsorted раз
для значений переменной unsorted, изменяющихся от
n-l.
Таким образом, в худшем случае алгоритм выполняет
1+ 2+ ... + (n-l) == n
* (n-l )/2 сравнений.
1 до
Кроме того, в худшем случае
столько же раз внутренний цикл сдвигает элементы.
Во
внешнем
выполняется
цикле
дважды,
перемещение
т.е.
в
сумме
элементов
2 *(n-l)
раз.
на
каждой
итерации
Итак, подведем итог:
в
наихудшем варианте выполняется.
n
* (n-l) + 2 * (n-l)
2
== n + n 2
основных операций.
Следовательно,
в
худшем
случае
сложность
алгоритма
сортировки
~
методом вставок равна О(n"). Для небольших массивов, скажем, содержащих не
более
25
элементов, алгоритм сортировки методом вставки предпочтительнее,
поскольку он понятнее остальных алгоритмов. Однако для больших массивов
этот метод совершенно неэффективен.
Сортировка слиянием Два важных алгоритма сортировки, основаны на принципе «разделяй
и властвуй», сортировка слиянием и быстрая сортировка, имеют элегантное рекурсивное воплощение и чрезвычайно эффективны.
24
Мы рассмотрим сортировку массивов методом слияний. Формулируя
алгоритм,
будем
пользоваться
обозначением
отрезка
массива
theArray{firat ... last}. Алгоритм сортировки методом слияний является рекурсивным. Его
эффективность не зависит от порядка следования элементов в исходном массиве.
Допустим,
что
мы
разделили
массив
пополам,
рекурсивно
упорядочили обе половины, а затем объединили их в одно целое, как показано на рис.
3>
8.
На рисунке показано, что части массива
объединяются в
стоящие
массив
<1, 2, 3, 4, 8>.
в разных частях массива,
меньший
элемент
отправляется
попарно
во
В
<1, 4, 8>
и
<2,
ходе слияния элементы,
сравниваются
временный
друг
массив.
с
другом,
Этот
и
процесс
продолжается до тех пор, пока не будет использована одна из двух частей массива.
Теперь достаточно просто скопировать оставшиеся элементы
временный
массив.
В
заключение
содержимое
временного
во
массива
копируется обратно в исходный массив.
theArray:
' - - - - _ - - - - ' - - - _ - - - - - - ' -_ _. . . . L . . - - _ - - - - ' - - _ - - - - - - '
Исходный массив
Делим массив пополам
'-------7---'---------'
Упорядочиваемполовины массива
а Объединяем половины а)] <2. поэтому копируем
] из theArray копируем 2 из
левой
половины в массив б)4>2.по')'гому
правой половины в массив тпс Аггау в)4>3,поэтому копируем
3
правой половины в массив
из
theArray
г)Правая половина исчерпана. остаток левой половины копируем А .---_--.----L-----,-_=-------.-----:г_ _т-_ _'\=--,в массив
tempArray
Временный массив
тегпр Аггау
Копируем содержимое временного массива
назад в исходный массив
theArray: РИСУНОК
8.
Сортировка методом слияния с помощью вспомагательного массива
Хотя остается
в
результате
неясным,
Сортировка
как
слиянием
слияния
возникает
выполняется
упорядоченный
сортировка
выполняется
рекурсивно.
на
предыдущих
Ее
псевдокод
следующий вид.
merge..чопитош
theArrcly.·lte}n./:l}·l·(l.~'. Ган), il1Iep;er) ,// Упорндочиеает
in ./zrsl: i11te~er, i71 отрезок
1/
tJ1с1'4}··ГСl..-V [fir.\'t.. Га .\ 'I),
25
массив, этапах.
имеет
сортируя первую половину массива:
// Z)
// 2)
сортируя 61'n()p)'1() половину , массива:
// _1)
объединяя две упорядоченные
//1 1'10J/OBU1-1 ы массива.
ij-
(!lГSl
<~
I(Jst)
(
l
1111(:! == (fiг~,;'t + las~),/2
// Определяем серед и ну ,
,// Сортируем отрезок /1 т t пеАз-па, '[!Z}'"5;t.. т ilij
т erge.s'()rt(tI1C/4 кпау, .fl1~,,'t, тла) /~/ Сортируем (JI'1'11)(!3()K
t h сА г}"' (1)' [т i(1-+- 1.. 1с' s') т f? Г<-«е s о Г' t (! пс.! ,..,--(J) i, П1 i(I + 1.
[ая«)
,// Объед 1111>1 е./\;l упоря до чен 1-[ ы е отрез !{11 ///11(;./1 }'"I'"(J},[111'"8 {.. П'1 i{}J 1heA rr(l~~'" ["т 1(J-t- J .. /(1 S 1) n1el .llr.\'I, т и], /CI8~)
} // Конец оператора j!~
/'l Е(~.Л иПг»t > ~= Га» 1, операции з авериссн ы
н
Af{e(the/11"·(I)J,
Совершенно выполняются
на
ясно, этапе
что
основные
слияния,
и
все
же,
операции почему
в
этого
результате
алгоритма возникает
упорядоченный массив? Рекурсивные вызовы продолжают разделять части массива
пополам,
Очевидно,
что
пока
в
массив,
них
не
останется
состоящий
из
только
одного
по
одному
элементу.
элемента,
является
упорядоченным. Затем алгоритм объединяет фрагменты массива, пока не образуется один упорядоченный массив. Рекурсивные вызовы и
mergesort
результаты слияний
проиллюстрированы на
функции рис.
примере массива, состоящего из шести целых чисел. Рекурсивные вызовы
I I
I I
\
I I
I
/ \
/
~ <,
<, <,
I I
.............. I
/
/
~ <,
<,
Слияние
<,
<, <,
РИСУНОК
9. Сортировка методом
СЛИЯНИЯ массива, состоящего из шести целых чисел
26
9
на
Ниже
приведена
функция
на
языке
С++,
реализующая
алгоритм
сортировки методом слияний. Для того чтобы упорядочить массив состоящий из
(theArray,
О,
элементов, выполняется рекурсивный вызов
n-l
theArray, mergesort
n-l).
const int М'А): SlZE = JW(IК(~li.Л1(IJlыt()t:!-/{·()Jllл-че(~гrl()()-.ЭJlе.Л1е11111()(~-)W(I(~С~ll(j(J; void meT"cRe([)alaT))!Je tI7e..:·4r""Cf))/·.. /, int .fir,st, i"t тл а, int last) ,// Обьединяет
два упорядоченных отрезка Ille!Jl·~J'{J)iflтos't .. 111i(i] и
/>'
thеАr,.а~У[Jпid·+-1 .. Та.«] в один упорядоченный массив.
// Предусловие:jirst ~<~ == 111iJ <~::::::. Так: Оба подмассива
/1 ,1'1e./1rra.\;'[/i,4.\,t..пиа] и the./4J,,"clJJ[nlid+·j .lаs~1.')fl0!)ядочеНbl
>
// по возрастанию.
1/ Постусловие:
отрезок t-}1е~'1гrа.1)[first. ./(15;1] упорядочен.
// Замечаниео реализации:функция выполинет слияние дв}'х
// подмассивовво временныймассие. а ](l1}и!.1-1 копирует его
// содержимоев исходный . массив fJ1е/lгr{~1/.
1/------------------------------------------------------------------------------------------------------- f
i
// Инициализируемлокальные индексы, выделяя подмассивы ;nt./iгstl
::::::.firs/, - // Начало первого подмассива
йи Та.\:tl
111id,'
inl./iгst1
-=
int !ClstJ
пиа
» 1: /:>' Н{lчаJZО
// Конец второго подмассива
10s/,'
=:
в/110!)О(?О подмассива
/~/ Пока оба подмассива не пусты. копирус.н .меньшии элемент
/гео временный массив
inl таех
-== ..111;0.\"11;
Следующая свободная ячейка
// массива teml).l41Itr(~."
for (; (fil">c\'/ 1 <~==
[ая! 1)
&&
(jiгst2 <:=:~ /(lsI2): ++'inc!ex)
I l
// Инвариант: 0I11jJC301\' // упорядочен.
tеnl/JА,·га)'[fi,"stl..[паех-Г]
ij(theL'117 {,IJ'l f irslZ} .< 111е~/lгrа.);ifiГSI2]) 4
the..41"/"l.1)/{fiT".\
t·enlf)·~41·,·4c'.y/i}1(fe.x~.1
.+ I
"+~fi У.\' t 1:
1
el..ке (
l
temp.A r пау /} паех] ..+-. '+:/1 rs t 2,'
=
'11е ~4 ккау
Ifi1·5' t ~.I,.
} // Конец оператора ff) /! Конец оператораюп Скопировать подмассив. оставшиися непустым
// Скопировать первыи подмассив. если необходимо
27
!()Г (; ,/iгstl <~ =- Газ: 1,. +- 4~fi"$·11. -+- -+ таех)
// Инвариант: отрезок tеmjJ.4Г'lt+lIJ'l/;,-stl.. таех-Г]
// упорядочен.
lеn1/JL4ггu}'[indех]о::::: lhеL1Г/'llJ'[jiгstlj :
//(/"1\оnиров{11nЬ в1110РОЙ подмассив. ее.'! и необходимо
/
'
(,:.I/I"sl -.. 2
. ()I·
. dС.Х)' +-zn
'") +- '+-. j"[Jtf.,'t..:;,
1..., <~= lQsf.:.,-
// Инвариант: отрезок tеm/J/1Гlt+{1.}'Lii,.stl.. таех-Г]
// упорядочен
(етрА rlt"(l)'[index]
11'zеL1'·ГО)i[/iг.\'12],·
// Копируемрезультат обратно в [о»
==,!IГSI; таех
(index
1пеА пауйпаех]
исходныймассив
-<-= [ах): -+ +indc.x)
I е nlp~'<11~/~(lJ'[in(fex~j,'
} // Конец функции тепг«
1,'о;t!111егgеsоrl(Гииа'Гуре Ihe-<4rf·С~);'f], int.iirst.
int
I(ISI)
// -------------------------------------------------------------------------- // Упорядочиваетэлементы массива в возрастающсмпорядке. // Предуслов ие: отрезок l}lе/:1г·rс/у·[/ZI·SI".. Гаы] массив, /г Постуслояие:
.t'1/1aC
O(:116
lJlеАГI·С1J'[fiг.\·I.. lа,,\'tl
упорядочен
// по возрастанию. // Вы зываемые функции: тепге. ~/ -------------------------------------------------------------------------- (
1
//
Упорядочиваем каждую из частей массива
Гп: тй!
/;/ Объединяем
(fi1",,~)t
+ 1(},\'I)/2;
// Ин дек. .с среднего эл ем ен т а //' Упорядоч116 аем лев. , чо 1]().71()t) 111(1' 1/1CА /'/'а..\' {fi,·s[.. /'11 i('-I П1егgе5;()rl(1hеАгга)', .11,",S1, пи а); // Упорядочиваемправую полоеину thej4/·1"(!Y'[nlid--+-l .. 1{15;~1 lnel·f,;e.\'()1"/(lheArrcl.J', тл а : 1, I(/~""'[)_:
==
две половины
теег« (tlle~4 ккау.
.,firs1,
/11
id,
'а5;1):
} /~/ Конец операт ора ~'f' } // Конец функции nlеrкс>-\'()гl Анализ.
Поскольку
основные
операции
в
этом
алгоритме
выполняются на этапе слияния, начнем анализ с него. На каждом шаге
происходит объединение подмассивов theArray
/mid+ 1.. .last}.
[first. . . тла]
и
theArray
На рис.lО показан пример, в котором требуется выполнить
максимальное количество сравнений. Если общее количество элементов объединяемых отрезков массива равно выполнить
n-l
состоящий
из
сравнений. шести
n.,
то при их слиянии потребуется
(Например,
элементов,
на
рис.
следовательно,
1О
показан
массив,
выполняется
пять
сравнений.) Кроме того, после сравнений осуществляется копирование
n
элементов временного массива в исходный. Таким образом, на каждом шаге выполняется
3 *n-l
основных операций.
28
first
mid
last
l-t-h-еА-гг-а-у:-~Г--с-л-и-я-н-и-:е-п-о-л-о-в-и-н-: ----'1 а)
а
1<4"\ поэтому копируем 1 из подмассива
theArray[first..mid] в массив tempArray
б) 2<4~ поэтому копируем 2 из подмассива
theArray[first..mid] в массив tempArray
в) 8>4., поэтому копируем 4 из подмассива
theArray[mid+ l ..last] в массив tempArray г) 8>5, поэтому копируем 5 из подмассива theArray[mid+ l ..last] в массив tempArray
б
tempArray: д)
8>6"\ поэтому копируем 6 из подмассива theArray[mid+ l ..last] в массив tempArray ж) Подмассив theArray[mid+ 1..last] исчерпан, поэтому копируем 8 в массив tempArray
РИСУНОК 10. Наихудший случай на этапе слияния
в
ФУНКЦИИ
mergesort
выполняются два рекурсивных вызова. Как
показано на рис.П, если исходный вызов функции
mergesort
принадлежит
нулевому уровню, то на первом уровне возникают два рекурсивных вызова.
Затем каждый из этих вызовов порождает еще два рекурсивных вызова второго уровня и т.д. Сколько уровней рекурсии возникнет? Попробуем их подсчитать.
Уровень О
8
Уровень
4
1
Уровень О
Уровень О
1 Уровень О: вызов функции
Уровень
1: вызов
функции
Уровень О: вызов функции Уровень О: вызов функции Рисунок
11.
1
mergesort для 8 элементов mergesort для 4 элементов mergesort для 2 элементов mergesort для 1 элемента
Уровни рекурсивных вызовов функции
mergesort при сортировки массива,
состоящего из восьми элементов
Каждый вызов функции этапе
исходный
массив
mergesort
оказывается
29
делит массив пополам. На первом
разделенным
на
две
части.
При
следующем рекурсивном вызове функции снова
делится
пополам,
образуя
mergesort
четыре
части
каждая из этих частей
исходного массива.
При
следующем рекурсивном вызове каждая из этих четырех частей опять делится
пополам,
образуя
восемь
частей
массива,
и
т.д.
продолжаются до тех пор, пока части массива не
Рекурсивные
вызовы
станут содержать только по
одному элементу, иными словами, пока исходный массив не будет разбит на п частей, что соответствует количеству его элементов, если число п является
степенью ДВОЙКИ (n=2 ) , глубина рекурсии равна k=log2.n. Например, как показано на рис.ll, если исходный массив содержит восемь элементов (8=23 ), то глубина рекурсии равна 3. Если число n является степенью двойки, глубина рекурсии равна 1+ Iog 2n (округленноезначение). k
Исходный вызов функции функции
merge
только
один раз. Затем функция
слияние п элементов, выполняя
О)
обращается
к
осуществляет
merge
операций. На первом уровне рекурсии
3*n-l
выполняются два вызова функции
merge.
(уровень
mergesort
mergesort
И, следовательно, функции
Каждый из этих двух вызовов приводит К слиянию
элементов и
n/2
требует выполнения выполняется
3*(n/2)-1 операций. Таким образом, на этом уровне 2*(3*(n/2)-1 )==3*n-2 операций. На т-м уровне рекурсии
выполняются 2т вызовов функции К слиянию
В
целом,
merge.
Каждый из этих вызовов приводит
n 12т элементов, а общее количество операций равно 3*(nI2 n1)-2. 2т
рекурсивных вызова функции
merge
порождает
3*n-2
т
операций. Таким образом, на каждом уровне рекурсии выполняется О(n) операций. Поскольку количество уровней рекурсии равно
в наихудшем
и среднем
вариантах
функция
Iog 2n или Iog 2n
mergesort
+ 1,
имеет сложность
O(n*log2n). Посмотрите на рис.3 и еще раз убедитесь, O(n*log2n) растет намного быстрее, чем величина о(n2) .
что
величина
Хотя алгоритм сортировки слиянием имеет чрезвычайно высокое
быстродействие, у него есть один недостаток. Для выполнения операции
Объединить упорядоченные подмассивы
theArray[first
mid]
theArray[mid+l
и
last]
необходим вспомогательный массив, состоящий из
n элементов.
Если объем
доступной памяти ограничен, это требование может оказаться неприемлемым.
Выстряя сортировка Рассмотрим
два первых
шага
решения
задачи
о
поиске
k-ro
наименьшего элемента массива theArray[fist ... last}, Выберите в массиве thеАrrаvfПrst...
Поделите массив
last [опорный элемент р theArray[first... last] относительно элемента р
Разбиение, показанное на рис.
множества
12,
характеризуется тем, что все элементы
S 1 == theArray [first... pivotlndex-1}
30
меныпе опорного элементар,
а множество
S2=theArray [pivotlndex+ 1. . .last]
состоит из
элементов,
больших или равных опорному. Хотя из этого свойства не следует, что массив
упорядочен, из него вытекает чрезвычайно полезный факт: если массив упорядочен, элементы, стоящие на
позициях от
до
first
pivotlndex-l,
остаются на своих местах, хотя их позиции относительно друг друга могут
измениться. Аналогичное утверждение выполняется и для элементов, стоящих
на позициях от pivotlndex+ 1 до
last.
Опорный элемент в полученном упорядоченном массиве останется на своем месте.
Такое
разбиение
алгоритма.
Разбиение
порождает
две
леВОЙ(Sl)
и
задачи
правой/Б-)
массива массива
определяет относительно
сортировки
частей
рекурсивный опорного
меньшего
массива.
элемента
размера-
Решив
эти
характер р
сортировка
две
задачи,
мы
получим решение исходной задачи.
82
>=
<р
fiгst РИСУНОК
Р
pivotlndex
12. Разбиение
Иными
last
относительно опорного элемента
словами,
разбиение
массива
перед
рекурсивными
оставляет опорный элемент на своем месте и гарантирует, что
вызовами
левый и правый
отрезки массива окажутся упорядоченными. Кроме того, алгоритм быстрой сортировки конечен: размеры левого и правого отрезка массива меньше размера
исходного массива, причем каждый шаг рекурсии приближает нас
когда массив состоит из одного элемента.
к базису,
Это следует из того факта, что
опорный элемент р не принадлежит ни одному из массивов
8!
и
82.
Псевдокод алгоритма быстрой сортировки выглядит следующим образом. qиiсksоrt(inоиt
theArray.·ltemArray,
in first: integer, in last: integer)
// Упорядочиваетмассив theArray[first.. Zast]
(first < last)
If
Выбрать опорный элементр из массива
Разбить массив
theArray[first.. Zast]
theArray[first.. Zast]
относительно
опорного элементар
// Разбиение имет //
вид
theArray[fir.st..pivotlndex. . Zast]
Упорядочиваеммассив
81
first, pivot Index-1)
// Упорядочиваем массив 82
qиiсksоrt (theArray, pivotZndex+l, Zast)
qиiсksоrt(thеАl/8rау,
31
} // Конец оператора if
// еслиfirst >= Таз», ничего не
делаем
kSmaZZ(in k:integer~ in theArray:/temArray,
in first: integer~ in Zast: integer): Item Туре
// Возвращаетзначение k-го наименьшегоэлементамассива
// theArray{first.. Zast}. Выбрать в массиве
Разбить массив
опорный элемент р
theArray[first.. last}
относительно элемента р
theArray[first.. Zast}
If (k < pivotZndex - first + 1)
return kSmaZZ(k~ theArray~ first, pivotZndex-Z)
else if (k = = pivotZndex - .first + 1)
return р
else
return kSmaZZ(k-(рivоtZndех-:fir . s,t+I}, спе.Ат-ау.
pivotlndex+l, last) Функция частей
kSmall
массива
вызывается
содержит
рекурсивно,
искомый
элемент.
только если одна из
Если
этим
элементом
является опорный, функция не вызывается вообще. В то же время
функция
quicksort
вызывается рекурсивно для обеих частей массива.
Различия между этими двумя функциями проиллюстрированона рис.13
kSmall(k, theArray, first, last) или
kSmall(k, theArray, first, pivotlndex-1)
kSmall(k-(рivоtlndex-first+ 1),
theArray, pivotl ndex+ 1, last)
quicksort(theArray, first, last)
quicksort(theArray, first, pivotlndex-1)
РИСУНОК
13.
quicksort(theArray, first, pivotlndex+1,last)
Сравнение фУНКЦИЙ k~Sтall и qиiсkSО1~t
Использование
инварианта
в
алгоритме
разбиения.
Рассмотрим функцию разбиения массива, которая должна вызываться
функциями
kSmall
и
quickSort.
В обоих алгоритмах именно разбиение
массива представляет собой наиболее трудную задачу.
32
Функция, предназначенная для разбиения массива, получает в
качестве аргумента отрезок распределить
Функция должна
theArray[first. .last}.
элементы
массива,
руководствуясь
следующим
правилом: в множество Stвключаются элементы, меньшие опорного, а в множество
является
отрезком
множество S2 выбрать
остальные. Как показано на рис.12, множество
S2 -
массива
отрезком
опорный
theArray [first. .pivot Index-l}, а массива theArray [pivotlndex+l. . last}. Как
элемент?
Если
элементы
произвольном порядке, в качестве опорного элемент,
S1
например
theArray [first}.
массива
можно
записаны
выбрать
в
любой
(Более детально процедура
выбора опорного элемента будет рассмотрена позднее.) При разбиении
массива
опорный
элемент
удобно
помещать
в
ячейку
theArray
независимо от того, какой именно элемент выбран в качестве
[first},
опорного.
Часть
массива,
в
которой
распределенные по отрезкам рассмотрим
firstUnknown
массив,
и
last
и
82,
изображенный
на рис.14.
не
Итак,
first, lastS 1,
theArray
неизвестны.
82
<р
Неопределенная часть
?
>=р
first
firstUrlknown
Last8 1 14.
Индексы
элементами неопределенной части
элемент
РИСУНОК
еще
называется неопределенноЙ.
Опорный
р
элементы,
разделяют массив на три части. Отношения между
опорным элементом и
{firstUnknown.. last]
81
находятся
last
Инвариант алголритма разбиения
в процессе разбиения массива должно выполняться следующее условие.
Элементы множества элементы множества
81 82 -
должны быть меньше опорного элемента, а больше или равны ему.
Это утверждение является инвариантом алгоритма разбиения. Для того чтобы в начале алгоритма выполнялся его инвариант, необходимо проинициализировать
индексы
массива так,
опорного элемента, считался неопределенным.
1а s t S 1 == fi r s t
first Unknown == first + 1
33
чтобы весь
массив,
кроме
Исходное
состояние
массива
изображено
на рис.
15.
Неопредепенная часть
?
р
fiгst
fiгstUnknown
last
lastS1
Рисунок
15.
Исходное состояние массива
На каждом шаге алгоритма разбиения проверяется один элемент из неопределенной части. В зависимости от его значения он помещается в
множество
8]
неопределенной останавливается,
или
82.
части
Таким
образом,
уменьшается
на на
когда размер неопределенной
нулю, т.е. выполняется условие
каждом единицу.
firstVnknown > last.
(inоиt theArray:ltemArray,
in first: integer, in last: infegel",
оиt pivotlndex: integer)
// Разделяет массив theArray[first.. last}
// Инициализация Выбрать опорный элемент и поменять его местами
с элементом theArray[first}
р =
theArray[fir5.~t}
// р -
опорный элемент
// Задаем пустые множества 8 } и 82.,
а неопределенную
// часть массива инициализируемотрезком
// theArray[fir,st+ 1 .. Газ«]
Za"f)tSZ == ./ir,st
firstUnknown == first + 1
// Определяеммножества 8]и S2. while (firstUnknown < = last) ( // Вычисляем индекс самого левого элемента
// неопределеннойчасти массива
if (theArray[firstUnknown) < р)
Поместить элемент theArray[firstUnknown} else
в
Поместить элемент theArray[fir,-~;tUnknоwnJ в
} // Конец оператора while
34
S].
S2.
размер
Алгоритм
части становится
Рассмотрим псевдокод этого алгоритма.
partition
шаге
равным
// Ставим опорный элементмеждумножествами8/ и 82. //
и запоминаем его новый индекс
Поменять местами theArray
и
[first}
theArray [lastSZ}
pivotZndex == ZastSl Алгоритм
достаточно
прост,
разъяснения. Рассмотрим два
но
операция
перемещения
требует
возможных действия, которые необходимо
выполнить на каждой итерации цикла while. Поместить Множество
8/
элемент
И
theArray
в
[firstUnknown}
неопределенная часть,
как
множество
правило,
смежными.Обычно между ними располагается множество операцию
можно
выполнить
thеАrrау[firstVnknоwn}можно множества
S2., т.е.
более
поменять
не являются
82.
Однако эту
эффективно.
местами
с
8/.
первым
Элемент
элементом
с элементом
theArray[lastSI+ l}, как показано на рис. 16. Как быть с элементом множества 52, который был помещен в ячейку theArray[firstVnknown}? Если увеличить индекс firstVnknown на единицу, этот элемент становится самым правым в множестве 82. Таким образом, для переноса элемента theArray [firstUnknown} в массив 8/ необходимо выполнить следующие шаги.
Поменять местами элементы theArray[firstlnknown} и
theArray[lastSl+ 1] Увеличить индекс ZastSZ на единицу Увеличить индекс firstUnknown на единицу
Эта стратегия остается верной, даже если множество
случае величина
last81 + 1
равна индексу
firstUnknown,
S2
пусто. В этом
и элемент просто
остается на своем месте. Инвариант при этом не нарушается.
Поместить элемент операцию
легко
theArray[firstUnknown}
выполнить.
Напомним,
что
в
множество
индекс
крайнего
82.
Эту
правого
элемента множества
82 pabehfirstUnknown-l, т.е. множество S2 инеизвестная часть являются смежными (рис. 17). Таким образом, чтобы переместить элемент theArray [firstUnknown} в множество 82, нужно просто увеличить индекс firstUnknown на единицу, расширяя множество 82вправо.Инвариант
при этом не нарушается.
После переноса всех элементов из неопределеннойчасти в множества S 1 И
82
остается
решить
между множествами
последнюю
8/
и
82.
задачу.
Нужно
поместить
опорный
Обратите внимание, что элемент
является крайним правым элементом множества
35
S/.
элемент
theArray [last81}
Перестановка
82
81 _
А
~
<,
(
• •
>=р.
<р
first
Last8 1
/ -----------_/
~
------
-------
--------.,
(
у
р
Неопределенная часть
\
?
>=р
firstUnknown
Last81 +1
last
Аау[firstUnknО"И
Рисунок
Jn}в множестве Sl перестановкис 16. Перенос элемента thеАrl элементом theArray[lastSI+ 1) с последующим увеличением индексов lastSI иjirstUnknоwn на
единицу Опорный
82
элемент
Неопредепенная часть
/----------
_._----~
\
( <р
р
first
?
>=р
firstUnknown
Last8 1
Рисунок
17. Перенос элемента thеАrrау[firstUnknо"И)n}в индекса jirst Unknown на единицу
множестве
S2
last
после увеличения
Если поменять его местами с опорным элементом, тот станет на
правильное место. Следовательно, оператор
pivotlndex =
ZastSI
позволяет определить индекс опорного элемента. Этот индекс можно
использовать в качестве границы между множествами 8/ и
82..
Результаты
трассировки алгоритма разбиения массива, состоящего из шести целых чисел, когда опорным является первый элемент, показаны на рис.
36
18.
Опорный элемент
Исходный массив
Опорный
Неопределеннаячасть
элемент
fiгstUnknown
38
=1 (указывает
на
принадлежит множеству
38) 82
Неопределеннаячасть
Множество
81
пусто;
принадлежит множеству
12
поэтому меняем местами
81 38 и 12 J
Неопределенная часть
27
12
38.
81
52
39 принадлежит множеству 82
Опорный
элемент
Неопределенная часть
27
принадлежит множеству
52
Опорный элемент
81
27
16
12
принадлежит множеству
поэтому меняем местами
81, 38 и 16
Опорный элемент
52
81
27
12
16
Множества 51 и
39
82
определены
)~ ~ ::I:
0(')
81 Полное разбиение
РИСУНОК
16
Ф
Q..:! о ф с: r:::;
12
27
Помещаем опорный элемент
39
между множествами 81 и
18. Первое разбиение массива, Прежде
чем
82
когда опорным является первый элемент
приступить
к
реализации
алгоритма
быстрой
сортировки, проверим корректность алгоритма разбиения, используя его инварианты.
Инвариант
цикла,
входящего
в
алгоритм,
имеет
следующий вид.
Все элементы множества
S/. (theArray[first+ 1..last S/}) меньше опорного, а все элементы множества S2 (theArray [lastSI.. /irstUnknown-1J) больше или равны опорному Напомним,
что
для
определения
правильности
алгоритма
помощью его инвариантов, необходимо выполнить четыре шага.
37
с
1.
Инвариант
должен
выполнения
быть
цикла.
В
истинным
с
алгоритме
самого
начала,
разбиения
до
опорным
элементом является
theArray [first},неизвестной частью отрезок массива theArray [first+l.. last}, а множества 8/ и 82. пусты. Очевидно, что при этих условиях инвариант является истинным.
2.
Итерации
цикла
не
должны
нарушать
инвариант.
Иными
словами, если инвариант был истинным перед определенной итерацией цикла, он должен оставаться истинным и после ее
выполнения. В алгоритме разбиения каждая итерация цикла
переносит один элемент из неизвестной части в множество или
82,
8]
В зависимости от его значения по сравнению с опорным.
Итак, если до переноса инвариант был истинным, он должен сохраняться
3.
и после переноса.
Инвариант
должен
определять
корректность
алгоритма.
Иными словами, из истинности инварианта должна следовать
корректность
алгоритма.
прекраrцается, пустой.
когда
В
множеству
случае
.last]
81 , л и б о
корректности
алгоритма
разбиения
область
становится
неопределенная
этом
theArray[first+ 1.
Выполнение каждый
должен
множеству
инварианта
82.
следует,
элемент
отрезка
принадлежать
В
любом
что алгоритм
либо
случае
из
достиг своей
цели.
4.
ЦИКЛ
должен
показать,
что
быть
конечным.
выполнение
Иными
цикла завершится
словами, после
нужно
конечного
числа итераций. В алгоритме разбиения размер неопределенной части
на
каждой
Следовательно, итераций
итерации
после
уменьшается
выполнения
неопределенная
часть
на
конечного
становится
единицу.
количества
пустой,
и
цикл
завершается.
Рассмотрим функцию на языке С++, реализующую алгоритм
quick8ort.B ней для выбора опорного элемента используется функция choosePivot, а функция swap работает, как и раньше, в алгоритме selection8ort. Для упор ния массива theArray, состоящего из n элементов, выполняется вызов quickSort(theArray, О, n-l). void
cho()~\'ePivot(DataTypetheArray[},
int..fir5;t, int ILМ·t)~·
;1;1 -------------------------------------------------------------------
// Выбирает опорный элемент для алгоритма быстрой сортировки.
// Меняет его местами с первым элементоммассива.
// Предусловие: отрезок theArray[first..last} - массив;
//first < == last.
// Постусловие:элемент theArra})[first} является опорным.
// ---------------------------------------------------------------------------- // Реализация этой функции предоставляетсячитателям.
38
void partition(Data Туре theArray[}, intjirst, int last, int& pivotlndex)
hI ---------------------------------------------------------------------------- // Разбиваетмассив для быстрой сортировки. // Предусловие: отрезок theArray[first..last} -)\1ассив; //jirst < == last. // Постусловие:массив theArray[first..last} разбит // следующимобразом: // S1 == theArray[{irst..pivotlndex-l] < pivot // theArray [pivotlndex] ==== pivot
// 82 == theArray {pivotlndex +1.. last] > = pivot
// Вызываемыефункции: choosePivot и swap. /
!/ ---------------------------------------------------------------------------- { // Помещаем опорный элемент в ячейку theArray[first}
choosePivot(theArray, jirst, last);
Гииа'Гуре
pivot
== theArray[first};
// Копируем // опорный элемент
// В исходном положениивсе элементы, кроме опорного,
// принадлежатнеопределеннойчасти массива
int lastSl == jirst; // Индекс последнего элемента
//Jwножесmваk-')l int jirstlJnknown ==jirst + 1,. // Индекс первого элемента // неопределенной части // Переносим элементы один за другим.
// пока неопределеннаячасть массива не станет пустой
/о, (: .firstUnknown < == last; + +jirst Unknown)
{
// Инвариант: theArray[first+ 1..last81] < pivot // theArray[lastSl + 1...firstUnknОИJn-l} > = pivot // Переносим элемент из неопределеннойчасти // в множество~S1 или S2 if (theArray[firstUnknown] < pi1t'of) f
l
// Элемент принадлежитмножеству81
++lastSI;
~}
swap(theArray[firstUnknown}, theArray[lastSZ]) ;
// Конец оператора if
//Иначеэлемент принадлежитмножеству82
} //Конец оператора.fOr
//Поставить опорныйэлемент на // и запомнить
соответствующееместо
его индекс
swap (theArray[first) , theArray[la\,t8l]);
pivotZndex = ZastS1,.
39
} //Конец функцииратпоп
void qиiсksоrt (DataType theArray {), int jirst, int last)
!/ ---------------------------------------------------------------------------- 1/ Упорядочивает элементы массива в возрастающем порядке.
// Предусловие: отрезок theArray [first .. last} - массив.
// Постусловие: массив theArray[first..last} упорядочен.
// Вызываемаяфункция: partition.
!/ ---------------------------------------------------------------------------- { int pivotlndex;
if (first < last)
(
// Создаемразбиение: 8L опорный элемент. S2 partition(theArray, Lfirst, last, pivotlndex): // Упорядочиваеммножества8/ и 82
qиiсksогt(thеАrrау, jirst, pivotlndex-l):
qиiсksоrt(thеАrгау, pivotlndex+l, last):
} // Конец оператора ~r }// Конец функции qиiсksоrt Дальнейший анализ покажет, что желательно избегать такого
выбора опорного элемента, при оказываются
пустыми.
Лучше
котором множество
всего
выбирать
Sj
опорный
или
S2
элемент
поближе к медиане массива.
Алгоритмы
guickSort и mergeSort «близки по духу», хотя в алгоритме guickSort основная работа выполняется до рекурсивных вызовов, а в алгоритме mergeSort после. Схему псевдокода алгоритма guickSort можно записать так. qиiсksоrt (inout
if
theArray: ItemArray, injirst .-integer,
т
last: integer)
(first < last)
{ Подготовить массив
theArray
qиiсksогt (отрезок
массива
.Sl
qиiсksогt (отрезок 82 массива
к рекурсивным вызовам
theArray)
theArray)
} // Конец оператора ij'
В
то
же
время общая схема алгоритма mergeSol-еt выглядит
следующим образом.
• mergesort (inout theA1
Aray:
If (first < Zast,· { mergesort (левая часть массива тепгезоп (правая часть массива Собрать массив,
и
от друга,
S2.
(пеАпау) (пеАпау)
quicksort
выполняется разбиение массива на
Затем алгоритм упорядочивает отрезки
поскольку
in Zast: integer)
~f·
Перед вызовом функции
8/
jirst: integer,
полученный после рекурсивныхвызовов
} // Конец оператора
части
[п
ItemArray,
любой
элемент
отрезка
40
8/
8/
и
82
независимо друг
находится левее любого
элемента отрезка
В функции
S2.
mergeSort,
наоборот, перед рекурсивными
вызовами никакая работа не выполняется. Алгоритм упорядочивает каждую из частей массива, постоянно учитывая отношения между элементами обеих
частей. По этой причине алгоритм должен объединять две половины массива после выполнения рекурсивных вызовов.
Анализ. Основная работа в алгоритме разбиения
массива.
неопределенной
{firstunknown] 82.
части,
каждый
необходимо
выполняется на этапе
элемент,
сравнивать
является
8/,
или
S2
принадлежащий
элемент
с опорным и помещать его либо в отрезок
Один из отрезков
элементом
Анализируя
quicksort
theArray
либо в отрезок
8/,
может быть пустым; например, если опорным
наименьший
элемент
отрезка,
множество
SI
останется
пустым. Это происходит в наихудшем случае, поскольку размер отрезка
при
каждом
вызове
функции
quicksort
82
уменьшается только на единицу.
Таким образом, в этой ситуации будет выполнено максимальное количество
рекурсивных вызовов функции quicksort.
Исходный массив
Опорный
Неопределенная часть
элемент
5
I\I~I
Опорный
элемент
5
82
6
Неопределенная часть
.t;:i\~
Множество
81
пусто
Множество
51
пусто
Множество
81
пусто
Множество
51
пусто
Опорный Неопределенная часть
элемент
5
6
7
· · :· · ·: ~. il : I
~~~(~{
:::!:;:
...i,.:;.:::.:.: :.:.:.:,:.·.i,:..•.:.:,.:!,:.'.;.:.I ....• :.I .•. .•.•. .
.
Опорный элемент
5
6
Опорный элемент
Первое
5
разбиение
6
4 Рисунок
19..
сравнения, Оперестановок
Разбиение массива в алгоритме qиiсksоrt в наихудшем варианте
При следующем рекурсивном вызове функции
partition
просмотрит
n-l
элемент. Чтобы распределить их
понадобится
функцией
quicksort по
функция отрезкам,
n-2 сравнений. Поскольку размер отрезка, рассматриваемого quicksort, на каждом уровне рекурсии уменьшается только на
41
единицу,
возникнет
quicksort выполняет 1 + 2 + ...+ (n-l) == n
n-1
уровней
рекурсии.
Следовательно,
функция
* (n-l)/2
сравнений. Напомним, однако, что при переносе элемента в множество
выполнять
перестановку
лишь изменить индекс
элементов
не обязательно.
82
Для этого достаточно
firstUnknown.
Аналогично, если множество
остается пустым, потребуется
п
*
при
82
каждом
рекурсивном
вызове
(n -1)/2 сравнений. Кроме того, в этом
случае для переноса каждого элемента из неизвестной части в множество
81
придется выполнять перестановку элементов. Таким образом, понадобится n
*
(n -1 )/2 перестановок. (Напомним, что каждая перестановка выполняется с помощью трех операций присваивания.) Итак, в худшем случае сложность
алгоритма quicksort равна О( n 2). Для
множества случае,
контраста
8]
и
когда
приблизительно
произвольном
82
на
рис.
продемонстрирован
20
пример,
когда
состоят из одинакового количества элементов. В среднем
множества
одинакового
порядке,
и
Sl
-
S2
состоят
количества
рекурсивных
из
одинакового
элементов,
вызовов
-
или
записанных
функции
в
quick801 потребуется меньше. Как и при анализе алгоритма merge8ort, легко показать, что глубина рекурсии в алгоритме quick80rt равна log2n или log2n+ 1. При каждом вызове функции quick80rt выполняется т сравнений и не больше, чем m перестановок, где т количество элементов в подмассиве, подлежащем сортировке.
42
At
Исходный массив
Опорный
Неопределенная часть
элемент
Опорный
Неопределенная часть
5,
элемент
5
3
iI
5,
52
Опорный
элемент
Неопределенная часть "
:~?:
, , "
.
3
5 Опорный элемент
8,
5
3
Неопределенная часть
82
Опорный элемент
5,
5
52
3
7
4 >S
:а
Множества
81
и
52
определены
.
:r:
Ф Q..~ О Ф
:I:
с
Первое
52
5
3
разбиение
с:
0(1)
51
7
Вставляем опорный элемент
между множествами
РИСУНОК
20.
51
и
52
Разбиение массива в алгоритме ашскзоп в среднем варианте
Формальный анализ алгоритма
quicksort для среднего варианта показывает, что его сложность является величиной O(n*log n). Таким образом, с большими массивами алгоритм quicksort работает значительно быстрее, чем алгоритм insertionSort, хотя в наихудшем варианте они оба имеют приблизительноодинаковое быстродействие.
Алгоритм массивов.
quicksort
Причина
его
часто используется для
сортировки больших
популярности заключается в
быстродействии, несмотря
на
исключительном
обескураживающие оценки
наихудшего
варианта. Дело в том, что этот вариант встречается крайне редко, и на
практике алгоритм
quicksort
отлично работает с относительно большими
массивами.
Значительное различие между оценками сложности в наихудшем
вариантах
выделяет
алгоритм
быстрой
среднем и
сортировки
среди
остальных алгоритмов, рассмотренных в данной главе. Если порядок записи
элементов в исходном массиве является "случайным", алгоритм работает,
по
крайней
мере,
не
хуже
43
любого
другого
quickSort
алгоритма,
использующего сравнения элементов. Если исходный массив совершенно не
упорядочен, алгоритм
quickSort работает лучше всех. Алгоритм mergeSort имеет приблизительно такую же эффективность. В некоторых случаях быстрее работает алгоритм quickSort, в других алгоритм mergeSort. Несмотря на то, что оценка сложности алгоритма mergeSort в наихудшем варианте имеет тот же порядок, что и оценка сложности алгоритма quickSort в среднем варианте, в большинстве случаев алгоритм quickSort работает несколько быстрее. Однако в наихудшем варианте быстродействиеалгоритма quicksort намного ниже. Сравнение алгоритмов сортировки На рис.21 показаны приближенныеоценки сложности алгоритмов сортировки, в наихудшем и среднем вариантах.
Сортировка
Наихудший вариант
Методом выбора Методом пузырька Методом вставок
Методом слиянием
Быстрая
2
n n2 n2 n*log n'l
Средний вариант
n2 n
2
n2 n*log п n*log п
п
РИСУНОК 2 J. Приближенные оценки сложности алгоритмов сортировки
Лабораторная работа «Методы сортировок» Цель рвботы Изучение алгоритмов сортировок и оценка их эффективности.
Задания на лабораторную работу «Методы сортировок»
Общее задание в
данной
лабораторной
работе
требуется
разработать
программу,
выполняющую следующие действия:
1. 2. 3. 4. 5.
Ввод размера массива (или двух
- в зависимости
от задания)
Ввод исходного массива (массивов) Вывод введенных массивов
Обработка массива (массивов) в соответствии с вариантом Вывод получившихся массивов
44
Замечания:
1) 2) 3) 4)
Количество элементов в исходных массивов до
20
ШТУК.
Элементами массивов являются целые числа.
Множества в программах не использовать.
После каждого изменения массивов новое состояние необходимо
вывести на экран.
5)
"Скопировать
элементы"
элементы
из
исходного
массива
исходного
массива
добавляются в результирующий массив.
6)
"Перенести
элементы"
элементы
из
добавляются в результирующий массив, после чего удаляются из исходного.
Ввривнты Ввести массив А. В массив В скопировать все элементы массива А
1. имеющие
четный
индекс
и
четное
значение.
Массив
В
отсортировать
по
убыванию, используя метод выбора. Ввести массив А. В массив В перенести все элементы массива А
2.
имеющие четный индекс и нечетное значение. Массив В отсортировать по возрастанию, используя метод пузырька.
3.
Ввести массив А. В массив В скопировать все элементы массива А,
имеющие четный индекс, слева от которых расположены элементы снечетным значением. Массив В отсортировать по возрастанию, используя метод вставок.
4. имеющие
Ввести массив А. В массив В перенести все элементы массива А, четный
индекс,
справа
от
которых
расположены
элементы
с
нечетным значением. Массив В отсортировать по убыванию, используя метод слияния.
5.
Ввести массив А. В массив В перенести все элементы массива А,
имеющие
нечетный
индекс,
нечетным
значением,
а
справа
слева
-
с
от
которых
четным.
расположены
Массив
В
элементы
отсортировать
с
по
убыванию, используя быструю сортировку.
6.
Ввести массив А. В массив В перенести все элементы массива А,
стоящие левее минимального элемента, и имеющие нечетный индекс. Массив В
отсортировать по убыванию, используя быструю сортировку.
45
7.
Ввести массив А. В массив В перенести все элементы массива А,
стоящие правее максимального элемента, и имеющие нечетный индекс. Массив В отсортировать по возрастанию, используя метод слияния.
8. стоящие
Ввести массив А. В массив В перенести все элементы массива А, между
минимальным
и
максимальным
элементами.
Массив
В
отсортировать по возрастанию, используя метод вставок.
9.
Ввести массив А. В массив В перенести все элементы массива А,
имеющие значение больше чем элемента массива, а тах
(min+max)/2,
- значение
где
min -
значение минимального
максимального элемента массива. Массив В
отсортировать по убыванию, используя метод пузырька.
10.
Ввести массив А. В массив В перенести все элементы массива А,
имеющие значение меньше чем элемента массива, а тах
-
(min+max)/3,
где
min -
значение минимального
значение максимального элемента массива. Массив В
отсортировать по убыванию, используя сортировку методом выбора.
11.
Ввести массивы А и В. В массив С скопировать те элементы, которые
есть и в массиве А и в массиве В. Из массива В удалить все четные элементы. Массивы А, В и С отсортировать по возрастанию, используя метод выбора.
12.
Ввести массивы А и В. В массив С скопировать те элементы, которые
есть в массиве А, но которых нет в массиве В. Из массива А удалить все нечетные элементы. Массивы А, В и С отсортировать по убыванию, используя
быструю сортировку.
13.
Ввести массивы А и В. В массив С скопировать те элементы массива
А, которых нет в массиве В, и те элементы массива В, которых нет в массиве А. Из массива В удалить все четные элементы. Массивы А, В и С отсортировать по возрастанию, используя сортировку методом
14.
слияния.
Ввести массивы А и В. В массив С скопировать те элементы массива
А, которых нет в массиве В, и те элементы массива В, которые встречаются в массиве А по крайней мере
2
раза. Из массива А удалить все элементы стоящие
левее минимального элемента. Массивы А, В и С отсортировать по убыванию, используя сортировку методом вставок.
15.
Ввести массивы А и В. В массив С скопировать те элементы массива
А, которые встречаются в массиве В по крайней мере массива В, которые встречаются в массиве А ровно
1 раз.
2
раза, и те элементы
Из массива А удалить
все элементы стоящие левее максимального элемента. Массивы А, В и С отсортировать по убыванию, используя сортировку методом пузырька.
46
16.
Ввести массивы А и В. В массив С перенести те элементы массива А,
которые меньше минимального элемента массива В, и те элементы массива В,
которые больше максимального элемента массива А.
Массивы А, В
и
С
отсортировать по возрастанию,используя сортировку методом выбора.
17.
Ввести массивы А и В. В массив С перенести те элементы массива А,
которые больше минимального элемента массива В, и те элементы массива В,
которые
больше максимального элемента массива А.
Массивы А,
В
и С
отсортировать по возрастанию, используя метод пузырька.
18.
Ввести массивы А и В. В массив С перенести те элементы массива А,
которые больше максимального элемента массива В, и те элементы массива В, которые меньше максимального элемента массива А. Массивы А, В
и С
отсортировать по убыванию, используя метод пузырька.
19.
Ввести массивы А и В. В массив С перенести четные элементы
массива А, и нечетные элементы массива В.
Массивы А, В и С отсортировать
по убыванию, ИСПОЛЬЗУЯ сортировку методом вставок.
20.
Ввести массивы А и В. В массив С перенести те четные элементы
массива А, левее которых стоят элементы с нечетным значением. Также в массив С перенести элемент массива В, который по значению
(min+max)/2,
где
min -
ближе всех к
значение минимального элемента массива В, шах
значение максимального элемента массива В. Массивы А, В и С отсортировать по возрастанию, используя сортировку методом слияния.
21.
Ввести массивы А, В и С. в массив
D
скопировать те элементы
массивов А, В и с, которые встречаются во всех трех массивах. Массивы А, В,
С и
D
отсортировать по возрастанию, используя быструю сортировку.
22.
Ввести массивы А, В и С. в массив
D
скопировать те элементы
массивов А и В, которых нет в массиве С. Массивы А, В, С и
D
отсортировать
по убыванию, используя сортировку методом слияния.
23.
Ввести массивы А, В и С. В массив О перенести из массива А те
элементы, левее которых расположены элементы, имеющие значение больше
(minB+maxC)/3, где minB - значение минимального элемента массива В, а тахС - значение максимального элемента массива С. Массивы А, В, С и D отсортировать по возрастанию, используя сортировку методом вставок.
24.
Ввести массивы А, В и С. в массив О перенести из массива А те
элементы, правее которых расположены элементы, имеющие значение меньше
чем
(minB+maxC)/2,
где
minB -
значение минимального элемента массива В, а
47
тахС и
D
-
значение максимального элемента массива с. Массивы А, В, С
отсортировать по возрастанию, используя сортировку методом пузырька.
25.
Ввести массивы А, В и с. в массив
элементы,
правее
которых
расположены
D
перенести из массива А те
элементы,
имеющие
значение
меньше чем массива В,
(minB+maxC)/2, где minB - значение минимального элемента а тахе - значение максимального элемента массива С. Из массива
С удалить те элементы, которые имеют значение больше максимального элемента массива В. Массивы А, В, С и
D
отсортировать по убыванию,
используя сортировку методом выбора.
Библиографический список 1.
Седжвик,
Р.
Фундаментальные
алгоритмы
данных/Сортировка/Поиск: пер. сангл.
-
на
С++.
Анализ/Структуры
К.: Издательство «Диасофт»,200 1.
- 688
с.
2.
Шилдт,
Г. Полный справочник по С++, 4-е ИЗД.: пер. сангл.
дом «Вильямс»,
3.
2002. - 704
-
М.: Издательский
с.
Каррано, Ф. М Абстракция данных и решение задач на С++. Стены и зеракала, 3-е
изд.,пер. сангл.
-
М.: Издательский дом «Вильямс».,2003.
- 848
с.: ил.
Учебное издание Методы сортировки и их реализации Составители: БЕЛЯЕВА Ирина Владимировна, БЕЛЯЕВ Константин Сергеевич
Редактор А.Камышанов
Подписано в печать
08 . 11.2006.
Формат 60х84
1/16.
Печать трафаретная. Усл. печ. л.2,79 .Тираж
Бумага офсетная.
100 экз.
Заказ N2/S5.г. Ульяновский государственный технический университет,
432027,
г.Ульяновск, ул. Северный Венец,
Типография УлГТУ,
432027,
32.
г.Ульяновск, УЛ. Сев. Венец,
32.