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

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

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

Ю.Л. Костюк

ОСНОВЫ ПРОГРАММИРОВАНИЯ РАЗРАБОТКА И АНАЛИЗ АЛГОРИТМОВ

Учебное пособие

УДК 681.142.2

Костюк Ю.Л. Основы программирования. Разработка и анализ алгоритмов: Учебное пособие. –

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

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

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

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

Рецензент –

ISBN

© Ю. Л. Костюк, 2004

Предисловие

Самым главным в профессии программиста является умение создавать хорошие программы. Все же остальное (знание языка программирования, транслятора, опера­ ционной системы, умение быстро работать за компьютером) необходимо лишь по­ стольку, поскольку помогает писать хорошие алгоритмы и, тем самым, разрабаты­ вать хорошие программы.

К сожалению, большинство учебников по программированию мало внимания уделяет алгоритмизации – науке об алгоритмах. В этой книге не только рассматрива­ ются и исследуются важнейшие классы простых алгоритмов. В ней также излагаются основы технологии программирования и методы доказательства правильности алго­ ритмов. Книга написана на основе лекций, читавшихся на протяжении более 10 лет для студентов первого курса факультета информатики ТГУ.

Для успешного освоения начального университетского курса программирования необходимы предварительные знания по математике и информатике в объеме сред­ ней школы. Весьма желательным является не простое знакомство с каким-либо язы­ ком программирования, но умение записывать на этом языке программы для реше­ ния простых задач, а также владение навыками отладки таких программ на компью­ тере. В книге используется язык Паскаль, точнее, его базовое подмножество, доста­ точное для написания большинства алгоритмов. В него входят такие основные эле­ менты Паскаля, как:

1)целые, вещественные и символьные типы данных;

2)одно- и двумерные массивы, символьные строки;

3)присваивания, формулы, арифметические операции;

4)условные операторы if и case, операции сравнения, логические операции;

5)циклы while и for;

6)процедуры и функции, их описание, вызов, подстановка параметров;

7)стандартные процедуры и функции.

Для тех случаев, когда перечисленных элементов Паскаля недостаточно, они описываются дополнительно. Это, в частности, последовательные файлы, записи данных, указатели и динамическое распределение памяти. Когда требуется учиты­ вать особенности конкретного варианта языка Паскаль, алгоритмы излагаются в рас­ чете на транслятор Turbo Pascal® фирмы Borland.

Выбор в пользу Паскаля сделан из следующих соображений:

4

1)Паскаль был создан для начального обучения программирования и с успехом выполняет свою роль: более чем в половине школ, где изучают программирование, используют именно Паскаль;

2)Паскаль широко применяется также в профессиональном программировании для разработки больших программных комплексов, например, при использовании си­ стемы Delphi® фирмы Borland;

3)после освоения Паскаля легко изучать другие языки профессионального про­ граммирования, в частности, Cи.

В книге также приведены краткие сведения о языке Cи, как языке программиро­ вания для настоящих профессионалов, а также примеры программ на Cи.

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

Отзывы, замечания и предложения по книге просьба присылать по адресу:

634050, г. Томск, пр. Ленина, 36, ТГУ, факультет информатики

E-mail: kostuk@inf.tsu.ru

Глава 1 Тестирование и отладка программ

1.1Алгоритмы и программы

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

В математике требуется более строгое определение алгоритма. Понятие алгорит­ ма начало складываться со времен Евклида (около 300 г. до н.э.), однако только в 1930-е годы появилась математическая теория алгоритмов. Согласно этой теории, под алгоритмом понимается совокупность правил, определяющих процедуру реше­ ния любой задачи из некоторого множества задач. Алгоритм можно представить в виде устройства, на вход которого подаются некоторые входные данные, а на выхо­ де, в процессе исполнения алгоритма, формируются выходные данные (рис. 1.1).

Входные

 

 

Алгоритм

 

Выходные

данные

 

данные

 

 

 

 

 

 

 

 

Рис. 1.1

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

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

Еще одно свойство алгоритма – существование для него исполнителя, который понимает все отдельные действия в алгоритме и реализует их в таком порядке, как предписано структурой алгоритма. Если исполнителем является человек, то алгоритм

6

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

Алгоритмы можно определять и записывать многими способами, в частности, ис­ пользуя естественный (русский или английский) язык. Однако такую запись трудно сделать однозначно понимаемой для человека и практически невозможно – для компьютера. Поэтому для записи алгоритмов применяют искусственные языки про­ граммирования. Так как компьютер понимает только свой внутренний (машинный) язык, то алгоритм должен быть записан на машинном языке (тогда его называют ма­ шинной программой). Создание машинной программы – сложная и кропотливая ра­ бота, так как команды машинного языка выполняют простые действия и кодируются числами. Поэтому для реализации даже простого алгоритма потребуется машинная программа большого размера. Изобретение в 1950-х годах языков программирования наконец-то избавило программистов от копания в машинных командах и привело к настоящей революции в программировании.

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

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

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

Текст алгоритма на языке программирования называют исходным модулем, а транслированную программу на машинном языке – исполняемым модулем. Весь процесс трансляции и исполнения программы представлен на рис. 1.2.

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

7

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

Входные

данные

Исходный

Транслятор

 

 

Исполняемый

 

Выходные

 

 

 

 

 

 

 

данные

модуль

 

 

модуль

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Компьютер

Рис. 1.2

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

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

семантической ошибки.

Таким образом, чтобы быть уверенным в правильности программы, необходимо провести ее испытание (тестирование) на различных входных данных. Можно гипо­ тетически представить, что нам удалось проверить программу, исполняя ее столько раз для различных данных, сколько всего существует их возможных вариантов. Та­

8

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

Однако в большинстве случаев исчерпывающее тестирование невозможно физи­ чески. Рассмотрим следующий простой пример. Пусть, например, требуется прове­ рить программу умножения двух 10-значных неотрицательных десятичных чисел. Количество различных значений для каждого сомножителя равно 1010, а количество различных пар сомножителей – 1020. Даже если выполнять по 109 проверок в секунду, для полного тестирования потребуется более трех тысяч лет! Если же испытывать программу не на всех вариантах входных данных, то нет никакой гарантии, что для произвольно выбранного варианта программа выдаст правильный результат: как раз для этого (непроверенного!) варианта программа может ошибиться. На основе подоб­ ных рассуждений Э. Дейкстра сформулировал закон: «Тестированием можно до­ казать наличие ошибок в программе, но никогда – их отсутствие».

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

После того, как при выполнении какого-либо теста обнаружен неправильный ре­ зультат, необходимо найти причину ошибки и устранить ее. Этот процесс называется отладкой. Поиск ошибки может оказаться непростым делом, ведь требуется найти именно то действие в программе, из-за которого произошла ошибка. Иногда для это­ го приходится полностью переосмыслить алгоритм и переписать программу заново. Это происходит, если программа пишется бессистемно, наобум. При таком «методе» приходится делать много попыток, пока не удастся написать нечто удобоваримое. Если же подойти к разработке программы грамотно, то, как правило, большинство ошибок будут иметь характер «описок», которые, как правило, легко обнаруживают­ ся и легко исправляются.

9

Втехнологии программирования предлагается два основных метода тестирова­ ния небольших программ: метод черного ящика и метод белого ящика. Рассмотрим оба метода.

1.2Тестирование методом черного ящика

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

Конкретный вариант входных данных (тест) представляет собой набор данных, поступающих на вход алгоритма. Множество всех потенциально возможных тестов будем называть множеством входов I. Тогда все множество допустимых входных данных, т.е. таких данных, для которых алгоритм в принципе способен вычислить правильный результат, является некоторым подмножеством множества входов I. Это подмножество будем называть областью входов P I . Аналогично определим

множество выходов O, образованное всеми потенциально возможными варианта­ ми выходных данных. Областью выходов Q O будем называть множество вари­

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

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

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

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

Для обоснования выбора тестов сделаем предположение о «разумности» состави­ теля программы. А именно, предположим, что внутренняя структура программы поз­

 

10

воляет всю

область входов P разделить на непересекающиеся подобласти Pi Ì P ,

Pi Ç Pj = Æ

при i ≠ j, называемые подобластями эквивалентности. Подобласти

{Pi} таковы, что для любых двух тестов из одной и той же подобласти в программе выполняются одни и те же действия и в одном и том же порядке. Поэтому, если алго­ ритм выдает неверный результат для теста t'i Î Pi , то он выдаст неверный результат

и для теста t''i Î Pi ,

а из того, что результат для теста t'i правильный, следует, что

результат и для теста

t''i также правильный. При выполнении этого предположения

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

Иногда задача разбиения области входов P на подобласти Pi вызывает затруд­ нение. Тогда можно попытаться аналогичным образом всю область выходов Q раз­ делить на такие непересекающиеся подобласти Qj Ì Q ( Qj Ç Qk = Æ при j ≠ k), что

для любых двух вариантов выходных данных q' j ,q'' j , принадлежащих одной и той же подобласти выходов q' j ,q'' j ÎQj , в алгоритме выполняются одни и те же дей­

ствия в одном и том же порядке.

Таким образом, задача разработки тестов методом черного ящика состоит в сле­ дующем:

1) разбиение области входов P на подобласти эквивалентности Pi и для каждой из подобластей Pi выбор какого-либо принадлежащего ей теста ti ;

2) разбиение области выходов Q на подобласти Qj, для каждой из подобластей Qj выбор какого-либо принадлежащего ей варианта выходных данных qj и соответ­ ствующего ему теста t j Î P .

Тогда {ti }È{t j } Ì P есть полное множество тестов. Следует заметить, что, так

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

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