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

Костюк - Основы программирования

.pdf
Скачиваний:
134
Добавлен:
30.05.2015
Размер:
1.3 Mб
Скачать

31

i:=a; while i<=b do begin S; i:=i+1 end

(2.12)

и

 

for i:=a to b do S

(2.13)

а также эквивалентны циклы

 

i:=b; while i>=a do begin S; i:=i-1 end

(2.14)

и

 

for i:=b downto a do S

(2.15)

Эквивалентность условных операторов if и case. Эквивалентны операто­

ры

 

case i of a1:S1; a2:S2 else S3 end

(2.16)

и

 

if i=a1 then S1 else if i=a2 then S2 else S3

(2.17)

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

Пример 2.7. Докажем, что алгоритм (2.5) вычисляет минимальное значение в це­

лочисленном массиве A из n элементов.

 

 

 

 

 

 

min:=A[1];

 

 

 

for i:=2 to n

do

(2.5)

 

if min>A[i]

then min:=A[i]

 

Доказательство. Используя эквивалентность (2.12) – (2.13), можно записать предусловие: i=2 и постусловие: i=n+1. Тогда инвариант цикла будет следую­

щим:

min= min{A[1],...,A[i-1]},

где функция min обозначает минимальное значение из совокупности аргументов. То, что записанное соотношение действительно является инвариантом, доказывается непосредственной проверкой по тексту алгоритма.

Конец примера.

Приведенный список взаимно эквивалентных алгоритмов далеко не полный, его необходимо постоянно пополнять по мере создания новых алгоритмов. Еще одной разновидностью эквивалентности является относительная эквивалентность.

32

Определение 2.1. Пусть имеются два алгоритма, A и B, в которых над множе­ ством переменных M0 исполняются одни и те же действия. Кроме того, в алгорит­ ме A имеются также действия над множеством других переменных M1 , а во втором алгоритме – над множеством переменных M2. Тогда алгоритмы A и B являются эк­ вивалентными относительно множества переменных M0.

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

2.3Разработка сложных алгоритмов и доказательство их правильности

Понятие сложности алгоритма неоднозначно. Опытный программист может счи­ тать алгоритм из 10 страниц текста простым, а для начинающего бывает, что алго­ ритм из 20 строк текста кажется непостижимо сложным. Кроме того, на сложность алгоритма влияет его структура. Если, например, в алгоритме кроме строго последо­ вательных вычислений по формулам больше нет ничего, то алгоритм можно считать простым независимо от длины. Конечно, и в этом случае его необходимо проверять и тестировать. Однако здесь все определяется самими формулами: если формулы пра­ вильны и записаны в алгоритме без ошибок, то и сам алгоритм правильный, поэтому проверка необходима лишь для того, чтобы убедиться, что при переписи формул в алгоритм в них не внесли новых ошибок.

Любая конструкция в алгоритме, изменяющая строго последовательное выполне­ ние операторов, значительно усложняет структуру алгоритма и, соответственно, усложняет его проверку. Особенно "вредным" является оператор goto, для которо­

го к тому же не существует эффективных методов доказательства правильности. Ча­ сто бывает, что алгоритм с двумя–тремя goto оказывается гораздо сложнее алго­ ритма с многими десятками операторов if-then-else. Именно поэтому на языке

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

Простой алгоритм можно придумать весь сразу, разработать более-менее слож­ ный алгоритм таким "способом" не удается. Наиболее удобен для сложных алгорит­ мов метод сверху–вниз, называемый также методом поэтапной разработки алго­ ритма. Идея состоит в том, что внутри сложного алгоритма отдельные его части представляют как вспомогательные алгоритмы второго уровня. И поэтому вначале придумывают структуру из операторов (верх), связывающую алгоритмы второго уровня, которые еще не написаны. После этого переходят к детальной разработке и написанию отдельных алгоритмов второго уровня (низ). Конечно, уровней может быть и больше двух, большая программа может содержать, например, 8–10 уровней.

33

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

При создании сложного алгоритма никакие полезные советы не помогут, если программист не накопил достаточного опыта разработки хотя бы простых алгорит­ мов. Этому, в свою очередь, можно научиться, лишь тщательно изучив большое ко­ личество "типовых" алгоритмов, не слишком сложных, но очень широко встречаю­ щихся, как алгоритмы второго уровня, во многих больших программах. Квалифици­ рованный программист всегда пытается разработать большую программу в виде структуры операторов, связывающей известные ему алгоритмы. Эти алгоритмы при­ ходится, как правило, модифицировать, подгонять под конкретные условия. Таким образом, чтобы успешно разрабатывать сложные алгоритмы, программист должен:

1)знать и понимать большое количество типовых алгоритмов;

2)уметь модифицировать типовые алгоритмы;

3)уметь из типовых алгоритмов собирать, как из "кирпичиков", сложные алго­ ритмы.

Тестирование сложного алгоритма также является непростым делом. Его можно значительно облегчить, если придерживаться структуры алгоритма. Можно, напри­ мер, начинать тестирование с неполного алгоритма, содержащего часть операторов первого уровня и один из алгоритмов второго уровня. Конечно, этот неполный алго­ ритм должен выдавать осмысленный результат, чтобы его можно было проверить. На следующем этапе к тестируемому алгоритму необходимо добавить еще один алго­ ритм второго уровня и т.д. Тогда при обнаружении ошибочной работы алгоритма ошибку следует искать в операторах, добавленных именно на последнем этапе.

Особенно непросто доказывать правильность сложных алгоритмов. Для этого применяется метод абстракции, который состоит в следующем. Предварительно для алгоритма первого уровня и для каждого алгоритма второго уровня задают предусло­ вия и постусловия. После этого доказательство проводят отдельно для каждого алго­ ритма второго уровня, абстрагируясь от остальных операторов алгоритма. Наконец, при доказательстве правильности алгоритма первого уровня абстрагируются от структуры алгоритмов второго уровня и используют только пред- и постусловия для них. Рассмотрим следующий пример двухуровневого доказательства.

Пример 2.8. Докажем, что алгоритм (2.6) упорядочивает по возрастанию число­ вой массив A из n элементов. Этот алгоритм получил название сортировки выбо­

ром.

Доказательство. Используем эквивалентности (2.12) – (2.13). Запишем преду­ словие i=2 и постусловие i=k+1 для внутреннего цикла. Тогда инвариант этого

цикла будет следующим:

34

mi= argmin{A[1],...,A[i-1]},

где функция argmin обозначает номер элемента массива с минимальным значением из перечисленных в аргументах argmin. Этот внутренний цикл является модификаци­ ей алгоритма (2.5), и доказательство инварианта для него проводится аналогичным образом. Тогда окончательное постусловие для внутреннего цикла (после подстанов­ ки i=k+1):

mi= argmin{A[1],...,A[k]}.

for k:=n downto 2 do begin mi:=1;

for i:=2 to k do

if A[mi]>A[i] then mi:=i;

z:= A[mi]; (2.6)

A[mi]:= A[k]; A[k]:=z

end

Для внешнего цикла предусловие: k=n, постусловие: k=1. Для него инвариант следующий:

{A[j],j=1,...,n}=inv,

A[k+1]...A[n],

A[1]A[k+1],...,A[k]A[k+1].

Здесь первая часть условия означает, что набор значений n элементов масси­ ва A не изменяется, вторая часть – что элементы массива A с k+1 по n упорядо­ чены, а третья часть – что элементы массива A с 1 по k не больше элемента A[k+1]. Инвариант доказывается непосредственной проверкой по тексту алгоритма

с учетом того, что последние три присваивания внутри внешнего цикла, обмениваю­ щие значениями два элемента массива A, являются модификацией ранее рассмот­ ренного алгоритма (2.1). При подстановке в инвариант постусловия k=1 получим постусловие A[1]...A[n], которое и доказывает правильность алгоритма.

Конец примера.

Многие алгоритмы второго (и более) уровня являются модификациями типовых алгоритмов с известным доказательством правильности. Правильность таких алго­ ритмов можно доказывать методом эквивалентов.

2.4 Исследование эффективности алгоритмов

Любой алгоритм для своего выполнения требует двух основных ресурсов – про­ странства и времени. Пространство измеряется объемом памяти для входных и вы­ ходных данных, переменных внутри алгоритма, а также памятью, необходимой для

35

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

В то же время объем памяти для размещения внутренних переменных в алгорит­ ме может различаться для разных алгоритмов, решающих одну и ту же задачу. Алго­ ритм, которому требуется меньше памяти, более эффективен по памяти. Пусть объем памяти зависит от целочисленного входного параметра n. Если для работы алгоритма потребуется массив размерностью 10 n элементов, то говорят, что такому алгоритму требуется линейная память, а если n2/2 элементов – то квадратичная па­ мять и т.п. При разных функциях объема памяти более эффективен тот алгоритм, у которого функция с увеличением n растет медленнее. Например, для всех n > 20 функция n2/2 больше, чем функция 10 n. Если функция есть сумма частей, по-разно­ му растущих с увеличением n, то достаточно учитывать ту ее часть, которая растет быстрее всего при больших n. Например, функция n2/2 + 10 n является квадратич­ ной, так как уже при n = 100 первая часть в 5 раз больше, чем вторая.

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

Если для двух алгоритмов, решающих одну и ту же задачу, получены функции объема памяти и трудоемкости, то как определить, который из алгоритмов более эф­ фективен? Типична ситуация, когда первый алгоритм более эффективен по памяти, зато второй – по трудоемкости. В этом случае, как правило, предпочитают алгоритм, более эффективный по трудоемкости. Дело в том, что у большинства алгоритмов функция объема памяти растет медленнее, чем функция трудоемкости. Кроме того, при выполнении алгоритма на компьютере время является более дефицитным ресур­ сом. Это связано, прежде всего, с тем, что с развитием техники объем памяти в компьютерах растет быстрее, чем быстродействие процессоров. Так, за последние 20 лет объем памяти "среднего" компьютера увеличился примерно в 10000 раз, а бы­ стродействие – "всего" в 1000 раз.

Мы предполагали, что функции объема памяти и трудоемкости зависят только от одного параметра n. Однако для очень многих алгоритмов ситуация оказывается го­

36

раздо сложнее. Пусть, например, алгоритм обрабатывает числовой массив из n эле­ ментов. Часто бывает, что время работы алгоритма зависит не только от n, но и от конкретных значений элементов и от их взаимного расположения. Тогда надо поста­ раться аналитически получить функции объема памяти и трудоемкости для наихуд­ шего случая. Пессимист был бы доволен: он все предусмотрел. Однако оптимисту это вряд ли понравилось бы, так как во многих случаях оказалось бы, что алгоритм работает гораздо быстрее или тратит гораздо меньше памяти, чем предсказано выве­ денными функциями.

Что касается объема памяти, то потакать оптимисту не стоит: даже если в одном случае из ста программе не будет хватать выделенной памяти (быть может, всего нескольких байтов!) и программа из-за этого аварийно прекратит работу, то это, в конце концов, не понравится даже оптимисту. В то же время, если изредка для полу­ чения результата придется подождать подольше, то чаще всего с этим можно сми­ риться. Но для этого необходимо знать (наряду с временем работы в наихудшем) так­ же среднее время работы алгоритма, т.е. функцию трудоемкости от n в среднем. Аналитически вывести такую функцию можно методами теории вероятностей, однако, как правило, это весьма непросто. Поэтому для алгоритма, реализованного в виде конкретной программы, производят экспериментальные испытания, так называ­ емый численный эксперимент или моделирование, с целью вычислить среднее вре­ мя работы для различных n. При этом достаточно надежные численные результаты можно получить, лишь применив методы математической статистики.

Рассмотрим методы анализа объема памяти и трудоемкости в наихудшем. Ана­ лиз строится на основе доказательства правильности алгоритма с использованием свойств операторов языка программирования.

Анализ объема памяти. Объем памяти, необходимый для входных и выходных данных, определяется самой задачей, и, тем самым, не зависит от конкретного алго­ ритма. Объем памяти, необходимый для размещения промежуточных переменных при работе алгоритма, нередко задается непосредственно в самом алгоритме описа­ нием простых переменных и массивов. При этом требуется детальный анализ того, что размеры массивов, длины которых зависят от n, будут достаточны для правиль­ ной работы алгоритма при любом конкретном n, которое задано на входе.

Заметим, что в рассмотренных выше примерах алгоритмов объем памяти для промежуточных переменных является константой – он не зависит от размерности входных данных.

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

Анализ трудоемкости в наихудшем. Для проведения такого анализа необходи­ мо знать трудоемкость основных операторов и различных структур алгоритмов.

37

Трудоемкость присваиваний. Каждая арифметическая или логическая опера­ ция над скалярными данными (не массивами), а также непосредственно присваива­ ние выполняется за определенное (константное) время. Пусть в присваивании выпол­ няется m операций, каждая из них имеет трудоемкость t1, …, tm соответственно.

Общая трудоемкость T равна их сумме:

 

T = t1 + … + tm.

(2.18)

Трудоемкость последовательного алгоритма: S1; …; Sm, в которой каждый из алгоритмов, входящих в последовательность, имеет трудоемкость t1, …, tm соот­ ветственно, также определяется их суммой (2.18).

Трудоемкость ветвящегося алгоритма:

if B then S1 else S2

в котором трудоемкость выполнения проверки условия B равна tb , а трудоемкость выполнения алгоритмов S1 и S2 – t1 и t2 соответственно, определяется равен­

ством:

T = tb + max(t1, t2).

(2.19)

Трудоемкость циклического алгоритма:

 

while B do S

 

в которой трудоемкость выполнения проверки условия

B равна tb , трудоемкость

выполнения алгоритма S t, причем t – константа, и в котором известно общее количество выполнений цикла n, определяется равенством:

T = (n + 1) tb + n t.

(2.20)

Пример 2.9. Трудоемкость алгоритма (2.3) вычисления факториала, так же как и эквивалентного ему алгоритма (2.4) – линейная. Действительно, алгоритм содержит начальные присваивания и цикл, выполняемый n – 1 раз. Причем трудоемкость каж­ дого выполнения цикла – константа, так как в цикле производится лишь несколько присваиваний, среди которых самое трудоемкое – это умножение.

Трудоемкость алгоритма (2.5) вычисления минимального значения среди элемен­ тов массива – также линейная, так как выполняемый n – 1 раз цикл содержит услов­ ный оператор, имеющий в наихудшем константную трудоемкость.

Трудоемкость алгоритма (2.6) упорядочивания элементов массива (сортировки выбором) – квадратичная. Действительно, алгоритм содержит внешний цикл, выпол­

няемый n – 1 раз и внутренний цикл, выполняемый k – 1 раз, где

k изменяется от

n до 2. Таким образом, общее количество

T выполнений внутреннего цикла:

T = (n – 1) + (n – 2)

… + 1 = n (n – 1)/2 ≈ n2 / 2 .

(2.21)

Конец примера.

Исследуем алгоритм (1.8), для которого докажем правильность и подсчитаем трудоемкость.

38

Пример 2.10. Вначале докажем завершимость алгоритма (1.8). Для этого введем следующее определение инверсии элементов массива:

Пара элементов массива X[i] и X[j], i<j образует инверсию, если вы­ полняется условие: X[i]>X[i+1].

Подсчитаем максимально возможное количество инверсий в массиве из n элементов. Если i=1, то при различных j от 2 до n возможно не более чем n- 1 инверсия, если i=2, то возможно не более, чем n-2 инверсии, и т.д. Тогда об­ щее максимально возможное число инверсий I равно сумме

I = (n-1)+(n-2)+...+1 = n*(n-1)/2.

Цикл в алгоритме выполняется таким образом, что если условие X[i]<=X[i+1] внутри цикла оказалось истинным, то переменная i увеличивается на 1, а если лож­ ным (т.е обнаружена инверсия) то элементы X[i] и X[i+1] обмениваются свои­ ми значениями, в результате чего общее количество инверсий уменьшается на 1. Та­

ким образом, за конечное количество шагов цикла число инверсий в массиве умень­ шится до нуля. Но тогда переменная i будет увеличиваться на 1 до тех пор, не станет равной n, после чего цикл закончит повторения.

Предусловие цикла в алгоритме: i=1. Постусловие: i=n.

Инвариант: {A[j],j=1,...,n}=inv, A[1]...A[i].

Доказательство инварианта легко проверяется непосредственно по алгоритму. Подсчитаем трудоемкость алгоритма для наихудшего случая, когда в массиве

имеется максимально возможное количество инверсий. Заметим, что если условие X[i]<=X[i+1] оказалось ложным, то это значит, что: X[1]<=X[2]<=...<=

X[i], т.е. цикл, начиная с i=1, проработал i раз. Тогда, чтобы упорядочить все

элементы до (i+1)–го: X[1]<=X[2]<=...<= X[i+1] цикл должен выполниться

R(i) раз:

 

R(i) =i+(i-1)+...+1 = i*(i+1)/2.

 

Таким образом, чтобы в массиве были устранены все инверсии, цикл в алгоритме

должен проработать T(n) раз:

 

T(n) = R(1) + R(2) +…+ R(n – 1) ≈ n3 /6.

(2.22)

В формуле (2.22) отброшены слагаемые, имеющие степень n меньшую, чем 3. При выводе использована формула (2.1) для суммы квадратов чисел от 1 до n.

Конец примера.

Таким образом, из двух алгоритмов (1.8) и (2.6), решающих одну и ту же задачу

упорядочения массивов, алгоритм (1.8) гораздо менее эффективен,

чем алго­

ритм (2.6). В табл. 2.1приведены значения таких функций, как élog2 nù ,

n élog2 nù ,

n2, n3, 2n, которые характеризуют трудоемкость ряда типовых алгоритмов.

39

Превосходство более эффективного алгоритма особенно наглядно при больших значениях n. Так, алгоритму (2.6) для упорядочения массива из миллиона элементов придется выполнить 5∙1011 циклов (n2/2), а алгоритму (1.11) – 166∙1015 циклов (n3/6). Если компьютер выполняет по 109 циклов в 1 секунду, что соответствует быстродей­ ствию около 4–5 млрд. операций в секунду, то алгоритм (2.6) будет работать 8 минут, а алгоритм (1.8) – более 5 лет!

Таблица 2.1

n

élog2 nù

n élog2 nù

n2

n3

2n

4

2

8

16

64

16

10

4

40

100

1000

1024

20

5

100

400

8000

≈106

100

7

700

10000

106

≈1030

1000

10

10000

106

109

≈10300

106

20

20∙106

1012

1018

≈10300000

К счастью, идея алгоритма (1.8) не так плоха: алгоритм можно значительно улуч­

шить, внеся в него небольшие изменения, а именно, заменив строчку «i:=1» на «if i>1 then i:=i-1». В результате получим алгоритм:

i:=1;

while i<n do

if X[i]<=X[i+1] then i:=i+1

else begin (2.7) z:=X[i];X[i]:=X[i+1];X[i+1]:=z

if i>1 then i:=i-1 end

Доказательство правильности измененного алгоритма почти не отличается от до­ казательства алгоритма (1.8): даже инвариант цикла будет таким же. Что же касается трудоемкости, то легко доказать, что количество выполнений цикла будет около n2, что больше, чем у алгоритма (2.6), всего в два раза. Действительно, если на какомлибо этапе работы алгоритма при упорядочении массива оказалось, что:

X[1]<=X[2]<=...

<= X[i], X[i]>X[i+1],

(2.23)

то, чтобы для элементов в начале массива выполнялись соотношения:

X[1]<=X[2]<=...

<= X[i+1],

(2.24)

40

в наихудшем случае цикл должен выполниться 2i раз. А для того, чтобы полностью упорядочить массив, потребуется не более 2 + 4 + … + 2(n – 1) = n (n – 1) выполне­ ний цикла.

Чтобы избавить алгоритм (2.7) от половины работы, введем еще одно усовершен­ ствование, записав дополнительный внутренний цикл, который превращает преду­ словие (2.23) в постусловие (2.24). Во внешнем цикле на каждом шаге i будет уве­ личиваться на 1. В целом, новый алгоритм, получивший название обменной сорти­ ровки, будет таким:

 

 

for i:=1 to n-1 do

 

begin j:=i;

 

while (j>0)and(X[j]>X[j+1]) do

 

begin

(2.8)

z:=X[j]; X[j]:=X[j+1]; X[j+1]:=z

 

j:=j-1;

 

end

 

end

 

Полное доказательство правильности алгоритма (2.8) читателю предлагается провести самостоятельно.

Вопросы и задания

1.В чем состоит доказательство правильности алгоритма? Что значит «доказать заверши­ мость алгоритма»?

2.Что такое предусловие и постусловие, как их задавать для алгоритма?

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

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

5.При доказательстве какой структуры алгоритма применяется метод математической ин­ дукции?

6.При доказательстве какой структуры алгоритма применяется метод инварианта?

7.При доказательстве каких алгоритмов применяется метод абстракции?

8.В чем состоит доказательство правильности методом эквивалентов?

9.Доказать методом математической индукции, что 1 + 2 + … + n = n (n + 1)/2 .

10.Дан целочисленный двумерный массив из n строк и m столбцов. Написать алгоритм вы­ числения суммы всех его элементов. Какова трудоемкость этого алгоритма?

11.Провести доказательство правильности алгоритма (2.8) и вывести его трудоемкость.