Файл: Программная инженерия, 18. 04. 2013. Красноярск сфу 2013 2.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 25.10.2023

Просмотров: 39

Скачиваний: 2

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

1 Министерство образования и науки Российской Федерации Сибирский федеральный университет Р. Ю. Царев АЛГОРИТМЫ И СТРУКТУРЫ ДАННЫХ Рекомендовано УМО РАЕ по классическому университетскому и техническому образованию в качестве учебного пособия для студентов высших учебных заведений, обучающихся по направлению подготовки 231000.62 Программная инженерия, 18.04.2013. Красноярск
СФУ
2013

2
УДК 004.021(07)
ББК я Ц181
Р е цензе н ты АН. Антамошкин, др техн. наук, профессор Красноярского государственного аграрного университета ИВ. Ковалев, др техн. наук, профессор, заведующий кафедрой системного анализа и исследования операций Сибирского государственного аэрокосмического университета им. акад. М.Ф. Решетнева Царев, Р.Ю. Ц Алгоритмы и структуры данных : учеб. пособие / Р. Ю. Царев. – Красноярск : Сиб. федер. унт, 2013. – 160 c.
ISBN Рассмотрены структуры и алгоритмы, которые являются основой современной методологии разработки программ. Изложено детальное описание и анализ основных алгоритмов обработки данных сортировка данных, поиск образа в строке, алгоритмы обработки графов. Предназначено для бакалавров направления 231000.62 Программная инженерия и преподавателей дисциплины Алгоритмы и структуры данных.
УДК 004.021(07)
ББК я
ISBN 978-5-7638-2834-4 © Сибирский федеральный университет, 2013

3 ОГЛАВЛЕНИЕ ВВЕДЕНИЕ 1. ОБЩИЕ СВЕДЕНИЯ ОБ АЛГОРИТМАХ 1.1. Свойства алгоритмов 1.2. Примеры алгоритмов 1.3. Типы и структуры данных 1.4. Абстрактные типы данных 1.5. Время выполнения программ 1.6. Вычисление времени выполнения программ 2. ПОИСК ОБРАЗА В СТРОКЕ 2.1. Прямой поиск строки 2.2. Алгоритм Кнута, Морриса и Пратта……………………………
34 2.3. Алгоритм Боуера и Мура 3. СОРТИРОВКА МАССИВОВ 3.1. Сортировка с помощью прямого включения 3.2. Сортировка с помощью прямого выбора 3.3. Сортировка с помощью прямого обмена 3.3.1. Пузырьковая сортировка 3.3.2. Шейкерная сортировка 3.4. Сортировка Шелла……………………………………………….
50 3.5. Сравнение различных алгоритмов сортировки 4. СОРТИРОВКА ПОСЛЕДОВАТЕЛЬНОСТЕЙ 4.1. Простое слияние 4.2. Естественное слияние 4.3. Многопутевая сортировка 4.4. Многофазная сортировка 5. ОРИЕНТИРОВАННЫЕ ГРАФЫ 5.1. Основные определения 5.2. Представления ориентированных графов 5.3. Задача нахождения кратчайшего пути 5.4. Нахождение кратчайших путей между парами вершин 5.5. Обход ориентированных графов 5.6. Ориентированные ациклические графы 5.7. Сильная связность

4 6. НЕОРИЕНТИРОВАННЫЕ ГРАФЫ 6.1. Основные определения 6.2. Остовные деревья минимальной стоимости 6.3. Обход неориентированных графов 6.4. Точки сочленения и двусвязные компоненты 6.5. Паросочетания графов 7. СОВРЕМЕННЫЕ АЛГОРИТМЫ ОБРАБОТКИ ДАННЫХ 7.1. Алгоритмы и простые числа 7.2. Генетические алгоритмы 7.3. Муравьиные алгоритмы 7.3.1. Биологические принципы поведения муравьиной колонии 7.3.2. Идея муравьиного алгоритма 7.3.3. Формализация задачи коммивояжера в терминах муравьиного подхода 7.3.4. Области применения и возможные модификации………
154
ЗАКЛЮЧЕНИЕ………………………………………………………….
156
БИБЛИОГРАФИЧЕСКИЙ СПИСОК ………………………………….
157

5 ВВЕДЕНИЕ Решение любой вычислительной задачи предполагает выполнение определенной последовательности шагов. При этом один и тот же результат может быть получен различными способами. Один из способов может быть более эффективным, другой менее эффективным, но более легким в реализации. Алгоритм, будучи той самой последовательностью шагов, можно оценить так, как это будет показано дальше. Имея набор алгоритмов, предназначенных для решения какой-либо проблемы, программист в состоянии выбирать удовлетворяющий его нуждам. В настоящем учебном пособии приведены различные алгоритмы решения довольно широкого класса задач. Данные алгоритмы не привязаны к какому-либо языку программирования, хотя предполагается, что программная реализация будет выполняться на языке программирования высокого уровня. Теоретическая часть наглядно иллюстрируется примерами на языках Pascal, Сии псевдоязыке, сочетающим высказывания на естественном языке с одним из указанных выше языков программирования. Приведенные в данном пособии алгоритмы представляют собой классику в области обработки данных. Существенный вклад в развитие современной теории алгоритмов и алгоритмизации решения практических задач внесли такие ученые, как Н. Вирт [2, 3], Д. Кнут [4], Дж. Макконел
[6], к трудам которых можно обратиться для более глубокого исследования проблемы построения и анализа алгоритмов. Большое количество информации об алгоритмах можно также найти в сети Интернет как на англоязычных, таки на русских сайтах. Невозможно говорить о практической или даже формальной реализации алгоритмов безотносительно структур и типов данных. В данном пособии структуры данных не вынесены в отдельную главу, однако они рассматриваются по мере необходимости в контексте тех или иных алгоритмов обработки данных. Учебное пособие освещает алгоритмы решения наиболее распространенных задач, покрывая практически всю область программирования. Здесь приведены как фундаментальные алгоритмы, таки современные методы обработки данных. Пособие состоит из девяти глав, посвященных алгоритмам решения различных классов задач. Впервой главе приводятся основные сведения об алгоритмах, их свойства. Отражена разница между такими понятиями, как структура данных, тип данных и абстрактный тип данных. Показано, как может измеряться временная сложность алгоритма, которая в дальнейших главах служит основным критерием оценки скорости выполнения алгоритма.

6 Следующая глава касается поиска образа в строке. Наглядным примером этой задачи может служить поиск слова в тексте в редакторе Word. Вообще задача поиска является одной из фундаментальных задач теоретического программирования. В этой главе приведен алгоритм прямого поиска, а также два алгоритма, позволяющих произвести более эффективный поиск. Сортировка или упорядочивание ряда объектов по возрастанию или убыванию является одной из основных задач обработки данных. Выбор алгоритма и даже класса алгоритмов, используя который возможно произвести сортировку, зависит от структуры данных. В связи с этим выделяют два типа сортировки внутреннюю и внешнюю. Третья глава пособия посвящена алгоритмам внутренней сортировки. При внутренней сортировке все данные помещаются в оперативную память, где можно получить доступ к данным в любом порядке. Работа алгоритмов данного класса показана на примере упорядочивания элементов массива. Внешняя сортировка применяется в том случае, когда объем упорядочиваемых данных настолько большой, что невозможно поместить все данные в оперативную память. Здесь задействуется механизм обмена большими блоками данных между внешними запоминающими устройствами и оперативной памятью. Тот факт, что физически непрерывные данные необходимо для удобства перемещения организовывать в блочную структуру вынуждает использовать специальные алгоритмы внешней сортировки. Алгоритмам внешней сортировки посвящена четвертая глава, в которой предполагается, что все данные хранятся в файлах. В пятой главе рассматриваются ориентированные графы. Решается такая важная задача, как поиск кратчайшего пути между двумя вершинами. Умение решать данную задачу помогает в планировании кратчайшего маршрута, например, при передвижении на транспортном средстве или передаче сообщений по компьютерной сети. Также в этой же главе показано, как можно перебрать все вершины графа методом обхода в глубину, что может потребоваться при поиске одной вершины во всем множестве вершин графа. В шестой главе приведены основные определения, связанные сне- ориентированными графами. Помимо обхода в глубину рассматривается обход в ширину, который, впрочем, также применим и к ориентированным графам. Другой немаловажной задачей, алгоритмы решения которой рассматриваются в данной главе, является построение минимального остовно- го дерева графа. Одно из применений минимальных остовных деревьев – организация локальной компьютерной сети с минимальными затратами на объединение всех компьютеров в единую сеть. Здесь же рассматривается связность графа и задача о паросочетании.

7 Седьмая глава содержит краткое изложение некоторых идейна основе которых разрабатываются современные алгоритмы. Примечательно, что данные алгоритмы используются не только для решения практических задач, но также активно применяются в научных исследованиях. Генетические алгоритмы можно встретить в диссертационных работах по различным направлениям. Здесь же представлены общие сведения о новом перспективном подходе к решению задач оптимизации на основе муравьиных алгоритмов. Особый интерес представляет тот факт, что в их основе лежит моделирование поведения колонии муравьев. Несмотря на столь широкий спектр рассмотренных алгоритмов, данное учебное пособие не претендует на полноту охвата всех нюансов современной теории алгоритмов и их применения. Тем не менее информации, представленной в пособии, достаточно для решения реальных задач. Рассмотренные алгоритмы могут быть применены в большом числе предметных областей и практических ситуаций.

8
1. ОБЩИЕ СВЕДЕНИЯ ОБ АЛГОРИТМАХ В основе любой компьютерной программы всегда лежит некоторый алгоритм, программа является изложением алгоритма на некотором языке, понятном вычислительной машине. Первым дошедшим до нас алгоритмом в его интуитивном понимании как конечной последовательности элементарных действий, решающих поставленную задачу, считается предложенный Евклидом в III веке дона- шей эры алгоритм нахождения наибольшего общего делителя двух чисел. Отметим, что в течение длительного времени, вплоть до начала XX века, само слово алгоритм употреблялось в устойчивом сочетании алгоритм Евклида. Для описания последовательности пошагового решения других математических задач чаще использовался термин метод. Во всех сферах своей деятельности, в частности в сфере обработки информации, человек сталкивается с различными способами или методиками решения разнообразных задач. Они определяют порядок выполнения действий для получения желаемого результата. Можно трактовать это как первоначальное, или интуитивное, определение алгоритма. Таким образом, можно нестрого определить алгоритм как однозначно трактуемую процедуру решения задачи. Дополнительные требования о выполнении алгоритма за конечное время для любых входных данных приводят к следующему неформальному определению алгоритма Алгоритм это заданное на некотором языке конечное предписание, задающее конечную последовательность выполнимых и точно определенных элементарных операций для решения задачи, общее для класса возможных исходных данных. Пусть Dz – область (множество) исходных данных задачи Z, a R – множество возможных результатов, тогда можно говорить, что алгоритм осуществляет отображение DzR. Поскольку такое отображение может быть неполным, то вводятся следующие понятия Алгоритм называется частичным алгоритмом, если результат может быть получен только для некоторых d
Dz и полным алгоритмом, если алгоритм получает правильный результат для всех d
Dz. Несмотря на усилия ученых, сегодня отсутствует одно исчерпывающе строгое определение понятия алгоритм. Из разнообразных вариантов словесного определения алгоритма наиболее удачные, по мнению автора, принадлежат российским ученым АН. Колмогорову и А. А. Маркову: Определение по Колмогорову алгоритм это всякая система вычислений, выполняемых по строго определенным правилам, которая после какого-либо числа шагов заведомо приводит к решению поставленной задачи.

9 Определение по Маркову: алгоритм – это точное предписание, определяющее вычислительный процесс, идущий от варьируемых исходных данных к искомому результату. Отметим, что различные определения алгоритма, в явной или неявной форме, постулируют следующий ряд общих требований
 алгоритм должен содержать конечное количество элементарно выполнимых предписаний, те. удовлетворять требованию конечности записи
 алгоритм должен выполнять конечное количество шагов при решении задачи, те. удовлетворять требованию конечности действий
 алгоритм должен быть единым для всех допустимых исходных данных, те. удовлетворять требованию универсальности
 алгоритм должен приводить к правильному по отношению к поставленной задаче решению, те. удовлетворять требованию правильности. Неудобства словесных определений связаны с проблемой однозначной трактовки терминов. В таких определениях должен быть, хотя бы неявно, указан исполнитель действий или предписаний. Алгоритм вычисления производной для полинома фиксированной степени вполне ясен тем, кто знаком с основами математического анализа, но для прочих он может оказаться совершенно непонятным. Это рассуждение заставляет указать также вычислительные возможности исполнителя, а именно уточнить какие операции для него являются элементарными. Другие трудности связаны стем, что алгоритм заведомо существует, но его очень трудно описать в некоторой заранее заданной форме. Классический пример такой ситуации алгоритм завязывания шнурков на ботинках в бантик. Вы сможете дать только словесное описание этого алгоритма без использования иллюстраций ? В связи с этим формально строгие определения понятия алгоритма связаны с введением специальных математических конструкций – формальных алгоритмических систем или моделей вычислений, каковыми являются машина Поста, машина Тьюринга, рекурсивно-вычислимые функции Черча, и постулированием тезиса об эквивалентности такого формализма и понятия алгоритм. Несмотря на принципиально разные модели вычислений, использующиеся для определения термина алгоритм, интересным результатом является формулировка гипотез о эквивалентности этих формальных определений в смысле их равномощности.
1.1. Свойства алгоритмов В качестве содержательных свойств, характеризующих алгоритм, А. А. Марков отмечает следующие

10 1. Определенность Алгоритм должен быть точным, недвусмысленными понятным исполнителю. Исполнитель, выполняя алгоритм, должен однозначно понимать предписание и знать, что надлежит делать на данном шаге вычислительного процесса и каков будет следующий шаг. Определенность не всегда означает детерминированность, те. когда на каждом шаге вычислительного процесса исполнитель должен исполнить единственное предписываемое действие и результат этого действия определен. Хотя в дальнейшем будем предполагать, что алгоритм детерминирован, однако не стоит забывать, что могут существовать и недетерминиро- ванные алгоритмы, особенно при наличии коллектива исполнителей (что характерно для параллельного программирования. При недетерминиро- ванности на некоторых шагах вычислительного процесса возможен не определяемый алгоритмом выбор из конечного фиксированного набора действий, но этот набор должен быть точно определен – определенность есть неотъемлемое свойство алгоритма.
2. Массовость Алгоритм предлагает всегда решение некоторой массовой проблемы, его исходные данные варьируются. Существует некоторое (заведомо непустое и не единичное) множество наборов исходных данных, определяемое решаемой проблемой, для которых алгоритм обеспечивает получение искомого результата. Не для любой массовой проблемы существует решающий ее алгоритм в теории алгоритмов для ряда массовых проблем доказана их неразрешимость, те. отсутствие алгоритма, получающего искомый результат для множества наборов исходных данных, возможного для этой проблемы. Близкой для нас неразрешимой массовой проблемой является установление эквивалентности двух произвольных программ – доказано, что не существует алгоритма, который для двух произвольных программ устанавливал бы, всегда ли они для одинакового набора исходных данных получают одинаковый результат. Вместе стем нас должен утешать тот факт, что большинство реальных проблем в их надлежащей постановке вполне допускают наличие алгоритма их решения.
3. Результативность Обычно алгоритм должен обеспечивать завершение своего выполнения ожидаемым результатом вычислительного процесса в конечное (и хотелось бы, приемлемое) время, разумеется, принадлежащих допустимых исходных данных. Допустимость исходных данных следует из существа решаемой проблемы так, если все исходные данные должны быть положительны, то нельзя требовать от алгоритма разумной реакции на то, что одно изданных ошибочно оказалось отрицательным. Вместе стем обычно стараются обезопасить алгоритм предварительной проверкой исходных данных на допустимость и при их недопустимости явно сообщать пользователю об этом. В пользовательском программировании все реализуемые алгоритмы носят такой завершающийся характер.

11 Однако результатом алгоритма может быть и поддержание некоторого постоянного процесса процесса управления некоторым устройством, процесса контроля его состояния, наконец, процесса операционной системы, управляющей функционированием компьютера. Результатами таких алгоритмов являются сигналы и сообщения, посылаемые пользователю или устройству, ив этом их результативность. Такие алгоритмы, как правило, находятся за рамками пользовательского программирования.
1.2. Примеры алгоритмов Классической массовой проблемой, известной с древности, является нахождение наибольшего общего делителя (НОД) двух натуральных чисел. Алгоритм, решающий эту проблему, может быть следующим. Дана пара произвольных чисел, назовем их т и n. Предписание, представляющее алгоритм, будем описывать на естественном языке в виде последовательности нумеруемых шагов Шаг 1. Возьмем в качестве р значение наименьшего из m и n. Шаг 2. Возьмем в качестве t значение 1. Шаг 3. Если р = 1, то перейдём к шагу 5, иначе к шагу 4. Шаг 4. Задавая в качестве i последовательно все целые значения от 2 до р (по возрастанию, повторяем Шаг 4.1. Шаг 4. 1. Если остаток (m / i) = 0 и остаток (n / i) = 0, то возьмем в качестве t значение i. Шаг 5. Завершаем алгоритм с результатом НОД = t. Сточки зрения исполнителя-человека это предписание точно и однозначно. Шаги исполняются последовательно, причем шаг 4 является повторением определенного числа раз шага 4.1 каждый раз со следующим значением i. Будем считать, что мы знаем, как находить остаток отделения, те. определенность здесь присутствует. То, что приведенное предписание обладает массовостью, тоже очевидно. Существует множество пар чисел, для которых предписание дает результат. Ясно, что вычислительный процесс будет нормально исполняться для любых конечных значений тип больше нуля. Именно такие пары и дают нам допустимые для решаемой проблемы значения исходных данных. В данном случае результативность совпадает с завершаемостью.
Завершаемость вычислительного процесса следует из того, что тип конечны, и, следовательно, число повторений шага 4.1 тоже конечно, остальные же шаги выполняются однократно. Число исполненных шагов зависит

12 прежде всего от числа повторений шага 4.1, а он будет повторяться
(min(m, п) – 1) раз. То, что результатом будет действительно наибольший общий делитель, очевидно из того, что если одно из значений пары есть 1, то полученным НОД будет 1; если ни одно из чисел от 2 до наименьшего из пары не является одновременно делителем и пи т, то полученный
НОД тоже 1, а если среди этих чисел есть делители и пи т, то значением t после шага 4 будет наибольшее такое число. Именно оно будет последними на шаге 4.1 будет взято в качестве t. Итак, приведенное предписание обладает всеми упомянутыми свойствами алгоритма. Заметим, что здесь мы применили подход в лоб – перебрали все значения, которые в принципе могут быть делителями m и пи проверили, может ли каждое быть общим делителем, а найдя больший, чем ранее найденный, делитель, заменили им ранее найденный. Такие, так называемые переборные, алгоритмы (перебираются все потенциально возможные значения решений) мы вынуждены применять, если у нас нет никаких соображений, как сократить число перебираемых значений. Зачастую такие соображения у нас могут быть. Так, для данной проблемы давно известно, что
НОД (m, n) = НОД (остаток (n / m), m), где m – наименьшее число пары. Таким образом, нахождение НОД для пары чисел можно свести к нахождению НОД для такой пары, в которой прежнее наименьшее становится наибольшим. Перебор уже становится перебором пар, определяемых приведенным выше соотношением, что сокращает его по отношению к уже приведенному алгоритму. Алгоритм (так называемый алгоритм Эвклида) выглядит следующим образом Шаг 1. Пока m неравно, выполнять шаги 1.1 – 1.4, иначе перейти к шагу 2. Шаг 1.1. Возьмем значение t как остаток (n / m). Шаг 1.2. Возьмем n как значение m. Шаг 1.3. Возьмем m как значение t. Шаг 1.4. Вернемся к шагу 1. Шаг 2. Завершаем алгоритм с НОД = n. Приведенное предписание действительно является алгоритмом, так как оно обладает определенностью (как и предыдущее предписание, массовостью (о допустимых значениях мы поговорим далее) и результативностью (повторения шагов 1.1 – 1.4 создают пары с уменьшающимися неотрицательными значениями и, значит, рано или поздно m станет равным нулю. Что касается допустимых значений, то ясно, что m и п должны быть больше нуля (впрочем, они могут быть и равны нулю если т = 0,

13 то за НОД возьмется значение п. На первый взгляд, из предыдущих рассуждений следует, что от должно быть не больше п Однако если проанализировать исполнение алгоритма, то увидим, что если т > п то припер- вом же исполнении шагов 1.1–1.4 значения переставятся: большее станет значением па меньшее – значением m, что и нужно для дальнейшего исполнения алгоритма. Число шагов алгоритма здесь оценить гораздо труднее, чем в предыдущем случае – оно существенно зависит от свойств тип по отношению друг к другу. Средняя ожидаемая оценка числа повторений шагов 1.1–1.4 равна приблизительно (12 * ln 2/ π
2
) * (ln m). Видно, что это значительно меньше, чем в предыдущем случае, и привлеченные соображения позволили существенно уменьшить перебор. Важно, что в последнем примере более явно, чем в предыдущем, видна изменяемость данных – исходная пара, идентифицируемая обозначениями тип, меняется и перестраивается, сохраняя введенные обозначения. Видим, что приходится вводить объекты с фиксированным обозначением и меняющимся значением – то, что в языках программирования называется переменной.
  1   2   3   4   5   6   7   8   9   ...   14

1.3. Типы и структуры данных Хотя термины тип данных (или просто тип, структура данных и абстрактный тип данных звучат похоже, они имеют различный смысл. В языках программирования тип данных переменной обозначает множество значений, которые может принимать эта переменная. Например, переменная булевого (логического) типа может принимать только два значения значение true (истина) и значение false (ложь) и никакие другие. Набор базовых типов данных отличается в различных языках в языке Pascal это типы целых (integer) и действительных (real) чисел, булев (boolean) тип и символьный (char) тип. Правила конструирования составных типов данных на основе базовых типов также различаются в разных языках программирования. В таких языках высокого уровня, как Сии легко и быстро строить составных типы. Абстрактный тип данных (АТД) – это математическая модель плюс различные операторы, определенные в рамках этой модели. Алгоритм может разрабатываться в терминах АТД, но для реализации алгоритма в конкретном языке программирования необходимо найти способ представления АТД в терминах типов данных и операторов, поддерживаемых данным языком программирования. Для представления АТД используются структуры данных, которые представляют собой набор переменных, возможно, различных типов данных, объединенных определенным образом. Базовым строительным блоком структуры данных является ячейка, которая предназначена для хранения значения определенного базового или составного типа данных. Структуры данных создаются путем задания имен совокупностям (агрегатам) ячеек и (необязательно) интерпретации значения некоторых ячеек как представителей (те. указателей) других ячеек. В качестве простейшего механизма агрегирования ячеек в Pascal и большинстве других языков программирования можно применять (одномерный) массив, те. последовательность ячеек определенного типа. Массив также можно рассматривать как отображение множества индексов (таких как целые числа 1, 2, …, п) во множество ячеек. Ссылка на ячейку обычно состоит из имени массива и значения из множества индексов данного массива. В языке Pascal множество индексов может быть нечислового типа, например (север, восток, юг, запад, или интервального типа (как 1..10). Значения всех ячеек массива должны иметь одинаковый тип данных. Объявление имя array[ТипИндекса] of ТипЯчеек; задает имя для последовательности ячеек, тип для элементов множества индексов и тип содержимого ячеек. Кстати, Pascal необычайно богат на типы индексов. Многие языки программирования позволяют использовать в качестве индексов только множества последовательных целых чисел. Например, чтобы в языке For-
tran в качестве индексов массива можно было использовать буквы, надо все равно использовать целые индексы, заменяя А на 1, В на 2, и т. д. Другим общим механизмом агрегирования ячеек в языках программирования является структура записи. Запись (record) можно рассматривать как ячейку, состоящую из нескольких других ячеек (называемых полями, значения в которых могут быть разных типов. Записи часто группируются в массивы тип данных определяется совокупностью типов полей записи. Например, в Pascal объявление
var
reclist: array[1..4] of record
data: real;
next: integer
end задает имя reclist (список записей) элементного массива, значениями которого являются записи с двумя полями data (данные) и next (следующий.

15 Третий метод агрегирования ячеек, который можно найти в Pascal и некоторых других языках программирования, – это файл. Файл, как и одномерный массив, является последовательностью значений определенного типа. Однако файл не имеет индексов его элементы доступны только в том порядке, в каком они были записаны в файл. В отличие от файла, массивы и записи являются структурами с произвольным доступом, подразумевая под этим, что время доступа к компонентам массива или записи не зависит от значения индекса массива или указателя поля записи. Достоинство агрегирования с помощью файла (частично компенсирующее описанный недостаток) заключается в том, что файл не имеет ограничения на количество составляющих его элементов и это количество может изменяться вовремя выполнения программы.
1.4. Абстрактные типы данных Перед тем, как рассмотреть абстрактный тип данных, обсудим его роль в процессе разработки программ. Сначала сравнивним абстрактный тип данных с таким знакомым понятием, как процедура. Процедуру, неотъемлемый инструмент программирования, можно рассматривать как обобщенное понятие оператора. В отличие от ограниченных по своим возможностям встроенных операторов языка программирования (сложения, умножения и т. пс помощью процедур программист может создавать собственные операторы и применять их к операндам различных типов, не только базовым. Примером такой процедуры-оператора может служить стандартная подпрограмма перемножения матриц. Другим преимуществом процедур (кроме способности создавать новые операторы) является возможность использования их для инкапсулиро- вания частей алгоритма путем помещения в отдельный раздел программы всех операторов, отвечающих за определенный аспект функционирования программы. Пример инкапсуляции использование одной процедуры для чтения входных данных любого типа и проверки их корректности. Преимущество инкапсуляции заключается в том, что мы знаем, какие инкап- сулированные операторы необходимо изменить в случае возникновения проблем в функционировании программы. Например, если необходимо организовать проверку, являются ли значения входных данных положительными, следует изменить только несколько строк кода, и мы точно знаем, где эти строки находятся. Определение абстрактного типа данных Определим абстрактный тип данных (АТД) как математическую модель с совокупностью операторов, определенных в рамках этой модели.

16 Простым примером АТД могут служить множества целых чисел с операторами объединения, пересечения и разности множеств. В модели АТД операторы могут иметь операндами не только данные, определенные АТД, но и данные других типов стандартных типов языка программирования или определенных в других АТД. Результат действия оператора также может иметь тип, отличный от определенных в данной модели АТД. Номы предполагаем, что по крайней мере один операнд или результат любого оператора имеет тип данных, определенный в рассматриваемой модели абстрактных типов данных. Две характерные особенности процедур – обобщение и инкапсуляция о которых говорилось выше, отлично характеризуют абстрактный тип данных. АТД можно рассматривать как обобщение простых типов данных (целых и действительных чисел и т. д, точно также, как процедура является обобщением простых операторов (+, – и т. д. Абстрактный тип данных инкапсулирует типы данных в том смысле, что определение типа и все операторы, выполняемые над данными этого типа, помещаются в один раздел программы. Если необходимо изменить реализацию АТД, мы знаем, где найти и что изменить водном небольшом разделе программы, и можем быть уверенными, что это не приведет к ошибкам где-либо в программе при работе с этим типом данных. Более того, вне раздела с определением операторов АТД можно рассматривать типы АТД как первичные типы, так как объявление типов формально не связано сих реализацией. Однако в этом случае могут возникнуть сложности, так как некоторые операторы могут инициироваться для более одного АТД и ссылки на эти операторы должны быть в разделах нескольких АТД. Можно реализовать тип данных любым способом, а программы, использующие объекты этого типа, не зависят от способа реализации типа – за это отвечают процедуры, реализующие операторы для этого типа данных. Термин реализация АТД подразумевает следующее перевод в операторы языка программирования объявлений, определяющие переменные этого абстрактного типа данных, плюс процедуры для каждого оператора, выполняемого над объектами АТД. Реализация зависит от структуры данных, представляющих АТД. Каждая структура данных строится на основе базовых типов данных применяемого языка программирования, используя доступные в этом языке средства структурирования данных. Структуры массивов и записей – два важных средства структурирования данных, возможных в языке Pascal. Например, одной из возможных реализации переменной типа SET может служить массив, содержащий элементы множества Одной из основных причин определения двух различных АТД в рамках одной модели является то, что над объектами этих АТД необходимо выполнять различные действия, те. определять операторы разных типов.

17 В идеале желательно писать программы на языке, базовых типов данных и операторов которого достаточно для реализации АТД. С этой точки зрения язык Pascal не очень подходящий язык для реализации различных АТД, нос другой стороны, трудно найти иной язык программирования, в котором можно было бы так непосредственно декларировать абстрактный тип данных.
1.5. Время выполнения программ В процессе решения прикладных задач выбор подходящего алгоритма вызывает определенные трудности. В самом делена чем основывать свой выбор, если алгоритм должен удовлетворять следующим противоречащим друг другу требованиям
1. быть простым для понимания, перевода в программный код и отладки. эффективно использовать компьютерные ресурсы и выполняться по возможности быстро. Если написанная программа должна выполняться только несколько раз, то первое требование наиболее важно. Стоимость рабочего времени программиста обычно значительно превышает стоимость машинного времени выполнения программы, поэтому стоимость программы оптимизируется по стоимости написания (а невыполнения) программы. Если мы имеем дело с задачей, решение которой требует значительных вычислительных затрат, то стоимость выполнения программы может превысить стоимость написания программы, особенно если программа должна выполняться многократно. Поэтому, с финансовой точки зрения, более предпочтительным может стать сложный комплексный алгоритм (в надежде, что результирующая программа будет выполняться существенно быстрее, чем более простая программа. Но ив этой ситуации разумнее сначала реализовать простой алгоритм, чтобы определить, как должна себя вести более сложная программа. При построении сложной программной системы желательно реализовать ее простой прототип, на котором можно провести необходимые измерения и смоделировать ее поведение в целом, прежде чем приступать к разработке окончательного варианта. Таким образом, программисты должны быть осведомлены не только о методах построения быстрых программно и знать, когда их следует применить (желательно с минимальными усилиями. Измерение времени выполнения программ На время выполнения программы влияют следующие факторы
1. Ввод исходной информации в программу.

18 2. Качество скомпилированного кода исполняемой программы.
3. Машинные инструкции (естественные и ускоряющие, используемые для выполнения программы.
4. Временная сложность алгоритма соответствующей программы
1
Поскольку время выполнения программы зависит от ввода исходных данных, его можно определить как функцию от исходных данных. Но зачастую время выполнения программы зависит не от самих исходных данных, а от их размера. В этом отношении хорошим примером являются задачи сортировки которые мы подробно рассмотрим в главе 3. В задачах сортировки в качестве входных данных выступает список элементов, подлежащих сортировке, а в качестве выходного результата – те же самые элементы, отсортированные в порядке возрастания или убывания. Например, входной список 2, 1, 3, 1, 5, 8 будет преобразован в выходной список
1, 1, 2, 3, 5, 8 (в данном случае список отсортирован в порядке возрастания. Естественной мерой объема входной информации для программы сортировки будет число элементов, подлежащих сортировке, или, другими словами, длина входного списка. В общем случае длина входных данных – подходящая мера объема входной информации, и если не будет оговорено иное, тов качестве меры объема входной информации будем далее понимать именно длину входных данных. Обычно говорят, что время выполнения программы имеет порядок
Т(п) от входных данных размера п Например, некая программа имеет время выполнения Т(п) = сп
2
, где с – константа. Единица измерения Т(п) точно не определена, номы будем понимать Т(п) как количество инструкций, выполняемых на идеализированном компьютере. Для многих программ время выполнения действительно является функцией входных данных, а не их размера. В этой ситуации определяем
Т(п) как время выполнения в наихудшем случаете. как максимум времени выполнения по всем входным данным размера п Также будем рассматривать Т

(n) как среднее (в статистическом смысле) время выполнения по всем входным данным размера п Хотя Т
ср
(п) является достаточно объективной мерой времени выполнения, но часто нельзя предполагать (или обосновать) равнозначность всех входных данных. На практике среднее время выполнения найти сложнее, чем наихудшее время выполнения, так как математически это трудноразрешимая задача и, кроме того, зачастую не имеет простого определения понятие средних входных данных. По Под термином временная сложность алгоритма понимается время выполнения алгоритма, измеряемое в шагах (инструкциях алгоритма, которые необходимо выполнить алгоритму для достижения запланированного результата. В качестве синонима для этого термина часто используют выражение время выполнения алгоритма. В этой связи необходимо различать время выполнения алгоритма и время выполнения программы.

19 этому в основном будем использовать наихудшее время выполнения как меру временной сложности алгоритмов, ноне будем забывать и о среднем времени выполнения там, где это возможно. Теперь сделаем замечание о втором и третьем факторах, влияющих на время выполнения программ о компиляторе, используемом для компиляции программы, и машине, на которой выполняется программа. Эти факторы влияют на то, что для измерения времени выполнения Т(п) нельзя применить стандартные единицы измерения, такие как секунды или миллисекунды. Поэтому можно только делать заключения, подобные время выполнения такого-то алгоритма пропорционально n
2
». Константы пропорциональности также нельзя точно определить, поскольку они зависят от компилятора, компьютера и других факторов. Асимптотические соотношения Для описания скорости роста функций используется символика. Например, когда мы говорим, что время выполнения Т(п) некоторой программы имеет порядок O(n
2
) (читается о большое отв квадрате или просто о отв квадрате, то подразумевается, что существуют положительные константы си такие, что для всех n, больших или равных n
0
, выполняется неравенство Т(п)≤ сп
2
.
Например,предположим, что Т) = 1, T(l) = 4 ив общем случае
T(n) = (n + 1)
2
. Тогда T(n) имеет порядок O(n
2
): если положить пи сто легко показать, что для n ≥1 будет выполняться неравенство (n + 1)
2
≤ Отметим, что нельзя положить n
0
= 0, так как T(0) = 1 и, следовательно, это значение при любой константе с больше с 2
= 0. Подчеркнем мы предполагаем, что все функции времени выполнения определены на множестве неотрицательных целых чисел и их значения также неотрицательны, но необязательно целые. Будем говорить, что
T(n) имеет порядок O(f(n)), если существуют константы си такие, что для всех nn
0
выполняется неравенство Т(п) ≤ cf(n). Для программу которых время выполнения имеет порядок O(f(n)), говорят, что они имеют порядок (или степень) роста f(n). Рассмотрим еще один пример. Функция T(n) = 3n
3
+ 2n
2
имеет степень роста O(n
3
). Чтобы это показать, надо положить n
0
= 0 и с = 5, так как легко видеть, что для всех целых n ≥ 0 выполняется неравенство
3n
3
+ 2n
2
≤ 5n
3
. Можно, конечно, сказать, что Т(п) имеет порядок O(n
4
), но это более слабое утверждение, чем то, что T(n) имеет порядок роста O(n
3
). В качестве следующего примера докажем, что функция 3n не может иметь порядок O(2n). Предположим, что существуют константы сип такие, что для всех nn
0
выполняется неравенство 3
n
≤ с. Тогда с ≥ (3 / для всех nn
0
. Но (3 / 2)
n
принимает любое, как угодно большое, значение

20 при достаточно большом п поэтому не существует такой константы с, которая могла бы мажорировать (3 / 2)
n
для всех n. Когда мы говорим, что Т(п) имеет степень роста O(f(n)), то подразумевается, что f(n) является верхней границей скорости роста Т(п). Чтобы указать нижнюю границу скорости роста Т(п), используется обозначение
Т(п) есть Ω(g(n)) (читается «омега-большое от g(n)» или просто омега от
g(n)»), это подразумевает существование такой константы с, что бесконечно часто (для бесконечного числа значений п)выполняется неравенство
T(n) ≥ Например, для проверки того, что Т(п) = п+ п есть Ω(n
3
), достаточно положить с = 1. Тогда Т(п)≥ сп
3
для п = 0, 1, … Для другого примера положим, что Т(п) = п для нечетных пи
Т(п) = п / 100 – для четных п ≥ 0. Для доказательства того, что Т(п) есть
Ω(n
2
), достаточно положить си рассмотреть множество четных чисел n = 0, 2, 4, 6, … Ограниченность показателя степени роста Итак, мы предполагаем, что программы можно оценить с помощью функций времени выполнения, пренебрегая при этом константами пропорциональности. С этой точки зрения программа с временем выполнения п, например, лучше программы с временем выполнения O(n
3
). Константы пропорциональности зависят не только от используемых компилятора и компьютера, но и от свойств самой программы. Пусть при определенной комбинации компилятор-компьютер одна программа выполняется за 100n
2
миллисекунда вторая – за 5n
3
миллисекунд. Может ли вторая программа быть предпочтительнее, чем первая Ответ на этот вопрос зависит от размера входных данных программ. При размере входных данных n < 20 программа с временем выполнения
5n
3
завершится быстрее, чем программа с временем выполнения Поэтому, если программы в основном выполняются с входными данными небольшого размера, предпочтение необходимо отдать программе с временем выполнения O(n
3
). Однако при возрастании п отношение времени выполнения 5n
3
/ 100 n
2
= n / 20 также растет. Поэтому при больших п программа с временем выполнения O(n
2
) становится предпочтительнее программы с временем выполнения O(n
3
). Если даже при сравнительно
1
Отметим асимметрию в определениях О- и символики. Такая асимметрия бывает полезной, когда алгоритм работает быстро на достаточно большом подмножестве, ноне на всем множестве входных данных. Например, есть алгоритмы, которые работают значительно быстрее, если длина входных данных является простым числом, а не (к примеру) четным числом. В этом случае невозможно получить хорошую нижнюю границу времени выполнения, справедливую для всех n ≥ n
0

21 небольших n, когда время выполнения обеих программ примерно одинаково, выбор лучшей программы представляет определенные затруднения, то естественно для большей надежности сделать выбор в пользу программы с меньшей степенью роста. Другая причина, заставляющая отдавать предпочтение программам с наименьшей степенью роста времени выполнения, заключается в том, что чем меньше степень роста, тем больше размер задачи, которую можно решить на компьютере. Другими словами, если увеличивается скорость вычислений компьютера, то растет также и размер задач, решаемых на компьютере. Однако незначительное увеличение скорости вычислений компьютера приводит только к небольшому увеличению размера задач, решаемых в течение фиксированного промежутка времени, исключением из этого правила являются программы с низкой степенью роста, как O(n) и O(n log n). На рис. 1.1 показаны функции времени выполнения (измеренные в секундах) для четырех программ с различной временной сложностью для одного итого же сочетания компилятор-компьютер. Рис. 1.1. Функции времени выполнения четырех программ Предположим, что можно использовать 1 000 секунд (примерно 17 минут) машинного времени для решения задачи. Какой максимальный размер задачи, решаемой за это время За 10 секунд каждый из четырех алгоритмов может решить задачи примерно одинакового размера, как показано во втором столбце табл. 1.1. Предположим, что получен новый компьютер (без дополнительных финансовых затрат, работающий в десять раз быстрее. Теперь за туже цену можно использовать 10 4
секунд машинного времени – ранее 10 3
секунд. Максимальный размер задачи, которую может решить за это время каждая из четырех программ, показан в третьем столбце табл. 1.1. Отношения значений третьего и второго столбцов приведены в четвертом столбце этой таблицы. Здесь мы видим, что увеличение скорости компьютера на 1 000 % приводит к увеличению только на 30 % размера задачи, решаемой с помощью программы с временем выполнения O(2
n
). Таким образом, кратное увеличение производительности компьютера дает в процентном отношении значительно меньший эффект увеличения размера решаемой задачи. В действительности, независимо от быстродействия компьютера, программа с временем выполнения O(2
n
) может решать только очень небольшие задачи. Таблица 1.1 Эффект от кратного увеличения быстродействия компьютера Время выполнения T(n) Максимальный размер задачи для
10 3
секунд Максимальный размер задачи для
10 4
секунд Увеличение максимального размера задачи
100n
10 100 10.0 5n
2 14 45 3.2
n
3
/ 2 12 27 2.3 2
n
10 13 1.3 Из третьего столбца табл. 1.1 ясно видно преимущество программ с временем выполнения п кратное увеличение размера решаемой задачи при кратном увеличении производительности компьютера. Программы с временем выполнения пи п) при увеличении быстродействия компьютера надают увеличение размера задачи соответственно на 230 % и 320 %. Эти соотношения сохранятся и при дальнейшем увеличении производительности компьютера. Поскольку существует необходимость решения задач все более увеличивающегося размера, приходим к почти парадоксальному выводу. Так как машинное время все время дешевеет, а компьютеры становятся более быстродействующими, мы надеемся, что сможем решать все большие по размеру и более сложные задачи. Но вместе стем возрастает значимость разработки и использования эффективных алгоритмов именно с низкой степенью роста функции времени выполнения. Приведем несколько соображений, позволяющих посмотреть на критерий времени выполнения с других точек зрения.
1. Если создаваемая программа будет использована только несколько раз, тогда стоимость написания и отладки программы будет доминировать в общей стоимости программы, те. фактическое время выполнения

23 не окажет существенного влияния на общую стоимость. В этом случае следует предпочесть алгоритм, наиболее простой для реализации.
2. Если программа будет работать только с малыми входными данными, то степень роста времени выполнения будет иметь меньшее значение, чем константа, присутствующая в формуле времени выполнения. Вместе стем и понятие малости входных данных зависит от точного времени выполнения конкурирующих алгоритмов. Существуют алгоритмы, асимптотически самые эффективные, но которые никогда не используют на практике даже для больших задач, так каких константы пропорциональности значительно превосходят подобные константы других, более простых и менее эффективных алгоритмов.
3. Эффективные, но сложные алгоритмы могут быть нежелательными, если готовые программы будут поддерживать лица, не участвующие в написании этих программ. Будем надеяться, что принципиальные моменты технологии создания эффективных алгоритмов широко известны, и достаточно сложные алгоритмы свободно применяются на практике. Однако необходимо предусмотреть возможность того, что эффективные, но хитрые алгоритмы не будут востребованы из-за их сложности и трудностей, возникающих при попытке в них разобраться.
4. Известно несколько примеров, когда эффективные алгоритмы требуют таких больших объемов машинной памяти (без возможности использования более медленных внешних средств хранения, что этот фактор сводит на нет преимущество эффективности алгоритма.
5. В численных алгоритмах точность и устойчивость алгоритмов не менее важны, чем их временная эффективность. Еще раз подчеркнем, что степень роста наихудшего времени выполнения не единственный, но едва лине самый важный критерий оценки алгоритмов и программ.
1   2   3   4   5   6   7   8   9   ...   14

1.6. Вычисление времени выполнения программ Теоретическое нахождение времени выполнения программ (даже без определения констант пропорциональности) – сложная математическая задача. Однако на практике определение времени выполнения (также без нахождения значения констант) является вполне разрешимой задачей. Для этого нужно знать только несколько базовых принципов. Но прежде чем представить эти принципы, рассмотрим, как выполняются операции сложения и умножения с использованием символики. Пусть T
1
(n) и T
2
(n) время выполнения двухпрограммных фрагментов и P
2
, T
1
(n) имеет степень роста O(f(n)), а T
2
(n) – O(g(n)). Тогда
T
1
(n) + T
2
(n), те. время последовательного выполнения фрагментов P
1

24 и P
2
, имеет степень роста ах, g(n))). Для доказательства этого вспомним, что существуют константы c
1
, c
2
, n
1
и n
2
такие, что при п ≥ выполняется неравенство T
1
(n) ≤ c
1
f(n), и, аналогично, T
2
(n) ≤ c
2
g(n), если п
n
2
. Пусть n
0
= max(n
1
, n
2
). Если п ≥ n
0
, то, очевидно, что T
1
(n) + T
2
(n)≤
c
1
f(n) + c
2
g(n). Отсюда вытекает, что при п ≥ n
0 справедливо неравенство
T
1
(n) + T
2
(n)≤ (c
1
+ c
2
) max(f(n), g(n)). Последнее неравенство и означает, что T
1
(n) + имеет порядок роста O(max(f(n), g(n))). Правило сумм, данное выше, используется для вычисления времени последовательного выполнения программных фрагментов с циклами и ветвлениями. Предположим, что есть три фрагмента с временами выполнения соответственно п пи п log n). Тогда время последовательного выполнения первых двух фрагментов имеет порядок O(max(n
2
, п, те. п. Время выполнения всех трех фрагментов имеет порядок
O(max(n
3
, п log n)), это тоже самое, что O(n
3
). В общем случае время выполнения конечной последовательности программных фрагментов, без учета констант, имеет порядок фрагмента с наибольшим временем выполнения. Иногда возможна ситуация, когда порядки роста времен нескольких фрагментов несоизмеримы (ни один из них не больше, чем другой, но они и неравны. Для примера рассмотрим два фрагмента с временем выполнения O(f(n)) и O(g(n)), где
n
4
, если n четное
f(n) =
n
2
, если n нечетное,
n
2
, если n четное
g(n) =
n
3
, если n нечетное. В данном случае правило сумм можно применить непосредственно и получить время выполнения O(max(f(n), g(n)), те при n четном и n
3
, если нечетно. Из правила сумм также следует, что если g(n)≤ f(n) для всех п превышающих, то выражение O(f(n) + g(n)) эквивалентно O(f(n)). Например, п + п) тоже самое, что п. Правило произведений заключается в следующем. Если T
1
(n) и T
2
(n) имеют степени роста O(f(n)) и O(g(n)) соответственно, то произведение
T
1
(n) x T
2
(n) имеет степень роста O(f(n) x g(n)). Из правила произведений следует, что O(cf(n)) эквивалентно O(f(n)), если с – положительная константа. Например, O(n
2
/ 2) эквивалентно O(n
2
). Прежде чем переходить к общим правилам анализа времени выполнения программ, рассмотрим простой пример, иллюстрирующий процесс определения времени выполнения.

25 Рассмотрим программу сортировки bubble (пузырек, которая упорядочивает массив целых чисел в возрастающем порядке методом пузырька. За каждый проход внутреннего цикла (операторы (3)–(6)) пузырек с наименьшим элементом всплывает в начало массива.
procedure bubble (var A: array[1..n] of integer);
Процедура упорядочивает массив A в возрастающем порядке
var
i, j, temp: integer;
begin
(1) for i:= 1 to n – 1 do
(2) for j:= n downto i + 1 do
(3) if A[j – 1] > A[j] then begin
перестановка местами A[j – 1] и A[j]}
(4) temp := A[j – 1] ;
(5) A[j – 1] := A[j];
(6) A[j] := temp;
end
end; Число элементов п, подлежащих сортировке, может служить мерой объема входных данных. Сначала отметим, что все операторы присваивания имеют некоторое постоянное время выполнения, независящее от размера входных данных. Таким образом, операторы (4) – (6) имеют время выполнения порядка O(1). Запись O(1) означает равнозначно некой константе. В соответствии с правилом сумм время выполнения этой группы операторов равно ах, 1, 1)) = O(1). Теперь необходимо подсчитать время выполнения условных и циклических операторов. Операторы и for вложены друг в друга, поэтому мы пойдем от внутренних операторов к внешним, последовательно определяя время выполнения условного оператора и каждой итерации цикла. Для оператора проверка логического выражения занимает время порядка
O(1). Мы не знаем, будут ли выполняться операторы в теле условного оператора (строки (4) – (6)), но поскольку мы ищем наихудшее время выполнения, то, естественно, предполагаем, что они выполняются. Таким образом, получаем, что время выполнения группы операторов (3) – (6) имеет порядок O(1). Далее рассмотрим группу (2) – (6) операторов внутреннего цикла. Общее правило вычисления времени выполнения цикла заключается в суммировании времени выполнения каждой итерации цикла. Для операторов) время выполнения на каждой итерации имеет порядок O(1).

26 Цикл выполняется п – i раз, поэтому по правилу произведений общее время выполнения цикла имеет порядок O((ni) х 1), что равно O(ni). Теперь перейдем к внешнему циклу, который содержит все исполняемые операторы программы. Оператор (1) выполняется п – 1 раз, поэтому суммарное время выполнения программы ограничено сверху выражением, которое имеет порядок п. Таким образом, программа пузырька выполняется за время, пропорциональное квадрату числа элементов, подлежащих упорядочиванию. В главе 3 будут рассмотрены программы с временем выполнения порядка п log n), которое существенно меньше п, поскольку при больших п log n
1
значительно меньше п Перед формулировкой общих правил анализа программ позвольте напомнить, что нахождение точной верхней границы времени выполнения программ только в редких случаях также просто, как в приведенном выше примере, в общем случае эта задача является интеллектуальным вызовом исследователю. Поэтому не существует исчерпывающего множества правил анализа программ. Можно дать только некоторые советы и проиллюстрировать их с разных точек зрения примерами, приведенными в этом пособии. Дадим несколько правил анализа программ. В общем случае время выполнения оператора или группы операторов можно параметризовать с помощью размера входных данных и/или одной или нескольких переменных. Но для времени выполнения программы в целом допустимым параметром может быть только п, размер входных данных.
1. Время выполнения операторов присваивания, чтения и записи обычно имеет порядок O(1). Есть несколько исключений из этого правила, например в языке PL / 1, где можно присваивать большие массивы, или в любых других языках, допускающих вызовы функций в операторах присваивания. Время выполнения последовательности операторов определяется с помощью правила сумм. Поэтому степень роста времени выполнения последовательности операторов без определения констант пропорциональности совпадает с наибольшим временем выполнения оператора в данной последовательности Если не указано другое, то будем считать, что все логарифмы определены по основанию. Однако отметим, что выражение O(log n) не зависит от основания логарифма, поскольку log
a
n = c log
b
n, где с = log
a
b.

27 3. Время выполнения условных операторов состоит из времени выполнения условно исполняемых операторов и времени вычисления самого логического выражения. Время вычисления логического выражения обычно имеет порядок O(1). Время для всей конструкции состоит из времени вычисления логического выражения и наибольшего из времени, необходимого для выполнения операторов, исполняемых при значении логического выражения true (истина) и при значении false (ложь.
4. Время выполнения цикла является суммой времени всех исполняемых итераций цикла, в свою очередь состоящих из времени выполнения операторов тела цикла и времени вычисления условия прекращения цикла (обычно последнее имеет порядок O(1)). Часто время выполнения цикла вычисляется, пренебрегая определением констант пропорциональности, как произведение количества выполненных итераций цикла на наибольшее возможное время выполнения операторов тела цикла. Время выполнения каждого цикла, если в программе их несколько, должно определяться отдельно. Вызовы процедур Для программ, содержащих несколько процедур (среди которых нет рекурсивных, можно подсчитать общее время выполнения программы путем последовательного нахождения времени выполнения процедур, начиная стой, которая не имеет вызовов других процедур. (Вызов процедур определяем по наличию оператора call.) Так как мы предположили, что все процедуры нерекурсивные, то должна существовать хотя бы одна процедура, не имеющая вызовов других процедур. Затем можно определить время выполнения процедур, вызывающих эту процедуру, используя уже вычисленное время выполнения вызываемой процедуры. Продолжая этот процесс, найдем время выполнения всех процедур и, наконец, время выполнения всей программы. Если есть рекурсивные процедуры, то нельзя упорядочить все процедуры таким образом, чтобы каждая процедура вызывала только процедуры, время выполнения которых подсчитано на предыдущем шаге. В этом случае мы должны с каждой рекурсивной процедурой связать временную функцию Т(п), где n определяет объем аргументов процедуры. Затем необходимо получить рекуррентное соотношение для Т(п), те. уравнение (или неравенство) для Т(п), где участвуют значения T(k) для различных значений. Техника решения рекуррентных соотношений зависит от вида этих соотношений. Проанализируем простую рекурсивную программу. Ниже представлен листинг программы вычисления факториала n!, те. вычисления произведения целых чисел от 1 до n включительно.

28
function fact (n: integer): integer;
{ fact(n) вычисляет п }
begin
(1) if n <= 1 then
(2) fact := 1
else
(3) fact := n * fact(n – 1)
end; Естественной мерой объема входных данных для функции fact является значение n. Обозначим через Т(п) время выполнения программы. Время выполнения для строки) имеет порядок O(1), а для строки (3)
O(1) + Т(п – 1). Таким образом, для некоторых констант си имеем
c + Т(п – 1), если n > 1;
T(n) = (1.1)
d, если n ≤ 1. Полагая, что пи раскрывая в соответствии с соотношением (1.1) выражение Т(п – 1) (те. подставляя в (1.1) п –
1 вместо п, получим
Т(п) = с + Т(п – Аналогично, если п > 3, раскрывая Т(п – 2), получим
Т(п) = с + Т(п – 3). Продолжая этот процесс, в общем случае для некоторого п > i, имеем Т(п) = ic + Т(п i). Положив в последнем выражении
i = п – 1, окончательно получаем
Т(п) = с(п – 1) + T(1) = с(п – 1) + d. (1.2) Из (1.2) следует, что Т(п) имеет порядок п. Отметим, что в этом примере анализа программы мы предполагали, что операция перемножения двух целых чисел имеет порядок O(1). На практике, однако, программу, представленную выше, нельзя использовать для вычисления факториала при больших значений п, так как размер получаемых в процессе вычисления целых чисел может превышать длину машинного слова. Общий метод решения рекуррентных соотношений, подобных соотношению из рассмотренного примера, состоит в последовательном раскрытии выражений T(k) в правой части уравнения (путем подстановки в исходное соотношение k вместо п) до тех пор, пока не получится формула, у которой в правой части отсутствует Т (как в формуле (1.2)). При этом часто приходится находить суммы различных последовательностей. Если значения таких сумм нельзя вычислить точно, то для сумм находятся верхние границы, что позволяет, в свою очередь, получить верхние границы для Т(п).

29 Программы с операторами безусловного перехода При анализе времени выполнения программ мы неявно предполагали, что все ветвления входе выполнения процедур осуществлялись с помощью условных операторов и операторов циклов. Мы останавливаемся на этом факте, так как определяли время выполнения больших групп операторов путем применения правила сумм к этим группам. Однако операторы безусловного перехода (такие как goto) могут порождать более сложную логическую групповую структуру. В принципе операторы безусловного перехода можно исключить из программы. Но, к сожалению, язык Pascal не имеет операторов досрочного прекращения циклов, поэтому операторы перехода по необходимости часто используются в подобных ситуациях. Предлагается следующий подход для работы с операторами безусловного перехода, выполняющих выход из циклов, который гарантирует отслеживание всего цикла. Поскольку досрочный выход из цикла, скорее всего, осуществляется после выполнения какого-нибудь логического условия, то можно предположить, что это условие никогда не выполняется. Но этот консервативный подход никогда не позволит уменьшить время выполнения программы, так как мы предполагаем полное выполнение цикла. Для программ, где досрочный выход из цикла не исключение, а правило, консервативный подход значительно переоценивает степень роста времени выполнения в наихудшем случае. Если же переход осуществляется к ранее выполненным операторам, тов этом случае вообще нельзя игнорировать оператор безусловного перехода, поскольку такой оператор создает петлю в выполнении программы, что приводит к нарастанию времени выполнения всей программы. Тем не менее анализ времени выполнения программ, использующих операторы безусловного перехода, создающих петли, возможен. Если программные петли имеют простую структуру, те. о любой паре петель можно сказать, что они или не имеют общих элементов, или одна петля вложена в другую, тов этом случае можно применить методы анализа времени выполнения, описанные в данном разделе. Анализ программ на псевдоязыке Если известна степень роста времени выполнения операторов, записанных с помощью неформального человеческого языкато, следовательно, можно проанализировать программу на псевдоязыке (такие программы будем называть псевдопрограммами). Однако часто невозможно в принципе знать время выполнения той части псевдопрограммы, которая еще не полностью реализована в формальных операторах языка программирования. Например, если в псевдопрограмме имеется неформальная часть, оперирующая абстрактными типами данных, то общее время выполнение программы в значительной степени зависит от способа реализации АТД. Это не является недостатком псевдопрограмм, так как одной из причин написания программ в терминах АТД является возможность выбора реализации АТД, в том числе по критерию времени выполнения. При анализе псевдопрограмм, содержащих операторы языка программирования и вызовы процедур, имеющих неформализованные фрагменты (такие как операторы для работы с АТД), можно рассматривать общее время выполнения программы как функцию пока неопределенного времени выполнения таких процедур. Время выполнения этих процедур можно параметризовать с помощью размера их аргументов. Часто выбранная математическая модель АТД сама подсказывает логически обоснованный размер аргументов. Например, если АТД строится на основе множеств, то часто количество элементов множества можно принять за параметр функции времени выполнения. В завершении данной главы опишем в общем виде процесс создания программы на языке программирования, например на языке Pascal. На рис. 1.2 схематически представлен процесс программирования. На первом этапе создается модель исходной задачи, для чего привлекаются соответствующие подходящие математические модели (например, теория графов) [1]. На этом этапе для нахождения решения также строится неформальный алгоритм. Математическая модель Абстрактные типы данных Структуры данных Неформальный алгоритм Программа на псевдоязыке Программа на языке Pascal Рис. 1.2. Функции времени выполнения четырех программ Наследующем этапе алгоритм записывается на псевдоязыке – смеси конструкций языка Pascal и менее формальных и обобщенных операторов на простом человеческом языке. Продолжением этого этапа является замена неформальных операторов последовательностью более детальных и формальных операторов. С этой точки зрения программа на псевдоязыке должна быть достаточно подробной, так как в ней фиксируются (определяются) различные типы данных, над которыми выполняются операторы. Затем создаются абстрактные типы данных для каждого зафиксированного типа данных (за исключением элементарных типов данных, таких как целые и действительные числа или символьные строки) путем задания имен процедурам для каждого оператора, выполняемого над данными абстрактного типа, и замены операторов вызовом соответствующих процедур. Третий этап процесса программирования обеспечивает реализацию каждого абстрактного типа данных и создание процедур для выполнения

31 различных операторов над данными этих типов. На этом этапе также заменяются все неформальные операторы псевдоязыка на код языка Pascal. Результатом этого этапа должна быть выполняемая программа. После ее отладки получается работающую программу. Предполагается что, используя пошаговый подход при разработке программ, схема которого показана на рис. 1.2, процесс отладки конечной программы будет небольшими безболезненным. Контрольные задания
1. Рассмотрим следующие функции от n:
f
1
(n) = n
2
;
f
2
(n) = n
2
+ 1000n;
n, если n нечетно,
f
3
(n) =
n
3
, если n четно
n, если n ≤ 100,
f
4
(n) =
n
3
, если n > 100. Укажите для каждой пары функций, когда f
i
(n) имеет порядок O(f
i
(n)) и когда f
i
(n) есть
(f
i
(n)).
2. Рассмотрим следующие функции от n:
n
2
для четных n ≥ 0,
g
1
(n) =
n
3
для нечетных n ≥ 1;
n для 0 ≤ n ≤ 100,
g
2
(n) =
n
3
для n > 100;
g
3
(n) = Укажите для каждой пары функций, когда g
i
(n) имеет порядок
O(g
i
(n)) и когда g
i
(n) есть
(g
i
(n)).
3. Предположим, что T
1
(n) есть
(f(n)) и T
2
(n) –
(g(n)). Какие из следующих выражений истины а) T
1
(n) + T
2
(n) =
(max(f(n), g(n))); б) T
1
(n)T
2
(n) есть
(f(n), g(n)).
4. Докажите, что следующие утверждения истинны а) 17 имеет порядок Об имеет порядок О, в) max (n
3
, 10n
2
) имеет порядок О, г) есть O(n
k + 1
) и
(n
k + 1
) для целых k,

32 д) если p(x) – полином степени k с положительных старшим коэффициентом, то p(x) есть O(n
k
) и
(n
k
).
5. Расположите следующие функции в порядке степени роста а) n, б)
n , в)
log n, где, ж) n
log
2
n, з) (и) (3/2)
n
, к) 17.

33
1   2   3   4   5   6   7   8   9   ...   14

2. ПОИСК ОБРАЗА В СТРОКЕ Одно из наиболее часто встречающихся в программировании действий поиск. Он же представляет собой идеальную задачу, на которой можно испытывать различные структуры данных по мере их появления. Существует несколько основных вариаций этой темы, и для них создано многоразличных алгоритмов. В данной главе будет рассмотрен специфи- ческийпоиск, так называемый поиск строки. Его можно определить следующим образом. Пусть задан массив
str из N элементов и массив img из М элементов, причем 0 ≤ М < N. Описаны они так
char str[N];
char img[M]; Поиск строки обнаруживает первое вхождение
img в str. Оба массива содержат символы, так что
str можно считать некоторым текстом или строкой, а
img – образом, первое вхождение в строку которого необходимо найти. Это действие типично для любых систем обработки текстов, отсюда и очевидная заинтересованность в поиске эффективного алгоритма для этой задачи.
2.1. Прямой поиск строки Прежде чем обратить внимание на эффективность, разберем некий прямолинейный алгоритм поиска, который называется прямым поиском строки. При прямом поиске строки сравнивается первый символ образа и соответствующий ему символ из строки. Вначале работы алгоритма этим символом будет первый символ строки. Если сравниваемые символы совпадают, то рассматриваются следующие, вторые, символы образа и строки. Таким образом происходит перемещение по образу от его начала к концу. Если образ закончился, значит, все символы образа совпали с символами рассматриваемой части строки, те. поиск образа в строке выполнен успешно. В том случае, если произошло несовпадение символов образа и строки, образ сдвигается по строке на один символ вправо, и снова происходит сравнение образа, начиная с первого символа. Теперь уже первый символ образа сравнивается со вторым символом строки, второй символ образа – с третьим символом строки и т. д. Поиск происходит до тех пор, пока не будет найдено вхождение образа в строке или жене закончится строка, что значит, что вхождение образа не было найдено. На рис. 2.1 приведен пример прямого поиска строки, сравниваемые символы образа подчеркнуты.
Hoola-Hoola girls like Hooligans.
Hooligan
Hooligan
Hooligan

Hooligan Рис. 2.1. Прямой поиск образа в строке При программной реализации можно использовать два цикла. Внешний цикл отвечает за продвижение образа по строке, во внутреннем цикле происходит продвижение по образу. Листинг данной программы выглядит следующим образом = –1;
do{
i++;
j = 0;
while((j < m) && (str[i + j] == img[j]))
j++;
}
while((j != m) && (i < n – m)); В действительности член (
i < nm) в условии окончания определяет, не закончилась ли строка ив случае отрицательного ответа свидетельствует, что нигде в строке совпадения с образом не произошло. В тоже время невыполнение условия (
j != m) соответствует ситуации, когда соответствие образа некоторой части строки найдено. Эффективность прямого поискав строке. Данный алгоритм работает достаточно эффективно, если допустить, что несовпадение пары символов происходит, по крайней мере, после всего лишь нескольких сравнений во внутреннем цикле. Можно предполагать, что для текстов, составленных из 128 печатных символов, несовпадение будет обнаруживаться после одной или двух проверок. Тем не менее, в худшем случае производительность будет внушать опасение. Например, для строки, состоящей из
N – 1 символов А иединственного В, а образа, содержащего М –
1 символов Аи опять В, чтобы обнаружить совпадение в конце строки, требуется произвести порядка
N * M сравнений. Далее рассматриваются два алгоритма, которые намного эффективнее решают задачу поиска образа в строке.
2.2. Алгоритм Кнута, Морриса и Пратта В 1970 г. Д. Кнут, Д. Моррис и В. Пратт изобрели алгоритм, фактически требующий только
N сравнений даже в самом плохом случае. Новый

35 алгоритм основывается на том соображении, что, начиная каждый раз сравнение образа с самого начала, после частичного совпадения начальной части образа с соответствующими символами строки пройденная часть строки фактически известна и можно вычислить некоторые сведения (на основе самого образа, с помощью которых можно продвинуться по строке дальше, чем при прямом поиске. Алгоритм Кнута, Морриса и Пратта или КМП-алгоритм использует при сдвиге образа таблицу
d, которая формируется еще до начала поиска образа в строке. Можно выделить два этапа работы КМП-алгоритма:
1. формирование таблицы
d, используемой при сдвиге образа по строке
2. поиск образа в строке. Таблица
d формируется на основе образа и содержит значения, которые в дальнейшем будут использованы при вычислении величины сдвига образа. Размер данной таблицы равен длине образа, которую можно определить при помощи функции
int strlen(char *) библиотеки <string.h>. Таким образом, таблица
d фактически является одномерным массивом, состоящим из числа элементов равного количеству символов в образе. Первый элемент массива
d всегда равен –1. Все элементы массива d соответствующие символам одинаковым первому символу образа также приравниваются –1. Как показано на рис. 2.2, элемент
d[0], соответствующий первому символу образа –
a – равен –1, также и четвертый элемент
d[3], также соответствующий символу a равен –1. образ a b c a b c значения элементов массива d:
–1 –1 Рис. 2.2. Образ и значения некоторых элементов массива d Для остальных символов образа значение элементов массива
d вычисляется следующим образом значение
d[j], соответствующее j-му символу образа, равно максимальному числу символов непосредственно предшествующих данному символу, совпадающих с началом образа. При этом если рассматриваемому символу предшествует
k символов, то во внимание принимаются только
k – 1 предшествующих символов. образ a b c a b c значения элементов массива d:
–1 0 0 –1 1 2 Рис. 2.3. Образ и значения элементов массива d На рис. 2.3 показаны значения элементов массива
d. Отметим, что пятому символу образа, символу
b, предшествует символ a, совпадающий с первым символом образа, поэтому
d[4], соответствующий символу b, равен. Аналогично,
d[5] равен 2, поскольку символу c предшествуют два символа
a и b, совпадающие с двумя первыми символами образа.

36 Поскольку
d[j] принимает значение, равное максимальному числу символов предшествующих
j-му символу, то, как показано на рис. 2.4, элемент
d[6], соответствующий символу c, равен четырем, а не двум. образ a b a b a b c значения элементов массива d:
4 Рис. 2.4. Образ и значения элемента d[6] Можно сделать следующий вывод значения массива
d определяется одним лишь образом и не зависит от строки текста. Для определения значения элементов массива
d необходимо найти самую длинную последовательность символов образа, непосредственно предшествующих позиции
j, которая совпадает полностью с началом образа. Так как эти значения зависят только от образа, то перед началом фактического поиска необходимо вычислить элементы массива
d. Эти вычисления будут оправданными, если размер строки или текста значительно превышает размер образа. Вторым этапом работы КМП-алгоритма является сравнение символов образа и строки и вычисления сдвига образа в случае их несовпадения. Символы образа рассматриваются слева направо, те. от начала к концу образа. При несовпадении символов образа и строки образ сдвигается вправо по строке. Величина сдвига вычисляется следующим образом если при переборе символов образа используется индекс
j, то сдвиг образа происходит на
j – d[j] символов. На рис. 2.5 приведен ряд примеров иллюстрирующих сдвиг образа при работе КМП-алгоритма. Строка
a a a a a c Образ
a a a a a b
j = 5, d[5] = 4,
j
0 1 2 3 4 5 смещение = j – d[j] = 1
d[j] –1 –1 –1 –1 –1 4 Сдвинутый
a a a a a b образ Строка
a b c a b d Образ
a b c a b c
j = 5, d[5] = 2,
j
0 1 2 3 4 5 смещение = j – d[j] = 3
d[j] –1 0 0 –1 1 2 Сдвинутый
a b c a b c образ Строка
a b c d e a Образ
a b c d e f
j = 5, d[5] = 0,
j
0 1 2 3 4 5 смещение = j – d[j] = 5
d[j]
–1 0
0 0 0 0 Сдвинутый
a b c d e f образ Рис. 2.5. Частичное совпадение и смещение образа

37 Приведем пример поискав строке образа «
Hooligan». На рис. 2.6 приведены значения элементов массива
d. На рис. показан принцип работы КМП-алгоритма, сравниваемые символы подчеркнуты. образ
Hooligan значения элементов массива d:
–10000000 Рис. 2.6. Образ и значения элементов массива d
Hoola-Hoola girls like Hooligans.
Hooligan
Hooligan
Hooligan
Hooligan
Hooligan

Hooligan Рис. 2.7. Поиск образа в строке по методу Кнута, Морриса и Пратта Обратите внимание при каждом несовпадении символов образ сдвигается на все пройденное расстояние, поскольку меньшие сдвиги не могут привести к полному совпадению. Эффективность КМП-алгоритма. Точный анализ КМП-поиска, как и сам его алгоритм, весьма сложен. Его разработчики доказывают, что требуется порядка М + N сравнений символов, что значительно лучше, чем
M * N сравнений из прямого поиска. Они также отмечают то приятное свойство, что указатель сканирования строки
i никогда не возвращается назад, в то время как при прямом поиске после несовпадения просмотр всегда начинается с первого символа образа и поэтому может включать символы строки, которые уже просматривались ранее. Это может привести к затруднениям, если строка читается из вторичной памяти, ведь в этом случае возврат обходится дорого. Даже при буферизованном вводе может встретиться столь большой образ, что возврат превысит емкость буфера.
2.3. Алгоритм Боуера и Мура Поиск образа в строке по методу Кнута, Морриса, Пратта дает выигрыш только в том случае, когда несовпадению символов из образа и строки предшествовало некоторое число совпадений. Если при сравнении образа и анализируемой части строки сопоставляемые символы различны, то образ сдвигается только на один символ. Продвижение образа более чем на единицу происходит лишь в этом случае, когда происходит совпадение нескольких символов образа и строки. К несчастью, это скорее исключение,

38 чем правило совпадения встречаются значительно реже, чем несовпадения. Поэтому выигрыш от использования КМП-стратегии в большинстве случаев поискав обычных текстах весьма незначителен. В 1975 г. Р. Боуер и Д. Мур предложили метод, который не только улучшает обработку самого плохого случая, но дает выигрыш в промежуточных ситуациях. Скорость в алгоритме Бойера-Мура достигается за счет того, что удается пропускать те части текста, которые заведомо не участвуют в успешном сопоставлении. Данный алгоритм, называемый БМ- поиском, основывается на необычном соображении – сравнение символов начинается с конца образа, а нес начала. Как и при работе КМП-алгоритма, перед началом поиска образу сопоставляется таблица
d, используемая в дальнейшем при смещении образа по строке. При создании матрицы
d используется таблица кодов символов
ASCII. Любой печатный или служебный символ имеет свой код. Например, код символа
n – 110, g – 103, код пробела – 32. Коды ASCII печатных символов приведены в табл. 2.1. Таблица ASCII содержит 256 символов. Поэтому матрицу
d можно объявить, как одномерный целочисленный массив, состоящий из 256 элементов
int d[256]. Таблица 2.1 Таблица ASCII. Печатные символы Код Символ Код Символ Код Символ Код Символ Код Символ Код Символ Код Символ пробел 55 7 78 Р
231 з
33
!
56
8
79 Си Т
233 й
35
#
58
:
81 У
234 к
36
$
59
;
82
R
105
i
168
Ё Ф
235 л
37
%
60
<
83
S
106
j
184
ё Хм Ц
237 н
39
'
62
>
85 А Ч
238 о
40
(
63
?
86 Б Ш
239 п
41
)
64
@
87 В Щ
240 р
42
*
65
A
88 Г Ъ
241 с
43
+
66
B
89 Д Ы
242 т
44
,
67
C
90 Е Ь
243 у
45
-
68
D
91 Ж Эф З Ю
245 х
47
/
70
F
93 И Я
246 ц
48
0
71
G
94 Й а
247 ч
49
1
72
H
95 К б
248 ш
50
2
73
I
96 Л вы Мг ь
52
4
75
K
98 Н д
253 э
53
5
76
L
99 О ею П ж
255 я

39 Первоначально всем элементам матрицы
d присваивается значение равное длине образа. Длину образа можно получить, используя функцию
int strlen(char *) из библиотеки <string.h>. Следующим шагом является присвоение каждому элементу таблицы
d, индекс которого равен коду ASCII текущего рассматриваемого символа образа, значения равного удаленности текущего символа от конца образа. Рассмотрим формирование таблицы
d на примере образа «Hooligan». Поскольку данный образ содержит 8 символов, то его длина равна 8. Соответственно, на начальном этапе все элементы массива
d инициализи- руются числом 8:
d[0] = d[1] =… = d[254] = d[255] = 8 Далее происходит присваивание элементам массива
d соответствующих значений, равных расстоянию от рассматриваемого символа до конца образа. При этом индекс элемента массива
d, который получает новое значение, определяется кодом ASCII рассматриваемого символа. Так, код ASCII последнего символа образа «
Hooligan» – n – равен 110. Поскольку данный символ является последним, то удаленность от конца образа равна нулю. Таким образом
d[110] = 0; также на языке программирования Си можно записать
d[‘n’] = 0; Код ASCII предпоследнего символа образа «
Hooligan» – a – равен 97. Удаленность данного символа от конца образа равна 1 и следовательно
d[97] = 1; или
d[‘a’] = 1; Аналогичным образом изменяются значения элементов таблицы
d, соответствующие символам образа
g, i, l, o:
d[‘g’] = 2; или d[103] = 2;
d[‘i’] = 3; или d[105] = 3;
d[‘l’] = 4; или d[108] = 4;
d[‘o’] = 5; или d[111] = 5; В том случае, когда образ содержит несколько одинаковых символов, элементу таблицы
d, соответствующему данному символу, присваивается значение равное удаленности от конца образа самого правого из одинаковых символов. Так, образ «
Hooligan» содержит два символа o, удаленность от конца образа первого из них равно шести, удаленность второго – пять. В этом случае
d[‘o’] будет равно пяти. В рассматриваемом примере присваивание значений элементам массива происходит при продвижении по образу справа налево. Таким образом, легко определить, был ли уже рассмотрен тот или иной символ, выполнив проверку на содержимое соответствующего элемента массива
d: если элемент содержит значение равное длине образа, значит данный символ еще не рассматривался. Поэтому, когда будет рассматриваться второй символ образа «
Hooligan», символ o, удаленность которого от конца образа равна шести, проверка содержимого соответствующего элемента массива
d покажет, что
d[‘o’] неравно длине образа – 8, что свидетельствует о том, что символ
o уже встречался, и d[‘o’] изменяться не должно. Следует отметить также, что элементу массива
d, соответствующего символу
H, должно быть присвоено значение семь, те. фактическая удаленность символа
H от конца образа
d[‘H’] = 7; или d[72] = 7; На рис. 2.8 показан образ и значения элементов массива
d, соответствующие символам данного образа. образ
Hooligan значения элементов массива d:
75543210 Рис. 2.8. Образ и значения элементов массива d, соответствующие символам образа Вторым этапом работы алгоритма БМ-поиска после построения таблицы является собственно сам поиск образа в строке. При сравнении образа со строкой образ продвигается по строке слева направо, однако посимвольные сравнения образа и строки выполняются справа налево. Сравнение образа со строкой происходит до тех пор, пока 1) небу- дет рассмотрен весь образ, что говорит о том, что соответствие между образом и некоторой частью строки найдено 2) не закончится строка, что значит, что вхождений, соответствующих образу в строке нет 3) не произойдет несовпадения символов образа и строки, что вызывает сдвиг образа на несколько символов вправо и продолжение процесса поиска. В том случае, если произошло несовпадение символов, смещение образа по строке определяется значением элемента таблицы
d, причем индексом данного элемента является код ASCII символа строки. Подчеркнем, что, несмотря на то, что массив
d формируется на основе образа, при смещении индексом служит символ из строки. На рис. 2.9. показан пример работы алгоритма БМ-поиска, сравниваемые символы подчеркнуты.

41
Hoola-Hoola girls like Hooligans.
Hooligan
Hooligan
Hooligan
Hooligan
Hooligan Рис. 2.9. Поиск образа в строке по методу Боуера и Мура На первой итерации произошло несовпадение символа образа
n и символа строки o. Значение элемента d[‘o’] равно пяти. Поэтому образ смещается вправо на пять символов. Если бы произошло совпадение этих двух символов, то далее рассматривался бы предпоследний символ образа и соответствующий ему символ строки и т. д. При очередном сравнивании образа и строки происходит несовпадение символов
n и g. Опять же используя в качестве индекса элемента массива код ASCII символа строки g, получаем значение два d[‘g’] = 2. Образ сдвигается на два символа вправо. Таким образом, образ постепенно сдвигается по строке до тех пор, пока образ в строке не будет найден или не кончится строка. Эффективность БМ-алгоритма. Замечательным свойством БМ- поиска является то, что почти всегда, кроме специально построенных примеров, он требует значительно меньше
N сравнений. В самых же благоприятных обстоятельствах, когда последний символ образа всегда попадает на несовпадающий символ строки, число сравнений равно
N / M. Контрольные задания
1. Написать программу поиска образа в строке по методу Кнута,
Морриса и Пратта. Предусмотреть возможность существования в образе пробела. Ввести опцию чувствительности / нечувствительности к регистру.
2. Написать программу поиска образа в строке по методу Боуера и Мура. Предусмотреть возможность существования в образе пробела. Ввести опцию чувствительности / нечувствительности к регистру.
3. Ниже приведен листинг программы, формирующей таблицу
d по
КМП-алгоритму. При каком образе таблица
d будет сформирована неверно При какой строке и каком образе положительный результат не будет получен
m = strlen(img); n = strlen(str);
j = 0; k = –1; d[0] = –1;
while(j < m) {
while((k >= 0) && ( img[j] != img[k]))
k = d[k];
j++; k++;

42
if (img[j] == img[k]) d[j] = d[k];
else d[j] = k;
}
4. Реализовать в программе алгоритм прямого поиска строки и КМП- алгоритм. Сравнить эффективность поиска образа в строке обоими алгоритмами по количеству итераций.
5. Реализовать в программе алгоритм прямого поиска строки и БМ- алгоритм. Сравнить эффективность поиска образа в строке обоими алгоритмами по количеству итераций.
6. Разработать и программно реализовать усовершенствованный алгоритм, объединяющий БМ-алгоритм с КМП-алгоритмом, который при сдвиге образа использует две таблицы, полученные согласно данным алгоритмам. СОРТИРОВКА МАССИВОВ В данной главе представлены принципиальные алгоритмы внутренней сортировки. Задача сортировки состоит в упорядочивании элементов в неубывающем (невозрастающем) порядке. Основным условием, предъявляемым к алгоритмам сортировки, является экономное использование доступной памяти. Это предполагает, что перестановки элементов с целью их упорядочивания должны выполняться на том же месте, те. методы, в которых элементы из массива а передаются в результирующий массив b, представляют существенно меньший интерес. Эффективность алгоритмов сортировки массива можно оценить по таким критериям, как число необходимых сравнений элементов (Си число перестановок элементов (М. Эти числа фактически являются функциями от п – числа сортируемых элементов. Хотя хорошие алгоритмы сортировки требуют порядка
n *
log(n) сравнений, рассмотрим несколько простых и очевидных методов, называемых прямыми, где требуется порядка n
2
сравнений элементов. Разбор прямых методов целесообразен ввиду следующих причин
1. Прямые методы особенно удобны для объяснения характерных черт основных принципов большинства сортировок.
2. Программы этих методов легко понимать, и они коротки. Следует помнить, что сами программы также занимают память.
3. Усложненные методы требуют небольшого числа операций, но эти операции обычно сами более сложны, и поэтому для достаточно малых прямые методы оказываются быстрее, хотя при больших n их использовать, конечно, не следует. Методы сортировки на том же месте можно разбить в соответствии с определяющими их принципами натри основные категории
 Сортировки с помощью включения (by insertion).
 Сортировки с помощью выбора (by selection).
 Сортировки с помощью обмена (by exchange). Все рассматриваемые алгоритмы будут оперировать массивом
а,в котором будут храниться переставляемые на месте элементы. Данный массив объявляется следующим образом
int a[n];
1   2   3   4   5   6   7   8   9   ...   14

3.1. Сортировка с помощью прямого включения При сортировке элементов массива с помощью прямого включения массив делят на две части отсортированную или готовую аи неотсортированную или исходную а, …,
a
n

44 Вначале работы алгоритмы в качестве отсортированной части массива принимается только один первый элемента в качестве неотсортиро- ванной части – все остальные элементы. На каждом шаге, начиная си увеличивая i каждый раз на единицу, из неотсортированной последовательности извлекается й элемент и вставляется в отсортированную так, чтобы не нарушить в ней упорядоченности элементов. Каждый шаг алгоритма включает четыре действия
1. Взять й элемент массива, он же является первым элементом в неотсортированной части, и сохранить его в дополнительной переменной.
2. Найти позицию
j в отсортированной части массива, куда будет вставлен й элемент, значение которого теперь хранится в дополнительной переменной. Вставка этого элемента не должна нарушить упорядоченности элементов отсортированной части массива.
3. Сдвиг элементов массива с
i – 1 позиции пою на один элемент вправо, чтобы освободить найденную позицию для вставки.
4. Вставка взятого элемента в найденную ю позицию. На рис. 3.1 приведены два первых шага работы алгоритма сортировки с помощью прямого включения. Отсортированная и неотсортированная части отделены вертикальной чертой. Шаг 1. i = 2, j = 1 Шаг 2. i = 3, j = 2 35 12 28 47 20 31 12 35 28 47 20 31 12 35 28 47 20 31 12 28 35 47 20 31 Рис. 3.1. Первые два шага алгоритма сортировки с помощью прямого включения На рис. 3.2 показан пример работы алгоритма сортировки массива из шести элементов. Отсортированная часть массива подчеркнута.
35 12 28 47 20 31
i = 2 12 35 28 47 20 31
i = 3 12 28 35 47 20 31
i
=
4 12 28 35 47 20 31
i
=
5 12 20 28 35 47 31
i
=
6 12 20 28 31 35 47 Рис. 3.2. Пример сортировки с помощью прямого включения
12 28

45 Анализ алгоритма сортировки с прямым включением.
Число сравнений ключей (
C
i
) прим проходе самое большее равно i – 1, самое меньшее – 1. Если предположить, что все перестановки из
n элементов равновероятны, то среднее число сравнений –
i / 2. Число же перестановок М равно
C
i
+ 2. Данный алгоритм показывает наилучшие результаты работы в случае уже упорядоченной исходной части массива, наихудшие – когда элементы первоначально расположены в обратном порядке. Алгоритм с прямым включением можно легко улучшить, если обратить внимание на то, что готовая последовательность, в которую надо вставить новый элемент, сама уже упорядочена. Естественным является остановиться на двоичном поиске, при котором делается попытка сравнения с серединой готовой последовательности, а затем процесс деления пополам идет до тех пор, пока не будет найдена точка включения. Такой модифицированный алгоритм сортировки называется методом с двоичным включением insertion). К несчастью, улучшения, порожденные введением двоичного поиска, касаются лишь числа сравнения, а не числа необходимых перестановок. А поскольку движение элемента, те. самого элемента, и связанной с ним информации занимает значительно больше времени, чем сравнение двух ключей, то фактически улучшения не столь уж существенны, ведь важный член М таки продолжает оставаться порядка n
2
. И на самом деле, сортировка уже отсортированного массива потребует больше времени, чем в случае последовательной сортировки с прямыми включениями. Этот пример показывает, что очевидные улучшения часто дают не столь уж большой выигрыш, как это кажется на первый взгляда в некоторых случаях (случающихся на самом деле) эти улучшения могут фактически привести к ухудшениям. После всего сказанного сортировка с помощью включения уже не кажется столь удобным методом для цифровых машин включение одного элемента с последующим сдвигом на одну позицию целой группы элементов не экономно. Остается впечатление, что лучший результат дадут методы, где передвижка, пусть и на большие расстояния, будет связана лишь с одним-единственным элементом.
3.2. Сортировка с помощью прямого выбора При сортировке с помощью прямого выбора массив также делится на две части отсортированную или готовую последовательность а, …,
a
i–1
инеотсортированную или исходную – а, …, Метод прямого выбора в некотором смысле противоположен методу прямого включения. При прямом включении на каждом шаге рассматриваются только один первый) элемент исходной последовательности и все

46 элементы готовой последовательности. При этом отыскивается точка включения этого элемента в готовую последовательность так, чтобы при вставке не нарушить ее упорядоченности. При прямом же выборе происходит поиск одного элемента из исходной последовательности, который обладает наименьшим (наибольшим) значением, и уже найденный элемент помещается вконец готовой последовательности. На момент начала сортировки методом прямого выбора готовая последовательность считается пустой, соответственно, исходная последовательность включает в себя все элементы массива. Алгоритм сортировки с помощью прямого выбора можно описать следующим образом
1. Из всего массива выбирается элемент с наименьшим значением.
2. Он меняется местами с первым элементом а 3. Затем этот процесс повторяется с оставшимися
n – 1 элементами,
n – 2 элементами и т. д. до тех пор, пока не останется один элемент с наибольшим значением. Рассмотрим несколько первых шагов алгоритма сортировки с помощью прямого выбора на примере упорядочивания по возрастанию следующего массива
35 12 28 47 20 31 На первом шаге готовая последовательность не содержит ни одного элемента. Выбираем наименьший элемент из исходной последовательности, куда пока что входят все элементы массива. Таким элементом является второй элемент массива, значение которого – 12. Он меняется местами с первым элементом. Теперь готовая последовательность включает в себя один элемент –
12. На рис. 3.3, иллюстрирующем текущее состояние массива, готовая последовательность подчеркнута сплошной линией. Наименьший элемент исходной последовательности – 20, он должен занять место второго элемента. Оба эти элемента выделены на рис. 3.2 полужирным шрифтом.
12
35 28 47 20 31 Рис. 3.3. Первый шаг алгоритма сортировки с помощью прямого выбора На втором шаге готовая последовательность состоит уже из двух элементов 12 и 20. Наименьший элемент исходной последовательности –
28. Он является третьим элементом массива и именно эту позицию он

47 должен занять. Поэтому на рис. 3.4 полужирным шрифтом выделен только один данный элемент.
12 20
28 47 35 31 Рис. 3.4. Второй шаг алгоритма сортировки с помощью прямого выбора На рис. 3.5 наглядно показаны все шаги алгоритма сортировки с помощью прямого выбора отмечены готовые последовательности и пере- ставляемые элементы 12 28 47 20 31
i = 2 12 35 28 47 20 31
i = 3 12 20 28 47 35 31
i = 4 12 20 28 47 35 31
i
=
5 12 20 28 31 35 47
i
=
6 12 20 28 31 35 47 Рис. 3.5. Пример сортировки с помощью прямого выбора Анализ алгоритма сортировки с прямым выбором При работе данного алгоритма число сравнений элементов (Сне зависит от начального порядка элементов. Можно сказать, что в этом смысле поведение данного метода менее естественно, чем поведение прямого включения. Для С имеем СВ случае изначально упорядоченных элементов число перестановок М минимально
M
min
= 3 * (
n – l). Если же первоначально элементы располагались в обратном порядке, число перестановок будет максимальным
М
maх
=
n
2
/ 4 + 3 * (
n – 1). В общем случае алгоритм с прямым выбором, как правило, предпочтительнее алгоритма прямого включения. Однако если элементы вначале упорядочены или почти упорядочены, алгоритм с прямым включением выполнит сортировку быстрее.
3.3. Сортировка с помощью прямого обмена Алгоритм прямого обмена основывается на сравнении и перестановке пары соседних элементов и продолжении этого процесса до тех пор, пока не будут упорядочены все элементы. Обмен местами двух элементов представляет собой характерную особенность данного метода, хотя, конечно, в обоих рассматривавшихся до этого методах переставляемые элементы также обменивались местами.
3.3.1. Пузырьковая сортировка Как ив методе прямого выбора при сортировке данным методом выполняется ряд проходов по массиву, сдвигая каждый раз наименьший элемент оставшейся последовательности к началу массива. Однако в отличие от рассмотренных ранее методов при пузырьковой сортировке происходит сравнивание и перестановка только соседних двух элементов. Если расположить элементы массива вертикально, то за первый проход элемент с наименьшим значением, те. самый легкий элемент, поднимется на самый верх массива и станет его первым элементом. В результате следующего прохода второй по легкости элемент поднимется к началу массива и станет его вторым элементом. Такое продвижение элементов по массиву вызывает ассоциации с пузырьком воздуха, всплывающим вводе. Отсюда и название данного метода – метод пузырька или пузырьковая сортировка. Описать алгоритм сортировки методом пузырька можно следующим образом. На ом проходе алгоритма (1 ≤ in) рассматриваются в обратном порядке элементы массива с го пой включительно. Сравниваются только соседние элементы. При этом, если производится сортировка по возрастанию и элемент
a[j] оказывается меньше предшествующего ему
a[j – 1], то они меняются местами. Если говорить терминами готовой и исходной последовательностей, то элементы до го представляют собой готовую последовательность, ас го пой исходную. Вначале готовая последовательность пуста. По мере работы алгоритма элементы всплывают из исходной последовательности и занимают место в конце готовой. На рис. 3.6 показана работа алгоритма сортировки методом пузырька. Перемещение элементов к началу массива показано стрелками.
i = 1
i =2
i =3
i = 4
i = 5
i = 6
i = 7
i = 8 21 4 4 4 4 4 4 4 25 21 9 9 9 9 9 9 9 25 21 12 12 12 12 12 17 9 25 21 17 17 17 17 43 17 12 25 21 21 21 21 12 43 17 17 25 25 25 25 4 12 43 32 32 32 32 32 32 32 32 43 43 43 43 43 Рис. 3.6. Пример пузырьковой сортировки

49 Улучшения этого алгоритма напрашиваются сами собой. На примере рис. 3.6 видно, что три последних прохода не влияют на порядок элементов поскольку, они уже отсортированы. Очевидный прием улучшения этого алгоритма – запоминать, были или небыли перестановки в процессе некоторого прохода. Если в последнем проходе перестановок не было, тора- бота алгоритма может быть завершена. Это улучшение, однако, можно опять же улучшить, если запоминать не только сам факт, что обмен имел место, но и положение (индекс) последнего обмена. Ясно, что все пары соседних элементов выше этого индекса уже упорядочены. Поэтому просмотр можно заканчивать на этом индексе, а не идти до заранее определенного нижнего предела
i.
3.3.2. Шейкерная сортировка Пример пузырьковой сортировки, представленный на рис. 3.6, отражает некоторую своеобразную асимметрию работы алгоритма легкий пузырек всплывает сразу – за один прохода тяжелый тонет очень медленно – за один проход на одну позицию. Например, массив
15 29 31 55 70 93 8 с помощью пузырьковой сортировки будет упорядочен за один прохода для сортировки массива
93 8 15 29 31 55 70 потребуется шесть проходов. Это наводит на мысль о следующем улучшении чередовать направление просмотра на каждом последующем проходе. Алгоритм, реализующий такой подход, называется «шейкерной сортировкой. Рис. 3.7 иллюстрирует сортировку данным методом тех же (рис. 3.6) восьми элементов. Переменные
L и R содержат индексы элементов, до которых должен происходить просмотр при движении влево (или вверх) и вправо (или вниз) соответственно.
L =
1 2 2 3 3
R
=
8 8 7 7 6 направление






21 4 4 4 4 25 21 21 9 9 9 25 9 21 12 17 9 17 12 17 43 17 25 17 21 12 43 12 25 25 4 12 32 32 32 32 32 43 43 43 Рис. 3.7. Пример шейкерной сортировки

50 Если ввести переменную, которая будет отвечать зато, были липе- рестановки элементов, то алгоритм шейкерной сортировки выполнит сортировку за пять проходов, в то время как для пузырьковой сортировки без улучшений потребовалось бы семь проходов. Если же к алгоритму шейкерной сортировки добавить переменную
k, содержащую индекс последнего перестановленного элемента, то число проходов сократится до четырех, как показано на примере рис. 3.7. Анализ пузырьковой и шейкерной сортировок.
Число сравнений в базовом обменном алгоритме – алгоритме пузырьковой сортировке – равно С = (n
2

n) / 2, а минимальное и максимальное число перестановок элементов равно
M
min
= 0, М = 3 * (
n
2

n) / 4. Анализ же улучшенных методов, особенно шейкерной сортировки, довольно сложен. Стоит отметить, что все перечисленные выше усовершенствования не влияют на число перемещений, они лишь сокращают число излишних проверок. К несчастью, обмен местами двух элементов – чаще всего более дорогостоящая операция, чем их сравнение. Поэтому очевидные на первый взгляд улучшения дают не такой уж большой выигрыш, как ожидалось.
Шейкерная сортировка с успехом используется лишь в тех случаях, когда известно, что элементы почти упорядочены, что на практике бывает редко. Анализ показывает, что обменная сортировка и ее усовершенствования фактически оказываются хуже сортировок с помощью включений и выбора.
3.4. Сортировка Шелла Все методы прямой сортировки фактически передвигают каждый элемент на всяком элементарном шаге на одну позицию. Поэтому они требуют порядка
n
2
таких шагов. Следовательно, в основу любых улучшений алгоритмов сортировки должен быть положен принцип перемещения на каждом такте элементов на большие расстояния. Можно показать, что среднее расстояние, на которое должен продвигаться каждый из
n элементов вовремя сортировки, равно
n / 3 позиций. Эта цифра является целью в поиске улучшений, те. в поиске более эффективных методов сортировки. Рассмотрим улучшенный метод, основанный на методе прямого включения – сортировке с помощью включений с уменьшающимися расстояниями. В 1959 г. Д. Шеллом было предложено усовершенствование

51 алгоритма сортировки с помощью прямого включения. Рассмотрим его работу на примере следующего массива
35 28 49 79 45 11 89 70 91 67 54 19 13 24 Сначала отдельно группируются элементы, отстоящие друг от друга на расстоянии 4. Таких групп будет четыре, они показаны на рис. 3.8 (
аг). Элементы, принадлежащие одной группе, объединены дугами. а
35 28 49 79 45 11 89 70 91 67 54 19 13 24 б
35 28 49 79 45 11 89 70 91 67 54 19 13 24 в
35 28 49 79 45 11 89 70 91 67 54 19 13 24 г
35 28 49 79 45 11 89 70 91 67 54 19 13 24 Рис. 3.8. Разбиение массива на группы элементов, отстоящих друг от друга на четыре Следующим шагом выполняется сортировка внутри каждой из групп методом прямого включения. Сначала сортируются элементы 35, 45, 91,
13, затем 28, 11, 67, 24, следом 49, 89, 54, и, наконец, 79, 70, 19. В результате получаем массив
13 11 49 19 35 24 54 70 45 28 89 79 91 67 Такой процесс называется четверной сортировкой. Следует отметить, что алгоритм сортировки Шелла также является алгоритмом сортировки на месте. Поэтому все перестановки происходят водном и том же массиве. Следующим проходом элементы группируются так, что теперь вод- ну группу входят элементы, отстоящие друг от друг на две позиции. Таких групп две, они показаны на рис. 3.9 (
аб). а
13 11 49 19 35 24 54 70 45 28 89 79 91 67 б
13 11 49 19 35 24 54 70 45 28 89 79 91 67 Рис. 3.9. Разбиение массива на группы элементов, отстоящих друг от друга на два

52 Вновь в каждой группе выполняется сортировка с помощью прямого включения. Это называется двойной сортировкой, ее результатом будет массив
13 11 35 19 45 24 49 28 54 67 89 70 91 79 И наконец, на третьем проходе идет обычная или одинарная сортировка с помощью прямого включения (рис. 3.10).
13 11 35 19 45 24 49 28 54 67 89 70 91 79 Рис. 3.10. Группа элементов, отстоящих друг от друга на один Результатом работы алгоритма сортировки Шелла является отсортированный массив
11 13 19 24 28 35 45 49 54 67 70 79 89 91 На первый взгляд можно засомневаться если необходимо несколько процессов сортировки, причем и каждый включаются все элементы, тоне добавят ли они больше работы, чем сэкономят Однако на каждом этапе либо сортируется относительно мало элементов, либо элементы уже довольно хорошо упорядочены и требуется сравнительно немного перестановок. Очевидно, что такой метод в результате дает упорядоченный массив. Видно также, что каждый проход от предыдущих только выигрывает (т. к. каждая сортировка объединяет две группы, уже отсортированные сортировкой. Расстояния в группах можно уменьшать по-разному, лишь бы последнее было единичным, ведь в самом плохом случае последний проходи сделает всю работу. Однако совсем не очевидно, что такой прием уменьшающихся расстояний может дать лучшие результаты, если расстояния не будут степенями двойки. При выполнении лабораторных работ рекомендуется начальный шаг взять равным нач =
n / 3, где n – количество элементов массива, и уменьшать его на каждом проходе вдвое
h
i–1
=
h
i
/ 2. Как отмечалось выше
h
конеч
= 1. Анализ сортировки Шелла.
Анализ этого алгоритма поставил несколько весьма трудных математических проблем, многие из которых так еще и не решены. В частности, неизвестно, какие расстояния дают наилучшие результаты. Известно, однако, что выбор расстояний должен быть таким, чтобы взаимодействие различных цепочек проходило какможно чаще. В работе Кнут [3] показал, что имеет смысл использовать следующую последовательность (она записана в обратном порядке 1, 4, 13, 40,
121, ..., где
h
k–1
= 3
h
k
+ 1,
h
t
(или
h
конеч
) = 1 и
t = log
3
(
n) – 1. Он рекомендует и другую последовательность 1, 3, 7, 15, 31, ... где
h
k–1
= 2
h
k
+ 1,
h
t
= 1 и
t =
log
2
(
n) – 1. Математический анализ показывает, что в последнем случае для сортировки
n элементов методом Шелла затраты пропорциональны
n
1.2
, что значительно лучше
n
2
, необходимых для методов прямой сортировки. Сравнение различных алгоритмов сортировки Проанализируем эффективность различных алгоритмов сортировки массивов, состоящих из
n сортируемых элементов. Анализ будет проводиться по двум критериям числу необходимых сравнений элементов Си числу перестановок элементов М. Для всех прямых методов сортировки можно дать точные аналитические формулы. Они приведены в табл. 3.1. Таблица 3.1 Сравнение прямых методов сортировки Метод Минимальное Среднее Максимальное Прямое включение С =
M =
n – 1 2(n – 1)
(n
2
+ n – 2) / 4
(n
2
– 9n – 10) / 4
(n
2
n) / 2 – 1
(n
2
– 3n – 4) / 2 Прямой выбор С =
M =
(n
2
n) / 2 3(n – 1)
(n
2
n) / 2
n * (ln n + 0.57)
(n
2
n) / 2
n
2
/ 4 + 3(n – 1) Прямой обмен С =
M =
(n
2
n) / 2 0
(n
2
n) / 2
(n
2
n) * 0.75
(n
2
n) / 2
(n
2
n) * 1.5 В табл. 3.2 приведены фактические значения показателей
C и M для массива с числом элементов равным тысячи. Для сортировки Шелла, как и для других усовершенствованных методов, простых и точных формул не существует. Однако в случае сортировки Шеллавычислительные затраты составляют в среднем
n
1.2
, в то время как для прямых методов затраты составили бы Таблица 3.2 Значения показателей C и M
1   2   3   4   5   6   7   8   9   ...   14