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

Демидов Основы программирования в примерах на языке ПАСЦАЛ 2010

.pdf
Скачиваний:
128
Добавлен:
16.08.2013
Размер:
1.28 Mб
Скачать

дует указывать условие завершения, а для цикла while – условие выполнения.

Оператор цикла for

Оператор предназначен для описания циклов с параметром, записывается с помощью ключевых слов for, to/downto, do и читается так: «Изменяя <параметр-переменную> от <начального значения> до <конечного значения>, выполнять <инструкции>».

Если требуется указать несколько инструкций в теле цикла, то следует использовать составной оператор begin…end.

Параметр цикла всегда меняется на единицу на каждом следующем витке цикла, а с помощью ключевых слов to и downto можно указать направление изменения. Если требуется, чтобы параметр цикла возрастал с каждым витком, то следует использовать ключевое слово to, при этом начальное значение должно быть не больше конечного значения параметра:

for i:=1 to 7 do begin

end;

Чтобы параметр цикла убывал с каждым витком, следует использовать ключевое слово downto, при этом начальное значение должно быть не меньше конечного значения параметра:

for i:=10 downto 1 do begin

end;

Операционная семантика оператора цикла for:

1)присвоить параметру цикла начальное значение;

2)если текущее значение параметра цикла не превышает конечное значение (для цикла to), то выполнить оператор в теле цикла, иначе завершить выполнение цикла;

3)вычислить новое значение параметра цикла;

4)перейти на шаг 2.

Замечания:

если начальное значение параметра цикла равно конечному, то выполнится один виток цикла, а если начальное значение больше конечного (для to), то ни одного;

не следует менять значение параметра цикла явно;

51

значение параметра цикла после завершения цикла считается неопределённым.

Операторы управления циклами: continue, break, exit

Оператор continue досрочно завершает очередной виток цикла и переходит к новому витку.

Оператор break досрочно прерывает выполнение цикла и передаёт управление оператору, следующему за оператором цикла.

Оператор exit досрочно прерывает выполнение процедуры или программы.

Все три оператора не имеют параметров.

Правила применения операторов цикла

Если требуется проверять выполнимость некоторого условия до витка цикла, то имеет смысл использовать оператор цикла с предусловием.

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

Если заранее известно количество витков цикла, то разумно использовать оператор цикла с параметром.

Если известно максимальное количество витков цикла и в то же время цикл следует прервать, когда условие повторения перестанет выполняться, то можно использовать как циклы с условием, так и цикл с параметром в сочетании с оператором break.

Проиллюстрируем последнее правило на двух примерах:

// первый вариант i := 1;

while Condition and (i <= Max) do begin

i := i+1; end;

// второй вариант

for i:=1 to Max do begin

if not Condition then break;

end;

52

Алгоритмы сортировки и поиска

Вернёмся к задаче сортировки и поиска. Чтобы построенные схемы можно было реализовать на ЭВМ, необходимо выбрать типы данных и переписать алгоритм в терминах операторного языка.

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

Начнём с более простой задачи поиска минимального значения в массиве. Заметьте, что нас будет интересовать не само минимальное значение, а его местонахождение, т.е. индекс элемента массива. По индексу элемента в массиве всегда можно выяснить его значение. Для хранения индекса текущего минимального значения нам понадобится дополнительная переменная minIndex.

Блок-схема алгоритма поиска приведена на рис. 4. По завершении алгоритма индекс элемента с минимальным значением будет храниться в переменной minIndex. Программу, реализующую этот алгоритм, можно написать как с помощью цикла while, так и с помощью цикла for:

const

Max = 200; type

TCubes = array [1..Max] of integer; var

cubes: TCubes;

i, minIndex: integer; begin

//инициализация массива

//поиск минимального элемента minIndex := 1;

for i:=2 to Max do begin

if cubes[i] < cubes[minIndex] then minIndex := i;

end;

// вывод на экран минимального значения writeln(cubes[minIndex]);

end.

53

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

Установить minIndex = индекс первого элемента массива

Установить i = индекс второго элемента массива

Индекс i < индекса последнего элемента массива

Нет

Да

Элемент с индексом i < элемента с индексом minIndex?

Да

Нет

Установить minIndex = i

Увеличить i на 1

Рис. 4. Блок-схема алгоритма поиска

54

Сортировка

Будем считать, что исходные данные также хранятся в массиве. Результат сортировки поместим в дополнительный массив той же длины. Для простоты допустим, что числа в массиве не повторяются. Так как убирать элементы из массива нельзя, то при повторных просмотрах будем игнорировать элементы, уже вставленные в результирующий массив. Их определить просто: они все меньше или равны элементу, вставленному в результирующий массив последним. Поэтому запомним индекс последнего вставленного в результирующий массив элемента в переменной CountIndex. Одновременно её значение будет равно числу уже вставленных элементов.

Построим блок-схему алгоритма сортировки (рис. 5).

По завершении алгоритма индекс CountIndex равен индексу последнего элемента исходного массива. Программа может выглядеть следующим образом:

const

Max = 200; type

TCubes = array [1..Max] of integer; var

cubes, sorted: TCubes;

i, minIndex, CountIndex: integer; begin

//инициализация массива

//поиск первого минимального элемента minIndex := 1;

for i:=2 to Max do begin

if cubes[i] < cubes[minIndex] then minIndex := i;

end;

// вставка первого элемента sorted[1] := cubes[minIndex];

for CountIndex := 2 to Max do begin

//поиск минимального элемента большего,

//чем sorted[CountIndex-1]

minIndex := 1;

for i:=2 to Max do begin

if (cubes[i] > sorted[CountIndex-1]) and (cubes[i] < cubes[minIndex]) then minIndex := i;

end;

// вставка

sorted[CountIndex] := cubes[minIndex];

55

end;

// вывод результата на экран

end.

Найти индекс минимального элемента исходного массива

Поместить элемент в начало результирующего массива

Установить CountIndex = 2

СountIndex < индекса по-

следнего элемента массива

Да Нет

Найти индекс минимального элемента исходного массива, который больше элемента, помещённого результирующий массив последним

Поместить элемент в результирующий массив по индексу CountIndex

Увеличить CountIndex на 1

Рис. 5. Блок-схема алгоритма сортировки

56

Данная программа не способна сортировать массивы с повторяющимися элементами, однако её можно модифицировать или реализовать иной способ сортировки.

Оценка сложности алгоритмов и оптимизация

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

Два ресурса – память и время – являются основными при оценке алгоритмов. При сегодняшнем уровне развития вычислительной техники стоимость памяти упала настолько, что ресурс памяти перестал быть дефицитным и оптимизация по объему памяти отошла на второй план. Если раньше нехватка памяти приводила к большим изменениям архитектуры программ, то сейчас это влияние минимизировано. Главный невосполнимый ресурс – время – вышел на первый план. Несмотря на то, что быстродействие техники выросло на порядки, это не повод писать медленные программы и транжирить память. Следует помнить, что выросли размер и сложность программ, а компьютеры теперь решают гораздо более сложные задачи.

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

Оценка требуемой памяти складывается из объема памяти, необходимого для размещения значений всех переменных в течение всего времени выполнения программы. Время выполнения можно оценить только относительно исполнителя, т.е. конкретной ЭВМ, что мало говорит о качестве самого алгоритма. В связи с этим оценивают не само время, а количество команд (операций, шагов, инструкций), выполняемых во время работы алгоритма (программы), с учетом относительного времени выполнения каждой команды. Конечно, базисный набор команд также зависит от исполнителя, но

57

большинство современных ЭВМ имеет общие архитектурные корни, что снижает значимость этого различия.

Оценим сложность алгоритма поиска минимального элемента в массиве:

minIndex := 1;

for i:=2 to Max do begin

if cubes[i] < cubes[minIndex] then minIndex := i;

end;

Емкостная сложность вычисляется просто: требуется n = Max ячеек для хранения элементов массива, а также ячейки для переменных minIndex, I и константы Max. Если все они типа Integer, под который отводятся два байта, то всего получается

(n + 3)*2 = 2*n+6 байт.

Оценим временную сложность алгоритма. Очевидно, в любом случае выполняются следующие инструкции: одно начальное присваивание, n–1 присваиваний значения счетчику цикла i и столько же сравнений в условии оператора if. Если копать глубже, то для витка цикла требуется также проверка условия выполнения цикла i<=n, переход к следующему витку и т.п, однако, как будет показано далее, по большому счёту всё это не имеет большого значения для общей сложности.

Присваивание внутри ветвления выполняется в зависимости от истинности условия. Можно предположить, что в среднем при вероятности 50/50 будет выполнено (n–1)/2 присваиваний. Обращение к элементу массива по индексу, вообще говоря, требует некоторого времени для вычисления адреса ячейки памяти, но для простоты будем считать доступ по индексу мгновенным, тем более, что счетчик цикла for неявно как раз и соответствует адресу ячейки памяти с элементом массива.

Тогда всего получается 1+(n–1)+(n–1)+(n–1)/2 = 2.5*n–1.5 ко-

манды. Если предположить, что присваивание и сравнение выполняются за один квант времени, то в среднем потребуется ave = 2.5*n–1.5 кванта времени.

Рассмотрим некоторые экстремальные случаи. Если массив отсортирован по возрастанию, то минимальный элемент является первым элементом массива. В этом случае присваивание внутри ветвления вообще не выполняется, т.е. суммарное количество ко-

58

манд равно min = 1+(n–1)+(n–1) = 2*n–1. Это нижняя оценка сложности алгоритма – минимально возможное время выполнения. Если массив отсортирован по убыванию, то минимальный элемент будет последним в массиве, а присваивание внутри ветвления будет выполняться на каждом витке цикла. Т.е. суммарное количество ко-

манд равно max = 1+(n–1)+(n–1)+(n–1)=3*n–2. Это верхняя оценка сложности алгоритма.

Видно, что все полученные оценки линейно зависят от длины исходного массива, несмотря на различные коэффициенты пропорциональности. Длина массива в этом случае является наиболее важной характеристикой задачи, её размерностью или величиной, обусловливающей сложность задачи. Полученная зависимость означает, что при увеличении размерности задачи (увеличении массива) время поиска будет возрастать линейно, поэтому говорят о линейной сложности алгоритма, что записывается как O(n). Именно определение зависимости сложности решения от размерности задачи является целью оценки сложности алгоритма. Коэффициенты пропорциональности имеют значение лишь при сравнении алгоритмов одного класса сложности.

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

Оценим сложность алгоритма сортировки. Емкостная сложность складывается из двух массивов длины n, трёх переменных и константы, что даёт (2*n + 4)*2 = 4*n + 8 байт.

Для оценки временной сложности этого алгоритма удобно использовать предыдущие оценки для алгоритма поиска, поскольку он часто здесь используется. В любом случае выполняется алгоритм поиска, далее присваивание, далее n–1 раз в цикле выполняется алгоритм поиска и присваивание. Таким образом, всего получаем n раз для алгоритма поиска + n присваиваний:

ave = n * (2.5*n–1.5) + n = 2.5 * n2 – 0.5 * n

min = n * (2*n–1) + n = 2 * n2 max = n * (3*n–2) + n = 3 * n2 – n

Замечание: часто за среднюю оценку можно взять среднее арифметическое минимальной и максимальной оценки. Методы оценки сложности алгоритмов описаны в замечательной книге Дональда Кнута «Искусство программирования».

59

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

Почему так получилось? Из-за вложенности циклов каждый элемент исходного массива теперь просматривается не один, а n раз (по числу витков цикла верхнего уровня), т.е. каждый уровень вложенности циклов даёт увеличение сложности на порядок. Полученный алгоритм сортировки является одним из наиболее медленных. Существуют алгоритмы сортировки меньшей сложности, например O(n*log(n)). Если сравнить графики этих функций, то будет видно, что парабола растет гораздо быстрее.

60

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]