Text
                    С. ОКУЛОВ
осн □□Ш
Москва
Лаборатория Базовых|ЗйЛкМ*'
2 0 0 2

УДК 519.85(023) ББК 22.18 О 52 Окулов С. М. О 52 Основы программирования. — М.: ЮНИМЕДИАСТАИЛ, 2002. — 424 с.: ил. ISBN 5-94774-003-6 В учебнике рассмотрены основные управляющие конструкции сис- темы программирования Турбо Паскаль, процедуры н функции, стро- ковый, вещественный и файловый типы данных. Приводится матери- ал для изучения массивов, методов сортировки и поиска, а также по динамическим структурам данных Рассмотрены следующие структу- ры данных: списки, стеки, очереди, двоичные деревья, АВЛ-деревья и Б деревья. В материалах для чтения обсуждаются практически все во- просы, входящие в школьный минимум знаний по информатике Книга является достаточно полным учебником по программиро- ванию, реализующим сложную задачу — формирование у читателя структурного стиля мышления Учебным материалом является сис- тема программирования Турбо Паскаль, а также большое число за- дач, включая задачи на алгоритмы сортировки и поиска Достаточно подробно рассмотрена работа с динамическими струк- турами данных. Книга рассчитана на широкий круг читателей от школьника и студента до специалиста, решающего с помощью программирования прикладные задачи. УДК 519.85(023) ББК 22.18 Серия «Технический университет» Учебное издание Окулов Станислав Михайлович Основы программирования Художник Н Лозинская Компьютерная верстка Л Катуркиной Лицензия на издательскую деятельность Хг066140 от 12 октября 1998 г. Подписано в печать 30.01.02. Формат 60х90’/1а. Гарнитура Школьная. Бумага офсетная. Печать офсетная. Усл. печ.л. 26,5. Тираж 5000 экз. Заказ 452 ООО «Издательство Лаборатория Базовых Знаний», 2002 г. Адрес для пере- писки: 103473, Москва, а/я 9. Телефон (095)955-0398. E-mail: lbz@aha.ru Гигиеническое заключение 77.99.1.953.П.1815.4.99 от 12.04.1999 г. Отпечатано с готовых диапозитивов в полиграфической фирме «Полиграфист». 160001, г. Вологда, ул. Челюскинцев, 3. ISBN 5-94774-003-6 © Окулов С. М., 2002 © ЮНИМЕДИАСТАЙЛ, 2002
Содержание Содержание..........................................3 Предисловие.........................................5 Часть первая. Основные управляющие конструкции........................................10 Занятие № 1. Первая программа......................10 Занятие № 2. Целый тип данных......................20 Занятие № 3. Команды редактора для работы с блоками, работа с окнами....................................28 Занятие № 4. Логический тип данных, операции сдвига. ... 37 Занятие № 5. Составной оператор и оператор If......46 Занятие № 6. Оператор For.........................56 Занятие № 7. Оператор While.......................66 Занятие № 8. Оператор Repeat~Until................73 Занятие № 9. Вложенные циклы......................84 Часть вторая. Процедуры и функции — элементы структуризации програм.............................99 Занятие № 10. Одномерные массивы. Работа с элементами................................99 Занятие № 11. Процедуры...........................115 Занятие № 12. Функции.............................133 Занятие № 13. Рекурсия............................149 Занятие № 14. Символьный и строковый типы данных .... 167 Занятие № 15. Вещественный тип данных.............186 Занятие № 16. Текстовые файлы.....................202 Часть третья. Массив — фундаментальная структура данных..................................220 Занятие №17. Методы работы с элементами одномерного массива...............................220 Занятие № 18. Множественный тип данных............234 Занятие № 19. Методы сортировки...................243 Занятие № 20. Методы быстрой сортировки...........252 Занятие № 21. Поиск данных........................266 Занятие № 22. Двумерные массивы. Работа с элементами...............................288 Занятие № 23. Двумерные массивы. Вставка и удаление................................302
Основы программирования Занятие № 24. Несколько задач на технику работы с двумерными массивами...........................311 Занятие № 25. Комбинированный тип данных (записи). . . . 323 Часть четвертая. Динамические структуры данных...........................................335 Занятие № 26. Динамические структуры данных......335 Занятие № 27. Стек...............................350 Занятие № 28. Очередь............................360 Занятие № 29 Поиск в графе......................373 Занятие № 30. Двоичные деревья...................384 Занятие № 31. Сбалансированные деревья...........399 Заключение.......................................421
Предисловие Моему отцу — Окулову Михаилу Митрофановичу, рано ушедшему из этой жизни, посвящаю. Из истории возникновения учебника. В 1988 году перед ав- тором возникла проблема: «чему учить в информатике и как учить». Исходным «багажом» при принятии решения было об- разование в классическом университете по специальности при- кладная математика и более чем 15-летний опыт разработки программного обеспечения специализированных вычислитель- ных комплексов в промышленности. Попытки обучения по су- ществующим в то время учебникам, а их было не так много, как в настоящее время, не принесли ни удовлетворения, ни ре- зультатов. Информатика воспринималась как обычный пред- мет. Дидактические возможности компьютера «оставались за бортом». Может быть, причиной явилось отсутствие у автора соответствующего опыта и педагогического образования. Огра- ничимся констатацией факта, оставим критику этого результа- та, так же как и обсуждение достоинств и недостатков сущест- вующих учебников, доброжелателям. Тезисно обозначим исходные положения при принятии ре- шения. Первой посылкой было убеждение в том, что програм- мирование является «стержнем информатики». В нем синтезиро- вано все, что десятилетиями нарабатывалось в Computer Science. Это и результаты работы специалистов, работающих на «сты- ке» математики и информатики, это и достижения в вычисли- тельной технике, это и, наконец, огромный опыт формализации и решения сложнейших проблем в самом программировании, связанный с созданием больших программных комплексов. Второй исходной посылкой являлось убеждение в том, что занятия по информатике в корне должны отличаться от тра- диционных занятий по любому другому предмету. Во-первых, на занятиях по информатике должна поощряться ошибка, ибо только через ошибку можно прийти к результату, при изуче- нии же любого другого предмета ошибка карается двойкой. Во-вторых, постоянная обратная связь с обучаемым через компьютер, объективная и лишенная эмоций, — это инстру- ментарий индивидуального и развивающего обучения. В-тре- тьих, стиль мышления у программистов свой, отличающийся от стиля мышления как математика, так и любого другого специалиста. Он настроен, если так можно выразиться, на бо-
Основы программирования рьбу с хаосом. Любая сложная программа — это миллионы со- ставляющих, движущихся и взаимодействующих. И в резуль- тате этого взаимодействия должен получаться определенный результат. Представим себе техническую систему такого уров- ня сложности ... Итак, за основу обучения следует взять программирование, с максимальным использованием компьютера на занятиях, и при этом должен формироваться определенный стиль мышле- ния. В таком ключе и шла вся последующая работа. Большое влияние на нее оказала подготовка школьников к олимпиадам по информатике. Синтезировались и обобщались все педагоги- ческие находки, которые затем находили применение в препо- давании информатики и подтверждали обозначенные выше по- ложения. Основной методический принцип учебника — все познается через труд, через преодоление ошибок (собственных), через процесс решения задач. Этот принцип определяет структуру за- нятий. Вводная часть - обсуждение нового материала, экспери- менты с заготовками решений задач, самостоятельное решение задач. Данный учебник нельзя считать продуктом творчества од- ной личности. Коллеги по работе в Вятском государственном педагогическом университете и физико-математическом лицее г. Кирова сделали много для того, чтобы работа была заверше- на. Влияние моих учеников на осмысление того, как учить и чему учить, просто огромно. Я благодарен им за совместную ра- боту. В первой части учебника изучаются основные управляющие конструкции языка программирования Турбо Паскаль, но не только. Целевой установкой этих занятий было конструирова- ние решения задач из минимального числа инструкций. В про- цессе занятий необходимо достичь такого уровня их понима- ния, чтобы работа программы школьником воспринималась в динамике. Ему необходимо «видеть» эту работу, динамику из- менения значений переменных в процессе работы программы. С этой целью в начале применяется «ручная» прокрутка про- граммы, а затем — средства отладчика системы программиро- вания. В конечном итоге, использование этих инструкций уча- щимися должно осуществляться не по заученным схемам, а так же естественно, как применение русского языка. Учащемуся нужно как бы забыть об их существовании, писать их автома- тически. Таким образом закладывается первый «кирпичик» в фундамент структурного стиля мышления.
Предисловие Вторая часть учебника посвящена механизму использования процедур и функций, создания «блоков» логики с одной точкой входа и одной точкой выхода. При этом взаимодействие по дан- ным должно осуществляться по заданным правилам — в соот- ветствии с механизмом передачи параметров. Пересечение «бло- ков» как по управлению, так и по данным должно быть минимальным. Материал, так же как и в первой части учебника, разбит на занятия. Что под этим понимать? Это материал не для одного урока. Автор не дает рекомендаций по этому поводу. Все зави- сит от уровня подготовки школьников. Приведу пример. Одну из программ четвертого занятия ему приходилось объяснять и 5 минут и целое занятие (да и того не хватало): Program Му4_3; Begin WriteLn (1365 And 2730) ; WriteLn (1365 Or 2730) ; WriteLn (1365 'or 2730) ; UriteLn (1365 And $FF) ; WriteLn (1365 And $FF0); Wr^teLn(1365 And $FF00); ReadLn; End. В целом, на наш взгляд, содержание учебника и стиль изло- жения соответствуют среднему уровнмю подготовки ученика, хотя и для сильного ученика в материале каждого занятия есть достойные задачи и проблемы для исследования. Третья часть учебника посвящена, в основном, фундамента- льному понятию информатики — массиву. При этом весь мате- риал является очередным витком в освоении и закреплении це- левых установок первых двух частей. Разумеется, задачной основой этой части являются в основном алгоритмы сортиров- ки и поиска, ибо для достижения нашей цели — изучения мас- сива как структуры данных, трудно найти что-либо лучшее. Занятие по комбинированному типу данных стоит несколько особняком. Оно не согласуется с целью этой части учебника, но является необходимым для работы с четвертой частью — динами- ческими структурами данных (последним «кирпичиком» фунда- мента нашего здания). В четвертой части учебника рассмотрены динамические струк- туры данных: списки, стеки, очереди, двоичные деревья, АВЛ- деревья и Б-деревья. Обычно этот материал не затрагивается при работе со школьниками. Однако без него невозможен пере- ход на следующий «виток» обучения программированию. В объектно-ориентированных, визуальных технологиях и т. д. он
Основы программирования является одним из стержневых, без которого понимание техно- логий остается «любительским». Это один аспект. Второй, и ве- сьма немаловажный, заключается в том, что на этом материале «шлифуется» структурный стиль мышления. В задачах этой час- ти учебника без автоматизма в структуризации решений (испо- льзования принципа «разделяй и властвуй») обойтись — очень трудно. Еще одна особенность — отладка программ является достаточно сложным мероприятием. Предвидеть и рассмотреть все варианты, случаи работы программы при различных исход- ных данных очень не просто. Оксфордский словарь английского языка трактует эвристику как искусство нахождения истины. Мы уточним — искусство в убеждении себя самого и окружаю- щих в правильности работы программы. Владение этим искусст- вом при решении задач четвертой части потребуется полное. Итак, подведем итоги. Главная цель учебника — развитие мышления ученика: от алгоритмического к структурному, а за- тем к эвристическому мышлению. Речь не идет о том, что вна- чале мы обучаем одному, затем другому и, наконец, третьему. Элементы обучения синтезированы в нечто единое, и те, и дру- гие, и третьи аспекты проблемы развиваются одновременно. Обозначим на уровне общих положений каждый стиль мыш- ления. Умение написать алгоритм назовем алгоритмическим стилем мышления. Алгоритм — план будущей деятельности (модель деятельности), записанный в заранее выбранной фор- мальной системе обозначений с ограниченными возможностя- ми. Умение «расчленять» задачу, «разделять и властвовать» на- зовем структурным стилем мышления. Когда мы говорим о структуре, то обязаны сказать о том, из каких элементов она состоит, и как элементы связаны между собой. Характерные черты этого стиля: простота и ясность; использование только базовых (основополагающих) конструкций; отсутствие многоце- левых функциональных блоков и т. д. Умение находить истину, доказывать факт правильности ре- шения задачи (работы программы) назовем эвристическим сти- лем мышления. Факторами успешной эвристической деятель- ности являются умение оценивать, рациональность действий, принцип экономии и т. д. Отметим, что компьютер, система программирования не яв- ляются целью обучения, они — инструмент реализации целей, хотя при этом, разумеется, познается, в определенном объеме, и сам инструмент.
Предисловие 9 В заключении хотелось бы еще раз сказать слова благодар- ности (огромной!) всем моим коллегам и ученикам. Сергею Львовичу Островскому, главному редактору газеты «Информа- тика», хотелось бы выразить особую признательность. Его ум- ное и ироничное молчание значило для меня иногда больше, чем все жаркие дискуссии по поводу ... Моей дружной семье, все члены которой мирились с ежеве- черней работой, ибо это единственное время, которое можно было выделить для написания учебника, низкий поклон. Ви- деть постоянно чем-то озабоченного мужа и отца, наверное, со- мнительное удовольствие. Но они терпели и терпят, и я благо- дарен им, и не только за это. Все замеченные ошибки и опечатки автор с благодарностью примет (okulov@vspu.kirov.ru).
Часть первая Основные управляющие конструкции Занятие № 1. Первая программа План занятия □ загрузка Турбо Паскаля; □ структура диалога; □ первая программа — нахождение произведения двух це- лых чисел; □ компиляция, сохранение и запуск программы; □ разбор программы. Опишем основные моменты проведения занятия. Структура диалога. После загрузки системы на экране по- является три окна; [File Edit. [ — окно 1 — главное меню Line! Col 1 I —окно 2 — основное, или рабочее, окно | F1-Help || — окно 3 — окно помощи, в нем указывается назначение основных функциональных клавиш Переход из первого окна во второе и наоборот осуществляет- ся при помощи нажатия клавиши F10. Набор программы Program Му1_1 ; Var a,b,rez: Integer; Begin WriteLn ('Введите два числа через пробел'); ReadLn(a,b); rez:=a*b; WriteLn ('Их произведение равно ', rez) ; WriteLn ('Нажмите <Enter>'); ReadLn; End.
Основные управляющие конструкции ц Краткий разбор примера. Структура программы. Програм- ма начинается с заголовка, имеющего следующий вид: Program <имя программы>; за ним идет раздел описаний, в котором должны быть описаны все идентификаторы (константы, пере- менные, типы, процедуры, функции, метки), которые будут ис- пользованы в программе. После раздела описаний идет раздел операторов, который начинается со служебного слова Begin и заканчивается служебным словом End. В этом разделе задают- ся действия над объектами программы, введенными в разделе описаний. Операторы в этом разделе отделяются друг от друга точкой с запятой. После последнего слова End ставится точка. Имя этой программы Му1_1 (заметим, что в имени програм- мы не должно быть пробелов, оно должно начинаться с буквы или знака подчеркивания, состоять только из латинских букв, цифр и знаков подчеркивания, не допускается использование символов точки и запятой). Из разделов описаний имеется лишь один — раздел переменных. Он начинается со служебно- го слова Var, после которого идет последовательность объявле- ния переменных, разделенных точкой с запятой. В каждом объявлении перечисляются через запятую имена переменных (идентификаторы) одного типа, после чего ставится двоеточие и указывается тип переменных. В нашем примере описаны три пе- ременные: все они (а, Ъ и rez) имеют целый тип (Integer), то есть значениями переменных этого типа являются целые числа. Понятие переменной — центральное в любом языке программи- рования. Для описания переменной (величины, которая может изменяться в процессе работы программы) следует указать имя переменной, ее тип и значение. Следует соблюдать следующий принцип: «Использовать переменную можно лишь тогда, когда ей присвоено некоторое значение». Это позволит Вам избежать многочисленных ошибок в работе программ. После описательной части идет раздел операторов, начинаю- щийся со служебного слова Begin, после которого записывают- ся операторы языка. Первый встречающийся оператор — это WnteLn('<meKcm>'), — записать (вывести) на экран текст, за- ключенный между апострофами, Ln добавляется после Write для того, чтобы курсор автоматически переходил на следую- щую строку при выводе на экран текстов или результатов вы- полнения программы. Следующий оператор — это ReadLn(a.b); — читать данные с клавиатуры. В данном случае необходимо ввести два целых
12 Часть первая числа через пробел, тогда переменной а присваивается значе- ние, равное первому введенному числу, а переменной Ъ присва- ивается значение, равное второму введенному числу. Напри- мер, вы ввели числа 12 и 45, тогда а=12, а Ъ=45. После Write также можно ставить Ln. После этих двух операторов стоит оператор присваивания: rez:=a*b; (:= — это знак оператора присваивания в языке Пас- каль). Значение выражения из правой части оператора присваивания заменяет текущее значение переменной из левой части. Тип значение выражения обычно должен совпадать с типом пере- менной. При выполнении оператора переменная rez получит значе- ние, равное произведению числа а на число b (см. рис.). Так как в результате умножения двух целых чисел получается це- лое число, то переменная rez описана типом Integer (значения- ми которого могут быть лишь целые числа). Следующий оператор — это снова оператор WriteLn ( <текст>', rez) — он выведет на экран текст, заключенный между апострофами, а за ним значение переменной rez. Затем следующий оператор WriteLn выведет на экран сообщение: На- жмите <Enter>, а оператор ReadLn будет ожидать этого нажа- тия в окне выполнения. В конце раздела операторов стоит слу- жебное слово End с завершающей точкой. Запуск программы. Для того, чтобы запустить программу, выходим в главное меню (нажатием F10) — первое окно, выби- раем режим Run и дважды нажимаем <Enter>. На экране по- является сообщение:
Основные управляющие конструкции 13 Введите два целых числа через пробел Курсор мигает в следующей строке, вводим два целых числа через пробел и нажимаем <Enter>, после этого появляется со- общение: произведение равно. . . нажмите <Enter> Вместо точек будет написано значение переменной гег, то есть число, равное произведению первого введенного числа на второе. Это сообщение будет на экране до тех пор, пока не на- жата клавиша <Enter>. Сохранение программы. Для того, чтобы сохранить про- грамму на внешнем носителе, необходимо: □ выйти в главное меню и выбрать режим File-, □ нажать <Enter>, из появившегося окна выбрать режим Save as... и нажать клавишу <Enter>; □ в появившемся окне набрать имя файла и нажать клави- шу <Enter>.. Например, a:\pnml_l.pas; здесь а:\ — это логическое имя диска, на котором будем сохранять файл, priml_l — имя файла (оно может содержать не более 8 символов), pas — расширение, сообщающее о том, что файл содержит программу, написанную на языке Пас- каль. Примечание Для быстрого сохранения файла можно воспользоваться команда- ми Save или Save all меню File. Выход из системы программирования Турбо-Паскапъ. Для того, чтобы закончить работу, необходимо: □ выйти в главное меню и выбрать режим File-, □ нажать <Enter> и из появившегося окна выбрать режим Quit, после чего нажать либо <Enter>, либо комбинацию <Alt>-<X>. Экспериментальный раздел работы 1. Введите числа при работе программы, например 4567 и 789. Убедитесь, что у Вас получается неправдоподобный резуль- тат — отрицательное число (-1117). Найдите эксперимента- льным путем тот интервал значений переменных а и Ь, когда результат умножения правильный. 2. Вместо ввода числа введите какой-нибудь символ. Убедитесь, что выполнение программы не произошло, компьютер выдал сообщение об ошибке Error 106: Invalid numeric format.
14 Часть первая 3. Добавьте лишний знак апострофа в операторе WnteLn. Убе- дитесь, что компиляция программы не произошла, а компь- ютер Вас порадовал ошибкой Error 8: String constant exceeds line. 4. Измените в программе Му1_1 оператор гег.=а*Ь на rez:=a-(a Div b)*b. Выясните действие операции Div для переменных целого типа. Измените текстовую часть следу- ющего за оператором присвоения оператора WnteLn, отра- зив в ней результат Вашего исследования. 5. Добавьте к программе предыдущего примера переменную с именем ost, оператор присвоения ost~a Mod b и оператор вы- вода WriteLn(‘????????’,ost);. Выясните, что за действие вы- полняется с помощью оператора Mod. Замените знаки вопро- са Вашими пояснениями его работы. 6. Наберите следующую программу. Program Му1_2; Var a: Integer; Begin WriteLn('Введите целое числос); ReadLn (a); WriteLn('???????? Abs(a)); WriteLn('Нажмите <Enter>'); ReadLn; End. Вашей задачей является замена знаков вопроса в операторе WnteLn на текст, поясняющий работу операции Abs. Выполни- те аналогичное исследование для операций Sqr(a), Ord(a), Succ(a), Pred(a). Примечание Рекомендуется следующий порядок работы. Текст программы Му1_1 сохраняется под новым именем, например Pnml_2.pas, а затем из- меняется и вновь сохраняется. Это позволит сократить время на на- бор программы. Задания для самостоятельной работы 1. Изменить программу для нахождения суммы двух чисел. 2. Изменить программу для нахождения суммы четырех чисел. 3. Найти значение выражения: (a+(d-12)*3)*(c-5*k), где значе- ния переменных a,d,c и к вводятся с клавиатуры.
Основные управляющие конструкции 15 4. Нарисовать рисунок по типу рисунка, приведенного в тек- сте занятия, отражающего действия из следующего фраг- мента программы: х:=13; у:=25; 5. Изменить рисунок из предыдущего примера для отражения действий фрагмента программы при замене последних трех операторов на: у:=х+у; 6. Написать программу вывода на экран нескольких чисел в виде: 13 15 16 или 101 102 103 Материал для чтения 1. Из истории. Турбо Паскаль появился на рынке програм- мных продуктов в 1983 году и совершил революцию в програм- мировании. До этих пор предпочтение отдавалось Бейсику — простому, дешевому и легко усваиваемому. Паскаль же был ап- паратно зависимым, дорогим и сложным в обращении. С появ- лением Турбо Паскаля положение меняется. Турбо Паскаль со- стоит из языка программирования и среды программирования, которая создает удобства в работе. Изучение Паскаля как языка программирования идет вместе с изучением всей системы Турбо Паскаль. Язык программирования Паскаль был разработан Н. Виртом в 1968-1970 годах и получил широкое распространение благодаря наглядности программ и легкости при изучении. Он послужил основой для разработки других языков программирования (например, Ада, Модула-2).
16 Часть первая Первая версия Турбо Паскаля использовалась не очень дол- го _ появилась в 1983 году, а уже в 1984 году ее заменила вто- рая версия, которая получила широкое распространение. К осе- ни 1985 года появляется третья версия, более удобная в работе (быстрее работают компилятор и редактор, возможен вызов MS-DOS из программы). Четвертая версия (1988 год), представила Турбо Паскаль в новом виде (появление новой среды, компилятор стал встроен- ным). Осенью этого же года разработана пятая версия, у кото- рой еще больше развита среда и появился встроенный отлад- чик. А в 1989 году появилась версия 5.5, позволившая перейти к объектно-ориентированному программированию. Шестая версия уже обеспечивала многооконный и много- файловый режим работы, использование «мыши», применение объектно-ориентированного программирования, обладала встро- енным ассемблером и имела другие возможности. В 1992 году фирма Borland International выпустила два па- кета программирования на языке Паскаль — это Borland Pas- cal 7.0 и Turbo Pascal 7.0. Пакет Turbo Pascal 7.0 использует новейшие достижения в программировании. Язык этой версии обладает широкими воз- можностями, имеет большую библиотеку модулей. Среда про- граммирования позволяет создавать тексты программ, компи- лировать их, находить и исправлять ошибки, компоновать программы из отдельных частей, использовать модули, отла- живать и выполнять отлаженную программу. 2. Некоторые команды редактора. Режим работы Edit (глав- ное меню) — режим редактирования текста программы. управления движением курсора — перемещение курсора на символ вправо Команды — перемещение курсора на символ влево — перемещение курсора на строку вверх перемещение курсора на строку вниз — перемещение курсора в начало текущей строки — перемещение курсора в конец текущей строки — перемещение курсора на страницу вверх. — перемещение курсора на страницу вниз.
Основные управляющие конструкции 17 Примечание Страница — это число строк текста, составляющих один экран. — перемещение курсора в левый верхний угол — перемещение гурсора в левый нижний угол Команды вставки и удаления текста Insert — включение и выключение режима вставки, Примечание Если режим вставки включен, то на экране курсор имеет вид мига- ющей черты. В режиме вставки набираемый символ вводится в по- зицию, в которой стоит курсор, а все символы (начиная с символа, стоящего в позиции курсора ранее), расположенные правее, сдвига- ются вправо. Если режим вставки выключен, то набираемый сим- вол заменит тот символ, который находится в позиции курсора, та- ким образом можно старый текст заменить на новый. Delete — удаление символа, стоящего в позиции курсора Backspase — удаление символа, стоящего перед курсором. Примечание Иногда на этой клавише написано BS, а иногда это левосторонняя стрелка, расположенная над клавишей ввода (ENTER). — вставка пустой строки над строкой, где находится курсор — удаление строки, где находится курсор. 3. Данные — общее понятие всего того, с чем работает компьютер. В аппаратуре все данные преДдяивЗЯЮТеятрвк-яо— следовательности двоичных цифр (разрядов).’ Н языказмнаовк' кого уровня, а к ним относится Турбо ПаркЭЛБ, ся от деталей представления данных в (памяти кЛадм^тера.
Любой тип данных определяет множество значений, которые может принимать величина этого типа, и те операции, кото- рые можно применять к величинам этого типа. В Турбо Пас- кале работают с пятью типами данных: простыми, строковы- ми, составными, ссылочными и процедурными. К простым типам данных относятся целые, вещественный, логический, символьный, перечислимый и ограниченный (два последних определяются пользователем). На простых типах данных, кро- ме вещественного, определено отношение порядка. Что это та- кое? Все множество значений типа рассматривается как упо- рядоченное множество, и каждое значение связано с некоторым целым числом, которое есть его порядковый номер. В любо?4 порядковом типе для каждого значения, кроме первого, суще- ствует предшествующее значение, и для каждого значения, за исключением последнего, существует последующее значение. Определены следующие стандартные функции для работы с порядковыми типами: □ Ord — возвращает порядковый номер значения любого по- рядкового типа. □ Pred — возвращает предшествующее значение для задан- ного значения порядкового типа (а если заданное значе- ние первое?). □ Succ — возвращает следующее значение для заданного значения порядкового типа (а если заданное значение по- следнее?). В Турбо Паскале есть возможность конструировать (созда- вать) свои типы данных из имеющихся типов, причем весьма сложные. Итак, абстрагирование и конструирование суть кон- цепции типа данных. 4. Каждая программа взаимодействует с окружающей сре- дой с помощью операторов ввода, вывода. Если трактовать тер- мин программа очень вольно, то можно считать, что любая про- грамма что-то откуда-то берет, что-то делает с введенными данными (преобразует) и затем выводит куда-то полученные ре- зультаты. В Турбо Паскале связь программы с внешними устрой- ствами осуществляется через имена файлов. Самый простой случай, когда эти имена связаны с клавиатурой (для ввода дан- ных) и с экраном дисплея (для вывода). Для ввода с клавиатуры используется оператор Read или Re- adLn.
Основные управляющие конструкции Вызов: Read(rj,r2,...,rn) Параметры: r1,r2,...,rn имеют тип Integer или Real или Char или String. Действие: если имеет тип □ Integer или Real, то считывается одно число и значение его присваивается переменной гг При этом знаки пробела и перевода строки перед числом игнорируются; □ Char, то считывается один символ и присваивается пере- менной Гр □ String, то считывается максимум q символов при длине q строковой переменной г;. Оператор ReadLn действует так же, как и Read. Отличие в том, что после ввода осуществляется переход на начало следующей строки. Вывод на экран осуществляется с помощью операторов Wri- te или WriteLn. Вызов: 1Уп(е(г;,г,,...,гп) Параметры: r1,r2,...,rn имеют тип Integer или Real или Boolean или Char или String. Действие: на экран выводится значение в стандартном фор- мате. Работа WriteLn отличается тем, что после вывода осуществля- ется переход на начало следующей строки.
20 Занятие № 2. Целый тип данных План занятия □ обсуждение целого типа данных; □ разбор программы выделения цифр из десятичного числа;^ □ эксперименты с программой: перевод числа из десятичной системы счисления в двоичную систему счисления, выяс- нение сути арифметических операций с переменными це- лого типа, преобразование переменных целого типа; □ выполнение самостоятельной работы. Целый тип данных. Существует пять целых типов: Shor- tlnt, Integer, Longlnt, Byte, Word. Они отличаются диапазоном значений, а значит, и размером памяти, отводимой для их представления. II Тип Диапазон значений Объем памяти I Shortlnt -128 . 127 1 байт, со знаком II [ Integer -32768 .. 32767 2 байта, со знаком II Longlnt -2147483648 . . 2147483647 4 байта, со знаком Byte 0 . 255 1 байт, без знака 1 Word 0... 65535 2 байта, без знака Операции с величинами целого типа: сложение (+), вычита- ние (-), умножение (*), нахождение целой части деления (Div), нахождение остатка от деления (Mod). Так как целый тип дан- ных относится к типам, на которых определено отношение по- рядка, то работают стандартные функции Ord, Succ и Pred. Примечание Переменной целого типа присваивать значение результата обычной операции деления «/» нельзя. Убедитесь в этом с помощью про- стой модификации программы первого занятия. Попробуйте най ти объяснение этому факту. Возникают, по крайней мере, два, достаточно сложных на этой стадии освоения языка, вопроса. Почему при представле- нии целых чисел со знаком диапазон отрицательных чисел на одно значение больше диапазона положительных чисел и как выполняются операции, например, при вычислении выраже- ний, если все величины имеют разные целые типы. Разбору программы предшествует обсуждение выполнения операций Div и Mod.
Основные управляющие конструкции 21 19 Div 4=4 Т 19 Mod 4=3___|| -19Div4=-4 -19Mod4=-3 19Div-4=-4 19 Mod-4=3 1 -19 Div-4=4 I -19 Mod ~4=~3 j В программе определяются цифры трехзначного числа. Мож- но ее использовать и для определения цифр двузначного числа, просто цифра сотен в этом случае равна нулю, и это делается проще. Program Му2_1 ; Var a, one, dec, bun, rez:Integer; Begin WriteLn('Введите число'); ReadLn (a) ; one:=a Mod 10; WriteLn('Цифра единиц числа — ',one); dec: = (a Div 10) Mod 10; WriteLn('Цифра деечтков числа — ',dec); hun:=a Div 100 ; WriteLn('Цифра сотен числа - ',пнп); Rez:=hun*100+dec*10+one; WriteLn ('А это тоже число - ',rez); Write('Enter '); ReadLn; End. Например, если Вы введете число 137, то зна- 137 [1Q— чение переменной one будет равно 7, dec — 3 и 130 и 110 — j. Вспомните деление чисел столбиком. 7 10 1 — 1 Примечание Не забудьте сохранить программу под именем prim2_l.pas. Экспериментальный раздел работы 1. Измените программу Му2_1 для нахождения цифр двузнач- ного числа. Сохраните ее под именем prim.2_2.pas. 2. Измените программу Му2_1 для нахождения цифр четырех- значного числа. Сохраните ее под именем Prim.2_2.pas. 3. Деление на 10 и нахождение остатков от деления мы рассмот- рели выше. Рассмотрите пример деления столбиком на 2. Остатки от деления или 0, или 1.
Часть первая 22 О 1 Наберите следующую программу, отладьте ее и попробуйте дать объяснение полученному результату. Измените программу так, чтобы она правильно работала, например, с числом 115. Program Му2_2; Var rez:Integer; Begin WriteLn('137'); WriteLn ('10001001'); Rez:=l *128+0*64+0*32+0*16+1*8+0*4+0*2+1 *1; WriteLn (rez); ReadLn; End. 4. Наберите следующую программу: Program My2_3; Uses Crt; Var a: Integer; b:Word; rl:Integer; r2:Longlnt; Begin ClrScr;(Очистка экрана, процедура модуля Crt) а:=32000;b:=64 000; rl:=a+b; WriteLn (rl); r2:=a+b; WriteLn (r2); ReadLn; End. После запуска Вы увидите, что значение переменной rl рав- но 30464, а значение переменной г2 — 96000. Если изменить тип переменной rl на Word, то результат не изменится. Испо- льзуя информацию из таблицы, приведенной в начале занятия, измените программу так, чтобы проделать аналогичные экспе- рименты с другими целыми типами. Попробуйте найти логику получения результата компьютером. 5. Добавьте в программу Му2_3 перед оператором ReadLn сле- дующие два оператора: Wri teLn (Longlnt (100*а) ) ; Wri teLn (1 00*LongIn t (a)) ;
Основные управляющие конструкции 23 Функция Longlnt преобразует переменную типа Integer в тип Longlnt. В первом случае преобразование осуществлялось после умножения, а во втором — перед умножением. В первом случае получен результат, далекий от истины, — отрицатель- ное число -11264, во втором правильный — 3200000. Приведем основные правила, по которым в Турбо Паскале осуществляются операции с переменными целых типов. Перед выполнением операций (бинарных) над двумя операнда- ми оба операнда преобразуются к общему для них типу. Им яв- ляется тип с наименьшим диапазоном, включающим все воз- можные значения обоих типов. Например, общим типом для Integer и Byte будет Integer, для Integer и Word — Longlnt. Ре- зультат будет общего типа. Выражение в правой части оператора присваивания вычисля- ется независимо от размера или типа переменной в левой части! Перед выполнением любой арифметической операции любой операнд длиной в 1 байт преобразуется в промежуточный опе- ранд длиной в 2 байта, который является совместимым как с Integer, так и с Word. Задания для самостоятельной работы 1. Чему равны значения переменных а и b после выполнения по- следовательности действий: □ а- 15 Div (16 Mod 7); b:= 34 Mod a*5 — 29 Mod 5 + 2; □ a:= 4 * 5 Div 3 Mod 2; b~ 4 * 5 Div ( 3 Mod 2); 2. Дано двузначное число. Определить: □ сумму и произведение цифр числа; □ число, образованное перестановкой цифр исходного чис- ла. 3. Дано трехзначное число. Определить: □ сумму и произведение цифр числа; □ число, образованное перестановкой цифр исходного числа; □ число, полученное перестановкой цифр десятков и еди- ниц; □ число, полученное перестановкой цифр сотен и десятков; □ четырехзначное число, полученное приписыванием циф- ры единиц в качестве цифры тысяч (например, из числа 137 необходимо получить число 7137). Примечание Сколько различных чисел можно получить из трехзначного числа путем перестановки цифр?
Часть первая 24 4. Решить задачу 3 (кроме последнего пункта) для четырехзнач- ных чисел. Примечание Предложить максимальное количество разумных модификаций рассматриваемой задачи. 5. Арифметическая прогрессия — это последовательность чисел, в которой разность между последующим и предыдущим эле- ментами остается неизменной. Последовательность 12, 15, 18, 21, 24, ... является арифметической прогрессией, 12 — пер- вый член прогрессии (а;), разность прогрессии равна 3. Лю- бой член прогрессии вычисляется по формуле a^aj+cHn-l), где d — разность прогрессии, п — номер взятого члена. Даны а; и d. Найти п, при котором значение ап выходит за диапа- зон типа Integer (экспериментальным путем). 6. Сумма первых п членов арифметической прогрессии вычис- ляется по формуле Sn=(aI+an)*n/2. Даны а; и d. Найти п, при котором значение Sn выходит за диапазон типа Integer (экс- периментальным путем). Материал для чтения 1. Выражения. Они состоят из операций и операндов. Разли- чают бинарные операции — выполняются над двумя операнда- ми, и унарные (одноместные) — над одним операндом. Бинар- ные операции записываются в обычной математической форме, знак унарной операции предшествует операнду. Порядок вы- полнения операций в выражении определяется правилами при- оритета. Приведем их. Примечание Если некоторые из операций Вам не очень понятны, то не отчаи- вайтесь. Всему свое время. Операции Приоритет Тип операции | @, Not, +, - 1-й (высший) унарный | *, /, Div, Mod, And, Shi, Shr 2-й мультипликативный || +, Or, Xor 3-й аддитивный || | =,<> , c, >, <=, >=, m 4-й (низший) операции отношения [| Сформулируем основные правила приоритета: □ операнд, расположенный между знаками двух операций с разными приоритетами, является границей операции с бо- лее высоким приоритетом;
Основные управляющие конструкции □ операнд, расположенный между двумя операциями с рав- ными приоритетами, является границей для операции, расположенной слева; □ выражения в скобках вычисляются до того, как они будут обрабатываться как один операнд. 2. Системы счисления — способ записи чисел. Системы счис- ления, в которых вклад каждой цифры в величину числа зави- сит от ее позиции в последовательности цифр, изображающей число, называются позиционными. Число в десятичной систе- ме счисления 3377=3*1000+3*100+7*10+7 =3*103+3*102+7*101+ +7*10° — вклад как цифр 3, так и цифр 7 различен. Это же число в пятеричной системе счисления записывается как 102002 (3377= 1*55 +0*54 +2*53 +0*52 +0*51+2*5°). Подскажем, что 55=3125, а 53=125. Это же число в двоичной системе счис- ления записывается как 110100110001 (3377 = 1*2И +1*210 + +0*29 + 1*28 +0*27 +0*2® +1*25 + 1*24 +0*23 +0*22 +0*2‘ +1*2°). Приведем значения степеней двойки: 2п=2048, 21о=1О24, 29 = = 512, 28 =256 и т. д. Алгоритм перевода целого числа из деся- тичной системы счисления в двоичную: делим исходное число на 2 нацело в десятичной системе счисления и считаем новым значением числа целую часть частного, остаток от деления, а это 0 или 1, запоминаем; продолжаем процесс деления до тех пор, пока в результате не будет получен 0. Пример: 3377:2=1688 (1) 4 1688:2=844 (0) 844:2=422 (0) 422:2=211 (0) 211:2=105 (1) 105:2=52 (1) 52:2=26 (0) 26:2=13(0) 13:2=6 (1) 6:2=3 (0) 3:2=1 (1) 1:2=0 (1) В скобках указано значение остатка от деления. Итак, 33771о=11О1ОО11ООО12. Методические рекомендации Ученикам следует предложить выполнить порядка 15-20 упражне- ний по переводу целых чисел из десятичной системы счисления в двоичную и обратно.
Часть первая 3. Представление целых чисел в памяти компьютера. Компь- ютер работает только с данными, представленными в двоичной системе счисления. Не имеет значения, какие это данные: текст, звук, рисунок, целые числа, они должны быть переведены в двоичное представление (закодированы). Для хранения одной двоичной цифры, а это 0 или 1, используется один бит (разряд) памяти компьютера. Запомните, что 8 битов называют 1 бай- том, 1024 байт — 1 килобайтом (1Кбайт), 1024Кбайт — 1 мега- байтом (1Мбайт), 1024Мбайт — 1 гигабайтом (1Гбайт). Б 2 би- тах можно хранить 4 различные последовательности из 0 и 1: 00, 01, 10, 11 (22). (Б 3 битах — 8: 000, 001, 010, 011, 100, 101, 110, 111 (23). А в 8 битах или 1 байте?) Если перевести эти 8 последовательностей из 0 и 1 из двоичной системы счисления в десятичную, то получим десятичные цифры от 0 до 7. Пусть у нас есть 4 бита, а это 16 различных двоичных последовательно- стей, для хранения целых неотрицательных чисел. Итак, мы можем хранить в этом случае числа из интервала от 0 до 15. Проверьте. А что делать, если необходимо хранить и отрицате- льные числа? У нас по-прежнему 4 бита памяти компьютера. Естественным шагом является выделение одного бита для хра- нения знака числа. Получаем, что, например, -5 хранится как 1101, а 5 — 0101. И диапазон представления чисел в этом слу- чае от -7 до 7, т. е. 15 различных значений, а у нас есть воз- можность хранить в 4 битах 16 различных последовательно- стей. Последовательность 1000 не задействована или, что еще более неприятно, она может трактоваться как -0, а это уже из разряда необъяснимых фактов, 0 и -0. Давайте научимся вы- полнять следующие действия: инверсию и прибавление 1 к дво- ичному числу. Рассмотрим на нашем примере. Число Двоичное представление Инверсия Прибавление 1 Отрицательное число 0001 1110 -1 1 2 0010 1101 1110 -2 3 0011 1100 1101 -3 4 0100 1011 1100 -4 5 0101 1010 1011 -5 6 0110 1001 1010 -6 7 0111 1000 1001 -7 8 1000 0111 1000 -8 0 0000 1111 10000 (пятый, старший разряд -отбрасываем»)
Основные управляющие конструкции 27 В предпоследнем столбце мы получаем двоичное представле- ние отрицательных чисел. Все 16 двоичных последовательно- стей задействованы, диапазон представления от -8 до 7 и пред- ставление 0 однозначно. Представление отрицательных чисел в том виде, который приведен в таблице, называется дополните- льным кодом. Естественно, что представление положительных чисел в дополнительном коде совпадает с их обычным пред- ставлением. Сравните полученный результат с диапазоном зна- чений величин целого типа из таблицы, приведенной в начале занятия. Методические рекомендации Вручную выполнить аналогичные действия при 5 и 6 разрядах, от- водимых для представления целых чисел.
28 Часть первая Занятие № 3. Команды редактора для работы с блоками, работа с окнами Плак занятия □ знакомство с командами редактора для работы с блоками; □ знакомство с режимом Window системы программирова- ния; □ программа вычисления степеней двойки; □ программа перевода чисел из десятичной системы счисле- ния в двоичную систему счисления; □ выполнение самостоятельной работы. Знакомство с командами редактора для работы с блока- ми. Блок — любой фрагмент текста программы. Одновременно в тексте может быть только один блок. Отмеченный блок выде- ляется на экране яркостью. Для того, чтобы что-то сделать с блоком, его необходимо выделить, т. е. отметить начало и ко- нец блока. После этого выполняются действия с блоком Приведем основные команды редактора (режим Edit главного меню) для работы с блоками; Ctrl + к + в Отметить начало блока Ctrl + к к Отметить конец блока Ctrl + к + Т Отметить одно слово как блок Ctrl + к + с Скопировать блок Ctrl + к + Y Удалить блок Ctrl + к + Н Убрать выделение блока Ctrl + к + V Переместить блок Ctrl + к + R Ввести с диска ранее записанный блок, начиная с позиции курсора Ctrl + к + W Записать отмеченный блок на диск Примечание 1. Последовательность выполнения команд: □ устанавливается курсор в требуемое место текста; □ нажимается клавиша Ctrl; □ не отпуская клавишу Ctrl, нажать клавишу с буквой К; □ отпустив клавишу с буквой К, не отпуская клавишу Ctrl, нажать клавишу со второй буквой. 2. Знание команд работы с блоками значительно ускоряет набор Ваших программ. Используйте их при работе.
Основные управляющие конструкции 29 Структура задания 1. Набор любого содержательного текста. Например. «На сче- тах десятичное число кодируется положением костяшек на спицах, а в арифмометре (механическом вычислителе) чис- ло кодируется положением диска с 10 зубцами. С помощью таких связанных между собой дисков можно построить сум- мирующее устройство. Идея такого устройства принадлежит Леонардо да Винчи (1452-1519). А впервые такое устройство сделал в 1642 году французский философ и математик Блез Паскаль (1623-1662). Выдающийся американский математик Джон фон Нейман (1905-1957) при работе в составе группы исследователей над машиной «ЭНИАК» сформулировал принципы, лежащие в основе функционирования современных вычислительных ма- шин. Суть этих принципов; в памяти ЭВМ хранятся не толь- ко данные, но и обрабатывающая их программа; представле- ние в памяти данных и программы совпадает; программа должна выполняться последовательно команда за командой». 2. Выделение его фрагментов как блоков. 3. Копирование фрагментов текста (выделяется блок, курсор устанавливается в требуемое место, выполняется команда копирования). 4. Перемещение фрагментов текста (выделяется блок, курсор устанавливается в требуемое место, выполняется команда пе- ремещения). 5. Вставка фрагментов текста через диск (выделяется блок, за- дается команда записи на диск, при этом необходимо опре- делить имя блока, курсор устанавливается в требуемое место, выполняется команда чтения блока с диска). Работа с окнами. В Турбо Паскале есть возможность рабо- ты с несколькими окнами (режим Window главного меню). Окно — ограниченная область экрана. Его размеры и положе- ние на экране можно изменять. Программа, а она хранится на диске как файл с определенным именем, размещается в опреде- ленном окне. Активным в текущий момент является только одно окно, в нем находится курсор.
30 Часть первая Команда Функциональная клавиша Назначение j Tile Последовательное размещение окон ц Cascade Каскадное размещение окон Close All Закрытие всех окон Size/Move Ctrl + F5 Изменение размера, перемещение окна. При выполнении команды изменяется цвет рамки окна Для изменения размера окна используется комбинация клавиш 1 Shift+fl, V, <=, =>. Для перемещения — ft, j !),<=,=> Для завершения работы с окном • следует нажать клавишу Enter Цвет • рамки окна изменится на первоначальный |1 J Zoom F5 Размеры активного окна устанавливаются | равными полному экрану Next F6 Переход к следующему окну | Previos Shift + F6 Переход к предыдущему окну । Close Alt + F3 Закрытие активного окна ( bst Просмотр списка открытых окон ц Структура задания 1. Открыть все программы предыдущих занятий. 2. Выполнить перечисленные выше команды работы с окнами. Экспериментальный раздел работы 1. Выполните обычные действия с программой МуЗ_1 (набор, компиляцию, запись на диск, запуск). Program МуЗ_1; Uses Crt; Var r0,rl,r2,гЗ,r4,г5,гб:Integer; Begin ClrScr; WriteLn('Вычисляем степени числа 2') ; r0: =1; г 1: =2 ; r2: =rl *rl ; r3:=r2*rl; г4:=г2*г2; r5 : =rl *rl *rl*rl*rl ; гб:=гЗ*гЗ; WriteLn ('Нулевая степень, ' ,г0); WriteLn(’Первая степень, ’,rl); WriteLn ('Вторая степень, ’,г2); WriteLn('Третья степень, ’,гЗ); WriteLn('Четвертая степень, ',г4); WriteLn('Пятая степень, ’,г5); WriteLn('Шестая степень, ' ,гб) ; ReadLn; End.
Основные управляющие конструкции 31 Примечание При наборе программы рекомендуется использовать изученные команды работы с блоками. Набирается л2;=г1*г1, а затем этот фрагмент выделяется как блок, копируется 4 раза и модифициру- ется. Аналогичные действия выполняются с операторами WriteLn. Давайте подсчитаем, за какое количество операций выпол- няется, например, вычисление 5-й степени двойки в нашей программе — 4 операции. А можно ли вычислить за меньшее количество операций? Оказывается, «да», за 3 операции — г5:=г2*г2*г1. Вычисление г2 требует одну операцию умноже- ния и две операции для вычисления результата. Модифицируйте программу так, чтобы она вычисляла степе- ни двойки до 10-й включительно (за минимальное количество операций). 2. Выполните обычные действия с программой МуЗ_2 (набор, компиляцию, запись на диск, запуск). Примечание При наборе программы рекомендуется использовать изученные команды работы с блоками и окнами. Скопируйте через диск текст программы МуЗ_1, запишите его как текст программы МуЗ_2, а затем измените его, максимально используя команды работы с блоками. Например, набирается sO~b Mod 2; b.=b Div 2: и копируется столько раз, сколько необходимо. Program МуЗ_2; Uses Crt; Var r0, rl,r2,r3,r4,r5,гб:Integer; s0,sl,s2,s3,s4,s5,s6:Integer; a,b,rez:Integer; one,dec,hun:Integer; Begin ClrScr;{*0чистка экрана *} {^Вычисление степеней двойки*} r0:=l;rl:=2;r2:=rl*rl;r3:=r2*rl; r4:=r2*r2; r5:=r3*r2; гб:=гЗ*гЗ; WriteLn ('Введите число меньше 128 и больше 64'); ReadLn (а);Ь:=а; I*Перевод числа в двоичную систему счисления*} s0:=b Mod 2; b:=b Div 2; sl:=b Mod 2; b:=b Div 2; s2:=b Mod 2; b:=b Div 2; s3:=b Mod 2; b:=b Div 2; Mod 2; b:=b Div 2; s5:=b Mod 2; b:=b Div 2; s6:=b Mod 2; b:=b Div 2; b:=a;
Часть первая 32 (^Выделение десятичных цифр в записи числа*) one:=b Mod 10; b:=b Div 10; dec:=b Mod 10; b:=b Div 10; hun:=b Mod 10; WriteLn ('Мы правильно выделили десятичные цифры’) ; WriteLn (’Вывод числа в десятичной системе счисления’, hun* 100+dec* 10+опе) ; WriteLn (hun ,dec,опе) ; WriteLn('Это же число в двоичной системе счисления’); WriteLn (s6,s5,sl,s3,s2,sl,s0); WriteLn (' Переводим число из двоичной системы счисления в десятичную’); Rez:=s6*r6+s5*r5+sl*rl+s3*r3is2*r2+sl*rl+sO*rO; (★Перевод*) WriteLn(’Все сделано правильно, числа совпадают’); Writein (а, ’ ’,rez); Readin; End. Введите числа больше, чем 128. Оцените результат работы программы. Модифицируйте программу так, чтобы и в этом случае она была работоспособна. Задания для самостоятельной работы 1. Вычислять не очень большие степени двойки А мы научились. Предположим, что у Вас есть некая последовательная (элементы следуют ----------- один за другим, и они одинаковые по своим 2 свойствам) структура для работы с данными. Она имеет имя, например А, и элементы из ----------- этой структуры выбираются по номеру (она последовательная). Так, запись А[10], говорит о том, что мы работаем с 10-м элементом иа- шей структуры. А задача заключается в том, --------- чтобы, используя эту структуру данных, пред- дожить идею (нет, нет, не программу!) вычис- ления больших степеней двойки за минималь- ное количество операций. При наборе программ в Турбо Паскале (здесь и далее по тексту) перено- сить строковую константу па следующую строку нельзя.
Основные управляющие конструкции 33 Структура — взаиморасположение и связь составных частей чего-либо (из словаря иностранных слов). Итак, когда мы гово- рим о структуре, то обязаны сказать о том, из каких элементов она состоит и как они (элементы) связаны между собой. Следу- ет также понимать, что структура обладает новыми свойства- ми, качествами по отношению к свойствам элементов, ее состав- ляющих. Если структуре дать какое-то имя, то мы получаем нечто новое. В информатике структура из однородных элемен- тов, расположенных последовательно, называется массивом. 2. Модифицируйте программу МуЗ_1 так, чтобы она вычисля- ла степени тройки. 3. Модифицируйте программу МуЗ_2 так, чтобы она осущест- вляла перевод чисел из определенного интервала в троичную систему счисления и обратно. 4. На предыдущем занятии в материале для чтения мы рассмот- рели представление отрицательных чисел в дополнительном коде. Пусть по-прежнему у нас только 4 разряда для представ- ления чисел. Необходимо найти 7-3 или 7+(-3). Складыва- ем в двоичной системе счисления 0111+1101=10100. Первое двоичное число это 7, второе -3 в дополнительном коде. Ре- зультат пятиразрядный, у нас 4 разряда, отбрасываем стар- ший разряд, получаем 0100, а это двоичная запись числа 4. Еще пример. Требуется найти 2-5, или 2+(-5). В двоичной системе счисления 0010+1011=1101, а это запись -3 в допол- нительном коде. Пусть Вам дано 6 разрядов для представле- ния целых чисел. Измените предыдущие операции так, чтобы они выполнялись и для этого случая. Разберите еще несколько аналогичных примеров. В компьютере нет необходимости реализовать операцию вычи- тания целых чисел! Достаточно уметь складывать числа, инвер- тировать двоичное представление числа и прибавлять единицу к младшему разряду. А сейчас представим себе, что любое сло- жение можно реализовать путем последовательного прибавле- ния единицы к младшему разряду. Что остается? Инверсия и прибавление единицы к младшему разряду! 5. Подсчитайте сумму пятеричных чисел в интервале от 205 до 405, включая границы интервала.
34 Часть первая 6. Восстановите цифры двоичного числа, на месте которых за- писан символ «*». 1**1+0011=1100. Придумайте и выполни- те еще несколько примеров такого типа. 7. Определите, является ли число 43015 четным? Найдите все четные числа в интервале от 43005 до 43405. 8. В четверичной и восьмеричной системах счисления составить таблицы сложения и умножения. 9. Сравнить числа в различных системах счисления 3124 и 72g. Изменится ли результат, если вычесть из чисел соответствен- но 124 и 128? 10. Существует ли система счисления с основанием х, в которой выполняются следующие равенства: Зх+4х=7х, 3х*4х=13х и 39х+29х=70х? Ответ обосновать. 11. Возможно ли в какой-нибудь системе счисления с основанием х выполнение следующего равенства 600х=211х+252х-<-53х? Ответ обосновать. Материал для чтения Попытаемся ответить на вопрос, что такое программа? Суще- ствуют различные, очень умные, научные трактовки этого тер- мина. Определим очень просто. Программа — это «откуда взять, что сделать, куда положить, а если это не так?» (А. Н. Венц «Профессия программист», 1999.) Правда, в книге таким обра- зом определено понятие алгоритма, но, на наш взгляд, оно бо- лее соответствует тому, что понимается под словом программа. Оставим пока в стороне нюансы терминологии. Пусть оно будет нашим рабочим определением. Вспомним разобранные програм- мы и ответим на вопросы. «Откуда взять» — мы пока берем только из файла, в который вводятся данные с клавиатуры компьютера, но есть и другие места, откуда можно взять исход- ные данные. «Что сделать» — например, в нашей первой про- грамме один оператор присваивания. «Куда положить» — от- вет на вопрос очевиден, пока этим местом является то, что связано с монитором нашего компьютера. И наконец, четвер- тый и, наверное, самый главный вопрос — «а если это не так». Мы видим, что даже в первой программе ответ не очевиден. Она работает не при всех исходных данных. Если Вы откомпи- лировали и один или два раза запустили Вашу программу и по- лучили правильные результаты, то это еще не значит, что у Вас есть работающая программа. Она пока лишь первое приближе- ние к программе. Итак, сформулируем один из основных прин- ципов нашей с Вами работы: «Все подвергай сомнению, все
Основные управляющие конструкции 35 проверяй сам, ни одного факта на веру». Это относится не толь- ко к программам, но и к тому, о чем говорит учитель, к тому, что пишут в книгах по информатике! Сформулируем еще один из принципов. Работа по схеме вос- произведения, пусть даже творческого, того, что написано в учебнике, или того, что говорит учитель, не приводит к успеху в информатике. Львиная доля успешности освоения предмета при- ходится на самостоятельную, кропотливую работу. А. Н. Венц в своей книге приводит формулу великого программиста (ВП), выведенную экспериментальным путем: ВП=50%К+30%Т+10%0+5%3+5%ТЛ, где К — знать, как это делать, Т - трудолюбие, О — опыт, 3 — знание, ТЛ — та- лант. На то, что относится к вундеркиндам, только 5%, осталь- ное — труд, ежедневный труд. А сейчас подвергните сомнению эту формулу. Автор, например, не очень понимает разницу между терминами: «знать, как ...» и просто «знание». Продолжим уточнение термина программа. Языков програм- мирования существует великое множество, но, на каком бы из них Вы ни работали, при создании программы необходимо вы- полнить следующее: □ ввести данные в программу (ввод); □ определить представление этих данных в памяти компью- тера (данные, точнее, структуры данных); □ определить операции по обработке данных (операторы)-, □ выполнить вывод результатов работы программы (вывод); Организация операций в программе может быть различна: □ некоторые из них выполняются только при определенных условиях (условия); □ часть из них выполняется несколько раз (циклы); □ часть из них допускает разбивку на блоки и выполняется в различных частях программы (подпрограммы). Итак, эти семь элементов ввод, данные, операторы, вывод, условия, циклы, подпрограммы являются основными при созда- нии небольших программ на любом языке программирования. Однако следует заметить, что рассмотренное уточнение пони- мания того, что есть программа, относится примерно к 1975 году, да и то с некоторыми оговорками. Нет, например, того, что понимается под термином технологии программирования. Мы не говорим (пока) о том, как из этих элементов «склеивает- ся», собирается программа (об этом позднее), а реальные про- граммы, по оценкам экспертов, намного сложнее автомобилей. И не только об этом. Абсолютно не затронут вопрос о том, как
36 Часть первая программа должна реагировать на события во внешней среде и многое другое. Таким образом, наша трактовка работы про- граммы сводится к некоему последовательному процессу, а это не всегда соответствует действительности. Однако «нельзя объ- ять необъятное», программирование — сложнейший раздел ин- форматики, сложнейшая отрасль производства, а учебник по- священ «основам программирования». В материалах для чтения мы сделаем обзор технологий программирования для того, что- бы составить общее представление о том, какой «виток» спира- ли развития технологий программирования изучается.
Основные управляющие конструкции 37 Занятие № 4. Логический тип данных, операции сдвига План занятия □ обсуждение логического типа данных; □ обсуждение операций сдвига; □ эксперименты с программами вывода таблиц истинности, выполнения операций сдвига и логических операций; □ выполнение самостоятельной работы. Логический тип данных. Переменные логического типа опи- сываются с помощью идентификатора Boolean. Диапазон значе- ний — два: False (ложь) или True (истина), размер выделяемой памяти — 1 байт (False и True — стандартные константы). Тип является перечислимым, поэтому: False<True, Ord(False)=0, Ord(True)=l, Succ(False)=True, Pred(True)=False. Перечислим четыре логические операции, реализованные н Турбо Паскале: логическое сложение, или дизъюнкция, — Or; логическое умножение, или конъюнкция, —And; отрицание — Not, исключающее «Или» (сложение по модулю два) — Хог. Результаты выполнения операций над переменными логиче- ского типа х и у приведены в таблице. Значение операнда Значение операции 1 х У Not к. к And у *Ory x Хог у || False False True False False False J False True True False True True || II True False False False True True I I True True False True True False Выше приведены четыре таблицы истинности (сведены в одну таблицу), с помощью которых в математической логике обычно описываются значения логических функций. Таблица истинности представляет собой таблицу, устанавлива- ющую соответствие между возможными значениями наборов переменных и значениями операции. Следует четко понимать, что результатом выполнения опе- раций сравнения (отношения): «<» (меньше), «>» (больше), «<=» (меньше или равно), «>=» (больше или равно), «<>» (не равно), « = » (равно) является величина логического типа. Ее
38 Часть первая значение равно True, если отношение выполняется для значе- ний входящих в него операндов, и False — в противном случае. В языке Турбо Паскаль нет возможности ввода логических данных с помощью оператора Read. Однако предусмотрен вы- вод значений переменных логического типа с помощью опера- тора Write. Операции сдвига. Речь идет о двух операциях: Shi — сдвиг влево и Shr — сдвиг вправо. Тип операндов и результата в опе- рациях сдвига Integer. Итак, т Shi п — значение m сдвигается влево на п разрядов; а при т Shr п — значение m сдвигается вправо на п разрядов. При выполнении операций разряды, вы- шедшие за пределы области памяти, выделяемой для типа дан- ных, теряются, а с другой стороны добавляются нули. Напри- мер, если m равно 32, то сдвиг влево на один разряд дает 64, а сдвиг вправо — 16. Операции равносильны умножению и деле- нию на два. Экспериментальный раздел работы 1. Выполните обычные действия с программой Му4_1 (набор, компиляцию, запись на диск, запуск). Program Му4_1; Uses Crt; Var a ,b:Boolean; Begin ClrScr; a:=True;b:=True;WriteLn(a:6,b:6,a And b: 6) ; a:=True;b:=False;WriteLn(a:6,b:6,a And b: 6) ; a : =False;b:=True;WriteLn(a:6,b: 6,a And b: 6) ; a:=False;b:=False;WriteLn(a:6,b:6,a And b: 6) ; ReadLn; End. Примечание При наборе программы не забывайте использовать команды рабо- ты с блоками. Набирается a:=True;b:=True;WriteLn(a.6,b:6,a And b:6); а затем этот фрагмент выделяется как блок, копируется 3 раза и модифицируется. Измените программу для проверки остальных рассмотренных выше логических операций. 2. Выполните обычные действия с программой Му4_2 (набор, компиляцию, запись на диск, запуск).
Основные управляющие конструкции 39 Program Му4_2; Uses Crt; Var т,п:Integer; Begin ClrScr; WriteLn('Введите число и количество сдвигов'); ReadLn (m,n); WriteLn(' При сдвиге на ',п,' разрядов влево числа ',т,' получаем число ' ,т Shi п) , WriteLn('Введите число и количество сдвигов'); ReadLn (m,n); WriteLn (' При сдвиге на ' ,п,' разрядов вправо числа ' ,т,' получаем число ' ,т Shr п) ; ReadLn; End. Введите в том и другом случае числа 32 и 1, убедитесь, что получаются числа 64 и 16. Сдвиги вправо отрицательных чисел приводят к интересным результатам. Например, если Вы введе- те -1 и 1 для того и другого сдвигов, то получите -2 и 32767. Если первый результат вполне объясним, то второй требует вспомнить о представлении отрицательных целых чисел в до- полнительном коде. Пусть у нас не шестнадцать разрядов для представления чисел (тип Integer), а 4. Представление -1 в до- полнительном коде есть 11112. Сдвиг вправо на один разряд приводит к числу 01112, а это не что иное, как 710. 3. Выполните набор программы Му4_3, набрав только первые три оператора WnteLn). Откомпилируйте ее, запишите на диск. Program Му4_3; Uses Crt; Begin ClrScr; WriteLn(1365 And 2730) ; WriteLn(1365 Or 2730) ; WriteLn (1365 Xor 2730); WriteLn (1365 And $FF) ; WriteLn(1365 And $FF0) ; WriteLn(1365 And $FF00) ; ReadLn; End.
Основные управляющие конструкции Проверьте, что х|у эквивалентно Not(x) Or Not(y). Соста- вьте программу проверки эквивалентности этих днух логиче- ских функций. 3. В математической логике известна функция Вебба, или стрел- ка Пирса, (xUy) ее таблица истинности имеет вид X У XUy False False True False True False True False False | True True False Проверьте, что xUy эквивалентно Not(х) And Not(у). Соста- вьте программу проверки эквивалентности этих двух логиче- ских функций. 4. Дана логическая функция, например, (x=>y)=>z. Построить таблицу истинности данной функции. Схема построения при- ведена в таблице. В первом столбике приведены возможные значения наборов переменных х, у и z (значение True обозна- чено как единица, значение False — как нуль). xay (X=.y)=>z 000 1 0 00 1 1 1 0 1 0 o 0 1 1 1 1 0 0 0 101 0 1 110 1 0 1 1 Преобразуйте эту формулу в эквивалентную ей. Составьте программу проверки эквивалентности этих двух логических формул. 5. Постройте таблицы истинности для следующих функций: □ (х I у)Iz; □ (xUy)Uz; □ (х=>у) And z; □ Not (x Or Not(y) And z); □ x And Not(y Or Not(z)); □ Not(Not(x) Or у And z).
Часть первая 6. Дана логическая функция (xUy)U(zUv). Построить таблицу ис- тинности. Возможных значений наборов переменных х, у, z и v в данной задаче шестнадцать. Обозначим значение True единицей, а значение False — нулем. ху Z V xUy zUv | (xCy)U(zUv) | 00 0 0 1 ° ООО 1 0 I 0 00 1 0 1 ° [ 0 I 00 1 1 1 ] о L 0 I , 01 0 0 0 1 j 0 f 01 0 1 0 ° 1 I 01 1 0 0 0 1 01 1 1 0 ° I ! 10 0 0 0 1 ! о I | 10 0 1 0 ° \ 1 10 10 0 i 0 I 1 li 1011 0 oil'! 110 0 0 ! I 2 '! 1101 0 0 11 | 1110 0 0 1 II 1111 0 0 1 1 _ I Материал для чтения 1. Слово «логика» употребляется в разных значениях, на- пример логика событий, логика характера и т. д. В этом случае имеется в виду определенная последовательность и взаимозави- симость событий или поступков. Слово «логика» употребляет- ся и в связи с процессами мышления. Когда говорят о логич- ном мышлении, то рассматривают его последовательность, дока- зательность и т. д. Итак, логика — особая наука о мышлении. Основателем ее считается древнегреческий философ Аристо- тель (IV в. до н. э.). Позднее она стала называться формальной логикой, и ее цель на протяжении всей истории развития неиз- менна: исследование того, как из одних утверждений можно выводить другие, при этом считается, что правильность рас- суждения определяется только его логической формой и не за- висит от конкретного содержания входящих в него рассужде- ний. В XIX веке благодаря усилиям английского ученого Джор- джа Буля возникла наука математическая логика. Джордж Буль перенес на логику законы и правила алгебраических дей- ствий, ввел логические операции, предложил способ записи вы-
Основные управляющие конструкции 43 сказываний в символической форме. Алгебра логики — раздел математической логики, изучающей строение (форму, структу- ру) сложных высказываний и способы установления их истин- ности с помощью алгебраических методов. Высказывание — повествовательное предложение, относительно которого можно сказать, истинно оно или ложно. Все высказывания условно разделяются на простые и сложные, или составные. Составные высказывания образуются из простых. Высказывания х и у — простые, высказывание х And у — составное, оно называется конъюнкцией и имеет 4 логические возможности, рассмотрен- ные выше, для определения возможности его истинности. Вы- сказывание х Or у тоже составное и называется дизъюнкцией. 2. Алгебра логики и ее законы. Операции алгебры: конъюнк- ция, дизъюнкция и отрицание. Эти операции позволяют произ- водить тождественные преобразования логических выражений. Законы: □ закон одинаковости: х Or х=х; х And х=х; □ закон коммутативности: х Ory=y Or х, х And у=у And х; □ закон ассоциативности: х Or(y Or z)=(x Or у) Or z, x Anc?(y And z)=(x And y) And z; □ законы дистрибутивности: x And (y Or z)=x And у Or x And z — первый; x Or у And z=(x Or y)And(x Or z) — вто- рой; □ закон двойного отрицания: Not(Not(x))=x; □ законы де Моргана: Not(x) Or Not(y)=Not(x And у); □ законы поглощения: х Or х And у=х; х And (х Or у)=х; □ законы, определяющие действия с логическими констан- тами False и True: х Or False=x; х And False=False; х Or True=True\ x And True=x~, Not(False)=True; Not(True)= =False; Not(x) Or x^True: Not(x) And x=False. Дополнительные законы (они выводятся из основных зако- нов): □ законы склеивания: х And у Or Not(x) And у=у; (х Or у) And (Not(x) Or y)=y; □ закон Блейка-Порецкого: x Or Not(x) And y=x Or y; □ закон свертки логического выражения: х And у Or Not(x) And z Or у And z=x And у Or Not(x) And z. Примечания 1. Приведем пример вывода для закона Блейка-Порецкого: х Or Not(x) And у= х And True Or Not(x) And y= x And (y Or Not(y)) Or Not(x) And y= x And у Or x And Not(y) Or Not(x) And y= x And у Or x And Not(y) Or Not(x) And у Or x And y= x Or y.
44 Часть первая 2. Тип упражнений для закрепления материала может быть следу- ющим. Дается логическое выражение, например, Not(Not(x) Or Not(y))=? И варианты ответов: Not(x) Or у или х Or у и т. д., необ- ходимо выбрать правильный ответ. 3. Логические функции можно преобразовать в две различ- ные формы: □ дизъюнктивную нормальную форму (ДНФ); □ конъюнктивную нормальную форму (КНФ). В первом случае логическая функция записывается в виде дизъюнкции конъюнкций, образованных из переменных и их отрицаний. Во втором случае наоборот. Примеры: Not(x) Or у And z, х And у And z Or Not(y) And Not (z) — ДНФ, a x And (y Or Acrt(z)) -нет, (Aot(x) Or y) And z, x And у And (z Or Not(v)) — КНФ, x And (y And z Or Notty)) — нет. Всякая сложная логическая функция может быть преобра- зована как к ДНФ, так и к КНФ. Алгоритм преобразования: □ записать функцию с использованием только операций Or, And, Not: □ с помощью законов де Моргана операцию отрицания дове- сти до отдельных переменных и убрать выражения типа Not(Not(x)) по закону двойного отрицания; □ с помощью первого закона дистрибутивности убрать все конъюнкции дизъюнкций и провести поглощение. В результате получим ДНФ представления логической фун- кции. Для получения записи в виде КНФ следует изменить тре- тий пункт алгоритма: □ с помощью второго закона дистрибутивности убрать все дизъюнкции конъюнкций и провести поглощение. Если все конъюнкции в ДНФ содержат все логические пере- менные или их отрицания, то ДНФ называется совершенной. Аналогично определяют и совершенную КНФ. х у z (x=>y)=>Not(z) ООО 1 00 1 о 0 10 1 0 11 о 100 1 10 1 1 110 1 111 о
Основные управляющие конструкции 45 Рассмотрим построение совершенных ДНФ и КНФ на незна- чительно измененном 4-м примере из раздела самостоятельной работы занятия. Дана логическая функция (x=>y)=>Not(z). По- строим таблицу истинности данной функции. СДНФ ((х=>у) ^>Not (z))=Not (к) AndNot(y) AndNot(z) Or Not (x) And у And Not (z) Or x And Not (y) And Not (z) Or x And Not (y) And (z) Or x And у And Not (z) . СКНФ ( (x=>y) ^Not (z) ) =Not (Not (x) And Not (y) And z) And Not (Not (x) And у And z) And Not (x And у And z) = (x Or у Or Not (z)) And (x Or Not (y) Or Not (z)) And (Not (x) Or Not (y) Or Not (z)).
46 Часть первая Занятие № 5. Составной оператор и оператор If Плак занятия □ обсуждение операторов; □ эксперименты с программами нахождения наибольшего из двух и трех чисел; □ выполнение самостоятельной работы. Составной оператор. Он состоит из ряда операторов, вы- полняемых в той последовательности, в которой они записаны в программе. Его схема: Begin оператор; оператор; оператор; End; Примечание Разделитель «;» перед End можно не записывать. Оператор If, или условный оператор. Оператор записыва- ется следующим образом: полный условный оператор /f<ycnoBMe> Then <оператор 1> Else <оператор 2>; неполный условный оператор If <условие> Then <оператор>, Выполнение условного оператора начинается с вычисления значения логического выражения, записанного в условии. Про- стые условия записываются в виде равенств или неравенств. Сложные условия составляют из простых с помощью логиче- ских операций. Как известно, значением логического выраже- ния является или True, или False. В первом случае выполняется «оператор 1>, во втором — «оператор 2>. В качестве «опера- тор 1> или < оператор 2> может выступать любой оператор языка программирования Турбо Паскаль, в частности и состав- ной оператор, и условный оператор. В последнем случае полу- чаемая конструкция называется вложенными условными опе- раторами. Допускается запись неполного условного оператора, без ветви Else. В этом случае при значении False никаких дей- ствий не производится. В записи условных операторов возника- ет неоднозначность типа: If «условие 1> Then «оператор 1> If «условие 2> Then «оператор 2> Else «оператор 3>; Неясно, к какому оператору If относится ветвь Else. Она (не- однозначность) разрешается по следующему правилу: «Else от- носится к ближайшему оператору If, у которого еще отсутству- ет данная ветвь».
Основные управляющие конструкции 47 Экспериментальный раздел работы 1. Разбор оператора можно выполнить на следующем простом примере. Вывести на экран наибольшее из двух данных чисел. Program Myb_l; Var х,у: Integer; Begin WriteLn('Введите 2 числа'); ReadLn(и,у); If х>у Then WriteLn('.)Else WriteLn(у); ReadLn; End. Поставьте «;» после оператора WriteLn(x). Убедитесь, что по- явилась ошибка «Error 113: Error in statement». Конструкция (оператор) If — Then — Else неделима, поэтому разделитель «;» недопустим. В случае равенства чисел Ваша программа выводит значение переменной у. Измените программу так, чтобы в этом случае она выводила на экран сообщение «Числа равны». Попробуем найти наибольшее из трех чисел — значения пе- ременных х, у и z. Предположим, что нет равенств, т. е. все числа различны. Возможны шесть различных случаев, они при- ведены на рисунке. Программа определения значения наибольшего из трех чи- сел имеет вид Program Му5_2; Var х,у, z:Integer; Begin WriteLn ('Введите три числа через пробел'); ReadLn (х,у, z) ;
48 Часть первая If (х>у) And (x>z) Then WriteLn (x) Else If (y>x.) And (y>z) Then WriteLn (y) Else WriteLn (z) ; {★ If x>y Then If x>z Then WriteLn (x) Else WriteLn (z) Else If y>z Then WriteLn (y) Else WriteLn(z);*) ReadLn; End. Вторая версия решения заключена в фигурные скобки (ком- ментарии). Уберите их, включите первую реализацию как ком- ментарий, убедитесь в правильности решения. Измените про- грамму так, чтобы анализировался и случай равенства чисел. Обратите внимание на то, что при написании сложных условий простые условия заключаются в скобки. Это связано с тем, что операции сравнения имеют более низкий приоритет, чем логи- ческие операторы. Задания для самостоятельной работы 1. Имеется условный оператор: If DolO Then ivnteln(ypa!') Else Wnteln(nM)xo...'); Можно ли заменить его следующими опе- раторами: If D=10 Then Writein(' ура'') Else Writein ('плохо.. .') ; If Not (0=10) Then Writein ('ура 1 ') Else Write In('плохо. . . ') ; If Not(0=10) Then Writeln ('плохо...') Else Wri teln ('ура ! ') ; IfNot(DOlO) Then Wri teln ('плохо...') Else Wri teln ('ура ! ') . 2. Запишите условный оператор, в котором значение перемен- ной вычисляется по формуле: а+Ъ, если а — нечетное и а*Ь, если а — четное. 3. Вычислить значение функции: (х2 +5, при х > 3 [х - 8, при х < 3 4. Вывести на экран номер четверти, которой принадлежит точ- ка с координатами (х,у), при условии, что х и у отличны от 0. 5. Дано двузначное число. Написать программу определения: □ является ли сумма его цифр двузначным числом;
Основные управляющие конструкции 49 □ больше ли числа X сумма его цифр, число X вводится с клавиатуры; □ кратна ли шести сумма его цифр; □ больше ли цифра десятков цифры единиц; □ оканчивается ли число цифрой 5. Придумать не менее 8 аналогичных задач с трехзначными числами. 6. Написать фрагмент программы, подсчитывающий сумму то- лько положительных из трех данных чисел. 7. Даны три числа. Написать фрагмент программы, подсчиты- вающий количество четных чисел. 8. Дано трехзначное число. Написать программу определения является ли оно палиндромом («перевертышем»), т. е. числом, десятичная запись которого читается одинаково слева напра- во и справа налево. 9. Если целое число М делится нацело на целое число N, то вы- вести на экран частное от деления, в противном случае — со- общение «М на N нацело не делится». 10. Составьте программу, которая уменьшает первое число в пять раз, если оно больше второго по абсолютной величине. 11. Вычислить значение функции: х - 12, при х > О 5, при х = О х2, при х < О при х = О при х < О 12. Даны три целых числа, найти среднее из них. Средним на- зовем число, которое больше наименьшего из данных чисел, но меньше наибольшего. 13. Составьте программу нахождения произведения двух наибо- льших из трех введенных с клавиатуры чисел. 14. Найти количество положительных (отрицательных) чисел среди четырех целых чисел А, В, С и D. 15. Дано двузначное (трехзначное) число. Написать программу определения: □ входит ли в него цифра 5; □ входят ли в него цифры 4 и 7; □ входят ли в него цифры 3, 5, 7. 16. Даны целые числа х и у. Написать программу определения знака разности х-у. Разность не вычислять. Разрешается сравнивать числа х и у с нулем; а между собой можно срав- нивать только модули чисел х и у.
50 Часть первая 17. Известна текущая дата. Пользователь вводит день, месяц и год своего рождения. Написать программу, определения, ис- полнилось или нет пользователю полных 16 лет. 18. Написать программу определения факта попадания точки М(х,у) в заштрихованную область, изображенную на рисун- ке. 19. Написать программу определения принадлежности точки М(х,у) заштрихованной области, изображенной на рисунке (уравнение окружности х2 +у2 =г2). 20. Составьте программу вычисления выражения □ max(x+y+z, xyz)+3; □ min(x2+y2,y2+z2) - 4, если x,y,z вводятся с клавиатуры. 21. Составьте программу, которая из трех введенных с клавиа- туры чисел возводит в квадрат положительные, а отрицате- льные оставляет без изменения.
Основные управляющие конструкции 51 22. Даны два конверта прямоугольной формы с длинами сторон (а,Ь) и (c,d). Определить, можно ли один из конвертов вложить в другой? 23. Составьте программу, которая определяла бы вид треуголь- ника по длинам его сторон а, b и с (если данные отрезки по- зволяют его построить). Напомним, что условием того, что треугольник может быть составлен, является одновременное выполнение следующих условий: а+Ь>с, Ь+с>а, а+с>Ь, а так- же то, что треугольники могут быть разными: равносторон- ними, равнобедренными, прямоугольными, равнобедренны- ми прямоугольными и т.д. Кроме того, следует учесть, что в качестве длин сторон могут быть случайно введены как ну- левые, так и отрицательные значения. Примечание Эту, хорошо известную задачу, иногда называют тестом Г. Майер- са на профессиональную пригодность (из книги «Искусство тести- рования программ»). Если Вы сможете реализовать в своей про- грамме порядка 10 различных ситуаций, то Вам следует выбрать программирование своей специальностью Материал для чтения 1. Давайте обсудим то, чем мы пытаемся заниматься, т. е. программирование. Определение: «Программирование — тео- ретическая и практическая деятельность по обеспечению про- граммного управления обработкой данных, включающая созда- ние программ, а также выбор структуры и кодирования данных». Попробуем определить несколько иначе, более конструктивно, рассматривая то, чем занимается программист. Есть задача или проблема. В первую очередь программист должен определить возможность ее решения, выбирая соответствующий метод. За- тем разработать проект программы, состоящий из алгоритма на каком-либо из языков программирования, доказать правильность работы программы и предусмотреть возможность ее изменения, внесения изменений на этапе сопровождения. Таким образом, в укрупненном виде мы видим три этапа: до программирования, программирование и после программирования. Только часть работы связана с выбором структур данных и кодированием — использованием языков программирования. Программирование есть, если так можно выразиться, инженерная работа по конст- руированию некой целостной системы обработки данных. От- личие программы, например, от некоторой механической сис- темы в том, что число взаимодействующих частей в программе настолько велико, что не поддается никакому разумному объ-
52 Часть первая яснению, и проверить работу этой программной системы, пере- бирать все возможные способы взаимодействия ее частей не- мыслимо даже на сверхбыстродействующих компьютерах в ра- зумные сроки. Где же выход? Видимо, только в технологиях, обеспечивающих на выходе качественный и надежный про- дукт. «Технология — совокупность методов обработки, изготовле- ния, изменения состояния, свойств, форм сырья, материала или полуфабриката в процессе производства или наука о спосо- бах воздействия на сырье, материалы или полуфабрикаты соот- ветствующими орудиями производства» (словарь иностранных слов). Приведем более подходящее определение. «Технология — это способ реализации людьми конкретного сложного процесса путем разделения его на систему последовательных взаимосвя- занных процедур и операций, которые выполняются более или менее однозначно и имеют целью достижение высокой эффек- тивности. Под процедурой понимается набор действий (опера- ций), посредством которых осуществляется тот или иной главный процесс (или его отдельный этап), выражающий суть конкретной технологии, а операция — это непосредственное практическое решение задачи в рамках данной процедуры, т. е. однородная логически неделимая часть конкретного процесса». В нашем случае технология должна поддерживать все три этапа работы программиста, о которых речь шла выше, ибо технология в данном случае есть искусство, мастерство изготовления, конст- руирования систем обработки данных. Обычно, когда говорят о технологиях программирования, то имеют в виду этап разра- ботки программ, первый и третий этапы не рассматриваются. Действительно, можно считать этот этап решающим, качест- венный выход на этом этапе обеспечивает достаточную просто- ту третьего этапа. Его часто трактуют как этап сопровождения, но мы вкладываем более широкий смысл — эволюционное из- менение системы обработки данных. 2. Поговорим о простых и сложных программах. Первые имеют ограниченную область применения, разработаны и со- провождаются одним человеком. Они могут быть профессиона- льно изготовлены, но так или иначе они обозримы, и их слож- ность не превышает интеллектуальные возможности человека. Но не все программы простые. Программы управления желез- нодорожными или воздушными сообщениями, системы управ- ления базами данных, обеспечивающих параллельный доступ многих пользователей и т. д. — это другой класс программ по уровню их сложности. Считается, что сложность программ от-
Основные управляющие конструкции 53 нюдь не случайное свойство, скорее необходимое. Где пролегает граница между простым и сложным? Обычно сложность про- грамм определяется количеством операторов исходного текста программы. Это условная градация, простая количественная мера, которую можно принять в качестве первого приближе- ния. Известны разработки программ с менее 10000 оператора- ми исходного текста, относящиеся по выполняемым функциям к сложным и даже сверхсложным. Программа Количество операторов исходного текста (до) ' Простая 1000 Средней сложности 10 000 1 Сложная 100 000 Сверхсложная 1 000 000 I Гиперсложная 10 000 000 и более Ограничимся интуитивным пониманием простого и сложно- го. Философский анализ этих понятий применительно к про- граммам — предмет отдельного разговора. Отметим еще одну особенность больших программ. Они имеют тенденцию к эво- люции в процессе их использования. Это часто называют сопро- вождением, но сопровождение есть устранение ошибок, а эволю- ция подразумевает внесение в программу изменений, обуслов- ленных изменяющимися требованиями к ней. Как «бороться» со сложностью? Принцип известен со времен древних греков — «разделяй и властвуй». Сложную задачу сле- дует разделить на взаимосвязанные подзадачи. Последние в свою очередь опять разделяются на свои подзадачи и т. д., вплоть до самых низших уровней нашего понимания задачи. Что мы имеем? Во-первых, сложная задача (проблема, система) описывается некой иерархической структурой, во-вторых, опре- деляются принципы ее декомпозиции и, в-третьих, так как каж- дый уровень — это определенный уровень абстрагирования, со- здается инструментарий для описания абстракций. Иерархия — это ранжированная, или упорядоченная, система абстракций, расположение частей или элементов целого в порядке от вы- сшего к низшему. В результате деятельности программиста со- здается иерархическая структура из абстракций, а «абстрак- ция — это такие существенные характеристики некоторого объекта, которые отличают его от всех других видов объектов и, таким образом, четко определяют особенности данного объ- екта с точки зрения дальнейшего рассмотрения и анализа». Г. Бруч разделяет абстракцию сущности объекта — объект
54 Часть первая представляет собой модель существенных сторон предметной области и абстракцию поведения — объект состоит из обобщен- ного множества операций, каждая из которых выполняет опре- деленную функцию. Другими словами, сущность объекта мы описываем на уровне данных в программе, а поведение — дей- ствиями над этими данными. Выскажем еще один тезис. На наш взгляд, программа — это не что иное, как модель реальной действительности, реальной задачи, причем динамическая мо- дель. 3. В этом и последующих материалах для чтения мы коснем- ся истории развития технологий программирования. Операцио- нальное программирование. Этот этап развития технологий про- граммирования характерен для ЭВМ первого поколения (с 1945 до 1959 года). Быстродействие ЭВМ этого поколения до 50 ты- сяч арифметических операций, объем оперативной памяти в лучшем случае несколько килобайт ячеек. Ресурсы минималь- ны. Если сравнивать эти характеристики ЭВМ с современными компьютерами: быстродействие — миллиарды операций в се- кунду, объемы памяти — мегабайты, то различие поразитель- но. ЭВМ того времени понимала только цифровые команды, и программа состояла из множества строк, состоящих из цифр, интерпретируемых центральным процессором. Напри- мер, 05 825 631 трактовалось как команда сложения двух чи- сел (код 05), записанных в ячейки с номерами 825 и 631. Ми- нимальные ресурсы ЭВМ требовали строжайшей экономии оперативной памяти и эффективных алгоритмов обработки. Программа по взаимосвязи составных частей напоминала «спа- гетти», примерно то, что изображено на рисунке. Представим программу, состоящую из тысячи таких строк, и отдадим должное программистам того времени. Производите- льность программистов очень низкая, так как ему вручную не- обходимо было распределить все переменные своей программы в оперативной памяти (!). Следующий этап развития техноло- гий программирования мало отличается от первого. Он связан с
Основные управляющие конструкции 55 ЭВМ второго поколения. Появились языки программирования типа ассемблера и автокода. Различие, на примере нашей коман- ды, заключается в том, что команда сложения записывалась с ис- пользованием мнемоники — ADD (английское сложить) PR1, ZET, где ADD — код команды, PR1, ZET — имена ячеек. Перевод программы (трансляция), записанной таким образом, в цифровое представление, а только такое понимает ЭВМ, осуществлялся с помощью специальных программ, называемых ассемблерами. Чем характеризуется этот этап развития технологий? Его мож- но назвать операциональным программированием. Первый и третий этапы работы программиста не обсуждают- ся. Программа «собирается» из мелких деталей, отдельных операций и имеет достаточно простую структуру, если исклю- чить принцип «спагетти» из управления вычислительным про- цессом. Уровень абстрагирования — отдельное действие, прин- ципы декомпозиции задачи отсутствуют, во всяком случае, о них не говорят. Существует разрыв между требованиями прак- тики и возможностями программирования. Круг решаемых с помощью ЭВМ задач достаточно ограничен — в основном рас- четные задачи.
56 Часть первая Занятие № 6. Оператор For План занятия □ обсуждение оператора; □ разбор программ (с использованием ручной трассировки логики) определения того, что число является палиндро- мом, в записи четырехзначного числа ровно три одинако- вые цифры; □ выполнение самостоятельной работы. Оператор For. Оператор задает многократное выполнение некоторого оператора (может быть и составным) с одновремен- ным изменением значения управляющей переменной. Вид опе- ратора For: For управляющая переменная>.= А То В Do <оператор>, For управляющая переменная> =А DownTo В Do <оператор>, где А — начальное значение управляющей переменной, В — конечное значение управляющей переменной. Начальное (А) и конечное (В) значения управляющей пере- менной могут быть представлены константами, переменными или арифметическими выражениями. Они определяются один раз в начале выполнения оператора For и не изменяются во время выполнения этого оператора. Если окажется, что А>В при использовании слова То, то оператор после слова Do («тело» цикла) не будет выполнен ни разу и выполнение цикла с пара- метром сразу же закончится (соответственно при DownTo, если А<В). Управляющая переменная, а также А и В должны быть одного типа, обязательно порядкового. Оператор после слова Do выполняется один раз для каждого значения управляющей пе- ременной из диапазона, определяемого значениями А и В. Если в операторе For используется слово То, то значение управляю- щей переменной увеличивается на единицу при каждом повто- рении А, А+1,..., В-1, В, при DownTo — уменьшается на еди- ницу. Рекомендации по использованию. Оператор For применяют тогда, когда известно число повторений одного и того же дейст- вия (оператора). Изменение значения управляющей перемен- ной в теле цикла может привести к ошибкам, считается «дур- ным тоном» в программировании, поэтому договоримся о том, что это действие запрещено законом, т. е. учителем. Управляю- щая переменная после выполнения оператора For имеет неоп- ределенное значение. Запретим использование ее значения для анализа чего-либо после выполнения оператора For, а также
Основные управляющие конструкции 57 «искусственные» выходы из For с помощью операторов GoTo, Exit и т. д. Оператор For должен иметь одну точку входа и одну точку выхода, он оператор! Экспериментальный раздел работы 1- Дано натуральное число п (п<9999). Определить, является ли оно палиндромом («перевертышем»), с учетом четырех цифр. Например, палиндромами являются числа: 2222, 6116, 0440. Начнем не с программы, а с ручной трассировки логики ре- шения. Это важно, очень важно. Мы с Вами с помощью этого приема должны достичь того, чтобы при написании программы у Вас одновременно складывался «зрительный образ» ее рабо- ты, Вы видели ее работу, причем это должна быть не статиче- ская «картинка», а динамическая. Трассировка обычно выпол- няется для конкретных значений входных параметров задачи. Итак, у нас четырехзначное число, поэтому переменная опе- ратора For изменяется от 1 до 4. В переменной с именем m хра- нится «остаток» числа, в первоначальный момент времени он равен введенному числу. В переменной с именем г формируем значение числа — «перевертыша». Основными операциями яв- ляются: r~10*r + т Mod 10 (добавление очередной цифры к числу «перевертышу») и т:=т Div 10 (изменение проверяемого числа). Процедура трассировки приведена в таблице. После ее выполнения написание программы — «техническая работа». i m - 3994 ° 1 399 0*10+3994 mod 10=0+4=4 2 39 4*10+399 mod 10=40+9=49 3 3 49* 10+39 mod 10=490+9=499 0 499* 10+3 mod 10=4990+3=4993 Program Му6_1; Var п,т,г,i:Integer; Begin WriteLn('Введите целое число, не большее 9999'); ReadLn(п); т:=п;г:=0; For i:=l То 4 Do Begin {так как число четырехэначное) г: =r* 10+m Mod 10; т:=т Div 10; End;
58 Часть первая If г=л Then WriteLn('ДА') Else WriteLn('НЕТ'); ReadLn; End. Измените программу так, чтобы была возможность обраба- тывать целые числа из диапазона Longlnt. 2. Даны натуральные числа n, к (п, к<9999). Из чисел от п до к выбрать те, запись которых содержит ровно три одинаковых цифры. Например, числа 6766, 5444, 0006, 0060 содержат ровно три одинаковых цифры. 1 2 3 4 1 X X X — первое условие | 2 х X X — второе условие | 3 X X X — третье условие | 4 X X X — четвертое условие | Если данное число содержит ровно три одинаковых цифры, то только одна из цифр отличается от остальных, то есть воз- можны четыре случая, приведенных в таблице. Пусть в качест- ве п и к введены числа 3732 и 3740. В переменных al, а2, аЗ, а4 храним значения цифр текущего числа 1. i а1 а2 аЗ а4 Результат сравнения 3732 3 7 3 2 ЛОЖЬ 3733 3 3 3 истина 3734 3 7 3 4 ложь 3735 3 3 5 ложь 3736 3 7 3 6 ложь 3737 3 7 3 7 ложь 3738 3 7 3 8 ложь 3739 3 7 3 9 ложь 3740 3 7 4 0 ложь Примечание Пусть Вас не смущает «элементарность» выполняемых действий. Вспомните одно из качеств «великого программиста». Program Му6_2; War п,к,i,al,а2,аЗ,а4,т:Integer; Begin WriteLn (' Введите два числа, не больших 9999'); ReadLn (п, к); For i:=n То к Do Begin
Основные управляющие конструкции 59 m:=i; {выделение цифр: al — первая, а2 — вторая, аЗ — третья, а4 — четвертаяj а4:=П1 Mod 10; m: Div 10;a3:=m Mod 10; m: =m Div 10; a2:=m Mod 10; al:=m Div 10; If { (al=a2) And (al=a3) And (alOal)) Or {первое условие} ( (al=a2) And (al=a4) And (аКлаЗ)) Or {второе условие} ((al=a3) And (al=al) And (alc>a2)) Or {третье условие) ((a2=a3) And (a2=a4) And (a2Oal)) {четвертое условие j Then Wri teLn (i: 5) ; End; ReadLn; End. Измените программу для обработки 4, 5 или 6-значных чи- сел. Если Ваше решение будет идейно копировать приведенное, то это хорошо, но не очень. Все же оно будет достаточно громоз- дким. Постарайтесь найти другое решение, его не обязательно оформлять в виде программы. Задания для самостоятельной работы Перед написанием программ требуется выполнить ручную трассировку основных фрагментов решения, естественно, цик- лы трассируются не при всех значениях управляющей пере- менной. 1. Найти все двузначные числа, в которых есть цифра N или само число делится на N. 2. Составить программу возведения натурального числа в квад- рат, используя следующую закономерность: I2 =1 22 =1 + 3 З2 =1 + 3 + 5 42 =1+3+5+7 п2 =1+3+5+7+9+...+2п-1 3. Определить количество трехзначных натуральных чисел, сум- ма цифр которых равна заданному числу N.
60 Часть первая 4. Составить программу вычисления суммы кубов чисел от 25 до 55. 5. Среди двузначных чиселнайти те, сумма квадратов цифр ко- торых делится на 13. 6. Написать программу поиска двузначных чисел, таких, что если к сумме цифр этого числа прибавить квадрат этой сум- мы, то получится это число. 7. Квадрат трехзначного числа оканчивается тремя цифрами, которые как раз и составляют это число. Написать програм- му поиска таких чисел. 8. Написать программу поиска четырехзначного числа, которое при делении на 133 дает в остатке 125, а при делении на 134 дает в остатке 111. 9. Найти сумму положительных нечетных чисел, меньших 100. 10. Найти сумму целых положительных чисел из промежутка от А до В, кратных 4 (значения переменных А и В вводятся с клавиатуры). 11. Найти сумму целых положительных чисел, больших 20, ме- ньших 100, кратных 3 и заканчивающихся на 2, 4 или 8. 12. В трехзначном числе зачеркнули первую цифру слева, когда полученное двузначное число умножили на 7, то получили данное число. Найти это число. 13. Сумма цифр трехзначного числа кратна 7, само число также делится на 7. Найти все такие числа. 14. Среди четырехзначных чисел выбрать те, у которых все че- тыре цифры различны. 15. Среди двузначных чисел найти те, сумма цифр которых рав- на п(0<п<18) и число делится без остатка на числа q. 16. Дано четырехзначное число п. Выбросить из записи числа п цифры 0 и 5, оставив прежним порядок остальных цифр. На- пример, из числа 1509 должно получиться 19. 17. Натуральное число из п цифр является числом Армстронга, если сумма его цифр, возведенных в n-ю степень, равна са- мому числу (например, 153=13+53+З3). Получить все числа Армстронга, состоящие из трех и четырех цифр. 18. Дана последовательность из 20 целых чисел. Определить ко- личество чисел в наиболее длинной подпоследовательности из подряд идущих нулей. 19. Дано натуральное число. Найти все его делители и их сум- му.
Основные управляющие конструкции 61 Материал для чтения Из истории программирования. О нисходящем проекти- ровании программ, структурном и модульном программи- ровании. Третье поколение ЭВМ (наиболее известная — IBM/ 360) связано с появлением интегральных схем. Существенной частью ЭВМ становятся операционные системы, на которые возлагаются задачи управления работой компьютера. Операци- онные системы — ядро системного программного обеспечения. Развиваются языки программирования высокого уровня. Если первые версии FORTRAN I, ALGOL-58 обеспечивали запись ма- тематических формул и не более, то в этом поколении языков реализуются новые идеи: подпрограммы и раздельная компи- ляция (FORTRAN II); блочная структура и типы данных (AL- GOL-60); описание данных и работа с файлами (COBOL); обра- ботка списков и указатели (Lisp). В следующих версиях языков продолжается развитие: PL/1 (FORTRAN+ALGOL+COBOL), AL- GOL-68 (преемник ALGOL-60), Pascal (развитие ALGOL-60), Si- mula (классы, абстрактные данные). Следует особо сказать о языке PL/1. В истории программирования была, и, наверное, есть, идея создания универсального языка программирования, позволяющего программировать задачи различных классов, дру- гими словами — всевозможные задачи. Обилие языков програм- мирования, а их зарегистрировано более трех тысяч, говорит о тщетности этих попыток. Возможности языков программирова- ния обеспечивают поддержку (начальные этапы) нисходящей технологии конструирования программ. Суть нисходящего кон- струирования программ в разбивке большой задачи на меньшие подзадачи, которые могут рассматриваться отдельно. Основны- ми правилами для успешного применения данной технологии являются: □ формализованное и строгое описание программистом вхо- дов функций и выходов всех модулей программы и систе- мы; □ согласованная разработка структур данных и алгоритмов; □ ограничение на размер модулей. Нисходящая технология разработки программ не есть свод жестких правил, скорее это основной принцип, допускающий вариации в соответствии с конкретными особенностями решае- мой задачи. В свое время в обширной литературе по этому по- воду говорилось и о восходящей технологии. В этом случае решение (программа) как бы «складывалось из отдельных кирпичиков», из известных решений подзадач. Таким обра- зом, данной технологией оговаривается определенный принцип
62 Часть первая декомпозиции и иерархическая структура программы. Важ- нейшей составляющей этой технологии является структурное программирование (языки программирования Паскаль, Моду- ла-2). Профессор Э. Дейкстра был первым инициатором струк- турного программирования. В 1965 году он высказал предполо- жение о том, что оператор GO ТО мог бы быть исключен из языков программирования. Разумеется, структурное програм- мирование представляет собой нечто большее, чем один лишь отказ от оператора GO ТО. Структурное программирование — это некоторые принципы написания программ. Теоретически- ми основаниями структурного программирования являются □ формальные системы теории вычислимости (операторные схемы программы А. А. Ляпунова, системы Поста, алго- ритмы Маркова, исчисление Черча); □ анализ программ по нисходящей схеме, декомпозиция, основанная на разбивке задач по уровням 0, 1.....к. В классической работе Бома и Джакопини показано, что та- кая структура (иерархическая, разбитая на уровни) может быть реализована в языке, включающем только ограни- ченное число управляющих конструкций. Для реализации программ требуется три основных составля- ющих блока: □ функциональный блок или конструкция следования; □ конструкция обобщенного цикла; □ конструкция принятия двоичного или дихотомического решения. Графическое изображение этих блоков приведено на следую- щих трех рисунках.
Основные управляющие конструкции Характерные черты структурного стиля программирования: □ простота и ясность (программа легко читается и анализи- руется, достаточное комментирование); □ использование только базовых конструкций; □ отсутствие сетевых структур в программе; □ отсутствие многоцелевых функциональных блоков; □ отсутствие неоправданно сложных арифметических и ло- гических конструкций; □ расположение в строке программы не более одного опера- тора языка программирования; □ содержательность имен переменных. Пример неструктурированной логики приведен на рисун- ке. При этом процесс нисходящей разработки программы может продолжаться до тех пор, пока не будет достигнут уровень «атомарных» блоков, т. е. базовых конструкций (присвоения, if-then-else, do-while). Итак, если формулировать суть в сжатом виде, то в струк- турном программировании уточнен принцип декомпозиции за- дачи (в основном ее алгоритмического аспекта, управляющей
64 Часть первая компоненты, т. е. действий, однако уровень интеграции дейст- вий и данных «на совести» разработчика) и сделана попытка его строгой формализации. К нисходящей технологии следует отнести и то, что называется модульным программированием. Достаточно независимые фрагменты задачи оформляются как модули. Создаются библиотеки модулей, определяется меха- низм включения модулей в разрабатываемую программу. Мо- дуль должен иметь строго определенный интерфейс и скрытую часть, одну точку входа и одну точку выхода. Из фольклора computer science — «модульность в программировании подобна честности в политике: каждый утверждает, что она — одно из его достоинств, но кажется, никто не знает, что она собой пред- ставляет, как ее привить, обрести или добиться». Очередной этап развития принципов декомпозиции и абстрагирования. Схематично структура получаемой программы изображена на рисунке. модули н" данные is'1 подпрограммы Следует отметить еще одно важное обстоятельство. В инфор- матике существовало и существует как бы два программирова- ния: теоретическое и практическое. Естественно, без теоретиче- ского программирования не было бы практического, но если строго следовать первому, то любую часть программы следует строить математическими методами, доказывая правильность ее работы. Вопросы взаимного влияния этих подходов — пред- мет отдельного исследования. В данном учебнике речь идет, в основном, о втором программировании. И если теоретическое программирование является уделом математиков — програм- мистов и ему, в принципе, учат, в основном, на факультетах вычислительной математики и кибернетики классических уни- верситетов, то практическое программирование — это сочетание знаний, интеллекта (аналитических способностей ума) и здра-
Основные управляющие конструкции 65 вого смысла, другими словами это искусство разработки про- грамм, причем этому искусству не учат практически нигде. Следует отметить, что на этом витке развития теоретическое программирование оказало очень большое влияние на станов- ление и развитие технологий практического программирова- ния. Проведем черту под этим этапом развития технологий про- граммирования. Структурная технология предоставляет в рас- поряжение разработчиков строгие формализованные методы описания программ и принимаемых технических решений. Ис- пользуется наглядная графическая техника (схемы, диаграм- мы). Однако труд этот не был автоматизирован, а вручную невозможно разработать и графически представить строгие формальные спецификации программы, проверить их полноту и непротиворечивость и тем более изменить. Программы имеют последовательную структуру (управление вычислительным про- цессом), идеи Э. Дейкстры реализованы в полной мере, что по- зволило сделать скачок в развитии технологий. Но, несмотря на возможность конструирования структур данных различной сложности, данные и действия над данными по-прежнему раз- делены. Разрыв между потребностями практики и возможно- стями разработки (по времени, надежности, возможности вне- сения изменений на стадии эксплуатации) сложных программ в пределах данной технологической схемы сохраняется. Образ- ная формулировка сути этого разрыва заключается в том, что «мы не знаем, как этого достичь, у нас нет инструментария для обеспечения процесса правильной разработки программы, но так примерно должна работать эта программа».
66 Занятие № 7. Оператор While План занятия □ обсуждение оператора; □ эксперименты с программами определения количества цифр в числе, преобразования натурального числа п в 1; □ выполнение самостоятельной работы. Оператор While меет вид: | While Логическое выражение» Do <оператор (составной оператор)?, | Оператор While содержит логическое выражение, значение которого (True или False) управляет повторным выполнением оператора (после служебного слова Do), им может быть и со- ставной оператор. Значение выражения вычисляется перед вы- полнением оператора. Если результат равен True, то оператор выполняется, при значении False — нет. Таким образом, если в начале логическое выражение имеет значение False, оператор после Do вообще не выполняется. В операторе (составном опе- раторе) обязательно изменение значений переменных, влияю- щих на значение логического выражения. При невыполнении этого условия получаем пример того, что называется «зацикли- ванием». Простейший пример — While True Do <что-то> — бесконечный цикл. Экспериментальный раздел работы 1. Дано натуральное число п. Подсчитать количество цифр дан- ного числа. Количество цифр в числе п неизвестно, поэтому необходимо использовать оператор While. Использование For потребует или введения дополнительных переменных, или искусственного вы- хода из цикла, а мы договорились, что это плохо — каждый фрагмент нашей логики должен иметь одну точку входа и одну точку выхода. Подсчет количества цифр начнем с последней цифры числа. Увеличим счетчик цифр на единицу (к). Число (ш) уменьшим в 10 раз, убирая тем самым из него последнюю цифру (подсчитанную). Далее с получившимся числом проделаем ту же последовательность действий и т. д., пока число не станет равным нулю. Для этого и всех остальных примеров занятия, требуется выполнять ручную «трассировку». Пусть введено число 65387, присвоим это значение переменной с именем ш, значение К m 0 65387 1 — 6538 2 653 3______65 4______6 I 5 1..J
Основные управляющие конструкции 67 счетчика числа цифр (к) равно 0. Выполним действия, описан- ные выше, их результат приведен в таблице. Итак, окончатель- ное значение переменной к равно 5, в числе 5 цифр. После это- го работаем с программой Му7 1 по традиционной схеме: набор, компиляция, сохранение, запуск и проверка работы на конк- ретных примерах. Program Ply : Var п, о : Longh t; к: Integer;{счетчик числа цифр) Be gm Wr.teLn (' Внесите целое число r-.еравное С); ReadLn (п) ;гг : =п ; к: =0; While tOQ Во Began Inc (к); Liv 10; En d ; WriteLn (' В числе ' ,n,' - ' ,k,’ цифо ReadLn; End. Модифицируя программу My7_l, решить следующие задачи: □ найти сумму цифр числа; □ найти первую цифру числа, например для числа 7265 это цифра 7; □ поменять порядок цифр числа на обратный. Например, было 12345, стало 54321; □ найти количество четных цифр числа; □ найти самую большую цифру числа; □ найти сумму цифр числа, больших 5; □ ответить на вопрос, сколько раз данная цифра встречается в числе? Придумать еще 5 задач (как минимум), решаемых по дан- ной схеме. 2. В программе Му7_2 реализована «гипотеза Сиракуз». Осу- ществляется последовательное преобразование натурально- го числа п в 1. Запустите программу, проверьте ее работу при нескольких значениях п. Результат есть. Мы делаем пред- положение, что при любом значении п работа программы за- вершится, т. е. будет получен результат. Однако это неиз- вестно, доказательства факта завершения работы программы (алгоритма) при любом значении п в настоящее время ни- кем не получено. Проверка циклов типа While требует осо- бо тщательной работы, ибо циклы этого типа «потенциаль- но бесконечны во времени».
Часть первая Program Му7_2; Var п:Integer; Begin WriteLn(Введите натуральное число:'); ReadLn (п) ; Wri te (п) ; While п<>1 Do Begin If n Mod 2=0 Then n:=n Div 2 Else n:=(3*ntl) Div 2; Write ( ’ - ’ ,n) ; End; ReadLn; End. С помощью последовательного запуска программы оцените среднюю длину цепочек чисел при изменении п от 2 до 20. Как избавиться от этой ручной работы по запуску программы? На- блюдая за полученными цепочками чисел нетрудно заметить, что фрагменты этих цепочек часто повторяются. Например, 8=>4=>2=>1, 5=>16=>8=>4=>2=>1. Как использовать этот факт при подсчете средней длины цепочек для чисел (п) из большого ин- тервала, например типа Integer? Потребуется ли в приведенном фрагменте программы что-либо изменять в этом случае? Задания для самостоятельной работы. 1. Дана последовательность операторов: а:=1; Ь:=1; While a+b<8 Do Begin а:=а+1; b:=b+2 End; S:=a+b Сколько раз выполняется проверка логического выражения в операторе While? Определите значения переменных а, Ъ, и s после завершения этой последовательности операторов? 2. Определите значения переменных а и Ъ после выполнения опе- раторов: а:=1; Ь:=1; While а<=3 Do а:=а+1; b:=b+l. 3. Определите значение переменной s после выполнения следу- ющих операторов: □ s:=0; i:=0; While i<5 Do Inc(i); s:=s+100 Div i; □ s:=0; i:=l; While i>l Do Begin s:=s+100 Div i; dec(i) End;
Основные управляющие конструкции 69 4. Дан фрагмент программы с ошибками (их не больше 5) вы- числения факториала f числа п: А : -1 ; f:= О; While к<п Do f=f*k к:=к+1; Найдите эти ошибки. 5. Найдите и исправьте ошибки в следующем фрагменте про- граммы, определяющей для заданного натурального числа п число, записанное цифрами числа п в обратном порядке. р: =п ; While р>=0 Do Begin а:=а+р Mod 10; р:=р Div 10 End; Примечание Задания №1-5 рекомендуется выполнять, используя режим руч- ной трассировки. 6. Найти минимальное число, большее 300, которое нацело де- лится на 19. 7. Приписать по 1 в начало и в конец записи числа п. Напри- мер, было п=3456, стало п=134561. 8. Поменять местами первую и последнюю цифры числа. Напри- мер, из числа 8547 должно быть получено число 7548. 9. Приписать к исходному числу п такое же число. Например, из числа 1903 должно быть получено число 19031903. 10. Определить, является ли заданное число степенью 3. 11. Составить программу, проверяющую, является ли заданное натуральное число палиндромом, то есть таким, десятичная запись которого читается одинаково слева направо и справа налево. Примечание Задача отличается от ранее рассмотренной тем, что количество цифр в числе неизвестно, а из этого следует, что тип используемо- го цикла должен быть другой. 12. Выяснить, является ли последовательность цифр натураль- ного числа при просмотре их справа налево возрастающей по- следовательностью. Например, для числа 76431 ответ поло- жительный, для чисел 6331, 9782 — отрицательный.
70 Часть первая 13. Вводится последовательность целых ненулевых чисел, при- знак окончания ввода — ввод 0. Количество чисел не мень- ше 2. Выяснить: □ является ли последовательность возрастающей; □ есть ли в ней хотя бы одна пара одинаковых «соседних» чисел; □ является ли последовательность знакочередующейся (3, -2, 4, -5, 0 — «да»; 5, -4, -7, 8, 0 — «нет»), 14. Выяснить, сколько раз в натуральном числе встречается его максимальная цифра. Например, в числе 581088 — 3 раза, в числе 4537 — 1 раз. 15. Выяснить, является ли разность максимальной и минималь- ной цифр числа четной. Материал для чтения Объектно-ориентированное программирование. Компь- ютеры 4-го поколения конструируются на основе БИС — бо- льших интегральных схем и СБИС — сверхбольших. Персо- нальные компьютеры определяют лицо компьютеров этого поколения. Скорости обработки огромны, так же как и объе- мы оперативной памяти. Избыточность программного кода в несколько тысяч строк не играет принципиальной роли. Тех- нологии программирования, сделав виток, возвращаются на новом уровне к «детской игре в кубики». Но если в период первого, второго поколений программа «собиралась» из отде- льных операций и пирамида Хеопса не получалась (она разва- ливалась), то на этом этапе развития пирамида собирается из объектов — кубиков, интегрирующих в единое целое данные и допустимые действия над этими данными — объектно-ориен тированное программирование (языки программирования Тур- бо Паскаль, начиная с версии 5.5, Смоллток, C++). Объект- но-ориентированные языки программирования характеризуют- ся тремя основополагающими идеями: инкапсуляцией, насле- дованием, полиморфизмом. Инкапсуляция. Сочетание данных с допустимыми действиями над этими данными приводит к «рождению» нового элемента в конструировании программы — объекта. «Рожденный ползать — летать не может» — и наш объект действует только так, как это в нем заложено, и только над тем, что в нем описано. Обращение к данным объекта не через его действия недопустимо. Наследование. Программист для решения определенного класса задач строит иерархию объектов, в которой, и это самое главное, каждый следующий
Основные управляющие конструкции производный объект имеет доступ (наследует) к данным и дей- ствиям всех своих предшественников («прародителей»). Ха- рактер связей между объектами вертикальный. Полиморфизм. Выделение некоторого действия, т. е. действие должно иметь имя и создание средств использования действия объектами иерархии. Причем каждый объект реализует это действие так, как оно для него подходит. Пример: есть множество геометри- ческих фигур, образующих иерархию. Действие — перемеще- ние по экрану. Мы видим «скачок» в технологии программи- рования, впервые действия и данные образуют нечто единое — новый уровень абстрагирования. Для характеристики объектно-ориентированной технологии проектирования программ обратимся к классической работе Г. Бруча. Этой технологии присущи определенные принципы абстрагирования и декомпозиции. Задача описывается некой иерархической структурой из классов. Основным инструмен- том построения такой структуры является реализация концеп- ции наследования. Наследование означает такое соотношение между классами, когда один класс использует структурную или функциональную часть одного или нескольких других клас- сов (соответственно простое и множественное наследование). Иными словами, наследование — это иерархия абстракций, в которой подклассы наследуют строение от одного или несколь- ких суперклассов. Логическое завершение в объектно-ориенти- рованных системах получила концепция типизации, которая строится на понятии типов абстрактных данных. «Тип — это точное определение свойств строения или поведения, которое присуще некоторой совокупности объектов». При этом возмож- но определение как статических, так и динамических связей. Если, например, Турбо-Паскалю присуща строгая типизация, при которой осуществляется контроль на соответствие типам данных и связи статичны во времени, имена связываются с ти- пами во время компиляции, и связь не изменяется во время ра- боты программы, то в объектно-ориентированных средах воз- можна динамическая связь (поздняя связь). Это означает ситуацию, когда тип всех переменных и выражений определя- ется только во время исполнения программы, что позволяет ре- ализовать идею полиморфизма. Это свойство является самым существенным в объектно-ориентированном программировании наряду со свойством реализации абстракций. Именно это свой- ство отличает объектно-ориентированное программирование от более традиционных методов программирования с использова- нием типов абстрактных данных.
72 Часть первая Итак, подведем итоги. Развитие технологии привело к со- зданию методов объектно-ориентированного анализа. По боль- шому счету он мало отличается от того, который назывался структурным анализом на предыдущем этапе развития техно- логий. Если раньше результатом работы было алгоритмиче- ски-ориентированное решение задачи, то на этом этапе — опре- деление основных объектов и взаимодействий между ними. Первый этап работы программиста по-прежнему плохо форма- лизован и не обеспечен необходимой поддержкой. На втором этапе за счет интеграции данных и действий над данными (ин- капсуляции), строгого определения принципов декомпозиции (наследование и полиморфизм, модульность), ограничения до- ступа к данным объекта появилась возможность создания более надежных и качественных программ. Этапы тестирования и со- провождения упростились. Они в большей степени становятся инженерной работой. Структура конечного продукта (програм- мы) последовательная, как и, в целом, процесс его разработки. Потребности практики по-прежнему не соответствуют имею- щимся возможностям. Интенсивно развиваются сетевые техно- логии, системы обработки текстовой информации, мультиме- дийные программные системы и т. д. Ответные шаги процесса развития технологий программирования мы рассмотрим в сле- дующих материалах для чтения.
Основные управляющие конструкции Занятие № 8. Оператор Repeat-Until План занятия □ обсуждение оператора; □ эксперименты с программами определения простоты чис- ла, нахождения наибольшего общего делителя двух чисел с помощью алгоритма Евклида; □ выполнение самостоятельной работы. Оператор Repeat (повторять)-17пШ (до тех пор, пока) содер- жит логическое выражение (после Until}, которое управляет повторением выполнения последовательности операторов, за- писанных между Repeat и Until. Повторение продолжается до тех пор, пока логическое выражение не примет значение True. Последовательность операторов тела цикла выполняется не ме- нее одного раза. Repeat <оператор 1>; <оператор 2>; <оператор п>; Until логическое выражением При использовании оператора Repeat-Until (цикла с постус- ловием) необходимо учитывать следующее: □ перед первым выполнением оператора логическое выра- жение его окончания (или продолжения) должно быть определено; □ последовательность операторов должна содержать хотя бы один оператор, влияющий на значение логического выра- жения, иначе оператор Repeat-Until работает бесконечно долго; □ логическое выражение в конечном итоге должно принять значение True. Пример простейшей, бесконечной по времени работы, конст- рукции: Repeat ... Until False. Экспериментальный раздел работы 1. Целое положительное число р называется простым, если оно имеет только два делителя, а именно 1 и р. По соглашению 1 не считают простым числом. Начало последовательности про- стых чисел имеет вид;
74 Часть первая 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, Научимся устанавливать факт: является ли число п про- стым? Ниже приведен текст решения. Считаем, что делители числа находятся в интервале от 2 до л Div 2, точнее в интервале от 2 до целой части Sqrt(n). Program Му8_1; Var i,п:Longlnt; Begin WriteLn('Введите натуральное число'); ReadLn (п) ; 1: =1; Repeat Inc(i) ; Until (i> n Div 2) Or (n Mod i = 0) ; If i> n Div 2 Then Writein('Число ',n,' простое') Else WriteLn('Число первый делитель числа ' ,n); ReadLn; End. Естественно, что эту задачу можно решить и с использовани- ем оператора While. Сделайте эту небольшую работу. А затем измените программу так, чтобы осуществлялся вывод всех де- лителей числа п. Подсказка Логическое выражение оператора Repeat-Until упростится, оста- нется только условие i> п Div 2, а в операторах тела цикла появит- ся — If п Mod i =0 Then WnteLn(.,i). Основная теорема арифметики. Всякое натуральное число п разлагается на произведения простых чисел. Это разложение однозначно с точностью до порядка записи простых чисел в произведении. А сейчас задание. Выберите интервал чисел, не очень боль- шой, например от 101 до 120, и, используя программу, найдите разложения этих чисел на произведения простых чисел. Ваши записи должны иметь вид: n=Pialp2“2....p “ч, где (а^О), р, — простые числа, например, 24=23*31.
Основные управляющие конструкции 75 2. Наибольший общий делитель (НОД) двух целых чисел а и Ъ — это наибольшее целое число, которое делит нацело оба чис- ла. Для нахождения НОД чисел а и Ъ можно представить оба числаввиде: а^р^р/2...^ а’ и b=p1₽1p2₽2....pq₽4 а затем най- ти НОД(а,Ь)=р1т1п(а1Р1)р2пШ1С«2Р2).t p^nun(aqpq) , некоторые степе- ни простых чисел могут быть и нулевые. Отложим исследо- вание этого метода нахождения НОД, у нас пока не хватает знаний. Обратимся к алгоритму греческого математика Евк- лида, он открыл его в 330-320 гг. до н. э. Алгоритм основан на следующем свойстве целых чисел. Пусть а и Ъ одновремен- но не равные нулю целые неотрицательные числа и а>Ъ, тог- да если Ь=0, то НОД(а,Ъ) =а, а если Ъ^О, то для чисел а, b и г, где г — остаток от деления а на Ь, выполняется равенство НОД(а,Ъ)=НОД(Ъ,г). Действительно, r=aModb=a-(aDiub)*b. Если какое-то число делит нацело и а, и Ъ, то из приведенно- го равенства следует, что оно делит нацело и числа г и Ь. Например, пусть а=48, а Ь=18, найдем их наибольший об- щий делитель. Приведем ручную трассировку логики, она, как обычно, сведена в таблицу. a I b Результаты 48 ( 18 48 Mod 18=12 18 a>b НОД(48,18)=НОД(12.18) I 12 18 mod 12=6 a<b НОД(12,18)=НОД(12,6) 12 mod 6=0 6 a>b НОД(12,6)=НОД(0,6) I 0 6 a=0 НОД(0,6)=6 Таким образом, НОД(48,18)=6. Программная реализация алгоритма Евклида с использова- нием оператора Repeat-Until имеет вид Program Му8_2 ; Var a, b: Longlnt; Begin WriteLn (' Введите два числа о 0') /ReadLn (а,Ь) ; Repeat If a>b Then a:-a Mod b Else b:=b Mod a; Until (a=0) Or (b=0) ; WriteLn('НОД=',a+b) ); ReadLn; End.
76 Часть первая Измените программу так, чтобы вместо оператора Repeat- Until использовался оператор While. Какое ограничение в этом случае мы можем убрать? Последовательность чисел ej = l + l=2, е2=2 + 1 = 3, е3=2*3+1=7, е4=2*3*7+1=43, е5=2*3*7*43+1=1807, е6=2*3*7*43*1807+1=3263443, Е7=2*3*7*43*1807*3263443+1=547*607*1033*31051, и т. д. называют числами Евклида. Первые 4 числа наталкивают на мысль о том, что Евклидовы числа простые. Однако уже е5 яв- ляется составным — 1807=13*139. Известно, что Евклидовы числа взаимно простые — НОДСе^е^! при i^J. Проверьте этот факт с помощью программы Му8_2 для первых 6 чисел Евкли- да. Для работы с остальными числами Евклида типа Longlnt недостаточно, необходимо, по крайней мере, изучить основы «длинной» арифметики. Используя программу Му8_1, пока- жите, что число е6 простое. Методические рекомендации В режиме ручной трассировки необходимо просчитать для заданий 1, 2 по 7-10 примеров. Найти НОД(342,612). 3. В теории чисел, разделе дискретной математики, доказы- вается следующая теорема. Если а и Ъ одновременно не равны нулю, то существуют целые числа х и у, такие, что НОД(а,Ъ)= =а*х+Ъ*у. Теорема не утверждает, что х и у определены од- нозначно, она лишь говорит о том, что НОД(а,Ъ) может быть выражен в таком виде. Например, 6=НОД(12,-30)=12*3+ +(-30)*1=12*(-2)+(-30)*(-1). Сформируем последовательность: □ а0=а, а0=а*х0+Ъ*у0, где х0=1, а у0=0; □ aj=b, a1=a*x1+b*y1, где Xj=O. а у1=1; D а2=ао-а1 *91 > гДе Qi=ao Div ai> подставляем значения а0 и ар получаем a2=a*x0+b*y0-(a*xI+b*y1)*q1=a*(x0-x1*q1)+ +b*(y0-y! *q1)=a*x2+b*y2; □ a3=a1-a2*q2, где q2=a3 Div а2, подставляем значения а7 и а2, получаем a3=a*x1+b*y1-(a*x2+b*y2)*q2=a*(x1-x2*q2)+ +b*(y1-y2*q2)=a*x3+b*y3; □ ...
Основные управляющие конструкции 77 □ гДе Ч1_1=а1_2 Div ai-i> a1=a*x1_2+b'lry1_2- -(a*x1_1+b*y1_1)*q1_1=a*x1+b*y1; □ ... ° 0=ak-i-ak*<Ik’ гДе Qk=ak-i Div ak> - 0=a*xk+1+b*yk+1. □ Что мы имеем? Итерационный процесс, в котором: □ 2 Div ах_р □ ai=ai-2—ai-i*qi-i; □ x1=x1_2-x1_I*q1_1; n У^^-2-yi-i*^-!- Рассмотрим трассировку логики на примере чисел а=48 и Ь=18. Итак, а=НОД(48,18)=6 и 6=48*(-1)+18*3. Методические рекомендации Проделайте аналогичную трассировку для нескольких примеров типа а=1292, Ь=798. Программное решение данной задачи имеет вид: Program Му8_3; Var a,b,aO,al,xO,xl,yO,yl,q,t:Integer; {Нижних индексов у имен переменных на языке Турбо Паскаль нет. } Begin WriteLn('Введите два числа, первое должно быть больше второго'); ReadLn(a,b); а 0:=а;al:=Ь;хО:=1;х1:=0;у0:=0;у1:=1; Repeat q:=aO Div al; t:=aO;aO:=al;al:=t-al*q; t:-xO;xO:=xl;xl:=t-xl*q; t:^yO;y0:=yl;yl:=t-yl*q; Until al=0; WriteLn (aO,' = a,'*(',x0, ') + ' ,b,'*,y0,’) ') ; ReadLn; End.
78 Часть первая Сравните результаты решения примеров с помощью ручной трассировки с результатами работы программы при этих же ис- ходных данных. Задания для самостоятельной работы 1. Числа вида 2₽—1, где р — простое число, называются числа- ми М. Мерсенна (1588-1648 гг.). Являются ли числа Мерсен- на при значениях р 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 про- стыми? Написать программу. Почему мы ограничились то- лько этими простыми числами. Ваша программа должна со- стоять из двух частей: в первой — вычисляется число Мер- сенна для значения р (вводится с клавиатуры) , во второй — проверяется является ли оно простым. 2. Линейные уравнения от двух переменных (линейные диофан- товые уравнения), т. е. уравнения вида: а*х+Ь*у=с, в кото- ром а и Ъ не равны нулю одновременно, имеют целые реше- ния тогда и только тогда, когда d (d=HOfl(a,b)) делит нацело значение с. Причем, если х0, у0 — частное решение, то все ре- шения имеют вид: x=x0~n*(b Div d), y=y0+n*(a Div d) для всех n. Пример: 12*x-30*y=84, НОД(12,-30)=6, 84 делится наце- ло на 6. Уравнение разрешимо. Одним из его решений яв- ляется (х,у)=(2,-2). Все остальные решения имеют вид: х=2+5*п, у=-2+2*п. Даны целые числа а, Ъ, с. Написать программу определения разрешимости соответствующего диофантового уравнения и, если оно разрешимо, поиска частного решения. 3. Пусть а и b — ненулевые целые числа. Целое число т>0 назы- вается наименьшим общим кратным (НОК) чисел а и Ь, если ш делится и на а, и на b нацело, а также для любого с, которое делится нацело и на а и на Ъ, верно, что оно делится нацело и на ш. Если а иЪ — ненулевые числа, то их наименьшее общее крат- ное существует и справедливо равенство HOK(a,b)=Abs(a*b> Div НОД(а,Ь). Написать программу нахождения НОК двух не- нулевых чисел. 4. Числа Фибоначчи (fn) определяются формулами: f^f^l; ^п=^п-1+^п-2 ПРИ п==2, 3,..., т.е. это бесконечная последовате- льность чисел вида: 1,1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...Соста- вить программу □ определения номера последнего числа Фибоначчи, кото- рое входит в диапазон типа Integer (Longlnt);
Основные управляющие конструкции 79 □ вычисления s — суммы первых чисел Фибоначчи, таких, что значение s не превышает диапазона типа Integer (Longlnt). 5. Совершенным числом называется число, равное сумме всех своих делителей, меньших, чем оно само. Например: 6=1+2+3, 28=1+2+4+7+14. Древним грекам были известны только четыре первых числа. Составить программу, проверяю- щую, является ли заданное натуральное число совершенным. Примечания 1. Если Вы составите программу поиска 4 совершенных чисел, то материал следующего занятия Вы уже поняли. Подсказка: пер- вые два числа Вам известны, а четвертое число не превышает зна- чения 9999. 2. Поиск пятого числа и т. д. — отдельная проблема. Евклидом до- казано, что каждое число вида 2р1*(2',-1) является совершенным числом, если 2р-1 — простое число. Л. Эйлер доказал, что все чет- ные совершенные числа находятся по формуле Евклида, а относи- тельно нечетных совершенных чисел ничего неизвестно до сих пор. 6. Автоморфным называется такое число, которое равно послед- ним цифрам своего квадрата. Например: 52=25, 252=625. Оче- видно, что автоморфные числа должны оканчиваться либо на 1, либо на 5, либо на 6. Составить программу нахождения ав- томорфных чисел (с учетом приведенного факта), не превы- шающих значения 999. Примечание Не забывайте о диапазонах переменных целого типа, 9992=998001. 7. Кубические автоморфные числа равны последним цифрам сво- их кубов. Например: 63=216. Верно ли, что и такие числа дол- жны оканчиваться либо на 1, либо на 5, либо на 6? С учетом этого факта составить программу нахождения двузначных, трехзначных кубических автоморфных чисел. Материал для чтения 1. Одно из основных свойств целых чисел — это свойство де- лимости, или евклидовости. Мы его неявно использовали на предыдущих занятиях. В математике оно формулируется как теорема и, естественно, доказывается. Теорема. Для любого а и любого ненулевого b существуют един- ственные (целые) частное q и остаток г, такие, что a=b*q+r, 0<r<abs(b).
80 Часть первая Идея доказательства. Рассмотрим множество целых чисел вида a-k*b, где к пробегает все множество целых чисел, поло- жительных и неположительных: .... а-3*Ъ, а-2*Ъ, а-b, а, а+Ь, а+2*Ъ, а+3*Ъ, ... В этой последовательности выбирается наимень- шее неотрицательное число и обозначается через г, а через q обо- значается соответствующее значение k(r=a-q*b). Существование такого г следует из конечности множества чисел последователь- ности, удовлетворяющих условию теоремы, и их упорядоченно- сти. Далее, методом от противного, доказывается единственность значения г. 2. Простых чисел бесконечно много. Это доказано еще Евк- лидом. Его доказательство. Предположим, что простых чисел конечное множество. Пусть их к: 2, 3, 5, ....Рк. Рассмотрим чис- ло М=2*3*5*...*Рк+1. Ни одно из простых чисел не может де- лить М, ибо каждое из них делит М-1. Тогда должно быть дру- гое простое число, делящее М, или само М — простое. А это противоречит предположению о конечности множества про- стых чисел, тому, что только 2, 3, 5, .... Рк простые числа. 3. Продолжим наше знакомство с основными понятиями технологий программирования. Пусть Вам после первого чте- ния не все понятно, это нормальное явление. Только очень про- стые книги читаются «с листа». Познакомимся с понятием мо- дели жизненного цикла программ. В настоящее время понятие жизненного цикла программы является одним из базовых по- нятий в технологии программирования. Жизненный цикл — это непрерывный процесс, который начинается с момента при- нятия решения о необходимости создания программы и закан- чивается в момент ее полного изъятия из эксплуатации. Основ- ным нормативным документом, регламентирующим жизненный цикл программы, является международный стандарт ISO/IEC 12207 (ISO — International Organization of Standardization — Международная организация по стандартизации, IEC — Inter- national Electrotechnical Commission — Международная комис- сия по электротехнике). Он определяет структуру жизненного цикла, которая базируется на трех группах процессов: □ основные процессы (приобретение, поставка, разработка, эксплуатация, сопровождение); □ вспомогательные процессы, обеспечивающие выполнение основных процессов (документирование, управление кон- фигурацией, обеспечение качества, верификация, аттеста- ция, оценка, аудит); □ организационные процессы (управление проектами, созда- ние инфраструктуры проекта, обучение).
Основные управляющие конструкции 81 К настоящему времени известны две основные модели жиз- ненного цикла: □ каскадная (до 1985 г.) — первые два рисунка; □ спиральная модель (с 1986 г. по 90-е гг.) — третий рису- нок. Реализация и внедрение Проектирование Интеграция Оформление требований
82 Часть первая Каскадный подход хорошо зарекомендовал себя при постро- ении программ, для которых в самом начале разработки можно достаточно точно и полно сформулировать все требования. При- мер — расчетные задачи (первое, второе поколения). В случае неточного изложения требований или их изменения получается программа, не отвечающая потребностям пользователя, заказ- чика, поэтому реальный процесс разработки имеет вид, приве- денный на втором рисунке. Для преодоления недостатков каскадной схемы была пред- ложена, исторически сложилась (наверное, начиная с визуаль- ных сред) спиральная модель жизненного цикла программы (третий рисунок). Целевая установка — упор на анализ и про- ектирование, путем создание прототипов (моделей!!!). Каждый виток спирали соответствует созданию версии программы. На ней уточняются цели и требования, определяется качество, и планируются работы по следующему витку. Таким образом, уг- лубляются и последовательно конкретизируются детали про- граммы и выбирается окончательная версия, которая доводится до реализации. Сходящий вид спирали подчеркивает «размы- тость» первоначальных требований и их последующее уточне- ние. 4. Ошибки в программах разделяют по типам на синтаксиче- ские, семантические и логические. С первыми мы встречались на предыдущих занятиях. Они достаточно просто устраняются и говорят о несоответствии текста программы правилам языка Турбо Паскаль. Вторые возникают на стадии выполнения про- граммы. Например, деление на ноль не устраняется на стадии компиляции, а приводит к ошибке на стадии выполнения, ибо зависит от конкретного значения переменной. Третий тип оши- бок самый сложный в обнаружении. Программа работает так, как написана, но не так, как требуется. Такие случаи являются следствием многих причин. Главное, что их сложно обнару- жить. В системе Турбо Паскаль имеется достаточно мощный отладчик, позволяющий находить ошибки третьего типа в про- граммах. Он работает с исходным текстом программы, позволя- ет выполнять программу по шагам и отслеживать изменение переменных программы в процессе работы последней. Перечислим основные команды отладчика и соответствую- щие им клавиши, позволяющие выполнять программу по ша- гам и отслеживать значения переменных. Шагом при отладке является строка программы. Так, если в строке исходного тек- ста программы записано несколько операторов, то они будут выполнены за ottww тля?
Основные управляющие конструкции 83 Команда меню Функциональная I клавиша Назначение I Run/Run Ctrl+F9 Запуск программы Run/Go tu cursor F4 Выполняет программу до ! строки, в которой находится । курсор ! Run/Trace into Выполняет оператор |! (операторы), записанный в |> текущей строке Если в этой |i строке записан вызов 1 процедуры или функции, то осуществляется вход в эту : процедуру или функции, отслеживается их работа |, Run/Step over F8 Выполняется оператор, записанный в текущей строке без вхождения в процедуры или функции Debug/watch Открывает окно для наблюдения за значениями переменных (окно Watches) Debug/Breakpoints 1 Открывает окно для работы с । 1 точками останова , i Debug/Evaluate Ctrl+F4 Открывается окно Evaluate and 1 Modify Набирается выражение (можно с использованием переменных прогоаммы) и [ вычисляется его значение [ Debug/Add watch Ctrl+F7 Открывает окно Add watch для ; набора отслеживаемых 1 значений выражений или | переменных Эти действия можно выполнить и другим !! способом Сделать окно Watch | активным и использовать || клавиши Insert и Delete для вставки и удаления выражений и переменных | Debug/Add breakpoint Clrl+F8 Текущая строка (в ней 1 находится курсор) |
Часть первая Занятие № 9. Вложенные циклы План занятия □ обсуждение конструкции; □ эксперименты с программами (с использованием отладчи- ка системы программирования) вычисления выражения lk+2k+...+nk, нахождения цифрового корня числа; ста- ринной задачи о быках, коровах и телятах, нахождения натуральных чисел, удовлетворяющих определенному усло- вию, и сведения чисел, кратных 3, к числу 153; □ выполнение самостоятельной работы. Для решения задачи достаточно часто требуется использо- вать две и более циклические конструкций, одна из которых помещается в тело цикла другой. Такие конструкции называют вложенными циклами. Внутренний и внешний циклы могут быть любыми из трех рассмотренных ранее видов. Правила ор- ганизации внешнего и внутреннего циклов такие же, как и со- ответствующих простых операторов. Однако при использовании вложенных циклов необходимо соблюдать следующее условие: внутренний цикл должен полностью укладываться в цикличе- скую часть внешнего цикла. Экспериментальный раздел работы 1. Даны натуральные числа пик. Составить программу вычис- ления выражения 1к+2к+...+пк. Для вычисления указанной суммы целесообразно использо- вать оператор For с управляющей переменной 1, изменяющейся от 1 до п. В теле цикла, во-первых, вычислялось бы очередное значение y=ik и, во-вторых, осуществлялось бы накопление суммы прибавлением полученного слагаемого к сумме всех предшествующих (s:=s+y). Program Му9_1; Var п, к, у, i, s, j: Integer; Begin WriteLn('Введите исходные данные п и к ') ; ReadLn (п,к) ; s:=0 ; For i:=l То п Do Begin For j:=l To к Do y:=y*i; s:=s+y; End; WriteLn('Ответ: ’,s); End.
Основные управляющие конструкции 85 А сейчас попробуем работать с отладчиком нашей системы программирования. Вспомните материал для чтения предыду- щего занятия, пункт 4. Итак: □ Выполните команду Debug/Watch. Появится окно Watc- hes. □ Используя клавишу Ins (инициируется открытие окна Add Watch), введите переменные программы (n, k, s, i, j, у). □ Измените положение и размер окна Watches на экране. Оно должно находится в правом верхнем углу и быть та- ким, чтобы все введенные переменные были обозримы. С этой целью выполните команду Window/Size/Move (Ctrl+ +F5) и используйте клавиши Shift+(<=, 11) для изменения размера окна и клавиши (=>, U) для его перемещения. □ Выполните команду Run/Step over (F8). Строка с операто- ром Begin выделяется другим цветом (курсор). При нажа- тии на F8 курсор перемещается на следующую строку — пошаговое выполнение программы. П Выполните программу в пошаговом режиме (последовате- льное нажатие на F8), отслеживая изменение значений переменных в окне Watches. При выполнении ReadLn (n,k) введите, например, значения 4 и 2 для того, чтобы количество повторений в операторах For было не очень бо- льшим. Продолжим изучение отладчика. □ Установите курсор на строку программы с оператором s.~s+y. Выполните команду Debug/Add Breakpoint (Ctrl+ +F8). Строка будет выделена другим цветом. Мы создали точку останова в программе. □ Запустите программу (Ctrl+F9). Работа программы при- останавливается в том случае, когда достигнута точка останова. □ Выполните один шаг в работе программы — нажмите кла- вишу F8. □ Выполните команду Debug/Evaluate... В строке Expression окна Evaluate and Modify наберите имя переменной s. В строке Result окна мы видим значение переменной s, оно равно 1. □ Продолжите работу с программой по описанной схеме. Следующее значение переменной s равно 5.
86 Часть первая Методические рекомендации Максимально использовать отладчик системы программирования при выполнении заданий этого и следующих занятий. И так до тех пор, пока не появятся устойчивые навыки работы с ним. 2. Изменить программу так, чтобы вычислялась сумма 1*+22+ + ...+пп. Подсказка. Требуется изменить параметры у внут- реннего оператора For (For ]~1 То i Do y~y*i;) и убрать пе- ременную k. 3. Пусть значение к зафиксировано, например равно 5. Опреде- лить значение п, при котором диапазон целого типа данных Integer окажется недостаточным для хранения суммы степе- ней чисел. 4. Сложим все цифры какого-либо числа. Получим новое чис- ло, равное сумме всех цифр исходного числа. Продолжим этот процесс до тех пор, пока не получим однозначное число (циф- ру), называемое цифровым корнем данного числа. Например, цифровой корень числа 34697 равен 2 (3+4+6+9+7=29; 2+9=11; 1 + 1=2). Составить программу нахождения цифро- вого корня натурального числа. Program Му9_2; Var г,к,s:Longlnt ; Begin WriteLn('Введите число');ReadLn (п); s: =п ; While s>9 Do Begin k:=s; s:=0; Repeat s:=s+k Mod 10; k:=k Div 10; Until k=0; End; WriteLn ('Цифровой корень числа ' ,n,' равен ' ,s); End. 5. Старинная задача. Сколько можно купить быков, коров и телят, если плата за быка 10 рублей, за корову — 5 рублей, за теленка — полтинник (0.5 рубля), если на 100 рублей надо купить 100 голов скота. Решение. Обозначим через Ъ — количество быков; к — коли- чество коров; t — количество телят. После этого можно за- писать два уравнения: 10b+5k+0.5t=100 и b+k+t=100. Пре- образуем их: 20b+10k+t=200 nb+k+t=100.
Основные управляющие конструкции 87 На 100 рублей можно купить: □ не более 10 быков, т. е. 0<Ь<10, □ не более 20 коров, т. е. 0<к<20, П не более 200 телят, т. е. 0<t<200. Таким образом, получаем: Program Му9_3; Var b, к, t: Integer; Begin For b:=0 To 10 Do For k:=0 To 20 Do Fol t:=0 To 200 Do If (20*o+10^k^t=20G) Ano 00) Tien WriteLn ('быков ’,o,’ коров ',k,’ телят ',t); End. Сколько раз будет проверяться условие в данной программе (оператор //)? Значение переменной b изменяется 11 раз (от 0 до 10), для каждого ее значения переменная к изменяется 21 раз, а для каждого значения переменной к переменная t изме- няется 201 раз. Таким образом, условие будет проверяться 11*21*201=46431 раз. Но если известно количество быков и коров, то количество телят можно вычислить по формуле t=100—(b+k) и цикл по переменной t исключается. Program My9_3m; Var Ь, к, t: Integer; Begin For b:=0 To 10 Do For k:=0 To 20 Do Begin t:=100- (b+k) ; If (20*b+10*k + t=200) Then WriteLn ( ’быков ’ ,b,' коров ',k,' телят ',t); End; End. При этом решении условие проверяется 11*21=231 раз. Тре- буется еще уменьшить количество проверок. 4. Написать программу, которая находит все четырехзначные числа abed (a, b, с, d — цифры числа, причем между ними нет совпадений, т. е. числа, например, типа 1221 нас не устраи- вают, т. е. любые две цифры числа различны), для которых выполняется условие: ab-cd=a+b+c+d. Другими словами, раз- ность чисел, составленных из старших цифр числа и из млад- ших, равна сумме цифр числа.
88 Часть первая Задачу можно решать разными способами. Один из них — перебор всех четырехзначных чисел и проверка выполнения условия. Попробуем сократить перебор, для этого преобразуем условие. Из равенства 10*a+b-(10*c+d)=a+b+c+d получаем 9*(a-c)=2*(c+d) или (a-c)/(c+d)=2/9, а=с+2, d=9-c и 0<с<7. Program Му9_4; Var a,b,с,d:Integer; Begin For c:=0 To 7 Do Begin a:=c+2; d:=9-c; For b:=0 To 9 Do If (boc) And (boa) And (bod) ТПеп Wri teLn (a ,b, c, d) ; End; End. 5. Дано натуральное число, кратное 3. Найдем сумму кубов цифр данного числа. Получим новое число. Применим к нему та- кое же преобразование. А что в итоге? Оказывается, что лю- бая такая последовательность чисел, начиная с некоторого ме- ста, становится постоянной и ее элементы равны 153. Из теории чисел. Если число aja2...ak делится на 3, то и s=a1+a2+...+ak делится на 3. Это необходимый и достаточный признак делимости числа на 3. Из него следует, что и сумма ку- бов цифр числа кратна 3. Действительно: s3=a13+a23+...+ak3+ +3*Р, где Р — сумма попарных произведений цифр и их квад- ратов. Отсюда следует, что ai3+a23+...+ak3=s3-3*P, т. е. сумма кубов цифр рассматриваемого числа кратна 3. Осталось пока- зать, что последовательность является сходящейся. Это дока- зывается методом математической индукции. Вам же предлага- ется составить таблицу: в первой строке параметр к, задающий количество девяток в числе, во второй строке — сумма кубов цифр такого числа. Значение к изменяется от 1 до 10. Проанализируйте таблицу. Вы увидите, что при к>5 количе- ство цифр в числе, составленном из суммы кубов цифр, умень- шается. Таким образом, если Вы проверите сходимость после- довательности для всех натуральных чисел, например, меньших
Основные управляющие конструкции 89 или равных 33334, то для больших чисел последовательности сходятся в силу предыдущего утверждения. Вопрос о сходимо- сти последовательности именно к значению 153 остается от- крытым. Program Му9_5; Var п,Л1, t,s,q:Longlnt; Begin WriteLn('Введите чисто, оно умножается на 3, и для полученного числа строится последовательность');ReadLn (п); n: =3*n ; Write (то, ' '), Repeat s:=0 ;t:; hhile m>0 Do Begin q:=m Mod 10 ; s: =s + q*q + q; m:-ro Div 10; End; to : =s ; Wri te (m, ' ') ; Until m=t; ReadLn; End. Проверьте, что эта логика работает для всех чисел, напри- мер, меньших 33333. Для этого потребуется, естественно, до- полнить программу. Задания для самостоятельной работы 1. Что будет выведено на экране монитора после выполнения следующего фрагмента программы: а:=1; Ь:=1; For i:=0 То n Do Begin For j : =1 To b Do Write ('*'); WriteLn; c:=a+b; a:=b; b:=c; End; если n=6? Решение какой задачи выражает этот фрагмент про- граммы? 2. Что будет выведено на экране монитора после выполнения следующего фрагмента программы:
90 Часть первая b:=0; While а<>0 Do Begin b:=b*10+a Mod 10; a:=a Div 10; End; Write (b) ; если a=13305? Решение какой задачи выражает этот фрагмент программы? 3. Исходное данное — натуральное число q, выражающее пло- щадь. Написать программу для нахождения всех таких пря- моугольников, площадь которых равна q и стороны выраже- ны натуральными числами. 4. Составить программу для графического изображения дели- мости чисел от 1 до п (п — исходное данное). В каждой стро- ке надо печатать число и столько плюсов, сколько делителей у этого числа. Например, если исходное данное — число 4, то на экране должно быть напечатано: 1 + 2+ + 4 + + + 5. Дано натуральное число п. Можно его представить в виде сум- мы трех квадратов натуральных чисел? Если можно, то: □ указать тройку х, у, z таких натуральных чисел, что x2+y2+z2=n. □ указать все тройки х, у, z таких натуральных чисел, что x2+y2+z2=n. 6. Найти натуральное число от 1 до 10000 с максимальной сум- мой делителей. 7. Даны натуральные числа а, Ъ (а<Ъ). Получить все простые чис- ла р, удовлетворяющие неравенствам: а<р<Ъ. 8. Даны натуральные числа n, т. Получить все меньшие п на- туральные числа, квадрат суммы цифр которых равен т. 9. Даны натуральные числа пит. Найти все пары дружествен- ных чисел, лежащих в диапазоне от п до т. Два числа называ- ются дружественными, если каждое из них равно сумме всех делителей другого(само число в качестве делителя не рассмат- ривается). 10. В данном натуральном числе переставить цифры таким об- разом, чтобы образовалось наименьшее число, записанное эти- ми же цифрами.
Основные управляющие конструкции 91 11. Составить программу, печатающую для данного натурально- го числа k-ю цифру последовательности: □ 12345678910..., в которой выписаны подряд все натура- льные числа; □ 14916253649..., в которой выписаны подряд квадраты всех натуральных чисел; □ 1123581321..., в которой выписаны подряд все числа Фибоначчи. 12. Составить программу возведения заданного числа в третью степень, используя следующую закономерность: 23=3 + 5 33=7 + 9+11 43=13+15+17+19 53=21+23+25+27+29 13. Составить программу для нахождения всех натуральных ре- шений уравнения n2+m2=k2 в интервале [1, 10]. Примечание Решения, которые получаются перестановкой п и ш, считать сов- падающими. 14. Выписать фрагмент программы для решения указанной ниже задачи и обосновать, почему был выбран тот или иной вари- ант оператора цикла: □ вычислить факториал некоторого числа р; □ задано число р, определить, является ли оно факториа- лом некоторого числа, если «да», то найдите это число; □ определить, является ли заданное число степенью числа 3; □ вычислить у=1!+2!+3!+...+п1 □ вычислить хп, где п — целое положительное число; □ вычислить значения х1, х2, х3,..., хп. 15. Дано трехзначное число. Найти все трехзначные числа, удов- летворяющие каждому из условий: □ состоящих из тех же цифр, что и исходное число; □ равное среднему арифметическому всех трехзначных чи- сел (включая данное), имеющих тот же цифровой состав. 16. Стороны прямоугольника заданы натуральными числами М и N. Составить программу, которая будет находить, на ско- лько квадратов, стороны которых выражевы натуральными числами, можно разрезать данный прямоугольник, если от него каждый раз отрезается квадрат максимально большой площади.
92 Часть первая 17. Даны натуральные числа N и р. Получить все натуральные числа, меньшие N и взаимно простые с р. 18. Даны целые числа р и q. Получить все делители числа q, взаимно простые с р. 19. Сумма квадратов длин катетов а и b прямоугольного треуго- льника равна квадрату длины гипотенузы с: а2+Ь2=с2. Трой- ка натуральных чисел, удовлетворяющих этому равенству, называется Пифагоровыми числами Составить программу нахож- дения основных троек Пифагоровых чисел, используя следующие формулы a=u*v; b=(u*u-v*v) Div 2, c=(u*u+v*v) Div 2, где u и v — взаимно простые нечетные натуральные числа и u ->v. 20. Найти наименьшее натуральное число N, представимое дву- мя различными способами в виде суммы кубов двух натура- льных чисел х3 и у3 (х>у). 21. Даны натуральные числа т, пр п2,..., пга (т>2). Вычислить Н0Д(п1,п2,...,пт), воспользовавшись соотношением НОД(п1( п2,...,пт)=НОД(НОД(п1,п2,...,пт_1),пт) и алгоритмом Евкли- да. 22. Найти все простые несократимые дроби, заключенные меж- ду 0 и 1, знаменатели которых не превышают 7 (дробь зада- ется двумя натуральными числами — числителем и знаме- нателем). Материал для чтения Продолжим обзор технологий программирования. Визуаль- ные технологии. Визуальную технологию конструирования про- грамм (например, систему программирования Delphi) можно отнести к технологиям пятого поколения. Она, во-первых, пол- ностью поддерживает объектно-ориентированную технологию, во-вторых, идеи модульного программирования получают ло- гическое завершение, в-третьих, и это принципиально новое в данной технологии — создан инструментарий (автоматизация) программирования реакции на события (любая программа в процессе своей работы с чем-то или кем-то взаимодействует). Структура программного кода вероятностная. Не все маршру- ты, трассы кодов жестко определены. Элементы программного кода взаимодействуют, начинают работать при возникновении определенных событий. А главное — процесс разработки носит не каскадный, последовательный характер, он развивается по спирали. Модель многокомпонентных объектов. Модель многоком- понентных объектов (Component Object Model — СОМ) лежит в
Основные управляющие конструкции 93 основе OLE (Object Linking and Embedding — связывание и встраивание объектов) и ActiveX (одна из технологий Micro- soft). Традиционно программы разных типов предоставляли свои сервисы по-разному (вызовы локальных функций, переда- ча сообщения средствами связи между процессами, системные вызовы и др.). СОМ определяет общий способ доступа к про- граммным сервисам. Объекты СОМ предоставляют сервисы че- рез методы, объединенные в интерфейсы. Вспомним идеи объ- ектно-ориентированного программирования. Объединяются в единое целое данные и действия над данными. В визуальном программировании дается инструментарий для разработки взаи- модействия программы и пользователя, программ между собой. Логика развития программирования (как науки, так и отрасли производства) подводит к объединению определенных схем взаи- модействия объекта с другими объектами в нечто целое, назы- ваемое интерфейсом. Заметим, что в результате появляется новый инструментарий абстрагирования. Продолжим. Объ- ект, использующий ресурсы других объектов, обычно называ- ют клиентом. Описание поведения объекта включает описание операций, которые могут выполняться над ним, и операций, которые сам объект выполняет над другими объектами. При этом концентрируется внимание на внешних особенностях объ- екта. Полный набор операций, которые объект может осуществ- лять над другим объектом, называется протоколом. Протокол отражает все действия, которым объект может подвергаться сам и которыми может оказывать влияние на другие объекты, определяя тем самым полностью внешнее поведение абстрак- ции со статической и динамической точек зрения. Метод — это функция или процедура, которая выполняет некоторое дейст- вие и может быть вызвана программным обеспечением, исполь- зующим данный объект (клиентом объекта). Методы, составля- ющие каждый из интерфейсов, обычно определенным образом взаимосвязаны. Клиенты могут получить доступ к сервисам объекта СОМ только через вызовы методов интерфейсов объек- та — у них нет непосредственного доступа к данным объекта. Методы интерфейса служат для предоставления определенного сервиса. Объект СОМ реализуется внутри сервера и обычно под- держивает несколько интерфейсов. Сервер может быть либо ди- намически подключаемой библиотекой (DDL), подгружаемой во время работы приложения, либо отдельным самостоятель- ным объектом. Соотношение СОМ и объектно-ориентированного програм- мирования. Объекты — центральная идея СОМ. Объект состоит
из двух элементов: набора данных и групп методов. В отличие от СОМ большинство объектно-ориентированных технологий допускает не более одного интерфейса на объект. Распространенная концепция в объектно-ориентированном программировании — понятие класса. Объекты относятся к определенному классу, являются экземплярами класса (идея типизации в Турбо Паскале). СОМ объекты имеют классы. Класс в СОМ понимается как конкретная реализация набора интерфейсов. Может существовать несколько разных реализа- ций одного и того же набора интерфейсов, каждая из которых будет отдельным классом. Идеи объектно-ориентированного про- граммирования нашли свое отражение в СОМ технологии. Ин- капсуляция. Данные объекта недоступны его клиентам непо- средственно, они инкапсулируются, скрываются от прямого доступа извне. Клиент имеет доступ к данным объекта только через методы интерфейсов этого объекта. Полиморфизм. Воз- можность работы с объектами разных типов, каждый из кото- рых поддерживает данный набор интерфейсов, но реализует их по-разному. Наследование. Идея проста: имея некоторый объ- ект, можно создавать новый, автоматически поддерживающий все или некоторые «способности» старого. Различают наследо- вание реализации и наследование интерфейса. В первом случае объект наследует от своего родителя код. Когда клиент дочер- него объекта вызывает один из унаследованных методов, на са- мом деле выполняется код метода родителя. Таким образом, это механизм повторного использования кода (языки C++, Smal- ltalk). Наследование интерфейса означает повторное использо- вание спецификаций — определение методов, поддерживаемых объектом, облегчает решение задач полиморфизма. Определе- ние нового интерфейса путем наследования от существующего гарантирует, что объект, поддерживающий новый интерфейс, можно рассматривать как объект, который поддерживает ста- рый интерфейс. В чем отличие СОМ от объектно-ориентирован- ных технологий? СОМ действительно объектно-ориентирован- ная технология, однако способ определения и поведения объектов трактуется иначе. Идея, заложенная в СОМ технологии. Повторное использо- вание программных компонент. Так, разработчику аппаратуры значительно легче за счет интенсивного повторного использова- ния существующих компонентсоздавать продукт (изделие). Идею трудно назвать новой. Библиотеки (динамически подключае- мые), объекты дают эту возможность. Библиотеки поставляют- ся в двоичном виде, что позволяет скрыть секреты реализа-
Основные управляющие конструкции 95 ции. Основная сложность — расширение функциональных возможностей, библиотечный подход просто недостаточен. По- вторное использование объектов возможно, однако рынка объек- тов нет, то есть возможно использование только в рамках рабочей группы программистов. Причины: стандартов для компоновки двоичных объектов в единое целое нет. Необходимо распростра- нять объекты вместе с исходным текстом, что вряд ли возмож- но. Повторное использование объектов в различных системах программирования тоже не возможно. Рынок обязан постав- лять объекты, которые могут использоваться разными языка- ми и средами разработки. Но этого нет, объект на C++ затруд- нительно использовать в среде Delphi. Еще одна проблема связана с перекомпоновкой и перекомпиляцией всего приложе- ния при изменении одного объекта. СОМ технология решает эти проблемы, при этом преимущества библиотек и объектов сохраняются и вносится в программирование преимущество технологии всеобщего повторного применения. В WWW уже существуют узлы, полные компонентов, основанных на СОМ. Преимущества СОМ: □ Все преимущества объектно-ориентированного проектиро- вания программ. Разработчик может организовать проект в виде СОМ объектов, а затем определить интерфейсы каждого объекта. □ Общий подход к созданию всех типов программных серви- сов. Находит нужное программное обеспечение в библиоте- ке, в другом процессе, в операционной системе. Сглаживает различия между системным и прикладным программным обеспечением. □ Безразличен язык программирования. СОМ определяет двоичный интерфейс, который должны поддерживать объ- екты. Объекты СОМ можно создавать на любом языке, способном поддерживать данный интерфейс. □ Замена текущей версии программ на новую, с дополните- льными возможностями, не повредив существующим кли- ентам старой версии. Способность COM-объекта поддер- живать более одного интерфейса — ключ к решению этой проблемы. Технологии Active X и OLE. Первая реализация ОБЕ была предназначена для обеспечения механизма создания и работы с составными документами. Элементы, созданные в различных приложениях, например Microsoft Excel и Microsoft Word, ин- тегрируются в рамках единого документа. Составные докумен- ты создаются либо связыванием двух разных документов, либо
96 Часть первая внедрением одного документа в другой. Частный случай. Даль- нейшие версии — разные программные компоненты должны предоставлять друг другу сервисы. По-новому взглянуть на взаимодействие любых типов программ (библиотек, приложе- ний, системного программного обеспечения и др.). Под терми- ном OLE понималось все, что создавалось с использованием па- радигмы СОМ. В 1996 году Microsoft ввела в оборот новый термин ActiveX. Сначала он относился к технологиям, связан- ным с Интернетом, и приложениям, выросшим из него, вроде WWW (World Wide Web). Но так как разработки Microsoft были основаны на СОМ, то ActiveX также была связана с OLE. Все вернулось на круги свои: OLE — означает в настоящее вре- мя только технологию создания составных документов связы- ванием и внедрением, а разнообразные технологии на основе СОМ, ранее объединенные под названием OLE, называют Acti- veX. Технологии ActiveX и OLE — это не что иное, как про- граммное обеспечение, предоставляющее клиентам сервисы через COM-интерфейсы, поддерживаемые СОМ объектами. Различные части ActiveX и OLE определяют стандартные интерфейсы для различных целей. Рынок стандартных компонентов. Такую цель и преследует финансируемая Microsoft программа разработки интерфейсов подобного рода — OLE Industry Solutions (Про- мышленные решения на основе OLE). В рамках этой программы группы финансовых компаний, организаций здравоохранения, поставщиков оборудования для торговых точек и др. определили стандартные интерфейсы компонентов, применяемых в соот- ветствующих областях. Case технологии. (Computer Aided Software Engineering — автоматизация разработки ПО) При проектировании больших программ разработчик начинает с планирования своей работы, рисования некоторых диаграмм, написания каких-то предвари- тельных спецификаций, разработки некоторого макета, позво- ляющего определить, как все составные части будут взаимодей- ствовать между собой, решать поставленную проблему. В 60-х годах этот процесс был формализован с помощью блок-схем. Все, кто в то время занимался программированием, проходили через это. Считали, что эти понятия и инструментальные сред- ства можно было использовать не только для разработки про- грамм, но и для описания всех процессов на предприятии. Диаграммные методы играли ключевую роль в разработке про- грамм и в 80-е годы. С их помощью пытались описать все ста- дии разработки программ. Появились даже специальные рабо- чие станции для автоматизированного процесса разработки
Основные управляющие конструкции 97 диаграмм. Идея полного описания и контроля разработки по- лучила название программной инженерии (software enginee- ring), дисциплины, позволяющей сложную программу строить в предсказуемом стиле и с качеством, которое можно измерить и гарантировать. Была сделана попытка автоматизации собст- венной работы («сапожник без сапога). Эта старая идея жива и в настоящее время, она имеет только другое название — CASE технологии. Первоначально под этим термином понимались средства автоматизации разработки программ. Сейчас он трак- туется как программные средства, поддерживающие процессы создания и сопровождения программ, включая анализ и форму- лировку требований, проектирование, генерацию кода, тести- рование, документирование, обеспечение качества и управле- ния проектом разработки. Отношение к CASE технологиям не однозначное. С одной стороны, считают, что CASE технологии обладают высокими потенциальными возможностями в части увеличения производительности труда, улучшения качества про- граммных продуктов, поддержки унифицированного и согласо- ванного стиля работы. При этом существующие CASE средства, используя методы структурного и объектно-ориентированного анализа и проектирования программ, имеют различные инст- рументарии (например, диаграммы, тексты и т. д.) для описа- ния внешних требований, связей между компонентами про- граммы, динамики работы. Другая точка зрения заключается в том, что CASE технологии не более чем пакеты рисования и конструирования диаграмм. Истина, вероятно, находится посередине. Д. Васкевич при- водит пример из прошлого, связанного с индустрией обработки текстов. В 80-е годы системы обработки текстов были дорого- стоящими, большими и привязанными к конкретным ЭВМ. Эти системы были далеко за пределами возможностей обычных пользователей. Затем появилось следующее поколение для пер- сональных компьютеров, но и они были слишком ограничены и тяжелы в применении. Большинство пользователей не приме- няли их. В настоящее время программы типа Microsoft Word выполняют больше, чем аналогичные разработки 80-х годов, и для миллионов людей использование текстовых процессоров стало таким же привычным, как использование ручки. Исто- рия повторяется. Программ требуется все больше. Программи- стам необходимы инструментальные средства проектирования на базе компьютера. Подведем итог нашего сжатого обзора технологий програм- мирования. Их развитие происходило и происходит по спира- 4-452
98 Часть первая ли, не плоской, а пространственной. Каждый новый виток, «вбирая» все от предыдущего, решает основную проблему тех- нологии (разработка надежного и эффективного программного проекта с минимальными затратами) новым, более совершен- ным инструментарием (принципы декомпозиции и абстрагиро- вания, приемы анализа и синтеза и т. д.). Мера дезорганизации программных проектов (имеется в виду весь их «жизненный цикл») как сложных систем уменьшается. Первый виток — операциональный. Языки программирования: FORTRAN I и II, ALGOL 58, 60, COBOL, LISP и др. Нарабатываемые идеи: под- программа (подпрограммы возникли до 1950 года, но рассмат- ривались не как элемент абстрагирования, а как средство, упрощающее работу); типы данных и их описание, раздельная компиляция, блочная структура, обработка списков, указатели и т. д. Второй виток — структурный (объединяем этим терми- ном и нисходящее проектирование, и модульное). 1966-1985 годы. Языки программирования ПЛ/1, ALGOL 68, Pascal, Si- mula, С, Ada (наследник ALGOL 68, Pascal, Simula), Clos, C++ (возникший в результате слияния С и Simula) и т. д. В 70-е годы созданы тысячи языков и диалектов. Нарабатываемые идеи: подпрограммы как элемент абстрагирования (разработа- ны механизмы: передачи параметров; вложенности подпрог- рамм; локальных и глобальных переменных; теория типов; развитие модулей от группы логически связанных подпрог- рамм до раздельно компилируемых фрагментов со строго опре- деленным интерфейсом). Третий виток — объектно-ориентиро- ванный, с 1986 года. Языки программирования: Smalltalk, Object Pascal, C++. Основным элементом конструирования программы является модуль, составленный из логически связанных объек- тов и т. д. Учебник, если так можно выразиться, посвящен вто- рому витку развития этого сложного предмета — программиро- вания.
Часть вторая Процедуры и функции — элементы структуризации программ Занятие № 10. Одномерные массивы. Работа с элементами План занятия □ раздел описания типов данных: □ простой пример нахождения суммы чисел; □ экспериментальная работа с программами поиска суммы элементов массива, кратных заданному числу; нахожде- ния элементов с определенными свойствами в массиве це- лых чисел; формирование значений элементов массива с помощью генератора случайных чисел; вычисления фак- ториала числа; □ выполнение самостоятельной работы. Описание типов данных. При описании переменных через двоеточие указывается ее тип. До этого занятия использова- лись только два типа данных: Integer и Boolean. Типы данных Integer и Boolean относятся к простым типам (простые, потому что не могут состоять ни из каких других типов). Тип перемен- ной определяет множество значений, которые она может при- нимать, и операции, которые могут быть над ней выполнены. Утверждение, после проделанной работы, понятное. С каждой встречающейся в программе переменной может быть связан то- лько один тип данных. Естественно, перечнем Integer и Boolean не исчерпывается весь список типов Турбо Паскаля. Он не ис- черпывается любым списком, ибо в языке есть возможность для конструирования своих типов данных. Однако, так или иначе, но любой, сверхсложный тип данных, который Вы со- здали в своей программе, сводится (или строится) к простым типам. Описание типов данных имеет следующий вид: Туре <имя_типа>=<тип_данных>;
100 Часть вторая После этого в разделе описаний переменных у Вас появляет- ся возможность ссылаться на введенный тип данных, он ведь имеет имя! Var <имя переменной? : <1*мя_типа>; Примечание Не будем терять время на рассмотрение примеров. Схема использо- вания описания типов прояснится из последующего материала за- нятий. Простой пример. Необходимо найти сумму 5 целых чисел. Решение очевидно. Program Му10__1; Var al,а2,аЗ,а 4,а5,s: Integer; Begin WriteLn (' Введите пять целый чисел'); ReadLn(al,а2,аЗ,а4,а5) ; s:= al+a2+a3+a4+a5; WriteLn('Ии сумма равна ' ,з) ReadLn; End. А если требуется найти сумму 30 целых чисел? Решение по аналогии требует введения 30 однотипных переменных. Одномерный массив — это фиксированное количество эле- ментов одного и того же типа, объединенных одним именем, где каждый элемент имеет свой номер. Обращение к элементам массива осуществляется с помощью указания имени массива и номеров элементов. Нам для работы требуется массив из 30 це- лых чисел. Опишем в разделе типов свой тип — одномерный массив, со- стоящий из 30 целых чисел. Туре МуАггау = Array [1..30] Of Integer; Напомним, что раздел типов начинается со служебного слова Туре, после этого идет имя нового типа и его описание. Между именем типа и его описанием ставится знак «равно» (в разделе переменных между именем переменной и ее описанием ставится двоеточие). В нашем случае МуАггау — имя нового типа данных; Array — служебное слово (в переводе с англий- ского означает «массив», «набор»); [1..30] — в квадратных скобках указывается номер первого элемента, затем, после двух точек, номер последнего элемента массива; Of — служебное слово (в переводе с английского «из» ); Integer — тип элемен- тов массива. И решение, простое решение без 30 переменных.
Процедуры и функции — элементы структуризации программ 101 Program Му10_1т; Const п=30; Type MyArray=Array[1..п] Of Integer; Var A: MyArray; s,i: Integer; Begin WriteLn ('Введите ',n, ' чисел'); For i:=l To n Do ReadLn (A [i ]) ; s:=0; For i: =1 To n Do s:=s+A[i]; WriteLn('Их сумма равна ' ,s); ReadLn; End. Экспериментальный раздел работы 1. Найти сумму элементов массива, кратных заданному числу. Предыдущее решение изменится незначительным образом. Добавляется описание еще одной переменной для хранения значения числа, на кратность которому проверяются значе- ния элементов массива. Появляются операторы WriteLn('Введите число '); ReadLn (к) ; и изменяется оператор из тела цикла If А[1] Моо к = 0 Then s:=s+A[i] ; Примечание Не забудьте изменить значение константы п. Вводить при каждом запуске программы 30 чисел — утомительное занятие. Изменить программу так, чтобы определялось количество положительных и отрицательных элементов в данном массиве. Суть основного изменения программы заключается во введении двух переменных (счетчиков — pos, neg) для хранения значе- ний количества положительных и отрицательных элементов в массиве. Найдите местонахождение в программе следующих строк программного кода: pos, neg:Integer; pos:-0;neg:=0; If A[ i ]>0 Then Inc(pos) Else If A[ 1 ]<0 Then Inc (neg) ; WriteLn (pos: 4 ,neg: 4) ;
102 Часть вторая Данная программа не решает задачу нахождения и количе- ства нулевых элементов в массиве. Проведите ее модификацию и для решения этой задачи. Изменить программу так, чтобы в массив В записывались номера четных элементов массива А. Введение массива В и ра- бота с ним требует введения переменной (у) для обращения к элементам В. Суть решения заключается в просмотре элемен- тов массива А (это мы умеем), выявлении четных элементов и записи их номеров по текущему значению переменной ] в мас- сив В. Примечание Мы записываем не значения элементов, а их номера! К сожалению, отличие между этими понятиями осознается учащимися при пер- вом знакомстве не сразу, даже не за решение одной задачи. Program Му10_2; Const п=10; Type MyArray=Array[1..п] Of Integer; Var А,В: MyArray; i,j: Integer; Begin WriteLn('Введите ',n, ' чисел'); For i:=l To n Do ReadLn (A [ 1 ]) ; J :=0; For 1:=1 To n Do If A[ i ] Mod 2 = 0 Then Begin Inc (j) ;В [ j ] : =i ; End; For i:=l To j Do Write(B[i]:3); WriteLn; ReadLn; End. 2. Научимся определять, есть ли в массиве элемент с определен- ными свойствами, например, есть ли отрицательный элемент. А если таких элементов несколько? Требуется ли определять номера этих элементов в массиве или выводить их значения? Это все разные задачи. Уточним постановку — номер послед- него отрицательного элемента. Напрашивается очевидное решение, его фрагмент: к: =0 ; For i:=l То п Do If A[i]<0 Then k:=i;
Процедуры и функции — элементы структуризации программ 103 If к=0 Then (отрицательны;', элементов в массиве нет> Else (последний отрицательный элемент является к по счету в массиве А>; Это решение плохое. Действительно, если единственный от- рицательный элемент в массиве записан на первом месте, то мы выполняем лишние п-1 сравнение, просматривая массив до конца. Договоримся о том, что наш программный код должен экономно, рационально использовать ресурсы компьютера, в частности и время его работы. Эффективный алгоритм значит в computer science больше, чем сверхбыстродействующий компь- ютер. Следующее изменение If А[1]<0 Then Begin к:=1; Break; End; решает возникший вопрос, но... Это изменение приводит к тому, что наш небольшой фрагмент логики имеет одну точку входа и две точки выхода. Тоже плохо, очень плохо. Обилие Break, GoTo превратит нашу программу в «спагетти», а у нас не 60-е годы, первый виток развития технологий программирования мы оставили истории. И еще одно замечание. В элементарной задаче задействованы две переменные. А если задача не элемен- тарная и мы будем так их «транжирить»? Нам не хватит, об- разно выражаясь, никакого алфавита. Приемлемый вариант решения имеет вид: 1: =п ; While (1>=1) And (А[1]>=0) DoDec(i); If Kl Then (отрицательны:-, элементов нет в массиве> Else (последний отрицательный! элемент имеет номер 1>; Чуть-чуть изменим задачу. Требуется найти значения и но- мера всех отрицательных элементов. В этом случае использова- ние оператора For неизбежно, ибо нам необходимо просмотреть все элементы массива. Итак, не наше желание определяет тип используемого оператора цикла, а решаемая задача! ($R+) Program Му10_3; Const n=-T ; Type MyArray=Array[1..n] Of Integer; Var A:MyArray; i:Integer; Begin
104 Часть вторая WriteLn ('Ввод элементов массива. Не забудьте об отрицательный элементах. '); For i:=l То п Do Read(A[ij); WriteLn{'Вывод отрицательных элементов массива и их номеров (индексов)'); For 1:=2 То г do IfAh]<0 Then WriteLn (А[ 1] , ' ',1,' '); Readin; End. Текст программы поиска номеров всех отрицательных эле- ментов в массиве содержит строки: For i:=l То п do Измените во второй строке п на п+1 и поставьте перед текстом програм- мы директиву компилятора {$R+}. При запуске программы по- явится ошибка периода выполнения: Error 201: Range check er ror. Она является сообщением о том, что значение переменной i (индекс массива А) вышло за допустимый предел, т. е. за значе- ние константы п. Директивы компилятора управляют режимом компиляции. Ди- ректива начинается с символа $ после открывающей скобки комментария, за которым следует имя директивы (состоящее из одной или нескольких букв), определяющее ее назначение. Контроль вхождения в диапазон осуществляется с помощью директивы {$R+} ({$R-}), при этом устанавливается или отме- няется генерация кода контроля вхождения в диапазон. В ре- жиме {SR-1-} все индексы массивов и строк контролируются на выход за границы, а все присваивания значений скалярных пе- ременных и ограниченных типов проверяются на вхождение в диапазон. При выходе за границы выполнение программы пре- кращается и выдается сообщение об ошибке. Примечание При работе с массивами настоятельно рекомендуем использовать эту директиву при отладке программ. 3. Формирование значений элементов массива путем ввода их с клавиатуры — достаточно утомительное занятие. Давайте попробуем использовать для этих целей генератор случайных чисел. Это сложное название мы будем понимать очень про- сто. Есть что-то (черный ящик), и это что-то выдает числа, причем какое число выдается следующим за очередным, нам неизвестно. Этим черным ящиком в Турбо Паскале является функция Random. Она возвращает случайное число.
Процедуры и функции — элементы структуризации программ 105 Функция Random. Описание: Random[(range:Word)]. Напомним, что в квадрат- ных скобках указывается необязательный параметр конструк- ции Турбо Паскаля. Тип результата: Real или Word в зависимости от наличия пара- метра. Комментарий. Если параметр не задан, то результатом являет- ся число типа Real в диапазоне 0<х<1. При наличии параметра возвращается число типа Word в диапазоне 0<x<range. Обрати- те внимание на то, что верхняя граница диапазона не достига- ется — строгое неравенство. Примечание С типом Real у нас еще не было работы, она впереди. ISR-) Program Myl 0_ 4 ; Const п=10; Type MyArray=Array[1..п] Of Integer; Var A:MyArray; i:Integer; Begin (Randomize;) WriteLn ('Формирование значений элементов массива А') ; For i-.=l То п Do A[i] :=Random (21) (-10); WriteLn('Вывод'); For i:=l To n Do Write (A [ 1 ] : 5) ; ReadLn; End. Запустите несколько раз программу. На экране мы видим одну и ту же последовательность чисел в диапазоне от 0 до 20. Уберите фигурные скобки у -10 и снова запустите несколько раз программу. Последовательность чисел на экране одна и та же, но числа из интервала от -10 до 10. Объясните этот факт. Попробуйте получать числа из любого интервала, например от -17 до 25. Продолжим наши эксперименты. Уберите фигурные скобки у процедуры Randomize. Повторите многократный за- пуск программы. Последовательности чисел на экране разные. В чем дело? Наш черный ящик, функция Random, начинает ге- нерировать в первом случае числа (каким-то неизвестным нам образом) от фиксированного начального числа. Во втором слу- чае эти начальные числа меняются от запуска к запуску (про-
106 Часть вторая цедурой Randomize) и последовательности получаются разные. Следующим изменением является включение контроля на вы- ход за допустимый диапазон {$R+}. После запуска мы видим уже знакомую ошибку: Error 201: Range check error. Прочитай- те еще раз внимательно описание функции Random и попробуй- те найти объяснение причины возникновения ошибки. Если объяснение не найдено, то измените текст программы. ($R+1 Program Му10_4т; Const п=10; Var 1:Integer; Begin WriteLn('Вывод 10 случайных чисел в диапазоне от —10 до 10') ; For i:=l То n Do Wri teLn (Random (21)-1 0) ; ReadLn; End. Ошибки после запуска программы нет, но на экране не чис- ла из интервала от -10 до 10, а какая-то странная последовате- льность чисел. Она может быть и такой: 65530, 1, 2, 4, 65527, 2, 65528, 65534, 3, 2. Итак, где же истина? Напомним, что диа- пазон для целых чисел типа Word от 0 до 65535 и число 6553510 (216-1)=11111111111111112 , а это -1 в дополнитель- ном коде. Число 65534 соответствует -2 в дополнительном коде. В первой версии программы результат функции Random имеет тип Word. Из этого числа вычитается целое число типа Integer. Типы разные, выполняется преобразование типов по правилу, описанному в занятии 2, т. е. к типу Longlnt. А эле- мент массива имеет тип Integer — противоречие. Измените тип элементов массива А на Longlnt и сравните результаты работы программ. Во второй версии контроль на выход за диапазон не выполняется, может быть, потому что нет операции присвое- ния (контролировать нечего и некого)? Верните тип Integer в описании элементов массива и измените одну строку програм- мы на For г : =1 То n Do А [ 1 ] : =Integer (Random (21)) -10; Запустите программу. Она прекрасно работает. Объясните, что происходит в этом случае. 4. Научимся вычислять факториал натурального числа N. Фак- ториал числа — это произведение чисел 1*2*3*...*(N-1 )*N (обозначается как N!). Сложность задачи в том, что уже 8!=40320, а 131=6227020800. Типы данных Integer, Longlnt
Процедуры и функции — элементы структуризации программ 107 применимы весьма в ограниченном диапазоне натуральных чисел. Для представления факториала договоримся исполь- зовать массив. Пример. [ А[бГ~Г А[7] [~А[8] ~I [ 9 | 9 ! В массиве записано значение 111=39916800. Каким образом? В А[0] фиксируется число занятых элементов массива, в А[1] — цифра единиц результата, в А[2] — цифра десятков результата, в А[3] — цифра сотен результата и т. д. Почему так, а не наоборот? Такая запись позволяет исключить сдвиг элементов массива при переносе значений в старший разряд. А сейчас наберите, как обычно, текст программы, выполните ком- пиляцию и, открыв окно Watch, выполните ее в пошаговом ре- жиме, отслеживая изменение значений переменных при не очень большом значении N. Добейтесь полного понимания ло- гики работы программы. {$R+1 Program Myl0_5 ; Uses Crt; Const Max.N=300; Type MyArray=Array[0..MaxN] Of Integer; Var A:MyArray; Var i,g,r,w,N:Integer; Begin CIrScr; FillChar (A, SizeOf (A) ,0) ; WriteLn('Введите число, факториал которого необходимо подсчитать.'); ReadLn (N) ; А[0]:=1;А[1]:=1;j:=2;{^Начальные присвоения, начинаем вычислять 2’.*} While (J<—N) And (A[0]<MaxN) Do Begin (* Второе условие фиксирует факт выхода за пределы массива. *) г:=0;i : =1; { *г - перенос из разряда в разряд при выполнении умножения числа j на очередную цифру A[i] предыдущего результата.*) While (1<=A[OJ) Or (r<>0)Do Begin(*Пока не «прошли» все цифры предыдущего результата или есть перенос цифры в старший разряд. *)
108 Часть вторая w:=A[i] *j + r; I *Ушюжаем очередную цифру и прибавляем цифру переноса из предыдущего разряда. *) A[i]:=w Mod 10;{*Науодим новую цифру с номером 1. *} r:= vi Div 10; I * Вычисляем значение переноса . *) If А[А[0]+1]о0 Then Inc(A[0]) ; I *Иэменяем, если необходимо, количество задействованных элементов массива А, длину полученного результата. *} Inc(i) ; End; Inc(j) ; End; For 1:=A[0] DownTo 1 Do Write(A[1]);{*Вывод результата. *} WriteLn; ReadLn; End. Найдите число N, при котором 300 элементного массива А для хранения результата окажется недостаточным. Измените программу так, чтобы выводились факториалы всех чисел в интервале от 1 до N. Измените программу так, чтобы вычислялись степени чисел aN, например З100. Задания для самостоятельной работы 1. Дан массив целых чисел. Найти: □ сумму элементов массива, больших данного числа А (А вводить с клавиатуры); □ сумму элементов массива, принадлежащих промежутку от А до В (А и В вводить с клавиатуры); □ максимальный элемент массива и его номер, при усло- вии, что все элементы различные; □ номера всех элементов массива с максимальным значе- нием. □ минимальный элемент массива; □ сумму элементов массива с fe7-ro по кг-й, где и k2 вво- дятся с клавиатуры; □ количество нечетных элементов массива; □ количество отрицательных элементов массива; □ сумму первых пяти элементов массива;
Процедуры и функции — элементы структуризации программ 109 □ все элементы, кратные 3 или 5. Сколько их; □ сумму всех четных элементов массива, стоящих на чет- ных местах, то есть имеющих четные номера; □ сумму всех четных элементов массива (или сумму эле- ментов, кратных заданному числу); □ сумму положительных элементов массива; □ сумму элементов, имеющих нечетное значение: □ сумму элементов, имеющих нечетные индексы; □ сумму положительных элементов, значения которых ме- ньше 10; □ удвоенную сумму положительных элементов; □ сумму отрицательных элементов; □ индексы тех элементов, значения которых больше задан- ного числа А; □ количество элементов массива, значения которых боль- ше заданного числа А и кратны 5; □ индексы тех элементов, значения которых кратны 3 и 5; □ индексы тех элементов, значения которых больше значе- ния предыдущего элемента (начиная со второго); □ количество тех элементов, значения которых положите- льны и не превосходят заданного числа А. 2. Определить: □ сколько элементов массива превосходят по модулю за- данное число А; □ есть ли в данном массиве два соседних положительных элемента? Найти номера первой (последней) пары; □ есть ли в данном массиве элемент, равный заданному числу? Если есть, то вывести номер одного из них; □ есть ли в данном массиве положительные элементы, крат- ные k <k вводить с клавиатуры); □ номер первого отрицательного элемента, делящегося на 5 с остатком 2; □ пару соседних элементов с суммой, равной заданному числу; П есть ли 2 пары соседних элементов с одинаковыми знаками; □ номер последней пары соседних элементов с разными знаками. 3. Измените программу вычисления факториал числа N так, что- бы вычислялись; 1*3*5* ... * (2*N-1), 2*4*6*8* ... * (2*N). 4. Измените программу вычисления степени числа так, чтобы она могла вычислять степени отрицательных целых чисел. 5. Измените программу вычисления степени числа так, чтобы в степень возводились рациональные числа.
Часть вторая Материал для чтения 1. Одномерный массив — это конечное упорядоченное мно- жество элементов. За первым элементом идет второй, за вто- рым — третий и т. д. В силу этого элементы массива можно пере- нумеровать целыми числами — индексы элементов. Для доступа к элементу массива на логическом уровне достаточно знать имя массива и индекс элемента. Память компьютера представляет собой последовательность ячеек, имеющих адреса, с возможно- стью обращения к ячейкам по их номерам. Для массива обычно выделяется сплошной участок памяти компьютера. Как из ло- гического адреса элемента массива вычисляется физический адрес? С этой целью массив имеет, если так можно выразиться, описатель (дескриптор). Приведем его структуру, например, для массива Var A: Array[1..10] Of Integer. 1 Имя I А Адрес начального элемента | Определяется системой, обозначим — Addr(A[1]} Индекс начального элемента | 1 Индекс последнего элемента | ю |i Тип элемента | Integer i Il Длина элемента I 2 байта j Вычисление адреса элемента I выполняется по формуле: Addr(A[i])=Addr(A[l ])+(i-l )*2. 2. Общие сведения по организации ЭВМ. Структура простой ЭВМ показана на рисунке. Центральный процессор Устройства ввода- вывода
Процедуры и функции — элементы структуризации программ 111 «Мозгом» вычислительной машины является центральный процессор. В его функцию входит выполнение программ, находя- щихся в основной памяти, путем выборки, проверки и последова- тельного выполнения составляющих их команд. Центральный процессор состоит из нескольких частей. Устройство управления осуществляет выборку команд из основной памяти и определе- ние их типа. Арчфметическо-логическое устройство осуществ- ляет выполнение таких операций, как сложение, сдвиг и т. д. Центральный процессор содержит высокоскоростную память для запоминания промежуточных результатов и управляющей информации. Эта память состоит из регистров, каждый из ко- торых имеет определенное назначение. Одним из регистров яв- ляется счетчик команд, указывающий адрес команды, которую необходимо выполнить следующей. Регистр команд содержит текущую выполняемую команду. Центральный процессор вы- полняет каждую команду в виде последовательности простых операций: □ выбор очередной команды из основной (оперативной) па- мяти в регистр команд; □ изменение в счетчике команд, чтобы он указывал на адрес команды, следующей за выбранной; □ определение типа выбранной команды; □ проверка, требуются ли для выполнения выбранной коман- ды какие-либо данные, и если они нужны, то определение их места нахождения в памяти; □ загрузка во внутренние регистры центрального процессо- ра требуемых данных из памяти; □ выполнение команды; □ запоминание результатов выполнения команды в задан- ных ячейках памяти; □ переход к первому действию для выполнения следующей команды. Такое описание функционирования центрального процессо- ра по существу представляет программу, написанную на ка- ком-либо языке программирования. Память — это часть ЭВМ, где хранятся программы и дан- ные. Память состоит из ряда ячеек (наименьшая адресуемая единица памяти), каждая из которых может хранить данные. Каждой ячейке соответствует число, называемое ее адресом, при помощи которого программа может к ней обращаться. Если в памяти N ячеек, то они имеют адреса от 0 до N-1. Все ячейки памяти содержат одинаковое число битов. Если в ячей- ке k битов, то она может содержать любую из 2к их комбина-
112 Часть вторая ций. Центральный процессор связан с основной памятью при помощи, как минимум, двух регистров — регистра адреса па- мяти и буферного регистра. Для того, чтобы считать из памяти содержимое ячейки, центральный процессор загружает адрес этой ячейки в регистр адреса памяти и посылает памяти сигнал чтения. Память начинает обработку и после некоторого време- ни помещает содержимое запрашиваемой ячейки в буферный регистр, где оно доступно центральному процессору для после- дующей обработки. Для того, чтобы записать данное в память, центральный процессор записывает адрес ячейки памяти в ре- гистр адреса памяти и записываемое данное в буферный ре- гистр, а затем сигнализирует памяти о начале операции запи- си. Выше по тексту сказано, что на рисунке приведена структу- ра простой ЭВМ. В материале для чтения занятия 3 мы рас- сматривали понятие структуры. Структура ЭВМ определяется внутримашинным системным интерфейсом. Говоря простым язы- ком, это то, как связаны составные части. Логически возмож- ны два типа связей: каждая составная часть связана с другой отдельными «проводами»; все составные части связаны друг с другом через общую систему проводов (системная шина). Ка- кое-то количество проводов, объединенных (по назначению) об- щей целью, называют шиной. Все остальные «конфигурации» являются комбинациями этих типов. Исходя, например, из опи- сания принципов обмена данными между центральным процессо- ром и памятью, системная шина должна состоять из шины дан- ных, шины адреса, шины инструкции и, так как по проводам «бегают электрончики» — шины питания. При передаче адреса и данных логически возможны несколько вариантов: переда- вать их по различным проводам или передавать по одним и тем же проводам, но в разные моменты времени. Выбор того или иного принципа связей определяет структуру ЭВМ. Однако ограничимся этим. Детализация структуры ЭВМ не входит в круг наших задач, это предмет отдельных курсов, например «Архитектура ЭВМ», «Микроэлектроника». 3. Принципы Джона фон Неймана. Несмотря на большие разнообразия существующих в настоящее время ЭВМ, в основы их заложены некоторые общие принципы. Эти принципы были сформулированы в 40-х годах двадцатого столетия выдающим- ся американским математиком Джоном фон Нейманом и позво- лили создать устройство обработки информации с достаточно простыми и универсальными принципами работы. Это устрой- ство называется ЭВМ. Первый принцип — принцип произволь-
Процедуры и функции — элементы структуризации программ 113 ного доступа к основной (оперативной) памяти. Структурно основная память, как мы говорили, состоит из ячеек. Принцип гласит о том, что процессору в произвольный момент времени доступна любая ячейка, причем время доступа (чтения или за- писи) одинаково для всех ячеек. Чтобы обеспечить такой до- ступ, ячейки памяти обязаны иметь свои уникальные имена. Этими именами являются номера ячеек. Для лучшего понима- ния действия этого принципа можно сравнить этот доступ с до- ступом к данным на магнитной ленте (магнитофон). Данные, записанные на магнитной ленте, также допустимо разбить на элементарные единицы — слова. При произвольном положе- нии магнитной ленты доступно лишь то слово, которое нахо- дится под головками чтения — записи. Для чтения слова на другом участке магнитной ленты необходимо предварительно переместить этот участок под блок головок, т. е. доступ к этому слову возможен лишь после просмотра предыдущих участков магнитной ленты. Поэтому магнитную ленту принято называть памятью с последовательным доступом. Второй фундаментальный принцип Джона фон Неймана — принцип хранимой программы. Программа решения задачи хранится в оперативной памяти наряду с обрабатываемыми данными. В принципе ЭВМ не различает, что именно хранится в данной ячейке памяти — число, символ или команда. Для ре- шения другой задачи требуется смена в оперативной памяти программы и обрабатываемых данных. Итак, ЭВМ — универса- льный инструмент обработки информации. 4. Возникновение информатики как науки связано с появле- нием ЭВМ и относится к 60-м годам двадцатого столетия. В са- мом общем смысле под информатикой понимают фундамента- льную естественную науку, изучающую процессы передачи, накопления и обработки информации с помощью ЭВМ. Термин «Informatique» введен во Франции на рубеже 60- 70-х годов. Однако еще раньше в США был введен в употребле- ние термин «Computer Science» для обозначения науки о преоб- разовании информации с помощью ЭВМ. В настоящее время термины употребляются в эквивалентном смысле. Содержание ядра информатики, на наш взгляд, определяют программные и технические средства. На этом занятии рассмотрено понятие массива, а в материа- ле для чтения дан обзор структуры простой ЭВМ и принципов Джона фон Неймана. Понятие массива носит фундаменталь- ный характер. Эта структура данных наиболее соответствует структуре ЭВМ. Образно выражаясь, оперативная память ЭВМ —
114 Часть вторая это не что иное, как огромный одномерный массив. Элементы массива имеют номера, ячейки памяти имеют номера; массив имеет одно общее имя, оперативная память имеет одно общее имя (ПАМЯТЬ). Не из ничего возникло понятие массива, а из потребности практики для компактного обозначения больших объемов данных, которые необходимо хранить при решении за- дач в памяти компьютера. И, чтобы задействовать эту память, в нее, естественно, необходимо записать данные, и с этими дан- ными должны быть очень компактные и удобные инструменты для работы. Этими инструментами являются циклические кон- струкции и структура данных типа массива.
Процедуры и функции — элементы структуризации программ 115 Занятие № 11. Процедуры План занятия □ структура программы; □ о взаимодействии программы и процедуры; □ стандартные определения; □ экспериментальная работа с программами: перестановки значений переменных а, Ъ, с в порядке возрастания, вы- числения выражения y=an+xn+an_1’'xn+...+a2*x2+a1,,’x1+a0; формирования значений элементов одномерного массива с помощью датчика случайных чисел; поиска элементов, принадлежащих двум массивам одновременно; переста- новки частей массива; □ выполнение самостоятельной работы. Структура программы. Программы на языке Турбо Пас- каль состоят из заголовка программы, раздела описаний и тела программы. Раздел описаний может включать, если так можно выразиться, следующие подразделы: меток, констант, типов, переменных, процедур и функций. Последовательность подраз- делов в структуре программы произвольная, но естественно, что если вводится переменная нового типа, заданного в Туре, то подраздел Туре предшествует подразделу Var. Принцип нашего языка программирования «то, что используется, должно быть описано» сохраняется и для раздела описаний. Program <имя программы» ; Label <метки>; Const <описание констант»; Туре <описание типов данных»; Var <описание переменных»; <процедуры и функции»; Begin (основное тело программы»; End. До этого момента времени мы использовали из раздела опи- саний только описание переменных и типов. На этом занятии мы начнем изучать процедуры. Структура процедуры повторя- ет структуру программы. Отличия выделены «полужирным» шрифтом. Procedure <имя процедуры» (<параметры>); Label <метки>; Const <описание констант»; Туре <описание типов данных»;
Часть вторая Var Сописание переменных:»; <процедуры и функции>; Begin Сосновное тело процедуры:»; End; Какие вопросы возникают при знакомстве со структурами программы и процедуры? Их много. Начнем со взаимодействия программы и процедуры как по управлению, так и по данным. Примечание В наших решениях задач на протяжении всей книги описание ме- ток мы постараемся не использовать. О взаимодействии программы, и процедуры. Чтобы объяс- нение было наглядным, обратимся к примеру, приведенному на рисунке. Слева приведен фрагмент текста основной програм- мы, справа — процедура вычисления суммы двух целых чисел. Что мы видим? Процедура вызывается указанием ее имени, ко- торое записывается после зарезервированного слова Procedure, используемого для обозначения процедуры. Begin ^Procedure Sum (»,у: Integer ;Var z:Integer); . . . / Begin ReadLn (a ,b) ; / z:=x+y Sum(a,b,c);' ^^End; WriteLn(c) End Линейный ход выполнения основной программы становится нелинейным — управление вычислительным процессом пере- дается на участок программного кода, занимаемый процеду- рой. После выполнения процедуры осуществляется возврат на оператор основной программы, следующий за вызовом проце- дуры (End; в процедуре очень содержательный оператор, сверх- содержательный — это «стрелка», возврат управления в логи- ку, из которой вызывается процедура). Мы не рассматриваем вопросы о том, как это выполняется. Например, как осуществ- ляется возврат на оператор WriteLn(c). Наше объяснение идет на уровне констатации фактов, и только. Итак, взаимодействие программы и процедуры по управлению мы обозначили. Перей- дем к взаимодействию по данных. В программе определены пе- ременные а, Ь, с. В процедуре — х, у, г её параметры, но они яв- ляются переменными процедуры. Причем х, у описаны без идентификатора Var, аг — с идентификатором Var. Поясним разницу очередным рисунком.
Процедуры и функции — элементы структуризации программ 117 При вызове процедуры Sum(a,b,c) из основной программы значение переменной а присваивается переменной х процедуры Sum, а значение переменной Ъ становится значением перемен- ной у, и все. Стрелочка на рисунке в одну сторону. Чуть слож- нее с переменными сиг. Стрелочка на рисунке и в ту, и в дру- гую стороны. Образно выражаясь, процедура Sum знает о том, где в памяти компьютера находится переменная с, и она знает, что при изменении значения переменной г необходимо произ- вести соответствующее изменение значения переменной с. Это обратная связь по данным от процедуры к основной программе. На такой тип связи указывает идентификатор Var в описании параметров процедуры. Перейдем к ряду стандартных определений. В любой про- грамме все переменные делятся на глобальные и локальные. В нашем фрагменте программы переменные а, Ь, с — глобальные, а х, у, г — локальные. Глобальные переменные — это перемен- ные из раздела описаний основной части программы, а локаль- ные — из раздела описаний процедур и функций. Если разница только в месте описания переменных, то «стоит ли шкурка вы- делки»? Добавим ещё одно предложение. Локальные перемен- ные существуют только в течение времени работы процедуры, определяются (создаются) при её вызове и исчезают после за- вершении работы процедуры (после выполнения «сверхсодер- жательного» оператора End;). Таким образом, если бы в основ- ной программе было три фрагмента с вызовом процедуры Sum (желательно с различными параметрами), то для программного кода процедуры Sum три раза выделялось место в оперативной памяти, соответственно, и для переменных х, у, г и три раза
118 Часть вторая освобождалось. Если такое объяснение облегчает понимание разницы между глобальными и локальными переменными, то считаем его правильным. Параметры. При описании процедуры указывается список формальных параметров. Каждый параметр является локаль- ным по отношению к описываемой процедуре, к нему можно обращаться только в пределах данной процедуры (в нашем при- мере х, у, z — формальные параметры). Фактические парамет ры — это параметры, которые передаются процедуре при обра- щении к ней (а, Ъ, с — фактические параметры). Число и тип формальных и фактических параметров должны совпадать с точностью до их следования. Параметры-значения. Другими словами, передача парамет- ров по значению. Копия фактического параметра становится значением соответствующего формального параметра. Внутри процедуры можно производить любые действия с данным фор- мальным параметром (допустимые для его типа), но эти изме- нения никак не отражаются на значении фактического пара- метра, то есть каким он был до вызова процедуры, то таким же и останется после завершения ее работы (х, у — парамет- ры-значения). Параметры-переменные. Другими словами, передача пара- метров по ссылке. Это те формальные параметры, перед кото- рыми стоит идентификатор Var. Передается адрес фактическо- го параметра (обязательно переменной), после этого формальный параметр становится его синонимом. Любые операции с форма- льным параметром выполняются непосредственно над фактиче- ским параметром. Договоримся о том, как мы будем стараться использовать процедуры, может быть, в ущерб эффективности программ, на- пример, с точки зрения количества переменных и т. д. Каждая процедура должна иметь одну точку входа и одну точку выхо- да, использование глобальных переменных в процедуре должно быть минимально, взаимодействие вызывающей логики с про- цедурой должно осуществляться (по возможности) только через параметры. Зачем нам эти оговорки? Мы осваиваем структур- ную технологию разработки программ, при этом на каждом этапе текущая задача разбивается на ряд подзадач, определяя тем самым некоторое количество отдельных подпрограмм (под программа — это повторяющаяся группа операторов, оформ- ленная в виде самостоятельной программной единицы). При этом мы стараемся структурировать задачу не только по управ-
Процедуры и функции — элементы структуризации программ 119 лению, но и по данным, используя при этом весьма ограничен- ный набор инструментов (параметры-значения, параметры-ссыл- ки). Концепция процедур и функций — один из механизмов второго витка развития технологий программирования, а имен- но, структурного проектирования. Экспериментальный раздел работы 1. Составить программу перестановки значений переменных а, Ь, с в порядке возрастания, т. е. так, чтобы а<Ь<с. Текст ре- шения простой. Program Му11_1; Var а , Ь, с:Integer; Procedure Swap (Var x,у:Integer); Var t:Integer; Begin t:=x; '.:=y;y: = t; End; Begin WriteLn('Введите три числа ') ; ReadLn (a,b, c) ; If a>b Then Swap (a,b) ; If b>c Then Swap (b, c) ; If a>c Then Swap (a, c); Wri teLn (a, ' ' ,b, ' ' ,c) ; ReadLn; End. Найдите ошибку в решении. Исправлению подлежит имя одной переменной в одной строке программы. Составьте для по- иска ошибки полную систему тестов. Измените программу так, чтобы аналогичная задача решалась для четырех вводимых чи- сел. 2. Составить программу вычисления выражения у=ап*хп+ап_1 * *хп+ ...+а2*х2+а1*х1+а0, где все данные — целые числа. Ко- эффициенты ап, ап_р ..., ар а0 являются первыми членами арифметической прогрессии, определяемой первым элемен- том (q) и разностью между соседними элементами (d). Зна- чения q, d, п, х вводятся с клавиатуры. Например, q=2, d=3, п=5 и х=4. Нам необходимо вычислить выражение 2*45+5* *44+8*43+11*42+14*41+17*4°.
120 Часть вторая Program Myll_2: Var q,d,n,x,t,w,i: Integer; s: Integer; Procedure Degree(x,у: Integer; Var st: Integer); Var 1:Integer; Begun st:=l; For i:=l To у Do st:=st*x; End; Begin WriteLn('Введите исходные данные — четыре не очень больших числа ') ; ReadLn(q,d,n,x); s:=0;t:=n; For i:=l To n-tl Do Begin Degree (x, t, w) ; s : =s + g4'w; Dec(t);Inc(q,d); End; WriteLn('Результат ’ ,s); ReadLn; End. Использование процедуры для решения этой задачи неско- лько искусственно, но простит нас читатель. Наша цель — от- работать механизм использования процедур, а для этого соста- вьте таблицу изменения значений переменных q, t, u> при работе программы. Проверьте правильность заполнения таблицы, ис- пользуя отладчик и режим пошагового исполнения программы. Зафиксируйте значения q, d, х и найдите экспериментальным путем (или модифицируя программу) то значение п, при кото- ром диапазона типа Integer не должно «хватать» для хранения результата. Примечание Переменные с именем х в основной программе и в процедуре De- gree — это разные переменные! Запишем наш пример по-другому: 17+4*(14+4*(11+4*(8+ +4*(5+4*2)))). Результат вычислений, естественно, тот же са- мый. В общем виде запись выглядит следующим образом: а0 + +х*(а1 +х* (а2 +х*(... +х* (an_1 +x*aJ1)))). Запись полиномов, чи- сел, разложенных по степеням основания системы счисления, называется схемой Горнера. Измените программу так, чтобы вычисление выражения осуществлялось по схеме Горнера. Уч-
Процедуры и функции — элементы структуризации программ 121 тите, что в этом случае Вам до первой итерации необходимо знать последний член арифметической прогрессии. А что означает запись 42*5+4*8+4* 11 +4* 14 +4* 17 +? Можно ли вычислить выражение? Оказывается, да. Берём два числа 4 и 2 и выполняем с ними операцию, записанную по- сле них, т.е. умножение. Сохраняем результат и продолжаем вычисление. Берём 8 и 5 и выполняем сложение. Потренируй- тесь. Напишите несколько примеров и преобразуйте их запись к такому виду. Она называется обратной польской записью, в честь математика Я. Лукашевича. Для общего случая в нашей задаче обратная польская запись имеет вид: an х * an_j + ... а2 х * aj+x* а0 +-. У Вас есть процедура Part, выполняющая одно арифметической действие (умножение или сложение) над дву- мя числами. Напишите программу для решения нашей задачи с ее использованием. Procedure Part(x,y:Integer;t:Boolean;Var z: In teger) ; Begin If t Then z:='<+yElse z:=x*y; End; У нас есть три реализации одной и той же задачи. Сравните количество операций умножения и сложения, необходимых для получения результата в каждой из них. Какой вывод дол- жны мы сделать? 3. Задание значений элементам одномерного массива (с помо- щью генератора случайных чисел) и вывод результата на эк- ран мы рассмотрели на предыдущем занятии. Оформим эти действия как процедуры. Program Му11_3; Const n=8;l=-10;h=21; Type MyArray=Array[1..п] Of Integer; Var A:MyArray; Procedure Init (t,v,w:Integer;Var X:MyArray) ; Var i:Integer; Begin Randomize; For i:=l To t Do X [ i ] :=v+ Integer (Random (w)) ; End; Procedure Print (t: Integer ;X: MyArray) ; Var i:Integer;
Часть вторая Begin For i:=l To t Do Write(X[i]:5);I*Да простят нас читатели за использование числа 5 (5 позиции на экране для вывода X[i]) в этой процедуре, мы еше только учимся. *} WriteLn; End; Begin WriteLn('Формирование значений элементов массива А'); Init(n,1,L,А);{*Значения элементов массива А формируются из интервала целн< чисел от 1 до h. Количество элементов п. *) WriteLn('Вывод'); Print(n,А);f*п первые элементов массива А выводятся на экран (в данном случае) *< End. Казалось бы, что мы только проиграли. Было несколько строк программного кода, а сейчас ... Пожертвуем кажущейся простотой предыдущей версии программы. С этого момента бу- дем стараться создавать программы так, чтобы основная про- грамма состояла из вызовов процедур и функций. Нарисуйте схему (по подобию тех, что приведены в начале занятия), отра- жающую взаимосвязь основной программы и процедур как по управлению, так и по данным. Изменим заголовок процедуры Print на Procedure Printfn: Integer:X:Array[l..n] Of Integer) и запустим программу. Резу- льтат не заставит себя ждать. Это ошибка Error 54: OF expec- ted. Курсор находится на квадратной скобке, т.е. после слова Array ожидается Of. «Пойдем на поводке» у системы програм- мирования, изменим на Procedure Print(n:Integer:X:Array Of Integer). Результат чуть-чуть изменился, уже знакомая ошиб- ка — Error 201: Range check error. Отключим контроль выхода за пределы диапазона — {$R~}. Программа заработала, точнее, она выдает результат. Итак, сплошные загадки. Попробуйте дать им разумное объяснение. 4. Даны два одномерных массива. Найти элементы, принадле- жащие и тому и другому массивам. Наша технология напи- сания программ (использование процедур и функций) начина- ет приносить дивиденды. Процедуру Init при решении задачи требуется использовать два раза с различными параметрами, процедуру Print — три раза. Сделаем «костяк» программы. Процедуры Init и Print, естественно, не приводятся, они у нас
Процедуры и функции — элементы структуризации программ 123 почти универсальны и берутся из предыдущего задания. Про- грамма (ее «костяк») должна компилироваться. Сохраните ее. Набирать весь текст программы, а затем приступать к отлад- ке — это «дурной» тон в программировании, очень «дурной». Возьмите за правило и всегда его придерживайтесь — «в лю- бой момент времени у Вас должна быть компилируемая про- грамма, сохраненная на внешнем носителе». Кроме того, текст любой процедуры и, естественно, основной программы дол- жен помещаться на экране и, конечно, быть читаемым. ($R + ) Program Myll_4; Const п=10;la=~10;ha=21; т=8; lb=-15;hb=31; Type MyArray=Array[1п] Of Integer; Var А,В,С:МуArray; к:Integer; Procedure Solve(qx,qy:Integer;Var qz:Integer; X,Y:MyArray;Var Z: MyArray); Begin End; Begin Init (n, la, ba, A) ; WriteLn('Вывод значений элементов массива А') ; Print (п,А) ; Init (т, lb, hb, В) ; WriteLn('Вывод значений элементов массива В'); Print (т,В) ; Solve (п,т,к,А,В, С) ; WriteLn ('Вывод тех целых чисел, которые есть и в А, ив В') ; Print (к, С) ; ReadLn; End. Начнем уточнять наше решение, а именно, процедуру Solve. Procedure Solve(qx,qy:Integer; Var qz:Integer; X, Y:MyArray;Var Z: MyArray); Var i,j:Integer; Begin qz:=0; For i:=1 To qx Do For j:=1 To qy Do If X[i]=Y[j] Then Begin Inc (qz) ; Z [qz ] : =X [i ] ; (J :=qy ;)End; End;
124 Часть вторая После запуска программы окажется, что при некоторых ис- ходных данных результат неправильный. Например, в первом массиве 10 присутствует один раз, во втором — пять раз. Со- гласно формулировке задачи в ответе 10 должна присутство- вать один раз, а она выводится пять раз. Уберем фигурные скобки у оператора j:=qy. Мы «насильно» изменяем управляю- щую переменную цикла For j:=l ... Выводится правильный ре- зультат. Однако этот прием «насильственного», т. е. не самим оператором For, изменения управляющей переменной считаем плохим программированием. Procedure Solve(qx,qy:Integer;Var qz:Integer; X,Y:МуАггау; Var Z: МуАггау); Var i,j:Integer; Begin qz:=0; For i:-l To qx Do Begin While (J<=qi) And (X [i] <>Y [J J) Do Inc(j); If J<=qy Then Begin Inc (qz) ; Z [qz ) : =X [ i) ;End; End; End; Продолжите эксперименты с программой. Определите слу- чаи ее неработоспособности. 5. Дан массив А из п элементов и число т (1<т<п). Не исполь- зуя дополнительных массивов, переставить первые ш элемен- тов в конец массива, а элементы с т+1 по п — в начало, со- храняя порядок элементов. Идея решения. Переворачиваем первые т элементов, переворачиваем элементы, начиная с т+1. Переворачиваем все элементы массива. Пример. п=10, т=6., массив А: 5 1 -4 3 7 2-1 9 8 6. 273-415-1986 первое переворачивание т элементов. 273 -4 15689-1 второе переворачивание элементов с т+1 по п. -198651-4372 третье переворачивание элементов с 1 по п. В тексте решения не приводится реализация процедур Init и Print. Для процедуры Rev рекомендуется выполнить «ручную» трассировку при различных значениях t и I. !$R+) Program Myll_5;
Процедуры и функции — элементы структуризации программ 125 Const п=10;т=6; Type MyArray=Array[1..п] Of Integer; Var A:MyArray; Procedure Init(Var X:MyArray); Procedure Print (X:MyArray); Procedure Swap(Var a, b:Integer);{*Из первого задания. *) Procedure Rev(t,1:Integer;Var X:MyArray); Var i:Integer; Begin For i:=t To t+(l-t) Div 2 Do Swap (X[i],X[l-i + t]) ; End; Begin Init t A); Print (A) ; Rev(l,m,A); Rev(m+1,n,A); Rev(l,n,A); Print (A) ; ReadLn; End. Задания для самостоятельной работы 1. Даны два различных выражения типа: у=ап‘"х’1тап ] *хп+...+ +а2*х2+а1*х1+а0, z=bnJrxn+bn_1*xn+...+b2’<x2+b1*x1+b0, где все данные — целые числа. Коэффициенты выражений хранят- ся в массивах А и В. При заданном значении х найти макси- мальное значение (y,z). 2. Даны два одномерных массива из целых чисел. Найти эле- менты, которые есть в первом массиве и которых нет во вто- ром массиве. 3. Даны два одномерных массива из целых чисел. Найти эле- менты, которых нет одновременно и в том и в другом масси- вах. 4. Решить задачу 4 из раздела экспериментальной работы для трех массивов. 5. Даны три одномерных массива из целых чисел. Найти эле- менты, которые есть в первом массиве и которых нет во вто- ром и третьем массивах. 6. Решить задачу 3 для трех одномерных массивов. 7. Даны два одномерных массива из целых чисел, разной раз- мерности. Найти среднее арифметическое элементов каждо- го массива и их сумму. Решить задачу для трех массивов.
126 Часть вторая 8. Даны три одномерных массива из целых чисел одинаковой размерности. Сформировать четвертый массив, каждый эле- мент которого равен максимальному из соответствующих эле- ментов первых трех массивов. 9. Даны два одномерных массива (А, В) одинаковой размерно- сти п и число ш. Поменять местами первые элементы масси- вов и выполнить перестановку элементов массивов в обратном порядке (задание № 5 раздела экспериментальной работы). 10. Дано четное число п; проверить для этого числа гипотезу Кри- стиана Гольдбаха (1742 год). Эта гипотеза (по сегодняшний день не опровергнутая и полностью не доказанная) заключа- ется в том, что каждое четное п, большее двух, представля- ется в виде суммы двух простых чисел. Ограничим диапазон проверяемых чисел интервалом 999>п>2. Оформить проце- дурой логику распознания простых чисел. Примеры: 6=3+3, 12=5+7, 30=7+23, 308=31+277, 992=73+919. Исследовать ги- потезу К. Гольдбаха для больших значений п. 11. Известны следующие признаки делимости числа п: □ для делимости на 2 необходимо, чтобы последняя цифра числа делилась на 2; □ для делимости на 3 требуется, чтобы сумма цифр числа делилась на 3; □ для делимости на 4 необходимо, чтобы число из послед- них двух цифр делилось на 4; □ для делимости на 5 необходимо, чтобы последняя цифра числа была 0 или 5; □ для делимости на 8 необходимо, чтобы число из 4 по- следних цифр делилось на 8; □ для делимости на 9 необходимо, чтобы сумма цифр числа делилась на 9; □ для делимости на 11 необходимо, чтобы разность между суммой цифр, стоящих на четных местах, и суммой цифр, стоящих на нечетных местах, делилась на 11. Написать процедуры проверки признаков делимости. Прове- рить их для различных значений п. Материал для чтения 1. Некоторые факты из элементарной теории вероятно- стей. Всем нам достаточно часто приходится сталкиваться с ситуациями, когда, зная возможные исходы некоторого собы- тия (опыта), мы не можем точно предсказать его результат. На- пример, при бросании монеты — какой стороной она упадет
Процедуры и функции — элементы структуризации программ вверх; при бросании игральной кости — какая из шести сторон окажется вверху; при вытаскивании карты из игральной коло- ды — какая карта будет вытянута и т. д. Главное, что присуще всем этим ситуациям то, что исходы равноправны. Выпадет «орел» или «решка»; выпадет сторона с числом от 1 до 6; вытя- нута одна из 52 карт. Как оценивать исходы, какую меру взять для оценки того, что произойдет то или иное событие. Напри- мер, выпадет четное число при бросании игральной кости. Об- щее количество исходов 6, число благоприятных исходов 3. Вытянута карта пиковой масти, общее количество исходов 52, благоприятных — 13. Понятие меры в теории вероятности вво- дится следующим образом. Определяется множество элемен- тарных событий S, его элементы считаются равноправными, равновероятными. Каждое другое событие А — подмножество S, оно состоит из элементарных событий. Отношение IА | / | S I, где через I I обозначено количество элементов в множестве, на- зывается вероятностью события А и обозначается Р(А). Вероят- ность любого события А заключена между нулем и единицей: 0<Р(А)<1. Примеры: □ При бросании двух игральных костей вероятность получе- ния 10 и более очков равна 4/36=1/9, так как всего имеет- ся 36 равновероятных исходов и четыре из них благопри- ятны — (5,5), (5,6), (6,5) и (6,6). □ Вероятность того, что на первой кости очков выпадет ме- ньше, чем на второй, равна 15/36. □ Стрелок попадает в цель в среднем 92 раза из 100 выстре- лов. Вероятность попадания равна 92/100 (0,92). □ На каждую 1000 готовых деталей некоторого предприя- тия приходится в среднем 16 бракованных. Вероятность изготовления брака для данного производства равна 0,016. □ Первый стрелок попадает в цель с вероятностью 0,8, вто- рой — 0,7. Найти вероятность поражения цели, если оба стрелка стреляют одновременно. Цель считается поражен- ной при попадании в нее хотя бы одной из двух пуль. Пусть производится 100 двойных выстрелов. Примерно в 80 из них цель будет поражена первым стрелком, а в 20 случаях он промахнется. Второй стрелок из 10 выстрелов примерно 7 раз поражает цель, в 20 выстрелах — 14 раз. Таким образом, при 100 выстрелах цель окажется по- раженной примерно 94 раза. Вероятность поражения — 0,94.
Часть вторая Событие, вероятность которого равна 1, называется досто- верным, — оно наступает всегда. Монета опустится «гербом» или «решкой», считаем, что на ребро она встать не может. Со- бытие, вероятность которого равна 0, называется невозмож- ным, — оно не наступает никогда. В нашем примере бросания игральной кости (и других) есть то, что называют элементар- ным событием — выпадение стороны кости с определенным числом. Остальные события составляются из элементарных со- бытий, например выпадение стороны кости с четным числом. События А и В называют несовместимыми, если появление од- ного из них исключает появление другого. При бросании моне- ты «герб» и «решка» ие могут выпасть одновременно. Если А и В — несовместимые события, то Р(А+В)=Р(А)+Р(В). Утвержде- ние обобщается на п несовместимых событий. Говорят, что не- сколько событий Ар А2, ..., Ап образуют полную группу, если вероятность того, что произойдет одно из них, является досто- верным событием. Примеры: □ Опыт — бросание двух монет. События «два герба», «две решки» и «один герб, одна решка» образуют полную груп- пу и являются несовместимыми. □ Опыт — бросание игральной кости. События «1 или 2 очка», «2 или 3 очка», «3 или 4 очка», «4 или 5 очков» и «5 или 6 очков» образуют полную группу, но не являются несовместимыми. □ Опыт — вынимание одной карты из колоды в 36 карт. Со- бытия— вынимание «туза», «короля», «дамы», «валета», «десятки», «девятки», «восьмерки», «семерки» и «шес- терки» образуют полную группу и являются несовмести- мыми. □ Опыт — передача в одинаковых условиях трех сообщений равной длины. События «искажено первое сообщение», «искажено второе сообщение» и «искажено третье сооб- щение» не образуют полную группу и не являются несо- вместимыми. 2. Информация. Итак, об информатике говорят как о науке по работе с информацией с помощью ЭВМ. Мы называем ЭВМ универсальной машиной по обработке информации. Обсудим кратко термин информация, понимая при этом, что строгого определения нам не дать, ибо понятие информации является первичным, неопределяемым. Слово информация используется в двух значениях — качественном и количественном. С одной
Процедуры и функции — элементы структуризации программ 129 стороны, мы понимаем под информацией конкретные сведения о чем-либо, представленные в виде речи, текста, изображения, цифровых данных, графиков, таблиц и т. п. С другой сторо- ны — её численную меру, т. е. выраженное в битах количество абстрактной информации. Остановимся на втором аспекте, ибо обсуждение первого заведет нас в дебри словесной эквилибри- стики. Одним из параметров измерения информации является ее объём: количество бит, байт и т. д. Есть последовательность из единиц и нулей. Длину этой последовательности в одних книгах называют количеством информации, в других — объе- мом информации. Другой способ оценки количества информации основан на вероятностном подходе. Вводится мера неопределен- ности, мера нашего незнания чего-либо. Устранение неопределен- ности достигается за счет получения информации. Если есть «лапоть» для измерения неопределенности, то он может быть использован для наших целей — определения количества ин- формации. Приведем традиционное рассмотрение этой схемы на примере игры «Бар — Кохба». Игра «Бар — Кохба». Ее суть заключается в следующем. Один из игроков что-то загадывает, а второй должен отгадать загаданное, задавая вопросы, которые предполагали бы только ответы типа «да», «нет». Предположим, что в классе 16 учени- ков и учитель загадал номер по журналу одного из учеников, они по какой-то причине пронумерованы числами от 0 до 15. В начальный момент времени у нас полное незнание того, какой номер загадан. Первая серия наших вопросов. 1-й вопрос. Находится ли загаданный номер в интервале от 8 до 15? Ответ — да (1). 2-й вопрос. Находится ли загаданный номер в интервале от 12 до 15? Ответ — нет (0). 3-й вопрос. Находится ли загаданный номер в интервале от 11 до 12? Ответ — да (1). 4-й вопрос. Загадан номер 11? Ответ — нет (0). Вторая схема формулировок вопросов. 1-й вопрос. Находится ли загаданный номер в интервале от 0 до 3? Ответ — нет. 2-й вопрос. Находится ли загаданный номер в интервале от 12 до 15? Ответ — нет.
130 Часть вторая З-й вопрос. Находится ли загаданный номер в интервале от 4 до 7? Ответ — нет. 4-й вопрос. Находится ли загаданный номер в интервале от 8 до 11? Ответ — да. 5-й вопрос. Находится ли загаданный номер в интервале от 10 до 11? Ответ — да. 6-й вопрос. Загадан номер 11? Ответ — нет. Чем отличаются серии вопросов? Мера нашего незнания в том и в другом случаях одинакова, т. е. получено одно и то же количество информации. Примем за единицу измерения ин- формации (1 бит) количество информации, содержащееся в от- вете при условии, что ответы равновероятны. В первом случае каждый ответ содержит один бит информации, ибо ответы «да» и «нет» были равновероятны. Во втором случае — нет, в сред- нем один ответ содержал 4/6 бита информации. Всего получено 4 бита информации. Неопределенность полностью устранена, мы знаем загаданный номер. Пусть есть N равновероятных со- бытий и выделено одно из событий (t). Для того, чтобы опреде- лить t, необходимо получить количество информации I, равное Log2N. Эта формула для вычисления количества информации предложена американским инженером Р. Хартли в 1928 году. Если события не равновероятные, то количество информации вычисляется по формуле американского математика К. Шенно- на: ^-(pjLog^j +p2Log2p2+...+pNLog2pN), где р, — вероятности событий. Из формулы К. Шеннона получается формула Р. Хар- тли при p=l/N для всех значений i. Действительно, /=-/!/ N*Log2(l/N)+ ...+l/N*Log2(l/N))=Log2N. В скобках первой серии вопросов указаны цифры 0 или 1. Если выписать ответы в виде двоичного числа 10102 и перевес- ти его в десятичную систему счисления, то получим число 10 — номер загаданного ученика! Итак, каждая двоичная циф- ра «несет» в себе один бит информации, отсюда и название еди- ницы измерения — БИТ (binary digit — двоичная цифра). В игре «Бар — Кохба» вопросы задаются последовательно, на основании ответов учителя. А можно ли задать сразу четыре вопроса, а затем получить четыре ответа учителя и определить номер загаданного ученика. Оказывается, да. Вопросы форму- лируются так: «Вы загадали номер, двоичная запись которого содержит единицу в первом разряде». Обратите внимание на то, что и в этом случае ответы «да» и «нет» равновероятны.
Процедуры и функции — элементы структуризации программ 131 Предположим, что учитель одновременно загадал номера двух учеников (tt и t2) в разных классах. Количество учеников в классах — Nt и N2. Необходимо отгадать пару (х;,х2), принад- лежащую множеству всех пар — (ЛГ;*ЛГ2). Согласно формуле Хартли необходимо задать Log2(N *N2) вопросов, или полу- чить Log2(N2) бит информации. С другой стороны, номера учеников можно отгадывать независимо, для отгадывания 11 — Log2N: вопросов, для отгадывания второго — Log2N2 вопросов. Следовательно, общее число вопросов равно Log2N^Log^N2. Итак, мы получили Log2(N2)= Log2NI+Log2N2, что совпада- ет с хорошо известным свойством логарифмической функции. Это закон аддитивности информации. Примеры. Имеется 27 монет, 26 настоящих и одна фальши- вая, она легче настоящих. Сколько взвешиваний необходимо произвести, чтобы определить фальшивую монету? Фальшивой может оказаться любая из 27 монет, следовате- льно, по формуле Хартли количество недостающей информа- ции равно Log227 битов. Любое взвешивание имеет три исхода и может дать нам только Log,3 битов информации. Если мы производим X взвешиваний, то они дадут X*Log23 битов ин- формации. Итак, X*Log23>Logz27=Log233=3*Log23. Следовате- льно, Х>3. На самом деле достаточно ровно 3 взвешивания: первое по 9 монет, второе по 3 монеты из найденной группы и, наконец, по одной монете из найденной группы по 3 монеты. Чтобы найти элемент множества, состоящего из 7 элемен- тов, необходимо задать три вопроса, а чтобы найти элемент множества, состоящего из 9 элементов, — 4 вопроса, т. е. по формуле Хартли получить Log27+Log29 битов информации. Но если необходимо отгадать пару элементов из этих множеств, то требуется получить Logz( 7*9 ) битов информации, а это меньше 6 вопросов. Противоречия нет — Log2(7*9) = Log27+Log29= =5,97728<6. Как понимать формулу Хартли, если LogzN не яв- ляется целым числом? Предположим, что мы выполняем отга- дывание не один, a k раз, т. е. последовательность из k неизве- стных элементов — (х2,х2, ...,хА). Таких последовательностей Nk. Очевидно, что для числа вопросов (зА) в этом случае выполня- ются следующие неравенства: Log2Nk<sk<Log2Nk+l или Log2N<sk/k<Log2N+l/k. Число sh/k показывает, сколько вопросов в среднем необхо- димо для того, чтобы отгадать один элемент множества, содер- жащего N элементов. Выбирая значение k достаточно большим, величину 1/k можно сделать сколь угодно малой. Итак, в том
132 Часть вторая случае, когда N не совпадает со степенью двойки, число вопро- сов, которое в среднем необходимо задать для отгадывания од- ного элемента множества мощности У, будет отличаться от LogpN на сколь угодно малую величину.
Процедуры и функции — элементы структуризации программ 133 Занятие № 12. Функции План занятия □ описание функции; □ несколько фактов из комбинаторики; □ экспериментальная работа с программами вычисления чис- ла сочетаний из п по т; получения палиндрома из числа путем последовательного его «перевертывания» и сложе- ний; нахождений общей части п отрезков; □ выполнение самостоятельной работы. Описание функции. Функция выглядит почти так же, как и процедура. Почти единственное отличие в том, что заголовок функции начинается с ключевого слова Function и кончается типом возвращаемого данной функцией значения. Function <имя функщт> [ (список параметров) ] : <тип результатам Кроме того, в теле функции обязательно должен быть хотя бы один оператор присвоения, где в левой части стоит имя фун- кции, а в правой — ее значение. Иначе значение функции не будет определено. Несколько фактов из комбинаторики. Перестановкой из п элементов назовём упорядоченный набор из п различных нату- ральных чисел, принадлежащих интервалу от 1 до п. Пусть п равно 3. Перечислим все перестановки из 3 элементов: (1 2 3), (1 3 2), (2 1 3), (2 3 1), (3 1 2), (3 2 1). Количество перестановок из п элементов (обозначается как Рп) равно пг=1*2'х...*п (факто- риалу числа п). Действительно, есть п способов для выбора эле- мента на первое место, (п-1) способ для выбора элемента на второе место и т. д. Общее количество способов — это произве- дение п*(п-1 )*(п-2)*..*2*1. Перечислите все перестановки из 4 элементов. Количество перестановок — 24. Подсчитайте вручную значения л! для п из диапазона от 5 до 12. Размещения с повторениями. Элементы относятся к п раз- личным видам. Из них составляются всевозможные расстанов- ки по m элементов в каждой. При этом в расстановки могут входить и элементы одного вида. Две расстановки считаются различными, если они отличаются друг от друга или входящи- ми в них элементами, или порядком элементов. Пусть п равно 3, а т — 2 (как обычно, рассматриваем натуральные числа). Размещения с повторениями: (1,1), (1,2), (1,3), (2,1), (2,2), (2,3), (3,1), (3,2), (3,3). Число таких расстановок R(n,m)=nm. Доказывается с помощью математической индукции.
134 Часть вторая Перечислите все размещения с повторениями при п=4 и т=2. Подсчитайте вручную значение R(n,m) для нескольких значений пит. Размещением без повторений из п элементов по т называет- ся упорядоченный набор из m различных чисел, принадлежа- щих интервалу от 1 до п. Два размещения считаются различ- ными, если они либо отличаются друг от друга хотя бы одним элементом, либо состоят из одних и тех же элементов, но распо- ложенных в разном порядке. Пусть п равно 4, a m — 2. Пере- числим все размещения: (1, 2), (2, 1), (1, 3), (3, 1), (1, 4), (4, 1), (2, 3), (3, 2), (2, 4), (4, 2), (3, 4), (4, 3). Количество размещений (A(n,m)) вычисляется по формуле: A(n,m)=n*(n-l)*...*(n-m+l) или A(n,m)=n!/(n-m)! Перечислите все размещения из 5 элементов по 2. Подсчи- тайте вручную значения A(n,m) для фиксированного значения т, например 4, и п из интервала от 8 до 13. Сочетанием из п элементов по m называется множество из m различных чисел, выбранных из диапазона от 1 до n. С поняти- ем множества мы еще не знакомы. Пусть множества составля- ются из натуральных чисел от 1 до 5. Наборы (1, 2, 5) и (2, 5, 1) — это одно и то же множество. Множества (1, 1, 2, 5) не су- ществует, элемент 1 повторяется два раза. Пусть п равно 5, а m — 3. Перечислим все сочетания: (1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5), (2, 3, 4), (2, 3, 5), (2, 4, 5), (3, 4, 5). Каждому сочетанию соответствует т! размещений. Обозначим число сочетаний как C(n,m). Таким образом, A(n,m)=C(n,m)*m! или C(n,m)=A(n,m)/m!= n!/(m!*(n-m)!). Перечислите все сочетания из 7 элементов по 5. Подсчитайте число сочетаний для различных значений пит (не очень боль- ших). При подсчёте как числа сочетаний, так и числа размеще- ний, используется операция деления. Обоснуйте, почему в ре- зультате получаются только целые числа. Экспериментальный раздел работы 1. Составить программу подсчета числа сочетаний — C(n,m). Пусть у нас есть функция подсчета факториала числа — Fact(n). Написание основной программы сводится к про- граммированию формулы C(n,m)= n!/(m!*(n-m)!). Обратите внимание на то, что вызывать функцию можно прямо при вычислении выражения, а не в отдельной строке програм- мы, как мы делали при вызове процедур.
Процедуры и функции — элементы структуризации программ 135 Program Му12_1; Var п,т: Integer; с: Longint; Function Fact(n:Integer):Longint; Begin End; Begin WriteLn('Введите пит :'); ReadLn (n, m) ; c:= Fact (n) / (Fact(m) * Fact (n-m)) ; WriteLn(c); ReadLn; End. В этот момент времени у Вас только то, что Вы видите. Тела функции нет, но программа компилируется, и Вы ее сохранили на внешнем носителе. А компилируется ли? Да нет. Ошибка в строке вычисления с — Error 26: Type mismatch. — несоответ- ствие типов. Вспомним, типы данных определяют не только диапазон значений переменных, но и допустимые над этим ти- пом операции. Обычное деление «/» недопустимо для перемен- ных целого типа. Необходимо использовать операцию Div — с:= Fact(n) Div (Fact(m) * Fact(n-m));. Дополняем програм- му. Function Fact(n:Integer) tLongint; Var i: Integer; rez: Longint; Begin rez:=1; For i:=l To n Do rez:-rez*i; Fact:=rez; End; Запускаем её для различных значений. При п=10, т=5, ре- зультат 252, пока не вызывает сомнений. При п=13, т=5, резу- льтат 399, что-то не так, должен быть больше. При п=15, т=7, результат 9, программа не работает. Объясните причину. Под- сказка содержится в материале этого занятия. А можно ли упростить программу? Переменная с лишняя. Попробуйте убрать её. Сколько лишних вычислений (умножений) делает эта про- грамма? Подсчитаем С(13,5) = (1* 2* 3* 4* 5* 6* 7* 8* 9* 10* 11* 12* 13) / (1* 2* 3* 4* 5) * (1* 2* 3* 4* 5* 6* 7* 8). Вряд ли разумный человек будет выполнять все умножения. Он выпол-
136 Часть вторая нит: 9*11*13 и получит 1287. Почему же мы заставляем компь- ютер быть глупым? Напишите более эффективную функцию вычисления числа сочетаний. Например, даже нижеприведен- ный вариант программы даст более обнадеживающие результа- ты. Правильный результат при С(13,5), а именно, 1287, уже шаг вперед, но это не предел улучшений программы. Program Му12_1т; Var п,т:Integer; Function S(п,т:Integer) tLonglnt; Var i:Integer; rez,cht:Longlnt; Begin rez:=1;cht:=1; For i:-l To m Do Begin rez:=rez*i;cht:=cht*(n-i+1); end; S:=cht Div rez; End; Begin WriteLn('Введите два числа: '); ReadLn (n,m) ; WriteLn (S (n,m) ) ; ReadLn; End. Продолжим тему, хотя функции при решении модифициро- ванной задачи и не используются. На восьмом занятии мы по- знакомились с основной теоремой арифметики. Пусть нам не требуется вычислять С(п,т), а необходимо представить это значение в виде р;а2р2а2.-.рда’, где а>0, a pt — простые числа. Ограничения 1<п<100, 1<т<п. Вычислять, если так можно вы- разиться, «в лоб» — безнадежное занятие. Числа огромные, а пока мы не умеем работать с ними. Рассмотрим идею решения на примере. Вычисляем С(8,5)=8!/(5!*3!). Представим 8! как единицы в соответствующем массиве, например с именем Sn, Sn[i]=l говорит о том, что число i есть в произведении. Просмат- риваем массив Sn, выбираем очередной элемент. Если число i со- ставное, то находим его разложение на простые сомножители. К элементам Sn, соответствующим сомножителям, прибавляем ко- личество единиц, равное числу их вхождений в i, a Sn[i] обну- ляем. Выполним эти действия для п!, т! и (п-т).'. После этого останется только провести вычитание степеней одного и того же простого числа.
Процедуры и функции — элементы структуризации программ 137 Итак, С(8,5)=23*71. Program Му12_2; Const Q=100; Type MyArray=Array[1..Q] Of Integer; Var n,m:Integer; P:MyArray; Procedure Sol\e (sn, sm: Integer ; Var Sp:MyArray) ; Begin End; Procedure Print (pn : Integer ; Pp: МуАггау) ; Var 1:Integer; Begin For i:=2 To pn Do If Pp[i]0 Then WriteLn (i:5, Pp [ i]:5); End; Begin WriteLn ('Ввод чисел n и m') ; ReadLn (n,m) ; Solve (n,m,P) ; Print (n,P) ; ReadLn; End. Итак, есть только основная программа с вызовами проце- дур. Процедура Print приведена в силу ясности её написания. Программа компилируется. Программируем так, как говорим: ввели данные; выполнили обработку; вывели результат. При- держивайтесь этого принципа написания. Язык программиро- вания для Вас должен стать естественным языком для выраже- ния мыслей, так же как, например, русский язык в общении. Уточнение логики.
138 Часть вторая Procedure Solve(sn,sm:Integer;Var Sp:MyArray); Var i:Integer; Sni,Smi,Snmi:MyArray; Begin Calc(sn,Sni);{*Сформировали массив, соответствующий n'*) Calc (sm,Smi);{* Сформировали массив, соответствующий m! *) Calc(sn-sm,Snmi);{★Сформировали массив, соответствующий (n-m)!*) For i:=2 To sn Do Sp[i]:=Sni[i]-Smi[i]-Snmi [i] ; End; Сделав заголовок процедуры Calc с операторами Begin и End, Вы получите опять компилируемую программу. Вариант процедуры Calc, приведенный ниже, хотя и работает, но не так хорошо читабелен, как предыдущая логика. Выполните трасси- ровку этой процедуры в режиме отладки, определите назначение каждого оператора. Ваша задача улучшить процедуру. Вспомни- те о том, что в материале занятия №8 перечислены все простые числа до 100. Procedure Calc(п:Integer; Var X:MyArray); Var 1,j,t:Integer; Begin FillChar(X,SizeOf (X),0) ;{* SizeOf — вычисляет размер области памяти, выделенной для массива X; FillChar — записывает 0 значения в зту область памяти, инициализация X.*) For i:=l То n Do X [1] :=1 ; { *признаки чисел, задействованных в вычислении факториала числа. *) For i:=4 То n Do Begin t: =i; j :=2; While (j<=(t Div 2)) Do If t Mod j =0 Then Begin(*j делит t без остатка. *} Inc(X[j]); t:=t Div j; If t=j Then Inc (X[j ]) ; End Else Inc (j) ; If toi Then Begin (*Найдены делители числа i. *) X[i]:=0;If (toj) Then Inc(X[t]); End; End; End;
139 Процедуры и функции — элементы структуризации программ Вы решили исследовать задачу для большего диапазона зна- чений п. Пусть п<10000. Запуск программы принесет только разочарование, даже если изменить тип Integer на Longlnt. По- явится ошибка Error 202: Stack of erf low error. He занимаясь де- тальным выяснением ее сути, считаем, что просто не хватает оперативной памяти компьютера для хранения данных про- граммы. Действительно, массивов введено много. Достаточно одного массива Р при условии, что он объявляется как глобаль- ный и не используется при вызове процедур. Вариант програм- мы. l$R+) Program Му12_2т; Const Q=10000; Туре МуЛггау=Аггау[1..Q] Of Integer; Var n ,m: Longlnt; P:МуАггау; Procedure Inc_O (n : Longlnt) ; Var i, j , t: Integer; Begin For i:=2 To n Do Begin t:=i; j:=2; While (g<=(t Div 2)) Do If t Mod j =0 Then Begin Inc (P[j ]) ; t: =t Div j ; If t=j Then Inc(Pljl); End Else Inc (j) ; If toi Then Begin If (toj) Then Inc(P[t]); End Else P[1]:=1; End; End; Procedure Dec_O (n : Longlnt) ; Var i,j,t:Integer; Begin For i:=2 To n Do Begin t:=i; j:=2; While (j<=(t Div 2)) Do If t Mod j=0 Then Begin Dec (P [ j ]) ; t: =t Div j; If t=j Then Dec (Pfj]) ;End Else Inc(j) ; If toi Then Begin If (tog) Then Dec(P[t]); End Else Dec(P[i]) ; End; End;
Часть вторая Begin WriteLn('Ввод чисел п и т') ; ReadLn(п,т); 1пс_0(п) ; Dec_O (т) ; Dec_O (п-т) ; End. Программа работает, но она опять не лучшая (вывод резуль- тата не приводится). Код процедур 1пс_О и Dec_O почти повто- ряется. Это плохо. Избавьтесь от этого недостатка, а может быть, Вами уже сделан более достойный вариант обработки. В этой программе значение константы Q можно увеличить, на- пример до 30000. Возникает другая проблема — время работы программы, приходится ждать результат. 2. Задание 11 занятия №7 требовало определить, является ли введенное число палиндромом. Оформим «перевертывание» числа в виде функции. Function Pal(n:Long!nt):LongInt; Var x:Longlnt; Begin x:=0; While n<>0 Do Begin x:=x*10+n Mod 10; n:=n Div 10; End; Pal:=x; End; Пусть исходное число введено, например 59. Оно не палинд- ром. «Перевернем» его, получим 95. Найдем сумму чисел 59 и 95 — 154. «Перевернем» это число — 451. Находим сумму — 605. Еще раз: 506 и 1111. Получили палиндром. Найти для всех натуральных чисел из интервала от 50 до 80 количество шагов, необходимых для сведения их к палиндромам, с помо- щью описанной схемы. Program Му12_3; Const а=50;b-80; Var п,t:Longlnt; ent,i:Integer; Function Pal (n:LongInt) .-Longlnt; Begin End;
Процедуры и функции — элементы структур изацнипрогра мм 141 Begin For 1:=а То b Do Begin ent:=0;п:=1; While Pal(n)<>n Do Begin t:=Pal(nJ; n:=n+t; Wri te (t, ' ' ,n, ’ Inc (ent) ; End; WriteLn (1,' ',cnt); End; End. Измените границы интервала a, b на 89 и 89. Число 89, так же как и 98, очень неприятно. На 16-м шаге результат по- прежнему не является палиндромом, а сумма выходит за пре- делы типа Longlnt. Изменим программу, используя четвертый пример из раздела экспериментальной работы десятого заня- тия. Повторите материал о работе с большими числами. В этой задаче нам потребуются умения их складывать и «переверты- вать». ($Р+) Program Му12_3т; Uses Crt; Const MaxN=1000; Type MyArray=Array[0..MaxN+1] Of Integer;{ *Числа храним в массиве, они большие и типа Longlnt не хватает. *) Var A,Z:MyArray; Var ent,n:Integer; Function Eq:Boolean;{^Является ли число палиндромом?*} Var i: Integer; Begin i : =1 ; While (1<=A[O] Div 2) And (A[1]=A[A[O]-1+1J) Do Inc (i) ; Eq: = (i>A[0] Div 2) ; End; Procedure Swap(Var a,b:Integer); Var c:Integer; Begin c:=a;a:=b;b:=c; End; Procedure Rev;{* «Перевертываем» число. *} Var i:Integer; Begin
Часть вторая For i: =1 То А[0] Div 2 Do Swap (A[1] ,А[А[0]-1+1]) ; End; Procedure Change(n:Integer);{‘Переводим число в его представление в виде массива. *} Var i;Integer; Begin i:=l; While n<>0 Do Begin All] :=n Mod 10; n:=n Div 10; Inc (A[0]) ;Inc (i) ; End; End; Procedure Add;{‘Складываем два больших числа. *} Var i,г,w:Integer; Begin i:=1;r:=0; While (1<=A[O]) Or (r<>0) Do Begin w: =A [1] +Z [ i ] +r ; A[1] :=w Mod 10; r: = w Di v 10; If A[A[0]+l]<>0 Then Inc(A[0]); Inc(i) ; End; End; Begin ClrScr; n:=100; While (n<1000) Do Begin ( ‘Исследуем диапазон от 100 до 999. *) FillChar (A,SizeOf (А) , 0) ; Change(n);{‘Преобразуем число. *} ent:=0;l‘Счетчик количества шагов - преобразований числа.★) While (Not Eq) And (A[0] <=MaxN) Do Begin { ‘Пока не палиндром и нет выхода за пределы массива А считаем. *) Z :=А; Inc (ent) ;Rev; Add; { ‘Вызов процедур, они описаны и очевидны. *) End; WriteLn(n,' ent) ;{‘Вывод числа и количества шагов. *} If cnt>=MaxN Then ReadLn;(‘Фиксируем факт выхода за пределы массива А. *) Inc (п) ; End; End.
Процедуры и функции — элементы структуризации программ 143 Для чисел 196, 295, 394, 493, 592, 689, 691, 788, 790, 879, 887, 978, 986 массива А из 1000 элементов недостаточно для хранения результата. А конечен ли процесс, всегда ли можно получить палиндром? Для доказательства или опровержения этого факта требуются математические усилия. Нашими сред- ствами установить его вряд ли возможно, они могут только «натолкнуть» на схему доказательства. Однако рекомендуем Вам продолжить эксперименты с программой, с этой целью массивы в ней определены как глобальные переменные и не пе- редаются в процедуры и функции. Может оказаться, что опера- тивной памяти, выделяемой для задачи на Турбо Паскале, не будет хватать при этом исследовании. 3. Как находить наибольший общий делитель двух чисел, мы знаем из восьмого занятия. Оформим эти действия в виде фун- кции. Function Nod(a,b:Longlnt):Longlnt; Begin Repeat If a>b Then a:=a Mod b Else b:=b Mod a; Until (a=0) Or (b=0) ; Nod:=a+b; End; Рассмотрим следующую задачу. Дано п отрезков, длины ко- торых целые числа. Берем два отрезка и из большего вычитаем меньший. Разность — новый отрезок. Если же длины отрезков совпадают, то один из них исключаем из дальнейшего рассмот- рения. Продолжаем процесс. Ответом задачи является одно число, равное длине оставшегося в результате этих действий отрезка. Пример: п=4 и длины 6, 15, 3, 9. 6 15 3 9 1-й шаг 6 9 3 9 2-й шаг 6 3 3 9 3-й шаг 3 3 3 9 4-й шаг - 3 3 9 5-й шаг - - 3 9 6-й шаг - - 3 6 7—й шаг - - 3 3 8-й шаг - - - 3 Ответ задачи 3. Решение задачи «в лоб» по приведенной схеме возможно, хотя оно достаточно трудоемко и для больших
144 Часть вторая значений п требует значительных временных затрат. На самом деле все сводится к нахождению наибольшего общего делителя п чисел, а вычитания выполнять не требуется. Напишите про- грамму решения задачи. Исследуйте ее работоспособность для больших значений п. Проблемы с вводом исходных данных ре- шаются с помощью генератора случайных чисел, естественно, что длины отрезков хранятся в одномерном массиве. Задания для самостоятельной работы 1. Дано п целых чисел. Найти среди них число, у которого при- веденная ниже характеристика имеет максимальное значе- ние: □ сумма цифр; □ первая цифра; □ количество делителей; □ сумма всех делителей. 2. Дано п целых чисел. Найти среди них пару чисел, для кото- рых выполняется приведенное ниже условие: □ наибольший общий делитель имеет максимальное значе- ние; □ наименьшее общее кратное имеет наименьшее значение; □ есть ли среди заданных чисел «близнецы», т. е. простые числа, разность между которыми равна двум. 3. Дано п целых чисел. Написать программу подсчета количе- ства чисел, в записи которых нет цифры 8. При этом требу- ется использовать функцию вида: Function Yes8(x:LongInt): Boolean. Без использования компьютера решите эту задачу для всех трехзначных чисел. Ответ — 729. 4. Даны два числа п и k (k<n). При этом п означает основание системы счисления, a k — количество знаков в записи чис- ла. Написать программу подсчета количества натуральных чисел в n-ичной системе счисления, записываемых ровно k знаками. Без использования компьютера решите эту задачу для всех трехзначных чисел. Ответ — 900. 5. Объявлен конкурс на получение грантов. В конкурсе прини- мают участие п человек. Гранты выдаются первым пяти уча- стникам, набравшим наибольшее количество баллов. Один участник не может одновременно получить два и более гран- тов. Написать программу определения количества способов, которыми могут быть распределены гранты. Исследовать её область применимости.
Процедуры и функции — элементы структуризации программ 145 6. Дана шахматная доска размером п*п и п ладей. Определить количество способов расположения ладей на шахматной до- ске так, чтобы они не могли бить друг друга. Исследовать, для каких значений п работоспособны типы данных Integer, Word, Longlnt. 7. Дано п различных бусин. Сколько различных ожерелий мож- но составить из них? Исследовать область применимости Ва- шей программы. Примечание При п=7 число ожерелий равно (5040’7);2=360. Делением на двойку учитывается не только поворот (деление на 7), но и пере- ворот ожерелий. 8. Имеются предметы k различных типов. Количество предме- тов первого типа п}, второго — п2, ..., /г-го — пк. Написать про- грамму подсчета числа перестановок, которые можно сделать из этих предметов, и исследовать ее область применимости. Предварительно попытайтесь обосновать формулу: Р^=п '/ (П] ' *п_ ' * Есть текст. Его шифруют (кодируют) с помощью перестановки букв. В результате получается новый текст, называемый ана- граммой (слова лунка и кулан — анаграммы). Для механиче- ской расшифровки анаграммы требуется выполнить количе- ство перестановок, вычисляемых по выше — приведенной формуле, где nt — количество конкретных букв в анаграмме. Предположим, что у Вас есть компьютер с миллиардным быст- родействием (1 миллиард операций в секунду). Он выполняет миллиард умножений в секунду. Задайте значения величинам п, п,, п2,.... пк и подсчитайте, какое время Ваш компьютер бу- дет решать задачу. 9. Черепашка перемещается по кле- l| I I в |1 точному полю размером n*m. Ей-------------------- разрешено двигаться вправо и I___________________ вверх. В начальный момент вре- мени она находится в нижней, ле- вой клетке (А), и ей необходимо попасть в верхнюю, правую клет- —----------------- ку (В). Сколько различных путей J—-I—1—1—• есть у Черепашки. Подсчитать ко- личество путей для различных значений пит. Выяснить, для каких значений пит работоспособны типы данных Integer, Word, Longlnt, т. е. для хранения результата можно исполь-
146 Часть вторая зовать переменные этих типов. Предположим, что для ана- лиза одного пути Черепашки компьютеру с миллиардным бы- стродействием требуется 100 операций. За какое время он проанализирует все пути для конкретных значений п и т? 10. Последовательности длины п составляются из t нулей и k еди- ниц, причем таким образом, что никакие две единицы не на- ходятся рядом. Подсчитать количество таких последователь- ностей (ответ — С(t+l,k)). Найти способ перечисления всех таких последовательностей для небольших значений t и k. Пример: t=3, k=2. Последовательности: 00101, 01001, 01010, 10001,10010,10100. Материал для чтения 1. Историческая справка о термине «алгоритм». Термин «ал- горитм» обязан своим происхождением выдающемуся ученому средневековья Мухамеду ибн Муса ал Хорезми ( в переводе с арабского означает «Мухамед сын Мусы из Хорезма»), сокра- щенно Ал-Хорезми. Он жил приблизительно с 783 по 850 гг. В одном из своих трудов Ал-Хорезми описал десятичную систему счисления и впервые сформулировал правила выполнения ариф- метических действий над целыми числами и простыми дробя- ми. В латинском переводе труда Ал-Хорезми правила начина- лись словами Dixit Algorizmi (Алгоризми сказал). В других переводах автор именовался Algorithmus (Алгоритмус). Посте- пенно люди забыли, что Алгоризми — это автор правил, и ста- ли сами эти правила называть алгоритмами. 2. В настоящее время слово «алгоритм» стало научным тер- мином, обозначающим одно из фундаментальных понятий со- временной математики и информатики. Мы уже использовали этот термин, не обсуждая его, но не судите за это строго. Тер- мин веками использовался в математике без точного определе- ния, и для этого есть причины. Вспомним алгоритм Евклида нахождения наибольшего общего делителя двух натуральных чисел. Независимо от того, кем (чем) он выполняется, наиболь- ший общий делитель будет найден, ибо все действия однознач- но определены и требуется только записать их на понятном для исполнителя языке. Мы записываем их на языке Турбо Пас- каль. По этому алгоритму мы решаем задачу для всех целых чисел, подчеркнем еще раз — для всех, т. е. у этого алгоритма то, что называют массовостью алгоритма и его конечностью, ибо результат будет найден за конечное число действий. Однако
Процедуры и функции — элементы структуризации программ 147 вспомним задачу о преобразовании числа в палиндром. Алго- ритм есть, но для некоторых целых чисел результат мы не по- лучили. Пусть даже мы доказали средствами математики, что для этих чисел процесс бесконечный. Но то, что мы написали, является алгоритмом или нет? Или «гипотеза Сиракуз», приве- денная в седьмом занятии — является она алгоритмом или нет? В школьной учебной литературе приводятся различные не- формальные определения понятия алгоритм и обсуждаются его свойства: определенности, понятности, массовости, диск- ретности, конечности. Например, «под алгоритмом понимается предписание, точное и понятное, определяющее, какие дейст- вия и в каком порядке необходимо выполнить для решения лю- бой задачи из данного класса однотипных задач». Позволим себе не вдаваться в обсуждение определений и свойств, ибо ре- зультат вряд ли окупит затраты. Отметим только, что опреде- ление А. Г. Кушниренко и Г. В. Лебедева «Алгоритм — план будущей деятельности, записанный в заранее выбранной фор- мальной системе. Составляет алгоритм человек, а выполняет ЭВМ» нам кажется наиболее удачным и больше других соответ- ствует нашему пониманию этого термина. Формальной систе- мой в нашем случае является система программирования Турбо Паскаль. Почему формальная? Вспомните, все ошибки устра- ненные в рассмотренных программах. Остановимся коротко на способах записи алгоритмов. Их много, мы будем использовать только одну формальную сис- тему — язык программирования Турбо Паскаль. Различные приемы словесного описания, всевозможные диаграммы, блок- схемы не используются в данном учебнике. Причина проста. Поясним ее примером. Стандарты на документацию по про- граммным проектам 70-х годов двадцатого столетия требова- ли кроме текста самой программы оформления блок-схем. Автор помнит огромные тома этих блок-схем и ту титаниче- скую работу, которая стояла за этими томами. А затем про- граммные проекты жили «своей жизнью», и эти тома жили тоже своей жизнью — они покрывались пылью в хранили- щах. Отследить все изменения программ в этих блок-схемах было не под силу любому коллективу, и через несколько лет работы с программным проектом эти тома уже ничему не со- ответствовали. Приходилось выпускать новый комплект блок- схем (они назывались документацией, блок-схемы требова- лось еще описать) для хранилища. Программирование благо- получно прошло эту стадию развития. Блок-схемы бывают
148 Часть вторая иногда полезны при работе с миниатюрными программами и только. Однако «ничто не уходит бесследно», блок-схемы, диаграммы появились вновь в CASE технологиях, но уже для описания других вещей. То, что получится в результате, по- кажет этот виток развития технологий программирования.
Процедуры и функции — элементы структуризации программ 149 Занятие № 13. Рекурсия План занятия □ понятие рекурсии; □ короткие примеры для иллюстрации рекурсии; □ экспериментальная работа с программами перечисления всех последовательностей длины п из чисел от 1 до k; ге- нерации перестановок чисел от 1 до п; перечисления всех разбиений числа п на сумму слагаемых п=ц,+а.,+ ... +аА, где k, а,, а2... а,;>0; □ выполнение самостоятельной работы. Понятие рекурсии. Рекурсией называется ситуация, когда процедура или функция вызывает сама себя (образно выража- ясь, «этот глист страдал глистами, что мучились глистами сами»). Типичная конструкция рекурсивной процедуры имеет вид: Procedure Rec (t:Integer); Begin (действия на входе в рекурсию>; If <проверка условия> Then Rec (t+1) ; <действия на выходе из рекурсии>; End; Необходимо осознать два ключевых момента при написании рекурсивной логики: ход вычислительного процесса по управ- лению и связи в вычислительном процессе по данным. Проил- люстрируем рисунком для конкретной задачи вычисления фак- ториала числа. Function Factorial ( n: Integer) : Longint; Begin If n=l Then Factorial:=1 Else Factorial:=n* Factorial (n-1) ; End; Найдем 51. Как же вычисляется факториал числа? Первый вызов функции осуществляется из основной программы, на- пример, a:=Factorial(5), переменной а присваиваем значение 51. Действий на входе в рекурсию нет, этап вхождения выделен на рисунке жирными линиями. Он продолжается до тех пор, пока значение локальной переменной не становится равным 1. По- сле этого начинается выход из рекурсии (тонкие линии на ри- сунке). Итак, ход вычислительного процесса по управлению яв- ляется нелинейным! В рекурсивной логике обязательно должно быть условие завершения процесса вхождения в рекурсию (на
Часть вторая жаргоне — «заглушка»). Естественный вопрос — что необходи- мо знать для реализации этого процесса. С входом в рекурсию более понятно осуществляется вызов процедуры или функции, а для реализации выхода из рекурсии требуется помнить, отку- да мы пришли, т. е. помнить точки возврата в вызывающую ло- гику. Чтобы помнить, необходимо хранить. Это место, место хранения точек возврата, называется «стеком вызовов», и для него нашей системой программирования выделяется опреде- ленная область оперативной памяти. В этом стеке запоминают- ся не только адреса точек возврата, но и значения всех локаль- ных переменных, образно выражаясь, запоминается «слепок» процедуры или функции. По «слепку» восстанавливается вы- зывающая логика (процедура или функция), как только мы осознаем это утверждение, становятся понятными связи по данным в рекурсивном вычислительном процессе. 1 вызов (П=5) Примечания 1. Вы знакомы с развитием технологий программирования по мате- риалам для чтения из предыдущих занятий. Найдите для себя от- вет на вопрос «С какого момента времени развития технологий в системах программирования появилась возможность для реализа- ции рекурсивной логики»? 2. Из вышеприведенного примера. Создаются ли копии програм- много кода функции Factorial в процессе её работы? 3. Написание рекурсивных процедур по образцу, приведенному ниже, вряд ли можно считать приемлемым.
Процедуры и функции — элементы структуризации программ 151 Procedure Generate; Begin{*t — глобальная переменная. *} If t=n Then Exit Else Begin <действия 1>; Generate; t:=t-l; <действия 2>; End; End; Безусловно, что экономится место в стеке вызовов (стеке ад- ресов возврата), но сие — простая замена цикла на рекурсию, и последнюю можно рассматривать как трюк и не более, которым она никак не является. Приведем несколько коротких примеров, иллюстрирующих технику написания рекурсивной логики. 1. Сложение двух чисел (а+b). Рекурсивное определение опера- ции сложения двух чисел: 1а, если b = 0 (а + Т) + (Ь - 2), если Ь> О (а -1) + (b + 1), если b < О Function Sum ( a, b: Integer) : Integer; Begin If b=0 Then Sum:=a Else If b>0 Then Sum:=Sum( a+1, b-1) Else Sum:=Sum( a-1, b+1 ); End; 2. Перевод натурального числа из десятичной системы счисле- ния в двоичную. Procedure Rec ( п: Integer ); Begin If п>1 Then Rec/п Div 2) ; Wri te (n Mod 2) ; End; Как изменится процедура при переводе чисел в восьмерич- ную систему счисления? 3. Определить, является ли заданное натуральное число про- стым. Сформулируем по-другому. Верно ли, что заданное на- туральное число п не делится ни на одно число, большее или
152 Часть вторая a(n) = равное ш, но меньшее п. При этом значения числа m нахо- дятся в промежутке от 2 до п. Утверждение истинно в двух случаях: □ если m=n; □ и не делится на т, и истинно утверждение для чисел от т+1 до п-1. Function Simple(m,n: Integer): Boolean; Begin If m=n Then Simple:=True Else Simple:=(n Mod m <> 0) And Simple (m+1, n) ; End; Первый вызов функции — Simple(2,n), где n — проверяемое число. 4. Дано натуральное число п>1. Определить число а, для кото- рого выполняется неравенство: 2а-1<п<2а. Зависимость: 1, п = 1 а(п div 2) + 1, п > 1 Function а( п: Integer): Integer; Begin If n=l Then a:=l Else a:=a (n Div 2) +1; End; 5. Для вычисления наибольшего общего делителя двух чисел можно использовать рекурсивную функцию вида: Function Nod (a,b: Integer) -.Integer ; Begin If (a=0) Or (b=0) Then Nod:=a+b Else If a>b Then Nod: =Nod (a-b,b) Else Nod: =Nod (a,b-a) ; End; 6. Нахождение максимального элемента в глобальном массиве А реализуется с помощью следующей рекурсивной логики. Procedure Search_Max(п:Integer; Var х:Integer); Begin If n=l Then x:=A[l] Else Begin Search_Max (n-1, x) ; If A[n]>x Then x:=A[n]; End; End;
Процедуры и функции — элементы структуризации программ 153 7. Возведение целого числа а в целую неотрицательную степень п рекурсивно реализуется так. Function Pow(a,п:Integer):Integer; Begin If n=0 Then Pow:=l Else If n Mod 2=0 Then Pow: =Pow (a*a, n Div 2) Else Pow:=Pow (a,n-1) *a ; End; 8. Процедура ввода с клавиатуры последовательности чисел (окончание ввода — 0) и вывода ее на экран в обратном по- рядке имеет вид: Procedure Solve; Var п:Integer; Begin ReadLn (n) ; If n<>0 Then Solve; Wri te (n : 5) ; End; 9. Каждое число Фибоначчи равно сумме двух предыдущих чи- сел при условии, что первые два равны 1 (1, 1, 2, 3, 5, 8, 13, 21,...). В общем виде п-е число Фибоначчи можно опреде- лить так: {1, если п = 1 или п = 2 Ф(л - 1) + Ф(п - 2) , еслил > 2 Function Fib (п : Integer) : Integer; Begin If n<=2 Then Fib:=l Else Fib:=Fib (n-1) +Fib (n-2) ; End; 10. Найти сумму первых n членов арифметической (геометри- ческой) прогрессии. Арифметическая прогрессия определя- ется тремя параметрами: первым элементом (а), разностью между соседними элементами (d) и количеством членов про- грессии (п). Очередной элемент прогрессии вычисляется из предыдущего прибавлением разности — аТ10воЕ:=аСтароС+с'- Function Sa (п, a : Integer) : Integer ; Begin If n>0 Then Sa:=a+Sa(n-l,a+d) Else Sa:=0; End;
154 Часть вторая Примечание Попробуйте убрать ветвь Else и объяснить полученный результат. Для приведенных примеров прорисуйте схемы работы про- цедур и функций для конкретных входных данных (не очень больших, естественно). Экспериментальный раздел работы 1. Даны натуральные числа п и k. Перечислить все последова- тельности длины п из чисел от 1 до k. Например, п=3, k=2. Количество последовательностей — kn. Действительно, на каждое из п мест разрешается записывать числа от 1 до k, пе- ремножаем и получаем результат. Строгое доказательство осуществляется традиционно, с помощью простой индуктив- ной схемы. Пусть последовательности выводятся в следую- щем порядке: 1 1 1,1 1 2,1 2 1,1 2 2,2 1 1,2 1 2,2 2 1,2 2 2. Чем он (порядок) характеризуется? Возьмем две соседние по- следовательности, например 1 1 2 и 1 2 1. До какой-то пози- ции они совпадают, в данном случае до второй (несовпадение возможно и в первой позиции), а в этой позиции число во вто- рой последовательности больше, чем число в первой последо- вательности. Такой порядок называется лексикографическим. Как из очередной последовательности получать следующую в лексикографическом порядке? Идем справа. Находим пер- вый элемент, не равный k. Увеличиваем его на единицу, а «хвост» последовательности максимально ухудшаем, т. е. за- писываем 1. В последней последовательности сделать такое преобразование, естественно, не представляется возможным. Примечание Разумеется, сейчас самое время потренироваться на примерах. Как Вы поняли постановку задачи? Одномерный массив из п элементов — очевидная структура данных для хранения последовательности. Тип элементов в массиве можно определить как обычно Integer, но давайте чуть-чуть изменим: Type MyArray = Array[l..n] Of O..k; Мы ввели ограничение на тип Integer. Допустимыми значениями элементов массива А являются целые числа в интервале от 1 до h, и компьютер контролирует выход их за допустимый диапа- зон (использовался ограниченный тип данных ли на предыду- щих занятиях?). Определим «костяк» программы и очевидную процедуру вывода элементов массива.
Процедуры и функции — элементы структуризации программ 155 (SR+) Program Му13_1; Const п=3; к=2 ; Type MyArray=Array[1. . п] Of 0 .. к; Var А:MyArray; Procedure Print; Var j.: Integer; Begin For i: =1 To n Do Write (A[i] : 3) ; WriteLn ; End; Procedure Solve (t: Integer) ; Begin End; Begin FillCnar (A, SizeOf (A) ,0) ( *Очпстка . *) ; Solve (1) ; End. Из первого вызова рекурсивной процедуры Solve возникает вопрос — а что определяет параметр рекурсии 1? Позицию в по- следовательности (номер элемента в массиве А). Следующий вызов — Solve(t+1), при значении t, большем п, необходимо выводить очередную последовательность, а иначе — записать очередной элемент, он определяется значением управляющей переменной г, в позицию t. Procedure Solve (t: Integer) ; Var i: Integer; Begin If t>n Then Print Else For i: =1 To к Do Begin A[t] :=i; Solve (t+1) ; End; End; Измените программу так, чтобы последовательности выво- дились в следующей очередности: 2 2 2, 2 2 1, 2 1 2, 2 1 1, 1 2 2, 12 1, 1 1 2, 1 1 1. Предположим, что значение k больше значения п. Изменить программу. В последовательности чисел i-й элемент не должен превосходить г. Это простое изменение программы. А сейчас, при этом же предположении (k>n), требуется сгенерировать
156 Часть вторая возрастающие последовательности чисел. На месте t в последо- вательности допускается запись чисел из интервала от A[t-1 ]+1 до k-(n-t). Действительно, t=n, максимальное число в пози- ции п равно, естественно, fe; t=n-l, максимальное число в пози- ции п-1 есть k-1 и т. д. Это мысль приводит к следующему из- менению процедуры Solve. Procedure Solve(t:Integer) ; Var i:Integer; Begin If t>n Then Print Else For 1:=A[t-l}+1 To к-n+t Do Begin A[t]:—i; Solve (t + 1) ; End; End; Однако только этим изменением в программе ограничить- ся нельзя. Первый вызов процедуры Solvef 1) и описание типа Туре МуАггау=Аггау [ 1 ..п ] Of l..k; приводит к знакомой ошибке Error 201: Range check error, в строке For i:=A[t-1 ]+l To k-n+t Do. Причина очевидна — выход индекса за пределы массива А. Изменим описание типа на Type МуАггау=Аг- ray[0..n] Of l..k;. Программа выдает результат, но содержит логическую неточность. При первом вызове Solve использует- ся неопределенное значение А[t-1 ], т. е. 1 прибавляем неиз- вестно к чему. При изменении первого вызова на А[0]:=0; Solvef 1); приходим к новой ошибке — Error 76: Constant out of range (значение константы выходит за допустимый диапа- зон). Еще одно изменение — Type MyArray=Array[O..n] Of O..k; и можно, в первом приближении, закончить экспери- менты с программой. 2. Что такое перестановки чисел от 1 до п, определено на пре- дыдущем занятии. Необходимо написать рекурсивную про- цедуру генерации перестановок чисел от 1 до п. В чем здесь суть рекурсии? Фиксируем на первом месте очередное число и генерируем все перестановки этого вида. При этом посту- паем точно по такой же схеме. Фиксируем число на втором месте и генерируем все перестановки уже с фиксированны- ми элементами на первом и втором местах. Пусть п равно 3. Перестановки должны генерироваться в следующей после- довательности: 1 2 3, 1 3 2, 2 1 3, 2 3 1, 3 2 1, 3 1 2. Если не очень понятно, то рассмотрим при п=4. Вместо многоточия запишите генерируемые перестановки 123 4,1243,132 4, 134 2,143 2,142 3, 213 4, 214 3, 2314, 2341.....4 12 3.
Процедуры и функции — элементы структуризации программ 157 Необходимо осознать, что после генерации всех перестано- вок с фиксированным элементом в позиции t перестановка возвращается в исходное состояние и осуществляется новый обмен значениями элемента из позиции с номером t+1 и i+l. Program Му13_2;(*Процедура Print совпадает с соответствующей процедурой из предыдущей программы. *} Const п-4; Type MyArray=Array[1..п] Of Integer; Var А:МуАггау; 1:Integer; Procedure Swap(Var a,b:Integer); Var c:Integer; Begin c:=a;a:=b;b:=c; End; Procedure Solve (t:Integer) ; Var i:Integer; Begin If t>=n Then Print Else For i:=t+l To n Do Begin Swap (A [ t+1 ] , A [ i ] ) ; Solve (t + 1); Swap (A[t+1],A[i]); End; End; Begin For i:=l To n Do A[i]:=i;{*Первая перестановка - 12 3 n.*} Solve(0);(*Первый вызов. *) End. Для простоты будем считать, что каждый рекурсивный вызов приводит к созданию «копии» процедуры. Это не так, но нам легче при этом предположении формулировать вопро- сы. Сколько «копий» процедуры Solve создается при п=3? Сколько раз создаются копии с t=2? Сколько выполняется лишних обменов типа A[j]++A[j]'l На рисунке приводится часть схемы вызовов (обозначено стрелками) процедуры Sol- ие(при п=3). Для ответа на сформулированные вопросы за- кончите рисунок.
158 Часть вторая < 123> <132> <213> <231 > <321 > <312> Измените описание типа элементов у массива А на Туре MyArray=Array[l..n] Of Т.п; (ограниченный тип). Возникает ошибка (Error 26: Type mismatch) при вызове процедуры Swap. Устраните ее. Не исключено, что Вам придется ввести еще один тип данных. 3. Дано натуральное число п. Необходимо получить все разбие- ния числа п на сумму слагаемых п=а1+а2+ ... +ак, где к, ар а2, ..., ак>0. Будем считать разбиения эквивалентными, если суммы отличаются только порядком слагаемых. Множество эквивалентных сумм представляем последовательностью ар а2, ..., ак, где а, >а2 > ... >ак. При п=4 разбиения 1+1+1 + 1, 2+ +1+1, 2+2, 3+1, 4 перечислены в лексикографическом поряд- ке. Для каждой пары соседних разбиений находится номер позиции, в которой число из второго разбиения больше чис- ла из первого разбиения, а до этой позиции последователь- ности совпадают. Порядок 4, 3+1, 2+2, 2+1+1, 1+1+1+1 опре- деляют как обратный лексикографическому. Наша задача — генерировать разбиения в последнем порядке. Рекурсивная схема реализации просматривается достаточно легко. Мы за- писали в позицию t число at. Сумма чисел aI+a2+...+at=s, тог- да с позиции t+1 мы генерируем все разбиения числа n-s, причем для элемента (и остальных) должно выполняться не- равенство А[ t+1 ]<A[t], Переход к следующему разбиению в позиции t сводится, если это возможно, к вычитанию 1. Уточним некоторые технические детали. Разбиение хранит- ся в глобальном массиве А. Количество элементов в разбиении различно. Значение переменной t определяет текущий размер
Процедуры и функции — элементы структуризации программ 159 выводимой части массива А. Процедура Print претерпит небо- льшое изменение. Procedure Print (t: Integer) ; Var i: Integer; Begin For i:=l To t Do Write (A [i] : 3) ; WriteLn; End; Рекурсивная процедура Solve имеет два параметра: число п и позицию t, начиная с которой необходимо получить все раз- биения числа п. Первым разбиением является разбиение, соот- ветствующее записи числа п в позицию t, а затем ... Последова- тельно записываем в позицию t числа п-1, п-2 и так до 1, а с позиции t+1 генерируем все разбиения чисел 1, 2, .... п-1 со- ответственно. Приведем текст решения. (SR+) Program Му13_3; Const п=4; Type MyArray=Array[1..п] Of Integer; Var А: Му Ar ray ; Procedure Solve (п, t: Integer) ; Var i:Integer; Begin If n=l Then Begin A [t] : =1; Print (t) ;End Else Begin A[ t] : =n ; Print (t) ; For i:=l To n-1 Do Begin A[t] :—n-i; Solve (i, t+1) ; End; End; End; Begin Solve (n,l) ; ReadLn; End. Однако после запуска программы получается результат, не- сколько отличный от предполагаемого: 4, 3 1, 2 2, 2 1 1, 1 3, 1 2 1, 1 1 2, 1 1 1 1. Строки 5, 6, 7 лишние. Изменим процедуру. Procedure Solve (п, t: Integer) ; Var i: Integer ; Begin If n=l Then Begin A[t] :=1;Print (t) ;End
160 Часть вторая Else Begin If n<=A[t~l] Then Begin Aft]:=n;Print (t);End; For i:=l To n-1 Do Begin Alt]:=n-i; Solve(i,t+1); End; End; End; Появление индекса t-1 требует изменить описание типа мас- сива на Type MyArray=Array[O..n] Of Integer; и осуществлять начальное присвоение А[0]:=п перед первым вызовом Solve (п,1). Не внесение этих изменений приводит к известной ошиб- ке Error 201: Range check error. Что мы пытались сделать? От- сечь лишние разбиения при их выводе, но не генерации! Запуск программы нас очередной раз разочарует. Результат работы: 4, 3 1, 2 2, 2 1 1, 1 2 1, 1 1 1 1. Изменим процедуру. Procedure Solve(п, С:Integer); Var i,q:Integer; Begin If п=1 Then Begin A[t]:=1;Print (t) ;End Else Begin If n<=A[t-1] Then Begin A[t]:=n;Print (t);End; g:=Min(n-1,Aft-1]) ; For i:=q DownTo 1 Do Begin Aft]:=1; Solve (n-i, t + 1) ; End; End; End; Она потребует функцию Min, но ничего страшного, вставьте ее в текст решения. Function Min(a,b:Integer):Integer; Begin If a<b Then Min:=a Else Min:=b; End; Получаем правильный результат: 4, 31, 22. 211, 1111, но часть решений мы по-прежнему «отсекаем» только на выхо- де. Продолжите исследование. Научитесь выводить разбиения в следующих порядках: □ 1 1 1 1, 2 1 1, 2 2, 3 1, 4; □ 1 1 1 1, 1 1 2, 1 3, 2 2, 4; □ 4, 2 2, 1 3, 1 1 2, 1 1 1 1.
Процедуры и функции — элементы структуризации программ 161 Примечание При отладке программы и достижении понимания ее работы Вам очень поможет отладчик системы программирования. Задания для самостоятельной работы 1. Написать рекурсивную программу вывода на экран следую- щей картинки: 222222222222 33333333 33333333 222222222222 (16 раз) (12 раз) (8 раз) (4 раза) (8 раз) (12 раз) (16 раз) 2. В конце XIX века в Европе появилась игра под названием «Ханойские башни». Реквизит игры состоит из 3 игл, на ко- торых размещается башня из колец. Цель игры — перенести башню с левой иглы (1) на правую (3), причем за один раз мож- но переносить только одно кольцо, кроме того, запрещается помещать большее кольцо над меньшим. Выполните трассировку приведенного ниже решения с испо- льзованием отладчика системы программирования. Убедитесь в правильности решения. Попробуйте написать не рекурсивную программу решения задачи. Program Myl3_zl; Var п: Integer; Procedure MoveTown (High, FromI, Tol, WorkI: Integer) ; Begin 6-452
162 Часть вторая If High>0 Then Begin MoveTown(High-1,FromI,WorkI,To I); WriteLn('Перенести кольцо If',High,' с иглы ',FromI,' на иглу ', Tol); MoveTown(High-1,WorkI,Tol,FromI); End; End; Begin Write('Количество колец - '); ReadLn (n) ; MoveTown (n, 1,3,2) ; End. Данная задача своим происхождением обязана индусской легенде, которая рассказывает, что в большом храме Бенареса бронзовая плита поддерживает три алмазных стержня, на один из которых бог нанизал во время сотворения мира 64 золотых диска. С тех пор день и ночь монахи, сменяя друг друга, каж- дую секунду перекладывают по одному диску согласно описан- ным выше правилам. Конец мира наступит тогда, когда все 64 диска будут перемещены, на что потребуется чуть больше 58 млрд. лет. 3. Составить рекурсивную функцию вычисления значения фун- кции Аккермана для неотрицательных чисел пит, вводи- мых с клавиатуры. Im + 1, п = О А(л - 1,1) , п * 0, m = О А(п - 1, А(п, m - V) }, п > 0, m > О Найдите одну пару значений пит, при которых возникнет переполнение стека адресов возврата. 4. Функция F(n) определена для целых положительных чисел следующим образом: II, если n = 1 Л >. F(n div i) , если л > 2
Процедуры и функции — элементы структуризации программ Верно ли следующее решение? Function F (п : Integer) : Integer ; Var 1, s : Integer; Begin If n-1 Then s:=l Else For i:—2 To n Do s :=S+F(n Div 2) ; F:=s; End; Можно ли вычислить с помощью этой реализации функции значения F при п=5, 6, 7, 20. 5. Число сочетаний С(п.т) вычисляется по формуле С(п.т)= =С(п~1,т 1 )+С(п-1,т), при этом С(п,0)=1 иС(п,п)=1. На- пишите рекурсивную функцию для подсчета числа сочетаний. Определите область ее применения (диапазон допустимых зна- чений п и ш). Материал для чтения 1. Продолжим рассмотрение понятия алгоритма. В матема- тике это строгое понятие и теория алгоритмов является по сути одним из ее разделов. Почему возникла эта теория, ведь веками пользовались неформальным определением алгоритма? Дело в том, что появились задачи, для которых не могли найти алго- ритмов решения. После безуспешных попыток естественно воз- ник вопрос «Существуют ли вообще алгоритмы решения таких задач»? Опираясь на нестрогое понятие алгоритма, доказать этот факт средствами математики, а других нет, не представля- ется возможным. Примеры задач. А. Тьюринг доказал, что невозможно найти алгоритм, который по произвольной про- грамме определял бы, остановится эта программа на произволь- но заданном входе или нет. Для десятой проблемы Гильберта о разрешимости в целых числах полиномиальных уравнений также не найдено решения. 2. Алфавит — это непустое конечное множество символов. Всякую конечную последовательность символов алфавита назо- вем словом. Допускается существование пустого слова (обозна- чим Л). Примеры. Натуральные числа можно записать в виде слова в алфавите {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. Целые — {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -}. Рациональные — {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, —, /}.
Часть вторая Однако для записи натуральных чисел достаточно алфавита, состоящего из одного символа {I}. Слова в этом алфавите вы- глядят: I, ||, III, Illi, I I I I I, ... Дополнение этого алфа- вита, обеспечивающее запись целых и рациональных чисел, не вызывает трудностей. Рассмотрим квадратные уравнения с це- лыми коэффициентами, например 7х2-Зх+5=0. В алфавите {О, 1, 2, 3, 4, 5, 6, 7, 8, 9, -, +, *, =} они представимы в виде слов типа 7*х*х-3*х+5=0. Примеры можно продолжить. Главное: входные, промежуточные и конечные результаты любой прак- тически решаемой задачи можно интерпретировать как слова в некотором конечном алфавите. Если это не так, то вопрос о по- лучении результата решения задачи остается открытым. Как обрабатываются слова? Заменой, часть слова или все слово за- меняется другим. Эту замену называют подстановкой. Напри- мер, есть алфавит (|, #} и подстановка: I # I-» I I. Слово I I # I I I # I I Г I в результате последовательного применения подстановки преобразуется в слово I I I I I I I I I. 3. Нормальные алгоритмы А. А. Маркова. Дадим определе- ние понятия алгоритма по А. А. Маркову. Пусть А — некото- рый алфавит, не содержащий символов «->» и «.». Обычной формулой подстановки в алфавите А называется ориентиро- ванная подстановка P->Q, где Р и Q — слова в алфавите А. За- ключительной подстановкой в алфавите А называется подста- новка P.Q, где Р и Q — слова в алфавите А. При этом Р называется левой частью формулы подстановки, a Q — ее пра- вой частью. Обыкновенную и заключительную подстановки записывают единым образом Р—>yQ, где у есть или символ «.» или пустое слово Л. Определение.Конечная непустая упорядоченная система под- становок: Pi->YiQi P2-+Y2Q2 Pn—>ynQn называется нормальной схемой в алфавите А, все Рр Qx — слова в алфавите А для всех i от 1 до п. Нормальный алгоритм задается алфавитом А и нормальной схемой в этом алфавите.
Процедуры и функции — элементы структуризации программ 165 Схема работы нормального алгоритма. Дано слово Р в алфа- вите А. Выясняется, есть ли среди подстановок нормальной схемы хотя бы одна с левой частью Рр входящей в Р. Если та- ких подстановок нет, то алгоритм заканчивает работу и резуль- тат — само слово Р. Если же в нормальной схеме есть Рр то берется первая подстановка, удовлетворяющая этому условию, и применяется к слову Р. В результате получается новое слово Р’. В том случае, когда выбранная подстановка была заключи- тельная, алгоритм заканчивает работу, в противном случае — процесс продолжается со словом Р’. Слово Q, получаемое в ре- зультате работы алгоритма (к нему не применима ни одна под- становка), есть результат работы алгоритма. Итак, А. А. Марковым дано строгое с математической точки зрения определение алгоритма, нормального алгоритма. Что это дает? Тезис А. А. Маркова: «Если для решения задачи есть алгоритм, то этот алгоритм может быть представлен в виде нор- мального алгоритма». Тезис не доказывается, ибо, с одной сто- роны, есть нечеткое, нестрогое понятие алгоритма, а с другой стороны, строгое определение нормального алгоритма. Тезис может быть опровергнут только конкретным примером, кото- рого пока не найдено и, видимо, не будет найдено. Далее. Есть задача. Для этой задачи доказывается (строгое математическое доказательство), что для нее не может быть построена, создана нормальная схема. Вывод — данная задача не имеет алгоритма решения. 4. Проблема слов. Дан алфавит А, нормальная схема и два слова — Р и Q. Вопрос. Можно ли из слова Р получить слово Q, применяя подстановки из нормальной схемы? Если да, то слова считаются эквивалентными. Как установить факт эквивалент- ности для любых двух слов в алфавите А? Пример. Дан алфа- вит А={а,Ъ} и система подстановок: ba<->ab, аа<->Л и ЬЬ<->Л (обра- тите внимание на то, что подстановки неориентированные). Решите задачу для нескольких слов. Оказывается, что любое слово сводится к одному из следующих слов: a, b, ab, Л. Схема решения: среди всего множества слов W выделяется подмноже- ство ScW, такое, что любое слово из W преобразуется нормаль- ной схемой к одному из слов, принадлежащих S. Тогда, если слова Р и Q преобразуются в одно слово, то они эквивалентны, иначе — нет. Как находить подмножество S — открытый во- прос. Еще пример. Дан алфавит А={а,Ь) и система подстано- вок: ba«->aab, ааа«->Л и ЬЪ«->Л. Определить, эквивалентны ли два
166 Часть вторая произвольных слова — Р и Q. Проверьте, что множество S со- стоит из слов: Л, a, b, аа, ab, aab. В заключение отметим, что нормальные алгоритмы А. А. Маркова не единственная схема уточнения понятия алго- ритма, придания ему статуса строгого математического понятия со всеми вытекающими из этого факта последствиями. Известны алгоритмическая система Э. Поста (1935 г.), алгоритмическая система А. М. Тьюринга (1937 г.), аппарат частично рекурсив- ные функции и т. д.
Процедуры и функции — элементы структуризации программ 167 Занятие № 14. Символьный и строковый типы данных План занятия □ символьный тип данных; □ короткие программы иллюстрации работы с символьным типом данных; □ строковый тип данных; □ процедуры и функции работы со строковым типом дан- ных; □ экспериментальная работа с программами: выделения слов из текста; вставки символа пробела после запятой; нахож- дения расстояния между строками; генерации всех под- строк строки; определения эквивалентности двух слов; □ выполнение самостоятельной работы. Символьный тип данных. Идентификатором типа является зарезервированное слово Char. Значениями типа Char являют- ся элементы конечного и упорядоченного множества символов, т. е. на множестве значение типа Char есть отношение порядка. Американский стандартный код для обмена информацией (AS- СП — American Standard Code for Information Interchange) есть система кодирования, в которой алфавитные, цифровые и управляющие символы представлены в виде 8-разрядного дво- ичного кода. Символы с кодами от 0 до 127 составляют так на- зываемую основную таблицу кодов ASCII. Эта часть идентична на всех IBM-совместимых компьютерах. Так, цифрам от 0 до 9 соответствуют коды от 4810 (001100 002) до 5710 (001110012); буквам латинского алфавита от А до Z — коды от 6510 (010000012) до 9010 (010110102); буквам от а до z — коды от 9710 (011000012) до 12210 (011110102); буквам русского алфави- та от А до Я — коды от 12810 (1000 0 0 002) до 15910 (100111112). Константы этого типа обрамляются в апострофы, например: ’3’ ’G’. Для отображения множества символов на их порядко- вые номера и обратно существуют две функции: Ord и Chr. Функция Ord(w) дает порядковый номер символа w, Chr(i) определяет символ с порядковым номером i. Функции Ord и Chr обратные по отношению друг к другу: Chr(Ord(w))=w и Ord(Chr(i))~i. Отношение порядка на множестве символов по- зволяет выполнять операции сравнения. Из двух символов ме- ньше тот, который встречается раньше в кодировке ASCII. Для величин типа Char функции Pred и Succ работают следующим образом: Pred(q)=Chr(0rd(q) l) и Succ(q)=Chr(Ord(q)+l).
168 Часть вторая Короткие программы. В первом примере выводятся симво- лы и соответствующие им коды. Переменная к используется как счетчик для организации последовательного вывода — по 5 символов. Program Myl4_1; Var 1, k:Integer; Begin к:=0 WriteLn('Вывод порядковых номеров (кодов) символов — значение переменной г и самих символов.'); For i:=l То 255 Do Begin Write(i:4,' Символ ' ,Chr(i)) ; Inc(k); If k=5 Then Begin WriteLn;k:=0; End; End; End; Во втором примере показано, как переменная символьного типа используется в качестве управляющей переменной в опе- раторе For. Символы выводятся так: А ВВ ссс WWW,.WWW (23 раза) Program Му14_2; Var i:Char; I:Integer; Begin For i:='A' To 'W' Do Begin For j : =1 To Ord(i)-Ord('A')+1 Do Write(i); WriteLn; End; End. Определите, что выводится на экран в результате следую- щей не очень значительной модификации программы. Program Му14_2т; Var i,j:Char; Begin For i:='a' To 'z' Do For g:=’a’ To i Do Write (j) ; ReadLn; End.
Процедуры и функция — элементы структуризации программ 169 В следующем примере подсчитывается количество симво- лов, введенных с клавиатуры. Ввод заканчивается символом Вы вводите несколько символов, затем точку и нажимаете кла- вишу Enter. Программа выдает правильный результат. А если нажимать клавишу Enter после ввода каждого символа? Резу- льтат неверный, он как будто бы в три раза превышает истин- ный результат. На самом деле все верно. Нажатие клавиши Еп ter генерирует ввод еще двух символов (управляющих) — возврата каретки (код 13) и перевода строки (код 10). Program Му14_3; Var 1; Char; j:Integer ; Begin Read(i) ; While i<>'.' Do Begin Inc (j ) ;Read (1) ;End; Wri teLn (j ) ; End. Другая версия этой простой программы позволяет отказать- ся от символа точка как признака конца ввода данных. Символ # перед целым числом говорит о том, что это число следует рассматривать как ASCII код символа. Program Му14_3т; Var i-.Char; j:Integer; Begin Read (1) ; J :=0; While 1ОПЗ Do Begin Inc (J ) ;Read (i) ;End; WriteLn (j) ; ReadLn; End. А если изменить строку на While к>#10 Do Begin Inc(j); Read(i);End;, то количество подсчитанных символов на едини- цу больше. Объясните результат. В следующем примере подсчитывается количество цифр во вводимых с клавиатуры данных. Program Му14_4; Var ch:Char; к:Integer; Begin Read (ch) ;
170 Часть вторая к: =0 ; While ch<>#13 Do Begin IF (ch>='0') And (ch<='9') Then Inc (k); Read (ch) ; End; WriteLn('Количество цифр равно ' ,k) ; End. Измените программу так, чтобы она □ определяла, являются ли введенные данные правильной записью целого числа; □ вычисляла сумму цифр введенного числа. Функция Upcase(ch) преобразует значение переменной ch символьного типа, если оно соответствует строчной латинской букве, в код прописной буквы, иначе значение ей остается не- изменным. Определите, что за обработка вводимых символов осуществляется в следующем примере. Program Myl4_4т; Var ch:Char; Begin Read(ch); While ch<>#13 Do Begin IF (ch>='a') And (ch<='z') Then Write(Upcase (ch)) Else Write (ch); Read (ch); End; End. Строковый тип данных. Строкой называется последовате- льность символов определенной длины. Идентификатор типа — слово String. Примеры описания переменных типа String: Var Strl: StringflO]; Str2; String; Str3:String[13]. В квадратных скобках указывается максимальный размер (длина) строки. Если он не указан, то длина строки считается равной 255 сим- волам. Заметим, что строку можно рассматривать как одномер- ный массив из символов — к каждому символу строки допусти- мо обращение по его номеру (Strl[i] — это обращение к i-му элементу строки Strl). Первый символ строки (с индексом 0) содержит фактическую длину строки. Переменные типа String выводятся на экран монитора посредством стандартных проце- дур Write и WriteLn и вводятся с помощью ReadLn и Read. То есть вводятся и выводятся не поэлементно, как массивы, а сра- зу целиком. Следующие простой пример иллюстрирует сказан- ное.
Процедуры я функции — элементы структуризации программ 171 Program Му14_5; Var s:String; w:String[10]; v:String[5]; i,j :Integer; Begin ReadLn (v); WriteLn(v); WriteLn(Integer(v[0])); ReadLn(w) ; WriteLn(w); WriteLn(Ord(w[0])); ReadLn (s); WriteLn (s); WriteLn(Integer(s[0])); For i:=l To Ord(s[0]) Do Begin For j:-l To i-l Do Write (' '); Writeln(s [i]) ; End; ReadLn; End. Вы вводите строки v и w большей длины, чем указано в опи- сании. Вывод на экран показывает, что при этом они «обрезают- ся». Операторы WriteLn(Integer(v[O] )) и WnteLn(Ord(w[0] )) обеспечивают вывод значения длины строки. Если Вы измени- те первый оператор на WriteLn(v[O]), то вместо цифрового значения на экран выводится «непонятный» символ. Попро- буйте объяснить этот результат и понять смысл преобразования Integer(v[O] ). Следующие операторы примера демонстрируют посимвольное обращение к строке — строка рассматривается как s:Array[0..255] Of Char;. Вывод символов строки s на экран осуществляется «лесенкой». Сравнение строк происходит посимвольно слева направо: сравниваются коды соответствующих символов до тех пор, пока не нарушится равенство, при этом сразу делается вывод о знаке неравенства. Две строки называются равными, если они равны по длине и совпадают посимвольно. Примеры: □ 'Balkon’c'balkon' (Ord('B’)cOrd(’b')); □ 'balkon’>'balken' (Ord('o’)>Ord('e')); □ 'balkon'>'balk' (длина первой строки больше); □ 'кошка '>'кошка'(так как длина первой строки больше); □ 'Кот'=’Кот'(равны по длине и совпадают посимвольно). Присваиваемое значение строки (строковая константа), так же как и отдельный символ типа Char, заключается в апостро- фы. Например, Strl:=y Егорки; Str2:='ecezda отговорки; Стандартные процедуры и функции работы со строковым типом данных приведены в таблице.
Часть вторая Тип Вызов Параметры Действие I Процедура Delete(s,p,n) Var s. String, р,п-Integer Удаляются n символов из строки s, начиная с позиции р |] Процедура lnsert(w,s,p) w String, Var s String, p Integer, В строку s, начиная с [ позиции р, вставляется строка w | Если результат о превысит 255 | символов,строка | обрывается Процедура Str(v,s) v Integer или v Real, Var s'String, Число V преобразуется в строку, результат в s Процедура Val(s,v,w) s String, Var v integer или Var v Heal, Var w integer, Если строка s состоит из цифр, то они | преобразуются в | числовое значение переменной v, I значение wравно 0 В противном случае, строка состоит нв I только из цифр,— преобразование не [ выполняется, wO — | признакошибки | I Функция Concat(s1,s2, ,sm) или S1+S2+ +sm Значение функции типа String s1,s2, , sm String, Строки s1+s2+ +sm I записываются одна за другой Если I результат превысит 255 символов, строка обрывается | Функция Copy(s,p,n) Значение функции типа String s. String, p,n-Integer; Из строки s, начиная с позиции р, выбирается п символов Функция Length(s) Значение функции типа Integer s String; Определяется длина s, т. е. число символов из которых она состоит 1 Функция Pos(w,s) Значение функции типа Integer w,s String-, В строке s отыскивается первое вхождение строки w (номер позиции) Если вхождения нет, то выдается 0 Примеры. В приведенных примерах переменные si, s2, s3 имеют тип String, p,q — тип Integer. 1. в/:=’У Егорки всегда отговорки’; Delete(sl,7,8);
Процедуры и функции — элеметы структуризации программ 173 Результат — «2 = ’У Егора отговорки’. 2. sl:= ’У Егорки всегда отговорки’; «2:=’Матрены и ’; Insert(s2,sl,3); Результат — sl=’y Матрёны и Егорки всегда отговорки’. 3. р: = 1234;д:=34.5; Str(p,sl); Str(q,s2); Результат — s7 = ’1234’, s2=’34.5’. 4. s7:=’555’;s2:=’23.345’;s3:=’34rr2’; Val(sl.p.w); Val(s2,q,u>); Val(s3,p,w); Результат: в первом случае — р=555, w=0; во втором слу- чае — q=23.345, w=0; в третьем случае — w<>0, р=???. 5. в/:=’У Егорки всегда отговорки, ’; s2:=’y Миладки всегда шоколадки’; s3:=Concat(sl,s2); (или s3:=sl+s2;) Результат — вЗ=’У Егорки всегда отговорки, у Миладки все- гда шоколадки’; 6. з1:=’У Егорки всегда отговорки, у Миладки всегда шоколад- ки’ s2:=Copy(s,28,26); Результат — s2=’y Миладки всегда шоколадки’. 7. sl:=’y Егорки всегда отговорки’; p:=Length(s); Результат — р—25. 8. s7:=’y Егорки всегда отговорки’; p:=Pos(‘o',s ): Результат — р=5. Продолжите примеры. Напишите программу для исследова- ния работы перечисленных процедур и функций. Обратите особое внимание на граничные условия, т. е. когда какой-либо параметр выходит за пределы строки. В этой работе можно использовать приведенные ниже короткие примеры. 1. Подсчет количества символов (параметр q) в строке (пара- метр st). Function QChar(q:Char;st: String): Byte; Var 1, k: Byte; Begin k:=0; For i:=l To Length (st) Do If st[ij=q Then Inc(k); QChar:=k; End;
174 Часть вторая 2. Удалить среднюю букву при нечетной длине строки и две средние буквы при четной длине строки. Procedure MiDel ( Var st: String); Var k: Byte; Begin к:=Length (st); If к Mod 2=1 Then Deletefst, к Div 2+1,1) Else Delete (st,к Div 2,2); End; 3. Заменить все вхождения подстроки w в строке st на подстро- ку V. Procedure Ins(w, v:String; Var st: String); Var k: Byte; Begin While Pos(w,st)<>0 Do Begin k: = Pos(w,st); Delete (st,k,Length(w)) ; Insert(v,st,k); End; End; 4. Подсчитать сумму цифр, встречающихся в строке. Function Sumfst: String): Integer; Var i, к, d, s: Integer; Begin s:=0; For i:=l To Length (st) Do Begin Vai(st[i],d,k); If k=0 Then s:=s+d; End; Sum:=s; End; Экспериментальный раздел работы 1. Дана строка. Считаем ее отрывком текста. Группы символов, разделенных одним или несколькими пробелами, назовем сло- вами. Пробелы могут находиться как в начале текста, так и в конце. Наша задача — выделить слова из текста и каждое слово записать в соответствующий элемент массива. Приве- денная ниже программа решает эту задачу. Выполните ее в режиме отладчика, наблюдая за изменением значений пере- менных.
Процедуры и функции — элементы структуризации программ 175 Program Му14_6; Const п=20;т=10;('Количество слов в тексте и количество букв в слове. Естественно, что эти параметры программы допускается изменять. *} Type TString=String[m]; Var A:Array[l. .n] Of TString; s:String; k, i .'Integer; Procedure DelPr(Var s:String);{*Удаляем пробелы в начале текста. *} Begin While (s[l] = ' ') And (so") Do Delete (s, 1,1) ; End; Function GetWord(Var s:String):TString;{'Выделяем слово, при этом оно удаляется из текста, и убираем пробелы после слова. *) Begin GetWord:=Сору(s,1,Pos (' ',s)-l); Delete(s,1,Pos(' ',s)>; DelPr (s) ; End; Begin WriteLn('Введите текст'); ReadLn (s) ; s:=s+' ';(*Добавляем символ пробела в конец текста. Зачем?*} DelPr(s);{*Удаляем пробелы в начале текста. *} к:=0; While s<>" Do Вед1П{*Пока текст не пустой.*} Inc (к) ; А[к]:=GetWord(s);(*Берем слово из текста.*} End; For-i:=l То к Do WriteLn(A[i]); ReadLn; End. Удалите вызов процедуры DelPr(s) из основной программы, а в функции GetWord переставьте этот вызов в ее начало. Что изменится в работе программы? На каких исходных данных она не будет правильно работать? Что произойдет, если в конец текста не добавлять символ пробела? В процедуре DelPr изме- ните While (s[l]=‘ ‘) And (so”) Do Delete(s,l,l) на While (s[l]=’ ‘) Do Delete(s,l,l). Найдите пример исходного текста,
Часть вторая при котором результат работы программы окажется неверным или его вообще не будет. Продолжите эксперименты с програм- мой. 2. По правилам машинописи после запятой в тексте всегда ста- вится пробел. Программа исправляет такой тип ошибки в тек- сте. Program Му14_7; Var 1:Integer; s:String; Begin WriteLn('Введите текст'); ReadLn (s) ; 1: =1 ; While i<Length(s) Do Begin If (s[i]=',') And Not(s[i+l] = ' ') Then Insert (' ' ,s,i+l); Inc(i) ; End; WriteLn (s) ; ReadLn; End. Почему выбран оператор While, а не For? Почему в теле цик- ла нельзя использовать функцию Posf \s)? Обратите внимание, если запятая — последний символ текста, то мы не добавляем пробела. Измените программу для исправления ошибки типа «после символов '!’, ’?’, должен стоять пробел, а затем текст начинается с заглавной буквы». Вспомните еще ряд традицион- ных ошибок и модифицируйте программу для их исправления. 3. Даны две строки Хи У. Назовем расстоянием (г) между X и Y количество символов, которыми X и Y различаются меж- ду собой. Например: X=’abcd', Y='dxxc, r=4-, Х=' 1111111', Y= 111222', r=7. Написать программы вычисления расстояния между строка- ми. Решение. Program Му14_8; Var i,j,r:Integer; S,X,Y-.String; Begin WriteLn ('Первая строка') ;ReadLn (X) ; WriteLn('Вторая строка');ReadLn(Y);
Процедуры и функции — элементы структуризации программ 177 If Length(X)>Length(Y) Then Begin S:=X;X:=Y;Y:=S; End;{*Y строка большей длины .*} r:=Length(Y); For i:=l To Length (X) Do Begin While (j <=Length (Y) ) And (Y[j ] OX[1] ) Do Inc (j) ; If j>Length(Y) Then Inc(r) Else Begin Dec(r);Delete(Y,j,1);End;{*Есть совпадение. Уменьшаем расстояние и удаляем символ из Y.*} End; WriteLn('Расстояние ', г) ; ReadLn; End. К каким последствиям приведет исключение из решения строки со сравнением длин строк и записи в Y строки наиболь- шей длины? Проверьте. Исключим оператор Delete(Y,j,l). Что за результат у нас будет получаться? Измените программу так, чтобы в тексте находилось два слова, расстояние между кото- рыми имеет максимальное (минимальное) значение. 4. Дана строка не более чем из шести произвольных различных символов. Разработать программу вывода всех возможных подмножеств (подстрок), составленных из символов данной строки. Количество подстрок 2"-1, где п — количество сим- волов в строке. Естественно, что каждый символ входит в под- строку не более одного раза. Будем генерировать шестираз- рядные двоичные числа, так, 010101 соответствует подстрока ‘bdf ’ строки ‘abcdefh’. Принцип генерации: просматриваем число слева направо до первого нуля, заменяя при этом все 1 на 0, например после 110011 получаем 001011. Завершение генерации — число 0000001. Решение. Program Му14_9; Const MaxN=6; Type МуAr г ay=Arг ay[1. .MaxN+1] Of Byte; Var A,Last:МуАггау; i:Integer; s:String; Function Eq (X, Y:МуАггау;n: Integer) :Boolean; {^Сравнение текущего значения с 0000001, если п=6.*}
178 Часть вторая Var i:Integer; Begin i: =1 ; While (i<—n) And (X[i] = Y[i]) Do Inc(i); Eq:=(i>n) ; End; Begin WriteLn('Строка ’);ReadLn (s); FillChar(A,SizeOf(A), 0); FillChar(Last,SizeOf(Last),0); Last[Length(s)+1]:=1;(^Первое несуществующее разбиение. *) While Not(Eq(A,Last, (Length (s)+1))) Do Begin {★Пока нет совпадения, выполняем генерацию следующей подстроки. *} i:=l; While (K=Length (s) ) And (A[i]=l) Do Begin A[i]:=0;Inc(i);End; A[i]:=1; If K=Length(s) Then For i:=l To Length(s) Do If A[i]=l Then Write (S[i]); {★Вывод подстроки. *} WriteLn; End; ReadLn; End. В схеме генерации реализовано обычное прибавление едини- цы к двоичному числу, только число записывается слева напра- во: 000000, 100000, 010000, 110000, 001000 и т. д. Каждому такому числу ставится в соответствие подстрока. Измените схе- му генерации так, чтобы единица прибавлялась традиционным способом: 000000, 000001, 000010, 000011, 000100 и т. д. А можно ли исключить генерации последовательности двоичных чисел из решения задачи? 5. Рассмотрим следующую задачу (по материалу для чтения предыдущего занятия). Заданы две строки А и В длины N (1<А<100), состоящие из символов 0 и 1 (двоичные слова). Допускается следующее преобразование строк. Любую под- строку А или В, содержащую четное число единиц, можно перевернуть, т. е. записать в обратном порядке. Например, в строке 11010100 можно перевернуть подстроку, составлен- ную из символов с 3-й по 6-ю позиции. Получится строка
Процедуры и функции — элементы структуризации программ 11101000. Две строки считаются эквивалентными, если одь из них можно получить из другой с помощью описанных пр< образований. Определить эквивалентность заданных строк и В, если строки эквивалентны, предложить один из возмоя ных способов преобразования строки А в строку В. Пример. Даны строки 100011100, 001011001. Решить дл них задачу. Ответ — «да» и преобразования: 6 9, 3 8, 1 5. Основная идея решения задачи формулируется следующи образом. На множестве всех слов W следует выделить подмн< жество S (ScW), такое, что для любого слова teW можно ук< зать эквивалентное ему слово reS. Алфавит и слова у нас задг ны, преобразования тоже. Осталось определить множество ‘ свести каждое слово к представителю из множества S, и есл представители совпадают, то слова эквивалентны. Из каки слов состоит S? Слов вида 11...100...0100...0, причем как nej вая группа единиц, так и первая группа нулей, а также следук щая единица (слово из одних 0) могут отсутствовать. Основны фрагменты решения. Const Мах=100; Type Tstr=String[Мах]; Procedure Reverse(Var S:Tstr;i,j:Integer); (*Достаточно очевидная процедура — перевертываем подстроку S с позиции i до позиции J . *} Var ch: Char; Begin While Kj Do Begin ch:=S [i] ;S[i] :=S [j ] ;S[j] :=ch;Inc (i) ;Dec (j) ; End; End; Procedure Normalize (Var S:Tstr);(*Сводим слово к его представителю из S. ★) Var 1, j ,cnt:Integer; Begin cnt:=0; For i:=l To Length (S) Do If S[i]='l' Then Inc (ent) ; {*Подсчитываем количество 1 в слове. *) For i:=l To cnt-1 Do If S[i]='0' Then Begin (*Находим «левый» 0 в слове. *)
Часть вторая Repeat Inc (j) Until S[j] = '1';[*Поиск первой Repeat Inc(j) Until S[j]='1"Поиск второй 1, подстрока содержит четное число 1, ее можно «перевернуть».*) Reverse (S,i,j); End; End; Допишите программу. Заметим, что вывод преобразований, в случае эквивалентности слов, требует дополнительной струк- туры данных (массива) для запоминания значений 1 и j при каждом перевертывании. Если исключить из условия задачи вывод преобразований, то её можно решить гораздо проще. Рассмотрим таблицу. Столб- цы с заголовком «?» оставлены осознанно. № Слово число 1 ? 1111000010 5 4 2 11111001 6 0 2 3 0000001000 1 6 3 4 10010001100 4 5 2 5 11000100100 4 5 2 6 11100100000 4 5 2 7 0100101101 5 3 2 | 8 1001001101 5 3 2 | 9 1100100101 5 3 2 | 10 1110010001 5 3 2 I 11 1111000100 5 3 2 | Внимательно изучив таблицу, догадываемся, что в послед- них столбцах указано число нулей. Но каких? В четвертом столбце количество нулей после четного числа единиц, в пя- том — после нечетного. И вторая часть нашего вывода — эти числа не изменяются в процессе преобразования слов (строки таблицы с номерами 4-6 и 7-11). После этого можно напи- сать небольшой фрагмент подсчета этих чисел (сразу после их ввода)
Процедуры и функции — элементы структуризации программ СПt:=0; chl:=0;ch2:=0; For i:=l To Length (S) Do If S[i]='O' Then If ent Mod 2=0 Then Inc(chl) Else Inc(ch2) Else Inc (ent); Итак, если значения chl и ch2 для слов совпадают, то они эквивалентны в вышеприведенном значении. Нулевая позиция в слове считается при этом четной, например числа 1, 5, 4 од- нозначно определяют слово 0000010000. Обоснуем результат. Инвариантом преобразования является знакочередующая сумма вида: Т.(-1 )‘*at, где i — номер едини- цы, а а, — номер позиции I единицы. Схема доказательства. Обозначим через s номер позиции в подстроке, соответствую- щий центру переворачиваемой подстроки. Пусть х — номер по- зиции до «переворота», у — номер этой позиции после опера- ции «переворота». Очевидно, что (х+у )/2=s, то есть y=2s-x или al-^2s-ai. Количество элементов в сумме четно, знаки чере- дуются, и £(-1 )‘*at =Z(-1 )‘*(2s~al). Поэтому после ввода слов следует подсчитать количество 1 в каждом из них и суммы ука- занного типа. Если они совпадают, то слова А и В эквивалент- ны. Например, для строки 0101 сумма равна (-1 )}*2 +(-1 )2* *4=2, после переворачивания — 1010 сумма — (-1 )!*1 + (-1 )2* *3=2. Они совпадают, строки эквивалентны. Для примера (110100 и 110001) эти суммы равны -3 и -5, строки неэквива- лентны. Задания для самостоятельной работы 1. Написать программу вывода последовательности символов: □ ZYYXXX...AA..AA; □ ABC...ZZBC...ZZZC...ZZ..ZZ на экран. 2. Составить программу, которая выводит True, если в строке буква А встречается чаще, чем буква В, и False в противном случае. 3. Проверить, правильно ли в заданном тексте расставлены круглые скобки (т. е. находится ли справа от каждой откры- вающей скобки соответствующая ей закрывающая скобка, а слева от каждой закрывающей — соответствующая ей откры- вающая). 4. Подсчитать количество гласных латинских букв в строке.
182 Часть вторая 5. Удвоить вхождение некоторой буквы в текст. Например, текст «мама папа» должен иметь вид — «маамаа паапаа». 6. Даны две строки. Вывести буквы, встречающиеся и в той и в другой строках. 7. Дан текст. Вывести все слова, начинающиеся с согласных букв латинского алфавита. 8. Дан текст. Определите □ длину самого короткого и самого длинного слов; □ количество слов, начинающихся и оканчивающихся од- ной и той же буквой; □ количество слов, в которых содержится хотя бы одна за- данная буква; □ количество слов, которые содержат определенное коли- чество заданной буквы; □ количество слов, являющихся палиндромами. 9. Дан текст. Вывести слова, встречающиеся в тексте по одно- му разу. 10. Дан текст. Вывести различные слова. 11. Дан текст. Вывести все слова, предварительно преобразовав каждое из них по следующему правилу: □ удалить из слова все предыдущие вхождения последней буквы; □ оставить в слове только первые вхождения каждой бук- вы. 12. Дан текст. Вывести слова, которые отличны от последнего сло- ва и удовлетворяют следующему условию: □ в слове нет повторяющихся букв; □ буквы слова упорядочены по алфавиту; □ слово совпадает с начальным отрезком латинского алфа- вита (a, ab, abc, abed,...). 13. Дан текст. Вывести все слова, предварительно выполнив пре- образования их по правилу: заменить во всех словах первую букву на заглавную. 14. Написать программы решения ребусов: ОДИН+ОДИН+ +ОДИН+ОДИН+ОДИН=ПЯТЬ; КУБ=(К+У+Б)3; ТРИ+ДВА= =ПЯТЬ; VOLVO+FIAT=MOTOR. При решении ребусов одинаковым буквам соответствуют одинаковые цифры. Например, приве- дем часть ответов последнего ребуса: 15615+9743=25358, 15715+9643=25358, ..., 72672+9451=82123, их всего 10.
Процедуры и функции — элементы структуризации программ 183 15. Дан текст. Написать программу проверки правильности на- писания сочетаний «жи», «ши», «ча», «ща», «чу» и «щу». Исправить ошибки. 16. Даны три строки. Определить, можно ли из символов первых двух строк получить третью строку. 17. Даны две строки. Определить можно ли, переставляя симво- лы в первой строке, получить вторую строку. Примечание Термин текст в условиях задач следует понимать в смысле перво- го примера экспериментальной части данного занятия. Материал для чтения 1. О кодировании. Мы уже неоднократно использовали этот термин. Попробуем разобраться с ним. Из истории. Коды поя- вились в глубокой древности в виде криптограмм, зашифрован- ных (засекреченных) сообщений. В шифре, получившем назва- ние — шифр Юлия Цезаря, алфавит размещается как бы на круге по часовой стрелке. После последней буквы, например Я в русском алфавите, идет первая буква алфавита (буква А в русском алфавите). Для зашифровки буквы текста заменяются буквами, отстоящими по кругу на заданное число букв (сдвиг) дальше по часовой стрелке. Пусть сдвиг равен 2 и сообщение составлено в русском алфавите. Буква А заменяется на букву В, буква Я на букву Б. Принцип расшифровки (декодирования) понятен, если известно значение сдвига. Шифр Цезаря рас- шифровывается достаточно легко даже при неизвестном значе- нии сдвига. Известны вероятности букв pt, i=l, .... п (п — число букв в алфавите). Подсчитаем частоты букв Д в сообщении (считаем, что оно достаточно длинное). Вычисляем значение Sum(sh)=Abs(p-fi(sh)). Сумма берется по г от 1 до п. Значение sh — это значение сдвига. Найдем минимальное значение Sum, подсчитывая ее для различных значений sh от 1 до п. Значение sh, на котором достигается минимум значения Sum, считаем сдвигом в шифре Цезаря. С течением времени появились достаточно сложные шифры. Клодд Шеннон показал, что возможно построение криптограм- мы, которая не поддается никакой расшифровке, при условии, что не известен способ её составления. Итак, создание шиф- ров — объект криптографии, взлом шифров — криптоанализ. В целом эту область деятельности называют криптологией. Кодирование, как мы его понимаем в настоящее время, воз- никло позднее, и оно связано с проблемой передачи сообщений
Часть вторая по линиям связи (телеграф, телефон, радио, телевидение и т. д.). Исторически первый код, предназначенный для передачи сооб- щений, предложен изобретателем телеграфного аппарата Сэмю- элем Морзе. В этом коде каждому символу соответствует своя последовательность из кратковременных (точка) и длительных (тире) импульсов тока, разделяемых паузами (точка, тире, пау- за — три элемента для кодировки символов, код Морзе троич- ный). Другим широко используемым кодом в телеграфии явля- ется код Бодо. Для кодирования используются два элементарных сигнала — импульс и пауза. Символам сопоставляются кодо- вые слова одинаковой длины (из пяти сигналов). В коде Бодо, в отличие от кода Морзе, используется равномерное кодирова- ние. В коде Морзе наиболее часто используемым символам со- ответствуют кодовые слова меньшей длины (основополагающая идея в теории оптимального кодирования), поэтому для одно- значного распознавания (декодирования) и потребовался третий символ — пауза. Коды, использующие два различных элемен- тарных сигнала, называют двоичными. Договоримся обозначать эти сигналы символами 0 и 1. Фрэнсис Бэкон был первым, кто использовал для кодирования два символа. 2. Общая постановка рассматривае- мой задачи поясняется на рисунке. Бук- вами Aj, А2 обозначены два различных алфавита. Сообщение записывается с использованием алфавита Перевод (преобразование) этого сообщения (W) в сообщение, записанное в алфавите А2, есть кодирование. Обратный перевод (V) — декодирование. Основные требования к решению задачи W: оптимальность, помехоустойчивость и однозначность декоди- рования (преобразование W такое, что преобразование V одно- значно решает задачу перевода сообщения в исходный алфа- вит). Под оптимальностью понимается то, что закодированное сообщение должно быть как можно меньше (короче). Передача сообщения связана с затратами, при оптимальном кодировании затраты минимизируются. В любом канале передачи данных возможны искажения. Они приводят к ошибкам. Решение за- дачи, при котором ошибки и находятся, и, по возможности, устраняются, называется помехоустойчивым кодированием. И наконец, однозначность декодирования. В коде Морзе она до- стигается за счет введения символа «пауза», в коде Бодо — все символы кодируются одинаковыми по длине последовательно-
Процедуры и функции — элементы структуризации программ 185 стами. В последующих материалах для чтения рассматривают- ся схемы кодирования с А2={0, 1}, двоичное кодирование. 3. Ответим еще на один вопрос (повторим). Сколько различ- ных сообщений можно закодировать в щ двоичных разрядах? Если щ=1, то два — 0 и 1. В общем случае — 2т. Строкой из 0 и 1 мы кодировали целые числа, символы (кодировка ASCII). С помощью 0 и 1 кодируются все данные, обрабатываемые ЭВМ. В Computer Science способы кодировки и записи различной ин- формации в двоичном виде называют форматами. Они настоль- ко многочисленны, что их сложно перечислить: форматы графи- ческого вывода, форматы сжатия текстов, форматы для звуковых файлов, кинофайлов и т. д.
186 Часть вторая Занятие № 15. Вещественный тип данных План занятия □ вещественный тип данных; □ короткие программы иллюстрации работы с веществен- ным типом данных; □ экспериментальная работа с программами: решения урав- нения f(x)=0\ вычисления площади фигуры; вычисления элементарных функций с использованием представления этих функций в виде некоторых бесконечных сумм; □ выполнение самостоятельной работы. Вещественный тип данных. Числа записываются в виде мантиссы (дробной части числа), умноженной на порядок (сте- пень десяти, обозначается буквой Е). Существуют следующие типы вещественных чисел ( наиболее используемый — Real). 1 Тип Диапазон возможных значений Точность Формат | Real 2 9Е-39 1 7Е38 11-12 знаков 6 байт Single 1 5Е-45 3 4Е38 7-8 знаков 4 байта | Double 5.0Е-324 .1 7Е308 15-16 знаков 8 байт Extended 3 4Е-4932 1 1Е4932 19-20 знаков 10 байт Comp -9.2Е18 9 2Е18 19-20 знаков 8 байт Примечание Величины типа данных Сотр принимают целочисленные значе- ния, в известной степени этот тип является расширением типа Lon glnt до 19 разрядов. Допустимые арифметические операции: *+’ сложение, вычитание, умножение, деление и операции сравнения. На вещественном типе данных нет отношения порядка, поэто- му операцией сравнения *=’ следует пользоваться с большой осторожностью. Вещественные числа записываются □ путем отделения дробной части от целой с помощью точ- ки, например 127.3, 25.0, -16.003, 200.59, 0.54; □ с использованием символа Е, например 0.000009 — 9£-6, 0.62*104 - 0.62Д+4, -10.8 *1012--10.8Е12, 20*10’3 - 20Е-3.
Процедуры и функции — элементы структуризации программ 187 Перечислим стандартные функции работы с вещественными величинами: Sqr(X) Квадрат X | SqrtfX) Квадратный корень из X | Sm(X) Синус X, X в радианах j Cos(X) Косинус X, X в радианах Arctan(X) Арктангенс X, результат в радианах Exp(X) Число е возводится в степень X Ln(X) Вычисление натурального логарифма X | Pi Возвращает значение числа Pi | Int(X) Возвращает целую часть аргумента X | Frac(X) Возвращает дробную часть аргумента X | Trunc(X) X имеет тип Real, результат — целая часть X | Тгипс(6 8)=6, Тгипс(-7 9)=7 I Round(X) X имеет тип Real, результат — ближайшее к X целое число Round(5 в)=6, Round(5 5)=6 | Короткие программы. Первый пример связан со сравнени- ем двух вещественных чисел. Запустите эту простую програм- му. Program Му15__1; Var x:Real; у:Real; Begin х:~0.3;у:=С . 3; Wri teLn (х=у) ; ReadLn; End. Получен ожидаемый результат — значение True. Изменим описание переменной у на y:Smgle. Запуск программы не прой- дет, есть ошибка Error 116: Must be in 8087 mode to compile this.1 Суть в том, что выполнение арифметических операций с вещественными числами осуществляется по-другому, чем с це- лыми числами. Их реализация требует больших затрат процес- сорного времени. С этой целью созданы специальные быстро- действующие сопроцессоры для выполнения операций с числами вещественного типа. Только в случае, если на компьютере не установлен арифметический со- процессор и не включен эмулятор сопроцессора.
Часть вторая Директива {$N+}. Данная директива компилятора осуществляет переключение между двумя различными режимами генерации кода для вы- полнения операций с вещественными числами. В режиме {$N-}, а по умолчанию включен он, компилятор использует библиоте- ку подпрограмм для работы с 6-байтовыми вещественными числами, т. е. типом Real. В режиме {$N+} компилятор генери- рует программный код, обеспечивающий повышенную точность вычислений и доступ к четырем дополнительным веществен- ным типам данных. Итак, при использовании дополнительных вещественных ти- пов данных требуется записывать директиву в начало программы. Вставим ее и добавим еще два оператора. Текст программы имеет вид: {SN+} Program Му15_1т; Var x:Real; у:Single; Begin х:=0.3;у:=0.3; WriteLn (х=у) ; WriteLn(x); WriteLn(y); ReadLn; End. Результат сравнения есть False. Причина очевидна — для хра- нения значений х и у выделяется разное количество разрядов. Отметим еще один момент. При работе в режиме {$N+} стан- дартная процедура Write осуществляет вывод в формате Exten- ded, так же как и значения, возвращаемые стандартными фун- кциями, перечисленными выше по тексту. Сравните работу следующих фрагментов программ (sin(30°)=sin(0.5236...)=0.5) и проведите еще одно небольшое исследование. Задайте в разделе Const константы а и Ь. Затем, вместо оператора WriteLn(Pi) на- пишите WriteLn(Pi:a:b). Найдите зависимость между измене- нием значений а, Ь и результатами вывода числа Pi на экран. {$N-} Program Му15_2; Begin WriteLn (Sin (0.5236) ) ; WriteLn (Pi); End. и
Процедуры и функции — элементы структуризации программ 189 {$N+} Program Му15_2; Begin WriteLn(Sin(0.523 6)); WriteLn (Pi) ; End. Продолжим наши примеры. Известно, что (х/а)*а=х. Про- верим это утверждение с помощью следующей простой про- граммы. ($N+) Program Му15_3; Var х:Extended; i,cnt:Integer; Begin ent:=0 ; For i:=20000 To 30000 Do Begin x:=i/10000; x:=x*10000;If x=i Then Inc(cnt); End; WriteLn('Число совпадений ',cnt); WriteLn('Число несовпадений ',10001-cnt}; ReadLn; End. Результат. Число совпадений — 8209, число несовпаде- ний — 1792. Попытка вывести значения х и i при несовпадени- ях ничего не дает. Числа равные, а есть несовпадение, т. е. разли- чия в разрядах, которые не улавливаются при выводе значений переменных на экран. Они не улавливаются и другими способа- ми. Однако ограничимся этим, главное в том, что постараемся не использовать «лобовое» сравнение величин вещественного типа, а будем использовать для этих целей короткую функцию. Она приведена в следующей модификации программы. ($Ы+) Program Му15_3; Const eps=1.0E-10; Var x:Extended; i,cnt:Integer; Function Eq(x,y:Real):Boolean; Begin Eq:-Abs(x-y)<eps ; End; Begin cnt:=0;
190 Часть вторая For i:=20000 То 30000 Do Begin x:=i/10000; x:=x*10000; If Eq(x,i) Then Indent); End; WriteLn (' 'Число совпадений ',cnt); WriteLn ('Число несовпадений ',10001-cnt); ReadLn; End. Результат работы программы — 10001 совпадение. Экспериментальный раздел работы 1. Приближенное решение уравнения f(x)=O, где f(x) — задан- ная функция. Решить уравнение — значит найти такое значе- ние х*, при котором f(х*)=0. Поиск решения осуществляется на интервале [a,bJ, причем f(а)<0, a f(b)>0. Поиск такого ин- тервала — отдельный вопрос, мы его не рассматриваем. Для решения используем одну из фундаментальных идей инфор- матики — «разделяй и властвуй», в данном случае «метод де- ления пополам». Находим точку с — половину отрезка [а,Ь]. Если f(c)>0, то границу Ь изменяем на значение с, а если f(c)<0, то изменяем а. Процесс продолжаем, пока длина ин- тервала не будет меньше заданной точности. Пусть f(х)=хг-2, т. е. мы попытаемся найти значение квад- ратного корня из 2. Решение. {$N+) Program Му15_5; Const epsd . ОЕ-З ; Var a,b,c -.Real; Function Eq(x,у:Real):Boolean; ('Функция рассмотрена выше. *}
Процедуры и функции — элементы структуризации программ 191 Function F (х:Real):Real; Begin F:=Sqr (x)-2; End; Begin WriteLn('Введите интервал. '); ReadLn(a,b); If F(a)*F(b)>0 Then WriteLn('Ha этом интервале мы не можем найти решение уравнения.' ) Else Begin While Not Eq(a,b) Do Begin c:=(a+b)/2; If F(cl>0 Then b:=c Else a:=c; End; WriteLn('Результат ’ ,a); End; ReadLn; End. Обратите внимание на то, что вычисление f(x) оформлено как отдельная функция. Использование программы для других данных требует лишь ее изменения. Запуск программы при различных значениях а и Ь Вас не очень обрадует. Получаются значения 1.4140625 или 1.41357421875 (для различных интер- валов а и Ь), а фактическое значение равно 1.414213... Измени- те значение eps на 1.0Е-6. Точность вычислений имеет большое значение при работе с типом данных Real. Исследуйте еще ряд функций с помощью приведенной программы: □ х2-6х+5=0 □ x-cos(x)=0 □ х-1п(х)-2=0 □ 2х3—9х2-60х+1=0 Возникает вопрос, как определять интервал [а,&], удовлетво- ряющий условиям задачи? А начните с таблицы, вначале со- ставляйте её вручную (иапример, для первой функции): X I 0 I 2 I 3 I 6 I Знаках) | + | - I - | + | После ряда попыток Вы вспомните, что разумная лень — двигатель прогресса, так это называется, и автоматизируете процесс составления таблицы, т. е. добавите к программе еще одну процедуру.
Часть вторая 2. Пусть f(x) — непрерывная положительная функция на отрез- ке [a,t>] (a<t>). Вычислим площадь фигуры, ограниченную гра- фиком функции f(x), осью х и прямыми х=а и x=t>. Для этого разобьём отрезок [а,б] на п равных отрезков одинаковой дли- ны Ах=(Ь-а)/п-. a=x(0)<х(1 )<...<х(i)<x(i+l )< <х(п)=Ъ. На каждом из отрезков [x(i),x(i+l Д как на основании, построим прямоугольник с высотой f(x(i)). Площадь прямоугольников мы умеем считать. Сумма площадей всех прямоугольников даст приближенное значение площади фигуры. Общая форму- ла: 8прибл=(Ь~а)/п* (f(x(O)+f(x( 1))+...+f(x(n-l)). Естест- венно, что чем «мельче» будет разбиение, тем точнее мы под- считаем площадь фигуры. А сейчас вспомните один из тезисов нашей работы — «все подвергай сомнению, все проверяй». Где неточность в вышеизложенном материале? Для отладки программного решения возьмем простую функ- цию — f(x )=х2 на интервале от 1 до 2. Площадь фигуры равна 2.333... ($N+;) Program Му15_6; Const eps=1.0E-3; Var a,b,s,dx:Real; i,n:Integer; Function Eq(x,y:Real):Boolean;(*Функция рассмотрена выше. *} Function F (x: Real) .-Real; Begin F:=Sqr(x); End; Begin WriteLn('Введите интервал и число частей, на которые он разбивается'); ReadLn (a,b,n);
Процедуры и функции — элементы структуризации программ 193 х:=а; dx:=(b-a)/п; s:=0; For i:=l То п-1 Do Begin s:=s+F(x) ; x:~x+dx; End; WriteLn ('Результат ', (b-a)/n*s); ReadLn; End. Запускаем программу при фиксированных границах интерва- ла и различных значениях п. Результаты удручают. При п=6 он равен 1.5277..., при тг=50 — 2.224992..., при л=100 — 2.78749..., при п=300 — 2.315046... Давайте изменим решение. Будем сравнивать два соседних приближенных значения площади для различных п. Если их разность больше заданной точности вы- числения, то увеличим значение п и снова подсчитаем пло- щадь. Начальное значение п в программе взято 10, шаг измене- ния 100. Естественно, эти параметры программы вынесены в раздел констант, так же как и точность вычисления. Предыду- щая логика оформлена как функция вычисления площади. Мы не смешиваем задачи. Эту часть нам известно, как решать. {$Ы+;} Pro gram Myl 5_ 6m ; Const eps=l.0E-3; nb=10;ns=l00; Var a,b,wn,ws:Real ; i,n:Integer; Function Eq(x,у:Real):Boolean; ('Функция рассмотрена выше. *} Function F(x:Real):Real; Function Sq(n:Integer):Real; Var x,dx,s:Real; Begin x:=a; dx : = (b-a) /n ; s : =0 ; For i:=l To n—1 Do Begin s:=s+F(x) ; x:-x+dx; End; Sq: = (b-a) /n*s ; End; Begin WriteLn(' Введите границы интервала'); ReadLn (a,b) ; n:=nb;wn:=Sq(n); Repeat ws:=m; n:=n+ns; wn:=Sq (n) ; Until Eq (ws,wn) ; WriteLn('Результат: количество частей и значение площади',n,wn); ReadLn; End. 7—452
194 Часть вторая После запуска получаем, что при п=810 достигается требуе- мая точность вычислений, результат — 2.32654955.... Однако в уме мы подсчитали точнее. Изменим точность на 1.0Е-6. Ваш компьютер задумается на некоторое время, а результат «=23310 и площадь — 2.33309739... Для самоконтроля Вам предлагает- ся вычислить площадь для следующих функций: f(x)=l/(l+x), [0,1]; f(x)=l/x, [1,3]; f(x)=sin(x), [0,Pi/2]. 3. Для вычисления элементарных функций в математике ши- роко распространено представление этих функций в виде не- которых бесконечных сумм. Не вдаваясь в обоснование таких представлений, приведем некоторые из них: ех=1+х+х2/2 '+х3/3! + +х"/п' + sin (х) =х-х3/3 1+х3/5!-х7/7! + + (-1 )nx2n+1 / <2п+1) ’ + . cos (х) =1 -х2 /2 I +x‘s/4 ! -х6/6' + -t-(-l) пх2п/ (2п) ! + In (1+х) =х-х2/2+х3/З-х^/4 + + (~1)п+1хп/п+ (~1<х<1) В каждом из разложений точность представления функции будет, вообще говоря, тем выше, чем больше взято слагаемых в сумме. Причем значения самих слагаемых с ростом п стремят- ся к нулю. Для вычисления значений функции с некоторой за- данной точностью eps поступают следующим образом. Вычисля- ют и суммируют слагаемые до тех пор, пока очередное слагаемое не станет по абсолютной величине меньше eps или абсолютное значение разности между соседними слагаемыми не станет ме- ньше eps (что лучше?). Полученную сумму и принимают за приближенное значение функции. Продемонстрируем этот ме- тод на примере вычисления функции sin(x). 1$Х+) Program Му15_7; Const eps=l.ОЕ-3; Var х,sn,ss:Real; p,n:Integer; Function Eq (x,у:Real):Boolean ;!''Функция рассмотрена выше.*} Function F(n:Integer;Var x:Real):Real; Var i:Integer; s:Longlnt; Begin s;=l; For i:=2 To n Do s:=s*i; x:=x*Sqr(x); E;=x/s; End; Begin
Процедуры и функции — элементы структуризации программ 195 WriteLn('Введите значение х '); ReadLn (х) ; ss : =0;sn : =х;п : =1 ;р: =1 ; ( *р — для реализации чередования знака слагаемого. *) Repeat ss:=sn;(^Предыдущее значение слагаемого.*} п: =п+2;р: =-1 *р; sn :=ss+p*F (п,х) ; ( * Новое значение слагаемого.*} Until Eq (ss, sn) Or (n>=12) ; (*Второе условие требуется по простой причине — факториал числа мы умеем вычислять только при этих значениях п. *} WriteLn ('Результат ', sn) ; ReadLn; End. Известно, что sm(30°)=l/2, sm(45o) = Sgrt(2)/2, 30°=0,5236... радиан, 45°=0,7854... радиан. Проверьте работоспособность про- граммы. При какой точности вычисления нам не будет хватать наших знаний по вычислению факториала числа. Измените программу для вычисления других, приведенных выше функций. Исследуйте решения. Задания для самостоятельной работы 1. Квадратное уравнение ах2+Ьх+с=О задается своими коэффи- циентами. Его корни находятся по формуле Xj=(-b+Sqrt (b2-4*a*c))/(2*a) и x2==(-b~Sqrt(b2-4*a*c))/(2*a). Написать программу нахождения корней квадратного уравнения. 2. Написать программы вычисления следующих выражений: □ !/ = (1-|)(1-4л >2 □ j/ = cos(l + cos(2+...+ cos(39 + cos40)...)) □ у = sin х + sin sin x+...+ sin sin... sin xfn раз) □ у = sin x + sin x2 +...+ sin xn □ у = sinx + sin2 x+...+ sin" x „ cosl cosl + cos2 cos 1 + 7C + cosn □ у =--------------К---------------- sinl sinl + sin2 sinl+A + sinn □ !/ = |x| + x‘ □ j/ = |x| + 4-x3 -7 x2 □ у = |x-2| + 3 • хЛ □ j/ = 6b2 + |b-3|3 -15 □ у =(3 • Xя + 18 • x2) • x + 12 • x2 - 5 □ у = (d + c + b) • e - 5 • k - 1 г
196 Часть вторая 3. Вычислить приближенное значение бесконечной суммы: □ -L + -L + J- + ... 1-2 2 3 3-4 □ -L + J- + -L + ... 1-3 2-4 3 5 123 23-4 345 4. Написать программы вычисления 1_1 1_ 1 1 2 + 3 + 9999 10000 следующими четырьмя способами: □ последовательно слева направо; □ последовательно слева направо вычисляются 1 + 1 + + 1 и 1 + 1 + + 1 + 3 + ” + 9999 И 2 + 4 + + 10000’ затем второе значение вычитается из первого; □ последовательно справа налево; □ последовательно справа налево вычисляются суммы (как во втором случае), а затем выполняется вычитание. Сравните результаты и найдите их разумное объяснение. 5. Рассмотрим бесконечную последовательность чисел уг у2, у3, ... , образованную по следующему закону: х + т -1 = —((т - 1)У1Ч + —(i=2, 3, ...), где х — данное дейст- т У?-1 вительное число, т — натуральное число. Эта последовате- льность позволяет получить сколь угодно точные приближе- ния числа \/х(х > 0). Составить программу для вычисления значения "4х с заданной точностью eps. 6. Дано п вещественных чисел. Написать программы: □ вычисления разности между максимальным и минима- льным элементами; □ определения количества элементов, которые больше сво- их «соседей», то есть предыдущего и последующего чи- сел;
Процедуры и функции — элементы структуризации программ 197 □ определения минимального значения разности двух про- извольных чисел. 7. Представим обыкновенную дробь — (т < п) в виде цепной дроби — = Например, — = Процесс преобразования заключается в следующем: 5/7=1/ (7/5)=1/(1+2/5)=1/(1+1/(5/2))=1/(1 + 1/(2+1/2)). Даны числа а1( а2..ак, описывающие цепную дробь. Написать про- грамму вычисления значения цепной дроби. 8. Написать программу вычисления выражения (цепной дроби) 3 + 9. Перевод десятичных дробей в двоичное представление вы- полняется несколько иначе, чем целых чисел. Пусть есть дробь 0.37510. Схема перевода: 0.375*2 = 0.75, 0.75*2 = 1.5, 0.5*2 = 1.0, двоичное представление дроби 0.37510 имеет вид: 0.0112. Жирным шрифтом выделены те цифры, которые идут в двоичное представление дроби. После каждого умножения на 2 мы получаем очередную цифру в двоичной записи дроби (почему?). Всегда ли процесс умножения конечен? На какой итерации следует заканчивать перевод, если Вам дано m раз- рядов для представления дроби? Написать программу перевода действительного числа а (0<а<1) в двоичную систему счисления. Материал для чтения 1. Продолжим знакомство с кодированием. Рассмотрим не- сколько схем построения двоичных кодов для заданного мно- жества сообщений. Пусть имеется пять сообщений a, b, с, d, е, они передаются по каналу связи с вероятностями 0.3, 0.24, 0.21, 0.15 и 0.1. Закодируем их по методу американского мате- матика Р. Фано.
198 Часть вторая Сообщение Вероятность 1-й шаг 2-й шаг 3-й шаг Код””! а 03 0 00 - 00 ь 0 24 0 01 - 01 0 21 1 10 - 10 d 0 15 1 11 110 110 е 0 1 1 111 111 Что мы сделали? На первом шаге произвели разбивку всего множества сообщений на две группы так, чтобы суммарные ве- роятности сообщений в группах были как можно ближе друг к другу. Первая группа — сообщения а и Ь, вторая группа — со- общения с, d, е. Сообщениям первой группы приписали 0, вто- рой — 1. Процесс продолжается в каждой группе до тех пор, пока в очередной группе не останется одно сообщение. Почему именно такой принцип разбивки на группы? Вспомните игру «Бар — Кохба». Разряд, получаемый на первом шаге, «несет» в себе максимальное количество информации. Точно так же на втором и третьем шагах. Итак, очередной раз мы встречаемся с фундаментальной идеей информатики — деления пополам («раз- деляй и властвуй»). При равномерном кодировании для пяти сообщений требует- ся три разряда, т. е. длина кода равна трем. Подсчитаем сред- нюю длину кода Р. Фано — Lcp =11*р1+12*р2+13*р3+14*р4+15*р5= 2*0.3 + 2*0.24 +2*0.21 + 3*0.15 +3*0.1=2.25. Итак, получен выигрыш. Структура данных, изображенная на рисунке, называется двоичным деревом. Черными кружками выделены узлы дере- ва. Отрезки, соединяющие узлы, называют ребрами. Ребрам приписаны цифры 0 и 1 так, как сделано в схеме кодирова- ния. Узлы, из которых не выходят ребра (вниз), называют ли-
Процедуры и функции — элементы структуризации программ 199 стьями. Итак, каждому листу соответствует сообщение, а путь к нему от корня (верхнего узла), при выписывании соответству- ющих цифр, дает код этого сообщения. Обратим внимание на одну особенность кодирования. Кодам соответствуют листья дере- ва, т.е. ни один код не является началом другого. Такие коды на- зываются префиксными. Код Р. Фано — префиксный код. Про- верьте. Если бы некоторому коду соответствовала внутренняя вершина дерева, то это условие не выполнялось бы. Пусть есть последовательность кодовых слов, записанных подряд 010111110111111101011000. Попробуем декодировать это послание. Просматриваем последовательность слева напра- во и пытаемся выделить кодовое слово. 0 не является кодом, 01 — является, выделяем и отбрасываем. Продолжаем процесс: 01I01 I 111 I10 I 111 I 111 110I10I110I00 (символ I используем как разделитель). Сообщение декодировано — bbeceeccda. 2. Рассмотрим схему кодирования по К. Шеннону. Пусть есть п сообщения ар а2, ... ап, которые появляются с вероятно- стями рр р2, ... рп. Вероятности сообщений даются в порядке возрастания — pt < р2 < р3 < ... < рп. Находятся частичные сум- мы вида Sx= Pj + р2 + ...+ рх для всех i=l, 2, ...п. Затем опреде- ляются значения пр такие, что 2'nl < р; < 2-ш+1 . Частичные суммы SL представляют в виде двоичной дроби O.d1d2d3d4... Пер- вые пг цифр дроби дают кодовое слово для сообщения аг Значе- ние Sn равно 1 (полная группа сообщений, какое-нибудь да при- сутствует в передаче). Удобнее взять в схеме Шеннона Sn=0.999... Прежде чем рассмотрим пример, напомним некоторые фак- ты из ранее рассмотренного материала. Значения отрицатель- ных степеней двойки равны: 2-1=0.5, 2-2=0.25, 2-3=0.125, 2-4= =0.0625, 2"5= 0.03125. Тогда для дроби 0.1 выполняются нера- венства 2“4 <0.1 < 2-3 и значение п; равно 4. Правило перевода дробей в двоичное представление сводится к последовательно- му умножению на 2 и выделению целых частей результатов ум- ножения. Переведем 0.110: 0.1*2=0.2, 0.2*2=0.4, 0.4*2=0.8, 0.8*2=1.6, 0.6*2=1.2, 0.2*2=0.4. Двоичное представление — 0.000110...2. Постройте двоичное дерево для полученного кода (код пре- фиксный). Отбрасывание лишних ребер приводит к усеченному коду. Подсчитаем среднюю длину — Lcp.=2*0.1 +3*0.15 +3*0.21 +2*0.24 + 2*0.3=2.36. Этот результат лучше, чем при равно- мерном кодировании сообщений.
200 Часть вторая Сообщение Вероятность 1 S, П| О X I CQ О. Код >х н 8 > ООО а 0 1 4 0001 00 0 10 0 00011 ь 0 15 3 010 010 0 25 0 01000 с 0 21 3 011 011 0.46 00111 d 0 24 3 101 10 0 70 0 1010 е 0 3 2 11 11 1 00(0 99) 0 1111 3. Рассмотрим еще одну схему построения кода. Ее предло- жил американский математик Д. Хаффмен (1953 г.). Получае- мый в результате построения код является оптимальным, т. е. не существует кодов с меньшим значением средней длины. Пусть сообщения расположены в порядке убывания вероят- ностей их появления. Построение кода состоит из двух этапов. На первом сообщения с наименьшими вероятностями объеди- няются в одно. Получается новое множество сообщений. Осу- ществляется его перегруппировка так, чтобы опять их вероятно- сти находились в порядке убывания. Процесс, который называют сжатием, продолжается до тех пор, пока не останется два сооб- щения. Первому из оставшихся сообщений приписываем 0, второму — 1, и начинается процесс расщепления. Его суть. Если сообщение получено объединением двух в одно, то оно расщепляется на два, к текущему коду первого приписывается 0, второго — 1. Процесс продолжается до тех пор, пока мы не дойдем до первоначального множества сообщений. В результа- те будет получен код исходного множества сообщений. Для примера, описанного в предыдущих пунктах материала для чтения, схема построения кода Хаффмена приведена в таблице. Проверьте её. Убедитесь, что построенный код является пре- фиксным, а его средняя длина равна 2.25.
Q °- ° °- и Сообщение О О сл го к со Вероятность о го о го о со Процесс сжатия ° ° ° 1 1 сл ° 1 1 - Процесс расщепления о § - - о о о 2 о о - О о Процедуры и функ! g
202 Часть вторая Занятие № 16. Текстовые файлы План занятия: □ файловый тип данных; □ операторы работы с файловым типом данных; □ короткие программы иллюстрации работы с текстовыми файлами; □ экспериментальная работа с программами: подсчета час- тоты записи целых чисел в файле; правильности расста- новки скобок в тексте программы; подсчета количества строк в тексте; □ выполнение самостоятельной работы. Файловый тип данных. Данные, размещаемые программой в памяти компьютера, исчезают при выключении питания. Для долговременного хранения данных используются внешние но- сители, в частности магнитные диски. Единицей хранения ин- формации на диске служит файл. Файл имеет имя, но это имя не Турбо Паскаля, а операционной системы, под управлением которой работает Турбо Паскаль. С другой стороны, этот файл с точки зрения Турбо Паскаля является обычной переменной, иначе бы с ним нельзя было работать, и как любая переменная, он имеет имя. Это другое имя, это имя Турбо Паскаля, рассмат- ривающего файл как переменную. Естественно, чтобы не было «мешанины», должны быть средства установки соответствия между именами. Еще раз. В Турбо Паскале мы работаем с од- ним именем. Операционная система, фактически осуществляю- щая чтение и запись на диск, работает с другим именем. Следу- ющий основополагающий момент заключается в том, что с каждым файлом в конкретный момент времени можно рабо- тать или в режиме чтения, или в режиме записи. Одновременно нельзя, т. е. мы или читаем, или записываем — другого не дано. Для того чтобы, например, записать в файл, открытый для чтения, необходимо закончить работу с файлом в режиме чтения, а затем начать работу с этим файлом в режиме записи. В рамках данного занятия мы ограничимся работой с текстовы- ми или последовательными файлами. Текстовые файлы хранят информацию в виде последовательности символов. Символы со- ставляют строки произвольной длины. В конце каждой строки записываются два особых символа: #13 #10 (возврат каретки, перевод строки), которые отделяют строку от следующей. С текстовыми файлами, однако, мы работали, начиная с первого занятия, правда, не говоря об этом. Есть стандартные файлы ввода и вывода. Они связаны с клавиатурой и дисплеем. Имена
Процедуры и функции — элементы структуризации программ 203 этих файлов в системе Турбо Паскаль (файловые переменные) Input и Output, их имя в операционной системе Dos — Con (консоль). Таким образом, при записи операторов Read или Write фактически осуществлялось чтение из стандартного фай- ла ввода с клавиатуры или запись в стандартный файл вывода, связанный с дисплеем. Естественный вопрос. Почти все время осуществлялся ввод и вывод цифровой информации. Оказыва- ется, при чтении и записи чисел их преобразование в строко- вый тип и наоборот выполняется автоматически при работе с текстовыми файлами, a Input и Output — текстовые файлы. Операторы работы с файловым типом данных. В про- грамме на Турбо Паскале текстовый файл представлен фай- ловой переменной типа Text: Var <имя файловой переменной>: Text;. Связь файловой переменной с дисковым именем файла осу- ществляется с помощью оператора Assignf имя файловой пере- менной, дисковое имя файла). Второе имя больше нигде в про- грамме не появляется. Открытие файла для чтения выполняется оператором Reset ( <файловая переменная>), для записи — ReWrlte(<фaйлoвaя переменная> ), для пополнения — Append (файловая переменная). Окончание работы с файлом фиксиру- ется с помощью оператора Close(<файловая переменная>). Пример оформления работы с текстовым файлом: Var f:Text; s:String; Begin Assign(f,'a:\data.txt');{*Файл с именем data.txt находится на диске с логическим именем а. ★) Reset {f);{*Файл открывается для чтения.★) ReadLn(f,s);{*Читаем одну строку из файла.★) Close (f) ; End. Текстовый файл для оператора ReadLn такой же источник данных, как и клавиатура. Однако в этом случае данные следу- ют друг за другом не во времени, как при вводе с клавиатуры, а в измерении файла. Если файл представить в виде некой ли- нейки, то единственное разрешенное движение от левого конца линейки вправо и, чтобы попасть на 10-й сантиметр, необходи- мо обязательно пройти через все предыдущие сантиметры. Как во времени, так и в измерении файла можно перемещаться то- лько в одну сторону. Допускается лишь последовательное чте-
204 Часть вторая ние из файла. Мы не можем в начале прочитать 100-ю строку, а затем 10-ю. Последовательный файл, как лента магнитофона. При этом, конечно, между элементами данных файла обязаны быть разделители, ибо данные файла могут быть разной длины и файл обязан заканчиваться каким-то признаком конца фай- ла. А как иначе закончить чтение? В Турбо Паскале есть специ- альная функция Eof (<файловая переменная>), которая воз- вращает значение True, если достигнут конец файла, и False в противном случае. Короткие примеры. Начнем со сверхпростого примера. Вводим данные (строки) из файла и выводим их на экран. Program Му16_1; Var f:Text; s:String; Begin Assign(f,'Input. Txt') ; Reset(f); While Not Eof(f) Do Begin ReadLn(f,s); Wri t eLn (s) ; End; Close (f) ; End. Запуск программы даст ошибку Error 2: File not found. Фай- ла с именем Input.Txt нет на диске, его необходимо создать. От- крываем новый файл, записываем строки А ВЬ Ссс Dddd Еееее Ffffff Ggggggg и сохраняем файл с именем Input.Txt. После запуска мы видим на экране правильный результат, но только при одном условии. Ваша программа и файл Input.Txt находятся на одном и том же диске и в одной директории (каталоге). Измените текущий ка- талог — режим File/Change Dir — и выберите другой каталог. Запустите программу. Опять знакомая ошибка — Error 2: File not found. Ваша программа находится в одном каталоге, а файл Input.Txt в другом. Перепишите файл Input.Txt в тот каталог, где находится Ваша программа. Запустите. Результат нормальный.
Процедуры и функции — элементы структуризации программ 205 Измените программу. Вместо ReadLn(f,s) напишите Read(f.s). Результат плачевный. Приходится использовать Ctrl+Break, что- бы остановить программу, она «зациклилась». Причина — не может найти признак конца файла. Откройте окно Debug/Watch, введите имя переменной s и выполните программу в пошаговом режиме. Первую строку мы благополучно вводим и выводим, а затем идут пустые строки. Попробуйте дать объяснение. Обра- тите внимание на неудобство работы. Приходится все время ис- пользовать F6 для того, чтобы перейти от одного окна к друго- му. Вы забыли материал занятия №3, работу с окнами. Измените размеры окон и переместите их так, чтобы они не накладыва- лись друг на друга. Структура вашего экрана должна иметь вид: окно Input.Txt окно My16_1.Pas окно Watches А можно ли читать посимвольно? Изменим программу. Program Му] 61 т, Var f:Text; ch:Char; Begin Assign(f,'Input.Txt'); Reset (f) ; While Not Eof(f) Do Begin Read (f, ch) ; Write(ch); End; Close (f) ; End.
206 Часть вторая Результат правильный. Но если Вы измените Read(f,ch); Writefch); на Read(f.s); Write(s); где s имеет тип String, то ре- зультат опять отрицательный — «зацикливание». Осознайте разницу. Найдите еще варианты, при которых Ваша программа будет «зацикливаться». Проведите очередное изменение программы. Ее вид: Program Му16_1тт; Var f:Text; ch:Char; Begin Assign (Input,'Input.Txt'); Reset (Input); While Not Eof Do Begin Read (ch); Write (ch) ; End; Close (Input); End. Запуск программы дает положительный результат. Что мы сделали? Переопределили стандартный ввод с клавиатуры на ввод из нашего файла на диске. Файловую переменную в этом случае можно не записывать в операторах Read и Write. Доба- вьте после оператора Close(Input) операторы ReadLn(s); Wri- teLn(s), где s имеет строковый тип данных. Запуск программы приводит к ошибке Error 104: File not open for input (файл не открыт для ввода). Вставим после Close(Input) оператор Re- set(Input). Программа заработала. Однако вместо ввода с кла- виатуры строки s мы имеем ввод первой строки из файла In- put.Txt. Как вернуться к вводу с клавиатуры? Продолжим рассмотрение примеров. Запустите программу. Program Myl6_2; Uses Crt; Const MaxN=100; Type MyArray=Array[1..MaxN] Of Integer; Var A:МуАггау; i,j:Integer; Begin ClrScr; Assign(Input,'Input. Txt') ; Reset (Input); i:=0; While Not Eof Do Begin
Процедуры и функции — элементы структуризации программ 207 Inc (1) ; Read (А[1]) ; End; Close(Input); For j:=l To i Do Write (A [ j ] : 2) ; End. Результат традиционный — ошибка Error 106: Invalid пите nc format (неправильный числовой формат). Мы вводим в циф- ровой массив А, система преобразует символ в число, но символ не соответствует числу и как следствие — ошибка. Изменим файл Input.Txt на 1 2 2 3 3 3 5 5 5 5 5 Программа работает правильно. В массиве А эти самые чис- ла. Изменим файл Input.Txt. 1 2 22 3 3 333 5555 55555 Все почти правильно. И последние записи в строках прави- льно преобразовались в числа, правда, кроме 55555. Вместо этого числа выводится нечто странное -9981. Если Вы хорошо усвоили материал предыдущих занятий, то суть ошибки оче- видна. Если нет, то перед программой вставьте {$R+} и проана- лизируйте ошибку. Вернемся к посимвольному вводу из файла. Program Му16_2т; Uses Crt; Const MaxN=100; Type MyArray-Array[1..MaxN] Of Integer; Var A: MyArray; i,j,k:Integer; ch:Char; Begin ClrScr; Assign(Input,’Input.Txt'); Reset (Input) ; i: =0 ;
208 Часть вторая While Not Eof Do Begin Read(ch); {Vai (ch,g ,k);) {If k=0 Then Begin) Inc(i); A[i]:=Ord(ch)-Ord('0') ; {End;I End; Close(Input); For j : =1 To i Do Write (A [ j ] : 6) ; End. Результат — странная последовательность: 1 -35 -38 2 -16 2 2 -35 -38 3 ..но с определенной закономерностью. Объяс- ните ее. Уберите фигурные скобки и вновь запустите програм- му. Результат: 122233333... Измените программу так, что- бы результат все же был первоначальным 1 2 22 3 3 333 ...., но ввод осуществлялся посимвольно. А можно ли таким же обра- зом вводить вещественные числа? В программе Му16_2 изме- ните Type MyArray=Array[l..MaxN] Of Real; и проведите ана- логичные эксперименты, естественно, изменив при этом и файл Input.Txt. В следующем примере показано переопределение выходного файла. После запуска программы на экране монитора ничего нет. Необходимо открыть файл Output.Txt. Разместите на экра- не все три файла Му16_3, Input.Txt, Output.Txt (после первого прогона программы на диске в текущем каталоге будет создан выходной файл) так, чтобы они были одновременно обозримы. Измените несколько раз файл Input.Txt, при этом запуская программу. Program Myl6_3; Uses Crt; Var ch:Char; Begin ClrScr; Assign(Input,'Input.Txt'); Reset (Input); Assign (Output,'Output. Txt’); ReWrite (Output); While Not Eof Do Begin Read (ch); Write(ch) End; Close (Input); Close(Output); End. После строки с операторами Close (Input); Close( Output); вста- вьте следующие операторы.
Процедуры и функции — элементы структуризации программ 209 Assign(Input,'Con'); Assign(Output,'Con'); Reset (Input); Rewrite (Output); ReadLn (s) ; Wri teLn (s) ; Если программа будет что-то ждать, то наберите произволь- ный текст на клавиатуре и нажмите клавишу Enter. Мы научи- лись с Вами возвращаться к стандартному вводу и выводу дан- ных. Экспериментальный раздел работы 1. Известно, что в файле записаны целые числа, например из диапазона от -100 до 100. Необходимо подсчитать, сколько раз каждое число встречается в файле. Для решения задачи требуется создать файл с целыми чис- лами. Попробуем это сделать с помощью следующей програм- мы. Program Му16_4 ; Const MaxN=10000; а=100; b=200; с=15; Var 1,д,к:Integer; Begin Randomize; Assign (Output,'Number.Txt'); ReWrite(Output); l;=0; k:=0; While KMaxN Do Begin j:=Integer(Random(b))-a; Inc (i) ;Inc (k) ; Write (j) ;Write (' '); If k=c Then Begin WriteLn;k:=0;End; End; Close (Output); End. Она работает. Файл Nu.mber.Txt создается в текущем катало- ге. Он содержит MaxN целых чисел, разбитых на строки по с чисел в каждой строке. Можно ли записать в файл 100000 чи- сел? Простое изменение константы MaxN приводит к зацикли- ванию. Причина традиционная, мы забыли изменить тип дан- ных у переменной i на Longlnt. После каждого числа в файл
210 Часть вторая записывается символ пробела и после с чисел осуществляется переход на новую строку. Обязательно ли сие? Изменим про- грамму, возьмите в фигурные скобки текст {Writef ’)• If k=c Then Begin WriteLn;k:=O;End;} программного кода. Программа радует нас новыми сообщениями, они выдаются на стадии вы- полнения программы и оформлены несколько иначе, чем про- стые синтаксические ошибки (обратите на этот факт внима- ние). Первое сообщение: Error. Line too long, truncated (строка очень большая, она округляется); второе сообщение, но уже не как ошибка, а как предупреждение: Warning. Error encountered reading file NUMBER.TXT (ожидается ошибка при чтении фай- ла). Вернемся к исходному тексту программы, а в качестве упражнения попробуйте сформировать файл, у которого и ко- личество чисел в строке формируется случайным образом. Вторая часть задачи реализуется с помощью следующей про- граммы. Program Му16_5; Uses Crt; Const MaxN-100;c=20; Type MyArray=Array[-MaxN..MaxN] Of Integer; Var A:MyArray; i,j:Integer; Begin ClrScr; FillChar(A,SizeOf(A),0);{^Подсчитываем, сколько раз каждое число встречается в файле. Число используется как индекс к элементу массива А. Обратите на этот факт особое внимание. Это «принцип косвенной адресации» — одна из ключевых идей информатики, реализован как на уровне аппаратуры компьютера, так и в любых системах программирования. *) Assign(Input,'Number.Txt'); Reset (Input); While Not Eof Do Begin Read (i) ; Inc (A [i ] ) ;End; Close (Input); Assign (Output,'Count.Txt');{^Записываем результат в файл Count.Txt, не разбивая на строки. *) ReWrite(Output); For i : =-MaxN To MaxN Do Wri te(A[i] ,' ') ; Close(Output);
Процедуры и функции — элементы структуризации программ 211 Assign(Input,'Count.Txt');{‘Открываем файл Count.Txt для чтения.*) Reset(Input); Assign(Output,'Con')/{‘Результат выводим на экран. *) ReWrite(Output); j:=0; While Not Eof Do Begin Read (i) ;Inc (j) ; Wri te (i, ' ') ; If j=c Then Begin WriteLn ; j : =0 ;End; { ‘Разбиваем на строки, иначе трудно просматривать.*) End; End. Методическое замечание На экране должны быть обозримы все файлы: с первой и второй программами; Number.Txt и Count.Txt. В программе, хотя она и работает, содержится явная неточ- ность. Устраните ее. Вспомните, что Ваши программы должны работать при всех допустимых исходных данных. Предполо- жим, что датчик случайных чисел дал сбой и сгенерировал 10000 раз одно число. Что произойдет в этом случае? Подсчитать, сколько раз встречается каждый символ в соот- ветствии с кодировкой ASCII в некотором текстовом файле. Необходимо изменить решение задачи так, чтобы она рабо- тала и с вещественными числами. Если создание файла Num- ber.Txt не вызывает затруднений, то логика подсчета частоты повторяемости каждого числа «расползается». Использовать вещественное число в качестве индекса нельзя, косвенная адре- сация не работает. Требуется брать ближайшее целое число, но это уже другая задача. В этом случае вещественные числа из единичного интервала представлены одним целым числом. А если уменьшить интервал? Попробуйте исследовать эту пробле- му. Создается файл с не очень большим количеством слов. Затем формируется массив из этих слов. Случайным образом из мас- сива выбирается слово и записывается в другой файл, это дей- ствие выполняется 10000 раз. После того, как файл сформиро- ван, Вам необходимо решить аналогичную задачу — подсчитать, сколько раз встречается каждое слово и результат записать в третий файл.
212 Часть вторая 2. Дан текстовый файл, содержащий программу на языке Турбо Паскаль. Проверить правильность расстановки скобок (круг- лых, квадратных, фигурных) в тексте этой программы. Приведем простые примеры для пояснения сути задачи: О (0) — скобки в строке расставлены правильно; □ {(] } — скобки в строке расставлены неправильно; □ ({[}]) — скобки в строке расставлены неправильно. Между скобками допустима запись любых символов. Итак, если встречается одна из закрывающих скобок, то последняя открывающая должна быть такого же типа. Это основной кри- терий проверки правильности расстановки скобок в тексте про- граммы. Создадим простую работающую программу для про- верки работоспособности предполагаемого решения задачи. Program Myl6_sk; Begin WriteLn ('{*9 ((()))[[[[]]]]*}'); End. Далее выясним, как отличаются коды (по ASCII) рассматри- ваемых в задаче скобок. Этот факт устанавливается с помощью следующей простой программы. Program Му16_р; Uses Crt; Begin ClrScr; WriteLn(Ord(' (') ); WriteLn (Ord(')'));{*0твет: 40, 41.*} WriteLn(Ord('[')); WriteLn (Ord(']' ) ) ; (*Ответ: 91, 93.*} WriteLn(Ord('{')); WriteLn (Ord('}'));{*0твет: 123, 125.★} ReadLn; End. Итак, разность кодов для соответствующих друг другу ско- бок не превышает значения 2. Следующий момент. Хранить весь текст из файла в памяти нецелесообразно, он может быть очень большим, да это и не требуется в задаче. Необходимо хра- нить только открывающие скобки. Определим для этой цели массив с типом элементов Char и предположим, что уровень вложенности скобок не превышает 100. Если очередной считан- ный из файла символ — открывающая скобка, то она записы- вается в массив. При обнаружении закрывающей скобки срав-
Процедуры и функции — элементы структуризации программ 213 нивается её код с кодом последней скобки, записанной в массив. Если значение разности больше 2, то фиксируется ошибка в расстановке скобок, иначе скобка удаляется из массива, и про- смотр текста продолжается. ($R+) Program Му16_6; Const MaxN=100; Type MyArray=Array[1..MaxN] Of Char; Var A:MyArray; ch:Char; yk:Integer; ff -.Boolean ; Begin FillChar(A,SizeOf(A),0);yk:=0;ff:=True; Assign (Input,'Mil 6_SK. PAS'); Reset(Input); While (Not Eof) And ff Do Begin Read (ch); If (ch=' (') Or (ch='[') Or (ch='{') Then Begin Inc(yk);A[yk]:=ch; End Else If (ch = ') ') Or (ch=']') Or (ch=')') Then If Ord(ch)- Ord (A[yk] ) <=2 Then Dec(yk) Else ff:=False; End; Close (Input); If ff Then WriteLn('Скобки расставлены правильно') Else WriteLn ( 'В тексте программы есть ошибка в расстановке скобок'); End. На экране обозримы две программы — последняя и Myl6_sk. Можно приступить к экспериментам. Вносите изменение во вторую программу, сохраняя текст после каждого изменения, переходите в другое окно, делайте запуск программу и произво- дите анализ результата. Например, при некоторых исходных данных возникает ошибка Error 201: Range check error. Про- грамма содержит логическую неточность. Устраните её. Измените программу так, чтобы, например, проверялась пра- вильность записи слова WriteLn в тексте программы. Приду- майте еще ряд модификаций программы.
214 Часть вторая 3. Вы записали следующую программу на диск под именем MY16 7.PAS. В программе подсчитывается количество строк в текстовом файле, каждая строка сохраняется как элемент массива для последующего анализа, строки выводятся на эк- ран. Мы получили один из вариантов программы вывода соб- ственного текста на экран, причём не лучший. Ваша цель — предложить наибольшее количество модификаций задачи, ес- тественно, с их реализациями. Program Myl 6_7; Const MaxN=100; Type MyArray=Array[1..MaxN] Of String; Var A:MyArray; cnt,i:Integer; Begin cnt:=0; Assign (Input, 'MY16_7. PAS') ; Reset (Input); While Not Eof Do Begin Inc (ent); ReadLn (A[cntJ); End; Close(Input); WriteLn (ent); For i:=l To ent Do WriteLn (A[i]); End. Программы, выводящие на экран свой собственный текст, называют интроспективными. Пользоваться для этой цели хра- нящейся на диске копией текста запрещено. Таким образом, наша программа не является интроспективной. Эта тема была одно время довольно популярна в компьютерной литературе. Попробуйте свои силы в решении данной проблемы. Задания для самостоятельной работы 1. Дан текстовый файл, содержащий целые числа. Найти □ количество чисел в файле; □ максимальный элемент в файле, в каждой строке; □ сумму чисел в файле, в каждой строке; □ разность между максимальным и минимальным элемен- тами в файле, в каждой строке; □ арифметическое среднее чисел в файле, в каждой строке; □ номер максимального элемента в файле; □ сумму максимальных элементов в файле; □ сумму четных чисел в файле.
Процедуры и функции — элементы структуризации программ 215 2. Дан текстовый файл, содержащий строки. Найти □ количество строк, начинающихся с заглавных латин- ских букв; □ количество строк, начинающихся и заканчивающихся одинаковыми символами; □ самые короткие строки; □ симметричные строки (палиндромы). 3. Дан текстовый файл. Вставить в начало каждой строки ее но- мер и записать преобразованные строки в новый файл. 4. Даны два текстовых файла. Записать в третий только те стро- ки, которые есть и в первом и во втором файлах. 5. Дан текстовый файл. Дописать в его конце следующие дан- ные: количество строк, количество символов в каждой стро- ке, количество чисел в каждой строке. 6. Даны два файла А и В (тип элементов одинаковый). Поменять местами содержимое этих файлов. Использовать процедуру Rename не разрешается. 7. Содержимое текстового файла копируется в другой файл, при этом каждая строка циклически сдвигается вправо на п сим- волов. Пример циклического сдвига строки abcdefqwrt на 3 символа, результат — wrtabcdefq. То же самое, но только каж- дое слово сдвигается на половину своей длины. 8. Дано некоторое конечное множество слов. Исключить их из текстового файла. Например, из файла с текстом программы исключить все слова Begin и End. 9. Считаем, что длина строк текстового файла не превышает 80 символов. Преобразовать файл так, чтобы все строки были от- центрированы: более короткие строки дополняются символа- ми пробела, текст строки размещается по ее центру. 10. Дан файл, в котором встречаются даты. Каждая дата — это число, месяц и год (например, 13.05.1949 г.). Найти наимень- шую дату. Материал для чтения 1. В предыдущем материале для чтения мы рассмотрели раз- личные схемы построения двоичных кодов. Однако все они не являются помехоустойчивыми. Искажение в любом разряде приводит к тому, что декодировать код не представляется воз- можным. Проверьте это утверждение. Замените в произволь- ном разряде последовательности 010111110111111101011000 значение на противоположное, например в третьем. Получаем 011111110111111101011000.
216 Часть вторая Декодируем — 01 I 111 I 111 I 01 I 111 I 111 I 011 01 110 I 00, что соответствует beebeebbca. Это послание не соответствует истин- ному, а именно — bbeceeccda. Единственный путь решения проблемы построения помехоустойчивых кодов — это введение дополнительных разрядов, другого не дано. Вернемся к равно- мерному кодированию. Пусть для кодирования сообщений ис- пользуется четыре разряда. Очевидно, что кодируются только 16 различных сообщений. Введем один дополнительный раз- ряд — пятый. В этот разряд записываем 1, если в коде нечет- ное число единиц и 0, если четное. В таблице символом I обо- значены столбцы с исходными кодами, символом II — столбцы с соответствующими кодами, но дополненные пятым разрядом. В канал передачи идут не четыре разряда, а пять. Что дает введение одного разряда? Пусть передавался код 10010, произошло искажение в одном разряде, например в пер- вом, и принят код 10011. Подсчитываем число 1, их нечетное количество. Делаем вывод о том, что произошла ошибка в пере- даче. Установить, в каком разряде нам не под силу, информа- ции недостаточно. А если искажениям подверглись два разря- да, например первый и четвертый? Принят код 11011. Число единиц четно, по нашим правилам «игры» мы должны считать этот код правильным. Итак, с помощью одного дополнительно- го разряда возможно фиксирование факта ошибки в нечетном количестве разрядов и только. Рассмотрим другую схему. Каждый разряд сообщения дуб- лируем т раз (т — нечетно). Пусть m равно пяти. Тогда 10010 передается как 1111100000000001111100000. Ни о какой экономии в этом случае речь не идет, сплошное расточитель- ство. Произошли искажения и принята последовательность 1100100011001001101100011. Число m известно и в приемном устройстве. Наши действия. Считаем в каждой группе из m разрядов количество 1 и 0 и исправляем разряды по принципу «чего больше». Исправленная последовательность имеет вид: 1111100000000001111100000. Сообщение восстановлено. Мы рассмотрели два крайних случая — один дополнительный разряд и много дополнительных разрядов. Оба они неудовлетво-
Процедуры и функции — элементы структуризации программ 217 рительны. В первом случае фиксируется одиночная ошибка, о восстановлении сообщения речи не идет, во втором сообщение восстанавливается за счет сверхизбыточности, сверхрасточитель- ности. Истина, как обычно, находится посередине. 2. Рассмотрим на примере построение помехоустойчивого кода по Р. В. Хэммингу (1950 г.), способного обнаруживать и исправлять одиночные ошибки. Но прежде попытаемся дать со- держательную интерпретацию решаемой задачи. Пусть для ко- дирования используются т разрядов. Как мы знаем, различ- ных значений, представимых ш разрядами — 2т (обозначим это множество буквой S). Расстояние по Хеммингу (р(х,у)) между двумя различными элементами S, — это количество раз- рядов, в которых они отличаются друг от друга. Пример: т=4, х=1001, t/=0101, р(х,у)=2; х=0000, j/=llll, р(х,у)=4. Априори известно, что в канале не может произойти больше t ошибок. Другими словами, расстояние по Хеммингу между истинным и искаженным кодами не может превышать значения t. В качест- ве кодов среди 2т элементов множества S следует выбирать та- кое подмножество элементов (обозначим буквой W), что между любыми двумя его элементами расстояние по Хеммингу дол- жно превышать значение 2*t. В этом случае искажение ис- тинного кода не приводит к неоднозначности. Для любого ис- . — каженного кода существует только -х At Л один истинный код, расстояние меж- (• ДУ КОТОРЬ1МИ меньше t. Проиллюст- рируем сказанное рисунком, точка- / /Ат/ ми обозначены элементы множества хрА/ / А точками в кружках и соединен- ( —и'”* ) ных «жирными» отрезками — эле- ху менты множества S. Расстояние по * Хеммингу между любыми двумя эле- ментами S больше 2*t. Искажения оставляют код в t-окрестности, кружке, и искаженному коду соответствует один истинный. Продолжим рассмотрение примера. Пусть т=4 и t=l. Сколь- ко дополнительных разрядов (k) необходимо ввести, чтобы уметь устранять одиночную ошибку? Эти разряды обычно называют проверочными, или контрольными. Значение k определяется из неравенства 2k>m+k+l. Ошибка может произойти как в кон- трольных (р£), так и в информационных (т) разрядах, плюс 1 учитывает случай отсутствия ошибок. Для нашего примера т=4, 23=4+3+1. Принцип формирования контрольных разря- дов — дополнение до четного числа единиц определенной труп-
Часть вторая пы разрядов кода. В примере рассматриваются 3 группы, ибо контрольных разряда три. В результате проверки в приемном устройстве каждой из этих групп на четность числа единиц должен формироваться номер разряда, в котором произошла ошибка. На рисунке в овалах указаны разряды из одной груп- пы. Надписи «да», «нет» говорят о том, есть или нет ошибка в этих разрядах. В прямоугольниках указан номер искаженного разряда, 0 — нет ошибки. Осталось решить проблему — где в семи разрядах размещать контрольные разряды, а где информационные. В первой группе 1, 3, 5, 7-й разряды. Три их них должны быть информацион- ные, один контрольный. Пусть он седьмой, в нем фиксируется четность числа единиц в 1, 3, 5-м разрядах. Следующий конт- рольный разряд шестой, фиксируется четность числа единиц во 2, 3, 7-м разрядах, и наконец, третий контрольный разряд раз- мещаем на четвертом месте — четность числа единиц в 5, 6, 7-м разрядах. Кодировка цифр от 0 до 9 по Хеммингу приведе- на в таблице. Значения контрольных разрядов формируются по значениям информационных и уже сформированных контроль- ных в следующей очередности рр р2, р3. Номер разряда при передаче сообщения 7 | 6 | 5 | 4 | 3 | 2 | 1 Размещение контрольных и информационных разрядов Р1 Р2 т4 Рз то т2 т1 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 1 0 1 0 0 1 0 1
Процедуры н функции — элементы структуризации программ 219 Предположим, что передается цифра 5. В кодировке Хем- минга она записывается как 0101101. Ошибка произошла в третьем разряде, т. е. принят код 0101001. Вычисляем значе- ния контрольных разрядов. Сумма единиц в 1, 3, 5-м и 7-м раз- рядах нечетна — есть ошибка. Проверяем 2, 3, 6, 7-й разряды. Сумма единиц нечетна — есть ошибка. Проверяем сумму еди- ниц в 4, 5, 6-м и 7-м разрядах — нет ошибки. Если слова «есть ошибка» заменить на 1, а слова «нет ошибки» на 0 и записать 0112=310, то получим номер разряда, в котором данные искаже- ны. Выполните еще несколько примеров подобного типа, чтобы полностью понять кодирование по Хеммингу.
Часть третья Массив — фундаментальная структура данных Занятие №17. Методы работы с элементами одномерного массива План занятия □ соревнование на написание коротких программ работы с элементами массива; □ экспериментальная работа с программами вставки и уда- ления элементов массива; □ выполнение самостоятельной работы. Короткие примеры. В этих примерах используется следую- щее описание данных: Const MaxN= ;{*Максимальное количество элементов в массиве.★) Type МуArrays Array[1..MaxN] Of Integer;. А основная программа может иметь вид Begin Init(m,X);{*Ввод элементов массива и инициализация глобальных переменных. *} SoIveProc(<параметры>);{*Вызов основной процедуры решения задачи. *} Print (m,X) ; {"Вывод результата. * } ReadLn; End. Если для решения задачи необходимо использовать функ- цию, то возможен такой вид основной логики. Begin Init(m,X);{*Ввод элементов массива и инициализация глобальных переменных. *) WriteLn (SolveFun (<параметры>));{*Вызов функции, если результат ее действий выводится с помощью одного оператора WriteLn.*) ReadLn; End.
Массив — фундаментальная структура данных 221 1. Заменить отрицательные элементы массива на противопо- ложные по знаку. Procedure NegPos(п:Integer;Var A: MyArray); Var i: Integer; Begin For i:=2 To n Do If A[i]<0 Then A[1]:=-A[i]; End; 2. Прибавить к каждому элементу массива число 25. Procedure Add25 ( п:Integer;Var A:MyArray); Var i: Integer; Begin For i:=l To n Do A[1] :=A[1] + 25; End; 3. Если элемент четный, то прибавить к нему первый, если не- четный — последний элементы массива. Первый и послед- ний элементы не изменять. Procedure AddFirstLast(п:Integer; Var A: MyArray); Var i: Integer; Begin For i:=2 To n-1 Do If A[i] Mod 2=0 Then A[i] :=A[i] + A[1] Else A[1] :=A[1]+A[n] ; End; 4. Даны два одномерных массива одинаковой размерности. По- лучить третий массив такой же размерности, каждый эле- мент которого равен сумме соответствующих элементов дан- ных массивов. Procedure AddArray (п : Integer;А, В.-MyArray; Var С: MyArray ) ; Var i: Integer; Begin For i:=l To n Do C [i] :=A[i]+B [i] ; End; Даны два целочисленных массива, состоящие из одинаково- го числа элементов. Получить третий массив той же размер- ности, каждый элемент которого равен наибольшему из со- ответствующих элементов данного массива. Procedure AddMaxArray(п:Integer;A,B:MyArray; Var С: MyArray ) ; Var i: Integer; Begin
222 Часть третья For i:=l Ton Do If A[i]>B[i] Then C[i]:=A[i] Else C[i]:=B[i]; End; 5. Дан первый элемент арифметической прогрессии и разность между соседними элементами. Сформировать одномерный массив из первых п элементов арифметической прогрессии. Пусть one — первый элемент прогрессии, a k — разность. Если известно значение элемента с номером i-l, то элемент с номером I вычисляется по правилу: a=atl+k, или а=опе+ +k*(t-l). Procedure FormProg(one, к, n: Integer; Var A: MyArray); Var i: Integer; Begin A[l]:=one; For i: =2 To n Do A[1]:=A [ 1-1]+k; End; Для геометрической прогрессии очередной элемент вычисля- ется по правилу a=aL*k, где k — знаменатель прогрессии. Найти сумму первых п элементов прогрессии. Function FormProgm(one, к, п: Integer):Longlnt; Vari,t: Integer;sum:Longlnt; Begin sum:=one;t:=one; For i:=2 To n Do Begin t:=t*k; Inc(sum,t);End; FormProgm:=sum; End; 6. Даны два одномерных массива А и В. Найти их скалярное произведение. Скалярным произведением двух массивов оди- наковой размерности называется сумма произведений соответ- ствующих элементов. Она записывается в виде: А[1]*В[1]+ +А[2]*В[2] +... + А[п-1 ]*В[п-1 ] + А[п]*В[п], где п — это количество элементов в массивах. Function Product(п:Integer;А,В: MyArray): Longlnt; Var i: Integer;s: Longlnt; Begin s:=0; For i:=l To n Do s :=s+A[i] *B [i] ; Product:=s; End;
Массив — фундаментальная структура данных 223 7. Найти максимальный по модулю элемент массива. Function MaxAbs(п:Integer; A:MyArray):Integer; Var i,max:Integer; Begin max : =Abs (A[l] ) ; For 1:=2 To n Do If Abs(A[i])>max Then max:=Abs (A[iJ) ; MaxAbs:=max; End; Найти среднее арифметическое значение элементов. Function Average(п:Integer; А:МуАггау):Real; Var i,sum:Integer; Begin sum:=A[l] ; For i:=2 To n Do Inc(sum,A [ 1]) ; Average:=sum/n; End; Изменить знак у максимального по модулю элемента масси- ва. Procedure SignMaxElem(п:Integer; Var А:МуАггау); Var i,j,Мах:Integer; Begin Max:=Abs (A[l]);j:=1; For i:=2 To n Do I f Abs (A [ 1 ] ) >Max Then Begin Max:=Abs(A[i]);j:=i;End; A[J] :=-A[j] ; End; 8. Все четные элементы массива возвести в квадрат, а нечет- ные удвоить. Procedure MaxEven(п:Integer ; Var A:MyArray); Var i:Integer; Begin For i:=l To n Do If A[i] Mod 2 =0 Then A[i] :=A[i] *A[i] Else A[i]:=2*A[i]; End; 9. Из положительных элементов массива вычесть элемент с но- мером кр а отрицательные увеличить на значение элемента с номером к2> нулевые элементы оставить без изменения.
224 Часть третья Procedure ChangeElem(п,kl,к2:Integer; Var A:MyArray); Var i,w,v:Integer; Begin w: =A [kl] ; v: =A[k2] ; For 1:=1 To n Do If A[i] >0 Then A[i]—A[i]+w Else If A[1]<0 Then A[i]:=A[i]-v; End; Операторы w:=A[kl] v:=A[k2] обязательны. Использование конструкции If A[i] >0 Then A[l]:=A[i]+A[kl] Else If A[i]<0 Then A[1]:=A[1]-A[k2]; приводит к нежелательным последствиям. Проверьте. 10. Из элементов массива А сформировать элементы массива В по правилу: B[i]:=A[l]+A[2]+...+A[i]. Procedure SumElem(п:Integer; A:MyArray;Var В:MyArray); Var 1;-.Integer; Begin B[l] :=A[1] ; For i: =2 To n Do В [11 : =B[i-l ] +A [ 11 ; End; Экспериментальный раздел работы 1. Удалить из массива последний (в том случае, если их неско- лько) максимальный элемент. Для решения задачи необхо- димо: □ найти номер максимального элемента — k; □ сдвинуть все элементы, начиная с fe-го, на один элемент влево; □ последнему элементу присвоить значение 0. <?Е+) Program Му17_1; Const MaxN=10;la=-10;ha=21;I*Размер массива и интервал значений.*) Type MyArray=Array[l..MaxN] Of Integer; Var AiMyArray; к:Integer; Procedure Init(t,v,w:Integer;Var X.-MyArray) ; ( *Напомним процедуру формирования значений элементов массива.★)
Массив — фундаментальная структура данных 225 V ar 1:Integer; Begin Randomize; For i: =1 To t Do X[i] : =v+Integer (Random (w)) ; End; Procedure Print(t:Integer;X:MyArray); f*Вывод t первых элементов массива X. *) V ar i:Integer; Begin For i:=1 To t Do Write(X[i]:5) ; WriteLn; End; Function Max(t:Integer;X:MyArray):Integer; (*Поиск номера последнего максимального элемента. *} V ar i,Мх:Integer; Begin Мх:=1; For i:=2 To t Do If X[i]>=X[Mx] Then Mx:=i; Max:=Mx; End; Procedure Sz (t,q:Integer;Var X:MyArray);(*Сдвиг элементов массива влево на одну позицию, начиная с элемента с номером q+1. *) V ar 1:Integer; Begin For i:=q To t-1 Do X[1]:=X[i+1]; X[t]: =0 ; End; Begin n:=MaxN; Init (n,la,ha,A); WriteLn( 'Исходный массив.’); Print(n,A); k:=Max(n,A); Sz(n,k,A); WriteLn(Массив после удаления элемента.'); Print (п-1,А) ; ReadLn; End. Предположим, что максимальный элемент встречается не- сколько раз. Во-первых, следует изменить функцию Мах. Тре- буется находить не номер максимального элемента, а его значе- ние.
226 Часть третья Function Max(t:Integer;X:MyArray):Integer; Var i,Mx:Integer; Begin Mx:=X[l]; For i:=2 To t Do If X[i]>Mx Then Mx:=X[i]; Max:=Mx; End; Затем повторно просматриваем массив, удаляя элементы, равные максимальному, и уменьшая количество элементов в массиве. Procedure Solve(Var t: Integer;Var X:MyArray); Var k,i:Integer; Begin k:=Max(t,X); i:=t+l; While 1>1 Do Begin Dec (i) ; If X[i]=k Then Begin Sz (t, i, X) ;Dec (t) ; End; End; End; Измените программу так, чтобы при удалении массив про- сматривался не с конца, а с начала. 2. Вставка элемента в массив. Необходимо вставить элемент после первого максимального элемента массива (пусть его но- мер равен q). Операция выполняется следующим образом: □ первые q элементов массива остаются без изменений; □ все элементы, начиная с (д+1)-го, необходимо сдвинуть на одну позицию; □ на место (q+1) записываем значение константы InsC. ($R+) Program Му17_2; Const MaxN=10;la=l;ha=6;InsC=l00; Type MyArray=Array[1..MaxN+1] Of Integer; (★Вставляем один элемент. Резервируем для него место. *} Var А:МуАггау; п,k:Integer; Procedure Init(t,v,w:Integer;Var X:MyArray); {*Формируем исходный массив . ★} Procedure Print(t:Integer;X:MyArray);(*Вывод значений элементов массива.★) Function Max(t:Integer;Х:МуАггау):Integer;
Массив — фундаментальная структура данных 227 (★Поиск номера первого максимального элемента. *} Var 1,Мх:Integer; Begin Мх:=1; For i:=2 То t Do If X[i]>X[Mx] Then Mx:=i; Max:=Mx; End; Procedure InsPs(t,q:Integer;Var X:MyArray); {★Сдвиг на одну позицию вправо и вставка на место q+1 (после заданного элемента) элемента, равного InsC.★} Var i:Integer; Begin For i:=t DownTo q+1 Do X[i+l]:=X[i]; X[q+1]:=InsC; End; Begin n:=MaxN; Init (n,la,ha,A}; WriteLn('Вывод массива до вставки элемента.'); Print (n,A); k:=Мах(n,А);{^Определяем место первого максимального элемента.*} InsPs(n,k,A); WriteLn ( 'Вывод массива после вставки элемента.') ; Print (п+1,А); ReadLn; End. Для вставки элемента перед заданным элементом требуется незначительное изменение процедуры InsPs. Procedure InsPs (t, q: Integer ; Var X-.MyArray) ; Var 1 -.Integer; Begin For i:=t DownTo q Do X[ 1+1] : =X[1] ; X[q]:=InsC; End; Изменим задачу — требуется вставить InsC после всех мак- симальных элементов массива. Во-первых, следует изменить размер массива. Если массив состоит из одинаковых элементов, то в результате наших действий он увеличится в два раза. Итак, Type MyArray=Array[1..2*MaxN] Of Integer. Во-вторых, предыдущее решение в принципе не изменяется. Функция
228 Часть третья k:=Max(nA) и процедура InsPs(n.kA) включаются в новую процедуру Solve. Procedure Solve(Var t: Integer;Var X:MyArray); Var к, 1:Integer; Begi n к:=Max(t, X);{^Определяем значение максимального элемента. *} i: =1 ; While i<=t Do Begin If X[i]=k Then Begin InsPs(t.i,X);Inc(t); End; {^Вставляем элемент и увеличиваем размер массива.★} 1пс(1) ; End; End; А сейчас вопрос. При всех ли исходных данных эта програм- ма работает правильно? Оказывается, нет. Не трогая програм- мный код, а изменяя только значения констант в программе, найдите ошибку. Эксперименты с программой приведут Вас к знакомой ошибке: Error 202: Range check error, а она уже «на- толкнет» на неточность в программе. Измените программу так, чтобы вставка выполнялась перед всеми максимальными элементами массива. После этого необ- ходимо добиться, чтобы вставка выполнялась и перед максима- льными и после максимальных элементов. Задания для самостоятельной работы 1. Заменить □ элементы с krro по &2-й на противоположные по знаку; □ первый отрицательный элемент нулем; □ максимальный элемент на противоположный по знаку; □ первый элемент, кратный 5, нулем; □ элементы с нечетными номерами на квадрат их номера; □ последний положительный элемент на второй элемент массива; □ нулями элементы между минимальным и максимальным элементами массива; □ все элементы, кратные 3, на третий элемент массива; □ все элементы с нечетными номерами разделить нацело на первый элемент.
Массив — фундаментальная структура данных 229 2. Из элементов массива А сформировать массив В той же раз- мерности по правилу □ первые 10 элементов — B[i]~A[i]+i, остальные — B[i]:= =A[i]-i- □ если номер четный, то B[i]~i*A[i], если нечетный, то B[i]:=-A[i]-, □ элементы с 3-го по 12-й — B[i]:=A[i]*A[i], все осталь- ные — B[i]:=A[i] -1; □ если номер четный, то B[i]:=A[i]*A[i], если нечетный, то B[i]:=A[i] Dw i. 3. Сформировать массив А с помощью датчика случайных чисел целыми числами из следующих интервалов: [-21, 53], [-43, 32], [-11, 67], [-35,75], [-45,95]. 4. Удалить из массива все элементы □ с цифрой 5 в записи; □ состоящие из одинаковых цифр (включая однозначные числа); □ последняя цифра которых четная, и само число делится на нее; □ первая цифра которых четная; □ кратные 7 и принадлежащие промежутку [а, &] (а и Ь вводятся с клавиатуры); □ меньшие нуля; □ большие данного числа А (А вводится с клавиатуры); □ четные элементы, стоящие на нечетных местах; □ которые повторяются, оставив только их первые вхожде- ния, то есть получить массив из различных элементов; □ кратные 3 или 5; □ начиная с fe;-ro по /г2-й (/г; и k2 вводятся с клавиатуры). 5. Вставить число k (все числа, указанные в формулировках за- дач, вводятся с клавиатуры) □ после всех элементов, кратных своему номеру; □ перед всеми элементами, в которых есть цифра 1; □ перед и после всех элементов, заканчивающихся на дан- ную цифру; □ после всех элементов, больших заданного числа, а число t — перед всеми элементами, кратными 3; □ между всеми соседними элементами, которые образуют пару элементов с одинаковыми знаками; □ после первого отрицательного элемента; □ перед последним отрицательным элементом;
230 Часть третья □ после максимального элемента, а число t — перед макси- мальным элементом; □ перед всеми элементами, кратными заданному числу; □ перед всеми отрицательными элементами; □ после всех элементов, больших данного числа Р, а второе число t — перед всеми элементами, меньшими числа Р; □ перед всеми элементами, большими k, а число t — после всех элементов, меньших его. 6. Переставить в массиве □ первые три и последние три элемента местами, сохраняя их следование; □ первый положительный и последний отрицательный эле- менты; □ первый элемент и максимальный; □ второй и минимальный; □ первый и последний отрицательный; □ элементы следующим образом: А[ 1 ], А[ 12], А[2], А[ 11 ], .... А[5]. А[8], А[6], А[7]~, □ первые k элементов в конец, то есть: A[k+1 ], A[k+2]. А[п], A[l], А[2].. А[к]; □ в обратном порядке часть массива между элементами с номерами k, и k2, включая их. □ первый элемент с последним, второй с предпоследним и так далее; □ в обратном порядке элементы, расположенные между минимальным и максимальным элементами; □ элементы по следующим правилам (2*п элементов в мас- сиве): А[п+1 ], А[п+2].А[2п], А[1 ], А[2А[п]; А[п+1], А[п+2]..А[2п], А[п], А[п-1 А[1 ]; А[1], А[п+1], А[2], А[п+2].А[п], А[2п]; А[2п], А[2п-1 ] А[п+1 ], А[1 ], А[2],..„ А[п]. 7. Дан одномерный массив из целых чисел. Найти количество различных чисел среди элементов массива. 8. Дан одномерный массив из целых чисел. Известно, что значе- ния элементов массива находятся в интервале от А до В. Най- ти количество различных чисел среди элементов массива. 9. Дан одномерный массив из целых чисел и число q. Переста- вить элементы массива так, чтобы слева от некоторой грани- цы были записаны числа, меньшие q, а справа от границы — числа, большие или равные q. Дополнительные массивы не разрешается использовать.
Массив — фундаментальная структура данных 231 Модифицировать решения для случая: меньшие q, равные q, большие q. Ю. Дан массив из N целых чисел и число М (M<N). Для каждо- го участка из М стоящих рядом элементов массива вычислить значение суммы (количество участков N-M+1). Материал для чтения Алгоритмы и их сложность. Оценка сложности алгоритмов (методов) обычно осуществляется по времени решения и затра- там на объем требуемой памяти для хранения исходных дан- ных задачи. Рассмотрим первую характеристику понятия слож- ности алгоритма. С каждой конкретной задачей связывается некое число N, называемое ее размером и выражающее меру количества входных данных. В случае одномерного массива — это размер массива. Время, затрачиваемое на решение задачи, выраженное через значение N, называется временной сложно- стью алгоритма. Поведение этой характеристики (сложности) при увеличении размерности задачи (значения N) называют асимптотической временной сложностью. Эффективность алго- ритма определяется именно асимптотической временной слож- ностью. Если алгоритм обрабатывает задачу размера N за время c*N2, где с — некоторая постоянная, то говорят, что временная сложность алгоритма есть O(N2) (читается «порядка №»). Наи- более часто встречаемые оценки временной сложности приведе- ны в таблице. Временная сложность Тип зависимости Значение при N=1024 O(N) Линейная 1024 OfN'LogN) Логарифмическая 10240 OfN2) или О(№) Полиномиальная ~106или 10э 0(2») Экспоненциальная -1О300 Эффективный алгоритм — это тот, который решает задачу за меньшее время. Пусть есть два алгоритма А{ и А2, решаю- щих одну и ту же задачу. Первый алгоритм решает задачу раз- мерности А=106за N2 операций, а второй — за 10*N*LogN. Первый алгоритм выполняется на суперкомпьютере с быстро- действием 108 операций в секунду, а второй — на простом, с быстродействием 106 операций в секунду. Найдем время реше-
232 Часть третья ния задачи в том и в другом случаях: t7=1012/108 -2,8 часа; Z2=(6*107*Logl0)/106 -200 секунд. Поиск максимального элемента в одномерном массиве за N-1 сравнение мы умеем делать. Min:=А[1]; For 1: =2 То N Do If A[i]<Min Then Min:=A[i] ; А если необходимо найти и минимальный элемент? Решение за t,=2*N-2 операций сравнения очевидно. Можно ли улуч- шить результат (N — четное число)? If А[1]>А[2] Then Begin Max:=А[1];Min:=А[2];End Else Begin Min:=A[1];Max:=A[2];End; i:=3; While i<=N~l Do Begin If A [i] >A [i+l ] Then Begin If A[i]>Max Then Max:=A [i] ; If A[i+l]<Min Then Min:=A[i+l ] ; End Else Begin If A[i+l]>Max Then Max:=A[i+l] ; If A[i]<Min Then Min:=A[i]; End; Inc(i,2) ; End; На каждую пару чисел, кроме первой, требуется три сравне- ния. На первую — одно. Время решения t2 = 3*pV / 2~| -2 (в ко- личестве операций сравнения), где символами обозначено бли- жайшее целое число, превышающее значение N/2. Для сравнения методов найдите разность например, при А=109. Одним из основных приемов решения задач является реали- зация принципа «разделяй и властвуй», т. е. задача разбивает- ся на подзадачи, решаемые независимо, а затем результаты объединяются. Пусть размерность задачи описывается значени- ем N. Нам необходимо оценить время решения T(N). Слож- ность задачи обычно определяется числом и размером подзадач и в меньшей степени действиями, необходимыми для разбие- ния задачи на подзадачи. Следующая теорема (приводится без
Массив — фундаментальная структура данных 233 доказательства) является основой оценки времени решения за- дач при использовании принципа «разделяй и властвуй». Пусть а, Ь, с — неотрицательные постоянные. Решение уравнений: □ T(N)=b, при N=l, □ T(N)= a*T(N/c)+b*N, при N>1, где N — степень числа с, имеет вид: □ T(N)=O(N), если а<с, □ T(N)=O(N*LogN), если а=с, □ T(N)=O(N‘°e°), если а>с.
234 Часть третья Занятие № 18. Множественный тип данных План занятия □ описание множественного типа данных, операции с вели- чинами этого типа данных; □ экспериментальная работа с программами: вывода цифр, не входящих в десятичную запись числа п, поиска про- стых чисел с помощью «решета Эратосфена», решения ре- бусов; □ выполнение самостоятельной работы. Множество в языке Турбо Паскаль — это ограниченный на- бор различных элементов одного (базового) типа1. Базовый тип — это совокупность значений, из которых могут быть образованы множества. Мощность (количество различных элементов) не превышает 256. Значение переменной множественного типа может содержать любое количество различных элементов базо- вого типа. Иными словами, возможными значениями перемен- ных множественного типа являются все подмножества значе- ний базового типа. Вещественный тип данных не используется в качестве базового (нет отношения порядка, нельзя ввести ограничения на тип). Описание Туре <имя типа> = Set Of <тип элементов:*; Var <имя переменной множественного типа> : <имя типа>;, или Var <имя переменной множественного типа> : Set Of (тип элементов:*; , Пример: Type Mn_Char= Set Of Char; Var mnl : Set Of Char; mn2: Mn_Char; mn3: Set Of 'A'.. 'Z'; si: Set Of Byte; s2: Set Of 100..120; Значениями переменных mnl и mn2 являются множества, составленные из различных символов, тпЗ — множества из бо- льших латинских букв; si — множества из целых чисел от 0 до 255 (тип Byte содержит целые числа от 0 до 255), s2 — множе- 1 Элементы множества должны быть порядкового типа.
Массив — фундаментальная структура данных 235 ства из целых чисел от 100 до 120. В программе элементы мно- жества задаются в квадратных скобках, через запятую. Если элементы идут подряд друг за другом, то используется обозна- чение интервала (две точки). Пример: Type Digit = Set Of 1 .. 5; Var s : Digit; Количество различных значений переменной s 32. Перечис- лим их: □ [ ] — пустое множество; □ [1]> [2], [3], [4], [5] — одноэлементные множества; □ [1, 2], [1,3],..., [2,4], [4,5] — двухэлементные множест- ва; □ [1, 2, 3], [1,2,4],..., [3,4,5] — трехэлементные множест- ва; □ [1, 2, 3, 4], [1,2,3,5], [1,2,4,5], [1,3,4,5], [2,3,4,5] — четы- рехэлементные множества; □ [1, 2, 3, 4, 5] — множество из всех элементов базового типа. Операции над множествами. Объединением двух данных множеств называется множество элементов, принадлежащих обоим множествам. Знак операции — «+». В А+В Примеры: □ [‘A’, ‘F’] + [‘В’, ‘D’] = [‘A’, ‘F’, ‘В’, ‘D’]; □ [1..3,5,7,11] + [3..8,10,12,15..20] = [1..8,10..11,15..20]; □ Sf.= [1..5.9], S2:= [3..7Д2]. S:= Sx + S2 =[1..7,9,12]; □ Aj:=[‘a’..’z’]; A^Aj + [‘А’]. Результат — Aj-pA’, *a’..’z’]. Пересечением двух данных множеств называется множе- ство элементов, принадлежащих одновременно и первому, и второму множеству, то есть это общие элементы. Знак опера- ции — «*».
Часть третья А В А*В Примеры: □ [‘A’, ‘F’] * [‘В’, ‘D’] = [ ] — так как общих элементов нет; □ [1..3,5,7,11] * [3..8,10,12,15..20] = [3,5,7]; □ Sj:= [1..5.9], S2:= [3..7Д2]. S:= S, * S2=[3..5]. В результате операции вычитания формируется множество, состоящее из тех элементов первого множества, которые не яв- ляются элементами второго множества. Знак операции — «-». А в А-В Примеры: □ [‘A’, ‘F’] - [‘В’, ‘D’] = [ ‘A’, ‘F’ ] — общих элементов нет; □ [1..3,5,7,11] - [3..8,10,12,15..20] = [1..2.11]; □ Sl:= [1..5,9]; S2:= [3..7,12]; S:= SI - S2 =[1..2,9]; □ A1:=[‘A’..’Z’]; А1:=А1 — [‘A’]=[‘B’..’Z’]. Операция определения принадлежности элемента множе- ству. Эта логическая операция обозначается служебным сло- вом In. Результат операции — значение True, если элемент принадлежит множеству, и False — в противном случае. Примеры: □ 5 In [3 .. 7] дает значение True, так как 5 е [3 .. 7]; □ 'a' In ['А' .. ’Z’] дает значение False, буквы ‘а’ нет среди больших латинских букв. Операцию проверки принадлежности удобно использовать для исключения более сложных проверок. Например, оператор вида If (ch='а') or (ch='b') or (ch='x') or (ch='y') Then s;
Массив — фундаментальная структура данных 237 переписывается в более компактной и наглядной форме: If ch In ['a', 'b', ’х', ’у' ] Then s;. Сравнение множеств. Для сравнения множеств используют- ся обычные операции: □ = — равенство (совпадение) двух множеств; □ <> — неравенство двух множеств; □ <=, < — проверка на вхождение первого множества во второе множество; □ >=, > — проверка на вхождение второго множества в пер- вое множество. Первое множество меньше или равно второму (А < В): Экспериментальный раздел работы 1. Дано натуральное число п. Составить программу вывода цифр, не входящих в десятичную запись числа п (в порядке возрас- тания). Program Му18_1; Type Mn=Set Of 0. .9; Var s:Mn; n: Longlnt; i : Integer ; Begin WriteLn('Введите число '); ReadLn (n) ; s:=[0..9J; While n<>0 Do Begin
238 Часть третья s:=s-[п Mod 10];{*Исключаем цифру. *} п:=n Div 10; End; For i:=0 То 9 Do If i In s Then Write (i:2); WriteLn; ReadLn; End. Измените программу так, чтобы находились общие цифры в записи т чисел. 2. «Решето Эратосфена». Найти простые числа в интервале от 2 до п. Напомним, что простым числом называется число, не име- ющее других делителей, кроме единицы и самого себя. Реше- ние без использования множественного типа данных приведе- но ниже. Program Му18_2; Const п=1 000;{*Интервал чисел, в котором находятся простые числа. *] Var 1:Integer; Function Simple(m:integer):Boolean; Var i:Integer; Begin i:=2; While (i<= m Div 2) And (m Mod i<>0) Do Inc (i) ; Simple:=(i>m Div 2); End; Begin For i:=2 To n Do If Simple (i) Then Write(1,' '); ReadLn; End. Измените программу так, чтобы находилась первая 1000 простых чисел. Откажемся от этого простого решения. Применим идею «ре- шета Эратосфена», ибо наша цель — изучение множественного типа данных. Суть метода — считаем все числа интервала про- стыми, а затем «вычеркиваем» те, которые не удовлетворяют требованию простоты. Как осуществляется вычеркивание? На- ходим очередное невычеркнутое число, оно простое, и удаляем все числа, кратные ему. После такого «просеивания» в исход- ном множестве останутся только простые числа.
Массив — фундаментальная структура данных 239 Program Му18_2т; Const п=255; Type Mn=Set Of 0. .п; Var Sim:Mn; 1,J:Integer; Begin Sim: = [2..n); J : =2 ; While j<=n Div 2 Do Begin If J In Sim Then Begin f *Поиск очередного простого числа.*) While 1<~п Do Begin Sim:=Sim-[i];Inc (i,j) ; End;{^Вычеркивание. *) End; Inc (j) ; End; For i:=2 To n Do If i In Sim Then Write (i: 4) ; {★Вывод оставшихся после вычеркивания чисел, они простые. *} ReadLn; End. Поиск простых чисел из интервала, большего, чем 0.. 255, «упирается» в ограничение множественного типа данных — не более 256 значений базового типа. Уйдем от этого ограничения путем ввода массива, элементами которого являются множест- ва. Но прежде, чем рассмотрим решение, небольшой фрагмент: ($R+) Program Myl8_2mm; Var Mn: Set Of 1..255; a:Word; Begin Mn:=[l..255); a:=258; If a In Mn Then WriteLn('Yes') Else WriteLn ('No') ; ReadLn; End. После запуска программы видим знакомую до боли ошиб- ку — Error 202: Range check error. Значение переменной а вы- ходит за допустимый диапазон значений. Учтем этот факт при написании очередной версии программы.
240 Часть третья ($R+1 Program Myl8_2ттт; Uses Crt; Const m=255;n=1000; TypeMn=Set Of 1. .m; OMyArray=Array[0. . (n Div m) ] Of Mn; VarSim:Mn; A:OMyArray; i,j,к:Integer; Begin ClrScr; k: = (n Div m) ; For i:=0 To к Do A [i ] : = [ 1. . m] ; j:=2; While j<=n Div 2 Do Begin If (j Mod m) In A[j Div m] Then Begin i:=j+j; While K=n Do Begin A[i Div m] :=A[i Div m] ~[i Mod m];Inc(i,j);End; End; Inc (j) ; End; For i:=2 To n Do If (1 Mod m) In A[i Div m] Then Write 11,' ’); ReadLn; End. 3. Решение ребусов мы рассматривали на предыдущих заняти- ях. С использованием множественного типа данных програм- мный код получается более компактным. Подсчитаем коли- чество решений ребуса МУХА+МУХА=СЛОН. Program Му18_3; Type Mn=Set Of 0. .9; Var i,j,ent:Integer; Sm,Se:Mn; Procedure Changeft:Integer; Var S:Mn);(*Из цифр числа формируем множество. *) Begin S:-[]; While t<>0 Do Begin S:=S+[t Mod 10];t:=t Div 10; End; End;
Массив — фундаментальная структура данных 241 Function Qw(S:Mn):Integer;{*Подсчитываем количество элементов в множестве. *} Var 1,ent:Integer; Begin ent:=0; For i:=0 To 9 Do If i In S Then Inc (ent); Qw:=cnt; End; Begin cnt:=0;(*Счетчик числа решений. *) For i:=1000 To 4999 Do Begin{*Результат - четырехзначное число, поэтому слагаемое не превышает 4999. *} Change(i,Sm); If Qw (Sm) =4 Then Begin{*Если все цифры числа различны, то выполняем дальнейшие вычисления. *) J : =2 *1 ; Change (j,Se) ; If (Sm*Se=[J) And (Qw(Se)-4) Then Inc(cnt); {★Числа состоят из различных цифр, и все цифры результата различны.★) End; End; Wri teLn (ent) ; End. Задания для самостоятельной работы 1. Дан текст. Найти множества, элементами которых являют- ся встречающиеся в тексте: □ цифры от ‘0’ до ‘9’ и знаки арифметических операций; □ буквы от’4‘ до ‘F‘ и от ‘Х‘ до ‘Z‘; □ знаки препинания и буквы от *£* до ‘№. 2. Вывести элементы множества, составленного из произволь- ных букв от А ... Z, в алфавитном порядке. 3. Дан текст. Найти множество строчных латинских букв, вхо- дящих в него. Подсчитать количество знаков препинания и цифр в тексте. 4. Дан текст. Вывести в алфавитном порядке все буквы текста, входящие в него: □ не менее двух раз; □ не более двух раз; □ более двух раз;
242 Часть третья □ по одному разу. 5. Дан текст. Подсчитать количество гласных и согласных букв. 6. Написать программы решения ребусов: ЛОБ+ТРИ=САМ, ИСК+ИСК=КСИ, ТОЧКА+КРУГ=КОНУС, VOLVO+FIAT= =MOTOR, АВ+ВС + СА =АВС.
Массив — фундаментальная структура данных 243 Занятие № 19. Методы сортировки План занятия □ постановка задачи сортировки данных, изучение простых методов сортировки; □ экспериментальная работа с методом сортировки подсче- том; □ выполнение самостоятельной работы. Постановка задачи. Для решения многих задач необходи- мо упорядочить данные по определенному признаку. Процесс упорядочения заданного множества объектов по заданному при- знаку называется сортировкой. Для простоты изложения рас- сматривается одномерный массив из целых чисел (Туре МуАг- ray=Array[1..NMax] Of Integer; Var A:MyArray, значение Nmax определяется в разделе констант). Суть большинства алгорит- мов сортировки от такого упрощения не изменяется. Алгорит- мы сортировки отличаются друг от друга степенью эффективно- сти, под которой понимается количество сравнений и количество обменов, произведенных в процессе сортировки. В основном мы будем оценивать эффективность количеством операций сравне- ния (порядком этого значения). Заметим, что элементы масси- ва можно сортировать: □ по возрастанию — каждый следующий элемент больше предыдущего — А[1 ] < А[2] <... < A[N]; □ по неубыванию — каждый следующий элемент не меньше предыдущего, то есть больше или равен А[1 ] < А[2] < ... <A[N];. □ по убыванию — каждый следующий элемент меньше пре- дыдущего А[1] > А[2] > ... > A[N];. □ по невозрастанию — каждый следующий элемент не боль- ше предыдущего, то есть меньше или равен А[1] > А[2] > ... > A[N]. Научившись выполнять одну сортировку, изменить ее, что- бы получить другую, не составляет особого труда. Сортировка простым выбором. Рассмотрим идею на приме- ре. Пусть исходный массив А состоит из 10 элементов: 5 13 7 9 1 8 16 4 10 2. После сортировки массив должен выглядеть так: 1 2 4 5 7 8 9 11 13 16. Процесс сортировки представлен ниже. Максимальный эле- мент текущей части массива заключен в кружок, а элемент, с которым происходит обмен, в квадратик. Скобкой помечена рассматриваемая часть массива.
244 Часть третья 1-й шаг. Рассмотрим весь массив и найдем в нем максималь- ный элемент — 16 (стоит на седьмом месте), поменя- ем его местами с последним элементом — с числом 2. 5 13 7 9 1 8 ^4 10[7|[ Максимальный элемент записан на свое место. 2-й шаг. Рассмотрим часть массива — с первого до девятого элемента. Максимальный элемент этой части — 13, стоящий на втором месте. Поменяем его местами с последним элементом этой части — с числом 10. 5^7 9 1 8 2 4 {30)16 Отсортированная часть массива состоит теперь уже из двух элементов. 3-й шаг. Уменьшим рассматриваемую часть массива на один элемент. Здесь нужно поменять местами второй эле- мент (его значение — 10) и последний элемент этой части — число 4. 5^7 9 1 8 2Щ13 16 В отсортированной части массива — 3 элемента. 4-й шаг. 5 4 7(g)l 8[2]l0 13 16 5-й шаг. Максимальный элемент этой части массива является последним в ней, поэтому его нужно оставить на ста- ром месте. 5 4 7 2 1^9 10 13 16 6-й шаг. 5 4(7)2Щ8 9 10 13 16 7-й шаг. (бЮЩ? 8 9 10 13 16 8-й шаг. ,2(^5 7 8 9 10 13 16 9-й шаг. 5 7 8 9 10 13 16
Массив — фундаментальная структура данных 245 Фрагмент программной реализации. Procedure Sort(N:Integer; Var A:MyArray); Var i, j, k: Integer; m: Integer;{*3начение максимального элемента рассматриваемой части массива. *} Begin For i:=N DownTo 2 Do Begin (*Цикл по длине рассматриваемой части массива. *} (*Поиск максимального элемента и его номера в текущей части массива. *} k:=i; m:=A[i];{^Начальные значения максимального элемента и его индекса в рассматриваемой части массива. * } For j:=l То 1-1 Do If A[j]>m Then Begin k:=j; m:=A[j] End; If koi Then Begin {^Перестановка элементов.*} A[k]:=A[1];A[i]:=m; End; End; End; Количество сравнений. При первом просмотре N-1, при вто- ром — N-2, при последнем — 1. Общее количество C=N-l+N-2+ ...+l=N*(N-l)/2 (сумма элементов арифметической прогрес- сии) или C=O(NZ). Сортировка простым обменом. Рассмотрим идею метода на примере. Отсортируем по возрастанию массив из 5 элемен- тов: 5 4 8 2 9. Длина текущей части массива — N-k+1, где k — номер просмотра, i — номер первого элемента проверяемой пары, номер последней пары — N-k. Первый просмотр: рассматривается весь массив. 1=1 5 4 8 2 9 > меняем i=2 4 5 8 2 9 < не меняем 1=3 4 5 8 2 9 > меняем 1=4 4 5 2 8 9 < не меняем 9 находится на своем месте. Второй просмотр: рассматриваем часть массива с первого до предпоследнего элемента.
246 Часть третья 1=1 4 5 2 8 9 < не меняем 1=2 4 5 2 8 9 > меняем 1=3 4 2 5 8 9 < не меняем 8 — на своем месте. Третий просмотр: рассматриваемая часть массива содержит три первых элемента. 1=1 4 2 5 8 9 > меняем 1=2 2 4 5 8 9 < не меняем 5 — на своем месте. Четвертый просмотр: рассматриваем последнюю пару эле- ментов. i = l 2 4 5 8 9 < не меняем 4 — на своем месте. Наименьший элемент — 2 оказывается на первом месте. Количество просмотров элементов массива равно N—1. Итак, наш массив отсортирован. Этот метод также называют методом «пузырька». Название это происходит от образной ин- терпретации, при которой в процессе выполнения сортировки более «легкие» элементы (элементы с заданным свойством) ма- ло-помалу всплывают на «поверхность». Procedure Sort(N:Integer;Var A:MyArray); Var k,i,w:Integer;{*k - номер просмотра, изменяется от 1 до N-1; 1 — номер первого элемента рассматриваемой пары; w - рабочая переменная для перестановки местами элементов массива. *} Begin For k:=l То N-1 Оо{*Цикл по номеру просмотра .*} For i:=l То N-k Do If A[i]>A[i+l] Then {^Перестановка элементов.*} Begin w:=A[i];А[1]:=A[i+l];A[1+1]:=w; End; End;
Массив — фундаментальная структура данных 247 При сортировке методом «пузырька» выполняется N-1 про- смотров, на каждом i-просмотре производится N-i сравнений. Общее количество С равно N*(N-1 )/2, или O(N2). Сортировка простыми вставками. Сортировка этим мето- дом производится последовательно шаг за шагом. На г-м шаге считается, что часть массива, содержащая первые i-l элемен- тов, уже упорядочена, то есть А[1 ]<A[2]<..<A[i-l]. Далее бе- рется г-й элемент, и для него подбирается место в отсортирован- ной части массива, такое, что после его вставки упорядоченность не нарушается, то есть требуется найти такое j (0<j<i-l), что A[j]<A[i]<A[j+l] (при 7=0 происходит только одно сравнение, если не использовать «барьерную» технику). Затем выполняет- ся вставка элемента A[i] на место у. На каждом шаге отсорти- рованная часть массива увеличивается. Для выполнения пол- ной сортировки потребуется выполнить N-1 шаг. Рассмотрим этот процесс на примере. Пусть требуется отсор- тировать массив из 10 элементов по возрастанию. 1-й шаг 13 6 8 11 3 1 5 9 15 7 Рассматриваем часть массива из одного элемента 13 Вставляем второй элемент массива 6 так, чтобы упорядоченность сохранилась. Так как 6< 13, записываем 6 на первое место Отсортированная часть массива содержит два элемента (6 13) 2-й шаг 6 13 8 11 3 1 5 9 15 7 Возьмем третий элемент массива 8 и подберем для него место в упорядоченной части массива 8>6 и 8< 13, следовательно, его место второе. 3-й шаг 6 8 13 11 3 1 5 9 15 7 Следующий элемент—11 Он записывается в упорядоченную часть массива на третье место, так как 11 >8, но 1К13 4-й шаг 6 8 11 13 3 1 5 9 15 7 Далее, действуя аналогичным образом, определяем, что 3 необходимо записать на первое место 5-й шаг. 3 6 8 11 13 1 5 9 15 7 По той же причине 1 записываем на первое место 6-й шаг. 1 3 6 8 11 13 5 9 15 7 Так как 5>3, но 5<6, то место 5 в упорядоченной части — третье 7-й шаг. 1 3 5 6 8 11 13 9 15 7 Место числа 9 — шестое 8-й шаг. 1 356 8 9 11 13 15 7 Определяем место для предпоследнего элемента 15 Оказывается, что этот элемент массива уже находится на своем месте. 9-й шаг. 1 3 5 6 8 9 11 13 15 7 Осталось подобрать подходящее место для последнего элемента 7. 1 3 5 6 7 8 9 11 13 15 Массив отсортирован полностью.
248 Часть третья Процедура сортировки. Procedure Sort(N:Integer;Var A:MyArray); Var i,j,w:Integer; Begin For i:=2 To N Do Begin w:=A[i] ; j :=i-l; While (j>0) And (w<A[j]) Do Begin A[j+1]:=A[j]; Dec (j) ;End; A[j+1]:=w; End; End; Оценим эффективность метода. Для массива, например, 1 2 3 4 5 6 7 8 потребуется N-1 сравнение (напомним, что мы сор- тируем в порядке возрастания), а для массива 87654321 — N*(N-l)/2 или C=O(N2). Экспериментальный раздел работы Сортировка подсчетом. Пусть дан массив (А) из 10 элемен- тов: 10, 5, 11, -5, 1, -4, 13, 12, -4, 13. Возьмем первый эле- мент, он больше пяти элементов. Запишем 5 в дополнительный массив счетчиков (Count). Выполним эту операцию для всех элементов массива А. В массиве Count имеем: 5, 4, 6, 0, 3, 1, 8, 7, 2, 9. Если разрешается использовать дополнительный массив для хранения отсортированных данных, то остается переписать каждый элемент исходного массива на соответствующее место в результирующем массиве (В). Procedure Sort(N:Integer;A:MyArray; Var В:MyArray); Var i,j:Integer; Count:MyArray; Begin FillChar(Count,SizeOf(Count),0); For i:=N DownTo 2 Do For j:=i-l DownTo 1 Do If A[i]<A[j] Then Inc(Count [j]) Else Inc(Count[1]); For i:=l To N Do В [ Count [i ] +1 ] : =A [ i] ; End; Естественное желание — убрать массив В. Ниже по тексту приведена соответствующая процедура. Разберите её. Покажи- те, что она действительно работоспособна. В качестве подсказ-
Массив — фундаментальная структура данных 249 ки приведем значения элементов массива Count, увеличенные на единицу. I I I 657142983 10 I_____L I I Procedure Swap(Var х,у:Integer); Var w:Integer; Begin w:=x;x:=y;у:=w;End; Procedure Sort(N:Integer;Var A:МуАггау); Var i,j:Integer; Count:МуАггау; Begin Fillchar(Count,Sizedf(Count),0) ; For i:=N DownTo 2 Do For j:=i-l DownTo 1 Do If A[i]<A[j] Then Inc (Count[j ] ) Else Inc(Count[i]); For i:=l To N Do Inc (Count [1]) ; For 1:=1 To N Do While Count[i]<>i Do Begin Swap(A[i],A[Count[i]]);Swap(Count[i],Count [Count[i]]); End; End; Предположим, что значения элементов массива А принадле- жат интервалу [и,и]. Например, А состоит из элементов: 2, 4, 3, 2, 4, 2, 3, 4, 3, 2. Подсчитаем, сколько раз каждое значение встречается в массиве: двойка — 4 раза, тройка — 3 раза и чет- верка — 3 раза (массив Count: 4, 3, 3). Изменим значения Count на 4, 7, 10. При этих значениях (Count) элементы массива А можно сразу записывать на свое место в результирующий мас- сив В. Procedure Sort (N: Integer ;A:MyArray ;Var B:MyArray) ; {*Считаем, что значения элементов массива А принадлежат интервалу [1.. 4]. *) Const u=l;v=4; Var i:Integer; Count .-Array [u. .v] Of Integer; Begin Fill Char (Count,SizeOf(Count) , 0) ; For i : =1 To N Do Inc (Count [A[i] ]) ;
250 Часть третья For i:=и+1 То v Do Inc (Count[i],Count[i-l]); For i: =1 To N Do Begin В[Count[A[i]]]:=A[i];Dec(Count[A[1]]); End; End; А можно ли не использовать массив В? Оказывается, да. В следующей версии процедуры Sort показано, как это сделать. Procedure Sort(t:Integer;Var A:MyArray); Const u=l;v=4; Var i,j,q:Integer; Count:Array[u..v] Of Integer; Begin FillChar(Count,SizeOf(Count),0); For i: =1 To t Do Inc(Count[A[1]]); q:=l; For 1:=u To v Do For j:=1 To Count[1] Do Begin A[q] :-i;Inc (q) ; End; End; Задания для самостоятельной работы 1. Изменить решения в трех рассмотренных методах так, что- бы осуществлялась сортировка: □ четных элементов массива; □ элементов, записанных на нечетных местах; □ отрицательных элементов массива и т.д. 2. Дан, например, массив 12 3 5 7 9 10. За один просмотр мето- дом «пузырька» он становится отсортированным, остальные просмотры ничего не дают. Исключить лишние просмотры. 3. Массив 12357910 сортируется методом «пузырька» за один просмотр, а массив 579 10 123 — за пять (А-1). Явное не- равноправие. Устранить его можно путем смены направлений просмотров, т. е. первоначально в направлении -> получаем 5 7 9 10 3 12, а затем в направлении «- результат — 3 5 7 9 10 12. Итак, чередуем направления, пока массив не будет отсор- тирован. 4. Объединить требования заданий 2 и 3 в единое целое. Этот ме- тод называется «шейкер-сортировкой». Реализовать его. 5. В сортировке простыми вставками убрать переменную х, т. е. внутренний цикл записать в виде: While А[0]<A[j] Do. Под-
Массив — фундаментальная структура данных сказка. Массив А необходимо сделать типа Array [O..Nmax] Of Integer и i-й элемент записывать на 0 место, так называе- мый прием «барьерного элемента». 6. Метод вставок. На момент вставки элемента с номером i эле- менты массива с номерами от 1 до i-l отсортированы. Выбе- рем из них средний элемент (или один из двух средних) и срав- ним его с элементом А[1]. Если А[i] меньше этого элемента, то поиск места вставки следует продолжать в левой полови- не, иначе — в правой половине отсортированного массива. Эта схема получила название сортировки бинарными вставками. Она предложена Дж. Мочлив 1946 году и была одной из пер- вых публикаций по методам компьютерной сортировки. Ре- ализовать данную схему. 7. Д. Л. Шелл в 1959 году предложил следующую модифика- цию метода — «сортировку с убывающим шагом». Ее суть по- кажем на примере. Массив из 8 элементов. Делим его на 4 группы по 2 элемента, сортируем элементы в каждой груп- пе. Затем — на 2 группы по 4 элемента, выполняем для них сортировку и, наконец, сортируем одну группу из 8 элемен- тов. При простых вставках мы перемещали элемент только в соседнюю позицию, а в этом методе перемещаем на большие расстояния, что приводит к получению более отсортирован- ного массива и, в конечном итоге, к повышению эффектив- ности метода. Значения 4, 2, 1 (приращения) не являются догмой. Можно выполнить сортировку при 5, 3, 1. Послед- ний элемент должен быть равен 1. Д. Кнут дает оценку мето- да при грамотном выборе приращений — OfN1 2). Лучшим считается выбор в качестве значений приращений взаимно простых чисел. Реализовать «сортировку с убывающим ша- гом» для заданных значений приращений. 8. Пусть N является точным квадратом натурального числа, на- пример 3. Разделим массив n&Sqrt(N) групп по Sqrt(N) эле- ментов в каждой. Выберем максимальный элемент в каждой группе... Проще рассмотреть пример. Дан массив 7 10 3 5 15 9 6 12 8. При разбивке на группы — (7 10 3) (5 15 9) (6 12 8). Максимальные элементы 1015 12. Максимальный из них 15, он во второй группе. Если оставшиеся элементы из второй группы, а это 5 и 9, меньше 10 и 12, то мы нашли сразу три элемента, записываемые на свои места. Если нет, то заменя- ем максимальные элементы элементами из группы. Этот ме- тод предложен Э. X.Фрэндом в 1956 году и получил название метода квадратичного выбора. Количество сравнений имеет порядок O(N*Sqrt(N)). Реализовать метод.
252 Часть третья Занятие № 20. Методы быстрой сортировки План занятия □ изучение методов сортировки с временной оценкой O(N*LogN); □ выполнение самостоятельной работы. Сортировка слияниями. Прежде чем обсуждать метод, рас- смотрим следующую задачу. Объединить («слить») упорядочен- ные фрагменты массива A A[k],...А[т] и А[т+1 ]. A[q] в один A[k]A[q], естественно, тоже упорядоченный (kmq). Основная идея решения состоит в сравнении очередных элемен- тов каждого фрагмента, выяснении, какой из элементов мень- ше, переносе его во вспомогательный массив D (для простоты) и продвижении по тому фрагменту массива, из которого взят элемент. При этом следует не забыть записать в D оставшуюся часть того фрагмента, который не успел себя «исчерпать». Пример. Пусть первый фрагмент состоит из 5 элементов: 3 5 8 11 16, а второй — из 8: 1 5 7 9 12 13 18 20. Рисунок иллюстри- рует логику объединения фрагментов. Procedure S1(к,т,q:Integer;Var A:MyArray); Vari, j, t:Integer; D:MyArray; Begin i:=k; t:=l; While And (j<=g) Do Begin (*Пока не закончился хотя бы один фрагмент. *) If A[i]<=A[j] Then Begin D[t]:=A[iJ; Inc(i); End
Массив — фундаментальная структура данных 253 Else Begin D[t]:=A[j]; Inc(j); End; Inc (t) ; End;(*Один из фрагментов обработан полностью, осталось перенести в D остаток другого фрагмента. *} While i<=m Do Begin D[t]:=A[i]; Inc(i) ; Inc(t) End; While j<=q Do Begin D[t] :=A[j]; Inc(j); Inc(t) End; For i: =1 To t-1 Do A[k+i-l]:=D[i]; End; Параметр чп из заголовка процедуры можно убрать. Мы «сливаем» фрагменты одного массива А. Достаточно оставить нижнюю и верхнюю границы фрагментов, т. е. — Sl(k,q:Inte- ger;Var A:MyArray), где k — нижняя, a q — верхняя границы. Вычисление m (эта переменная становится локальной) сводится к присвоению: m:=k+(q-k) Div 2. При этом уточнении приведем процедуру сортировки. Первый вызов процедуры — Sort(l,N). Procedure Sort(1,j:Integer); Var t:Integer; Begin If Kj Then ("""Обрабатываемый фрагмент массива состоит более, чем из одного злемента. *) If J~1=1 Then Begin If A[j]<A[i) Then Begin ("^Обрабатываемый фрагмент массива состоит из двух элементов. *) t:=A[i] ;A[i] :=A[j] ;A[j] :=t; End; End Else Begin Sort (1,1+ (j-1) Div 2) ;{^Разбиваем заданный фрагмент на два.*) Sort (1+ (j-1) Div 2+1,j); SI (i,g,A) ; End; End; Метод слияний — один из первых в теории алгоритмов сор- тировки. Он предложен Дж. фон Нейманом в 1945 году. В ло- гике реализован один из фундаментальных принципов инфор- матики — «разделяй и властвуй». Рекомендуется проделать с учащимися «ручную» трассировку работы процедуры на раз- личных примерах. Это, во-первых, позволит в очередной раз глубже понять суть принципа и, во-вторых, проверить понима- ние и усвоение темы «рекурсия». Эффективность алгоритма, по Д. Кнуту, составляет C=O(N*logN).
254 Часть третья Быстрая сортировка. Метод предложен Ч. Э. Р. Хоаром в 1962 году. Эффективность метода достаточно высокая, кроме специально подобранных данных, поэтому автор назвал его «быстрой сортировкой». Идея метода. В исходном массиве А выбирается некоторый элемент X (барьерный элемент). Нашей целью является запись X «на свое место» в массиве, пусть это будет место k, такое, что слева от X были элементы массива, меньшие или равные X, а справа — элементы массива, большие X, т. е. массив А будет иметь вид: (А[1], А[2], .... A[k-1]). A[k] (X), (А[Ъ+1], .... А[п]). В результате элемент A[k] находится на своем месте и ис- ходный массив А разделен на две неупорядоченные части, барь- ером между которыми является элемент A[k]. Дальнейшие действия очевидны — независимо сортировать полученные час- ти по той же логике до тех пор, пока не останутся части масси- ва, состоящие из одного элемента, то есть пока не будет отсор- тирован весь массив. Пример. Исходный массив состоит из 8 элементов: 8 12 3 7 19 11 4 16. В качестве барьерного элемента возьмем средний элемент массива (7). Произведя необходимые перестановки, по- лучим: (4 3) 7 (12 19 11 8 16), элемент 7 находится на своем ме- сте. Продолжаем сортировку. Левая часть: (3) 4 7 (12 19 11 8 16) 3 4 7 (12 19 11 8 16) Правая часть: 3 4 7 (8) 11 (19 12 16) 3 4 7 8 11 (19 12 16) 3 4 7 8 11 12 (19 16) 3 4 7 8 11 12 (16) 19 3 4 7 8 11 12 16 19 Из вышеизложенного описания явно просматривается ре- курсивная схема реализации, параметрами которой являются нижняя и верхняя границы изменения индексов сортируемой части исходного массива. Приведем процедуру быстрой сорти- ровки из книги классика информатики Н. Вирта. Procedure Quicksort(т, t: Integer); Var i,j,x,w: Integer; Begin i:=m; g :=t;x:=A[ (m+t) Div 2] ; Repeat While A[i]<x Do Inc(i);
Массив — фундаментальная структура данных While А [j ] >х Do Dec (j) ; If 1<=J Then Begin w:=A[i]; A[i]:=A[j] ; A[j]:=w; Inc(i); Dec(j); End Until 1>J; If m<j Then Quicksort (m,j); If Kt Then Quicksort (1, t) ; End; Простая процедура, но возникает «тысяча и один вопрос». Начнем. Соответствуют ли результаты работы процедуры рас- смотренному выше примеру, естественно, при тех же исходных данных? Оказывается, что нет. Лучше, если в этом учащиеся убедятся самостоятельно. Результаты работы процедуры вы- глядят следующим образом (первые два столбца содержат пара- метры процедуры, третий — массив А). т А п 1 8 4 12 37 19 11 8 16 I 1 8 4 7 3 12 19 11 8 16 1 3 4 3 7 12 19 11 8 16 1 2 3 4 7 12 19 11 8 16 4 8 3 47 8 19 11 12 16 4 8 3 4 78 11 19 12 16 5 3 4 7 8 11 19 12 16 6 8 3 4 7 8 11 12 19 16 7 8 3 4 7 8 11 12 16 19 Продолжим наши рассуждения. В процедуре есть сравнение «If i<=j Then -^перестановка элементов A[i] nA[j]>t. Получа- ется, что при равенстве мы выполняем перестановку элементов массива. Эту конструкцию заменим следующей «If i<] Then». Результат — бесконечная работа процедуры («зацикливание»). Рекурсивный вызов процедуры осуществляется при сравнении «If т<] Then ...». А если просто вызвать? Результат не замед- лит сказаться. Произойдет переполнение стека адресов возвра- та. Конструкция «Repeat ... Until i>j;» работает и при i=j, что кажется неестественным. Заменим ее на «Until i>=j». Что про- изойдет? В цикле «Repeat... Until i>j;» еще два цикла типа Whi- le, причем по переменным внешнего цикла. Как этот факт трактует техника программирования, может быть, как «дурной тон»? При изучении темы «рекурсия» всегда обращается вни- мание на то, что в такой логике обязательно должна быть «за- глушка». Как она реализована здесь? Заключительным «ак-
Часть третья кордом» обсуждения процедуры может быть предложение о написании версии логики, соответствующей примеру, рассмот- ренному в начале параграфа. Один из вариантов реализации приводится ниже по тексту. Procedure Quicksort (т,t: Integer); Var i,j,x,w: Integer; Begin i:=m; j:=t;x:=A[(m+t) Div 2] ; While 1<=J Do If A[i]<x Then Inc(i) Else If A[j]>x Do Dec(j) Else Begin w:=A[i]; A[i]:=A[j]; A[j]:=w; Inc(i);Dec(j);End; If m<j Then Quicksort(m,j); If Kt Then Quicksort (i,t) ; End; С этой процедурой можно проделать «экзекуцию», анало- гичную той, что выполнена с предыдущей процедурой: изме- нить операторы сравнения, или параметры рекурсивного вызо- ва, или условие цикла While. В конечном итоге получается интересное занятие, особенно если совмещать «ручную» трас- сировку логики с пошаговым выполнением процедур. Оценим эффективность метода. Предположим, что размер массива равен числу, являющемуся степенью двойки (А=21?), и при каждом разделении элемент X находится точно в середине массива. В этом случае при первом просмотре выполняется А сравнений и массив разделится на две части размерами -N/2. Для каждой из этих частей N/2 сравнений и т. д. Следователь- но C=N+2*(N/2)+4*(N/4)+...+N*(N/N)=O(N*logN). Если N не является степенью двойки, то оценка будет иметь тот же поря- док. Для некоторых исходных данных время сортировки пропор- ционально O(N2). В этом случае на каждом шаге размер сорти- руемой части массива уменьшается только на единицу. Попробу- ем построить такие последовательности для различных значений А- Ограничимся случаем, когда исходными данными является перестановка чисел от 1 до N. Логику быстрой сортировки можно дополнить счетчиком числа сравнений, генерировать все перестановки как исходные данные и запоминать те, кото- рые дают максимальное число сравнений. А если N большое число, например до 10000? Этот путь исследования не подходит. Попробуем по-другому. При N=3 одной из искомых последовате- льностей является 1, 3, 2. При первом просмотре 3 переставляет-
Массив — фундаментальная структура данных 257 ся с 2 и рекурсивно вызывается обработка последовательности из двух первых элементов. При N=4 перестановка элементов на первой итерации должна приводить к тому, что на второй ите- рации сортируется 1, 3, 2, а это последовательность 1, 4, 2, 3. Исходя из этого принципа, продолжим «ручное» построение последовательности. Результаты для первых значений N приве- дены в таблице. N Место в последовательности 1 2 3 4 5 6 7 8 9 10 11 12 3 1 3 2 1 4 2 3 5 4 5 3 2 6 1 4 6 3 2 5 1 4 6 2 5 3 8 1 4 6 8 2 5 3 9 1 4 6 8 9 5 3 2 10 1 4 6 8 10 5 3 2 9 1 4 6 8 10 11 3 7 2 9 5 Иг 1 4 6 8 10 12 3 7 2 9 5 bd Если с генерацией первой части последовательности слож- ностей нет (начинается с 1, а затем четные числа, начиная с 4), то вторую получить несколько сложнее. При нечетных значе- ниях на месте N Div 5 + 1 записывается N. А затем на четных местах j числа j-1 и нечетных j — первое четное число, получа- емое в результате операции j:=(j+l) Div 2, из которого тоже вычитается единица. Исключения для вычитания единицы сле- дует сделать для значения j, равного 2. Программа генерации последовательности, для которой сортировка Хоара имеет вре- менную сложность О(А2), имеет вид. Program sequence; Var i,j, N:Integer; Begin WriteLn (' Длина последовательности '); ReadLn (N) ; Write ('1',' '); i:=2; While (2*1<=N) Do Begin Write (2*1,' '); Inc (i) ; End; 9—452
258 Часть третья If (N>1) And Odd(N) Then Begin Write(N,’ '); Inc (i) ; End; While i<=N Do Begin J : =1; While Odd (j) Do j:=(j+1) Div 2; If j>2 Then Write (j-1,' ') Else Write (j,' '); Inc (i) ; End; WriteLn; End. Пирамидальная сортировка. Метод предложен Дж. У. Дж. Уильямсом и Р. У. Флойдом в 1964 году. Элемен- ты массива А образуют пирамиду, если для всех значений i вы- полняются условия: A[i]<A[2*i] и A[i]<A[2*i+l]. Пример. Эле- менты массива 8 10 3 6 13 9 5 12 не образуют пирамиду (А[1 ]>А[3],А[2]>А[4] и т. д.), а элементы 3 6 5 10 13 9 8 12 — образуют. Очевидно, что часть элементов массива A[N/2+1..N] независимо от их значений образует пирамиду. Предположим, что нам необходимо отсортировать элементы массива А (8 10 3 6 13 9 5 12) по невозрастанию. Идею сортировки поясним с по- мощью следующей таблицы (курсивом выделена часть элементов массива, образующих пирамиду, жирным шрифтом — отсортиро- ванная часть массива). В конечном итоге массив отсортирован. Г д Комментарии 8 10 3 6 13 9 5 12 Строим пирамиду из элементов Ас 1-го по 8-й 8 10 3 6 13 9 5 12 3 8 1036 1395 12 4 8 63 10 13 9 5 12 5 365 10 1398 12 6 1265 10 13983 Меняем местами 1-й и 8-й элементы || 7 1265 10 13983 Строим пирамиду из элементов А с 1-го по 7-й 8 1265 10 13983 9 1265 10 13983 10 568 10 139 123 11 1268 10 13953 Меняем местами 1-й и 7-й элементы 12 1268 10 13953 Строим пирамиду из элементов А с 1-го по 6-й 16 9 108 12 13653 Меняем местами 1-й и 6-й элементы 1 17 9 108 12 1365 3 Строим пирамиду из элементов Ас 1-го по 5-й |
Массив — фундаментальная структура данных 259 18 19 20 21 29 30 Меняем местами 1-й и 5-й элементы____ Строим пирамиду из элементов Ас 1-го по 4-й Меняем местами 1-й и 2-й элементы Пусть мы умеем строить пирамиду из элементов массива А от 1 до q (процедура Pyram(q)'), тогда процедура сортировки имеет вид: Procedure Sort; Var t,w:Integer; Begin t:=N; Repeat Pyram(t); ( *Строим пирамиду из t элементов. ★} w: =A [ 1 ] ; A [ 1 ] : =A [ t ] ; A [ t] : =w ; l *Меняем местами 1 -й и t-й элементы. *) Dec (t) ; Until t<2; End; Из таблицы мы видим, что процесс построения пирамиды начинается с конца массива. Последние q Div 2 элементов явля- ются пирамидой по той простой причине, что удвоение индекса выводит нас за пределы массива. А затем берется очередной справа элемент и «проталкивается» по массиву до тех пор, пока он не будет меньше элемента с удвоенным индексом по отноше- нию к индексу рассматриваемого элемента. Процесс продолжа- ется до тех пор, пока мы «не поставим» на свое место первый элемент массива. Приведем текст процедуры. Procedure Pyram(q:Integer); Var r,i,j,v:Integer; pp:boolean; Begin r:=q Div 2+1;{*Эта часть массива является пирамидой. While г>1 Do Begin {*Цикл по элементам массива, для которых необходимо найти место в пирамиде. *} Dec (г) ; i;=r; v:=A[i];{*Индекс рассматриваемого элемента и сам элемент. *)
260 Часть третья j:=2*1;{*Индекс элемента, с которым происходит сравнение. *) pp:=False;{*Считаем, что для элемента не найдено место в пирамиде. ★) While (J<=q) And Not pp Do Begin If j<q Then If A[j ]>A[j+l] Then Inc(j) ; {★Сравниваем с меньшим элементом. *) If v<=A[j] Then pp:=True {*Элемент находится на своем месте. *} Else Begin A fi] : =А [j ] ; i: =j ;j : =2*i ;Eno ; (^Переставляем элемент и идем дальше по пирамиде.*} End; All]:=v; End; End; Метод имеет эффективность порядка O(N*logN ). Однако если рассмотреть вышеприведенную логику, то окажется, что ее оценка O(N2*logN). При сортировке процедура Pyram вызыва- ется N-1 раз. Эта процедура состоит из двух вложенных циклов сложности N и — logN. Итак, упорядочивание одного элемента требует не более logN действий, построение полной пирамиды N*logN действий. В вышеприведенной логике после каждой пе- рестановки элементов вновь строится пирамида для оставших- ся элементов массива. А зачем? Пирамида почти есть. Требует- ся только «протолкнуть» верхний элемент на свое место (строки таблицы с номерами 7-9, 12-14, 17-18, 21-22, 25-26, 28). Вы- делим «проталкивание» одного элемента в отдельную процеду- ру Рг(r,q), где г — номер проталкиваемого элемента, q — верх- нее значение индекса. Procedure Pr(г, q:Integer); Var г,i,j,v:Integer; pp:Boolean; Begin i:=r; v:=A[i];{★Индекс рассматриваемого элемента и сам элемент. ★) J : =2* 1; { *Индекс элемента, с которым происходит сравнение.*) pp:=False;{★Считаем, что для элемента не найдено место в пирамиде. *) While (j<=q) And Not pp Do Begin If J<q Then If A[j ] >A[j+l ] Then Inc(j);
Массив — фундаментальная структура данных 261 {^Сравниваем с меньшим элементом.★) If v<=A[j] Then pp:=True (^Элемент находится на своем месте. *) Else Begin А[1]:=А[j];i:=j;j:=2*i;End; {^Переставляем элемент и идем дальше по пирамиде. *} End; A[1]:=v; End; Какие изменения произойдут с процедурой Sort? Procedure Sort; Var t,w,i:Integer; Begin t:=N Div 2+1;{*Эта часть массива является пирамидой.★) For i:=t-l DownTo 1 Do Pr (i, N) ; { *Строим пирамиду (только один раз).★) For i:=N DownTo 2 Do Begin w:=A[1];A[1]:=A[i];A[i]:=w;{*Меняем местами 1 -й и i-й элементы. *} Pr (1,1-1);{*Проталкиваем 1-й элемент.★) End; End; Сортировка 5 элементов за 7 сравнений. Этот метод «быст- рее» рассмотренных ранее (5*Log5). Рассмотрим массив А из пяти элементов. С помощью процедуры Swap по значению ин- дексов i, j переставляются два элемента. Procedure Swap (i,j:Integer); Var t:Integer; Begin t:=A[i];A[i]:=A[j];A[j]:=t; End; Действия, приведенные на рисунке (3 сравнения), приводят все 120 различных первоначальных ситуаций к виду: А[1 ]<А[2]<А[4] и А[3]<А[4]. Что мы сделали? Сравнили попарно первые четыре элемента и сравнили максимальные элементы пар. Что осталось сделать? Вставить А[5] в отсорти- рованный массив из трех элементов, а затем вставить А[3] в от- сортированный массив из четырех элементов с учетом установ- ленного факта А[3]<А[4]. На то и другое действие отводится по два сравнения. Логика их выполнения приводится на рисун- ках. В некоторых случаях, например если А[5]<А[4], переста- новка элементов массива не выполняется. Объясните, почему возможны такие ситуации?
262_______ ____________________________________________________Часть третья (Лд[ 1 ] >А[2]^ нет 1 — (2^[3]>А[4]2^) £ нет 1 ——- <^А[2] > А[4]О £ 1 ► Swap( 1,2) @ ► Swap(3,4) )а ► Swap(2,4) Swap 1,3 нет (^A[1]>A[2] нет ^S[i]>a[2£^) нет ^^^да Swap(1,2) (^A[1]>A[2] нет^з^-—^^2 <^A[1]>a[213) C нет^/ \*Да Swap(1,2) да A[1]>A[2]^) нет^ 5"-эдт2> |lsi;:g "да А[1]>А[гР) нет I ^^~-~---^__да Swap(1,2) Swap(1,2) SwaP(1.2) Swap(1,2) Swap(1,2)
Массив — фундаментальная структура данных 263 Такой способ сортировки пяти элементов предложен Г. Б. Де- мутом в 1956 году. Ниже приводится фрагмент программной реализации этой логики. Begin If А[1]>А[2] Then Swap(l,2)/ If А[3]>А[4] Then Swap (3,4); If A[2]>A [ 4] Then Begin Swap(2, 4)/Swap (1,3)/End; If A[5]<A[2] Then If A[5]<A[1] Then Begin Swap(5,4) ;Swap(4,2) /Swap (2,1) /End Else Begin Swap(2,5);Swap (4,5)/ End Else If A[5]<A[4] Then Swap(5,4); If A[3]<A[2] Then If A[3]<A[1] Then Begin Swap (2,3)/Swap(1,2) /End Else Swap (2,3) Else If A[3]>A[4] Then Swap (3,4)/ End. Задания для самостоятельной работы 1. Дан массив 7 9 13 1 8 4 10 11 5 3 6 2. Видим возрастающий участок 7 9 13, а справа, если читать справа налево, есть уча- сток 2 6. Слияние этих двух фрагментов массива дает 2 6 7 9 13. А затем «сливаем» 1 8 и 3 5 11, получаем 1 3 5 8 11. Запи- сываем в массив и получаем: 2 6 7 9 13 4 10 118 5 3 1. Обра- тите внимание на то, как записываем! Продолжим. Если с пер- выми фрагментами 2679 13 и135811 все ясно, то вторые 4 10 и 10 перекрываются. Необходимо учесть сей факт и не дублировать 10 при записи в массив. Получаем: 1 2 3 5 6 7 8 9 11 13 10 4. Последнее «слияние» дает 123456789 10 11 13. Массив отсортирован. Этот метод называется у Д. Кнута «сортировкой естественным двухпутевым слиянием». Реали- зация его с использованием рекурсии доставит много хлопот, а при получении результата — удовлетворение от хорошо сде- ланной работы. При этом следует не забывать, что тестиро- вание решений должно составлять не менее 30% времени Ва- шей работы над программой! 2. Изменим схему выбора участков для «слияния». Не будем ис- кать монотонные участки, а работаем с участками фиксиро- ванной длины («простое двухпутевое слияние»). Пример. Исходный массив: 13 1 7 5 4 3 9 10 6 19 14 16 23 2 4 8.
Часть третья 1-й шаг (длина 1). 2-й шаг (длина 2). 3-й шаг (длина 4). 4-й шаг (длина 8). 8 13 2 7 4 16 9 19 10 6 14 3 23 5 4 1. 1 4 8 13 3 4 14 16 19 10 9 6 23 7 5 2. 1 2 4 5 7 8 13 23 19 16 14 10 9 6 4 3. 1 2 3 4 4 5 6 7 8 9 10 13 14 16 19 23. Реализовать данный алгоритм сортировки. Размер массива не всегда совпадает с числом, равным степени двойки. 3. Интеграция алгоритма простых вставок с предыдущим упражнением дает новое задание. Разделим массив на уча- стки определенной длины. Каждый из них отсортируем с помощью алгоритма простых вставок, а затем используем «простое двухпутевое слияние». Так, в предыдущем приме- ре слияние используется на 3-м и 4-м шагах, а первые два за- меняются работой по алгоритму простых вставок. 4. Обобщите предыдущие задания заменой слова «двухпутевое» на «трехпутевое» («четырехпутевое», ..., «/г-путевое»). Значи- тельное увеличение программного кода при этом недопустимо. 5. Считается, что выбор элементах случайным образом в мето- де быстрой сортировки оставляет среднюю эффективность не- изменной при всех типах массивов (упорядоченный, распре- деленный по какому-то закону и т. д.). Реализуйте эту идею. 6. Следует отметить, что метод быстрой сортировки является за- тратным с точки зрения использования оперативной памяти. Заменить рекурсивную схему реализации логики на не рекур- сивную и оценить значение N, при котором Ваша программа не будет компилироваться по этой причине. Разумеется, та- кую оценку можно сделать и при рекурсивной реализации, просто тип ошибки будет другой. 7. Изменить логику работы в методе пирамидальной сортиров- ки так, чтобы элементы массива А сортировались в порядке неубывания. 8. В таблице приводятся наименьшие известные значения чис- ла сравнений, достаточных для сортировки последователь- ностей из N элементов. ?L_ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Я j Число | сравнений 0 1 3 5 7 10 13 16 19 22 26 30 34 38 3 Разработайте алгоритмы сортировки для нескольких значе- ний N из таблицы, больших 5.
Массив — фундаментальная структура данных 265 9. Дано W отрезков [x[i], y[i]] на прямой (i=l..N). Найти мак- симальное k, для которого существует точка прямой, покры- тая k отрезками. Число действий O(N*LogN). Примечание Записать левые и правые концы отрезков в один массив. Сформиро- вать массив признаков, соответствующий концам отрезков, -1 — левый конец, +1 — правый конец. Отсортировать первый массив, одновременно переставляя элементы второго массива. Затем при просмотре массива подсчитывать суммы признаков. Максималь- ное значение этой суммы есть ответ задачи. 10. Значениями элементов сортируемого массива являются чис- ла 1, 2, 3. Сортировка элементов по неубыванию выполняет- ся с помощью попарных перестановок элементов. Отсортиро- вать массив за минимальное количество сравнений.
266 Часть третья Занятие № 21. Поиск данных План занятия □ изучение различных схем поиска данных, написание и ис- следование программ по образцам процедур поиска; □ выполнение самостоятельной работы. Линейный поиск. Основной вопрос задачи поиска: где в за- данной совокупности данных находится элемент, обладающий заданным свойством? Большинство задач поиска сводится к простейшей — к поиску в массиве элемента с заданным значе- нием. Разберем его. Пусть требуется найти элемент X в массиве А. В данном случае известно только значение разыскиваемого элемента, никакой дополнительной информации о нем или о массиве, в котором его надо искать, нет. Начнем последовате- льный просмотр массива и сравнение значения очередного рас- сматриваемого элемента массива А с X. Напишем фрагмент: For i:=l То N Do If A[i]=X Then k:=i; Находится место последнего элемента в А, равного X, при этом массив просматривается полностью. А если первого? For i:=N DownTo 1 Do If A[i]=X Then k:=i; А если исключить лишние проверки? Совпадение найдено, зачем «пустая» работа? J.: =7 ; While (1<=N) And (A[i]OX) Do Inch); If i=N+l Then <X нет в A> Else <значение I указывает на место совпадения!; Этим фрагментом мы решаем задачу поиска первого совпа- дения. Для нахождения последнего совпадения его требуется изменить. 1:=N; While (1>0) And (A(i]OX) DoDec(i); If 1=0 Then <X нет в A> Else <значение i указывает на место совпадения!; Примечания 1. Необходимо обратить внимание на порядок проверки составного условия цикла. Предполагается, что второе условие ие проверяет- ся, если результат логического выражения ясен после проверки первого условия. В противном случае возникает ошибка из-за вы- хода индекса за границы массива. 2. Использование директивы {$R+} уместно при изучении данной темы. 3. Замену цикла While иа цикл Repeat ... Until можно использовать в качестве упражнения.
Массив — фундаментальная структура данных 267 Вспомним технику «барьерных» элементов из темы «сорти- ровка» и применим ее для изучения поиска элемента в массиве. Это требует изменения типа массива A (Type MyArray=Array [O..NMax] Of Integer; или МуArray= Array [l..NMax+l] Of In- teger; Var A:MyArray, значение Nmax определяется в разделе констант). i:=l;A[N+l]:=Х; While A[i]OX Do Inc(i); If i=N+l Then <X нет в A> Else (значение I указывает на место совпадениях; Или для поиска последнего совпадения: i:=N;A[0]:=Х; While A[i]OX Do Dec(i) ; If 1=0 Then <X нет в A> Else (значение i указывает на место совпадения>; Очевидно, что число сравнений зависит от места нахожде- ния искомого элемента. В среднем количество сравнений равно (N+l) Div 2, в худшем случае, если элемента нет в массиве, N сравнений. Эффективность поиска O(N). Бинарный поиск. Если же заранее известна некоторая инфор- мация о данных, среди которых ведется поиск, например массив данных отсортирован, то удается сократить время поиска, испо- льзуя бинарный (двоичный, дихотомический, метод деления по- полам — другие термины, обозначающие ту же идею) поиск. Итак, требуется определить место X в отсортированном (на- пример, в порядке неубывания) массиве А. Идея бинарного ме- тода стара как мир. Еще древние греки говорили — «разделяй и властвуй». Вот и делим, делим пополам и сравниваем X с эле- ментом, который находится на границе этих половин. Отсорти- рованность А позволяет, по результату сравнения со средним элементом массива, исключить из рассмотрения одну из поло- вин. А остальное, остальное технические детали, требующие очень пунктуального прописывания — необходимого, но, к сча- стью, не достаточного, навыка специалиста по информатике. Пример. Пусть Х=6, а массив А состоит из 10 элементов: 3 5 6 8 12 15 17 18 20 25. 1-й шаг. Найдем номер среднего элемента: m=j^—^^=5. Так как 6<А[5], далее можем рассматривать только эле- менты, индексы которых меньше 5. 3 5 6 8 12 15 17 18 20 25.
268 Часть третья 2-й шаг. Рассматриваем лишь первые 4 элемента массива, нахо- дим индекс среднего элемента этой части 6>А[2], следовательно, первый и второй элементы из рассмотрения исключаются: З^б 6 8 42-15 47 18 20 25. 3-й шаг. Рассматриваем два элемента, значение т=^ * *^=3: 8—5 6 8 12 15-17 18-26 25; А[3]=6. Элемент найден, его номер — 3. Программная реализация бинарного поиска может иметь следующий вид. Procedure Search; Var i,j,т:Integer; f:Boolean; Begin i:=l; j:=N;{*Ha первом шаге рассматривается весь массив.*} f:=False;{‘Признак того, что X не найден. *) While (i<=j) And Not f Do Begin m:=(i+j) Div 2;{*Или m:=1+ (j-i) Div 2;, так как 1+fj-i) Div 2= (2*i+(j-i)) Div2=(i+j) Div 2.*) If A[m]=X Then f:=True {‘Элемент найден. Поиск прекращается. *) Else If A [m] <Х Then i: =m+l {‘Исключаем из рассмотрения левую часть A.*} Else {‘Правую часть.★) End; End; Предложим еще одну, рекурсивную, реализацию изучаемого поиска. Значение переменной t является индексом искомого элемента или ноль, если элемент не найден. Procedure Search(i,j:Integer;Var t:Integer); {★Массив А и переменная X - глобальные величины*} Var m:Integer; Begin If i>j Then t:=0 Else Begin m:=(i+j) Div 2; If A[m]<X Then Search(m+1,j,t) Else If A[m]>X Then Search(i,m-1,t) Else t:=m; End; End;
Массив — фундаментальная структура данных Каждое сравнение уменьшает диапазон поиска приблизите- льно в два раза. Общее количество сравнений имеет порядок O(N*logN). Рассмотрим обратную задачу. Дана позиция элемента (Роз) в неупорядоченном массиве А из N элементов и количество допу- стимых сравнений (L). Определить все интервалы значений в диапазоне от 1 до 10000, при которых элемент А с номером Pos может быть найден за L сравнений с помощью алгоритма би- нарного поиска. Массив А, как таковой, в решении задачи не требуется. Не- обходимо проверять, совпадает ли средний элемент отрезка в схеме бинарного поиска со значением Pos и за сколько сравне- ний это совпадение достигается при заданном значении N. Оформим модифицированную схему бинарного поиска как фун- кцию Сап. Function Can(N: Integer): Boolean; Var i,p,q,ll: Integer; Begin p := 0; q := N-1; 11 ;= 0; While p <= q Do Begin i := (p+q) shr 1; Inc (11) ; If i = Pos Then Begin Can := (11=L) ; Exit; End Else If 1 > pos Then q := 1-1 Else p := 1+1; End; Can := False; End; Поясним схему работы функции данными ручной трассировки логики ее работы для различных значений N при Роз=10 и L=3. р Q / II N Сап р q 1 II N Сал 0 5 1 12 0 14 1 15 6 8 2 - 8 14 2 - 9 11 10 3 - True 8 10 9 3 - 0 12 6 13 10 10 10 - False 7 12 9 2 - 0 15 1 16 10 12 11 3 - 8 15 2 - 10 10 10 4 - False 8 10 9 3 - 0 13 6 14 10 10 10 4 - False | 7 13 10 2 - False
270 Часть третья Итак, только одно значение N (из рассмотренных в таблице) удовлетворяет условию задачи (N=12). Осталось организовать цикл по количеству элементов в массиве и запоминать интерва- лы значений, удовлетворяющих условиям поиска (функция Сап внутренняя по отношению к процедуре Solve). Procedure Solve; Const MaxN = 10000; MaxCnt - MaxN Shr 1; Var i,j: Integer; Res: Array [1..MaxCnt,1..2] Of Integer; Pos, L,Cnt: Integer; Begin ReadLn(Pos, L) ; Cnt := 0; i := Pos; Repeat J ••= i; While (j <= MaxN) And Can(j) Do inc(j); If i < j Then Begin inc(Cnt) ; Res[cnt,1] := i; Res[cnt,2] := j~l; {^Запоминаем интервал значений. *) End; Until (i >= MaxN); WriteLn (cnt) ; For i : = 1 To Cnt Do WriteLn ( Res[i,l],' ', Res[i,2J); End; Ответом задачи при Pos=10 и L=3 являются 4 интервала: 12-12, 17-18, 29-30, 87-94, а при Роз=9000 и L=2 таких интер- валов нет. Случайный поиск. Организация поиска k-ro элемента в неу- порядоченном массиве X возможна следующим образом. Выби- рается случайным образом элемент с номером q. Массив X раз- бивается на три части: элементы, меньшие X[q], равные X[q] и большие X[q]. А затем, в зависимости от количества элемен- тов в каждой части, выбирается одна из частей для дальнейше- го поиска. Теоретическая оценка числа сравнений имеет поря- док k*N, т. е. для худшего случая N2, но на практике он работает значительно быстрее.
Массив — фундаментальная структура данных 271 Function Search (N, к:Integer;X:MyArray):Integer; {*N - количество элементов в массиве X, к — номер по порядку разыскиваемого элемента. *) Var i,cnl,cne,cnm,q:Integer; SI,Se,Sm:MyArray; Begin If N=1 Then Search:=X[1] Else Begin FillChar(SI,SizeOf(SI),0) ; FillChar (Se, SizeOf (Se) , 0) ; FillChar (Sm, SizeOf (Sm) , 0) ; cnl:=0;cne:=0;cnm:=0; q: =Integer (Random (N)) +1 ; For i:=l To N Do {*Разбиение на три части. ★} If X[i)<X[q] Then Begin Inc(cnl); SI[cnl]:=X[i] ; End Else If X[i]=X[q] Then Begin Inc (cne) ; Se [cne]:=X[i];End Else Begin Inc (cnm) ; Sm [cnm] : =X[i] ;End; If cnl>=k Then Search:^Search(cnl,k,SI)(*Выбор части для дальнейшего поиска.*) Else If cnl+cne>=k Then Search:=X[q] Else Search research(cnm,k-cnl-cne,Sm); End; End; Поиск элемента массива за линейное время. Для реализации этой схемы поиска требуется уметь сортировать массив из 5 элементов за 7 сравнений. Идея поиска поясняется рисунком. Из отсортированных пя- тиэлементных массивов формируется массив из средних элемен- тов — медиан (М). Он на рисунке изображен в упорядоченном виде. В массиве М выбирается средний элемент m и относительно него исходный массив разбивается на три части, как в преды-
272 Часть третья дущей схеме случайного поиска. В части А на рисунке выделе- ны элементы, заведомо большие т, в части В — меньшие т. Как минимум 1/4 часть элементов исходного массива исключа- ется из дальнейшего поиска за счет однократного применения действий этой схемы. Function Search (N, к : Integer ;Х .-MyArray) -.Integer ; {*N — количество элементов в массиве X, к — номер элемента (k<N) . *} Var i,j,cnl,cne,cnm,q,r:Integer; М, SI,Se,Sm,Y:MyArray; Begin If N<-5 Then Begin Sort(N,X); Search:=X[k];End {★Если количество элементов в массиве меньше или равно 5, то сортируем элементы массива за 7 сравнений и выбираем к-й элемент. ★} Else Begin i:=l;r:=0; While i<=N Do Begin g—0; While (g<=5) And (i+j<=N) Do Begin Inc (j);Y[j]:=X[i+g-1] ;{★Разбиваем на пятиэлементные массивы (не более).★} End; Inc (1,5) ; Sort (j, Y) ; Inc(r) ; M[r]:=Y[g Div 2+1];{★Средний элемент отсортированного массива Y записываем в массив М. *) End; q:=Search(r,r Div 2,M) ;(*Находим средний по значению элемент в массиве медиан.★) cnl:=0;спе:=0;епт:=0; FillChar(SI,SizeOf(SI),0); FillChar(Se,SizeOf(Se),0); FillChar(Sm,SizeOf(Sm),0); For i:=l To N Do {★Разбиваем массив на три части.★) If X[i]<q Then Begin Inc(cnl); SI[cnl]:=X[i]; End Else If X [ i]—q Then Begin Inc(cne); Se[cne]:=X[i];End
Массив — фундаментальная структура данных 273 Else Begin Inc (епт) ;Sm [епт] : =Х [1 ] ;End; If cnl>=k Then Search:^Search(cnl,k,SI){*Выбор части массива для дальнейшего поиска.★) Else If cnl+cne>=k Then Search:=X[q] Else Search:=Search (cnm,k-cnl-cne,Sm); End; End; Оценим время работы алгоритма T(N). После поиска элемен- та т не менее половины найденных медиан будут больше или равны т. Следовательно, по крайней мере половина рУ/б"] групп дадут по три числа, больших или равных т, кроме, может быть, последней группы и группы, содержащей т. Итак, 3А( р/2* рЛГ/б-] - 2“])>3*ЛГ/10-6 элементов исключаются из дальней- шей обработки. Остается 7'АЛ’/10+6 элементов. Поиск среднего элемента в массиве медиан (выделен жирным шрифтом) требу- ет T(N/5) времени. Рекурсивный вызов процедуры поиска (вы- делен жирным шрифтом) при разбивке массива требует Т(7*N/10+ +6). Остальные шаги алгоритма, включая сортиров- ку 5 элементных массивов, выполняются за время O(N). Таким образом, T(N)<T(N/5)+T(7*N/10+6)+O(N). Сумма коэффи- циентов в правой части 1/5+7/10=9/10 меньше 1, согласно тео- реме (занятие 17) T(N)<c*N для некоторой константы с. Примечание В разных источниках приводятся различные схемы оценки време- ни работы этого алгоритма и значения А (50 или 70), начиная с ко- торых время поиска пропорционально O(N). Продолжим тему бинарного поиска. Рассмотрим следующую задачу. Дан целочисленный массив М из N элементов (N — не- четно и 5<ЛГ<1499). Значения элементов массива различны и принимают значения от 1 до N. Пример: А=5, А=2, 5, 4, 3, 1. Требуется найти номер элемента i такой, что количество эле- ментов М, меньших M[i ], равно количеству элементов, боль- ших, чем M[i]. Для примера i равно 4. Единственной разре- шенной операцией при поиске является Med3, которая вызыва- ется с тремя различными номерами элементов массива в каче- стве аргументов и возвращает номер элемента, имеющего сред- нее значение. Для нашего примера Med3(l,2,3) возвращает 3, Med3(3,4,l) возвращает 4 и Med3(4,2,5) возвращает 4. Ответ 4. Пусть есть следующий массив М. 1| Номер 1 2 | 3 4 5 6 7 8 9 10 12 13 |Значение 8 1 I 3 7 5 4 13 10 2 11 12 9 6
Часть третья Все, что мы можем сделать с помощью операции Med3, это разбить массив на три части, выбрав случайным образом два несовпадающих номера (а и Ь) элементов. Не деление пополам, как в бинарном поиске, а деление на три части — «тринарный» поиск. Пусть а=5 и Ь=12. Просматриваем все номера элемен- тов, помеченные признаком True. В начальный момент време- ни для всех элементов этот признак равен True. Если операция Med3(a,b,i) дает в качестве результата: □ значение а, то помечаем элемент с номером i как элемент, принадлежащий левой части (признак _Left); □ значение i, то помечаем элемент с номером i как элемент, принадлежащий средней части (признак _Middle)-, □ значение Ь, то помечаем элемент с номером i как элемент, принадлежащий правой части (признак _Right). В таблице буквами L, М, R обозначены признаки _Left, _Midlle, _Right. Буквой N обозначен признак _None, элемент не рассматривается на данном шаге анализа. | Номер 1 2 3 4 5 6 | 7 I 8 | 9 f 10 I 11 I 12 I 13 | || Признак м L L М N 1Я Я /. Я Я М|М Итак, разбили, а что дальше? Напрашивается единственно разумный ход — выбрать одну из трех частей и продолжить по- иск (другого не дано). Вся сложность задачи в выборе парамет- ров поиска и в аккуратном переходе от одного этапа поиска к другому. Какую часть выбрать для следующего этапа поиска? Мы имеем в левой части результата поиска 4 элемента, в пра- вой — 4 и в средней (без учета а и Ь) — 3. Первоначально осуще- ствлялся поиск элемента со значением 7 в массиве из 13 элемен- тов. После этого этапа необходимо искать элемент со вторым значением среди элементов, помеченных признаком Middle. Часть параметров поиска определена: количество и признак эле- ментов, среди которых ищется искомый; номер элемента. Еще один параметр — одно из предыдущих значений а или Ь — требу- ет пояснений (это самый сложный момент). Поясним рисунком.
Массив — фундаментальная структура данных 275 Оказалось, что поиск следует продолжать в левой (по отно- шению к а) части. Генерируем новые значения а Ь (обозначены как ап, Ьп). Если после этого средний элемент (Med3(an,bn,ac 7) находится не на месте Ьп, то структура последующего поиска нарушается. Необходимо поменять значения у ап и Ьп. Если после первого этапа поиска мы выяснили, что его следует про- должать в левой части, то и следующее деление на три части должно осуществляться с учетом этого результата. Примечания 1. Прервите чтение и проделайте ручной просчет поиска по данным из условия задачи без смены значений. Результат уже на этом про- стом примере окажется отрицательным. 2. Сделайте еще два рисунка и выясните особенности использова- ния функции Med3 при продолжении поиска в средней и правых частях. Program Median;(“Без ввода элементов исходного массива М и функции Med3. *} Const MaxN = 1500; Type TDir = (_None, _Middle, _Left, _Right) ; Var N: Integer; Mas : Array [l..MaxN] Of TDir; Lt, Rt : Integer; (“Количество элементов в подмножествах ★) Procedure MakeMiddle(Dir: TDir) ;(‘Помечаем элементы, среди которых осуществляется поиск. *) Var i: Integer; Begin For 1 := 1 To N Do If Mas[i] = Dir Then Mas[i] := ^Middle Else Mas[i] := _None; End; Procedure Swap(Var a,b: Integer); Var c: Integer; Begin c := a; a := b; b := c; End; Procedure GetNext (Var a,b: Integer; len: Integer); (“Генерируем номера а и b.“) Var i,t,ca,cb: Integer; Begin t := 0; ca := Random (len) +1; cb Random (len) +1; While ca=cb Do cb Random (len) +1;
276 Часть третья For i := 1 То N Do {★Сложность в том, что номера оставшихся элементов для поиска идут не подряд. *} If Mas[i] = —Middle Then Begin Inc(t); If ca = t Then a := i; If cb = t Then b := i; End; End; Procedure Doit(Dir: TDir; Res, len, lb: Integer); {‘Основная процедура, параметры поиска описаны ранее. *) Var а, Ь, 1, t: Integer; Begin MakeMiddle(Dir); {★Выделяем элементы, среди которых осуществляется поиск.★} If len = 1 Then Begin a := 1; While Mas[a]<> _Middle Do inc(a); WriteLn (a) ; End Else Begin GetNext (a,b,len); If (Dir=_Right) Then Begin IfMed3(lb,a,b) <> a Then Swap (a,b) End Else If Med3(a,b,lb) <> b Then Swap (a,b); {*Корректируем значения а и b с учетом предыдущего поиска. Пояснение приведено на рисунке в тексте. *) If len = 2 Then Begin If Res = 1 Then WriteLn (a) Else WriteLn(b) End Else Begin It := 0; rt : = 0; Mas[a] := _None; Mas [b] :=_None; For i := 1 To n Do If Mas[i] = _Middle Then Begin t : = Med3(a,b,i); If t = b Then Begin inc(rt) ; Mas[i] : = _Right End{★Пишем в правую часть.★) Else If t = a Then Begin inc (It); Mas[i] : = —Left End; End;
Массиа — фундаментальная структура данных 277 If Res = lt+1 Then WriteLn (a) Else (‘Искомый элемент попал на границу интервала, поиск завершен. ★} If Res = len-rt Then WriteLn (b) Else If Res < lt + 1 Then Doit (-Left, Res, It, a) { *Поиск с новыми параметрами. ★} Else If res < len-rt Then Doit (_Middle, Res-lt-1,len-lt-rt-2,b) Else Doit(_Right,Res-len+rt,rt,b); End; End; End; Begin ReadLn (N);(*И ввод исходного массива *j Randomize; FillChar (Mas, SizeOf(Mas), —Middle) ; Doit (—Middle,N shr 1+1, N, 1);{‘Первый вызов, N shr 1+1 — среднее значение, 1- условно считаем, что ранее был первый номер (обеспечивает правильный поиск).★) End. Поиск второго минимального элемента в массиве. Известно, что второй минимальный элемент в массиве из А элементов можно найти за A’+pLog№|-2 сравнений. Так, при А=8 это 9 сравнений. Попробуйте свои силы, не читая дальнейшее изло- жение, самостоятельно найдите алгоритм решения задачи. Оче- видно, что без поиска минимального элемента найти второй ми- нимальный невозможно. Поиск минимального элемента требует ЛГ-1 сравнений, от этого никуда не уйти. Осталось Log'A']-! сравнение. Если искать по прежней схеме, исключив найден- ный элемент, то потребуется N-2 сравнения. Однако при этом мы как бы забываем, что делали на первом этапе. Единственно возможный (логически) вывод заключается в том, чтобы испо- льзовать результаты поиска минимального элемента для поис- ка второго минимального элемента. Но как это сделать? Пусть Л’=8 и элементы массива А имеют значения: 81, 34, 2, 90, 51, 45, 14, 31. На рисунке изображена схема поиска минимального элемента. Сравниваем соседние пары элементов, а затем срав- ниваем минимальные элементы пар и т. д. Если убрать записи в круглых скобках на рисунке, то это обычный поиск минима- льного элемента в массиве за А-1 сравнение (напишите проце- дуру такого поиска).
278 Часть третья 8 34 2 90 51 45 14 31 V V V V 34(81) 2(90) 45(51) 14(31) 2(34,90) 14(45,31) 2(14,34,90) А сейчас обратимся к записям в круглых скобках. Рассмот- рим, например, 2(34,90). Почему мы не включили 81? Причина в том, что этот элемент не может быть вторым минимальным элементом. Только, 34 и 90 кандидаты на это место. Точно так же и 14(45, 31). Элемент 51 исключается из «цепочки» претен- дентов. Последнее 7-е сравнение оставляет в этой «цепочке» 2(14, 34, 90) только три элемента, ибо 45 и 31 не могут претен- довать на эту роль, 14 заведомо их меньше, а этот элемент включен в «цепочку». Итак, в списке 3 элемента, за 2 сравне- ния мы находим второй минимальный элемент, общее количе- ство сравнений равно 9. Постройте точно такую же схему для массива из 16 элементов. Через 15 сравнений в последней «це- почке» окажется 4 элемента. За 3 сравнения мы найдем второй минимальный элемент. Итого, 18 сравнений. Перейдем к реализации логики. Описание данных програм- мной реализации имеет вид: Program SearchTwoMin; ( *5ез ввода исходных данных, вывода результата и основной программы.*} Const MaxN=10000; Type MyArray=Array[1..MaxN] Of Integer; Var A, IndNext, IndFirst: MyArray; (*Массив элементов. Номер следующего элемента цепочки. Номера первых элементов цепочек. *} N: Integer;{количество элементов в массиве А. *) Cntlf: Longlnt;{*Счетчик числа сравнений.*) res: Integer;{*Второй минимальный элемент. *) Предположим, что после поиска минимального элемента в дополнительном массиве IndNext записаны индексы (номера) элементов последней цепочки, например, так: *, 4, 7, 0, *, *, 2, * а индекс минимального элемента массива (а это 3 для рас- сматриваемого примера) является значением переменной Ind First[l]. Символом «*» обозначено не интересующее нас значе- ние, оно может быть любым, а 0 — признак конца «цепочки».
Массив — фундаментальная структура данных 279 Задача решена. Просмотр элементов последней «цепочки» по этим данным реализуется с помощью следующей процедуры. Procedure Search; Var t: Integer; Begin t:-IndNextГIndFirst[1]};{*Для примера значением t является 7. Определяем номер первого элемента «цепочки». *) res:=А[t];{*Начальнсе присвоение. *j t:=IndNext[t];{^Переход ко второму элементу «цепочки».★} While t<>0 Do Begin Inc(Cntlf);{^Изменяем значение счетчика числа сравнений.*) If A[t]<res Then res:-A[t];{^Изменяем значение минимального элемента. *} t:^IndNext[t];(* Переадресация к следующему элементу «цепочки». *} End; End; Осталось сформировать (создать) значения элементов масси- вов IndNext и IndFirst в процессе поиска минимального эле- мента массива А (напомним его значения для рассматриваемого примера: 81, 34, 2, 90, 51, 45, 14, 31). В начальный момент времени у нас 8 цепочек по одному элементу. После первого шага работы получаются 4 цепочки по два элемента. Номер первого элемента первой цепочки 2 (IndFirst[l]), номер следу- ющего элемента 1 (IndNext[2] ). Номер первого элемента вто- рой цепочки 3 (IndFirst[2]), номер следующего элемента 4 (Ind Next[3]). Номер первого элемента третьей цепочки 6 (Ind- First[3]), номер следующего элемента 5 (IndNext[6]). Номер первого элемента четвертой цепочки 7 (IndFirst[4]), номер сле- дующего элемента 8 (IndNext[7]). Результаты действий на пер- вом шаге обработки приведены в таблице. IndFirst IndNext j Начальные значения 1 2 3 4 5 6 7 8 0 0 0 0 0 0 0 0 A[lndFirst[1]] и A[lndFirst[2]] 2 2 3 4 5 6 8 0 0 0 0 0 0 0 A[lndFirst[3]] и A[lndFirst[4]] 2 3 3 4 5 6 7 8 0 4 0 0 0 0 0 A[lndFirst[5]] и A[lndFirst[6]J 2 3 6 4 5 6 8 0 0 0 5 0 0 A[lndFirst[7]] и A[lndFirst[8]] 2 3 6 7 5 6 7 8 0 1 4 0 0 5 8 0
280 Часть третья В результате осталось 4 элемента, точнее 4 цепочки эле- ментов, номера их первых элементов являются значениями Ind First[l] - 7ndFirsf[4]. После второго шага остается две цепочки элементов. IndFirst IndNext j Начальные значения 2 3 6 7 0 0 0 5 8 0 A[lndFirst[1J] и A[lndFirst[2)J 3 3 6 7 0 4 2 0 0 5 8 0 | A[lndFirst[3J] и A[lndFirst[4]] 3 7 6 7 0 4 j 2 0 0 8 6 0 Номер первого элемента первой цепочки 3 (IndFirst [1 ]), за- тем 2 (IndNext[3]) и 4 (IndNext[2]). Номер первого элемента второй цепочки 7 (IndFirst [2]), затем 6 (IndNext[7]) и 8 (Ind Next[6]). Осталось последнее сравнение элементов A[Ind First[l ]] и A[IndFirst[2]]. Элементы массива IndNext после корректировки ссылок примут значения: 0, 4, 7, 0, 0, 8, 2, 0, а IndFirst[l ] останется равным 3. Значение А[7] больше Л/Зу, про цепочку седьмого элемента забываем, его включаем пер- вым элементом в цепочку третьего элемента, а старое значение IndNext[3] становится значением IndNext[7]. Procedure Create; Var i,lf,rg,t: Integer; Begin Cntlf:=0 ;{‘Счетчик числа сравнений.★} FillChar(IndNext, SizeOf(IndNext), 0) ; {*Начальные значения элементов массива IndNext. *} For i:=l To N Do IndFirst[i]:=i;{★ Начальные значения элементов массива IndFirst. *} t:=N; While t>l Do Begin {‘Номер шага, t Div 2 - количество сравнений на данном шаге. Для примера : 8, 4, 2.*) i:=2; While 2 <—t Do Begin Inc(Cntlf);{‘Изменяем значение счетчика числа сравнений. *) lf:=i-l; rg:=i-l; If A[IndFirst[i-l]]<A[IndFirst[i]] Then rg:=i Else lf:=i;{*B сравниваемой паре элементов массива A, rg - номер большего, a If - номер меньшего элементов, определяемых по значению из IndFirst. *}
Массив — фундаментальная структура данных {^Следующие три строки кода - самый сложный момент логики. Связываем цепочки сравниваемых элементов массива А. Изменяем номер первого элемента новой цепочки. *} IndNext[IndFirst[rg]]:=IndNext [IndFirst[If]]; IndNext[IndFirst[If]]:=IndFirst[rg]; IndFirstfi Div 2]:=IndFirst[If]; Inc(l, 2) ; End; If t Mod 2=1 Then IndFirst[ (t+1) Div 2]: = IndFirst [ t ];{ ★Учитываем нечетность значения N. *) t:=(t+l) D^v 2; End; End; Основная программа, как обычно, состоит из вызова процедур: Begin Init;{ *Ввод исходных данных.★} Create; Search; Done; { *Выводрезультата . *} End Хеширование. В предыдущем материале рассматривался по- иск, основанный на сравнении значений элементов массива с некоторым заданным значением переменной X (ключом). В ко- нечном итоге определялся адрес, номер элемента в массиве, где записано данное значение X. Рассмотрим простую задачу. Ко- личество различных буквосочетаний длиной не более 4 симво- лов из 32 букв русского алфавита чуть больше 106. Мы проана- лизировали какой-то текст и выписали в соответствующий массив все встреченные буквосочетания. После этого на вход нашей простой системы анализа текста поступает некоторое буквосочетание. Нам необходимо ответить, встречалось оно ра- нее или нет. Очевидно, что каждому буквосочетанию соответст- вует некоторый цифровой код. Поэтому задача сводится к поис- ку в массиве некоторого цифрового значения — ключа. Эта задача рассмотрена ранее (разумеется, что можно хранить и сами буквосочетания). При линейном поиске, в худшем случае для ответа на вопрос «есть или нет», потребуется 106 сравне- ний, при бинарном — порядка 50. Можно ли уменьшить коли- чество сравнений и ослабить требования по оперативной памя- ти, резервировать не 106, а, например, 103 элементов памяти?
282 Часть третья Обозначим цифровой аналог буквосочетания символом R, а ад- рес в массиве (таблице) через А. Если будет найдено преобразо- вание f(R), дающее значение А, такое, что □ f выполняется достаточно быстро; □ случаев, когда f(R1)=f(R2), будет минимальное количест- во, то задача поиска трактуется совсем иначе, чем рас- смотрено выше. Этот способ поиска называется хеширова- нием (расстановкой ключей, рандомизацией и т. д.). Итак, ключ преобразуется в адрес, в этом суть метода. Требова- ние по быстродействию очевидно - одна из целевых уста- новок любого алгоритма, ибо быстродействие компьютера ограничено. Второе требование не столь очевидно. Пред- положим, что анализируется текст из специфической стра- ны «Д...», в которой все слова начинаются с буквы О, и в качестве f выбран цифровой аналог первой буквы. В этом случае получаем одно значение А для всех слов (от чего уходили, к тому и пришли, но в стране «Д...» все возмож- но). Следовательно, необходимо выбирать такое преобра- зование /, чтобы количество совпадений адресов (А) для различных значений ключей (R), это называют коллизия- ми, было минимально. Полностью избежать коллизий не удается. Для любого преобразования f (хеш-функции) мож- но подобрать такие исходные данные, что рассматривае- мый метод поиска превращается в линейный поиск. Одна- ко цель построения хеш-функции ясна — как можно более равномерно «разбрасывать» значения ключей по таблице адресов (ее размер обозначим через М) так, чтобы умень- шить количество коллизий. Итак, реализация метода ло- гически разбивается на две составляющие: □ построение /; □ схему разрешения коллизий. Коротко рассмотрим их, резюмируем известные факты из теории информатики. При построении хеш-функции, так или иначе, для значения ключа R находится цифровой аналог, обозначим его через t. В первом способе в качестве М выбирается простое число и нахо- дится остаток от деления t на М (t Mod М). Во втором способе находится значение t2. Из этого значения «вырезается» q ка- ких-то разрядов (лучше средних). Значение q выбирается таким образом, чтобы 21 было равно М. В третьем способе w разрядов для представления ключа t разбиваются на части длиной q раз- рядов. Выполняется, например, логическое сложение этих час- тей, и его результатом является номер элемента в таблице адре- сов. Считается (если не заниматься подбором специальных
Массив — фундаментальная структура данных 283 входных данных), что все эти способы дают достаточно хорошие, в смысле количества возможных коллизий, хеш-функции. Из известных методов разрешения коллизий рассмотрим один — «метод цепочек». Его суть в том, что элементом табли- цы является ссылка на список элементов, имеющих одно зна- чение хеш-функции. Зрительный образ этого метода представ- лен на рисунке. Качество хеш-функции оценивается средней длиной спис- ков. Работа со списками обсуждается позднее, поэтому перене- сем вопрос о разрешении коллизий данным методом на после- дующий материал. Рассмотрим следующую простую задачу. Дано .V (l<TV<15000) чисел типа Longlnt и таблица не очень большого размера. Оце- ним качество работы хеш-функций по среднему и максималь- ному значению количества различных чисел, для которых в ре- зультате преобразования получается один и тот же адрес в таблице. Построим три различные хеш-функции и проверим их на случайных тестах. Для первичного анализа первой хеш-фун- кции приведем полный текст решения. {$R+) Program Му21_1; Const InputFile-'Input.Txt'; MaxN-15000;l'Максимальное значение количества чисел. *} МахТЫ=547;{ 'Размер таблицы. *) Var ТЫ :Array[0. .МахТЫ] OfLonglnt; Count, Di f: Array [0.. МахТЫ] Of Integer; {'Подсчитываем количество совпадающих ключей и количество различных, но дающих один адрес в таблице.'}
284 Часть третья N: Integer; Procedure RandFile;(★Создание файла из N чисел. *) Var 1:Integer; Begin Randomize; Assign(Output,InputFile);Rewrite (Output); N:=Integer(Random(MaxN))+1; Wri teLn (N) ; For i:=l To N Do WriteLn(Longlnt(Random(65000)) ★29999); Close (Output); End; Procedure Solve; Var i,Ind:Integer; nm:Longlnt; Begin FillChar (Dif,SizeOf (Dif) ,0) ; FillChar (ТЫ , SizeOf (ТЫ) ,0) ; Assign (Input,InputFile);Reset(Input); ReadLn (N); For z;=l To N Do Begin ReadLn(пт);(★Читаем число. *) Ind:=nm Mod МахТЫ;(★Вычисляем адрес в таблице.★) If Tbl[Ind]-0 Then Begin Tbl[Ind]:=nm; Count[Ind]:=1; End(★Первая запись в таблицу по этому адресу.★} Else If nm=Tbl[Ind] Then Inc(Count[Ind]) Else Inc(Dif[Ind]);(★Коллизия - различные числа дают один адрес в таблице.*) End; Close (Input); End; Procedure Issl; Var s,i,Max,r:Integer; Begin s:=0;Max:=0 ; For i:=l To МахТЫ Do Begin Inc (s,Dif[i]);{★Подсчитываем количество коллизий.★) If Dif[i]>Max Then Max:=Dif[i];{★Определяем максимальное значение количества коллизий. *)
Массив — фундаментальная структура данных End; r:=Round (s/МахТЫ) ; Assign (Output,' Con ') ;ReWri te (Output) ; WriteLn (N,' ',r, ' ' ,Max) ; End; Begin RandFile;{*Формирование файла из случайных чисел. *) Solve;( * Анализ файла с помощью различных хеш-функций. *) Issl;{*Подсчет среднего и максимального значений коллизий. *) End. Решения для второй и третьей функций требуют изменения одной строки в процедуре Solve — Ind:=GetNum(nm), где Get- Num обеспечивает вычисления адреса элемента таблицы. Для второй хеш-функции GetNum имеет вид: Function GetNum (nw: Longlnt) : Integer; Var w,q:Longlnt; t:0. .MaxTbl; Begin q: = (nw And SFFFF0000 ) Shr 16;(*Выделяем старшие разряды числа и сдвигаем на 16 разрядов. *) w: = (nw And SFFFF) ; {^Выделяем младшие разряды числа. *} t : = (Integer (q*w) And $7FC0) Shr 6 ; ( ^Выделяем 9 бит произведения чисел. *) GetNum:=t; End; Вычисление третьей хеш-функции можно организовать сле- дующим образом. Function GetNum (nw: Longlnt) :Integer; Var r, t: Integer; Begin r:=Integer (nw And S1FF) /{‘Выделение младших 9 бит числа.*) t: =Integer(nw And $3FE00 Shr 9) ;{‘Выделение следующих 9 бит числа. *) r:=r Хог t;{*Первый шаг формирования адреса элемента таблицы.*} t:=Integer(nw And S7FC0000 Shr 18) ; {“Выделение следующей группы разрядов числа. *)
Часть третья r:=r Xor t; t:=Integer (nw And SF8000000 Shr 27) ; f^Выделение оставшихся разрядов.*} r:=r Xor t; GetNuml:=r; End; В таблице приведены результаты подсчета для трех рассмат- риваемых хеш-функций. Данные в таблице дают общую «картинку» работы хеш-фун- кций, и не более. Для сравнительного анализа необходимо по крайней мере исследовать их работу на одном и том файле из случайных чисел. Измените программу так, чтобы это требова- ние выполнялось, а также для каждой хеш-функции формиро- вались оценки NcpedHee, rcpedHee, MaxcpedHee. Сравните работу хеш- функций по этим оценкам, например, для 100 различных фай- лов с исходными данными. Задания для самостоятельной работы 1. Во входном файле дан массив А и массив из элементов, по- иск которых будет осуществляться в массиве А. Изменить массив А таким образом, чтобы суммарное количество срав- нений при поиске элементов было минимальным. Примечание По данвым из второго массива формируются частоты поиска каж- дого элемента и элементы массива А сортируются в порядке убы- вания значений этих частот («метод перемещения в начало»). За- дача достаточно легко превращается в небольшое исследование. Необходимо найти значения N, при которых массивы не помеща- ются в оперативную память, отводимую под программу в системе программирования.
Массив — фундаментальная структура данных 287 2. Дан массив А и число X. Переставить элементы массива та- ким образом, чтобы вначале были записаны элементы, мень- шие или равные X, а затем — большие или равные X. Переставить элементы массива таким образом, чтобы внача- ле были записаны элементы, меньшие X, затем равные X и, наконец, большие X. Примечание Указанные перестановки следует сделать за один просмотр А и без использования дополнительных массивов. 3. Найти реализацию (рекурсивную и нерекурсивную) бинарно- го поиска с использованием «барьерной» техники. 4. Ниже по тексту приведены три версии процедуры бинарного поиска. Какая из них верная? Исправьте ошибки. Какая из них более эффективная? Procedure Search; Var i, j, к: Integer; Begin i:-l,j =N; Repeat k: = (i+j) Div 2; If X<=A[k] Then j-=k-l; If A[k]<-X Then i-=k+l. Until i>j. End, Proceaure Search, Var x, j, k: Integer, Begin i: -1, j- Repeat k = (i+j)Div 2; If X<A[k] then J. ~k Else i:=k+l, Until i>=j; End, Procedure Search; Var i, j, k: Integer; Begin i:=l; j:=N; Repeat k: = (i+j) Div 2 If A[k]<X Then i:=k Else j:^k; Until (A[k]~X) Or (i>=j); End; 5. Дан массив из N различных целых чисел. Разрешенными опе- рациями являются сложение двух элементов массива и срав- нение сумм. Найти наибольший элемент массива за минима- льное количество сравнений. 6. Пусть Х[1.Х] и Y[1..N] — два возрастающих массива из раз- личных элементов. Найти за время О(LogN) средний элемент множества, полученного объединением этих массивов.
288 Часть третья Занятие № 22. Двумерные массивы. Работа с элементами План занятия □ структура двумерного массива и его описание; □ шаблон для решения задач на двумерные массивы; □ экспериментальная работа с программами поиска макси- мального элемента в массиве, формирование значений эле- ментов одномерного массива, поиска элементов с опреде- ленными свойствами, заполнения массива по заданным правилам; □ выполнение самостоятельной работы. Массивы, положение элементов в которых описывается дву- мя индексами, называют двумерными. Логическая структура такого массива может быть представлена прямоугольной мат- рицей. Каждый элемент матрицы однозначно определяется ука- занием номера строки и номера столбца. Образом памяти компь- ютера в наших предыдущих рассуждениях был одномерный массив ячеек. Как происходит отображение логической струк- туры двумерного массива в физическую? Известны два способа: отображение строками и отображение столбцами. В конкретной системе программирования используется один из них. Рассмот- рим первый, как обычно на примере. Пусть есть двумерный массив А из целых чисел (тип Integer — 2 байта). Количество строк равно 5, количество столбцов — 4. Пусть первый элемент А[1,1] записан в ячейке с номером 1000. Вычислим адрес А[4,3]. Addr(A[4,3] )= 1000+2*(4*(4-1)+(3-1))=1028. Что мы делали? В каждой строке записано по 4 элемента. В трех стро- ках — 12 элементов, в 4-й строке до 3-го элемента записано 2 элемента. Складываем и умножаем на 2, ибо 2 байта необходи- мо для хранения одного элемента. В общем случае для массива A[N,M] из элементов, занимающих V байт памяти, формула имеет вид: Addr(A[i,j])=Addr(A[l,1])+V*(M*(i-l)+(j-l)). Двумерный массив на языке Турбо Паскаль определяется по-разному. Примеры: Const MaxN=...; МахМ=...;{*Максимальные значения количест- ва строк и столбцов двумерного массива.*} Type OMyArray=Array[l.MaxM] Of 1тг1е£ег;{*Одномерный мас- сив из целых чисел.*} ТМуArray=Array[l.MaxN] Of ОМуАггау;{*Одномерный мас- сив, элементами которого являются одномерные мас- сивы из целых чисел.*}
Массив — фундаментальная структура данных ИЛИ Type TMyArray=Array[l..MaxN,l..MaxM] Of Integer; ^Двумер- ный массив из целых чисел.*} В учебнике отдается предпочтение второму способу описа- ния двумерного массива. Для работы с двумерными массивами изготовим шаблон. Он включает ввод данных из файла и вывод элементов двумерного массива в файл. Размеры двумерного массива вводятся из фай- ла. Далее по тексту, если этого не требует задача, процедуры ввода и вывода двумерного массива не приводятся. ISR + ) Program Му22_1; Const MaxN=10; MaxM=8; Type TMyArray-Array[1..MaxN,1..MaxMJ Of Integer; Var A:TMyArray; N,M:Integer; Procedure TInitfVar v ,w: Integer ; Var X-.TMyArray) ; Var i,g:Integer; Begin Assign (Input, 'Input. Txt') ; Reset (Input) ; ReadLn(v,w);(*B первой строке файла записаны значения N и М. ★} For 1:=1 То v Do For д : =1 То w Do Read (X (i ,д ] ) ; Close (Input) ; End; Procedure TPrint(v,w:Integer;X:TMyArray); Var i,g:Integer; Begin Assign(Output,'Output.Txt'); ReWri te (Output) ; For 1:—1 To v Do Begin For g:=l To w Do Write(X[i,gJ , " ) ; WriteLn; End; Close (Output) ; End; Begin TInit (N,M,A) ; TPrint (N,M,A) ; End. 10—452
290 Часть третья Массив просматривается так, как это показано на рисунке: элементы первой строки, второй и до N-й. Отметим еще одну технологическую особенность работы, точ- нее напомним, ибо она уже рассматривалась. Экран должен быть разбит на три окна, которые одновременно обозримы. Из- менение значений во входном файле (не забывайте его записы- вать) и запуск программы приводят к изменению выходного файла. Эти изменения видны без дополнительных операций по переходу от одного окна к другому. Текущим должен быть ка- талог с файлами программы, Input.Txt и Output.Txt. Названия входного и выходного файлов, естественно, могут быть други- ми. Возможно формирование массива с использованием датчика случайных чисел и путем ввода данных с клавиатуры, но мы не будем останавливаться на этом. Примечание Обычно значения размерности массива вводят с клавиатуры, а сам массив — из файла. «Разрыв» по данным. Процедура TInit полно- стью обеспечивает ввод массива — это законченный фрагмент логи- ки с одной точкой входа и одной точкой выхода. Фрагмент работа- ет с конкретными данными и взаимодействует с остальной логикой по определенным правилам. Разрыв не логичен как по управле- нию, так и по данным. Структуризация задачи должна осуществ- ляться как по управлению, так и по данным, иначе какой же переход к объектно-ориентированному программированию? Если на уровне деклараций или формального ознакомления с конструкциями или инструкциями, то это не наш путь. Экспериментальный раздел работы 1. Найти первый максимальный элемент массива и его индек- сы. Procedure Search(v,w:Integer;X:TMyArray; Mar smax,simax,sjmax:Integer);
Массив — фундаментальная структура данных 291 Var 1,д:Integer; Begin smax : =X [1,1 ] ; simax:=J; sjmax:=1; For i:=1 To v Do For J:=1 To w Do If X[i,g]>smax Then smax:=X[1,g]; simax:-i; sjmax:=g; End; End; Begin Процедура Append открывает существующий файл для добавле- ния в него информации. Описание —AppendfVar f:Text). Пере- менная f является файловой переменной типа Text, которая должна быть связана с внешним файлом с помощью процедуры Assign. Процедура Append открывает существующий внешний файл, имя которого поставлено в соответствие переменной f. Если файл уже открыт, то сначала он закрывается, а затем от- крывается повторно. Указатель текущей позиции (вспомните головки чтения, записи в магнитофоне) при этом устанавлива- ется на конец данного файла. Если сама процедура поиска первого максимального элемен- та массива вряд ли вызывает вопросы, то для вывода результа- та в файл Output.Txt наших знаний не хватает. Мы можем пе- реопределить вывод и значение максимального элемента с его индексами выводить на экран. Для этого необходимо: Assign(Output,'Con'); Rewrite{Output); и написать оператор WnteLn(Max,’ ’.iMax,' ’.jMax). Однако массив выводится в файл Output.Txt, а результаты работы — на экран. Где логика? Воспользуемся процедурой Append. Добавление к основной программе может иметь и такой вид: Search (п,т,А,Мах, iMax,дМах) ; Append (Output); Writein (Max: 5,iMax:5,дМах:5) ;
Часть третья Измените процедуру Search для поиска последнего максима- льного элемента, для поиска всех максимальных элементов и их индексов. 2. Сформировать одномерный массив, каждый элемент которо- го равен сумме отрицательных элементов соответствующей строки заданной целочисленной матрицы. Задача требует введения одномерного массива, поэтому при- ведем описание данных, процедуры подсчета суммы отрицате- льных элементов в строке и добавления данных в выходной файл, а также основную программу. {$R+} Program Му22_2; Const MaxN=10; МахМ=8; Type TMyArray=Array[1..MaxN,1..МахМ] Of Integer; OMyArray=Array[1. .MaxN] Of Integer; Var A:TMyArray; Sum;OMyArray; n,m:Integer; Procedure SumNeg(v,w:Integer;X:TMyArray;Var Ssum: OMyArray); Var i, j:Integer; Begin For i:=l To v Do Begin Ssum[i]:=0 ; For j:=1 To w Do If X[i,j]<0 Then Inc(Ssum[i], X [ 1, j]) ; End; End; Procedure OPrint(v:Integer;OSum:OMyArray); Var i:Integer; Begin Append (Output); For i : =1 To v Do Write (OSum[i] :4) ; WriteLn; End; Begin{^Основная программа.*) TInit(n,m,A); TPrint(n,m,A); SumNeg(n,m,A,Sum); OPrint (n,Sum); End.
Массив — фундаментальная структура данных 293 3. Определить, есть ли в массиве элемент, равный 0. Function SNeg(v,w:Integer;X:TMyArray):Boolean; Var i, j:Integer; pp:Boolean; Begin pp:=False; For 1:=1 To v Do For j:=1 To w Do If X [ 1,j]-0 Then pp:=True; SNeg:=pp; End; Фрагмент из основной программы. Append(Output); If SNeg(n,m,A) Then WriteLn('Yes') Else WriteLn ('No') ; Недостаток функции SNeg. Пусть первый элемент массива Х[ 1,1 ] равен нулю. Просмотр массива продолжается, несмотря на его бессмысленность. Устранение неточности — «задача опре- деляет тип используемых конструкций повторения» — требует использования операторов While или Repeat — Until в функции Sneg. Проделайте это. 4. Определить, является ли данный квадратный массив симмет- ричным относительно своей главной диагонали. Элементы на главной диагонали характеризуется совпадением значением индексов — i=j, на второй i=N—j+l, где N — размерность мас- сива. Главная диагональ Вторая диагональ, но не главная
Часть третья Function Sim (v,w:Integer;Х:TMyArray)boolean; Var i,j:Integer; pp:Boolean; Begin pp:=True; For i:-1 To v—1 Do For j:-i+l To w Do If X[i,j]<>X[j,1] Then pp:=False; Sim:=pp; End; Измените функцию так, чтобы симметричность массива про- верялась относительно второй диагонали. В предыдущем при- мере был рассмотрен недостаток функции SNeg. Он присущ и этой функции. Устраните его. 5. В массиве А размерностью п*ш к элементам четных столбцов прибавить элемент первого столбца соответствующей строки. Procedure Change ( v,w:Integer;Var X:TMyArray); Var i, j: Integer; Begin For i:=1 To v Do For J :=1 to nr Div 2 Do Inc(X[i,2*j],X[i,l]); End; Особенностью решения задач этого типа является возмож- ность изменения элемента, который должен использоваться в обработке. Например, прибавляем к элементам нечетных столб- цов. Модификация X[i,2*j-1 ] приведет к тому, что элементы 3, 5 и т. д. столбцов увеличатся на удвоенное значение элемен- тов первого столбца. Проведите корректное изменение процеду- ры Change. 6. Заполнить массив А размером п*пг следующим образом, на- пример п=6 и т=8: 12 34567 8 16 15 14 13 12 11 10 9 1' 17 18 19 20 21 22 23 24 Ч------> 32 31 30 29 28 27 26 25 ( 33 34 35 36 37 38 39 40 >> 48 47 46 45 44 43 42 41 То есть заполняется в виде «змейки».
Массив — фундаментальная структура данных Правило заполнения. Если номер строки — нечетное число, то A[i,j]=(i-1 )*т+], иначе — A[i,]]=i*m-j+l. Procedure Fi 11 (v,w:Integer;Var X: TMyArray); Var i, j : Integer; Begin For i:=1 To v Do For j:=1 To w Do If i Mod 2=1 Then X[i, j]: = (i-l) *w+j Else X[1,J]:=i*w-J+1; End; Заполните массив по следующим правилам. 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9 0 10 0 11 0 12 0 13 0 14 0 1 12 13 24 25 36 2 11 14 23 26 35 3 10 15 22 27 34 4 9 16 21 28 33 58 17 20 29 32 6 7 18 19 30 31 1 3 4 10 11 21 2 5 9 12 20 22 6 8 13 19 23 30 7 14 18 24 29 31 15 17 25 28 32 35 16 26 27 33 34 36 7. Составить программу, запрашивающую координаты ферзя на шахматной доске и показывающую поля доски, находящие- ся под боем. Заметим, что шахматную доску удобно представить в виде двумерного массива размером 8*8. Координаты ферзя опреде- ляются двумя числами (номер строки и номер столбца), но в шахматах принято вводить букву и число. Буква отвечает за номер строки, а число — за номер столбца. Поэтому не будем отступать от традиций и введем координаты именно таким об- разом. В программе сделаем проверку корректности ввода и если все правильно, то переведем букву в соответствующее ей число (‘а’ — 1, ‘Ь’ — 2, ‘с’ — 3,’d’ — 4, ‘е’ — 5, ‘f’ — 6, ‘g‘- 7,’h‘- 8 ). Для решения задачи используем ряд свойств шахмат- ной доски. Все диагонали доски делятся на восходящие и ни- сходящие.
Часть третья □ для клеток любой восходящей диагонали сумма номера строки и номера столбца постоянна (i+i=Const)-, □ для клеток любой нисходящей диагонали разность номера строки и номера столбца постоянна (i-j=Const). Проверьте, что для восходящих диагоналей сумма индек- сов изменяется от 2 до 16, а для нисходящих — разность от -7 до 7. Program Му22_3 ; Const MaxN-8;МахМ=8; Type TMyArray=Array[1.MaxN,1..МахМ] Of Integer; Procedure TInit(v,k, 1: Integer; Var X: TMyArray); Var i, j: Integer; Begin For 1:=1 To v Do For j:=1 To v Do If (i=k) Or (j=l) Or (i+j=k+l) Or (i-j=k-l) Then X[i,j]:=l Else X[i ,j] : ~0; { *1 — клетка под боем, 0 — нет. *} X[k,l]:-2; { *В этой клетке находится ферзь. *} End; Procedure Solve; Var A: TMyArray; c: Char;str, stl: Integer; Begin Assign(Input,'Input.Txt'); Reset(Input); Assign(Output,'Output.Txt'); ReWrite(Output); ReadLn (c, stl) ;(*Координаты ферзя записаны в файле Input.Txt.*)
Массив — фундаментальная структура данных If (сС'а') Or (c>'h') Or (stl<l) or (stl>MaxN) Then WriteLn ('Некорректный ввод.') Else Begin str:= Ord(ch) - Ord('a') +1;{*Преобразуем символ в номер строки. *) Unit (MaxN, str,stl,А) ; TPrint(MaxN,МахМ,A);{^Процедура совпадает с ранее рассмотренной. *) End; Close (Input) ; Close (Output) ; End; Основная программа состоит из одной строки — вызов про- цедуры Solve. Измените решение так, чтобы фиксировались клетки доски, находящиеся под боем хотя бы одной из фи- гур — ферзя и ладьи. Задания для самостоятельной работы 1. Дан двумерный массив. Найти сумму и количество элемен- тов в каждом столбце: □ кратных kj ты k2; □ попадающих в интервал от А до В; □ являющихся простыми числами; □ положительных и лежащих выше главной диагонали. 2. Дан двумерный массив. Найти: □ сумму элементов в строках с k; по k2; □ номера всех максимальных элементов; □ номера первых отрицательных элементов каждой строки (столбца); □ номера последних отрицательных элементов каждой стро- ки (столбца); □ количество элементов в каждой строке, больших (мень- ших) среднего арифметического элементов данной стро- ки; □ номера первой пары неравных элементов в каждой стро- ке. 3. Даны два квадратных массивах и В. Вывести тот из них, у которого след меньше (сумма элементов главной диагона- ли). 4. Дан двумерный массив. Определить: □ есть ли в данном массиве отрицательный элемент; □ есть ли два одинаковых элемента;
298 Часть третья □ есть ли данное число А среди элементов массива; □ есть ли в заштрихованной области массива элемент, рав- ный А (массив имеет размерность п*п): 12 5. Дан двумерный массив. Определить, есть ли в данном масси- ве строка (столбец): □ состоящая только из положительных элементов; □ состоящая только из положительных или нулевых эле- ментов; □ состоящая только из элементов, больших числа А; □ состоящая только из элементов, принадлежащих проме- жутку от А до В. 6. Дан двумерный массив. Выполнить следующие преобразова- ния с ним: □ в каждой строке сменить знак максимального по модулю элемента на противоположный; □ последний отрицательный элемент каждого столбца за- менить нулем; □ положительные элементы умножить на первый элемент соответствующей строки, а отрицательные — на послед- ний; □ заменить все элементы строки с номером k и столбца с номером I на противоположные по знаку; □ к элементам столбца с номером k: прибавить элементы столбца k2. 7. Написать программу, определяющую поля шахматной доски, находящиеся под боем коня. 8. Ввести координаты ферзя и коня и определить: □ бьют ли фигуры друг друга; □ если конь ходит первым, то бьет ли он ферзя; □ бьет ли ферзь коня, если первый ход ферзя. 9. Составить программу заполнения и вывода в файл таблиц сло- жения и умножения цифр в заданной (произвольной) систе- ме счисления. Для десятичной системы счисления они име- ют вид:
Массив — фундаментальная структура данных 299 0 2 3 4 5 6 7 8 9 0 0 1 2 3 4 5 6 7 8 9 2 3 4 5 6 7 8 9 10 2 2 3 4 5 6 7 8 9 10 3 3 5 6 7 8 9 10 11 12 4 5 6 7 8 9 10 12 13 5 5 6 7 8 9 10 11 12 13 14 6 6 7 8 9 10 12 13 14 15 8 9 10 11 12 13 14 15 16 8 8 9 10 11 12 13 14 15 16 17 9 9 1° 11 12 13 14 15 16 17 18 0 1 2 3 4 5 6 8 9 0 0 0 0 0 0 0 0 0 0 ° 1 0 1 2 3 5 6 7 8 9 2 0 2 4 6 8 10 12 14 16 18 3 0 3 6 9 12 15 18 21 24 27 4 0 4 8 12 16 20 24 28 32 36 5 0 5 10 15 20 25 30 35 40 45 6 0 6 12 18 24 30 36 42 48 54 7 0 14 21 28 35 42 49 56 63 8 0 8 16 24 32 40 48 56 64 72 9 0 9 18 27 36 45 54 63 72 81 10. Даны два двумерных массива одинаковой размерности. Со- здать третий массив той же размерности, каждый элемент ко- торого равен: □ сумме соответствующих элементов первых двух; □ 1 (True), если соответствующие элементы массивов име- ют одинаковый знак, иначе элемент равен 0 (False). 11. Дан двумерный массив. Сформировать одномерный массив, каждый элемент которого равен: □ произведению четных, положительных элементов соот- ветствующего столбца; □ количеству элементов соответствующей строки, больших данного числа; □ наибольшему по модулю элементу соответствующего столбца; □ количеству отрицательных элементов, кратных 3 или 5, соответствующей строки; □ первому четному элементу соответствующего столбца, если такого нет, то нулю.
300 Часть третья 12. Дан двумерный массив. Определить: □ есть ли в данном массиве строка, в которой ровно два от- рицательных элемента; □ есть ли в данном массиве столбец, в котором имеются одинаковые элементы; □ есть ли в данном массиве строка, в которой имеется два максимальных элемента всего массива; □ есть ли в данном массиве столбец, в котором равное ко- личество положительных и отрицательных элементов; □ есть ли в данном массиве строка, содержащая больше по- ложительных элементов, чем отрицательных. 13. Сформировать двумерные массивы по следующим правилам: 1 1 1 2 1 1 3 4 1 5 6 1 3 6 10 15 21 1 4 10 20 35 56 1 5 15 35 70 126 1 6 21 56 126 252 14. Определить, является ли массив (N*N) магическим квадра- том (значения элементов массива из интервала от 1 до N*N), то есть суммы по всем горизонталям, вертикалям и двум диа- гоналям должны совпадать. Примеры магических квадратов. 15 24 8 17 9 18 2 11 25 12 21 10 19 3 20 4 13 22 6 23 7 16 5 14 38 14 32 1 26 44 20 5 23 48 17 42 11 29 21 39 8 33 2 27 45 30 6 24 49 18 36 12 46 15 40 9 34 3 28 13 31 25 43 19 37 22 47 16 41 10 35 4
Массив — фундаментальная структура данных 301 Поскольку сумма элементов магического квадрата равна l+2+3+...+N2=N2*(Nz+l )/2, то соответствующие суммы равны N*(N2+1 )/2. Так, при N=5, в каждой строке, столбце и на главных диагоналях сумма элементов равна 5*(52+1)/2=65. 15. Дан двумерный массив (N*N). Зеркально отобразить его эле- менты относительно: □ горизонтальной оси симметрии; □ вертикальной оси симметрии; □ главной диагонали; □ побочной диагонали.
302 Часть третья Занятие № 23. Двумерные массивы. Вставка и удаление План занятия □ изучение различных методов вставки и удаления элемен- тов двумерного массива; □ выполнение самостоятельной работы. Экспериментальный раздел работы 1. Вставить строку из нулей после строки с номером t. Для ре- шения этой задачи необходимо: □ первые t строк оставить без изменения; □ все строки после t-й сдвинуть на одну; □ элементам строки t+1 присвоить заданное значение. В процедуре ввода данных кроме ввода размеров массива следует предусмотреть ввод номера строки — ReadLn(n,m,t). Ключевая процедура вставки строки имеет вид: Procedure InsertSti(v,w,г:Integer;Var X:TMyArray); Var i,j:Integer; Begin For i:=v DownTo ri 1 De For j:=l To v; Do X[i+l,j] :=X[i,j] ; For g;=l To w Do X[r+1,j]:=0; End; Измените процедуру для вставки строки перед строкой с за- данным номером. 2. Вставить строку из нулей после всех строк, содержащих мак- симальный элемент массива. Приведем полный текст реше- ния (для напоминания о нашей схеме решения задач). {$Р+) Program Му23_1; Const Max№=8 ; MaxM=9; Type TMyArray=Array[1..2*MaxN,1..MaxM] Of Integer; {★Резервируем 2*MaxN строк на тот случай, если максимальный элемент массива есть в каждой строке.*} Var A:TMyArray; n,m:Integer; Procedure TInit(Var v,w: Integer ;Var X:TMyArray); Var i,j:Integer; Begin
Массив — фундаментальная структура данных 303 Assign (Input, 'Input. Txt') ; Reset (Input) ; ReadLn (v, w) ; For i:To v Do Forj:=l To w Do Read (X[i, j ]) ; Close (Input) ; End; Procedure TPrint(v, w:Integer;X:TMyArray); Var i, j : Integer; Begin Assign (Output, ' Output. Txt') ; ReWrite (Output) ; For i:=l To v Do Begin For j : =1 To w Do Write (X [i, j ] : 4) ; Wri teLn; End; Close (Output) ; End; Procedure InsertStr (v, w,r: Integer;Var X:TMyArray); Var i,j : Integer; Begin For i:=v DownTo r+1 Do For j :=1 To w Do X[i + l,j] :=X[1, j] ; For j.-=i To w Do X[r+1,j]:=0; End; Function TMax(v,w:Integer;X:TMyArray):Integer; (★Поиск максимального элемента. *) Var i,j,max:Integer; Begin max:=X[1,1]; For i:=l To v Do For J :=1 To w Do If X[i,j]>max Then max:=X[i,j]; TMax:=max; End; Procedure Solve (Var v: Integer ;w: Integer ;Var X:TMyArray); Var i, j , smax: Integer; Begin smax:=TMax (v,w,X) ; (★Находим максимальный элемент.★} i: =1 ; While i<=v Do Begin {★Почему используем этот тип цикла?*)
304 Часть третья While (j<-u) And (X[i, j ] Osmax) Do Inc(j); If jow+l Then Begin (*B строке есть максимальный элемент.'] InsertStr(v, и, i ,Х) ; ('Вставляем строку. *} Inc (v);{^Увеличиваем количество строк.★] Inc (i,2){'Пропускаем вставленную строку. *) End Else Inc(i) ;('Обычное изменение индекса.★) End; End; Begin {'В основной программе только вызовы процедур - крупных «кирпичиков». *) TInit(n,m,A); Solve(n,m,A); TPrint(n,m,A); End. Измените решение для вставки нулевых строк, перед стро- ками, содержащими максимальный элемент массива. 3. Удалить строку с номером t. Для того, чтобы удалить строку с номером t, необходимо сдвинуть все строки, начиная с данной, на одну вверх. Послед- нюю строку можно «обнулить», можно не рассматривать как строку массива. Procedure DelStr(v,w,г:Integer;Var X:TMyArray); Var i,j:Integer; Begin For i:=r To v-1 Do For j :=1 To w Do X[i,jJ : =X [1+1, jJ; For j:=l To w Do X[v,j]:=0; End; 4. Удалить строки, содержащие максимальный элемент масси- ва. Procedure Solve(Var v:Integer;w:Integer;Var X:TMyArray); Var i,j,smax:Integer; Begin smax:=TMax(v,w,X); i:=l; While i<-v Do Begin
Массив — фундаментальная структура данных 305 While (j<=w) And (X[i, j ] Osmax) Do Inc(j); If j<>w+l Then Begin DelStr(v,w,i,X); Dec (v) ; End Else Inc (1) ; End; End; Сравните эту процедуру с процедурой Solve, рассмотренной ранее при вставке строк, и объясните отличия. 4. Поменять местами элементы столбцов с номерами и 12. С процедурой Swap мы уже встречались. Напомним ее. Procedure Swap (Var х, у: Integer); Var z: Integer; Begin z:=x; x:=y; у: =z ; End; Написание процедуры, реализующей требования примера, не вызывает сложностей. Procedure Change(v,stl,st2:Integer; Var X:TmyArray); Var i:Integer; Begin For i:=l To v Do Swap (X[1, stl ], X[ 1, st2]) ; End; 5. Удалить все строки и столбцы, содержащие максимальный элемент массива. Рассмотрим пример. Максимальный элемент равен 9. Если выбрать логику просмотра по строкам, а затем по столбцам, то результат окажется верным. При просмотре же вначале по столбцам, а затем по строкам исключается только третий стол- бец. Выход из ситуации состоит в запоминании номеров строк
306 Часть третья и столбцов, содержащих максимальный элемент массива. Пусть мы запомнили в массивах NumStr и NumStl номера удаляемых строк и столбцов. Для примера они имеют вид: (1, 2, 3, 4, 5) и (3, 3, 3, 3, 3). Пять раз удалять третий столбец явно нет смыс- ла, поэтому из массивов необходимо удалить повторяющиеся элементы, т. е. во втором массиве оставить одну 3. Однако этим не ограничиваются возникающие сложности. Первая строка удалена, требуется удалять вторую, но она уже в нашей матри- це стала первой. Необходимо после каждого удаления изменить номера строк (столбцов) в массиве NumStr — уменьшить на единицу те номера, которые больше номера удаленной строки. Ниже по тексту приведена только процедура Solve, остальные части программы совпадают с ранее рассмотренными. Procedure Solve(Var v, w:Integer;Var X:TMyArray); Type OMyArray-Array [ 1. .MaxNJ Of Integer; {★Определяем одномерный массив для хранения номеров удаляемых элементов. Константа MaxN берется при условии, что количество строк в матрице всегда больше количества столбцов.*) Var i,smax:Integer; NumStr,NumStl:OMyArray; {*Номера удаляемых строк и столбцов.*) ykStr,ykStl:Integer;{*Количество элементов в массивах NumStr и NumStl. *) Procedure FNumfVar q,г:Integer;Var Y,Z:OMyArray); {★Формирование массивов с номерами удаляемых строк и столбцов. *) Var i,j:Integer; Begin q:=0;r:=0; For i:=1 To v Do For j:= 1 To w Do If X[i, j ] =smax Then Begin Inc(q);Y[q]:=i; Inc(r) ;Z[r] ;=j; End; End; Procedure Sz (Var t:Integer; Var X:OMyArray); {★Удаление из массива повторяющихся элементов. *} Var i,j,1:Integer; Begin i: =1;
Массив — фундаментальная структура данных 307 Kt Do Begin J:~i+l; While j<-t Do If X[i]=X[j] Then Begin For l:=j To t-1 Do X[1]:=X[1+1]; X[t]:=0; Dec (t) ; End Else Inc(j); Inc(i) ; End; End; Procedure Change(t,a:Integer; Var X:OMyArray); {★Уменьшение на единицу значений элементов массива, превышающий заданную величину.★} Var 1:Integer; Begin For i:=l To t Do If X[i]>a Then Dec(X[i]); End; Begin {*Текст основной процедуры. ★) FillChar(NumStr,SizeOf(NumStr) ,0); {★Инициализация массивов. *) FillChar(NumStl,SizeOf(NumStl) ,0) ; smax:=TMax (v, w,X) ; { ★Находим максимальное значение - текст функции не приводится. *} FNum(ykStr,ykstl,NumStr,NumStl);{*Определяем номера удаляемых строк и столбцов.★} Sz (ykStr, NumStr) ; {*Удаляем повторяющиеся элементы. *} Sz (ykStl,NumStl); 1 -.=1 ; While i<=ykStr Do Begin DelStr (v,w,NumStr [i] ,X);{*удаляем строку. *} Change(ykStr,NumStr[i],NumStr);{★ Корректируем массив номеров строк. *) Dec (v) ; Inc (i) ; End; i: =1; While i<=ykStl Do Begin DelStl(v,w,NumStl[i],X);{*Удаляем столбец.*)
Часть третья Change (ykStl,NumStl[i],NumStl) ; {★Корректируем массив номеров столбцов.*) Dec(и); Inc(i) ; End; End; О технологии написания. Напоминаем, что процесс разра- ботки процедуры Solve заключается в написании «тела» проце- дуры с заголовками составляющих частей. Программа компили- руется, сохраняется и только затем дописываются составляющие части. В процедуре Solve есть два практически совпадающих фрагмента (циклы по i). Исключите эти повторения, они недо- стойны Вас. Задания для самостоятельной работы 1. Вставить □ первую строку после строки, в которой находится пер- вый встреченный минимальный элемент; □ второй столбец после первого столбца, в котором все эле- менты положительны; □ нулевую строку и нулевой столбец перед строкой и столб- цом, в которых находится первый минимальный эле- мент; □ последнюю строку после всех строк, в которых есть за- данное число А; □ второй столбец перед всеми столбцами, в которых нет от- рицательных элементов; □ первую строку перед всеми строками, в которых есть 0, и первый столбец после всех столбцов, в которых есть от- рицательные элементы; □ столбец из нулей после столбцов с минимальными эле- ментами; □ первую строку между средними строками; □ строку из нулей перед всеми строками, первый элемент которых делится на 3. 2. Удалить □ столбцы, в которых есть минимальный элемент; □ строку с номером k и столбец с номером I; □ все столбцы, в которых нет нулевого элемента; □ все строки и столбцы, на пересечении которых стоят от- рицательные элементы; □ среднюю строку (строки);
Массив — фундаментальная структура даииых 309 □ все столбцы, в которых первый элемент больше послед- него; □ столбец, в котором находится первый четный отрицате- льный элемент; □ все столбцы, в которых первый элемент больше заданно- го числа А. 3. Заменить □ минимальный элемент в каждой строке на противопо- ложный по знаку; □ все элементы первых трех столбцов на их квадраты; □ все симметричные элементы квадратной матрицы на нули. 4. Поменять местами □ средние столбцы; □ средние строки с первой и последней; □ средние столбцы со вторым и предпоследним; □ средние строки; □ первый максимальный и последний минимальный эле- менты; □ в каждой строке первый элемент и максимальный по мо- дулю; □ в каждой строке первый отрицательный и последний по- ложительный ; □ первую строку и строку, в которой находится первый ну- левой элемент; □ вторую и предпоследнюю строки; □ первую строку с последней строкой, вторую — с предпос- ледней и так далее; □ столбцы так, чтобы в конечном итоге они имели следую- щий порядок: □ последний, предпоследний, ..., второй, первый; □ первый, последний, второй, предпоследний, третий,... 5. Найти максимальный элемент массива, встречающийся бо- лее одного раза. 6. Расстоянием между строками назовем сумму произведений соответствующих элементов строк. Найти две строки с мак- симальным расстоянием. 7. Дан массив размерностью Л7*АГ, N - нечетное число. Вывес- ти элементы массива, при обходе его по спирали, начиная с центра. 8. Для заданного целочисленного массива (N*N) найти макси- мум среди сумм элементов диагоналей, параллельных глав- ной диагонали.
310 Часть третья 9. Для заданного целочисленного массива (N*N) найти мини- мум среди сумм элементов диагоналей, параллельной побоч- ной диагонали матрицы. 10. Значения элементов массива находятся в интервале от А до В. Две строки матрицы назовем похожими, если они отлича- ются только порядком элементов. Найти пары похожих строк. 11. Элемент массива А[i,j] называют седловой точкой, если он яв- ляется минимальным в строке с номером i и максимальным в столбце с номером ]. Найти седловые точки. 12. Для данного массива найти такие значения k, что k строка совпадает с k столбцом. 13. Среди столбцов заданного целочисленного массива найти столбцы, состоящие только из нечетных элементов. 14. Элемент A[i,j] назовем локальным минимумом, если он стро- го меньше всех своих соседей. Соседями являются элементы A[k,l] с (i-l< k< i+1), (j-l< I Найти количест- во локальных минимумов для заданного массива. Найти максимум среди локальных минимумов. 15. Решить задачу поиска числа X в двумерном массиве A[1..N,1..MJ (элементы в строках и столбцах упорядочены) за время, пропорциональное O(N+M).
Массив — фундаментальная структура данных Занятие № 24. Несколько задач на технику работы с двумерными массивами План занятия □ разбор и модификация программ: перемножения матриц, решения системы линейных уравнений, приведение мат- рицы к блочному виду, сортировки элементов строк мат- рицы и поиска суммы минимальных элементов в строках и столбцах матрицы; □ выполнение самостоятельной работы. Экспериментальный раздел работы 1. Составить программу вычисления произведения двух цело- численных матриц А[п,т] и B[m,t]. Элементы результиру- ющей, также целочисленной, матрицы C[n,t] определяются по формуле C[i,j]=A[i,l]*B[l,j]+A[i,2]*B[2,j]+...+A[i,m]* *B[m,j], где (п, т), (m.t), (n.t) — размерности матриц А, В, С соответственно. При выполнении умножения количест- во столбцов в первом массиве равно количеству строк во вто- ром массиве. Пример: 1*3+4*2+2*1 = 13 I 1 *4+4*0+2*2=8~ 2*3+1*2+5*1 = 13 | 2*4+1*0+5*2=18' Приведем полный текст решения. В файл Output.Txt запи- сываются массивы А, В и результат — массив С. Независимость фрагментов логики (процедур TInit, TPrint) дает результаты. Они используются для ввода и вывода двумерных массивов лю- бой размерности. {$R+> Program Му24_1; Const MaxN=8; MaxM=8; Type TMyArray=Array[1. .MaxN, 1. .MaxM] Of Integer; Var A,B,C:TMyArray; n,m,t:Integer; Procedure Init; Begin Assign(Input,'Input.Txt'); Reset(Input); Assign (Output,'Output.Txt') ; ReWrite (Output) ;
312 Часть третья End; Procedure TInit(Var v, w:Integer;Var X:TMyArray); Var i,j:Integer; Begin ReadLn (v,w); For i:=1 To v Do Forj:=l To w Do Read(X[i,j]) ; End; Procedure TPrint(v,w:Integer;X:TMyArray); Var i , j:Integer; Begin Append (Output J ; For i:=l To v Do Begin For j:=l To w Do Write (X [i , J ]: 4) ; WriteLn; End; WriteLn; End; Procedure Solve(v,w, r:Integer;X,Y:TMyArray;Var Z:TMyArray); Var 1,g,t:Integer; Begin For i:=l To v Do For t;=l To r Do Begin Z [i, t]:=0; For j:=1 To w Do Z[i,t] : =Z[i, t]+X[i,J]*Y[j,t] ; End; End; Procedure Done; Begin Close(Input) ; Close(Output); End; Begin Init; TInit (n ,m,A) ; TPrint(n,m,A); TInit (m,t,B); TPrint(m,t,B); Solve (n,m,t,A,B,C); TPrint (n,t,C) ; Done; End. Пусть A — квадратный массив. Измените программу так, чтобы вычислялось выражение А+А*А+А*А*А.
Массив — фундаментальная структура данных 313 2. Решение системы линейных уравнений методом Гаусса. Си- стема из и линейных алгебраических уравнений с и неизве- стными имеет вид: а22*х2+а22*х2+. +aln*x1„=b1 a2i*xI+a22:tx2 + +a2„*x2n=b2 аП1:/гх1^аП2*х2+ +а„„*хпп=Ьп Решением системы называется такая упорядоченная сово- купность чисел х2=с;, х2=с2, ..., хп=сп, которая обращает все уравнения системы в верные равенства. Мы предполагаем, что система имеет единственное решение (не будем вдаваться в тео- рию линейной алгебры). В этом случае ниже описанная схема решения имеет право на существование. Суть метода Гаусса - последовательное исключение неизвестных из уравнений (пря- мой ход метода). После применения схемы исключения исход- ная система уравнений преобразуется в равносильную (имею- щую то же решение) треугольную: Х1+12,,'Х2+13’еХз+ +1п*Х„ = 1 Х2+2з*Х2-1- +2Л*ХП=2 хп~п Из этой системы последовательно находятся значения всех неизвестных хп, хп_{, ..., х; (обратный ход метода). Значение хп подставляется во все уравнения, затем полученное значение хп_; и т. д., до первого уравнения, из которого находится значе- ние хг Для сведения системы к треугольному виду коэффици- енты первого уравнения делятся на aIt, получим: Х2 + а22/а22 *х2+. +а1п/а11*х„=Ь1/а11 или (обозначив частные по-другому) Х2+22*Х2 + 2з*Хз + +2-,*Х„=2. Если ап=0, то в первом столбце ищется ненулевой коэффи- циент, а затем переставляются соответствующие строки. Суще- ствование такого коэффициента следует из единственности реше- ния системы. Пользуясь полученным результатом, неизвестное х2 исключается из других уравнений системы. Для этого из каж- дого уравнения вычитается первое, предварительно умножен- ное на соответствующий коэффициент при х;. После этого пер- вое уравнение оставляют в покое, а над остальными совершаются аналогичные преобразования.
314 Часть третья {$R-H Program Му24_2; Const MaxN=10; MaxM=MaxN+l; Type TMyArray=Array[1..MaxN,1..MaxM] OfReal; OMyArray=Array[1..MaxN] Of Real; Var A:TMyArray; X:OMyArray; n:Integer; Procedure TInit(Var v: Integer;Var X:TMyArray); {★Ввод коэффициентов системы уравнений.★) Var i, j : Integer; Begin Assign(Input,'Input.Txt');Reset (Input) ; ReadLn(v) ; For i:=1 To v Do For j:=l To v+1 Do Read (X[i, j ]) ; Close (Input); End; Procedure TPrint(v:Integer;X:OMyArray); (★Вывод решения. *) Var i:Integer; Begin Assign(Output,'Output.Txt'); ReWrite (Output); For i:=l To v Do Write (X[i] : 6) ; WriteLn; Close (Output); End; Function Solve(v:Integer;A;TMyArray;Var X:OMyArray) :Boolean;(★Находим решение - массив X, если оно есть. *} Var 1,j,k,1:Integer; w.Real; Pp:Boolean; Procedure Swap(k,1:Integer);{★Переставляем строки с номерами k, 1. *} Var j:Integer;w:Real; Begin For j:=l To v Do Begin w:=A[k,j];A[k,j]:=A[l,j]; A[l,j]:=w;End; End; Function Search(j:Integer):Integer;{★Поиск ненулевого коэффициента в столбце с номером j.
Массив — фундаментальная структура данных 315 Поиск осуществляется в строках с номерами J+1 .. V.*} Var а:Integer; Begin While (i<=v) And (A[i,j]=O) Do Inc(i); Search:=i; End; Begin {^Основная часть решения.*) J :=1;Pp:=False; While (j<=v) And Not (Pp) Do Begin If A[j,j]=O Then Begin k:=Search(j); If k>v Then Pp:=True Else Swap (j ,k) ; End; If Not Pp Then Begin {*Если есть решение. ★) w:=A[j,j];{*Прямой ход. Преобразуем уравнение с номером j . *) For k:=j То v+1 Do A[j,k]:=A[j,k]/w; For k:=j+l To v Do Begin {*Искпючаем x-, из остальных уравнений. *} w:=A[k,j]; For l:=j To v+1 Do A [ k, 1 ] : =A [ k, 1 ]-A [ j , 1 ] *w; End; End; Inc (j);{*Переходим к другой строке.*} End; If Not Pp Then Begin (*Обратный ход. *) For j:=v DownTo 1 Do Begin X[j] :=A[j,v+l] ; For i:=j-l DownTo 1 Do {*Исключаем найденное xj из всех уравнений.★) А[i,v+1] : =А [ i, v+1 ] -А [ i, J ] *Х [j J ; End; End; Solve:= Not Pp; End; Begin{*Основная программа. *) TInit(n,A);{*Ввод системы уравнений. *} If Solve(n,A,X) Then TPrint(n,X);{*Если есть решение, то его поиск и вывод результата.*) End.
316 Часть третья 3. Дана матрица из 0 и 1. Требуется привести ее к блочному по столбцам виду, т. е. переставить столбцы так, чтобы вначале Пример: В первой строке указаны номера столбцов. Суть решения в просмотре каждой строки одновременно слева направо и справа налево. Если при просмотре слева направо найден столбец с 0, а справа налево — с 1, то эти столбцы переставляются. При пере- ходе к следующей строке просмотр слева направо следует про- должить со столбца, на котором закончены действия при обра- ботке предыдущей строки. Procedure Solve(v:Integer;Var X:TMyArray); Var i,w:Integer; Procedure Swap(q,r:Integer); Var i,w:Integer; Begin For i:=l To v Do Begin w: =X [ i, q] ;X [ i, q] : =X [i, r] ; X[1,r]:=w;End; End; Procedure Change_St (i:Integer;Var к:Integer); (*Обработка строки с номером i. Параметр k - номер столбца, на котором заканчиваются действия в строке 1. *) Var j:Integer; Begin While k<j Do Begin If X[i,k]~l Then Inc(k){★Поиск единицы.*) Else If X[i,j]=O Then Dec(j){*Поиск нуля.*) Else Begin Swap (k, j ) ; Inc(k); Dec(j); End; End;
Массив — фундаментальная структура данных 317 If X[i,k]—1 Then Inc (к) ; { *3ачем этот оператор?*} End; Begin w: =1 ; 1: =1 ; While (1<=V) And (w<v) Do Begin{*Пока не просмотрены все строки и не исчерпаны все столбцы, выполняем действия из тела цикла. *) Change_St (i,w) ; ^Обрабатываем строку с номером 1. *} 1пс(1);{*Изменяем номер строки.★} End; End; Логика обработки соответствует примеру, приведенному выше Проверить, имеет ли матрица блочный вид. 4. Рассмотрим следующую задачу. Дана целочисленная матри- ца А. Сформировать матрицу В таким образом, чтобы в каж- дой строке были записаны номера столбцов матрицы А в по- рядке, соответствующем неубыванию значений элементов в той же строке матрицы А. Пример. Для простоты решения изменим типы данных. Двумерный массив определим через одномерные массивы (найдите место в тексте процедуры, где этот факт используется). д 21 12 10 43 37 25 14 52 74 12 42 13 13 10 20 15 25 11 40 21 30 10 20 31 32 22 12 41 25 40 10 20 30 40 50 60
318 Часть третья Type OMyArray=Array[1..MaxN] Oflnteger; TMyArray=Array[l..MaxN] Of OMyArray; Элементы каждой строки переписываются в дополнитель- ный массив, а затем сортируются одним из известных методов с одновременной перестановкой соответствующих элементов в стро- Procedure Solve (v:Integer;X:TMyArray;Var Y:TMyArray); Var i,t,g:Integer; Z:OMyArray; Procedure Swap (Var q,r:Integer); Var w: Integer; Begin w:=q;q:=r;r:=w ; End; Begin For i: =1 To v Do Begin Z:=X[i]; For t:=v-l DownTo 1 Do{*Сортировка. *) For j:=1 To t Do If Z[j]>Z[j+l] Then Begin Swap(Z[j] ,Z[j+l] ) ;Swap (Y[i, j ] ,Y[i,j+l] ) ; End; End; End; 5. Дана целочисленная матрица А. Требуется выполнить следу- ющее преобразование: найти в каждой строке минимальный элемент и вычесть его из всех элементов данной строки; за- тем аналогичное преобразование выполнить и со всеми столб- цами; при выполнении действий подсчитывать сумму мини- мальных элементов.
Сумма минимальных элементов для приведенного примера равна 31. {^Функция возвращает сумму минимальных элементов. *) Var s,i,w,j:Integer; Function MinStr (t: Integer):Integer;{*Поиск минимального элемента в строке. *} Var j,min:Integer; Begin min:=X[t,1]; For j:=l To v Do If X[t,g]<min Then min:=X[t,j] ; MinStr:=min; End; Function MinStl (t: Integer) : Integer; { *Поиск минимального элемента в столбце. *) Var i,min:Integer; Begin min:=X[l, t]; For 1:=2 To v Do If X[i,t]<min Then min:=X[1,t]; MinStl:=min; End; Procedure SubStr (i, k: Integer) ;{ *Вычитание из элементов строки с номером i значения к. *}
320 Часть третья Var j:Integer; Begin For j:=l To v Do X [ i, j ] : =X [ 1, j J -k ; End; Procedure SubStl(j,k:Integer);{*Вычитание из элементов столбца с номером j значения к.*) Var 1:Integer; Begin For i: =1 To v Do X[i, J ] :=X[1, j ] -k ; End; Begin s:=0;(Искомое значение.*} For i:=l To v Do Begin{Преобразование no строкам. *} w:=MinStr(i); Inc (s, w) ; SubStr(i,w) ; End; For j:=l To v Do Begin ( Преобразование no столбцам. *} w:=MinStl (j) ; Inc (s, v) ; SubStl (j ,w} ; End; Solve:=s;(*Результат. *} End; Модифицируйте решение задачи следующим образом. Из по- лученной в результате преобразования матрицы исключите про- извольный столбец и строку, на пересечении которых получен нуль. С новой матрицей выполните описанные выше операции. Продолжите этот процесс до тех пор, пока это возможно. Най- дите общее значение суммы минимальных элементов, получае- мой в результате этих действий. Задания для самостоятельной работы 1. Решить системы линейных уравнений: 4,4х, - 25*г + 192х3 - 105*з = 4,3 55*, - 9,3хг - 142*э + 132*4 = 63 7,1*, - 115*2 + 5,3*э - 6,7*4 = -18 142*, + 23,4хг -85*э + 5,3*з = 72 15,7х, + 6,6х2 -5,7х3 + Ц5х4 = -2,4 88*, - 6,7х2 + 55*з - 45*з = 5,6 6.3*, - 5,7хг - 23,4*3 + б.бХз = 7,7 5,6х, + 85хг - 6,7х3 - 235х4 = 7,3
Массив — фундаментальная структура данных 2. Следом квадратной матрицы называется сумма элементов, расположенных на главной диагонали. Даны квадратная матрица А порядка п, натуральное число т. Вычислить следы матриц А, А2, А3, .... Ат. 3. Дана квадратная матрица А порядка п. Переставить строки так, чтобы элементы в первом столбце были упорядочены по неубыванию. 4. В матрице А порядка п*т заменить нулями элементы, стоя- щие в строках и столбцах, где имеются нули. 5. Дана квадратная матрица А порядка п, каждый элемент ко- торой равен 0, 1, 5 или 11. Подсчитать в ней количество чет- верок А[i,j], А[i+l J], А[i,j+l], A[i+1 ,j+l], в каждой из ко- торых все элементы различны. Примечание Если все числа различны, то их сумма равна 17. Верно и обратное утверждение. 6. Дана квадратная матрица А порядка п из 0 и 1. Прямоуголь- ником назовем часть матрицы, заполненной 1. Известно, что прямоугольники не соприкасаются друг с другом. Найти ко- 7. Дана квадратная матрица А порядка п из натуральных чи- сел. Находится минимальный элемент А и вычитается из всех элементов матрицы. Затем строки и столбцы, содержащие ну- левые элементы после вычитания, «вычеркиваются» из мат- рицы. Процесс продолжается до тех пор, пока не будет полу- чено одно число. Найти суммы минимальных элементов. 8. Дана квадратная матрица А порядка п из 0 и 1. Считаем, что столбец «покрывается» другим столбцом, если множество строк, представленных 1 в этом столбце, содержится в мно- жестве строк, представленных 1 в другом столбце. Исключить такие столбцы из матрицы.
322 Часть третья 9. Дана квадратная матрица А порядка п из 0 и 1. Исключить из матрицы столбцы и строки, содержащие одну единствен- ную 1. Если после исключения появились новые строки и столбцы, удовлетворяющие этому условию, то продолжить процесс исключения. 10. Квадратную матрицу А порядка п из 0 и 1 назовем правиль-
Массив — фундаментальная структура данных 323 Занятие № 25. Комбинированный тип данных (записи) План занятия □ описание типа данных; □ разбор процедур и функций работы с датами и простейши- ми геометрическими объектами; □ выполнение самостоятельной работы. Описание типа данных. При работе с массивами основное ограничение заключается в том, что каждый элемент должен иметь один и тот же тип. Однако при решении многих задач возникает необходимость хранить и обрабатывать совокупности данных различного типа как единое целое. Запись — это составной тип данных, содержащий набор элемен- тов разных типов. Составляющие запись элементы называются ее полями. В записи каждое поле имеет свое собственное имя. Чтобы описать запись, необходимо указать ее имя, имена объ- ектов, составляющих запись и их типы. Общий вид такой: Туре <имя зэписи> = Record <поле 1>:<тип 1>; (поле 2>:<тип 2>; <поле п>:<тип л; End; Пример. Опишем дату. Она состоит из года, месяца и дня. Определив их, мы определяем дату как запись, состоящую из соответствующих полей. Туре уеаг=1583..3000 ; month_number=l. . 12 ; day=l..31; date=Record dyear:year; dmonth:month_number; Dday:day; End; Разумеется, что тип date можно было определить и так: Type date=Record dyear: 1583..3000; dmon th: 1..12; dday: 1..31; End;
324 Часть третья но остановимся на первом способе, ибо при решении задач с да- тами нам потребуется работа и с типами данных year, month_ number, day. С полем записи в программе можно поступать, как с пере- менной того же типа, что поле. Обращаются к полю по состав- ному имени: <имя записи>.<имя поля>. Пример. Есть Var t:date; Следующие операторы присвоения имеют силу: t.dyear:=1987; t.dmonth:=12; t.dday:=30; Для не любителей писать много эта запись сокращается с по- мощью оператора With: With <имя записи> Do <оператор>;. Операторы присвоения в этом случае записываются несколько по-другому. With t Do Begin dyear:=1987; dmonth:=12; dday:=30; End; Оператор Case состоит из выражения (селектора) и списка опе- раторов, каждому из которых предшествует либо одна или не- сколько констант (называемых константами варианта), либо слово Else. Селектор должен быть порядкового типа, причем порядковые значения верхней и нижней границ этого типа дол- жны находиться в диапазоне от -32768 до 32767. Оператор Case выбирает для выполнения тот оператор, перед которым стоит константа, равная значению селектора или диа- пазону, содержащему значение селектора. Если в диапазоне выбора не существует такой константы выбора и в операторе имеется часть Else, выполняется оператор, написанный после слова Else. Если часть Else отсутствует, ни один оператор не выполняется. Примеры: Var c:Char, Begin ReadLn (с) Case c Of
Массив — фундаментальная структура данных '0'. .'9': WriteLn ('с — цифра ' а'. . ' z': WriteLn ('с — маленькая латинская буква .') ; 'А'. . ' Z' :WriteLn (' с — большая латинская буква. ') ; Else WriteLn ('с — некоторый другой символ.') End; End; Var i:Integer; Begin ReadLn (i) ; Case i Of 0,2,4,6,8: WriteLn (' i — четное число меньше 10.'); 1,3,5, 7,9: WriteLn ('i — нечетное число меньше 10.') ; End; End; Экспериментальный раздел работы 1. Работа с датами. Используются типы данных year, months number, day, date, определенные выше по тексту. Определим ряд процедур и функций. Год считается високосным (366 дней), если он не последний год столетия и делится без остатка на 4. Кроме того, последний год любого столетия считается ви- сокосным только в том случае, если его номер делится на 400. Function Leap (х:year):Boolean; Begin Leap:=(x Mod 400 =0) Or (x Mod 100O0) And (x Mod 4=0) ; End; Задача определения количества дней в месяце решается с помощью следующей функции: Function DayM (х: year ;у :month_number) :day; Begin Case у Of 4, 6,9,11:DayM:=30;{*B апреле, июне, сентябре и ноябре 30 дней. *} 1,3, 5, 7, 8,10,12: DayM: =31; { *В январе, марте, мае, июле, августе, октябре и декабре 31 день. *) 2: If Leap(x) Then DayM:=29 Else DayM:=28; (*Если год високосный, то в феврале 29 дней. *) End; End; 12—452
326 Часть третья Для вывода даты, например в виде 30.12.1987, используется ее преобразование в тип String. Function StringDate(w:Date): String; Var s,q:String; Begin str(w.dday,s); If w.dday<10 Then s:='0'+s;{*Преобразуем день даты. *} s:=s + '.'; str (w. dmonth,q);{*Преобразуем месяц даты. *} If w.dmonth<l0 Then s:=s+'O'+q+'.' Else s:=s+q+'.'; str(w. dyear,q);{* Преобразуем год даты. *} s:=s+q; StringDate:=s; End; Из текущей даты требуется получить дату следующего дня. При решении этой задачи необходимо учитывать, является ли: □ день даты последним днем месяца; □ месяц даты последним месяцем года. Procedure Tomorrow(х:date;Var y:date); Begin If x. ddayoDayM (x. dyear ,x. dmonth) Then у.dday:=x.dday+1{*День даты не является последним днем месяца . *) Else If х.dmonth<>12 Then Begin у.dmonth:=x.dmonth+1;{*Месяц даты не является последним месяцем в году. *} у.dday:=l; End Else Begin у.dyear:=x.dyear+1; у. dmonth:=1; у. dday:=1; End; End; Для определения даты, которая наступит в будущем, через определенное количество дней (х), можно воспользоваться пре- дыдущей процедурой, продвигаясь вперед на один день х раз. Однако шаг продвижения может быть больше, если w превы- шает количество дней в году...
Массив — фундаментальная структура данных 327 Procedure Future(х:Integer;Var у:Date);{*х — количество дней, у — текущая дата. ★} Var 1:Integer; Function Dyears (z :year):Integer;{‘Определяем количество дней в году. *} Begin If Leap(z) Then Dyears:=366 Else Dyears:=365; End; Begin For i:=l To y.dmonth-1 Do x:=x+DayM(y.dyear,i); {★Возвращаемся в начало года, определенного текущей датой. . *} х:=х+у.dday;{‘Прибавляем количество дней из даты. Получаем общее количество дней от начала текущего года.★} While x>Dyears(y.dyear) Do Begin{‘Счет идет по годам. *} х: =x-Dyears (у. dyear) ; Inc (у. dyear) ; End;{*Год будущей даты сформирован. *) у.dmonth:=1;{★Определяем месяц будущей даты. *} While x>DayM (у. dyear,у. dmonth) Do Begin {‘Счет идет по месяцам.★) х: =x-DayM (у. dyear,у. dmonth) ; Inc (у. dmonth) ; End;{‘Месяц будущей даты получен. *) у.dday:=x;{★Оставшиеся дни являются днями будущей даты. *) End; Основная программа для работы с датами может иметь сле- дующий вид: Program Му25_1; Var t, г: date ; w -.Integer ; { * Процедуры и функции. ★) Begin WriteLn(' Введите год, месяц, день даты: '); ReadLn (t. dyear, t. dmonth, t. dday) ; Tomorrow (t, r) ; WriteLn (StringDate (r)) ; WriteLn (' Введите количество дней до будущей даты.'); ReadLn (w) ;
328 Чвсть третья Future(w,t); WriteLn(StringDate (t)); ReadLn; End. Многочисленные модификации данной программы получают- ся при выполнении ряда заданий для самостоятельной работы. 2. Начальные сведения из компьютерной геометрии. Точка на плоскости в декартовой прямоугольной системе координат описывается парой вещественных чисел. Введем соответст- вующий тип данных — запись. При использовании вещест- венного типа операции сравнения лучше оформить специаль- ными функциями. Причина известна, на типе Real в системе программирования Турбо Паскаль нет отношения порядка, поэтому записи вида а=Ь, где а и b вещественные числа, луч- ше не использовать. По этой же причине все вычисления с ве- щественным типом лучше выполнять с какой-то, заранее определенной, точностью. Type TPoint=Record х, у: Real; End; Const Eps: Real=le-7;{*Точность вычисления.*I ZeroPnt: TPoint = (x:0; y:0);{*Точка c координатами 0,0. *) Пример реализации операций сравнений: Function RealEqfa, b: Real): Boolean; {*Строго равно. *) Begin RealEq:=Abs(a-b)<=Eps; End; А функция проверки совпадения двух точек на плоскости имеет вид: Function EqPoint(A, В: TPoint):Boolean;{fСовпадают ли точки ?*} Begin EqPoint:=RealEq(A.x, В.х) And RealEqfA.y, В.у) ; End; При вычислении расстояния между двумя точками на плос- кости удобнее опять же использовать функцию. Function Dist (А, В: TPoint): Real;{^Расстояние между двумя точками на плоскости. *) Begin Dist:=Sqrt(Sqr(А.х-В.х) + Sqr(А.у-В.у)); End;
Массив — фундаментальная структура данных 329 Любая точка на плоскости определяет вектор (ц) — направ- ленный отрезок, соединяющий точку (0,0) с точкой (х,у). Век- тор v можно задать в полярной системе координат через его длину (модуль) v и угол относительно оси ОХ. Координаты по- лярной системы координат (у,) и прямоугольной декартовой (х, у) связаны соотношениями: x=v*cos (а), y=v*sin(a), v=jx2 + у2 , tan (а)=у/х. Разработаем функцию определения угла а (в радианах) по координатам точки в декартовой системе координат.ъ Function GetAngle(w:TPoint):Real;(*w - ненулевая точка на плоскости. *} Var v, angle:Real; Begin v:=Dist(w,ZeroPnt); {^Расстояние.*} If RealEq(v,0) Then GetAngle:=O Else Begin If RealEq (w. x, 0) Then If w.y>0 Then angle:=Pi/2 Else angle:=3*Pi/2 ('Выделяем особые случаи. Точка находится на одной из осей. *) Else If RealEq(w.у,0) Then If w.x>0 Then angle:=0 Else angle:=Pi Else Begin angle::=ArcTan (Abs (w.y/w.x)) ; (Определяем четверть плоскости. *) If w.x*w.y>0 Then Begin If w.x<0 Then angle:=Pitangle; End
330 Часть третья Else If w.x<0 Then angle:=Pi/2+ angle Else angle:=2*Pi-angle; End; GetAngle:=angle; End; End; Скалярным произведением двух векторов называется число, равное произведению длин этих векторов на косинус угла меж- ду ними. Если хоть один из векторов нулевой, то угол не опре- делен, и скалярное произведение по определению считается равным нулю. Таким образом, (v,w)= v*w*Cos(a), где а — угол между векторами v и w. Напомним, что косинус угла между векторами v, w вычис- ляется по формуле: Cos(a)=(v.x*w.x+v.y*w.y )/(v*u>). Таким об- разом, скалярное произведение (v,w)= v.x*w.x+v.y*w.y. Function ScDecfv, w: TPoint): Real; {★Скалярное произведение векторов в прямоугольной декартовой системе координат.★} Begin ScDec:=v.x*w.x+v.y*w.у; End; Вычисление скалярного произведения векторов в полярной системе координат ((и.о.) и (w,f))) выполняется по формуле: v*w=u.x*w.x+v.y*w.y =v*Cos('a)*w*CosCP)+v*Sin('a)*w*Sin('P)= =L>*w*Cos(a-p/ Из этого соотношения следует, что скалярное произведение □ ненулевых векторов равно нулю тогда и только тогда, ког- да векторы перпендикулярны; □ больше нуля, если угол между векторами острый, и мень- ше нуля, если угол тупой. Прямая линия на плоскости, проходящая через две точки и р2, определяется следующим линейным уравнением от двух переменных: (p^x-p^x^fy-p^Mp^y-p^^fx-p^). После пре- образований получаем: -(р2-у~р1.у) *х +(р2.х-р1.х)*у+(р2.у~р1.у)* * Р,-х - (pg.x-p^x) * pt.y=O, илиА*х + В*у + С = 0, после соот- ветствующих обозначений. Если прямые заданы с помощью уравнений А^х+В^у+С^О и А2*х+В*у+С2=0, то точка их пересечения, в случае ее суще- ствования (А,*В2-А2*В, <>0), определяется по формулам:
Массив — фундаментальная структура данных 331 х=- (С2 *B2-C2 ★В1) / (Ат ★Вг-Аг'В!) , у=(А2*С1-А1*С2) / (А2 *B2-A2*B!) . Определим тип данных для описания прямой Туре Т1Лпе= =Record А, В, С: Real;End;, процедуру вычисления коэффициен- тов в уравнении прямой, проходящей через две точки и функ- цию вычисления координат точки пересечения двух прямых. Procedure Point2ToLine(А, В: TPoint; Var L: TLine}; { 'Определение уравнения прямой по координатам двух точек. *} Begin L.А:=В.у-А.у;L.В:—А.х~В.х;L.С:=- (A.'<*L.A + A.y'L.b); End; Function Line2ToPoint(fL, sL: TLine; Var P: TPoint): Boolean; {'Определение координат точки пересечения двух линий. Значение функции равно True, если точка есть, и False, если прямые параллельны. '} Var st: Real ; Begin st:=fL.A'sL.B-sL.A'fL.В; If Not (RealEq(st, 0) ) Then Begin Line2ToPoint:=True; P.x:=-(fL.O'sL.B-sL.C*fL.B)/st; P.у:=(sL.A* fL.C-fL.A'sL.C)/st; End Else Line2ToPoint:=False; End; Задания для самостоятельной работы 1. Написать процедуру (или функцию) определения: □ даты вчерашнего дня; □ даты, которая была за m дней до указанной даты; □ количества дней, прошедших от даты t; до t2; □ дня недели, выпадающий на дату t; (для решения зада- чи необходимо ввести тип данных «день недели» и опре- деленную дату «связать» с конкретным днем недели); □ годов столетия (1900-2000), которые начинаются и за- канчиваются в воскресенье; □ годов столетия, содержащих максимальное число вос- кресений.
332 Часть третья 2. Дана дата Вашего рождения (включая и день недели). Найти те даты, когда Ваш день рождения «попадает* на тот же день недели. 3. Даны даты рождения членов семьи из пяти человек. Столет- ними юбилеями семьи являются дни, когда сумма возрастов всех членов семьи кратна 100. Найти такие даты. 4. Дано время, описанное следующим образом: Type time = Record h: 0..23;т, s: 0..59 End; Описать: □ логическую функцию для проверки, предшествует ли время tt времени t2 (в рамках суток); □ процедуру, присваивающую параметру время, на 1 се- кунду большее времени t (учесть смену суток). 5. Дано следующее описание данных: Const п=300; Type MyRecord = Record key: Integer; name : String; End; Table=Array[1..n] Of MyRecord. Считая, что записи в массиве имеют различные ключи, опи- сать: □ процедуру, упорядочивающую записи массива по убыва- нию значений поля key; □ логическую функцию поиск(4,й,й), определяющую, есть ли в массиве t (все записи которой уже упорядочены по возрастанию значений поля key) запись со значением поля key, равным k, и, если есть, присваивающую ее но- мер параметру h. 6. Дан массив, содержащий информацию об учениках некото- рой школы (данные вводятся из файла). Написать програм- мы: □ формирования второго массива с данными об учениках только девятых классов; □ определения количества учеников восьмых и девятых классах. 7. Багаж пассажира характеризуется количеством вещей и об- щим весом вещей. Дан массив, содержащий сведения о бага- же нескольких пассажиров (данные вводятся из файла). Све- дения о багаже каждого пассажира представляют собой запись с двумя полями: одно поле целого типа (количество вещей) и одно — действительное (вес в килограммах). Определить:
Массив — фундаментальная структура данных 333 □ багаж, средний вес одной вещи в котором отличается не более чем на 0,3 кг от общего среднего веса одной вещи; □ число пассажиров, имеющих более двух вещей, и число пассажиров, количество вещей которых превосходит сред- нее число вещей; □ имеется ли пассажир, багаж которого состоит из одной вещи весом менее 30 кг. 8. Разработать процедуры перевода координат точки из декар- товой в полярную (и обратно) системы координат. Для опи- сания точки в полярной системе координат использовать тип данных Type TPol=Record r,ang:Real; End. Описание проце- дур должно иметь следующий вид; Procedure TurnDecPol(w: TPoint; Var q: TPol); {★Перевод из декартовой системы координат в полярную. *) Procedure TurnPolDec(q: TPol; Var w: TPoint); (*Перевод из полярной системы координат в Декартовую. *) 9. Разработать процедуры сложения, вычитания векторов в де- картовой (полярной) системе координат. Пример: Procedure AddPolfa, b: TPol; Var c: TPol); (*Сумма двух векторов в полярной системе координат. *) Begin с.ang:=(а.ang+b.ang)/2; С. r:=Sqrt (Sqr (а . r*Cos (а . ang) -b. r*Cos (b. ang)) + Sqr (a ,r*Sin (a . ang) -b.r*Sin (b.ang) )) ; End; 10. Координаты точек отрезка (pItp2) можно задать двумя пара- метрическими уравнениями от одной независимой перемен- ной t: р.х=р ^.x+fр2.х~р t.x )*t u p.y=pI.y+(p2.y~p1.x)*t. При 0<Л<1 точкар(х.у) лежит на отрезке, а при t<0 или t>l — вне отрезка на прямой линии, продолжающей отрезок. Даны координаты двух точек на плоскости, определяющих отрезок, и координаты третьей точки. Определить, принад- лежит ли третья точка отрезку? 11. Даны два отрезка. На рисунке точка пересечения отрезков, если она есть, обозначена какр=(х,у). Первый отрезок за- дан точками р1=(х1,у2) и р2=(х2,у2), а второй — точками р3=(х3,у3) ир4=(х4,у4). Определить координаты точки пере- сечения отрезков.
334 Часть третья р4 = (Х4,У4) 'Р2 = (Х2,У2) ^Р=(х,у) pi=(xi,yi) ' рз=(х3,уз) 12. ДаноК точек. Найти пару точек с максимальным расстояни- ем между ними. 13. Дано множество прямых. Подсчитать количество точек пе- ресечения этих прямых. 14. Дано два множества точек. Найти пересечение и разность этих множеств. 15. Известно, что два отрезка находятся на одной прямой. Опре- делить их взаимное расположение. 16. Дано множество точек. Выбрать из него две точки, такие, что- бы количество точек, лежащих по разные стороны от прямой, проходящей через эти две точки, различалось наименьшим образом. 17. Даны координаты вершин треугольника и точки. Определить взаимное расположение этих геометрических объектов. 18. Даны координаты вершин треугольник и отрезка. Определить их взаимное расположение. 19. Даны координаты вершин двух треугольников. Определить их взаимное расположение. 20. Дано множество прямых. Определить количество частей, на которые они разбивают плоскость. Примечания 1. В задачах все геометрические объекты задаются на плоскости. 2. Исходные данные в задачах вводятся из файла.
Часть четвертая Динамические структуры данных Занятие № 26. Динамические структуры данных План занятия □ основные сведения о ссылочном типе данных; □ линейные списки, изучение основных операций; □ экспериментальная работа с программой «считалочка» и реализацией списка через обычные массивы; □ выполнение самостоятельной работы. Основные понятия о ссылочном типе данных (указате- лях). Изученные до настоящего занятия типы данных явля- лись статическими. Область памяти для их размещения выде- лялась на стадии компиляции. Перераспределение ее на стадии выполнения программы не допускалось. Поэтому, в частности, приходилось резервировать, например для массивов, максима- льно возможный в задаче его размер. Выделение памяти для переменных на стадии выполнения программы возможно с использованием нового типа данных — указателей (ссылок). Значением указателя (переменной ссы- лочного типа) является адрес области памяти (первой ячейки) на переменные заданного базового типа. Итак, не значение пе- ременной (величины), а адрес памяти, в которой находится пере- менная (принцип косвенной адресации). Для указателей область памяти выделяется статически (как обычно), а для переменных, на которые они указывают, — динамически, т. е. на стадии вы- полнения программы (они и называются динамическими). Для хранения динамических переменных выделяется специальная область памяти, называемая «кучей». Работая с указателями, мы работаем с адресами величин, а не с их именами. Может быть, поэтому и появляется возможность выделять для дина- мических переменных память на стадии выполнения? Для объявления указателей (переменных ссылочного типа) используется специальный символ «А», после которого указы- вается тип динамической (базовой) переменной.
336 Часть четвертая Туре <имя_типа>=" <базовый тип> ; Var <имя_переменной>: <имя_типа>; или <имя переменной>: " <базовый тип>; Например: Type ss = "Integer; Var х, у: ss; {^Указатели на переменные целого типа. *) a: "Real;{^Указатель на переменную вещественного типа. *j Зарезервированное слово Nil обозначает константу ссылоч- ного типа, которая ни на что не указывает. Выделение оперативной памяти (в «куче») для динамиче- ской переменной базового типа осуществляется с помощью про- цедуры New(x), где х определен как соответствующий указа- тель. Обращение к динамическим переменным выполняется по правилу: <имя_переменной>". Например, х':=15 — в область памяти (два байта), адрес которой является значением указате- ля х, записывается 15. Процедура Dispose(х) освобождает память, занятую динами- ческой переменной. При этом значение указателя х становится неопределенным. New(x) Линейные списки. Списком называется структура данных, каждый элемент которой посредством указателя связывается со следующим элементом. Из определения следует, что каждый элемент списка содержит как минимум одно поле данных (на- зовем его data и для простоты считаем его типа Integer), оно может иметь сложную структуру, и поле ссылки на следующий элемент (назовем его next). Поле ссылки последнего элемента
Динамические структуры данных 337 списка имеет значение Nil. Указатель на начало списка (пер- вый элемент) является значением отдельной переменной. При- мер списка, содержащего в поле данных целые числа 3, 5, 1, 9, приведен на рисунке. First Описание элемента списка, используемого в течение данного занятия, имеет вид: Type pt^elem; {*Указатель на элемент списка.*) elem=Record data:Integer;{*Поле данных. *) next:pt;{*Укаэатель на следующие элемент списка. *) End; Var firstipt; {*Указатель на первый элемент списка. *) Основные операции с элементами списка: □ просмотр элементов списка; □ вставка элемента в список; □ удаление элемента из списка. Просмотр элементов списка, если он создан, очевидная про- цедура. Procedure Print (z;pt) ; Begin While zOHil Do Begin Write (z* .data, ' '); z:=z*.next; End; End; Ее рекурсивная реализация: Procedure Print (z :pt) ; Begin If zoNil Then Begin Write (z*.data, ' ’); Print (z*. next) ; End; End;
338 Часть четвертая Измените ее так, чтобы элементы списка выводились, начи- ная с последнего. Вставка элемента в список возможна логически в его нача- ло, конец и средину. Разберем эти случаи. Вставка в начало списка имеет вид: First New_first Procedure Ins_begin(Var first:pt; el:Integer); Var new_first:pt; Begin New (new_first) ;(*!*) new_firstЛ.next:=first;{*2*) ne№_firstA. data :=el; first: =new_first; { *3*) End; «Жирными» отрезками на рисунке выделены действия, вы- полняемые в процедуре. С помощью цифр на рисунке и в тексте процедуры показаны действия соответствующих операторов. Вставка в конец списка осуществляется с помощью следую- щей процедуры. Вопрос о том, каким образом значением указа- теля last стал адрес последнего элемента списка, пока оставим открытым. First
Динамические структуры данных Procedure Ins_end(Var last:pt; el:Integer); Begin New (last"'. next) ; (*1 * } last:=last'.next;(*2*} last".data:=el; lasf". next:=Nil; End; Вставку в средину списка проиллюстрируем очередным ри- сунком. Очевидно, что для вставки элемента в список необходимо знать, как минимум, адрес предшествующего элемента списка, т. е. элемента, после которого осуществляется вставка. И мы счи- таем, что этот элемент не последний. При вставке разрывается су- ществующая связь (отмечена «крестиком») и появляются две но- вые связи, выделенные на рисунке «жирными» отрезками. Пусть мы создаем упорядоченный по неубыванию список эле- ментов (информационная часть любого элемента списка меньше информационных частей следующих за ним элементов списка или равна им). Для того, чтобы найти место для вставки очеред- ного элемента (значение указателя рх на рисунке), следует про- сматривать элементы списка до тех пор, пока вставляемый эле- мент больше информационной части текущего элемента. t el [~7~1 гул Г _____________L Сравнение
340 Часть четвертая Procedure Ins_med(Var first:pt; el:Integer); Var t, new_m:pt; Begin New (new_m) ; t:=first; While el>t".next".data Do t:=t".next; new_m^ . next :=ЬЛ. next ; new_m''. data : =e; tл.next:=new_"; End; И наконец, общая логика вставки элемента в упорядочен- ный список. Создаваемый список отличается от декларирован- ного в начале тем, что он описывается двумя указателями: на первый и последний элементы списка. Procedure Ins(Var first,last:pt; el:Integer); Begin If eKfirst''. data Then Ins_begin(first,el) Else If el>last''. data Then Ins_end(last,el) Else Ins_med (first,el); End; Ввод исходных данных при создании упорядоченного списка осуществляется с клавиатуры. Напомним, что признак конца файла (Eof = True) в этом случае вырабатывается при нажатии клавиш Ctrl+Z. Все действия по организации ввода представле- ны в процедуре Solve. Procedure Solve(Var first,last:pt); Var el:Integer; Begin Write('First element: '); Read (el) ; If Not Eof Then Begin first:=Nil; Ins_begin(first,el) ; last:=first; End Else Begin first:=Nil;last:=Nil;End; While Not Eof Do Begin Write('Next: '); Read(el); If Not Eof Then Ins(first,last,el) ; End; End;
Динамические структуры данных 341 Проведите несколько экспериментов и проанализируйте ре- зультаты: □ исключите логику отдельного ввода первого элемента спи- ска из Solve; □ исключите анализ признака конца файла из тела цикла While; □ исключите переменную Last, список должен иметь только один указатель на начало. Действия при удалении элемента из списка различны в за- висимости от места удаляемого элемента: первый он или нет. На рисунке показано удаление элемента из средины списка. Для сохранения структуры списка необходимо помнить адрес элемента списка, предшествующего удаляемому элементу(йх), а также запоминать адрес удаляемого элемента для корректно- го использования процедуры Dispose. формационная часть которых равна заданному числу (el). Procedure Del_el (Var firstipt; elilnteger) ; Var t,x,dx:pt; Begin t :=first;{*Переменная цикла. *} While toNil Оо{*Пока список не просмотрен.*) If t‘'.data=el ТЬеп{*Есть совпадение.*) If t=first Then Begin{*Удаляем первый элемент списка. *) х:=first;{*3апоминаем, ибо «кучу» засорять не следует. *) First :=first'~. next; {*Изменяем значение указателя на первый элемент списка. *) Dispose (х) ; { *Освобождаем место в «куче». *)
342 Часть четвертая t:=first;{*Переменная цикла изменила свое значение. *) End Else Begin х: =t;{^Запоминаем адрес удаляемого элемента. *) t :=t''. next ; dx1'. next: =t; ( ‘Удаление элемента не должно нарушать структуру списка, ключевой оператор процедуры. *) Dispose (х); End Else Begin dx:=t;t:=t".next;End;{‘Переход к следующему элементу списка. Адрес текущего запоминается в переменной dx. *} End; Отладка программ, использующих указатели, достаточно не- простое занятие, особенно при первом знакомстве. Рекоменду ется создавать небольшой список, например из четырех элемен- тов 1, 2, 3, 4, и в окне Watches отслеживать изменение всех связей. При отладке процедуры Del_el в пошаговом режиме окно Watches может иметь в начальный момент времени следу- ющий вид. Watches t'.data t". next", data 2 t".next".next" data 3 t" next" next".next" data 4 first".data firstt" next".data 2 firstt" next" next" data 3 first".next".next" next" data 4 Экспериментальный раздел работы 1. «Считалочка». N ребят расположены по кругу. Начав отсчет от первого, удаляют каждого k-го, смыкая при этом круг. Определить порядок удаления ребят из круга. Для хранения данных об участниках игры используем коль- цевой список (значением поля Next последнего элемента явля- ется адрес первого элемента списка).
Динамические структуры данных 343 Program Count; Const N=10; Type pt='elem; elem=Record data:Integer; next:pt; end; Var first:pt; Procedure Print(u:pt);(*Вывод элементов списка.*} Var t:pt; Begin t:=u; Repeat Write It* . data, ' ') ;t:=t'.next; Until t=u;(*Ho тех пор, пока указатель не получит значение адреса на первый элемент списка.*} WriteLn; End; Procedure Init(Var first:pt);{*Первоначальное формирование списка.*} Var t,last:pt;i:Integer; Begin New(t);t'.data:=N;t".next:=Nil;first:=t;last:=t; For i:=N-l DownTo 1 Do Begin New(t) ; t'.next:=first;t'.data:=i; first:=t; last*.next:=t; End; End; Procedure Game(u:pt; k:Integer); Var t:pt; i:Integer; Begin t:=u; Repeat For i:=l To k-1 Do Begin { 'Пропускаем k-1 элемент. *}
344 Часть четвертая t:=и;и:=ил.next; End; Wri teLn (ил.data,' ') ; { *Удаляемый элемент. *} tA.next:=ил.next; Dispose (и); и =tл . next; Print (и) ;(*To, что осталось в списке.*} Until и=и''. next; { *Пока не останется один элемент в списке. *) End; Основная программа. Begin first:=Nil; Init (first);Print(first); Game (first,2);{★Удаляем каждого второго ребенка. *} End. Исследуйте задачу для различных значений N. Составьте таблицу оставшихся ребят (t — номер оставшегося ребенка) для значений N от 1 до 64. Экспериментальным путем установите закономерность: □ t(l )=1 при N=l, □ t(2*N)=2*t(N)-l при N>1, □ t(2*N+l )=2*t(N )-1 при N>1. Если N=2'n+g, где 2m — наибольшая степень 2, не превосхо- дящая N, a. q — разность N-2m, то номер оставшегося ребенка вычисляется по формуле: t(2m+q)=2*q+l при т>0 и 0<q<2m. Экспериментально проверьте правильность данной формулы и напишите версию программы, вычисляющей номер оставшего- ся ребенка без использования ссылочного типа данных. Срав- ните результаты. 2. Промоделируем работу со списком, используя массивы и не используя указателей. В массиве А храним значения элемен- тов в порядке их поступления на обработку, в массиве Next — ссылку на следующий элемент. Создадим упорядоченный по возрастанию список элементов. В переменной head храним ад- рес первого элемента списка. Глобальные переменные нашей программы:
Динамические структуры данных 345 Const N- ; Nl=7777; { *N1 - несуществующая ссылка на следующий элемент. *) Туре МуАггау=Аггау[1. . NJ Of Integer; Var A,Next:МуАггау; Пусть на обработку поступили числа 5, 7, 3, 4. Состояние переменных отражено на рисунке. После обработки чисел 1 и 9 значения элементов массивов А и Next изменится на следую- щее. В массив А элементы записываются в порядке их поступ- ления на обработку. Для такой записи необходимо знать адрес очередной свободной ячейки (значение переменной ind). Неско- лько сложнее с массивом Next. В переменной head указывается место записи наименьшего элемента массива А, а в Next [head] — адрес следующего по возрастанию элемента массива А. Next[ind] записывается адрес следующего по возрастанию эле- мента массива A, a ind является значением элемента Next, со- ответствующим предыдущему элементу. Итак, процедура встав- ки элемента в список имеет вид: В-452
Часть четвертая Procedure Insert (Var head:Integer;x:Integer) ; Var i,j:Integer; Begin Inc(ind);A[ind]:=x;Next[ind]:-0;{★Записываем в А по текущему значению переменной ind. *) i:=head;j:=head;(*B j храним адрес предыдущего элемента списка.*') While (1<>0) And (x>A[i]) Do Begin j:=i;i:=Next[i]; End; {*Обычный просмотр элементов списка. ★) Next[ind]:=i;{*Номер следующего элемента списка является значением переменной 1. *) If i=head Then head:=ind Else Next[j]:=ind; {★Если элемент вставляется в начало списка, то изменяется значение переменной t, иначе корректируется поле ссылки предыдущего элемента.*) End; Написание процедуры удаления элемента из списка не вы- зывает особых сложностей. Необходимо найти удаляемый эле- мент и изменить значение ссылки у предыдущего элемента — Next. В процедуре на место удаляемого элемента в массиве А записывается 0, а в массиве Next значение 7777. Procedure Delete(Var head:Integer;x:Integer); Var i,j:Integer; Begin i:=head;j:=head; While (i<>0) And (x<>A[i]) Do Begin { ★Поиск удаляемого элемента.*) J :=i;i:=Next[i]; End; If iohead Then Begin Next[jJ:=Next[i];{*Изменение значения ссылки на следующий элемент списка. *} A[i]:=0; Next[i]:=N1;{*Необязательные операторы.*) End Else Begin j:=head; head:=Next[head]; {★Необязательные операторы.*) Next [j] : =N1; { ★Удаляем первый элемент списка.*) End; End;
Динамические структуры данных 347 Разработайте полный текст программы для работы со спис- ком. Измените программу так, чтобы запись очередного эле- мента осуществлялась на первое свободное место массива А, т. е. в Вашей основной программе вставка и удаление элемен- тов должны быть не последовательными процессами, а взаимно чередующимися. Задания для самостоятельной работы 1. Разработать: □ функцию, вычисляющую среднее арифметическое эле- ментов непустого списка; □ рекурсивную функцию проверки наличия в списке за- данного элемента; □ процедуру перестановки первого и последнего элементов непустого списка; □ процедуру вставки нового элемента перед (после) каж- дым вхождением заданного элемента; □ функцию проверки совпадения списков и L2, □ функцию проверки вхождения списка в список L2, □ процедуру переноса в конец непустого списка L его пер- вого элемента; □ процедуру переноса в начало непустого списка его по- следнего элемента; □ процедуру копирования в список L за каждым вхожде- нием заданного элемента всех элементов списка □ процедуру объединения двух упорядоченных по неубы- ванию списков Lj и L2 в один упорядоченный по неубы- ванию список путем построения нового списка L и изме- нением соответствующим образом ссылок в и L2\ □ функцию проверки упорядоченности элементов списка; □ функцию подсчета количества слов списка (поле Data имеет тип String), начинающихся и оканчивающихся од- ним и тем же символом. 2. Разработать процедуру удаления из списка L: □ второго элемента, если такой есть; □ всех элементов, равных х; □ первого отрицательного элемента, если такой есть; □ всех отрицательных элементов. 3. Разработать процедуру формирования списка L путем вклю- чения в него по одному разу элементов: □ входящих хотя бы в один из списков Lj и L2; □ входящих одновременно в оба списка и L2;
348 Часть четвертая □ входящих в список Lj, но не входящих в список L2; □ входящих в один из списков и L2, но в то же время не входящих в другой из них. 4. Многочлен Р(х )=апхп+ап_1хп 1+...+а1х+ад с целыми коэффи- циентами можно представить в виде списка, причем если а=0, то соответствующий элемент не включается в список (на рисунке показано общее представление многочлена и пример s(x)=-5x6+3x2-x+7). нию многочленов, разработать следующие функции и процеду- ры для работы с этими списками-многочленами: □ логическую функцию Е quality (p,q), проверяющую ра- венство многочленов р и q; □ функцию Meaning(р,х), вычисляющую значение много- члена в целочисленной точке х; □ процедуру Add(p,q,r) вычисления суммы многочленов q и г, результат — многочлен р. 5. Выполнить задания 1, 3 при реализации списков с помощью массивов типа А и Next так, как это сделано во втором упраж- нении экспериментального раздела занятия. 6. В задаче о «считалочке» удаляется каждый второй ребенок. Написать программу определения номеров двух последних оставшихся ребят. 7. В задаче о «считалочке» Петя находится на месте i. Сущест- вует ли значение k (й-й удаляемый ребенок) такое, что Петя останется последним ребенком в круге. Написать программу поиска значений k. 8. Вставка в линейный список перед заданным элементом вы- полняется неэффективно, ибо есть ссылка только на следую- щий элемент списка. При использовании списка с двумя ука- зателями (адресами связи) на следующий и предыдущий элементы этот недостаток устраняется. Напишите програм- му работы (вставки, удаления элементов) с таким типом спис- ка. В заключении занятия отметим ряд преимуществ и недостат- ков динамических структур. Динамическое размещение структур данных имеет свои преимущества и недостатки. В компетенции
Динамические структуры данных программиста оценивать их с точки зрения конкретной решае- мой задачи. Преимущества □ Разумное использование динамических структур данных приводит к сокращению объема памяти, необходимого для работы программы. □ Динамические данные в противоположность статическим и автоматически размещаемым данным не требуют объяв- лений их как данных фиксированного размера. В боль- шинстве систем программирования для «кучи» выделяет- ся достаточно большой объем памяти. □ Ряд алгоритмов более эффективен при реализации их с использованием динамических структур. Например, встав- ка элемента в массив на определенное место требует пере- мещения части элементов массива. При вставке в середи- ну списка достаточно нескольких операторов присваивания. Недостатки □ Алгоритмы на динамических структурах обычно более сложны, трудны для отладки по сравнению с аналогичны- ми алгоритмами на статических данных. □ Использование динамических структур требует затрат на память для ссылок. В некоторых задачах объем памяти, отводимой для ссылок, превосходит объем памяти, выде- ляемой непосредственно для данных. □ Существуют алгоритмы, реализация которых более эф- фективна на обычных данных. Например, в ряде задач индекс элемента в массиве можно просто вычислять, в то время как использование списковых структур потребует обхода списка.
350 Часть четвертая Занятие № 27. Стек План занятия □ структура данных стек, основные сведения; □ экспериментальная работа с программами проверки пра- вильности расстановки скобок, вычисления выражения в постфиксной форме, преобразования выражения из ин- фиксной в постфиксную форму; □ выполнение самостоятельной работы. Стек — упорядоченный набор элементов, в котором добав- ление новых элементов и удаление существующих производит- ся с одного конца, называемого вершиной стека. Простой пример: детская пирамидка. , Процесс сборки и разборки пирамидки подобен процессу функционирования сте- ---->----------- ка. В любой момент времени доступен лишь один элемент стека — верхний. Из определения следует, что извлекать элементы из стека можно только в порядке, об- ратном порядку их добавления в стек («первый пришел, по- следний ушел»). Основные операции со стеком: запись элемента в стек, изв- лечение элемента из стека, проверка наличия элементов в сте- ке. Если использовать список для представления данных стека, то его можно определить как список, в котором добавление но- вых элементов и извлечение имеющихся происходит с начала (или конца) списка. Значением указателя, представляющего стек, является ссылка на вершину стека. Каждый элемент сте- ка содержит поле ссылки на следующий элемент. Таким образом, описать стек (элементом данных являются целые числа) можно следующим образом: Туре pt=~elem; elem=Record data: Integer;next: pt;End; Var st: pt; Если стек пуст, то значение указателя st равно Nil. Для работы со списками будем использовать процедуры, ра- зобранные на предыдущем занятии. Напомним их. Запись в стек. Процедура записи элемента в стек должна со- держать два параметра: первый определяет указатель на нача- ло стека, второй — записываемое в стек значение. Запись в стек производится аналогично вставке нового эле- мента в начало списка:
Динамические структуры данных 351 Procedure WriteStack(Var и:pt; dig:Integer) Var x: pt; Begin New (x) ; xdata : =dig ; x'.next:=u и: =x ; End; Извлечение элемента из стека. В результате выполнения этой операции некоторой переменной i должно быть присвоено значение первого элемента стека и изменено значение указате- ля на начало списка. Procedure Readstack(Var u:pt; Var dig:Integer); Var x: pt; Begin dig:=u^.Data; x:=u; и:=иЛ.Next; Dispose (x); End; Недостатком описанной процедуры является предположение о том, что стек не пуст. Для его исправления следует разрабо- тать логическую функцию проверки пустоты обрабатываемого стека и перед использованием процедуры ReadStack проверять наличие элементов в стеке. Function Free (и:pt):Boolean; Begin If u=Nil Then Free:=False Else Free:= True; End; Экспериментальный раздел работы 1. Написать программу, проверяющую своевременность закры- тия скобок типа «(, ), {, }, [, ]» в строке символов (строка со- стоит из одних скобок этих типов). Для решения задачи определим стек, элементами которого являются символы: Type pt="el; el=Record data: Char; next: pt; End; В процессе решения анализируем символы строки а: String. Если встречена одна из открывающих скобок, то она записыва- ется в стек. При обнаружении закрывающейся скобки, соответ- ствующей скобке, находящейся в вершине стека, последняя удаляется. При несоответствии скобок выдается сообщение об ошибке, которое фиксируется в логической переменной.
352 Часть четвертая Осталось выяснить, как определить, соответствует ли очеред- ная закрывающая скобка скобке, находящейся в вершине стека. Оказывается, что коды соответствующих друг другу скобок отли- чаются не более чем на 2: { } имеют коды 123-125; [ ] — 91-93; () — 40-41, причем код открывающей скобки меньше. То есть выполнение условия If (Ord(a[i])-Ord(stack'' .data))<=2 Then... говорит о соответствии обрабатываемых скобок. Program Parenth; Туре рЬ=ле1; el=Record data:Char;next:pt;End; Var s:String; f -.Boolean ; Procedure Readstack(Var t: pt;Var x:Char);(*3Ta и следующие процедура и функция описаны ранее.★} Procedure WriteStack(Var t:pt;x:Char); Function Free (t:pt) -.Boolean; Procedure Solve(a:String;Var pp:Boolean); Var head:pt; i:Integer; ch:Char; Begin head:=Nil;pp:=True; i : =1 ; While (i<=Length (a)) And pp Do Begin If a[i] In Then WriteStack (head, a [i]) Else Begin If Free(head) Then Begin Readstack (head,ch); If ABS (Ord(ch)-Ord(a[i]))>2 Then pp:=False; End Else pp:=False; End; Inc (i) ; End; End; Begin Writein('Введите строку.'); ReadLn(s); If SO" Then Begin Solve (s,f) ;
Динамические структуры данных 353 If f Then WriteLn('Строка записана правильно.') Else WriteLn('В строке содержится ошибка.'); End Else WriteLn('Строка пустая.'); ReadLn; End. Обработка строки (((()) с помощью этой программы дает со- общение о том, что строка правильная, а строки (()))) — непра- вильная. Устраните это неравноправие. Если в строке содер- жатся другие символы, кроме скобок, то независимо от способа расстановки последних выдается сообщение об ошибке, напри- мер для строки (а). Измените программу так, чтобы символы, не совпадающие со скобками, не влияли на результат обработ- ки. 2. Написать программу вычисления значения выражения, пред- ставленного в обратной польской записи (в постфиксной фор- ме). Выражение состоит из цифр от 1 до 9 и знаков операций. Обычная запись: Обратная польская запись: (b+c) *d b с + d ★ а+ (b+c) *d a b с + d * + Просматривая строку, анализируем очередной символ, если это: □ цифра, то записываем ее в стек; □ знак, то читаем два элемента из стека, выполняем матема- тическую операцию, определяемую этим знаком, и зано- сим результат в стек. После просмотра всей строки в стеке должен оставаться один элемент, он и является решением задачи. Необходимо выяснить, как записать условие: очередной сим- вол строки — знак, и как перевести символ, обозначающий цифру, в саму цифру. Процедура Val(s,x,k) преобразует симво- льное представление цифры s в соответствующее числовое зна- чение. При этом значение k=0, если такое преобразование воз- можно, в противном случае k<>0. Program Polish; Тур pt=',el; el=Record data:Real;next:pt;End; Var s:String; f:Boolean; rez:Real; Procedure ReadStack(Var t:pt;Var x:Real);
354 Часть четвертая Procedure WriteStack(Var t:pt;x:Real) ; Function Free(t:pt):Boolean; procedure Operation(ch:Char;a,b:Real; Var c:Real); Begin Case ch Of '+':c:=a+b; ':c:=b~a; '*':c:=a*b; '/':c:=b/a; End; End; Procedure Solve (a:String;Var pp:Boolean;Var z:Real); Var head: pt; i,k: Integer; r,w: Real; Begin head:=Nil; pp:=True; i : =1; While (i<=Length (a)) And pp Do Begin If a[i]<>' ’ Then Begin(*Пропускаем пробелы.*} If Not(a[i] In Then Begin Vai (a [i] , r, k) ; If k-0 Then WriteStack(head,r) Else pp:=False; End Else Begin If Free(head) Then ReadStack(head,r) Else pp:=False; If Free(head) Then ReadStack(head,w) Else pp:=False; If pp Then Begin Operation(a[i],r,w,r); WriteStack(head,r); End; End; End; Inc(i) ; End; ReadStack(head,z); End; Begin WriteLn('Введите строку.');
Динамические структуры данных 355 ReadLn (s) ; If so' ’ Then Begin Solve (s,f,rez) ; If f Then WriteLn(rez:6:2) Else WriteLn('Выражение не может быть вычислено.'); End Else Wri teLn ('Строка пустая.'); End. Обработка 3.1 6 * даст ответ 6. Измените программу так, чтобы обрабатывались и выражения, содержащие веществен- ные числа. При вводе выражения 3 4 6 5 + получается результат 11, что явно не соответствует действительности. Выражение ошибочно. Устраните эту неточность. Пусть операция возведения в степень имеет обозначение «“». Измените программу так, чтобы осуществлялась обработка и этой операции. 3. Рассмотрим выражение А+В. Оно записано в обычной, или инфиксной, форме. Запись А В + называют постфиксной, а запись + А В — префиксной. Префиксы «пре», «пост», «ин» говорят об относительной позиции оператора по отношению к обоим операндам. Преобразование ( и вычисление) выраже- ния А+В*С в А В С * + (или А В + С *) требует знания того, какая из двух операций выполняется первой. Для операций « + », «-», «*», «/» и «"» приоритет считаем традиционным (от высшего к низшему): возведение в степень, умножение/деле- ние, сложение/вычитание. Нашей задачей является написа- ние программы преобразования выражения из инфиксной в постфиксную форму. В предыдущем задании мы брали по- стфиксную форму (польская запись) как нечто заданное и то- лько вычисляли выражение. Но прежде чем перейти к раз- работке, приведем несколько примеров записи одного и того же выражения в различных формах. Инфиксное представление Постфиксное представление Префиксное представление А+В-С АВ+С- -+АВС (A+B)*(C-D) AB+CD-' ★+AB-CD A'B'C-D+E/F/(G+H) АВ'С'0-EF/GHO +-*ABCD/EF+GH A-B/(C’D~E) АВСОЕ'У- -A/B*CTDE
356 Часть четвертая Процесс преобразования выражения в постфиксную форму отражен в таблице для выражения (A+B)*(C+D). В первом столбце приводится читаемый символ из исходной строки, во втором столбце — результирующая строка и в третьем столб- це — состояние стека. ( ( I A A r + A (+ в AB (+ ) AB+ AB+ ( AB+ *( c AB+C AB+C D ab+cd ) AB+CD+ AB+CD+* Итак, идентификаторы переменных сразу переписываем в результирующую строку, знаки операций и открывающую круг- лую скобку записываем в стек. Если встречается закрывающая скобка, то из стека символы (до открывающей скобки) опера- ций переписываются в результат. После того, как вся исходная строка просмотрена, остается дописать (если они есть) из стека знаки операции в строку, содержащую выражение в постфикс- ной форме. Program Change_expression; Type pt = /'el; el=Record data:Char;next:pt;End; Var s,rez:String; Procedure ReadStack(Var t:pt;Var х:СЬаг);(*Эти процедуры и функция рассмотрены ранее. Изменения не существенны. *} Procedure WriteStack(Var t:pt;x:Char); Function Free(t:pt):Boolean; Procedure Solve(a:String;Var z:String); Var head:pt; i:Integer; w.Char; Begin head:=Nil;z:=' ' ;
Динамические структуры данных 357 While i<=Length(a) Do Begin Ifa[i]o' ' Then Begin{*Пропускаем пробелы *) IfNot(a[i] In [' + ’,>-> ro, /, ,Л, , , ’ Then z:=z+a[i] ' ' , J 7 Else If a[i] in /, f, r, (, ^ Then WriteStack(head, a[i ]) Else If a[i] = ')' Then Begin{*Считываем из стека до символа «(». *} ReadStack (head, w) ; While w<>' (' Do Begin z:=z + w; ReadStack (head, w) ; End; End; End; Inc (i) ; End; While Free(head) Do Begin{^Дополняем строку символами операций, запомненных в стеке. *) ReadStack(head,w); z:=z+w; End; End; Begin WriteLn ('Введите выражение в обычной (инфиксной) форме ') ; ReadLn (s) ; Solve (s,rez); Wri teLn (rez) ; End. В программе не предусмотрена обработка ошибок в исходной строке. Устраните этот недостаток, чем больше возможных ошибок будет анализироваться, тем лучше. Определите, при каких исходных данных приведенная программа выдает непра- вильный результат. Сохранение «костяка» программы (мини- мальные изменения) говорит о качестве его написания. Измените программу так, чтобы обрабатывались не только однобуквенные имена переменных, но и вещественные числа. Значения переменных могут быть заданы предварительно в от- дельном массиве.
358 Часть четвертая Задания для самостоятельной работы 1. Для хранения элементов стека используется обычный одно- мерный массив из N элементов. Реализуйте операции рабо- ты со стеком (ReadStack, WnteStack, Free) в этом случае. За- метим, что перед использованием WrtteStack необходимо проверять переполнение стека. 2. Дана строка вида: s*w (s, w — строки из символов, не содер- жащих символа *). Чтение разрешено по одному символу. Проверить, является ли строка w обратной строке s. Напри- мер, для случая s=ABCDEF, w=FEDCBA ответ положитель- ный. 3. Преобразуйте каждое из приведенных ниже выражений в пре- фиксную и постфиксную формы: □ А+В—С □ (A+B)*(C-D)~E*F □ (А+В )*(C~(D—E )+F )-G □ А+(((В-С )*(D~E )+F )/G)~(H—J) □ ( (А-( B+C ) )*D )~(E+F ) 4. Разработать программу преобразования выражения из инфик- сного в префиксное представление. 5. Разработайте программу вычисления выражения в префик- сной форме. 6. Разработать программу преобразования постфиксной формы в инфиксную. При этом результирующее выражение должно быть записано с необходимыми скобками. Например, АВ+С* должно быть преобразовано в (А+В)*С. 7. Абстрактная вычислительная машина имеет один регистр и шесть инструкций: LD А Помещает операнд А в регистр. ST А Помещает содержимое регистра в переменную с именем А. AD А Прибавляет значение Д к регистру Результат остается в регистре. SB А Вычитает значение А из регистра. Результат остается в регистре. ML А Умножает содержимое регистра на значение переменной с именем Д Результат остается в регистре. DV А Делит содержимое регистра на значение Д, Результат остается в регистре Дано выражение в постфиксной форме, состоящее из одно- буквенных операндов и операций « + », «-», «*», «/». Написать программу вывода инструкций вычислительной машины, необ-
Динамические структуры данных ходимых для вычисления выражения. Результат вычисления должен оставаться в регистре, разрешается использовать обо- значения Тп для временных переменных. Пример. Для выражения ABC*+DE-/ перечень инструкций имеет вид:
360 Часть четвертая Занятие № 28. Очередь План занятия □ экспериментальная работа с программами реализации оче- реди и дека через массив и с помощью двунаправленного списка; □ выполнение самостоятельной работы. Очередь — это упорядоченный набор элементов, в котором извлечение элементов происходит с одного его конца, а добав- ление новых элементов — с другого. Операции с очередью: □ запись элемента в очередь, если это возможно, т. е. нет пе- реполнения структуры данных, выбранной для хранения элементов очереди; □ чтение элемента из очереди, естественно, если очередь не пуста. В очереди, в силу её определения, доступны две позиции: ее начало (не с точки зрения обслуживания, а с точки зрения за- писи), куда заносятся новые элементы, и ее конец, откуда изв- лекаются элементы. Поэтому для работы с очередью (независи- мо от выбранной структуры данных для хранения её элементов) необходимо описать две переменные, назовем их head и tall. Экспериментальный раздел работы 1. Рассмотрим использование одномерного массива как струк- туру данных для хранения элементов очереди. На рисунке показан «срез» очереди на какой-то момент вре- мени. В очереди остался один не обслуженный элемент — 7. После следующего чтения очередь окажется пустой. Оговорим значения head и tail. В начальный момент работы они равны
Динамические структуры дандых 361 нулю. После записи элемента значение head указывает на запи- санный элемент, т. е. перед записью его необходимо увеличи- вать на 1. Значением tall является адрес прочитанного элемента, т. е. перед следующим чтением его также необходимо увеличить на 1. Запись в очередь и чтение из нее осуществляется по прин- ципу «кольца». После записи в ячейку с номером N выполняет- ся запись в первую ячейку, если, конечно, она свободна. После чтения из ячейки с номером N осуществляется чтение из пер- вой ячейки, если в нее выполнялась запись. Таким образом, в логике работы с очередью необходимо предусмотреть переход через границу — значение 2V. Второй момент, требующий разъяснения, это понятия «оче- редь пуста», «очередь переполнена». Очень сложный момент, ибо как значение tail может «догонять» значение head — в этом случае «очередь пуста», так и значение head «догоняет» tail — очередь переполнена. Как различить эти случаи? Отло- жим анализ этого момента логики и сделаем «костяк» програм- мы. {$R') Program Turn_Array; Const N=10; Type MyArray=Array[1..N] Of Integer; Va rA: МуАггау ; head,tail,w:Integer; Function Over_Turn (up,down: Integer) :Boolean; (^Первая версия функции, вряд ли работающая правильно. То же самое относится и к следующей функции.★) Begin 14—452
362 Часть четвертая If up =down Then Over_Turn:=True Else Over_ Turn:=False; End; Function Empty_Turn(up,down:Integer):Boolean; Begin If up =down Then Empty_Turn:=True Else Empty_ Turn:=False; End; Procedure Add_Turn (Var up:Integer;x:Integer); {^Добавляем элемент в очередь. *} Begin Inc (up); If up=N-tl Then up:=l; A[up]:=x; End; Procedure Extrac_Turn(Var down:Integer;Var x: Integer);{'Извлекаем элемент из очереди. *} Begin Inc (down); If down=N+l Then down:=1; x:=A[down];A[down]:=0; End; Procedure Print_Turn;{'Рабочий (отладочный) вариант процедуры. Требуется изменить ее так, чтобы просмотр очереди осуществлялся по значениям переменных up, down. Пока значение down меньше или равно up — выводить элемент и изменять значение down. *} Var i:Integer; Begin For i: =7 To N Do Wri te(A[i] ,' ') ; WriteLn; WriteLn(head, ' ' ,tail); End; Begin {'Основная программа. *) Randomize; FillChar(A,SizeOf (A),0); head:=0;tail:=0; ('Начальные значения указателей записи и чтения. *) WriteLn('Занесение в очередь первого элемента.'); Add_Turn (head, 100) ; While Not(Empty_Turn(head, tai 1)) And Not
Динамические структуры данных 353 (Over_Turn(head,tail) ) Do Begin[*Пока очередь не пуста (а вдруг окажется операция чтения из очереди) и нет переполнения очереди, выполняем действия.*) If Random(2)>0 Then Extrac_Turn(tail,w) (★Извлекаем из очереди.★) Else Add_Turn(head,Random(200)+1) {^Записываем в очередь. *); Print_Turn;{*Наблюдаем за изменением состояния очереди. *} End; If Not(Empcy_Turn (head, tail)) Then WriteLn (' Очередь переполнена . ') {’★Если очередь не пуста, то, значит, она переполнена.*) Else WriteLn('Очередь пуста.'); WriteLn(head,' ',tail); End. Запуск программы позволяет провести первичный анализ ее работы. Любые варианты проверки приводят к сообщению «оче- редь пуста». Даже в том случае, когда «очередь переполнена». Попытки изменить начальные значения tail или head, а также способов записи и считывания не изменяют ситуацию. После ряда безуспешных попыток достигается понимание того, что необходима дополнительная переменная логического типа для анализа ситуации: или tail «догоняет» head, или head «догоня- ет» tail. Договоримся о том, что в первом случае р (логическая переменная) равна True, а во втором — False. В начальный мо- мент её значение равно True и при переходе как того, так и другого указателей (heap и tail) через значение N она изменяет свое значение на противоположное. Модифицированный вариант анализа и изменения состоя- ния очереди имеет вид: Function Over_Turn (up,down .-Integer) -.Boolean; Begin If (up =down) And Not p Then Over_Turn:=True Else Over_Turn:=False; End; Function Empty_Turn(up,down:Integer):Boolean; Begin If (up =down) And p Then Empty_Turn:=True Else Empty_Turn:-False; End;
364 Procedure Add_Turn (Var up:Integer;x:Integer) ; Beg±n Inc (up) ; If up=N-tl Then Begin up:=l; p:=Not p;End; A[up]:=x; End; Procedure Extrac_Turn(Var down:Integer;Var x: Integer); Begin Inc (down); If down=N+l Then Begin down:=1;p:=Not p;End; x:=A[down];A[down]:=0; End; Естественно, что p необходимо ввести в описание перемен- ных и присвоить начальное значение р:=Тгие, например, в той строке, где head:=0 и tail:=0. На этих изменениях наши беды не кончаются. Если в оче- редь осуществляется только запись и не было ни одного чте- ния, то значение указателя tail остается равным 0, а значение указателя head «идет по второму кругу» и запись выполняется на занятые места. В функцию Over_Turn необходимо внести еще одно изменение: If ((up =down) And Not p) Or ((down=0) And (up=N) ) Then Over_Turn:=True Else Over_Turn:=False ; Впрочем, все проблемы с анализом указателей head и tail ре- шаются и «детским» образом. Перед каждым чтением из очере- ди выполняется проверка того, а не равны ли все элементы оче- реди нулю. Если «да», то очередь пуста, а если «нет», то выполняется чтение. Точно так же решается и вопрос о занято- сти очереди. Однако, во-первых, нарушается принцип работы с очередью (а это главное, очередь — «черный ящик», взаимо- действовать с которой допускается только через значения head и tail)-, во-вторых, имеет место лишний просмотр элементов массива; в-третьих, предполагается, что из нулевых элементов очередь состоять не может. Деком называется структура данных, в которой запись и удаление элементов разрешается с обоих концов. Реализация дека с использованием массива требует дополнительного введе- ния двух процедур: чтения «с головы» и записи «с хвоста».
Динамические структуры данных 365 ISR+) Program Dec_Array; Const N=10; Type MyArray=Array[1..N] Of Integer; Var A:MyArray; head,tail, w;Integer; p-.Boolean; Procedure Print_Dec;{*Эта и следующие процедуры и функции рассмотрены ранее (изменения только в названиях). *) Procedure Add_Head(Var up:Integer;x:Integer) ; Procedure Extrac_Tail(Var down:Integer;Var x: Integer) ; Function Over_Dec (up, down : Integer) -.Boolean; Function Empty_Dec (up,down:Integer):Boolean; {★Новые процедуры. *) Procedure Add_Tail(Var down:Integer;x:Integer); Begin A[down]:=x; Dec(down); If down=0 Then Begin down:=N; p:=Not p;End; End; Procedure Extrac_Head(Var up:Integer;Var x: Integer); Begin x:=A[up];A[up]:=0; Dec(up); If up=O Then Begin up:=N;p:=Not p;End; End; Begin{*Может быть, потребуется несколько запусков программы, чтобы отследить все варианты изменения содержимого дека.★) Randomize; FillChar (A, SizeOf (А) , 0) ; head:=O;tail:=O;p:=True; WriteLn('Запись первого элемента в дек.'); Add_Head (head, 100) ; While Not (Empty_Dec (head, tail)) And Not (Over_Dec (head, tail)) Do Begin If Random(2)>0 Then(*3апись и чтение с «головы» дека.*) If Random (3) >0 Then Add_Head (head,Random (50))
366 Часть четвертая Else Extrac_Head (head,w) Else{*3апись и чтение с «хвоста» дека. *} If Random (2)>0 Then Begin If tailOO Then Add_Tail(tail,Random (100) +50) End Else Extrac_Tail(tail,w); Print-Dec; ReadLn; End; If Wot (Empty_Dec (head, tail) ) Then WriteLn(' Дек переполнен.') Else WriteLn ('Дек пуст.'); WriteLn(head,' ',tail); End. 2. При выборе двунаправленного списка для реализации очере- ди снимается проблема анализа переполнения очереди, а при- знаком «пустой очереди» является значение указателя на на- чало списка (head), равное Nil. Для данной задачи можно использовать и однонаправленный список, но в этом случае при каждом чтении из очереди необходимо находить послед- ний элемент (требуется корректировка адреса связи предпос- леднего элемента). Структура очереди приведена на рисунке. Описание данных и процедуры работы с элементами очереди приведены ниже по тексту. Type pt^elem; elem=Record data:Integer; next,pred:pt; End; Var head,tail:pt; Procedure Wnte_Turn (Var up, down: pt; x: Integer) ; {★Запись в очередь — запись в начало списка, х - записываемое число.*} Var t:pt; Begin New(t) ; t''.data:=x; tЛ. next :~up; t*,pred:=Nil;
Динамические структуры данных 367 If up=Nil Then down:=t Else up''.pred:=t; up:=t; End; Procedure Read_Turn (Var up,down:pt;Var x: Integer); Чтение не из пустой очереди, чтение последнего элемента списка.*} Var t:pt; Begin х: =down''. data ; t:=down; If down*.pred=Nil Then Begin down:=Nil;up:=Nil; End Else Begin down : =down''. pred; down''. next: =Nil ; End; Dispose (t); End; Экспериментальная работа возможна с использованием сле- дующей основной программы (необходимо добавить процедуру Print и описать переменную w). Требуется несколько запусков, чтобы она вышла из режима однократной запись в очередь. Из- менение в операторе If Random(2) на Random(3), естественно, увеличит длину очереди, и, может быть, придется использовать Ctrl+Break для прерывания работы программы. Добавление оператора ReadLn после Print(head) позволит более детально отслеживать состояние очереди. Begin Randomize; head:=Nil;tail:=Nil; Write_Turn(head,tail,100);{*3апись в очередь первого элемента. *) While headoNil Do Begin{*Пока очередь не пуста. *} If Random(2)>0 Then Write_Turn(head,tail, Random(50)) {*3аписываем в очередь случайное число.★} Else Read_Turn(head,tail,w);{^Считываем иэ очереди первый иэ записанных элементов. *) Print(head);{*Стандартная процедура вывода элементов списка. *) End; End.
368 Часть четвертая Реализуйте структуру данных дек с помощью двунаправлен- ного списка. 3. Диапазон представления целых чисел (Integer, Word, Longlnt) ограничен. Об этом не раз уже шла речь в этом учебнике. Так, например, факториал числа 13! выходит за диапазон типа Longlnt. Использование типа Real частично решает пробле- му, но не гарантируется точность вычисления. Однако решать задачи с многозначными числами приходится. Наиболее ча- сто для представления таких чисел используется одномерный массив. Каждая цифра числа (или группа цифр) хранится в отдельном элементе массива. На этом занятии мы рассмотрим представление многозначного числа в виде двусвязного спи- ска. Каждая цифра числа — элемент списка. Так, число 25974 имеет следующее представление. Число хранится «задом наперед». Такое представление удоб- но при выполнении арифметических операций, а для вывода у нас есть адрес предшествующего элемента. Приведем «костяк» программы, а затем рассмотрим несколько арифметических операций. Program Long_Arithmetic; Const Basis=10;{*Основание системы счисления. *) Type pt=^elem; elem=Record data:Integer; next,pred:pt; End; Var head, tail :pt; Procedure WriteLong(t:pt);{*Вывод элементов списка, начиная с его конца. *} Begin While t<>Nil Do Begin Write(t/'.data); t:=t^.pred; End; WriteLn; End; Procedure Insert_Begin(Var up,down:pt;x:Integer); {★Процедура вставки элемента в начало списка рассмотрена ранее.*}
369 Динамические структуры данных Procedure ReadLong (Var up, down :pt) ; Var ch:Char; Begin Read (ch) ; While Not (chin ['0'..'9']) Do Read(ch); (★Пропуск символов. Пока не встретим символа цифры.★) While ch In ['0'..'9'] Do Begin Insert_Begin (up, down,Ord (ch) -OrdC 0')) ; Read(ch); End; End; Begin {★Основная программа .*) Assign(Input,'Input.Txt');Reset(Input); Assign (Output, 'Output. Txt') .-Rewrite (Output) ; head:=Nll; tail:=Nil; ReadLong (head, tail) ; WriteLong (tail); Close (Input) ; Close (Output) ; End. Примечание Напомним, что одновременно на экране должны быть обозримы три файла: файл с текстом программы; Input.Txt и Output.Txt. Если для отладки потребуется окно Watches, то и для него должно быть место на экране. Рассмотрим операцию сложения двух чисел. Она выполняет- ся как обычно, берутся две очередные цифры, находится их сум- ма с учетом значения разряда переноса из предыдущего разряда по основанию системы счисления и новое значение разряда пе- реноса. Результат запоминается. Единственная особенность — записывать результат необходимо в конец нашего списка, иначе потребуется новый вариант процедуры вывода результата. Procedure AddLong(x,y:pt;Var up,down:pt);(*х, у - указатели на начало списков слагаемых; up, down — указатели на начало и конец списка для представления результата. *) Var w,v,a,b:Integer; Begin v:=0;(*Разряд переноса.★)
370 Часть четвертая While (v<>0) Or (xoNil) Or (yoNil) Do Begin (★Берем очередную цифру первого слагаемого.*) If xOHil Then Begin a : =хл . data ;x: =x* . next; End Else a:=0; {★Берем очередную цифру второго слагаемого. *} If yoNil Then Begin b:=y*.data;y:=y*. next; End Else b:=0; w:=(v+a+b) Mod Basis;(*Находим сумму цифр с учетом разряда переноса.★} Insert_End(up,down,w);{*3аписываем.★} v:=(v+a+b) Div Basis;{*Вычисляем новое значение разряда переноса.*) End; End; Для полноты описания приведем процедуру вставки элемен- та в конец двусвяаного списка. Procedure Insert_End(Var up,down:pt;х:Integer); Var t:pt; Begin New(t); t*.Data:=x;t* .pred: =down; tЛ . next: =Nil; If down=Nil Then up:=t Else down*.next:=t; down:=t; End; Процедура умножения длинного числа на короткое число (меньше, как минимум, в 9 раз максимального числа типа Inte- ger) очень походит на процедуру AddLong. Procedure Multip (x:pt ;у: Integer ;Var up,down: pt) ; (*x — указатель на первый элемент списка, соответствующего множителю; у - короткое число; up, down — указатели на начало и конец списка для представления результата.★) Var w,v,а:Integer; Begin v:=0;{*Разряд переноса. *} While (v<>0) Or (xONil) Do Begin If xoNil Then Begin a :=x* . data;x: =x* . next ;End Else a:-0; w:-(v+a*y) Mod Basis;{^Вычисляем цифру результата.★}
хДдпп»»пг1еекместруктуры данных 371 . Insert_End (up^down, w) ;{*3аписываем цифру. *} < v: = {v+a*y) Div Basis; l *Вычисляем новое | значение разряда переноса.*} | End; j. . End; j Измените основную программу для проверки процедур сло- ркения длинных чисел и умножения длинного числа на корот- кое число. Как изменится программа, если в качестве основания систе- мы счисления (константа Basis) взять не 10, а, например, 1000? Задания для самостоятельной работы 1. За один просмотр файла f, элементами которого являются це- лые числа, и без использования дополнительных файлов пе- реписать его элементы в другой файл так, чтобы первонача- льно были записаны все числа, меньшие заданного а, затем все числа из отрезка [а, Ь] и все остальные. Взаимный поря- док чисел в каждой из групп должен быть сохранен. Идея решения. В решении задачи числа последовательно считываются из файла. Если очередное число меньше а, то оно записывается в файл, если оно принадлежит отрезку [а, Ь], то заносится в первую очередь, иначе — во вторую очередь. После завершения чтения в выходной файл записываются числа из первой и второй очередей. 2. Содержимое текстового файла f, разделенного на строки, пе- » реписать в текстовый файл g, перенося при этом в конец каж- дой строки все входящие в него цифры, с сохранением вза- имного исходного порядка. , 3. ^Вывести в порядке возрастания первые N натуральных чи- сел, в разложение которых на простые множители входят то- мс ^о.числаа, Зи5. воеДд&В&м_в_ч_ан_ие ДО» Рес4 £,£е2> в «отОВих. хранятся числа,
372 Часть четвертая Вывод элемента Deci Dec2 Dec3 || 2 3 5 2 4 3,6 5, 10 3 4,6 6, 9 5,10,15 4 6, 8 6, 9, 12 5,10,15,20 || 5 6, 8, 10 6, 9, 12, 15 10,15,20,25 6 8, 10, 12 9, 12, 15, 18 10,15,20,25,30 1 8 10, 12, 16 9, 12, 15, 18, 24 10,15,20,25,40 I Решите задачу двумя способами — реализацией деков через массивы и через двунаправленные списки. 4. Разработать функцию сравнения двух длинных чисел. 5. Разработать процедуру умножения длинных чисел. 6. Разработать программу вычисления 100!
Динамические структуры данных 373 Занятие № 29. Поиск в графе План занятия □ понятие графа и его представление в памяти компьютера; □ экспериментальная работа с программами поиска в глуби- ну и в ширину, топологической сортировки; □ выполнение самостоятельной работы. Термин «граф» впервые появился в работах венгерского ма- тематика Д. Кенига в 1936 году, хотя ряд задач по теории гра- фов решался ещё Л. Эйлером в XVIII веке. Пусть V — непустое конечное множество, V<2> — множество всех его двухэлементных подмножеств. Пара G=(V,E), где Е — произвольное подмножество множества V<2)t называется графом (неориентированным графом). Элементы множества V называются вершинами графа, а элементы множества Е — реб- рами. Мощность (количество элементов) множеств V и Е будем обозначать буквами N и М. Говорят, что две вершины и и и гра- фа смежные, если (и.и) является ребром, т.е. принадлежит мно- жеству Е. Граф G называется помеченным, если его вершинам присвоены некоторые метки, например числа 1, 2, ..., N. Отож- дествление каждой из вершин графа с ее номером приводит к увеличению количества графов. На рисунке показаны три раз- личных помеченных графа. 2 3 Представление графа в памяти компьютера. Рассмотрим два способа из всех существующих; матрицу смежности и перечень списков смежных вершин. Матрица смежности (А) имеет раз- мерность N*N. Элемент A[t,j]=l, если вершины с номерами i, j соединены ребром, и A[t,j]=O, если — t и у не связаны ребром. Представление графа G в виде перечня списков смежных вер- шин требует введения N списков, по количеству вершин. Каж- дый из списков содержит номера всех смежных вершин. Указа- тели на первые элементы списков объединены в массив. Пример. Для помеченного (метки в центре кружков) графа на рисунке матрица смежности и перечень списков смежных вершин приведены ниже по тексту.
Часть четвертая Экспериментальный раздел работы 1. Поиск в глубину. Идея поиска в глубину формулируется сле- дующим образом: начиная с некоторой вершины и (напри- мер, первой) идем «вглубь», пока это возможно. Рассматри- вается вершина и, смежная с v. Она выбирается. Процесс повторяется с вершиной и. Если на очередном шаге оказа- лось, что нет непросмотренных вершин, связанных с теку- щей, то выполняется возврат к предыдущей вершине и ищет- ся новая вершина, связанная с ней. Если такой вершины не
Динамические структуры данных 375 найдено, то выполняется еще один шаг возврата к предыду- щей вершине и так до тех пор, пока вычислительный процесс не вернется к вершине, с которой начат просмотр. Для гра- фа, приведенного выше по тексту, очередность просмотра вер- шин при поиске в глубину указана в квадратных скобках рядом с вершинами. Просмотр начат с первой вершины и приведена только часть ребер графа, тех, по которым выпол- нялся следующий шаг поиска. При реализации поиска используется второй способ пред- ставления графа (перечень списков смежных вершин). Описа- ние элементов списка очевидно. Для хранения значений указа- телей на первые элементы списков (для каждой вершины графа) используется массив А. Номера вершин в той очередности, в которой они просматриваются в процессе поиска в глубину (то, что в квадратных скобках на рисунке), храним в массиве Rez. Для фиксации признака, просмотрена вершина графа или нет, требуется массив Nnew с элементами логического типа. Program Graph_Depths ; Const MaxN=10; Type pt="elem; elem=Record data:Integer;next:pt;End; MyArray=Array[1..MaxN] Of pt; MyNew=Array[1..MaxN] Of Boolean; MyRez=Array[1 . .MaxN] Of Integer; Var A:MyArray;/*Массив указателей на первые элементы списков. *) Nnew:MyNew; ( *Массив признаков просмотренных вершин графа. *} Rez:MyRez;{^Очередность просмотра вершин графа.*] N, ent: Integer ;{ *Количество вершин в графе. Счетчик числа записей в массив Rez. *] Procedure Insert_List_End (Var t ;pt;x: Integer) ;
376 Часть четвертая {'Вставка элемента в конец списка. Процедура рассмотрена на предыдущих занятиях.★} Procedure Init;{'Ввод и инициализация данных. Структура 234 входного файла: в первой строке * количество вершин графа (N); затем N 312 строк, по одной на каждую из вершин 215 графа. Первое число в строке - 347 количество ребер, выходящих из 237 вершины, а затем номера вершин, с 4 5 6 которыми связана текущая вершина. 257 Справа по тексту приведен пример у входного файла для графа, рассмотренного ранее. '} Var i,j,w,q:Integer; Begin Cnt:=0; Assign(Input,'Input.txt' }; Reset(Input); ReadLn(N);{'Количество вершин графа. *) For i:=l To N Do Begin A[i]:=Nil;Nnew[i]:=True;Rez[1J:=0; End; For i:=l To N Do Begin Read(q);{'Степень вершины графа — число ребер, связанных с данной вершиной.'} For j:=l То q Do Begin Read (w) ; Insert_List_End (A[i] ,w) ; End; End; Close(Input); End; Procedure Inc_Rez(v:Integer); Begin Inc (cnt); Rez[cnt]:=v; End; Procedure Search_ Depths (v:Integer); {'Рекурсивный вариант процедуры поиска в глубину. *} Var t:pt; Begin Nnew[v]:=False; ('Помечаем вершину как просмотренную. *}
Динамические структуры данных 377 t.-^л [v] ; I 'Начинаем просмотр вершин гоафа, смежных с вершиной v.*) While tONil Do Begin If Nnew[t".data] Then Begin(*Если вершина не просмотрена, то записываем ее номер в результирующий массив и идем «вглубь», т.е. К этой вершине. */ Inc_Rez (t''. data) ; Search_ Depths (tл.data); End; t:=tл.next;{'Переход к следующему элементу списка. *) End; End; Procedure Print;('Вывод результата.*) Var 1:Integer; Begin Assign(Output,'Output.txt'); ReWrite(Output); For 1: =1 To N Do Wri te (Rez [1] , ' ') ;Wri teLn ; Close(Output); End; Begin{'Один из вариантов основной программы. *) Init; Inc_Rez (1) ; Search_ Depths (1); Print; End. Выше рассмотрена рекурсивная реализация поиска в глуби- ну. Уход от рекурсии требует введения стека для запоминания просмотренных вершин. Последняя запомненная вершина, по логике обработки, должна первой выбираться на последующую обработку. Реализуйте не рекурсивный вариант процедуры по- иска в глубину. 2. Поиск в ширину. Поиск начинается с фиксированной верши- ны v0. Затем проверяются все вершины, смежные с v0 и не про- смотренные ранее. Их номера заносятся в очередь. Далее про- цесс продолжается с первой из вершин, записанной в очередь. И так до тех пор, пока очередь не окажется пустой. На ри- сунке приведена часть ребер исходного графа, тех, по кото- рым осуществлялся «переход» в процесс просмотра вершин графа при поиске в ширину. В квадратных скобках указана очередность просмотра вершин.
378 Часть четвертая Program Graph_Breadth; Const Type Var {★Описание данных совпадает с тем, что приведено в предыдущей программе. ★} Procedure Insert_List_End(Var t:pt;x:Integer) ; (★Процедуры совпадают с приведенными в предыдущей программе. *} Procedure Init; Procedure Inc_Rez (v:Integer); Procedure Print; {★Новая процедура. Поиск в ширину. *} Procedure Search_Breadth(v:Integer);{*v — номер вершины, с которой начинается поиск в ширину. *} Var t:pt; w: Integer; Type ptr=',el_turn ; ( ★Для реализации очереди используется двусвязный список.★} el_turn=Record data:Integer; next,pred:ptr; End; Var head,tail:ptr; Procedure Write_Turn(Var up,down:ptr;x:Integer); {★Действия с элементами очереди рассмотрены на предыдущих занятиях.★) Procedure Read_Turn (Var up,down:ptr; Var x: Integer); Function Empty_Turn (t,q:ptr):Boolean; Begin{★ «Тело» процедуры Search_Breadth. *) head:=Nil;tail:=Nil; Nnew[v]:=False;Write_Turn(head,tail,v);
Динамические структуры данных 379 {‘Записываем в очередь номер вершины графа, с которой начинается просмотр. ★) While Not (Empty_Turn (head, tail)) Do Begin{★Пока очередь не пуста, выполняем действия.★} Read_Turn (head, tail, w);(‘Читаем элемент из очереди. *} t:=A[w]; While tONil Do Begin(‘Пока не просмотрены все элементы списка. *} If Nnew[t‘.data] Then Begin {‘Вершина «не посещалась» ранее. *) Inc_Rez (t".data); Nnew[t". data]:=False; Write_Turn (head, tail, f'.data); End; t:=t л.next; End; End; End; (★Один иэ вариантов основной программы. *) Begin Init; Inc_Rez(l); Search_Breadth (1); Print; End. Изменить программу так, чтобы реализация очереди выпол- нялась через массив, содержащий не более N/2 ячеек. Приве- дите примеры графов, для которых данный вариант программы окажется неработоспособным. 3. Топологическая сортировка. Ориентированный граф (или орграф) — это пара G=(V,E), где V- множество вершин, Е — множество ориентированных ребер, которые называются ду- гами. На рисунках дуги отмечаются стрелками. Для оргра- фов методы обхода вершин графа, рассмотренные выше, также имеют силу. Предположим, что в орграфе нет циклов (интуитивно этот термин понятен) и он связен. Оказывается, что при этом предположении вершины, а им присвоены зна- чения меток и они пронумерованы, можно перенумеровать так, что каждая дуга будет иметь вид (и^), где i<j. В этом суть задачи топологической сортировки. Эта задача, естест- венно, отличается от рассмотренных ранее задач сортировки. В них мы переупорядочивали элементы массива (в основном), в этой задаче требуется переопределить метки вершин графа. Суть алгоритма. При вводе описания графа, а мы по-преж- нему работаем с его описанием в виде списков смежности, фор- мируем для каждой вершины (i) характеристику Deg[i] — чис-
380 Часть четвертая ло дуг, входящих в вершину с данным номером i. Просматриваем вершины и номера вершин i, имеющих нулевое значение Deg[ i], запоминаем в стеке. После этого, пока стек не пуст, выбираем первый элемент из стека, присваиваем новый текущий номер выбранной вершине и уменьшаем степени всех вершин графа, связанных с данной, на единицу. Если оказалось, что при этом степень вершины стала нулевая, то ее номер заносим в стек. Используется логика просмотра вершин графа — метод поиска в глубину (а можно ли использовать поиск в ширину?). Приведем описание данных нашего решения. Program Topol_Sort; Const MaxN= Type pt=^elem;l*Тип используется как длч формирования описания графа в виде списков связи, так и для организации стека. *} elem=Record data:Integer;next:pt;End; MyArray=Array [1. .MaxN] Of pt;(*Массив указателей для хранения адресов на первые элементы списков, связанных с каждой вершиной графа. *} MyRez=Array[1..MaxN] Of Integer; Var A:MyArray; Deg,NewNum:MyRez;{^Степени вершин и новые номера вершин. *) N:Integer;{^Количество вершин графа. *} Рассмотрим пример, он приведен на рисунке, и трассировку логики для него (она дана в таблице) по процедуре реализации топологической сортировки. Procedure ChangeNum; Var head:pt;(^Указатель на начало стека. *] nm,v,1:Integer; t:pt; Begin head:=Nil;
Динамические структуры данных 381 пт: —0;{*Текущий новый номер вершины. *} For i:=l То N Do If Deg[i]=O Then WriteStack(head,i); {★Первоначальное занесение в стек.*} While Free (head) Do Begin{*Пока стек не пуст. Процедуры WriteStack, Readstack и функция Free рассмотрены ранее на занятии по стеку. *} Readstack (head,v) ;Inc (rtm) ;NewNum [v] {★Присваиваем новый номер вершине, выбранной из стека.★} t:=A[v]; While tONil Do Begin{*Просматриваем вершины, \ связанные с данной. *} Dec (Degft".dataJ);(‘Уменьшаем степень этой вершины на единицу. ★) If Deg [ f*. data ] =0 Then WriteStack (head, t*~. data) ; { *Если оказалось, что степень вершины после этого равна нулю, то заносим ее в стек.★} t: =t . next; { *Переходим к следующей вершине, связанной дугой с данной.*} End; End; End; Номер итерации Deg NewNum Состояние стека пт | Начальные значения 2, 2, 2, 1,0, 1 0, 0, 0,0, 0,0 5 0 1 2, 2, 1,0,0, 1 0,0, 0,0, 1,0 4 1 2 1,2, 0, 0, 0, 1 0,0, 0,2, 1,0 3 2 3 0,2,0, 0,0, 0 0, 0,3, 2, 1,0 6,1 3 4 0, 1,0, 0, 0,0 0, 0,3, 2, 1,4 1 4 5 0, 0,0,0, 0,0 5,0,3, 2, 1,4 2 5 | Г 6 0, 0,0,0, 0,0 5,6,3, 2, 1,4 - 6 I В процедуре In.it, рассмотренной ранее, следует добавить формирование элементов массива Deg. Разработайте полный текст решения, проверьте его на различных примерах. Другой способ топологической сортировки состоит в том, чтобы после- довательно находить вершины со степенью 0, запоминать их (или выводить) и удалять из графа вместе со всеми входящими в них дугами. Реализуйте эту модификацию алгоритма.
382 Часть четвертая Задания для самостоятельной работы 1. Чередующаяся последовательность е;, и2, е2, ..., е(, и(+; вер- шин и ребер графа, такая, чтоe=vtvl+1 (1=1,...,1), называется маршрутом, соединяющим вершины и и1+1- Очевидно, что маршрут можно задать последовательностью о;, и2, ..., о(+; его вершин. Маршрут называется цепью, если все его ребра раз- личны, и простой цепью, если все его вершины, кроме, воз- можно, крайних, различны. Граф называется связным, если любые две его несовпадающие вершины соединены маршру- том. Рассмотренные выше методы обхода вершин графа ра- ботают только для связных графов. Если граф несвязен, то просмотр вершин выполняется только в пределах одной связ- ной компоненты. Напишите программу определения количе- ства связных компонент графа. 2. Разработайте программы поиска в глубину и поиска в шири- ну (рекурсивный и не рекурсивный варианты) при описания графа с помощью матрицы смежности. 3. Маршрут называется циклическим, если его начальная и ко- нечная вершины совпадают. Циклическая цепь называется циклом, а циклическая простая цепь — простым циклом. Определить, есть ли в связном графе простые циклы нечет- ной длины. 4. Граф G=(V,E) называется двудольным (У=ХиУ), если сущест- вует такое разбиение множества его вершин на две части (X и У), что концы каждого ребра принадлежат разным частям. Д. Кёниг сформулировал простой критерий двудольности гра- фа. Для двудольности графа необходимо и достаточно, чтобы он не содержал циклов нечетной длины. При поиске в ширину приписываем вершине, с которой начинаем поиск, номер 0. Всем вершинам, в которые мы попадаем из исходной, припи- сываем номера 1, всем вершинам, еще не пронумерованным, приписываем номер 2, если мы попадаем в них из вершин с но- мерами 1. Запишем в множество X вершины с четными но- мерами, а в множество Y — вершины с нечетными номера- ми. Осталось проверить, что вершины, принадлежащие X и Y, не смежны. Напишите программу проверки двудольности графа, и если он двудолен, то найдите множества X и У. 5. В ориентированном графе аналогом понятия цепи является понятие «путь». Если (u,t) дуга, то вершины и и t называют- ся её началом и концом соответственно. Вершина t называ- ется достижимой из вершины и, если существует путь из и в t. В ориентированном графе найти множество всех вершин,
Динамические структуры данных 383 достижимых из заданной вершины v. Найти для каждой вер- шины графа множество достижимых из нее вершин. 6. Пусть G — связный граф, а и и и — две его несовпадающие вершины. Длина кратчайшего маршрута между и к и (он, ес- тественно, является простой цепью) называется расстоянием между вершинами и и о, обозначим как d(u,v). Поиск в ши- рину дает кратчайшие расстояния от первой вершины (обо- значим как и) до всех остальных вершин. Максимальное зна- чение этих расстояний называют эксцентриситетом вершины и. Максимальное значение среди всех эксцентриситетов вер- шин называется диаметром графа d(G). Минимальное из эк- сцентриситетов вершин связного графа называется его ради- усом и обозначается через r(G). Написать программу поиска диаметра и радиуса графа. Найти простые цепи, длины ко- торых совпадают с диаметром и радиусом графа. 7. Вершина и и ребро е называются инцидентными, если v явля- ется концом ребра е. Степенью вершины графа G называется число инцидентных ей ребер и обозначается deg( и). Напиши- те программу проверки «леммы о рукопожатиях»: сумма сте- пеней всех вершин графа — четное число, равное удвоенному числу ребер. Примечание Проверьте на примерах следствие этой леммы: «в любом графе число вершин нечетной степени четно». 8. Степенью ребра (и,и) назовем неупорядоченную пару (deg(и), deg(v)). Написать программу определения, совпадают ли сте- пени всех ребер заданного графа, и если нет, то можно ли уда- лить из него одну вершину (вместе с инцидентными ребрами) так, чтобы полученный граф обладал этими свойством. 9. Для произвольного графа G его дополнение — граф G’ — опре- деляется следующим образом. Любые две несовпадающие вер- шины смежны в G’ тогда и только тогда, когда они не смежны в G. Напишите программу проверки утверждения: «для любо- го графа либо он сам, либо его дополнение является связным». 10. Источником в ориентированном графе назовем вершину, от которой достижимы все другие вершины, стоком — верши- ну, достижимую от всех других вершин. Написать програм- му поиска всех источников и стоков данного ориентирован- ного графа.
384 Часть четвертая Занятие № 30. Двоичные деревья □ основные понятия; □ двоичные деревья, основные операции; □ экспериментальная работа с программами реализации де- рева с помощью массивов, пирамидальной сортировки с помощью двоичного дерева; □ выполнение самостоятельной работы. Основные понятия. Связный граф без циклов называется де- ревом. Пример дерева приведен на рисунке. Следующие определения дерева эквивалентны. Граф G=(V, Е), где I V | =N и I Е I =М является деревом, если: □ G — связный граф и M=N-1-, □ G — ациклический граф и □ любые две несовпадающие вершины графа G соединяет единственная простая цепь; □ G — ациклический граф, обладающий тем свойством, что если какую-либо пару несмежных вершин соединить реб- ром, то полученный граф будет содержать ровно один цикл. Определим терминологию, связанную с понятием дерева: □ корнем дерева называют единственную вершину, находя- щуюся вверху «перевернутого» дерева; □ самые нижние вершины дерева называют листьями; □ вершину называют внутренней, если она не является ни корнем и ни листом; □ о вершине, которая находится непосредственно над дру- гой вершиной, говорят, что она — родитель (предок), а вершина, которая расположена непосредственно под дру- гой вершиной, называется потомком.
Динамические структуры данных 385 На рисунке вершина с номером 1 — корень; вершины с но- мерами 4, 5, 6, 9, 10, 11 — листья; вершины 2, 3, 7, 8 — внут- ренние; вершина 3 — родитель вершины 7; вершина 8 — пото- мок вершины 3. Считают, что корень дерева расположен на первом уровне. Его потомки находятся на втором уровне и т. д. Максималь- ный уровень какой-либо вершины дерева называется глуби- ной или высотой дерева. Число потомков вершины называет- ся ее степенью. Максимальное значение этих степеней есть степень дерева. Степень дерева на рисунке, приведенном выше, равна трем. Суть двоичных деревьев, широко распространенных в про- граммировании, следует из названия. Степень дерева равна двум. Вершина (узел) дерева может иметь не более двух по- томков, их называют левыми и правыми. Двоичные (бинар- ные) деревья поиска (подкласс двоичных деревьев) характери- зуются тем, что значение информационного поля, связанного с вершиной дерева, больше любого соответствующего значе- ния из левого поддерева и меньше, чем содержимое любого узла его правого поддерева. Описание вершины дерева имеет вид; Type pt = ''node; node=Record data:Integer;{*Информационное поле. *} left,right:pt;{*CcbLHKH на левого и правого потомков. * } End; Var root:pt;{^Указатель на корень дерева.*) Основные операции; □ вставка элемента в дерево; □ удаление элемента из дерева; □ обход дерева. Вставка элемента в двоичное дерево поиска реализуется с помощью следующей простой рекурсивной процедуры. Перво- начальный вызов — Ins_Tree(root, number). Procedure Ins_Tree (Var t :pt ;x: Integer) ;( *x - значение вставляемого элемента. *) Begin If t=Nil Then Begin New(t) ; With t* Do Begin left:=Nil; right:=Nil; data:=x;
386 Часть четвертая End; End Else If x<=t''.data Then Ins_Tree(t*.left ,x) Else Ins_Tree (t''. right ,x); Выполните «ручную» трассировку логики процедуры на при- мере вставки нескольких элементов в произвольное двоичное дерево поиска. Реализация удаления элемента из дерева чуть сложнее. Если узел имеет одного потомка, то в поле ссылки родителя удаляе- мого элемента записывается ссылка, не равная Nil, и на этом все закончено. В том случае, когда у удаляемого элемента два потомка, для сохранения структуры дерева поиска на место этого элемента необходимо записать или самый правый эле- мент левого поддерева, или самый левый элемент правого под- дерева. Один из двух вариантов, другого не дано, и выбирается один из них. На рисунке приведена схема удаления элемента для первого варианта (самый правый элемент левого поддерева удаляемого элемента). Вид дерева до удаления Вид дерева после удаления Procedure Del_Tree {Var t:pt; x:Integer); Var q:pt; Procedure Del(Var w:pt);{*Поиск самого правого элемента в дереве. *)
Динамические структуры данных 387 Begin If w" . rightONil Then Del (w*. right) Else Begin q:=w;{★Запоминаем адрес для того, чтобы освободить место в «куче». ★} бЛ.data:=wA.data; w:=w~.left; End; End; Begin If tONil Then Ifx<t'\data Then Del_Tree(t*. left, x) Else If x>f'.data Then Del_Tree(t".right,x) Else Begin q:=t; If tA. right=Nil Then t:=t".left {★Правого поддерева нет.★) Else If t^. left=Nil Then t:=t~.right {★Левого поддерева нет.*) Else Del(f'.left);{*Находим самый правый элемент в левом поддереве. *) Dispose {q) ; End; End; Первоначальный вызов процедуры — I)el_Tree(root,number), где number — значение удаляемого элемента. Для вывода значений элементов двоичного дерева необходи- мо выполнить полный обход дерева. При обходе дерева его от- дельные вершины посещаются в определенном порядке. В про- цедуре Print_Tree, а с её помощью осуществляется обход дерева, можно шестью способами переставить операторы 1, 2, 3. Каж- дый способ перестановки даст вывод элементов дерева в опреде- ленной последовательности. Procedure Print_Tree (t:pt); Begin If tONil Then Begin Print_Tree (t* . left) ; { *Первый оператор (1) . *) Write (t*. data: 3) ; {*Второй оператор (2).*) Print_Tree (f. right) ; {* Третий оператор (3).*) End; End;
388 Часть четвертая Л В таблице перечислены все очередности вывода вершин дерева, приведенного на рисунке, полу- чаемые с помощью перестановки операторов в процедуре Pnnt Tree. Наиболее интересны первых три способа, их называют, соот- ветственно, слева направо, снизу вверх и сверху вниз. Для вы- ражения (А+В/С)*(D-E*F), представленного в виде дерева (на рисунке), они дают инфиксную (A+B/C*D-E*F — без скобокЛ постфиксную (ABC/+DEF*-*) и префиксную (*+A/BC~D*EF) формы записи. В некоторых случаях элементы дерева удобно выводить не в виде строки, при этом структура дерева не просматривается, а выполнять размещение на экране с учетом их взаимных связей в дереве. Следующая простая процедура является первым при- ближением решения этой задачи. Procedure Step_Tree(t:pt;h:Integer);{*h — количество выводимых пробелов, вставляемых перед выводом значения информационного поля узла. *) Var 1:Integer; Begin If toNil Then With Do Begin
Динамические структуры данных 389 Step_Tree (left,h+5) ; For i:=l TohDoWrite(' ') ;WriteLn (data : 3) ; Step_Tree (right,h+5) ; End; End; Разработать программу работы с двоичным деревом, в кото- рой вставка и удаление элементов осуществляются случайным образом. Экспериментальный раздел работы 1. Рассмотрим работу с двоичным деревом, если для его пред- ставления используются обычные массивы. Для хранения значений ключей (Key), левых (Left) и правых (Right) ссы- лок выделим свои массивы. Номер ячейки с корнем дерева является значением переменной Root. Для того, чтобы не ис- кать свободную ячейку, адрес первой свободной записывает- ся в HeadFree, а сами ячейки связаны адресами в отдельном массиве Next. Описание данных программы и процедура Init имеют вид: {$R+} Program Tree_Array; Const NMax=20; Type MyArray=Array{1..NMax] Of Integer; VarKey,Left,Right,Next:MyArray; N,Root,HeadFree:Integer; Procedure Init; Var i: Integer; Begin FillChar(Key,SizeOf(Key) ,0) ; FillChar (Left, SizeOf {Left) , 0) ; FillChar (Right, SizeOf (Right), 0) ; ReadLn(N); For i:=l To N-l Do Next [i] : =1+1; Next[N]:=0;Root:=0;HeadFree:=1; End; Для примера, рассмотренного выше по тексту, после фор- мирования дерева значения элементов массива представлены в таблице. Значение переменной Root равно 1, HeadFree — 12.
390 Часть четвертая Номер элемента массива Key Left Right Next I 1 100 3 2 0 2 120 0 0 0 3 20 4 5 0 15 0 0 0 5 50 6 0 6 30 3 9 0 55 0 10 0 8 25 0 0 0 9 35 0 0 10 60 0 0 0 11 33 0 0 0 12 0 0 0 13 13 0 0 0 14 0 0 0 | 20 0 0 0 0 Рассмотрим вставку элемента в дерево поиска. Procedure Inset_Tree{х:Integer); Var t,q,w:Integer; Begin If Root=0 Then Begin {*Вставка первого элемента в дерево — корня. *} Root:=HeadFree;HeadFree:=Next[HeadFree]; Key[Root]:=x;Next[Root]:=0; End Else Begin t:=Root; While t<>0 Do Begin {*Находим место элемента в дереве. *} q:=t;{*Номер предыдущего элемента.*) If x<Key[t] Then t:=Left[t] Else t:=Right[t]; {★Идем по правой или по левой ссылке. *) End; W:=HeadFree;HeadFree:=Next[HeadFree] ; Next[w]:=0;{*Берем ячейку иэ списка свободных. *) Key[w]:=х; If x<Key[q] Then Left[q]:=w Else Right[q]:=w; {*Изменяем ссылку у предыдущего элемента.★) End; End;
Динамические структуры данных 391 С удалением элемента из дерева чуть сложнее (много част- ных случаев). Прежде чем перейти к их разбору, приведем про- стую логику возврата ячейки с номером t в список свободных. Возвращаемая ячейка становится первой в списке свободных. Procedure Return(t:Integer);{‘Возврат ячейки в список свободных.*) Begin If tRoot Then Begin Left[t]:=0; Right[t]:=O; Key[t]:=0; Next[t]:=HeadFree; HeadFree:=t; End; End; Приведем «шаблон» процедуры удаления. Прежде чем рас- сматривать его, попытайтесь «прорисовать» все возможные слу- чаи удаления элемента из дерева и выполняемые при этом дей- ствия. Procedure Del_Tree(х:Integer); Var t,q,w,z:Integer; Begin t:=Root;q:=t;{‘Поиск удаляемого элемента.*) While (t<>0) And {xoKey [t] ) Do Begin q: =t;{‘Запоминаем адрес предыдущего элемента. *} If x<Key[t] Then t:=Left[t] Else t:=Right[t]; End; If t<>0 Then Begin{*Если элемент найден, то выполняем удаление.*} If Left[t]=0 Then{‘Левого поддерева у удаляемого элемента нет. *) If t=Left(qJ Then Left[q]:=Right[t] l ‘Удаляемый элемент — левый потомок родителя?*) Else If to Root Then Right[q]:=Right[t] Else Root:=Right[t] {‘Удаляется корень дерева и он не имеет левого потомка. *) Else If Right[t]=0 Then {‘Правого поддерева у удаляемого элемента нет.★) If t=Left[q] Then Left[q]:=Left[t] {‘Удаляемый элемент - левый потомок родителя?*] Else If toRoot Then
392 Часть четвертая Right[q]:=Left[t] Else Root:=Left[t] {^Удаляется корень дерева, и он не имеет правого потомка.★) Else Begin {^Удаляемый элемент имеет два потомка. Находим самый правый элемент в левом поддереве. *) w:=Left[t];z:=t;q:=z;(^Запоминаем сам элемент, его родителя и еще одного родителя (дедушку). Дайте объяснение этому факту.*) While wOO Do Begin q:=z;z:=w; и:=Right[w];End; If Left[q]= z Then Left[q]:=Left[z] {* Родитель удаляемого элемента является левым потомком своего родителя. *) Else Right[q]:=Left[z]; Key[t]:=Key[z];{^Переписываем значение, например 35, на место удаляемого элемента, равного 50 (в примере) . *) t:=z;(*B список свободных ячеек заносится самый правый элемент из левого поддерева. Вся информация из этого узла дерева переписана в как бы удаляемый элемент. *) End; Return (t);{^Возвращаем элемент в список свободных. *) End; End; Проверьте правильность работы рассмотренных процедур. Все ли случаи учтены? Разработайте динамическую модель ра- боты с двоичным деревом. Вставка и удаление элементов дерева должны выполняться по некоторому случайному закону. При этой работе Вам может оказаться полезной вспомогательная процедура просмотра элементов массивов. Procedure Print; Var i :Integer; Begin
Динамические структуры данных 393 For г : =1 То N Do Write (i: 4) ; Writein ; For i:=l To N Do Write(Key[i]:4); Writein; For i :=1 To N Do Write (Left [i ] : 4) ;Writeln ; For i:~1 To N Do Write(Right[i]:4);Writeln; For I: =1 To N Do Write (Next [ 1] : 4) ; Writein ; WriteLn (Root,' ’,HeaDFree); End; 2. Вернемся к теме пирамидальной сортировки (занятие 20). Данные во входном файле представлены в формате: N — чис- ло элементов (в первой строке), а затем со второй строки сами данные, естественно, не отсортированные. Например: 8 10 3 13 9 5 12 Нашей первой задачей является чтение данных из файла и представление их в виде правильного двоичного дерева (макси- мальное количество узлов дерева имеет два потомка). Затем это дерево необходимо преобразовать в пирамиду, так как это пока- зано на рисунке. Элементы дерева образуют пирамиду, если значение информационного поля в каждом узле дерева больше значений у узлов потомков. Очевидно, что после этого преобра- зования в корне дерева записан минимальный элемент. Запи- сываем его в результирующий файл, а на его место записываем число, заведомо большее (а после первого шага и равное) всех значений в узлах дерева. После этого «проталкиваем» верхний элемент вниз по дереву. В корне дерева окажется вновь следую- щий минимальный элемент. Записываем и заменяем его на максимально возможное значение. Процесс продолжается N раз. После первого «проталкивания» дерево имеет вид, показан- ный на следующем рисунке (до и после «проталкивания»). 15—452
394 Часть четвертая Приведем «костяк» решения, а затем начнем его уточнять. Program Tree_Pyramid; Type р!=ле1ет; elem=Record data:Integer; left,right:pt; End; Var head:pt;{'Указатель на корень дерева.*) N:Integer; f:Text; Begin Assign (Input,'Input.Txt');Reset (Input); {'Стандартные действия по открытию рабочих файлов программы. *) Assign (Output, ' Output. Txt') .‘Rewrite (Output) ; Assign (f, 'Result. Txt') .‘Rewrite (f) ; ReadLn (N);head:=Nil;('Значение N требуется для построения «правильного» дерева. *) head:=Tree(N);{'Функция Tree обеспечивает построение «правильного» дерева.★) Step_Tree (head,0);('Вспомогательная процедура вывода дерева. Рассмотрена ранее. Можно исключить, она не играет принципиальной роли в данном решении. ') Change(head);{'Строим пирамиду. *} Step_Tree (head, 0) ; Solve(1);{'Записывает значение из корня дерева в результирующий файл и проталкивает максимальное значение вниз по дереву. Параметр процедуры указывает на номер проталкивания. *} Close (Input) .-Close (Output) ,-Close (f) ; End. Рассмотрим функцию построения «правильного» дерева. Она возвращает в основную программу указатель на корень дерева и является, естественно, рекурсивной. Параметр функции (t) определяет количество элементов, из которых строится дерево.
Динамические структуры данных 395 Создается новый узел, а затем рекурсивно строятся левое и пра- вое поддеревья с количеством узлов в два раза меньшим, чем значение t (для правого поддерева уменьшаем количество узлов на единицу — минус, строящийся узел). Function Tree (t:Integer) :pt; Var x:Integer; w:pt; Begin If t=0 Then Tree: =Nil { *Нулевое количество узлов определяет ссылку типа Nil. *) Else Begin Read(x);{*Читаем число из входного файла.★) New(w);{*Выделяем место в «куче» для нового узла дерева. *) With w* Do Begin data:=x; left:=Tree(t Div 2] ; (*Формируем значения левой и правой ссылок узла. *) right: =Tree (t-t Div 2 -1) ; End; Tree;-w; End; End; При построении пирамиды необходимо поменять значения в узлах родителя и потомков, у родителя должно быть наимень- шее из трех значений. Эти действия выполняются при стандар- тном рекурсивном обходе дерева, например как при его выводе в постфиксном виде. Procedure Swap(Var х,у:Integer);{★Вспомогательная процедура. *} Var z: Integer ; Begin If x>y Then Begin z:=x;x:=y;y:=z;End; End; Procedure Change (t:pt) ; Begin If tONil Then Begin Change (tA . left) ; {*Идем в левое поддерево.*) Change ft''. right) ; (*3атем в правое поддерево.*) If t" . leftoNil Then Swap (t*. data, t~.left”. data) ; (*Записываем минимальное значение в
396 Часть четвертая корень поддерева.*) If . rightoNil Then Swap (t*.data,t*.right*.data) ; End; End; Основная процедура пишется по традиционному принципу «как говорим, так и программируем». Procedure Solve(k:Integer); Begin If k<=N Then Begin {*Пока данные не отсортированы, выполняем . . . ★) Write(f,head*.data, ' ');{^Записываем значение из корня дерева в результирующий файл. *} head*.data:=MaxInt;(^Присваиваем значение 32767. ★) Push (head); (^Проталкиваем значение из корня дерева вниз по дереву.*) Step_Tree(head,0);(*Вспомогательная процедура. Можно исключить.★} Solve(k+1);{^Переходим к обработке следующего элемента. *) End; End; И наконец, процедура «проталкивания» элемента по дереву. Procedure Push(t:pt); Var v ,w :pt;{*Вспомогательные переменные. Введены для того, чтобы текст процедуры помещался на экране, был обозримым. Вспомним о структурном программировании.*} Begin If to Nil Then Begin v:-t*. left;w:-t*.right;(^Запоминаем адреса корневых элементов левого и правого поддеревьев. *) If (vONil) And (wONil) Then {*Если оба поддерева есть, то выбираем поддерево с наименьшим значением в корне. *} If v*.data<w*.data Then Begin Swap(t*.data,v* .data);Push(v);End Else Begin Swap (t*.data,w*.data);Push(w);End (*Обработка случаев, когда узел имеет одно
Динамические структуры данных 397 поддерево или является листом. *) Else If vONil Then Begin Swap (t" . data, v*. data) ; Push (v) ;End Else If wONil Then Begin Swap(t" .data, ыЛ.data);Push(w);End; End; End; Протестируйте решение. Оцените временную сложность рас- смотренного метода сортировки. Обратите внимание на то, что память компьютера используется только (практически) из «кучи». Задания для самостоятельной работы 1. Разработать логическую функцию: □ проверки наличия заданного элемента в дереве; □ подсчета суммы элементов дерева; □ определения наибольшего (наименьшего) элемента в де- реве; □ подсчёта количества элементов в дереве; □ сравнения двух деревьев и Т2; □ определения максимальной высоты дерева; □ проверки наличия в дереве хотя бы двух одинаковых элементов. 2. Дан текст (в текстовом файле). Определить частоту вхожде- ния каждого из слов в текст. Примечание Тип элемента в узле дерева String, кроме того, необходимо ввести счетчик числа повторений каждого слова. 3. В задаче 2 определить количество вершин дерева, содержа- щих слова: □ начинающиеся на одну и ту же букву; □ являющиеся палиндромами; □ содержащие все гласные буквы латинского алфавита. 4. Написать процедуру удаления из созданного двоичного дере- ва поиска всех отрицательных элементов. 5. Разработать функции, возвращающие значение указателя на второй минимальный (максимальный) элемент в двоичном де- реве поиска. 6. Двоичное дерево считается идеально сбалансированным, если для каждой его вершины количество вершин в левом и правом
398 Часть четвертая поддеревьях различается не более чем на 1. Написать функцию проверки идеальной сбалансированности двоичного дерева. 7. В описании вершины двоичного дерева поиска добавьте два поля типа Integer для фиксации высот (глубин) левого и пра- вого поддеревьев. Разработайте процедуры (вставки, удале- ния элемента) работы с таким деревом. Измените два поля на одно, в котором фиксируется разность высот правого и лево- го поддеревьев. Проведите модификацию программы для это- го случая. 8. В файле даны N целых чисел в двоичной системе счисления (М бит каждое). Построить двоичное дерево, в котором чис- лам соответствуют листья дерева, а путь по дереву определя- ет значение информационного поля этого листа. 9. В текстовом файле записаны целые числа. Построить двоич- ное дерево, элементами которого являются числа из файла. Написать процедуру: □ определяющую количество нечетных чисел: □ вычисляющую сумму элементов дерева, кратных 3; □ определяющую число вершин дерева на каждом уровне; □ число вершин в правом и левом поддеревьях: 10. Построить двоичное дерево из букв строки и написать проце- дуру: □ определения количества повторяющихся букв в дереве; □ определения, каких букв больше — гласных или соглас- ных; □ вывода самого правого элемента левого поддерева; □ вывода самого левого элемента правого поддерева.
Динамические структуры данных 399 Занятие № 31. Сбалансированные деревья □ основные понятия; □ операции по восстановлению балансировки при вставке и удалении элемента в АВЛ-дерево; □ В-деревья, вставка элемента; □ выполнение самостоятельной работы. Основные понятия. Двоичное дерево считается идеально сбалансированным, если для каждой его вершины количество вершин в левом и правом поддеревьях различается не более чем на 1. Для одних и тех же данных, например целых чисел от 1 до N (в зависимости от порядка их поступления на обработку), структура двоичного дерева поиска будет различной. Возмож- ны варианты как от идеально сбалансированного дерева, так и простого линейного списка, когда или все левые, или все пра- вые ссылки узлов равны Nil. Реализация операций восстанов- ления идеальной сбалансированности при случайной вставке или удалении элемента из двоичного дерева поиска, разумеет- ся, возможна, однако она не рассматривается в учебной литера- туре из-за ее сложности. Изменим определение сбалансированности. Эта трактовка понятия сбалансированности предложена Г. М. Адельсоном-Ве- льским и Е. М. Ландисом в 1962 году. Дерево является сбалансированным тогда и только тогда, когда для каждого узла высота его двух поддеревьев различается не более, чем на 1. Деревья, удовлетворяющие этому условию, называют АВЛ- деревьями. Введем в описание каждого узла поле bal, значение которого определяет разность высот его правого и левого подде- ревьев. На рисунке в узлах указано значение bal. Дерево слева не удовлетворяет условию АВ Л-сбалансированности, справа — удовлетворяет. Ни то, ни другое дерево не удовлетворяет усло- вию идеальной сбалансированности.
Часть четвертая Рассмотрим операции, выполняемые по восстановлению ба- лансировки АВЛ-дерева. Каждый тип изменения рассмотрим на определенном наборе исходных данных. Так проще. Отсле- живание изменений на случайных файлах, когда выполняются различные типы балансировок, затруднительно. Пусть входной файл имеет вид: 86105791331. До вставки 1 балансировка дерева не нарушалась, ибо элементы вставлялись в той очередности, в которой они записаны во входном файле. Мы «находимся» в левом поддереве, и увеличивается (по высо- те) левое поддерево. Выполняем «малый правый поворот» для восстановления балансировки дерева. Его суть показана на сле- дующем рисунке. После действий, выделенных жирными линиями, в той по- следовательности, которая указана в квадратных скобках, из левого фрагмента дерева получается правый, из не сбалансиро- ванного фрагмента — сбалансированный. Значения указателей
Динамические структуры данных 401 t и v определены до выполнения описанных действий, и их из- менения передаются обратно в вызывающую логику. Первый вариант программного кода малого правого поворота имеет вид: Procedure Turn_right_small(Var t,v:pt); Begin tЛ. left: =v''. right; I * Первое действие. *) v''. right :=t; l ★Второе действие. ★) t^.bal:=0; t:-v;f^Третье действие. ★} End; Аналогичный тип поворота может возникнуть и при удале- нии элемента дерева. Пусть у нас есть фрагмент дерева, изобра- женный на следующем рисунке (левая часть), и удаляется эле- мент 7. Измененное дерево приведено на правой части рисунка. Используется малый правый поворот. Отличие в том, что изме- няется логика работы с полем bal. Проверьте ее на следующем фрагменте программного кода. Procedure Turn_right_small (Var t,v:pt;Var h: Boolean); Begin WriteLn ('Right small ';t".key); t-' .left:=vA.right;{★Первое действие.★) v''. right:=t; {★Второе действие. ★} If v''.bal=0 Then Begin t”.bal:=-l;v*.bal:=1; h:=False; End Else Begin .bal:=0;v'.bal:=0;End; t:=v;{★Третье действие.★) End;
402 Часть четвертая Это окончательный вариант процедуры малого правого пово- рота, используемой в дальнейшем. Необходимо осознать, что является входными параметрами процедуры, к каким резуль- татам она приводит, и попытаться сформулировать условия, при которых она вызывается. Первый вариант малого левого поворота осознаем на дан- ных: 8610579131517. После вставки элемента 17 требуется балансировка. Выполняем очевидные действия, показанные на следующем рисунке.
Динамические структуры данных 403 И первый вариант программного кода, реализующего малый левый поворот: Procedure Turn_left_small(Var t,v:pt); Begin . right.left;(★Первое действие. *} v".left:=t;{*Второе действие.★} t~.bal:=0; t:=v; {* Третье действие. *} End; Аналогичная ситуация возникает и при удалении элементов из дерева. Пример. Пусть удаляется элемент 12 из дерева, изоб- раженного на левой части рисунка. Нарушается балансировка. Выполняем малый левый поворот. Вид дерева после поворота на правой части рисунка. Если работа с полями ссылок совпа- дает с разобранными выше, то изменение поля Ъа1 осуществля- ется чуть иначе. Procedure Turn_left_small (Var t,v:pt;Var h:Boolean) ; Begin WriteLn('Left small ',t'~.key); tЛ . right:=vA.left;{* Первое действие.*} . left:=t;(*Второе действие.★} If v^.bal = 0 Then Begin .bal:=1;vn.bal:=-1; h:=Ealse;End Else Begin tл. ba 1: =0; v'~. bal: =0;End; t:=v;{^Третье действие.*} End; Это не единственные случаи нарушения балансировки дере- ва. Их еще два: «большой правый» и «большой левый» поворо- ты. Изменим входной файл: 10 6 14 5 8 12 16 2 4. После встав- ки элемента 4 балансировка дерева нарушается. Оно имеет вид, показанный на очередном рисунке.
404 Часть четвертая Малый правый поворот применить нельзя — нарушается принцип дерева поиска. Выполняемые действия по восстанов- лению балансировки показаны на следующем рисунке. Действий уже пять. Первое и третье на первый взгляд бес- смысленны. Однако надо иметь в виду тот факт, что рассматри- ваемые типы поворотов выполняются над любым фрагментом дерева, в котором нарушена балансировка. Узел со значением 4 в общем случае может иметь потомков. Procedure Turn_right_big(Var t,v:pt); Var w:pt; Begin WriteLn('Right big ',t^.key); w:=v''. right;
Динамические структуры данных 405 v' . right:=ыл.left;{*Первое действие. *} w" . left:=v;{^Второе действие. *) tл . left: =ыл. right; { * Третье действие . *} . right:=t;{*Четвертое действие. *} If и'' .bal=-l Then Ьл.Ьа1:=1 Else t“.bal:=0; {*Приведите примеры для каждого случая изменения значения поля bal. *} If W'.bal=l Then v*.bal:=-l Else .bal:=0; t:=w;{*Пятое действие. *} w''. bal: =0; End; Если работа с полями ссылок достаточно очевидна, то о дей- ствиях с полем bal этого не скажешь. В процедуре приведен код, повторяющий логику действий с полем bal в тех книгах, где рассматриваются АВЛ-деревья в полном объеме, их очень не много. Это книги Н. Вирта (Алгоритмы + Структуры данных = Программы. М.: Мир, 1985) и С. Д. Кондратьевой (Введение в структуры данных. М.: Издательство Ml ТУ им. Н. Э. Баумана, 2000). Вспомним наше «рабочее» определение программы — «а может быть, это не так, а может быть, это не верно»? Может быть, верен следующий программный код: If w'' .Ьа1=-1 Then .bal :=1 Else v'.bal^O; If w''.bal=l Then t^ ,bal:=-l Else t'- .bal:=0 ; Нахождение истины — вопрос отладки программы. Требует- ся найти те возможные входные данные, создать такую ситуа- цию в работе программы, при которой можно говорить об ис- тинности того или другого варианта программного кода. А это уже «высший класс (пилотаж)» в освоении материала. Последний тип поворота рассмотрим на следующих данных: 10 6 14 5 8 12 16 20 18. После вставки 18 нарушается баланси-
406 Часть четвертая ровка дерева. Для ее восстановления необходимо выполнить действия, показанные на следующем рисунке. 14 14 Программный код фрагмента имеет вид: Procedure Turn_left_big(Var t,v:pt}; Var w: pt; Begin WriteLn ( ' Left big ',f'.key); w:=иЛ.left; .left:.right;{*Первое действие. *} W'.right:=v;(* Второе действие. *} t*. right:=wn.left;{^Третье действие.*} w”. left: = t;{*Четвертое действие. *) If w^.bal=-l Then t^.bal^l Else tn.bal:=O; If ыл.Ьа1=1 Then v^.bal:=-l Else v".bal:=0; t:=w;(*Пятое действие. *} .bal:=0; End; Открытый вопрос — работа с полем bal. Предложим другой вариант из книги Н.Вирта [], выбор верного — Ваша задача. If w''.bal=-l Then v^.balt^l Else v^.balr^O; If w''.bal=l Then t^.bal^-l Else t".bal:=O; Исходный материал для дальнейшего рассмотрения есть. Сейчас требуется осознать — как осуществлять выход на эти процедуры, как выявлять эти ситуации при работе с деревом
Динамические структуры данных 407 (вставка, удаление элементов). Общая логика процедур включе- ния, исключения элементов аналогичны соответствующим про- цедурам работы с обычным двоичным деревом поиска за иск- лючением одной детали. В логической переменной h (значение True) фиксируется факт вставки (удаления) элемента в (из) де- рево (а), и это значение передается в вызывающую логику. Оно, а также значение поля bal узла дерева служат основанием для принятия решения о том, требуется ли балансировка относите- льно рассматриваемого узла. Так как процедуры поворотов об- щие для режимов вставки и удаления элементов, то вводится еще одна дополнительная логическая переменная. Далее с не- значительными изменениями (они выделены «жирным» курси- вом) повторим процедуры работы с обычным двоичным деревом поиска. Procedure Search (х:Integer; Var t:pt; Var h:boolean); Begin If t=Nil Then Begin New (t) ; t . key: =x;t*.bal :=0; t''. left :=N11; t^.right:=Nil; h:=true;{*Элемент вставлен. *} End Else If x<t".key Then Begin{*Идем в левое поддерево.★) Search (х,t'~.left,h); Bal_left(t,h,False);{^Проверяем необходимость балансировки при вставке элемента в левое поддерево. Логическая константа False говорит о том, что обращение идет в режиме вставки элемента. *) End Else If x>t,'.key Then Begin{*Идем в правое поддерево. *} Search (х, СЛ . right, h) ; Bal_right (t,h,False) ; { ^Проверяем необходимость балансировки правого поддерева. Логическая константа False говорит о том, что обращение идет в режиме вставки элемента. *) End
408 Часть четвертая Else h:=False;{"Элемент (ключ) повторяется. Проверка нарушения балансировки не требуется. *} End; Процедура удаления. Procedure Del_tree(х:integer; Var t:pt; Var h:Boolean); Var v:pt; Procedure Del_el(Var w:pt); Begin If w'.rightONil Then Begin Del_el (w'. right) ; Bal_left(w,h,True);{"Выполняем после удаления балансировку левого поддерева. Константа True говорит о том, что балансировка происходит в режиме удаления. *) End Else Begin v*. key: =w" . key; v: =w ; w:=wA.left; h:=true; End; End; Begin if t=Nil then WriteLn ( 'There is no such element here') else if x<t".key then begin Del_tree(x,t",left,h) ; Bal_right(t,h,True);{"Удаляем из левого поддерева, выполняем правую балансировку. В этом отличие от режима вставки. Ключевой момент для осознания работы с АВЛ-деревьями.*) end else if x>t"'.key then begin Del_tree (x,tA.right,h); Bal_left(t,h,True);{"Удаляем из правого поддерева. Делаем левую балансировку."} end else begin
Динамические структуры данных 409 End; if v'. right=Nil then begin t^v'.left; h:=true; end else if v‘.left=Nil then begin t:=v".right; h:=true; end else begin Del_el (v'-.left) ; Bal_right (t,h, True) ; end; end; Примечание В процедуре нет оператора Dispose. Найдите возможность его «без- болезненной» вставки. Логика почти детализирована. Есть процедуры поворотов (это проделано как бы по технологии «снизу — вверх»), есть об- щая часть (это проделано как бы по технологии «сверху — вниз»). Остался ключевой фрагмент, связывающий и то, и дру- гое — процедуры Bal_nght и Ba.lJ.eft. Но и он детализирован частично, ибо строго определены как входные, так и выходные параметры этих процедур. Procedure Bal_left(Var t:pt; Var h:Boolean; Del .-Boolean) ; Var v:pt; Begin If h Then Case t'-.bal Of (‘Необходимость балансировки анализируется относительно узла tA. *) 1 :Begin If Not Del Then h:=false; (*B режиме вставки элемента правое поддерево имело большую высоту. Левое поддерево увеличилось на единицу. Балансировка на требуется. *} t”.bal:=O; End; 0 : Begin If Del Then h: =False;{ ‘Удалили элемент из правого поддерева. Балансировка не требуется. Критерий АБЛ—дерева сохраняется. *}
410 Часть четвертая tл . bal:=-1;{*Высота левого поддерева увеличилась. *) End; -1 .-Begin { *Мы в левом поддереве {в режиме вставки) и увеличилась высота левого поддерева. Требуется балансировка. Аналогичная ситуация возникает при удалении элемента из правого поддерева — высота левого поддерева увеличивается. *} v:=t".left;(*Левый потомок рассматриваемого узла. *} If (vA.bal=-l) Or ((v'~.bal = 0) And Del) Then Turn_right_small (t,v,h) Else Turn_right_big(t, v);{*Если высота левого поддерева левого потомка была больше высоты правого поддерева левого потомка, то требуется малый правый поворот, иначе — большой правый поворот. Малый правый поворот в режиме удаления требуется и в том случае, если левый потомок узла Сл имеет поддеревья равной высоты. *} End; End; End; По аналогии пишется и процедура восстановления баланси- ровки правого поддерева. Procedure Bal_right (Var t:pt; Var h:Boolean; Del:Boolean); Var v;pt; Begin If h Then Case tA.bal Of -1:Begin If Del Then h : =false; t'-.bal:=0; End; 0: Begin If Del Then h:=False; tA.bal:=l; End; 1:Begin v: = tA.right;(*Правый потомок узла. *)
Динамические структуры данных If (vA .bal=l) Or ((vA .bal=O) And Del) Then Turn_left_small(t,v,h){*Если высота правого поддерева правого потомка была больше высоты левого поддерева правого потомка, то требуется малый левый поворот, иначе — большой левой поворот. В режиме удаления аналогичные действия выполняются и при равных высотах дерева. ★) Else Turn_left_big(t,v); End; End; End; Для справки приведем оставшуюся часть программного кода, связанного с организацией ввода исходных данных и вывода результатов манипуляций с АВЛ-деревом. Procedure Init(Var t:pt); Var x;Integer; h:Boolean; Begin Assign (Output, ' avl_ans. txt') ;ReWri te (Output) ; {'Результат обработки выводится в файл. Это удобнее для анализа, чем вывод на экран, ибо есть предыстория изменения структуры дерева. *} ClrScr; Assign (Input, ' 1 . txt') ; ReSet (Input) ; While Not EoLn Do Begin Read (x) ; Search (x,t,h) ; WriteLn; WriteLn ('After input: ' ,x) ; PrintTree (t, 0) ; End; Close (Input) ; End; Удаляемые элементы дерева берутся из другого файла. Procedure Init_Del (Var t:pt); Var h:Boolean; x:Integer; Begin Assign(Input,'l_del.txt');ReSet(Input) ; While Not Eof Do Begin Read (x) ; Del_Tree (x, t,h) ; WriteLn; WriteLn (' After delete: ' ,x) ;
412 Часть четвертая PrintTree (t,0); End; Close (Input); Close (Output); End; И наконец, основная программа. Begin head:=Nil; Init (head) ; PrintTree (head, 0) ; Init_Del(head);End. Возникает вполне естественный вопрос, а не упростится ли логика, если в каждом узле ввести адрес связи к родителю? Ис- следуйте его. Напишите вариант программы для этого случая. Другим типом сбалансированных деревьев являются Б-дере- въя (Btree). Они предложены Р. Бэйером в 1972 году и относят- ся к классу сильно ветвящихся деревьев. Отличие заключается в том, что в узле хранится не одно значение ключа, а несколько и, соответственно, это количество ключей определяет количест- во потомком узла. При этом узел уже называется страницей. Б-дерево определяется характеристикой, называемой порядком и обозначаемой (Б-дерево порядка .V)- Определение Б-дерева: □ каждая страница содержит не более 2*N элементов (клю- чей); □ каждая страница, кроме корневой, содержит не менее N элементов; □ каждая страница является либо листом, то есть не имеет потомков, либо имеет т+1 потомков, где т — число нахо- дящихся в ней ключей;
Динамические структуры данных 413 Для входных данных: 45, 55, 65, 75, 85, 10, 20, 70, 90, 100, 68, 80, 95, 105, 110, 72, 15 (на обработку поступают именно в такой очередности) Б-дерево порядка 2 имеет вид, представлен- ный на рисунке. По своему виду это дерево поиска, только бо- лее сложное по структуре узла. Описание узла (страницы), содержащей т ключей, должно иметь место как для хранения значений ключей, так и т+1 ссылок на потомков. Все это «навязывает» следующие типы данных программного решения задачи работы с Б-деревом (пока рассматривается только обход и вставка элемента в Б-дерево). Program В_Тгее; Const N=2;{^Порядок дерева.★) NMax=2*N;I*Максимальное количество ключей. *} Typept=*page;{^Указатель на узел (страницу) . *) index=0. .NMax; item=Record key:Integer; p:pt; count:Integer; End;{^Значение ключа; указатель на правого потомка, если он есть; Счетчик числа повторений ключа (предполагаем, что в файле с исходными данными есть повторяющиеся ключи).★) page=Record{^Описание страницы. *} m:index;(^Количество ключей.*} рО :pt;{^Указатель на самого левого потомка.*} A:Array[l..NMax] Of item;{*Массив, каждому ключу страницы соответствует элемент массива.★} End; Var root:pt;{*Единственная (явная, если так можно выразиться) переменная нашей программы — указатель на корень дерева. *} Рассмотрим вставку нового элемента в Б-дерево. Два случая из вышеприведенного примера. 45, 55, 65, 751 | 45, 55 [ | 75, 85 Вставка элемента 85 приводит к расщеплению одного узла корня на два и к образованию нового узла (корня). Значение
414 Часть четвертая ключа в новом узле равно среднему из значений ключей рас- щепляемого узла. Вставка элемента 100 приводит к расщеплению узла и до- бавлению среднего элемента в узел родителя. 10, 20, 45, 55 70, 75, 85, 90 10, 20, 45, 55 90, 100 70, 75 I Двоичный поиск элемента в упорядоченном массиве целых чисел (поле key записей типа item) осуществляется как обыч- но. Он оформлен в виде функции, значение которой определяет результат поиска. Если элемент х найден, то значение функции указывает на то место в массиве записей, где он находится. Function Find (t:pt;х:Integer):index;{*t — указатель на узел, в массиве записей которого осуществляется поиск.★} Var i,j,к:Integer; Begin i :=1; j:=tЛ .m; While i<j Do Begin k: = (i+j+l) Div 2; If x<tA,A[k].key Then j:=k-l Else If x>tA.A[k].key Then i:=k+l Else Begin i:=k; j:=k;End; End; If i>t''.rr, Then Find:=t* .m Else If icl Then Find:=0 Else If t*.A[i],key>x Then Find:=i-1 Else Find:=i; End; Самый простой тип вставки — это добавление элемента в су- ществующий узел, когда количество элементов в последнем не превышает максимального значения Nmax. Входными пара- метрами логики являются:
Динамические структуры данных 415 □ t — указатель на узел, в который вставляется элемент; □ г+1 — место в массиве записей узла, на которое вставля- ется элемент; □ и — значение элемента. Результатом работы является не только изменение узла Г, но и изменение значения переменной h (вспомните АВЛ-дере вья) — структура дерева (как нечто целого) сохранилась, изме- нение коснулось только одного узла. Procedure Ins_Simple (t :pt; Var h:Boolean;u:item; r:Index); Var i:Integer; Begin With t" Do Begin m: =m+l;{*Изменяем количество элементов в узле. *) For i:=m DownTo r+2 Do A[1]:=A[1-1];{*Сдвигаем элементы в массиве записей. *) А[г+1]:=и;{*Вставляем элемент.*} End; h:=False;{^Структура дерева не изменяется. *} End; А чем отличается поиск по Б-дереву от поиска по бинарному дереву? В принципе только деталями перехода к узлам нижне- го уровня, к потомкам и тем, что при выходе из поиска, то есть при возврате к родителям, передается не только значение пере- менной Л, но и запись и, вставляемая в один из узлов предков. Действительно, если родителю передана запись для вставки от потомка, а сам родитель имеет максимальное количество клю- чей, то формируется новая запись, которая передается дальше вверх по дереву. И так до корня, если это необходимо. Procedure Search(t:pt; х:Integer; Var h:Boolean; Var u:item); Var i:Integer; v:item; Begin If t=Nil Then Begin{*Дошли до листа. *} h:=True; With u Do Begin [*Формируем запись. *) key:=x ; count:=l; p:=Nil; End; End Else Begin 1:=Find(t,x);
416 Часть четвертая If tл.А[1].кеу=х Then Begin { *Находим ключ х в узле tA Он найден, есть совпадение. данные повторные. *} Inc(t". А[г].count);{*Изменяем значение счетчика числа повторений. *) h:=False;{^Структура дерева не изменяется.*) End Else Begin{ *Элемент х не найден. Продолжаем поиск. Значение переменной i определяет адрес потомка. *} If i<l Then Search(t^.рО,х,h ,v) Else Search (f'.A[i].p,x,h,v); ^Продолжение поиска.*) {*3начение переменной h определяет необходимость вставки элемента в дерево. *) If h Then If t* .m<Nmax Then Ins_Simple (t,r,h,v,i) (* Простой случай вставки элемента в узел. *) Else Ins_Div(t,h,u,v,i);{*Сложный случай вставки, требующий деления узла, его разбивки.*} End; End; Продолжим рассмотрение. Логика сложного случая вставки элемента в дерево: Procedure Ins_Div(t:pt; Var h:Boolean ;Var u:item; v:item; i:Integer;); Var w:pt; J:index; Begin New(w);/‘Создается новый узел. *} If 1<=N Then Begin If i=N Then u:=v Else Div_Left(t,u,v,i) /‘Содержимое нового узла формируется из левой части ключей и ссылок узла tn.*) For j:=l То N Do .A[j j :=t'~ .A[j+Nj ; End Else Div_Right (t,w,u,v, i) ; {‘Содержимое нового узла формируется из правой части ключей и ссылок узла бл.*} tA.m:=N;{‘Изменяем количество ключей в узлах tл и и*.*}
Динамические структуры данных 417 W\m:=N; w*.рО:=и.р; и.р:=ы;{*3аписъ и — передается вверх по дереву вплоть до корня. *} End; Примечание Если этот фрагмент логики не очень понятен при первом чтении, то сделайте шаг вперед — рассмотрите процедуры DivJLeft и Div_ Right, а затем вернитесь обратно. ~ Procedure Div^Right (t,u:pt;Var u:item;v:item,т : index) ; Var j :Integer; Begin i:=i~N;(*Ключ x принадлежал правой части массива ключей узла t". Работаем только с этой частью. *) u:=t^.A[N+1];(^Запоминаем запись на месте N+1, ибо она пойдет дальше вверх по дереву. *} For j:=l То 1-1 Do w'-.Afj] :=бл.А[3+N+1 ] ; {*Переписываем эту часть узла t' в новый узел и-. На месте 1 должна быть запись и, полученная этой процедурой. ★) For з:=1+1 То N Do W~. А [3 ] : =t~. А [ 3+N] ; I^Дописываем оставшуюся часть. *) End; Следующая процедура была бы чуть сложнее, если логика об- работки элемента (вставляемого) на границе не была «вынесена» в процедуру Ins Div. В этом случае элемент уже является значе- нием ключа записи и. Необходимо в w~ переписать записей из Г и на этом закончить обработку на этом уровне дерева. Procedure Div_Left (t :pt;Var и:item;v:item;i: index); Var 3 : Integer; Begin и:=t”.A[N] ;{*3апоминаем запись, которая пойдет к родителю. *) For j:=N DownTo 1+2 Do t". A [ 3 ] :. A [g-1 ] ; { *Сдвигаем элементы массива. *) t".A[i+1] :=v; { ^Вставляем запись, соответствующую ключу х. *} End;
418 Часть четвертая Итак, основная часть работы (разбора) сделана. Остались почти привычные детали: основная программа; вспомогатель- ная процедура вывода дерева и, конечно, процедура организа- ции ввода данных из файла. Для вывода элементов 23-дерева ис- пользуем его обход «сверху — вниз» (разобран ранее). Его модификация связана только с изменениями структуры дерева. Procedure PrintTree (р:pt; level:Integer); Var i:Integer; Begin If pNil Then With рл Do Begin For r.-=l To level Do Write!' '); For i:=l To m Do Write(A[i].key:4); WriteLn; PrintTree(pO,level+1) ; For i:=l To m Do PrintTree (A [ i ]. p, level+1) ; End; End; Основная программа. Begin Assign(Input,'Input.Txt');ReSet(Input); Assign (Output,'Output.Txt');ReWrite (Output); root:=Nil; Solve(root); Close(Output);Close(Input); End. Основная процедура включает ввод данных из файла, изме- нений корня дерева, если это необходимо, и вывод дерева после каждого изменения в результирующий файл. Procedure Solve(Var root:pt); Var q:pt; h:Boolean; и:Item; i,x,NInp:Integer; Begin ReadLn(NInp);(*Количество элементов в файле Input.Txt. *) For i:=l To NInp Do Begin Read(x);(★Считываем элемент. *) Search(x,root,h,u);(*Вставляем в дерево. *) If h Then Begin(^Необходимо изменить корень дерева. *}
Динамические структуры данных q:=roct; New (root); With root'' Do Begin m:=1; pO:=q; A[l]:=u;(*B u запись, которая содержит и необходимое значение указателя. *) End; End; PrintTree(root,1);{*Выводим дерево. *) WriteLn; End; End; Задания для самостоятельной работы 1. В файловой системе каталог файлов организован в виде сба- лансированного дерева. Каждый узел обозначает файл, содер- жащий имя и атрибуты файла, в том числе и дату последнего обращения к файлу. Написать программу, которая обходит дерево и удаляет все файлы, последнее обращение к которым происходило до определенной даты, при этом сбалансирован- ность дерева должна сохраняться. 2. Найти сбалансированноеАВЛ-дерево и короткую последова- тельность удалений, при которых используются все четыре вида балансировки хотя бы по одному разу. 3. Разработать логику и программу удаления элементов из Б-де- рева. 4. 2-3 деревом называется дерево, в котором каждый узел, не яв- ляющийся листом, имеет двух или трех сыновей, а длины всех путей из корня в листья одинаковы. Пример дерева приведен на рисунке. Разработать логику и программу работы (вставку, удаление элементов) с 2-3 деревом. ( 37 L 1:2 ) { 5:6 ) 7 8:9 10
420 Часть четвертая 5. Двоичное дерево поиска называется красно-черным деревом, если оно обладает следующими свойствами: □ каждая вершина либо красная, либо черная; □ каждый лист (Nil) — черный; □ если вершина красная, оба ее потомка черные; □ все пути, идущие вниз от корня к листьям, содержат одинаковое количество черных вершин. Описание узла красно-черного дерева имеет вид: Type pt = '~Node; liode=Record color:-1..1;{*Цвет вершины, -1 —красный, +1 — черный*} key: Integer;{*3начение ключа.*} left,right,parent:pt;{*parent - ссылка на родителя. *} End; Пример красно-черного дерева. Разработать программы вставки и удаления элементов в красно-черное дерево.
Заключение В первой части учебника рассмотрены основные операторы системы программирования Турбо Паскаль. Предметной обла- стью для выбора задач является «Теория чисел». Знание этого раздела математики — необходимая компонента математиче- ской культуры специалиста по информатике. В материалах для чтения приводится краткий обзор развития технологий про- граммирования. Цель — дать представления о сложнейшей от- расли деятельности — программировании и том месте в этом развитии, которому посвящен учебник. Во второй части учебника начато первое знакомство с одно- мерными массивами. Более детально работу с массивами пред- полагается рассмотреть в третьей части. Затем изучаются про- цедуры и функции, методы написания рекурсивных программ. Таким образом создаются предпосылки для формирования струк- турного стиля мышления. Весь дальнейший материал по сим- вольному, строковому, вещественному типам данных и тек- стовым файлам написан с использованием этих элементов структуризации путем последовательного уточнения задачи по технологии «сверху вниз». Предметной областью для выбора задач по-прежнему является «Теория чисел», а также «Комби- наторика» и ряд других разделов математики. В материалах для чтения приводятся сведения по структуре ЭВМ, начальные факты из «Теории вероятностей», а также рассматриваются традиционные темы «Алгоритмы», «Инфор- мация» и «Кодирование». Третья часть учебника, в основном, посвящена работе с мас- сивами. При этом в полной мере используется аппарат проце- дур и функций, рассмотренный во второй части. Достаточно по- дробно рассмотрены задачи сортировки и поиска элементов массива. Эти алгоритмы — лучшая база для освоения темы «массивы». На занятии по технике работы с двумерными мас- сивами используются задачи, являющиеся составными элемен- тами задач на перебор с возвратом. Занятие по комбинированно- му типу данных требуется для изучения проблематики четвертой части учебника «динамические структуры данных». В материале для чтения к первому занятию приводятся основные понятия по схемам оценки времени работы алгорит- мов. Технология обучения работе с массивами дается в учебной литературе обычно в очень компактном виде, поэтому рекомен-
422 Основы программирования довать что-либо затруднительно. Причина — то ли материал считается простым, то ли он не достаточно оценен по своим ди- дактическим возможностям. Автор считает его одним из клю- чевых в системе обучения программированию. По методам же сортировки и поиска, наоборот, огромное ко- личество публикаций. В любой книге по алгоритмам эти мето- ды, так или иначе, рассматривают, и считается обязательным при этом ссылаться на классику. К классике, в первую очередь, относится энциклопедическое издание Д. Кнута (Кнут Д. Искус- ство программирования для ЭВМ. Сортировка и поиск. Т. 3. — М.'.Мир, 1978. — 844 с.). Но если быть честным, то автор не любил работать с этой книгой и в 1978 году, не говоря уже о настоящем времени. Сверхтяжелый язык написания книги и слово MIX вызывают легкий трепет и очередное откладывание этой трехтомной монографии в дальний угол. Затем по попу- лярности идут книги А. Ахо (Ахо А., Хопкрофт Дж., Уль- ман Дж. Построение и анализ вычислительных алгоритмов. — М.:Мир, 1979. — 536 с.), Э. Рейнгольда (Рейнгольд Э., Нивер- гельт Ю., Део Н. Комбинаторные алгоритмы. Теория и практи- ка. — М.: Мир, 1980. — 476 с.) и не так давно появившиеся книга Т. Кормена (Кормен Т., Лейзерсон Ч., Ривест Р. Алго- ритмы. Построение и анализ. — М.: МЦНМО, 1999. — 960 с.), количество ссылок на которую еще «не набрало силу». Книги за- мечательные, каждая со своим взглядом (подходом) на изучае- мый предмет. Ни одну из них нельзя назвать простой, они на- писаны по курсам, читаемым в классических зарубежных университетах. Возьмем, например, последнюю книгу, весьма объемную, но очень неоднозначную (последние главы, особенно по приближенным алгоритмам, видимо, написаны уже на по- следнем дыхании). После глав с математической преамбулой рассматривается сортировка с помощью кучи, использующая понятие дерева и т. д. То есть динамические структуры данных лежат в основе всего последующего изложения. Прекрасно. Но опыт работы показывает, что для того, чтобы «довести» школь- ника, студента до понимания динамических структур, необхо- димо съесть совместно «не один пуд соли». При подготовке занятия по комбинированному типу данных автор использовал одну из лучших книг своей библиотеки как по содержанию, так и по языку написания (Дагене В. А., Гри- гас Г. К., Аугутис К. Ф. 100 задач по программированию. — М.: Просвещение, 1993. — 255 с.). Четвертая часть учебника посвящена динамическим струк- турам данных. При этом в полной мере используется аппарат
Заключение 423 процедур и функций, массивы, т. е. весь предыдущий матери- ал. Материал трудный, но усилия окупаются. Учащиеся в пол- ной мере начинают понимать сложнейший процесс отладки программы, проверки правильности ее работы при различных исходных данных. При написании этой части учебника автор чаще всего загля- дывал в замечательную книгу человека, вклад которого в Compu- ter Science, в образовательную информатику, по мнению автора, просто огромен. Это книга Никлауса Вирта (Вирт Н. Алгоритмы + Структуры данных = Программы. — М.: Мир, 1985. — 406 с.). В книге А. Ахо (Ахо А., Хопкрофт Дж., Ульман Дж. Построение и анализ вычислительных алгоритмов. — М.:Мир, 1979. — 536 с.) этот материал не полный и приведен в весьма своеобразном виде. Работать с ним очень трудно, и попытка изложить его в та- ком же стиле у автора не получилась. В не так давно появившей- ся и красиво изданной книге Т. Кормена (Кормен Т., Лейзер- сон Ч., Ривест Р. Алгоритмы. Построение и анализ. — М.: МЦНМО, 1999. — 960 с.) нет, например, АВЛ-деревьев. Для дополнительной работы можно порекомендовать следу- ющие книги. 1. О системах счисления и представлении чисел в памяти компь- ютера написано прекрасное учебное пособие. (Андреева Е., Фалина И. Информатика: Системы счисления и компьютер- ная арифметика. — М.: Лаборатория Базовых Знаний, 1999. — 256 с.) 2. О математической логике можно почитать во второй главе учебно-справочного издания А. И. Гусевой. (Гусева А. И. Учимся информатике: задачи и методы решения. — М.: «Диа- лог — МИФИ», 1998. — 320 с.) 3. По теории чисел лучше классической книги И. М. Виногра- дова трудно что-либо найти. Но, конечно, читать ее достаточ- но сложно, если особенно у Вас не очень основательная друж- ба с математикой. (Виноградов И. М. Основы теории чисел. — М.: Наука, 1972. — 167 с.) 4. О профессии программиста с большим юмором написана кни- га А. Н. Венц. В ней Вы найдете и экскурсы в историю про- граммирования. (Венц А. Профессия — программист. — Ро- стов н/Д: Изд-во «Феникс», 1999. — 384 с. 5. О технологиях программирования лучше всего читать в клас- сической книге Г. Буча, настольной книге любого программи- ста. (Буч Г. Объектно-ориентированное проектирование с при- мерами применения: Пер. с англ. — М.: Конкорд, 1992. 519 с.)
424 Основы программирования 6. Много полезных примеров из истории программирования можно найти в уникальной книге компьютерного провидца (так его называют) Д. Васкевича. (Васкевич Д. Стратегия кли- ент/сервер. Руководство по выживанию для специалистов по реорганизации бизнеса. — Киев: «Диалектика», 1996. — 384 с.) 7. Для первоначального знакомства с теорией вероятности луч- ше блестящей книги Б. В. Гнеденко и А. Я. Хинчина, выдер- жавшей многочисленные издания, вряд ли что-либо можно найти. (Гнеденко Б. В., Хинчин А. Я. Элементарное введение в теорию вероятностей. — М.: Наука, 1976. — 168 с.) 8. О комбинаторике написана прекрасная книга Наума Яковле- вича Виленкина. Автор очень её любит и не расстается с ней с момента ее выхода. (Виленкин Н. Я. Комбинаторика. — М.: Наука, 1969. — 328 с.) 9. Если не очень сильно обращать внимание на традиционную запись алгоритмов в виде блок-схем, то о них написана очень неплохая книга для школьников Ю. А. Макаренкова и А. А. Столяра. (Макаренков Ю. А., Столяр А. А. Что такое алгоритм? — Мн.: Нар. асвета, 1989. — 127 с.) 10. Интересна для школьников и книга Льва Васильевича Тарасо- ва. В ней можно узнать о том, что такое информация, вероят- ностный подход к её определению. (Тарасов Л. В. Мир, постро- енный на вероятности. — М.: Просвещение, 1984. — 191 с.). 11. Для дальнейшего изучения кодирования лучше всего по- дойдет книга М. Н. Аршинова и Л. Е. Садовского (Арши- нов М. Н. , Садовский Л. Е. Коды и математика (рассказы о кодировании). — М.: Наука, 1983. — 144 с.) К сожалению, рекомендовать книги по Турбо Паскалю очень трудно, несмотря на их огромное количество. Часть из них но- сит описательный характер, т. е. может использоваться только как справочный материал. Другие претендуют на обучение, но как это делать, трудно понять (мало задач, не разработан «стык» задач с изучаемыми конструкциями и возможностями системы программирования). Поэтому позволим себе воздержаться от рекомендаций.