Text
                    БИНОМ

К. Ю. Поляков. Программирование. Python. C++. Часть 1 К. Ю. Поляков. Программирование. Python. C++. Часть 2 К. Ю. Поляков. Программирование. Python. C++. Часть 3 К. Ю. Поляков. Программирование. Python. C++. Часть 4
К. Ю. Поляков ПРОГРАММИРОВАНИЕ Python Часть 2 Учебное пособие для общеобразовательных организаций Москва БИНОМ. Лаборатория знаний 2019
УДК 004.9 ББК 32.97 П54 Поляков К. Ю. П54 Программирование. Python. C++. Часть 2: учебное пособие / К. Ю. Поляков. — М. : БИНОМ. Лаборатория знаний, 2019. — 176 с. : ил. ISBN 978-5-9963-4135-1 Книга представляет собой вторую часть серии учебных пособий по программированию. В отличие от большинства аналогичных изданий, в ней представлены два языка программирования высокого уровня — Python и C++. Основные темы этого пособия — программирование с использова- нием подпрограмм, обработка символьных строк, использование мас- сивов и матриц для хранения большого количества данных. Рассмат- ривается понятие сложности алгоритмов, позволяющее сравнивать их эффективность. После каждого параграфа приводится большое число заданий для самостоятельного выполнения разной сложности и вариантов проек- тных работ. Пособие предназначено для школьников, изучающих программи- рование. УДК 004.9 ББК 32.97 ISBN 978-5-9963-4135-1 © ООО «БИНОМ. Лаборатория знаний», 2019 © Художественное оформление ООО «БИНОМ. Лаборатория знаний», 2019 Все права защищены
ПРЕДИСЛОВИЕ Вы держите в руках вторую часть пособия, в котором изучаются два языка программирования — Python и C++. Главные темы этой кни- ги — подпрограммы, символьные строки и массивы. Большие программы никогда не пишут как одно целое. Их всегда разбивают на части (подпрограммы), которые называются процедурами и функциями. С процедурами мы уже познакомились в первой части пособия) — они выполняют какие-то действия и их можно вызвать по имени. Функции — это ещё один вид подпрограмм, они возвращают результат — число или данные другого типа. В наше время компьютеры занимаются не только сложными вычис- лениями, но и обработкой текстов. В компьютере тексты представлены как символьные строки. Это составной тип данных, потому что они со- стоят из отдельных элементов — символов. Мы изучим методы ввода, вывода и обработки символьных строк, познакомимся со стандартными функциями библиотек языков Python и C++. Массивы — это группы однотипных переменных, которые названы общим именем и расположены в памяти рядом. К элементам массива можно обратиться по их номерам (индексам). Именно массивы позво- ляют составлять короткие программы, которые выполняют обработку огромного количества (сотен тысяч и даже миллионов) ячеек памяти. Двумерные массивы (они называются матрицами) служат для хране- ния в памяти табличных данных. Мы будем использовать массивы для создания собственной игровой программы. Как и в первой части пособия, после каждого параграфа приводятся задания для практических работ и возможные темы проектов. Слож- ные задания обозначены звёздочкой, а особо сложные — двумя звёз- дочками. Дополнительные материалы к пособию, в том числе файлы с про- граммами, можно загрузить с сайта автора: http://kpolyakov.spb.ru/school/pycpp.htm. 3
Предисловие Автор хочет поблагодарить своих коллег за внимательное чтение рукописи этого пособия и полезные критические замечания, которые позволили устранить неточности и существенно улучшить содержание: • И. Р. Дединского, преподавателя кафедры информатики МФТИ; • С. П. Митрофанова, руководителя школы юных программистов «Северная звезда», г. Сургут; • Т. Ф. Хирьянова, преподавателя кафедры информатики МФТИ; • М. Д. Полежаеву, разработчика веб-сайтов.
Глава 1 ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ PYTHON §1 Проектирование программ Ключевые слова'. • постановка задачи • тестирование • техническое задание • документирование • построение модели • внедрение • разработка алгоритма • сопровождение • кодирование • проектирование «сверху вниз» • программный код • проектирование «снизу вверх» • отладка • интерфейс • рефакторинг • реализация Этапы создания программ Все знают, что новые программы для компьютеров разрабатывают про- граммисты. Но это не совсем верно: любая достаточно сложная про- грамма проходит несколько этапов от рождения идеи до выпуска гото- вого продукта, и в этом участвует множество специалистов — команда разработчиков программного обеспечения (фирма, компания). Рассмо- трим кратко эти этапы. 1. Постановка задачи. Сначала определяют задачи, которые должна решать программа, и записывают все требования к ней в виде доку- мента — технического задания. Это очень важный этап, потому что ошибки и неточности в техническом задании могут привести к тому, что разработчики будут решать совершенно другую задачу. 2. Построение модели. Когда задача поставлена, нужно выполнить формализацию — записать все требования на формальном языке, на- пример в виде математических формул. В результате строится модель исходной задачи, в которой чётко определяются все связи между ис- ходными данными и желаемым результатом. 3. Разработка алгоритма и способа представления данных. Любая компьютерная программа служит для обработки данных. Поэтому очень важно определить, как будут представлены данные в памяти компьютера. Способ хранения данных определяет алгоритмы работы с ними: если выбрана неподходящая структура данных, очень сложно написать хо- роший алгоритм обработки. Известная книга швейцарского специали- ста Никлауса Вирта, автора языка Паскаль, так и называется «Алго- ритмы + структуры данных = программы». 5
1 Программирование на языке Python 4. Кодирование. Только теперь, когда выбран способ хранения дан- ных и разработаны алгоритмы для работы с ними, программисты при- ступают к написанию программы. Эта работа называется кодированием, потому что программист кодирует алгоритм — записывает его на язы- ке программирования. Результат его работы — текст программы — часто называют программным кодом или просто кодом. 5. Отладка. Ни один человек не может написать достаточно большую программу без ошибок. Поэтому программистам приходится искать и устранять ошибки в программах. Этот процесс называется отладкой программы. Отладку выполняют непрерывно во время разработки про- граммы, а не после того, как код программы будет полностью написан. Все ошибки можно разделить на две группы: синтаксические и ло- гические. Синтаксические ошибки — несоответствие правилам языка программирования — обнаруживаются транслятором, поэтому найти и исправить их достаточно просто. Сложнее исправлять логические ошибки — ошибки в составлении алгоритма. Из-за логических ошибок программа работает не так, как требуется. Чтобы исправить такую ошибку, программисту приходится внимательно изучить работу программы, иногда даже выполнить вы- числения вручную, без компьютера, и сравнить результаты каждого шага с теми результатами, которые даёт программа. Логические ошибки могут привести к отказу — аварийной ситуации, например к делению на ноль. Часто при отказе операционная система завершает работу программы, и данные могут быть потеряны. Отказы часто называют ошибками времени выполнения (англ, runtime error). Иногда, добившись правильной работы программы, делают рефакто- ринг — улучшают структуру программы так, чтобы она стала понят- нее для человека. Рефакторинг не должен приводить к новым ошибкам. Поэтому изменения вносят поэтапно, маленькими «порциями», проверяя правильность работы программы после каждого шага. 6. Тестирование. Профессиональные программисты чаще всего пишут код одновременно с тестированием — проверкой работы про- граммы. Прежде чем написать очередную часть программы, составля- ют тесты — наборы исходных данных, для которых программа должна выдавать заранее известный (правильный) результат. При любом изме- нении программы её проверяют на всех тестах, добиваясь того, чтобы все они отработали без ошибок. В идеале тесты должны учитывать все возможные варианты вычислений, которые могут встретиться. После того как программист исправит все обнаруженные им ошибки, он передаёт программу на второй этап тестирования, который выпол- няют тестировщики. Их задача — подобрать такие входные данные, при которых программа выдаст неверный результат, и таким образом найти ошибки. Тестирование в компании, которая разрабатывает программу, назы- вается альфа-тестированием. Когда оно завершено, начинается бета- 6
Проектирование программ §1 тестирование (внешнее тестирование). Почти готовая версия програм- мы (так называемая «бета-версия») рассылается некоторым клиентам, а иногда даже распространяется свободно. Цель этого этапа — при- влечь к тестированию множество людей, чтобы они смогли найти как можно больше ошибок в программе перед её официальным выпуском (релизом, от англ, release — выпуск). 7. Документирование — это разработка документации на программу. Этим занимаются технические писатели. Техническая документация описывает, как работает программа, а руководство пользователя содер- жит инструкцию по использованию программы. 8. Внедрение и сопровождение. Когда программа отлажена и доку- ментация по ней готова, продукт нужно передать заказчику. Компа- ния-разработчик берёт на себя сопровождение программы — обучение пользователей, исправление найденных ими ошибок, техническую под- держку (ответы на вопросы). Часто компании выпускают новые версии программ, в которых исправляются ошибки и добавляются новые воз- можности. Методы проектирования программ Современные программы очень сложны, они могут состоять из сотен тысяч и даже миллионов строк. Написать такую программу в одиноч- ку невозможно, поэтому над проектом работают большие команды про- граммистов, целые компании по разработке программного обеспечения. При этом нужно сделать так, чтобы разработчики не мешали друг дру- гу и каждый мог выполнять свою часть работы независимо от других. Для этого решаемую задачу разбивают на части (подзадачи) (рис. 1.1). Рис. 1.1 Решение каждой подзадачи оформляется в виде подпрограммы (вспо- могательного алгоритма). Если нужно, подзадачи разбиваются на более мелкие подзадачи (см. третий уровень дерева на рис. 1.1) и т. д. Такой приём называется последовательным уточнением или проек- тированием «сверху вниз»: от основной задачи к мелким подзадачам и подпрограммам, которые их решают. Существует и другой подход: проектирование «снизу вверх». В этом случае сначала разрабатывают подпрограммы для решения самых про- стых задач, а потом собирают из них подпрограммы для более круп- 7
1 Программирование на языке Python ных задач, как из кубиков. При этом мы строим дерево, показанное на рис. 1.1, снизу вверх, с нижнего уровня. На практике программисты обычно сочетают оба подхода. Проекти- рование программы «в целом» выполняется «сверху вниз», но часто сначала разрабатывают ещё и библиотеку подпрограмм нижнего уровня (например, для работы с графикой или для обмена данными с аппара- турой). В результате проектирования выделяются подзадачи, которые решаются отдельными подпрограммами. Каждая подпрограмма долж- на решать только одну понятную задачу. Хорошо, если её длина будет не больше, чем 20-30 строк (так, чтобы она целиком помещалась на экране монитора). Интерфейс и реализация Программист, работающий в команде, получает персональное зада- ние — разработать и отладить одну или несколько подпрограмм. Он может работать независимо от других, важно только соблюдать прави- ла обмена данными между «его» подпрограммой и остальными подпро- граммами. В техническом задании точно описывается интерфейс — все вход- ные данные и получаемые результаты (в том числе типы всех данных), способ вызова подпрограммы (рис. 1.2). Входные данные Подпрограмма Результаты Рис. 1.2 Всё, что происходит с данными внутри подпрограммы (это называет- ся реализацией), определяет сам программист, и его коллеги могут в это не вникать, занимаясь своей частью работы. Ещё более важно, что всегда можно поменять реализацию, не меняя интерфейса. Программист может полностью переписать подпрограм- му (например, применив более быстрый алгоритм обработки данных), но остальные подпрограммы при этом изменять не придётся, если со- хранился интерфейс, т. е. описание входных и выходных данных не поменялось. Документирование программы К выпуску программы компания-разработчик должна подготовить документацию на программу. Руководство пользователя (это наиболее важная часть документации) обычно описывает: • назначение программы; • формат входных данных; • формат выходных данных; • примеры использования программы. 8
Проектирование программ §1 Для примера составим документацию на простую программу, кото- рая решает квадратные уравнения. Назначение программы: вычисление вещественных корней квадрат- ного уравнения ах2 + Ьх + с = 0. Формат входных данных: значения коэффициентов а, Ь и с вводят- ся с клавиатуры через пробел в одной строке; значение а не должно быть равно нулю. Формат выходных данных: значения вещественных корней выво- дятся на экран через пробел в одной строке; перед значением первого корня выводится текст х1=, перед значением второго корня — текст х2=. Если уравнение не имеет вещественных корней, выводится сооб- щение «Вещественных корней нет.». Примеры использования программы: 1) решение уравнения х2 - 5х + 1 = 0: Введите а, Ь, с: 1-51 х1=4.791288 х2=0.208712 2) решение уравнения х2 + х + 1 = 0: Введите а, Ь, с: 1 1 1 Вещественных корней нет. 3) решение уравнения х + 1 = 0: Введите а, Ь, с: 0 1 1 Это не квадратное уравнение. Выводы • Разработка программного обеспечения состоит из следующих эта- пов: постановка задачи, формализация и построение модели, раз- работка алгоритма и способа представления данных, кодирование, отладка, тестирование, документирование, внедрение и сопрово- ждение. • При использовании метода проектирования «сверху вниз» (метода последовательного уточнения) задача разбивается на подзадачи. • При использовании метода проектирования «снизу вверх» раз- работка программы начинается с решения наиболее мелких под- задач, из которых основная программа затем собирается, как из кубиков. • Интерфейс — это способ обмена данными между подпрограммой и другими подпрограммами, т. е. описание входных и выходных данных подпрограммы. • Реализация — это программный код подпрограммы. Программист всегда может изменить реализацию своей подпрограммы, не меняя её интерфейс. 9
1 Программирование на языке Python Вопросы и задания 1. Почему алгоритмы и способы хранения данных обычно разрабатыва- ются одновременно? 2. Чем отличается тестирование от отладки? 3. Можно ли считать, что программа, успешно прошедшая тестирова- ние, не содержит ошибок? 4. Если программа плохо документирована, к каким последствиям это может привести? 5. Как вы думаете, почему важно сопровождение программы после её сдачи заказчику? 6. Чем различаются два подхода к проектированию программ: «сверху вниз» и «снизу вверх»? 7. Допустим, требуется написать программу, которая загружает изо- бражение из файла, выполняет его обрезку, преобразует из цветного формата в чёрно-белый и сохраняет полученное изображение в дру- гой файл. Выделите подзадачи в этой задаче. 8. Допустим, требуется написать программу для игры с человеком в «крестики-нолики». Выделите подзадачи в этой задаче. 9. Используя как образец документацию на программу решения квад- ратного уравнения, составьте документацию на одну из написанных вами программ и предложите соседу ей воспользоваться. Вместе с ним исправьте описание программы, если это потребуется. §2 Процедуры Ключевые слова: • подпрограмма • процедура • функция • аргумент • параметр • глобальная переменная • локальная переменная • инкапсуляция Подпрограммы: процедуры и функции Подпрограмма — это отдельная часть программы, имеющая имя и решающая отдельную задачу. Подпрограммы позволяют избежать дублирования кода. Если какую- то задачу требуется решить несколько раз в разных местах програм- мы, нужно оформить алгоритм решения этой задачи как подпрограмму и вызывать её по имени каждый раз, когда это требуется. Из подпрограмм составляются библиотеки, некоторые из которых входят в состав языков программирования. Программисты просто при- меняют их, думая только о том, какую работу они выполняют, а не 10
Процедуры §2 о том, какие алгоритмы в них используются. Это экономит время про- граммистов, освобождая их от повторного выполнения работы, которая уже была кем-то сделана раньше. Каждая подпрограмма должна решать одну и только одну задачу. Например, она может вычислять значение функции или выводить дан- ные в файл. Подпрограмму, которая делает и то, и другое, очень слож- но использовать. Достаточно представить себе ситуацию, когда требует- ся только вычислить результат, но не нужно записывать его в файл. Подпрограммы бывают двух типов — процедуры и функции. Подпро- граммы-процедуры выполняют некоторые действия. Например, print в языке Python — это стандартная подпрограмма-процедура, которая выводит данные на экран. В прошлом году мы уже использовали процедуры в графических про- граммах на языках Python и C++. Каждая процедура решала одну под- задачу, из процедур строилась программа для решения основной задачи. Подпрограммы-функции возвращают результат (число, символьную строку и др.). Например, подпрограмма sqrt из модуля math, вычис- ляющая квадратный корень числа, — это функция. В этом параграфе мы научимся писать свои подпрограммы-процеду- ры, а затем (в следующих параграфах) займёмся функциями. Простая процедура Предположим, что в нескольких местах программы требуется выводить на экран строку из 10 знаков «-» (например, для того чтобы отделить два блока результатов друг от друга). Это можно сделать, например, так: print ( "-----------" ) Конечно, можно вставить этот оператор вывода везде, где нуж- но вывести такую строку. Но это решение имеет два недостатка. Во-первых, строка из минусов будет храниться в памяти много раз. Во-вторых, если мы задумаем как-то изменить эту строку (например, заменить знак «-» на «=»), нужно будет искать эти операторы вывода по всей программе. Для таких случаев в языках программирования предусмотрены про- цедуры. Посмотрим на программу с процедурой: def printLine(): print( "-----------" ) printLine() printLine() Многоточием в текстах программ будем обозначать некоторые опера- торы, которые нас пока не интересуют. Сначала в программе расположена процедура, выделенная фоном. Она начинается со служебного слова def (от англ, define — опреде- 11
1 Программирование на языке Python лить). После имени процедуры записаны пустые скобки (чуть далее мы увидим, что они могут быть и непустыми!) и двоеточие. Все команды, входящие в тело процедуры, записываются с отступом (так же, как и команды, входящие в тело цикла или условного опера- тора). Для того чтобы процедура заработала, в основной программе (или в другой процедуре) необходимо её вызвать по имени (не забыв скобки), причём таких вызовов может быть сколько угодно (в нашей програм- ме — два). Процедура должна быть определена к моменту её вызова, т. е. долж- на быть выполнена команда def, которая создаёт объект-процедуру в памяти. Если процедура вызывается из основной программы, то нужно поместить определение процедуры раньше точки вызова. Использование процедур сокращает код, если какие-то операции должны выполняться несколько раз в разных местах программы. Кро- ме того, большую программу всегда разбивают на несколько подпро- грамм для удобства, выделяя этапы сложного алгоритма. Такой подход делает всю программу более понятной и позволяет разделить работу между программистами. Когда процедура написана и тщательно протестирована, можно пере- дать её другим программистам для использования в этом же или дру- гом проекте. Очень важно, чтобы автор процедуры подробно описал, что она делает, её входные и выходные данные. Тем, кто использует готовую процедуру, нужно убедиться, что она выполняет именно то, что нужно в данной задаче. Процедуры с параметрами Теперь представьте себе, что нужно выводить строки из знаков «минус» разной длины (5, 10 и др.). Конечно, можно добавить в про- грамму несколько процедур, например: def printLine5(): print( "------" ) def printLinelO() : print( "-----------" ) Но это некрасивое решение. Дело в том, что обе процедуры выводят знаки «минус» (т. е. выполняют одни и те же действия!), меняется только количество знаков. Если мы научимся управлять процедурой, передавая ей нужную длину цепочки, можно будет заменить все похо- жие процедуры одной. Процедуру printLinelO можно переписать, применив «умножение» строки на число (заменяющее, как и в математике, многократное сло- жение): def printLinelO(): print( "-"*10 ) 12
Эта процедура делает то же самое, что и первый вариант, — выво- дит 10 минусов подряд и переходит на новую строку. Чем будет отличаться процедура, рисующая 5 знаков «минус», от последнего варианта процедуры printLinelO? Только тем, что вместо «множителя» 10 мы поставим число 5. Если мы хотим, чтобы длину строки можно было менять, в про- цедуре вместо числа нужно использовать переменную. И значение этой переменной необходимо как-то передать процедуре. Оформляется это так: def printLine ( n ) : print ( ”-"*n ) Величина n называется параметром процедуры. Теперь круглые скоб- ки в заголовке процедуры не пустые. В них записано имя парамет- ра — специальной переменной, с помощью которой можно управлять работой процедуры. Параметр — это переменная, от значения которой зависит работа подпрограммы. Имена параметров перечисляются в заголовке подпро- граммы. Наша процедура printLine имеет один параметр, обозначенный име- нем п, — это длина строки из минусов. При вызове такой процедуры нужно в скобках передать ей фактичес- кое значение, которое присваивается переменной п внутри процедуры: printLine( 10 ) Такое значение называется аргументом (или фактическим парамет- ром). Аргумент — это значение параметра, которое передаётся подпро- грамме при её вызове. Аргументом может быть не только постоянное значение (число, сим- вол), но также и переменная, и даже арифметическое выражение. Давайте немного улучшим процедуру: сделаем так, чтобы можно было изменять не только длину строки, но и символы, из которых она строится. Для этого добавим в процедуру ещё один параметр, который можно назвать symbol: def printLine ( symbol, n ) : print ( symbol*n ) Имена параметров в заголовке процедуры отделяются запятой. Чем больше параметров у процедуры, тем больше разных задач она может решать, но тем сложнее её понимать и легче сделать ошибку. Поэтому не рекомендуется передавать в процедуру больше 3-4 пара- метров. 13
1 Программирование на языке Python Локальные и глобальные переменные Переменные, которые введены в основной программе, называются гло- бальными (общими). Их могут использовать все подпрограммы (про- цедуры и функции). Часто бывает нужно ввести дополнительные переменные, которые будут использоваться только в подпрограмме. Такие переменные назы- ваются локальными (местными), к ним можно обращаться только вну- три этой подпрограммы, и остальные подпрограммы (а также основная программа) про них ничего не «знают». Такой приём называется инкапсуляцией (от лат. «помещение в кап- сулу») — мы ограничиваем область видимости (область действия) пере- менной только той подпрограммой, где она действительно нужна. Составим программу, которая рисует на экране треугольник из сим- волов высотой п (рис. 1.3). ' о п 00 ООО . 0000 Рис. 1.3 Как видно из рис. 1.3, рисунок строится из п строк, причём в n-й по порядку строке выводится ровно п символов. Процедуру можно напи- сать так: def triangleO( n ): for i in range(1, n+1) : print( "0"*i ) Мы специально построили цикл по переменной так, чтобы значение переменной i (она принимает последовательно все целые значения от 1 до п) при каждом повторении цикла совпадало с длиной очередной цепочки символов «О». Переменная i — это локальная переменная, она введена и использу- ется внутри процедуры. Обращаться к ней вне этой процедуры нельзя. В других процедурах тоже можно создать переменные с таким име- нем, но они будут связаны уже с другими областями памяти, т. е. это будут другие переменные. Локальная переменная создаётся только при вызове процедуры. Как только работа процедуры будет закончена, все локальные переменные удалятся из памяти. Теперь посмотрим на такую процедуру: def show(): print ( i ) В процедуре выводится значение i, но откуда его взять? Транслятор сначала ищёт локальную переменную с таким именем — её нет. Потом начинает искать глобальную переменную: если такая переменная есть, 14
§2 на экран выводится её значение, если нет — будет выдано сообщение об ошибке. Теперь посмотрим на другую процедуру: def showLocal(): i = 2 print( i ) Даже если существует глобальная переменная i, в первой стро- ке этой процедуры будет создана новая локальная переменная i, и её значение (2) появится на экране. Что же делать, если в процедуре необходимо изменить глобальную переменную? Пусть переменная i — глобальная, её значение задано в основной программе: i = 15 Изменим её значение в процедуре. Для этого нужно явно объявить, что она глобальная, используя команду global: def showGlobal () : global i i = 2 print( i ) Эта процедура работает с глобальной переменной i. Она присвоит ей новое значение 2 (это «увидят» все остальные подпрограммы!) и выве- дет его на экран. Ещё раз подчеркнём, что, если мы забудем написать команду global, транслятор, не сомневаясь, создаст в памяти новую локальную переменную и будет с ней работать. Глобальная переменная при этом не изменится. Нужно стараться писать процедуры, которые не обращаются к гло- бальным переменным (как говорят, не имеют побочных эффектов). Ра- бота процедуры в идеале должна зависеть только от переданных значе- ний параметров, тогда можно легко использовать её в другом проекте. К сожалению, в языке Python это не всегда выполнимо. Выводы • Процедура — это вспомогательный алгоритм (подпрограмма), решающий самостоятельную задачу и не возвращающий результат. • Процедура может вызывать другие процедуры. • Локальная переменная — это переменная, объявленная внутри подпрограммы. Другие подпрограммы и основная программа не могут к ней обращаться. • Ограничение области видимости локальных переменных называет- ся инкапсуляцией («помещением в капсулу»). • При завершении работы подпрограммы все локальные переменные удаляются из памяти. 15
1 Программирование на языке Python • Параметр — это переменная, от значения которой зависит работа подпрограммы. Имена параметров перечисляются в заголовке под- программы. • Аргумент — это значение параметра, которое передаётся подпро- грамме при её вызове. • Нужно стремиться к тому, чтобы подпрограммы использовали только значения параметров и свои локальные переменные (но не глобальные данные). Вопросы и задания 1. Определите тип подпрограммы (процедура или функция), которая: а) рисует окружность на экране; б) определяет площадь круга; в) вычисляет значение синуса угла; г) изменяет режим работы двигателя; д) возводит число х в степень у; е) включает двигатель автомобиля; ж) проверяет оставшееся количество бензина в баке; з) измеряет высоту полёта самолёта. 2. Что произойдёт, если вызвать процедуру, но не включить её текст в программу? Проверьте этот вариант с помощью компьютера. 3. Что произойдёт, если включить текст процедуры в программу, но не вызывать её? Проверьте этот вариант с помощью компьютера. 4. Что будет выведено на экран при выполнении фрагмента програм- мы, где используется процедура printLine с одним параметром (из параграфа)? printLine( 7 ) printLine( 5 ) printLine( 3 ) 5. Для тестирования процедуры printLine Иван-царевич хочет напи- сать небольшую программу, в которой длина линии вводится с кла- виатуры. Где нужно поместить оператор ввода — в процедуре или в основной программе? Объясните ваш выбор. 6. Что будет выведено на экран при выполнении фрагмента програм- мы, где используется процедура printLine с двумя параметрами (из параграфа)? printLine( 10 ); printLine( 7 ); printLine( "о", 5 ); 16
Рекурсия §3 7. Напишите процедуру с параметром п, которая выводит квадрат раз- мером п х п из символов: 'жжжж „ жжжж п- жжжж I жжжж 8. Напишите процедуру кроной высотой п: с параметром п, которая выводит ёлочку с о ООО 00000 0000000 м 9. Напишите процедуру, которая принимает два параметра — W и Н, — и рисует на экране рамку из точек, ширина которой равна W, а высота — Н. Например, для W = 6 и Н = 4 программа должна вывести рисунок: 10. Напишите процедуру, которая выводит на экран в столбик все цифры переданного ей числа, начиная с последней. 11. Напишите процедуру, которая выводит на экран все делители переданного ей числа (в одну строку через пробел). *12. Напишите процедуру, которая выводит на экран в столбик все цифры переданного ей числа, начиная с первой. *13. Напишите процедуру, которая выводит на экран запись переданно- го ей числа в римской системе счисления. Интересные сайты interactivepython.org — «Алгоритмы и структуры данных с исполь- зованием Python» (бесплатная книга с интерактивным тренажё- ром) informatics.mccme.ru — сайт для подготовки к олимпиадам по информатике с автоматической проверкой решений. §3 Рекурсия Ключевые слова'. • рекурсия • условие остановки • базовые объекты • фрактал • рекурсивная процедура 17
Программирование на языке Python Что такое рекурсия? Рекурсией в математике называется определение объекта через такой же объект (или объекты), но с другими параметрами. Например, последовательность Фибоначчи — это последовательность натуральных чисел, в котором каждое следующее число равно сумме двух предыдущих. Обозначим n-е число Фибоначчи символами Fn. Тог- да из приведённого определения получается, что Fn = Fn_r + Fn_2. На- лицо рекурсия: число Фибоначчи определено через два других числа Фибоначчи с меньшими номерами. Как же вычислить эти числа? Простые рассуждения показывают, что одной формулы недостаточно. Действительно, вычислим третье число Фибоначчи, F3. По общей фор- муле оно равно F3 = F2 4- Fv Но мы не знаем ни Flt ни Их можно найти по той же общей формуле Р = Р 4- Р Р = Р 4- Р но Fq и F_x тоже неизвестны! Выход в том, чтобы задать первые два числа последовательности. Например, так: Fx = 1, F2 = 1. Теперь мы можем по формуле последо- вательно вычислить все числа Фибоначчи для п > 2: F3 = F2 + Fr = 1 4- 1 = 2, F4 = F3 4- F2 = 2 4- 1 = 3, Итак, чтобы определить рекурсивный объект, необходимо задать: 1) правило, по которому новый объект строится из уже известных объ- ектов; 2) базовые объекты — один (или несколько) начальных объектов в последовательности. Ханойские башни Согласно легенде, конец света наступит тогда, когда монахи Велико- го храма города Бенарас смогут переложить 64 диска разного диа- метра с одного стержня на другой. Вначале все диски нанизаны на первый стержень (рис. 1.4). За один раз можно перекладывать только один диск, причём разрешается класть только меньший диск на боль- ший, но не наоборот. Есть также и третий стержень, который можно использовать в качестве вспомогательного. 18
Рекурсия §3 Решить задачу для двух, трёх и даже четырёх дисков довольно про- сто. Проблема в том, чтобы составить общий алгоритм решения для любого числа дисков. Пусть нужно перенести п дисков со стержня 1 на стержень 3. Пред- ставим себе, что мы как-то смогли перенести п — 1 дисков на вспомога- тельный стержень 2. Тогда остаётся перенести самый большой диск на стержень 3, а затем п — 1 меньших дисков со вспомогательного стерж- ня 2 на стержень 3, и задача будет решена. Получается такой алго- ритм: # перенести п-1 дисков с 1 на 2 # 1 -> 3 # перенести п-1 дисков с 2 на 3 Здесь запись 1->3 обозначает «перенести один диск со стержня 1 на стержень 3». Процедура перенести (которую нам предстоит написать) принимает 3 параметра: число дисков, номера начального и конечного стержней. Таким образом, мы свели задачу переноса п дисков к двум задачам переноса п -1 дисков и одному элементарному действию — переносу одного диска. Процедура получает номера начального и конечного стержней, а для решения нужно ещё знать номер промежуточного стержня. Его легко определить по известным данным. Заметим, что сумма номеров всех стержней равна 1 + 2 + 3 = 6. Если два из этих номеров известны, то найти номер третьего (вспомогательного) легко: нужно вычесть из 6 сумму двух известных номеров: р = 6 - k - m Получается такая (пока не совсем верная) процедура: def Hanoi ( n, k, m ) : p = 6 - k - m Hanoi ( n-1, k, p ) print( k, m ) Hanoi( n-1, p, m ) Эта процедура вызывает сама себя, но с другими аргументами. Такая процедура называется рекурсивной. Рекурсивная подпрограмма вызывает сама себя напрямую или через другие подпрограммы. О Основная программа для решения задачи с четырьмя дисками будет содержать всего одну строку (перенести 4 диска со стержня 1 на стер- жень 3): Hanoi ( 4, 1, 3 ) Если запустить такую программу, появится сообщение об ошибке — превышено максимальное число рекурсивных вызовов. В чём же дело? 19
1 Программирование на языке Python Вспомните, что определение рекурсивного объекта состоит из двух частей. Во второй части определяются базовые объекты (например, первые два числа Фибоначчи), именно эта часть «отвечает» за останов- ку рекурсии в программе. Пока мы не определили такое условие оста- новки, поэтому процедура бесконечно вызывает сама себя (это можно проверить, выполняя программу по шагам). Заметим, что при каждом вызове в памяти размещаются три аргумента процедуры и локальная переменная р. Поэтому программа может израсходовать всю доступную память, отведённую под локальные переменные. Когда же остановить рекурсию? Очевидно, что, если нужно перело- жить 0 дисков, задача уже решена, ничего перекладывать не надо. Это и есть условие окончания рекурсии: если значение параметра п, пере- данное процедуре, равно 0, нужно выйти из процедуры без каких-ли- бо действий. Для этого в языке Python используется оператор return (в переводе с английского — возврат). Правильная версия процедуры выглядит так: def Hanoi( n, k, m ): if n <= 0: return p = 6 - k - m Hanoi ( n-1, k, p ) print( k, m ) Hanoi( n-1, p, m ) Условие n <= 0 в условном операторе защищает программу от бесконеч- ной рекурсии при ошибочном входном значении п < 0. Чтобы переложить N дисков, нужно выполнить 2N - 1 перекладыва- ний. Для N = 64 это число равно 18 446 744 073 709 551 615. Если бы монахи, работая день и ночь, каждую секунду перемещали один диск, им бы потребовалось 580 миллиардов лет. Пример Составим процедуру, которая выводит на экран двоичную запись натурального числа. Поскольку число, которое нужно обработать, будет меняться, это должна быть процедура с параметром: def printBin( n ) : Вспомним алгоритм перевода числа в двоичную систему: нужно делить число на 2, каждый раз выписывая остаток от деления, пока не получится 0. На языке Python этот алгоритм можно записать так: while п != 0: print( п % 2, end="" ) n = п // 2 20
Рекурсия §3 Напомним, что именованный параметр end, равный пустой строке, отключает переход на новую строку в конце работы функции print (иначе все цифры будут выведены в столбик). Если выполнить эту программу, например для числа 6, вручную (или с помощью транслятора Python) мы получим результат 011, хотя б = 1Ю2. Проблема в том, что первой мы получаем последнюю циф- ру двоичной записи, поэтому остатки выводятся в обратном порядке (не так, как нужно!). Есть разные способы решения этой задачи, которые сводятся к тому, чтобы запоминать остатки от деления (например, в символьной стро- ке) и затем, когда результат будет полностью построен, вывести его на экран. Однако можно применить ещё один красивый подход, основан- ный на следующей идее: чтобы вывести двоичную запись числа п, нужно сначала вывести двоичную запись числа п // 2, а затем — его последнюю двоичную цифру, которая равна п % 2. Что же получилось? Прочитайте ещё раз фразу, выделенную курси- вом в предыдущем абзаце. Выходит, что решение задачи для числа п сводится к решению той же самой задачи для меньшего числа, п // 2, и ещё одному простому действию — выводу остатка. Это и есть рекур- сия. Такой алгоритм очень просто программируется: def printBin ( n ): printBin ( n // 2 ) print ( n % 2, end="" ) получилось так, что процедура printBin вызывает сама себя, т. е. она рекурсивная. Проверим, используя ручную прокрутку, как работает приведён- ная процедура printBin. Представим себе, что мы передали процеду- ре число 2. Сначала она вызывает сама себя для значения 2 // 2 = 1, затем — для значения 1 // 2 = 0, и потом ещё бесконечно много раз для нуля. Такие вызовы никогда не закончатся. Чтобы этого не про- изошло, нужно выйти из процедуры (и закончить эти вложенные вы- зовы), когда значение параметра станет равно нулю: def printBin( n ): if n == 0: return printBin( n // 2 ) print ( n % 2, end="" ) Убедимся, что теперь процедура остановится при любом заданном натуральном числе. Действительно, при каждом вложенном вызове значение параметра делится на 2. В результате когда-нибудь оно обя- зательно станет равно нулю, и тогда вложенные вызовы закончатся. 21
1 Программирование на языке Python Фракталы Говоря о рекурсии, нельзя не вспомнить про фракталы. Так в мате- матике называют геометрические фигуры, обладающие самоподобием. Это значит, что они составлены из фигур меньшего размера, каждая из которых подобна целой фигуре. На рисунке 1.5 показан треуголь- ник Серпинского — один из первых фракталов, который предложил в 1915 году польский математик В. Серпинский. Равносторонний треугольник делится на четыре равных треугольни- ка меньшего размера (рис. 1.5, а). Затем каждый из полученных тре- угольников, кроме центрального, снова делится на четыре ещё более мелких треугольника и т. д. На рисунке 1.5, б показан треугольник Серпинского с тремя уровнями деления. Давайте построим более простую фигуру, состоящую из кругов. Назовём её «квадрокруг» и определим так: 1) квадрокруг уровня ноль — это «пустая» фигура; 2) квадрокруг уровня п размера R — это окружность радиуса R и четыре квадрокруга уровня и - 1 размера R / 2, центры которых рас- положены в крайних верхней, нижней, левой и правой точках этой окружности. На рисунке 1.6 показаны квадрокруги уровней 1, 2 и 3. 22
Рекурсия §3 Будем использовать модуль graph, который позволяет строить раз- личные фигуры в отдельном окне с помощью простых команд1). Про- грамма имеет следующий вид: from graph import * def quadroCircle( ... ) : ... # тело процедуры quadroCircle( ... ) run () После подключения всех функций модуля graph записана процеду- ра (многоточия обозначают пропуски, которые мы ещё должны запол- нить). Основная программа состоит из одной строки — вызова проце- дуры quadroCircle (не считая команды запуска run). Теперь нужно определить параметры процедуры. Очевидно, что необходимо задать координаты центра основной окружности (обозначим их именами xCenter и yCenter) и её радиус R. Кроме того, требуется задать уровень квадрокруга, назовём этот параметр level. Процедура может выглядеть так: def quadroCircle( xCenter, yCenter, R, level ): if level < 1: return circle ( xCenter, yCenter, R ) nextR = R // 2 quadroCircle( xCenter-R, yCenter, nextR, level-1 ) quadroCircle( xCenter+R, yCenter, nextR, level-1 ) quadroCircle( xCenter, yCenter-R, nextR, level-1 ) quadroCircle( xCenter, yCenter+R, nextR, level-1 ) Простейший квадрокруг имеет уровень 1, поэтому в первой строке записано условие окончания рекурсии: если процедуре передано значе- ние level < 1, ничего делать не нужно. Процедура строит окружность (основу квадрокруга) и вычисляет раз- мер следующего квадрокруга в локальной переменной nextR: по опре- делению он в два раза меньше текущего. Последние четыре строки процедуры — это рекурсивные вызовы: про- цедура вызывает сама себя. Координаты центров меньших четырёх фи- гур уровня п - 1 — это крайние точки окружности (сначала — левая, затем правая, верхняя и нижняя), а размер этих фигур равен nextR. Рисование квадрокруга — это один вызов процедуры. Например, команда quadroCircle( 200, 200, 100, 3 ) нарисует квадрокруг уровня 3 размера 100 с центром в точке (200, 200). Модуль graph можно скачать с сайта автора http://kpolyakov.spb.ru/school/ probook/python.htm. 23
1 Программирование на языке Python Выводы • Рекурсия — это определение объекта через такой же объект (или объекты), но с другими параметрами. • Чтобы определить рекурсивный объект, необходимо задать: 1) правило, по которому новый объект строится из уже извест- ных объектов; 2) базовые объекты — один (или несколько) начальных объек- тов. • Рекурсивная подпрограмма вызывает сама себя напрямую или через другие подпрограммы. • Фрактал — это фигура, обладающая самоподобием (составленная из меньших фигур того же типа). Фрактал можно нарисовать с помощью рекурсивной процедуры. Вопросы и задания 1. Используя дополнительные источники, узнайте, как связаны числа Фибоначчи с кролиководством. 2. Сформулируйте рекурсивный алгоритм вывода цепочки одинаковых символов. Напишите рекурсивную процедуру. Попробуйте приду- мать два варианта решения. 3. Напишите рекурсивную процедуру для перевода числа в восьмерич- ную систему счисления. 4. Напишите рекурсивную процедуру для перевода числа в любую систему счисления с основанием от 2 до 9. * 5. Постройте фрактал — кривую Коха. Информацию об этой кривой найдите в дополнительных источниках. * 6. Постройте фрактал — кривую Гильберта. Информацию об этой кри- вой найдите в дополнительных источниках. * 7. Постройте фрактал — кривую Серпинского. Информацию об этой кривой найдите в дополнительных источниках. 8. Проект. Постройте один из следующих фракталов. 24
Функции §4 г) в-я о-я д) г-а p-а е) 9. Придумайте свой фрактал и напишите рекурсивную процедуру для его построения. §4 Функции Ключевые слова: • функция • вызов функции • параметры • рекурсивная функция Что такое функция? Представьте себе, что вы заказываете товар с доставкой по телефону или в интернет-магазине. Если говорить на языке программистов, вы вызываете подпрограмму. Но, в отличие от процедуры, эта подпро- грамма не только выполняет какие-то действия, но и возвращает ре- зультат — товар, который вам привозит курьер. Такие подпрограммы называются функциями. 25
Программирование на языке Python Функция — это подпрограмма, которая возвращает результат (число, символьную строку и др.). Построим функцию, которая возвращает среднее арифметическое двух целых чисел. Функция принимает два параметра — исходные целые числа, — и возвращает результат — вещественное число: def average( а, b ): avg = (а + Ь) /2 return avg Заголовок функции ничем не отличается от заголовка процедуры. В языках Python и C++ (в отличие, например, от Паскаля) процеду- ры — это функции особого типа, не возвращающие никакого резуль- тата. Функция возвращает результат, записанный после специального опе- ратора return. В нашем примере функция average (по-английски — среднее) возвращает результат — значение локальной переменной avg. Если вызвать функцию так же, как и процедуру: average ( 5, 9 ) её значение потеряется. Но его можно сохранить в переменной: sred = average( 5, 9 ) В операторе присваивания вместо вызова функции транслятор «под- ставляет» результат вызова — то значение, которое эта функция вер- нёт. Поэтому предыдущий оператор равносилен такому: sred = 7 Результат функции можно сразу вывести на экран: print ( average ( 4, 8 ) ) Функции можно передавать не только постоянные аргументы (числа, символы, строки), но также значения переменных и арифметических выражений: а = 5 b = 7 sred = average( a, b + 8 ) В этом случае первый аргумент функции будет равен 5, а второй — 15. Наша функция average возвращает вещественное число, поэтому вызовы этой функции можно применять везде, где можно использо- вать вещественное число, в том числе в арифметических выражениях, условных операторах и циклах. Например: с = 2*average( х, у ) + z 26
Функции §4 if average ( a, b ) > 4: print( "Свистать всех наверх!" ) while average( a, b ) < x: a += 1 Примеры функций Задача 1. Составить функцию, которая возвращает наибольшее из двух целых чисел. Алгоритм определения наибольшего из двух чисел вы уже знаете. Остаётся только «завернуть» его в функцию. Например, такХ): def max2(а, b ): if а > Ь: maximum = а else: maximum = b return maximum Одна функция может вызывать другую. Например, можно составить функцию тахЗ, которая возвращает наибольшее из трёх чисел, исполь- зуя готовую функцию max2: def тахЗ( а, Ь, с ): return тах2( тах2( а, b ), с ) Задача 2. Составить функцию, которая вычисляет сумму цифр натурального числа. Последняя цифра — это остаток от деления числа на 10 (результат операции %). Для того чтобы «удалить» последнюю цифру числа, его нужно разделить на 10 без остатка (выполнить операцию //). Для решения задачи при каждом повторении цикла мы «отрезаем» от числа последнюю цифру, добавляем её значение к сумме и затем удаляем её из числа. Цикл заканчивается, когда все цифры удалены и осталось нулевое значение: def sumDigits ( n ) : summa = О while n != 0: digit = n % 10 summa += digit n = n // 10 return summa В языке Python есть встроенные функции max и min, которые вычисляют максимальное и минимальное значения. Теперь вы понимаете, как они устроены. 27
1 Программирование на языке Python Логические функции Программисты часто используют логические функции, возвращаю- щие логические значения (True/False, «да»/«нет», «истина»/«ложь»). Такие функции полезны для того, чтобы определять, успешно ли выполнена задача или обладают ли данные каким-то свойством. Мы напишем функцию isEven, которая определяет чётность числа, т. е. возвращает значение True («да»), если число-параметр чётное, и False («нет»), если нечётное: def isEven( n ) : return ( n % 2 == 0 ) В правой части оператора return записано условие: результатом функции будет True («да»), если условие истинно, и False («нет»), если оно ложно. Можно было записать то же самое иначе: if п % 2 == 0: return True else: return False; но эта запись более длинная, и опытные программисты так не делают. Результат, который возвращает логическая функция, можно использо- вать во всех условиях как обычное логическое значение. Например, так: if isEven( а ): half = а // 2 или так: count = 0 while isEven( х ): х = х // 2 count += 1 Рекурсивные функции Вы уже знакомы с рекурсивными процедурами, которые вызывают сами себя. Функции тоже могут быть рекурсивными, в некоторых слу- чаях это позволяет записать решение задачи проще и понятнее. Вернёмся к задаче вычисления суммы цифр числа. Можно сформу- лировать алгоритм её решения так: сумма цифр числа N равна значению последней цифры (она равна N % 10) плюс сумма цифр числа N // 10, полученного отбрасыва- нием последней цифры. Этот алгоритм можно записать по шагам: Вход: натуральное число п Шаг 1. digit = п % 10 Шаг 2. и1 = п // 10 28
Функции §4 Шаг 3. suml = сумма цифр числа nl Шаг 4. summa = suml + digit Результат: summa Итак, для того чтобы найти сумму цифр числа, нужно сложить его последнюю цифру и сумму цифр другого числа, т. е. выполнить тот же самый алгоритм, только с другими исходными данными (эта стро- ка в записи алгоритма выделена фоном). Получился рекурсивный алго- ритм, в программе его можно записать в виде рекурсивной функции: def sumDigRec( n ): if n == 0: return 0 digit = n % 10; suml = sumDigRec( n // 10 ) return suml + digit В рекурсивном варианте функции исчез цикл, поэтому можно сде- лать вывод: рекурсия может заменить цикл. Верно и обратное: любую рекурсивную функцию можно записать без рекурсии, с помощью цик- лов. Решение с помощью цикла (оно называется итерационным, от слова «итерация» — повторение) обычно работает быстрее, чем рекур- сивное, и требует меньше памяти. Однако рекурсивное решение очень часто получается короче и проще для понимания1*. Выводы • Функция — это подпрограмма, которая возвращает результат (чис- ло, символьную строку и др.). • Вызов функции можно использовать в арифметических выражени- ях и условиях так же, как и переменную того типа, который воз- вращает функция. • В теле функции можно вызывать другие функции и процедуры. • Логическая функция возвращает логическое значение (True/False, «истина»/«ложь», «да»/«нет»). • Рекурсивная функция — это функция, которая вызывает сама себя, напрямую или через другие процедуры и функции. Вопросы и задания 1. Определите, какие распоряжения начальника можно считать вызо- вом процедуры, а какие — вызовом функции. а) «Проводите Ивана Ивановича!» б) «Принесите, пожалуйста, кофе!» в) «Подготовьте годовой отчёт!» г) «Постройте конуру для собаки!» В некоторых языках программирования, например, в Haskell, нет «обычных» циклов, вместо них используют рекурсию. 29
1 Программирование на языке Python 2. Как по тексту функции определить, что она возвращает? 3. Какой недостаток, на ваш взгляд, имеет эта функция? def sqr( х ) : squaredX = х*х print ( squaredX ) return squaredX 4. Что будет выведено на экран в результате работы фрагмента про- граммы (используется функция average из параграфа)? sred = average( 3, 5 ) print ( sred + average( 7, 11 ) ) 5. Найдите любые значения переменных а, b и х, при которых в результате работы фрагмента программы будет выведено сообщение Да! Это круто! (используется функция average из параграфа). if average( a, b ) > х: print("Да! Это круто!") 6. Найдите любые начальные значения переменных а, Ь и х, при которых этот цикл выполнится ровно четыре раза (используется функция average из параграфа): while average( а, b ) < х - 1: Ь += 1 7. Постройте функцию шах4, которая вычисляет наибольшее из четырёх чисел, используя приведённую в параграфе функцию тах2. Приведите два варианта решения задачи и сравните их. 8. Запишите в развёрнутой форме оператор: return (а > Ь + с) 9. Запишите в краткой форме оператор: if а + Ь > 10: return False else: return True 10. Найдите любые значения переменных а и b, при которых в резуль- тате работы фрагмента программы будет выведено сообщение «Сни- женный тариф!» (используется функция isEven из параграфа). if isEven( а + 3 * b ): print( "Сниженный тариф!" ) 11. Найдите любое значение переменной а, при котором цикл выпол- нится ровно 4 раза (используется функция isEven из параграфа). while isEven( а ) and а > 5: а = а // 2 12. Напишите функцию, которая возвращает последнюю цифру деся- тичной записи числа. 13. Напишите функцию, которая определяет минимальное из пяти чисел. 30
Функции §4 14. Напишите функцию, которая вычисляет среднее арифметическое пяти целых чисел. 15. Напишите функцию numberOfDigits, которая вычисляет количест- во цифр числа. 16. Напишите функцию numberOfDivisors, которая возвращает коли- чество делителей натурального числа. 17. Напишите функцию numReverse, которая строит «перевёрнутое» трёхзначное число, например из числа 123 получается 321, а из числа 210 — 12. 18. Напишите функцию numberOfOnes, которая вычисляет количество единиц в двоичной записи числа. 19. Напишите функцию, которая возвращает количество цифр в вось- меричной записи числа. Число вводится в десятичной системе счисления. *20. На соревнованиях выступление спортсмена оценивают пять экспер- тов, каждый из них выставляет оценку в баллах (целое число от 0 до 100). Для получения итоговой оценки лучшая и худшая из оценок экспертов отбрасываются, а для оставшихся трёх находится среднее арифметическое. Напишите функцию, которая принимает пять оценок экспертов и возвращает итоговую оценку спортсмена. 21. Изучите текст функции sumDigRec (из параграфа) и ответьте на вопросы. а) Зачем добавлен условный оператор в начале программы? б) Что произойдёт, если удалить этот условный оператор? в) Как можно доказать, что для любого целого числа рекурсия обязательно закончится? 22. Сравните два решения задачи о сумме цифр числа: с помощью рекурсии и с помощью цикла. Какое из них вам больше нравит- ся? Обсудите этот вопрос в классе. 23. Напишите логическую функцию isByte, которая возвращает зна- чение True («да»), если переданное ей число помещается в 8-бит- ную ячейку памяти (вспомните, какое минимальное и максималь- ное числа можно записать с помощью 8 бит). 24. Напишите логическую функцию pointlnRect, которая возвраща- ет True («да»), если точка с заданными координатами находится внутри прямоугольника, для которого заданы координаты верхнего левого и правого нижнего углов. Стороны прямоугольника парал- лельны осям координат. *25. Напишите логическую функцию pointlnTriangle, которая возвра- щает True («да»), если точка с заданными координатами находится внутри треугольника, заданного координатами трёх своих вершин. Интересный сайт ru.stackoverflow.com — сайт вопросов и ответов для программистов 31
1 Программирование на языке Python §5 Символьные строки Ключевые слова' • символьная строка • подстрока • срез • длина строки • сцепление строк • удаление символов • вставка символов • поиск подстроки • преобразование типов Что такое символьная строка? Если в середине XX века первые компьютеры создавались, прежде всего, для выполнения сложных математических расчётов (например, для расчёта траекторий полёта ракет и снарядов), то сейчас они очень часто обрабатывают текстовую (символьную) информацию. Символьная строка — это последовательность символов, которая рас- сматривается как единый объект. В языке Python строка относится к типу str (от английского string — строка). Проверить это можно сле- дующей программой: s = "Питон" print( type(s) ) которая выведет <class 'str’> Новое значение записывается в строку с помощью оператора присва- ивания (как выше) или функции ввода с клавиатуры: s = input () Встроенная функция 1еп определяет длину строки — количество символов в ней. Вот так в переменную п записывается длина строки s: n = len ( s ) Сравнение строк Строки можно сравнивать между собой так же, как числа. Например, можно проверить равенство двух строк: password = input ( "Введите пароль:" ) if password == "sEzAm": print ( "Слушаюсь и повинуюсь!" ) else: print ( "No pasaran!" ) Можно также определить, какая из двух строк больше, какая — меньше. Если строки состоят только из русских или только из латин- 32
§5 ских букв, то меньше будет та строка, которая идёт раньше в алфа- витном списке. Например, слово «паровоз» будет «меньше», чем слово «пароход»: они различаются в пятой букве и «в» < «х». Это можно проверить экспериментально, например с помощью такой программы: si = "паровоз" s2 = "пароход" if si < s2: print ( si, s2 ) elif si == s2: print ( si, " = ", s2 ) else: print( si, , s2 ) Но откуда компьютер «знает», что такое алфавитный порядок? При сравнении используются коды символов. В современных кодировках1) и русские, и английские буквы расположены в алфавитном порядке, т. е. код буквы «в» меньше, чем код буквы «х». Сложение и умножение Оператор «+» используется для «сложения» (объединения, сцепления) строк. Эта операция иногда называется конкатенацией. Например: hello = "Привет" name = "Пантелеймон" greeting = hello + ", " + name + "!" В результате в переменную greeting будет записано значение "Привет, Пантелеймон!" В язык Python введена операция умножения строки на число: она заменяет многократное сложение. Например, s = "Уа! Уа! Уа! Уа! Уа! Уа! Уа! Уа! Уа! Уа! " можно заменить на s = "Уа! "*10 или s = 10*"Уа! " Обращение к символам В Python каждый символ строки имеет свой номер (индекс), причём нумерация, как и во многих других языках программирования (C++, Java), всегда начинается с нуля. х) Раньше использовалась кодировка K0I8-R, в которой русские буквы стояли не по алфавиту. 33
1 Программирование на языке Python 0 1 2 3 4 5 6 п р и в е т ! -7 -6 -5 -4 -3 -2 -1 п р и в е т ! Индекс можно понимать как смещение символа от начала строки. Первый по счёту символ имеет нулевое смещение (находится в самом начале строки), поэтому у него индекс 0: индексы 0 1 2 3 4 5 6 hello = " П ривет !" К любому символу можно обратиться по индексу, записав индекс в квадратных скобках после имени строки: print( hello[1] ) # р print( hello[5]thello[2]+"к" ) # тик В языке Python можно указывать отрицательные индексы. Это зна- чит, что отсчёт ведётся от конца строки, так что символ hello [-1] — это последний символ строки hello: индексы —7 —6 -5 -4 -3 —2 —1 hello = " П р и в е т !" Чтобы рассчитать «нормальную» позицию символа в строке, к отри- цательному индексу нужно добавить длину строки. Например: hello[-l] = hello[len (hello)-1] = hello[6] Предыдущую программу можно было переписать, используя отрица- тельные индексы: print ( hello [-6] ) # р print ( hello[-2]+hello[-5]+"к" ) # тик Если указать неправильный индекс, произойдёт ошибка — выход за границы строки, и программа завершится аварийно. Для строки дли- ной семь правильные индексы — от -7 до 6. Перебор всех символов Поскольку к символу можно обращаться по индексу, для перебора всех символов можно использовать цикл по переменной, которая будет принимать все возможные значения индексов. Пусть нужно вывести в столбик коды всех символов строки с име- нем s. Её длину можно найти с помощью функции len, индекс первого символа равен 0, а индекс последнего равен len(s)-l. Таким образом, все допустимые индексы — это последовательность, которую строит вы- зов функции range (len (s) ). Перебор можно выполнить так: for i in range(len (s)) : print ( s[i], ord(s[i]) ) В каждой строке сначала выводится сам символ, а потом — его код, который возвращает встроенная функция ord. 34
Символьные строки §5 В языке Python существует ещё один удобный способ перебора всех элементов строки: for с in s: print ( с, ord(с) ) Заголовок такого цикла можно «прочитать» так: «для всех с, вхо- дящих в состав s». Это означает, что все символы строки s, с первого до последнего, по очереди оказываются в переменной с, и в теле цик- ла нам остаётся вывести код символа с. В отличие от большинства современных языков программирования, в Python нельзя изменить символьную строку, поскольку строка — это неизменяемый объект. Это значит, что оператор присваивания s[5]="a" не сработает — будет выдано сообщение об ошибке. Тем не менее можно составить из символов существующей строки новую строку и внести в неё нужные изменения. Приведём полную программу, которая вводит строку с клавиатуры, заменяет в ней все буквы «э» на буквы «е» и выводит полученную строку на экранХ). s = input ( "Введите строку: " ) sNew = "" for с in s: if c == "э": с = "e" sNew += c print ( sNew ) Здесь в цикле for перебираются все символы, входящие в строку s. В теле цикла проверяем значение переменной с (это очередной символ исходной строки): если оно совпадает с буквой «э», то заменяем его на букву «е» и добавляем в конец новой строки sNew с помощью операто- ра сложения. Нужно отметить, что показанный здесь способ (многократное «сло- жение» строк) работает очень медленно. В практических задачах, где требуется замена символов, лучше использовать встроенную функцию replace, о которой пойдёт речь дальше. Срезы Для того чтобы выделить часть строки (подстроку), в языке Python применяется операция получения среза (англ, slicing). Например, s[3:8] означает «символы строки s с 3-го по 7-й» (то есть до 8-го, не включая его). Следующий фрагмент копирует в строку sMiddle симво- лы строки s с 3-го по 7-й (всего 5 символов): s = "0123456789" sMiddle = s[3:8] # sMiddle = "34567" И. Ильф и Е. Петров в романе «Золотой телёнок» описывают пишущую машинку «с турецким акцентом», в которой не хватало буквы «е», и её пришлось заме- нить буквой «э». 35
1 Программирование на языке Python Можно использовать и отрицательные индексы — в этом случае отсчёт идёт с конца строки: sMiddle = s[-7:-2] # sMiddle = "34567" Для получения «нормальной» позиции символа в строке к отрица- тельному значению добавляется длина строки. Например, второй ин- декс «-2» можно заменить на len (sMiddle) -2. Это означает, что последние два символа не входят в срез. Если первый индекс не указан, считается, что он равен нулю (берём начало строки), а если не указан второй индекс, то в срез включаются все символы до конца строки. Например: s = "0123456789" sFirst = s[:4] # sFirst = "0123" sLast = s[-4:] # sLast = "6789" Срезы позволяют легко выполнить реверс строки (развернуть её наоборот): sReversed = s[::-1] Так как начальный и конечный индексы элементов строки не ука- заны, задействована вся строка. Число -1 означает шаг изменения ин- декса и говорит о том, что все символы перебираются в обратном по- рядке. Удаление и вставка Для удаления части строки нужно составить новую строку, объединив части исходной строки до и после удаляемого участка: s = "0123456789" sEnds = s[:3] + s[9:] # sEnd = "0129" Срез s [: 3] означает «от начала строки до символа s [3], не включая его», а запись s [9: ] — «все символы, начиная с s[9] до конца стро- ки». Таким образом, в переменной sEnds остаётся значение "012 9". С помощью срезов и сцепления строк можно также вставить новый фрагмент внутрь строки: s = "0123456789" sABC = s [ : 3 ]+" ABC " + s [ 3 : ] # sABC = "012 ABC 3456789" Встроенные методы В Python существует множество встроенных подпрограмм для работы с символьными строками. Многие из них вызываются с помощью то- чечной записи, они называются методами обработки строк. Например, методы upper и lower позволяют перевести строку соответственно в верхний и нижний регистры: 36
Символьные строки §5 s = "аАЬВсС" sUp = s.upper() # sUp = "ААВВСС" sLow = s.lower() # sLow = "aabbcc" Слева от точки записывается имя строки (или сама строка в кавыч- ках), к которой нужно применить метод, а справа от точки — назва- ние метода. Например, возможна такая запись: sWow = "Wow!".upper() # "WOW!" Здесь метод upper применяется к строке "Wow!". Методы строк мы уже использовали, когда выводили данные на экран с помощью метода format: а = 5 b = 4 print ( "{}+{}={}".format(a, b, а + b) ) Ещё один метод, isdigit, проверяет, все ли символы строки — цифры, и возвращает логическое значение: s = "able" print ( s.isdigit() ) # False s = "123" print ( s.isdigit() ) # True Полезный метод strip (по-английски — лишать) удаляет пробелы в начале и в конце строки: sRaw = " Python & C++ " sClear = sRaw.stripO # sClear = "Python & C++" Этот метод удобно использовать для обработки ввода пользователя, когда пробелы в начале и в конце строки не нужны. О других встроенных методах обработки строк вы можете узнать в литературе или в Интернете. Поиск в символьных строках В Python существует метод для поиска подстроки (и отдельного симво- ла) в символьной строке, он называется find (по-английски — найти). В скобках нужно указать образец для поиска, это может быть один символ или символьная строка: s = "Здесь был Вася." n = s.find( "с" ) # п = 3 if п >= 0: print( "Номер символа", п ) else: print ( "Символ не найден." ) 37
1 Программирование на языке Python Метод find возвращает целое число — индекс символа, с которо- го начинается образец (буква «с») в строке s. Если образец в строке встречается несколько раз, функция находит первый из них. В рас- смотренном примере в переменную п будет записано число 3. Если образец не найден, функция возвращает -1. Аналогичный метод г find (от англ, reverse find — искать в обрат- ную сторону) ищет последнее вхождение образца в строку. Для той же строки s, что и в предыдущем примере, метод rfind вернёт 12 (ин- декс последней буквы «с»): s = "Здесь был Вася." n = s.rfind( "с" ) # п = 12 Замена Чтобы заменить в строке одну подстроку на другую, удобно использо- вать встроенный метод replace (по-английски — заменить). Например, эта команда заменит в строке s все знаки «/» на точки: date = "12/02/2018" dateRus = date.replace( "/", "." ) # "12.02.2018" Первый аргумент — строка-образец, второй — строка-замена. Замены выполняются по всей строке (многократно). Если нужно заменить только первые несколько образцов (например, один), указываем третий аргумент — количество замен: dateRus = date.replace( "/", 1 ) # "12.02/2018" Этот оператор заменит на точку только первый знак «/», потому что третий аргумент метода replace равен 1. Преобразования «строка — число» Иногда символьная строка, которая передаётся программе, содержит запись числа. С таким значением нельзя выполнять арифметические операции, потому что это символы, а не число. В языке Python есть встроенные функции для преобразования типов данных, некоторые из них мы уже использовали: int — переводит строку в целое число; float — переводит строку в вещественное число; str — переводит целое или вещественное число в строку. Приведём пример преобразования строк в числовые значения: s = "123" n = int ( s ) # п = 123 s = "123.456" х = float ( s) #х=123.456 38
Символьные строки §5 Если строку не удаётся преобразовать в число (например, если в ней содержатся буквы), возникает ошибка и выполнение программы завер- шается аварийно. Теперь покажем примеры обратного преобразования: п = 123 s = str( n ) # s = "123" х = 123.456 s = str ( х ) # s = "123.456" Эти операции всегда выполняются успешно (ошибка произойти не может). Функция str использует правила форматирования, установленные по умолчанию. При необходимости можно использовать собственное форматирование, например: п = 123 s = "{:5}".format( n ) # s = " 123" Здесь значение переменной п записано в 5 позициях, т. е. в начале строки будут стоять два пробела. Для вещественных чисел можно использовать форматы f (с фикси- рованной запятой) и е (научный формат, с плавающей запятой): X = 123.456 s = "{:7.2f}".format( х ) # s = " 123.46" s = "{:10.2е}".format( х ) # s = " 1.23е+02" Формат 7.2f обозначает «вывести в 7 позициях с двумя знаками в дробной части», а формат 10.2е — «вывести в научном формате в 10 позициях с двумя знаками в дробной части». Символьные строки в функциях Символьные строки могут передаваться процедурам и функциям как аргументы. Функции могут возвращать символьные строки как резуль- тат. Для примера напишем простую функцию, которая из полного адреса файла выделяет его имя (с расширением). Например, из адреса /home/ vasya/miner.ехе нужно выделить имя файла miner.exe. Назовём эту функцию extractFileName (по-английски — извлечь имя файла). Именем файла будем считать все символы после последнего символа «/» (он называется «слэш», от англ, slash). Найдём позицию последне- го слэша с помощью метода rfind, а затем выделим срез строки после этого символа (до конца строки), это и будет результатом функции: def extractFileName( fileAddr ): posSlash = fileAddr.rfind("/") fileName = fileAddr[posSlash+1: ] return fileName 39
1 Программирование на языке Python Проверим, не будет ли ошибки, если в строке нет слэша. В этом случае метод rfind вернёт «-1» (образец не найден), и результатом будет срез f ileAddr [0: ], т. е. вся переданная строка. В операционной системе Windows используется также обратный слэш «\». Для того чтобы функция правильно работала для обоих ва- риантов, в самом начале можно заменить все обратные слэши на пря- мые: fileAddr = fileAddr.replace( "/" ) Обратите внимание, что в строке-образце обратный слэш удвоен. Это связано с тем, что этот символ особый: он используется для добавле- ния в строку служебных символов. Например, \п — это (один!) сим- вол перехода на новую строку. Поэтому, когда мы хотим включить в строку именно обратный слэш, его нужно удвоить. Рекурсивный перебор Попробуем найти все трёхбуквенные «слова» (цепочки символов), кото- рые можно составить с помощью алфавита {А, Б, В}. Самое простое решение — это полный перебор вариантов с помощью тройного вложенного цикла: alphabet = "АБВ" for cl in alphabet: for c2 in alphabet: for c3 in alphabet: print( cl+c2+c3 ) Поскольку в переменной alphabet буквы расставлены по алфавиту, слова тоже будут выведены в алфавитном порядке. Сложности возникают тогда, когда нужная длина слов неизвестна заранее, например вводится с клавиатуры, из файла или через ком- пьютерную сеть. Мы не знаем, сколько циклов нужно добавить в такую программу, поэтому и не можем её написать, используя циклы. Существует красивое решение, основанное на рекурсии. Пусть нам нужно построить все слова длиной К. Давайте выберем первую букву, пусть это будет «А». Оставшаяся (пока неизвестная) часть слова — это слово длиной К - 1. Тогда для того, чтобы построить все слова дли- ны К, начинающиеся с буквы А, нам нужно перебрать все возможные слова длины К— 1, т. е. нужно решить ту же задачу, но с другими данными. Мы вышли на рекурсивное решение: в цикле перебрать все возможные первые буквы (ставя по очереди каждую букву алфавита на первое место) и для каждого случая построить все возможные «хво- сты» длиной К - 1. Остановить рекурсию (и вывести готовое слово) нужно тогда, когда оставшаяся часть пустая {К = 0), т. е. все буквы уже выбраны. Полу- чается такая рекурсивная процедура: 40
Символьные строки §5 def allWords( word, alphabet, К ): if К < 1: print ( word ) return for c in alphabet: allWords( word + c, alphabet, К - 1 ) При вызове этой процедуры нужно передать ей пустую начальную строку (ни одна буква пока не выбрана), строку-алфавит (второй аргу- мент) и нужное значение К: alphabet = "АБВ" К = 3 allWords( alphabet, К ) Теперь подсчитаем, сколько всего слов было найдено. Для этого под- программа allWords должна превратиться в функцию и возвращать количество построенных слов. Вызывать её мы хотим так: count = allWords( alphabet, К ) print ( count ) Во-первых, при выводе очередного слова функция должна возвра- щать 1 (построено одно слово). Во-вторых, когда в цикле перебираются все возможные первые буквы слова, результаты рекурсивных вызовов нужно складывать и в конце возвращать как результат функции: def allWords( word, alphabet, К ): if К < 1: print(word) return 1 count = 0 for c in alphabet: count += allWords( word + c, alphabet, К - 1 ) return count Все изменения в тексте процедуры выделены фоном. Выводы • Символьная строка — это последовательность символов. • Длина строки — это количество символов в строке. • Знак + при работе со строками означает сцепление (сложение) строк, а знак * заменяет многократное сложение. • Индекс — это число, которое определяет смещение символа от начала строки. В языке Python первый по счёту символ строки имеет индекс 0. • При обращении к отдельному символу строки его индекс записы- вают в квадратных скобках. 41
1 Программирование на языке Python • Для перебора символов строки удобно использовать цикл for ... in. • Подстрока — это часть символьной строки. • Срез — это операция выделения подстроки. • Функция поиска подстроки возвращает номер символа, с которого начинается подстрока, или -1 в случае неудачи. • Строку можно преобразовать в число для того, чтобы затем выполнять с ним вычисления. Число можно преобразовать в сим- вольную строку. Вопросы и задания 1. Определите, к каким типам относятся переменные в программе, s = "Гравицапа" n = len ( s ) si = s[:3] pos = sl.find( "цап" ) k = float ( "789.354" ) 2. Напишите полную программу, которая вводит строку с клавиатуры и выводит на экран её длину. Проверьте, как эта программа реаги- рует на строку с пробелами. 3. С помощью программы сравните пары слов и сделайте выводы: пар — парк, Пар — пар, steam — Пар, Steam — steam, 5Steam — Steam. 4. После выполнения предыдущего задания сравните пары слов, не используя программу: парта — парк, ПАрта — Парк, СПАМ — Spam, ПОЧТА — spam, ПО4та — ПОЧта, почТА — Post, 55 — 66, 9 — 128. 5. Используя обращения к отдельным символам строки inf = "информатика" составьте как можно больше слов русского языка из слова «инфор- матика», например: print ( inf[l] + inf[0] + inf[5] ) # ним 6. Используя только операции выделения подстроки и «сложения» строк, постройте из строки inf = "информатика" как можно больше слов русского языка. Постарайтесь использовать наименьшее возможное число операций. Проверьте ваши решения с помощью программы. 7. Предложите, как можно найти вторую букву «с» с начала строки. 8. Напишите программу, которая заменяет в символьной строке все точки на нули и все буквы «X» на единицы. Например, из строки ". . XX. . X. " должна получиться строка "00110010". 42
Символьные строки §5 9. Битовой строкой будем называть символьную строку, состоящую только из символов "О" и "1". Напишите программу, которая вы- полняет инверсию битовой строки: заменяет в ней все нули на единицы и наоборот. Например, из строки "00110010" должна получиться строка "11001101". 10. Введите битовую строку и дополните её последним битом — битом чётности. Он должен быть равен 0, если в исходной строке чётное число единиц, и равен 1, если нечётное (в получившейся строке должно всегда быть чётное число единиц). Например, из строки "00110010" должна получиться строка "001100101". 11. Приведите несколько способов построения строки "А. Семёнов" из строки "Семёнов Андрей". Какой из них лучше? Как вы сравни- вали эти способы? 12. Вводится строка, в которой сначала записана фамилия человека, а затем через один пробел — его имя, например "Семёнов Андрей". Запишите команды, которые позволяют: а) найти позицию пробела, разделяющего фамилию и имя, и записать его в переменную posSpace; б) выделить из строки фамилию и записать её в переменную fam; в) выделить из строки имя и записать его в переменную name; г) приписать перед фамилией первую букву имени, точку и пробел. 13. Напишите программу, которая принимает символьную строку, со- держащую фамилию и имя (они разделены одним пробелом). Нуж- но построить новую строку, в которой записан инициал (первая буква имени с точкой) и через пробел — фамилия. Например, из строки "Семёнов Андрей" должна получиться строка "А. Семёнов". 14. Напишите программу, которая принимает строку, содержащую фа- милию, имя и отчество человека (каждая пара слов разделена од- ним пробелом). Нужно построить новую строку, в которой запи- саны инициалы (первые буквы имени и отчества с точками после них) и через пробел — фамилия. Например, из строки "Семёнов Андрей Иванович" должна получиться строка "А.И. Семёнов". *15 . Дополните программу из предыдущего задания проверкой на оши- бочные данные. Программа должна вывести сообщение об ошибке, если, например, в строке одно или два слова (а не три). 16. Напишите программу, которая вводит адрес файла и «разбирает» его на части, разделённые символом «/». Каждую часть нужно вывести в отдельной строке. Например, при вводе адреса /home/cpp/lib/game.apk программа должна вывести: home срр lib game.apk 43
1 Программирование на языке Python *17. Дополните программу из предыдущего задания проверкой на оши- бочные данные. Программа должна вывести сообщение об ошибке, если: • строка заканчивается символом «\»; • в строке используются одновременно символы «\» и «/»; • в строке используются символы «?» или «*» 18. Напишите программу, которая определяет количество слов в стро- ке. Слова разделены пробелами, причём в начале строки, в конце строки и между словами может быть сколько угодно пробелов. 19. Напишите программу, которая определяет, сколько раз входит в символьную строку заданная цепочка символов (подстрока). 20. Напишите программу, которая «переворачивает» введённую стро- ку, т. е. переставляет символы так, чтобы первый стал последним, второй — предпоследним и т. д. 21. Напишите программу, которая принимает символьную строку и проверяет, является ли она перевёртышем. Слово-перевёртыш (па- линдром) читается одинаково в обоих направлениях, например сло- во «казак». 22. Какие из этих строк можно преобразовать в целое число, какие — в вещественное? а) "45" г) "14; 5" ж) " (30) " б) "5р." д) "tul54" з) "1ЕЗ" в) "14.5" е) "543.0" 23. Напишите программу, которая вычисляет сумму двух натуральных чисел, записанную в виде символьной строки, например «1 + 25». Не используйте готовую функцию. 24. Напишите программу, которая вычисляет сумму трёх натураль- ных чисел, записанную в виде символьной строки, например "1 + 25 + 56". Не используйте готовую функцию. *25. Напишите программу, которая вычисляет сумму неизвестного коли- чества натуральных чисел, записанную в виде символьной строки, например "1 + 25 + 12 + 34 + 89". Не используйте готовую функцию. *26. Напишите функцию, которая вычисляет значение арифметического выражения, содержащего только целые числа и знаки сложения и вычитания. Выражение записано в символьной строке. 27. Напишите функцию noDoubleSpace, которая удаляет все двойные пробелы в символьной строке, заменяя их на одиночные. 28. Напишите свою функцию trim, которая удаляет из символьной строки все пробелы в начале и в конце строки и возвращает но- вую строку (т. е. делает то же самое, что и метод strip). 29. Напишите функцию firstWord, которая возвращает первое слово из символьной строки (слева и справа от этого слова может быть сколько угодно пробелов). 44
Символьные строки §5 *30. На веб-странице команды разметки (тэги) заключаются в угловые скобки о. Напишите функцию, которая удаляет в символьной строке все тэги и возвращает новую строку. 31. Напишите функцию, которая извлекает из полного адреса файла название каталога. Например, из адреса /home/vasya/miner.ехе нужно извлечь название каталога /home/vasya 32. Напишите функцию, которая изменяет в имени файла расширение на заданное (например, на .Ьак). Функция принимает два параме- тра: имя файла и новое расширение. Учтите, что в исходном име- ни расширение может быть пустым. 33. Напишите программу, которая моделирует исполнитель Редактор. Редактор обрабатывает цепочку цифр, используя две команды (v и w обозначают цепочки цифр). Команда заменить (v, w) заменяет в строке первое слева вхождение цепочки v на цепочку w. Команда нашлось ( v ) проверяет, встречается ли цепочка v в строке. Дана программа для исполнителя Редактор: ПОКА нашлось ( 222 ) ИЛИ нашлось ( 555 ) ЕСЛИ нашлось ( 222 ) ТО заменить ( 222, 5 ) ИНАЧЕ заменить ( 555, 2 ) КОНЕЦ ЕСЛИ КОНЕЦ ПОКА Ваша программа должна ввести с клавиатуры исходную цепочку символов и определить результат работы Редактора по приведён- ной выше программе. 34. Постройте все битовые цепочки длиной К, в которых нет двух со- седних нулей. Сколько всего таких цепочек? Значение К вводите с клавиатуры. * 35. В языке племени «тумба-юмба» всего 4 буквы: {Ы, Ш, Ч, Э}. При образовании слов запрещено ставить две гласные буквы подряд, но одна гласная должна быть обязательно. Постройте все правильные слова длины К в этом языке и подсчитайте их количество. Значе- ние К вводите с клавиатуры. * 36. Напишите программу перебора всех «слов» длины К в задан- ном алфавите, не использующую рекурсию. Попробуйте составить функцию, которая на основе некоторой комбинации букв вычисля- ет следующую за ней комбинацию. Значение К вводите с клавиа- туры. 45
1 Программирование на языке Python * 37. К вам пришли К гостей. Напишите программу, которая выво- дит все перестановки — способы рассадить их за столом. Гостей можно обозначить латинскими буквами. Значение К вводите с клавиатуры. * *38. Исполнитель Калькулятор работает с целыми числами и умеет выполнять две команды: 1) прибавь 1; 2) умножь на 3. Программа для Калькулятора — это последовательность номеров команд. Например, 122 — это программа прибавь 1 умножь на 3 умножь на 3 которая преобразует число 5 в число 54. Напишите программу, которая вводит с клавиатуры два числа N и М (известно, что N < М) и составляет самую короткую программу для Калькулято- ра, которая преобразует число N в число М. **39.Проект. Напишите программу, которая играет с человеком в сле- дующую игру. Дано слово (или фраза). Игроки по очереди стира- ют буквы из слова. За один ход можно стереть либо одну любую букву, либо все одинаковые буквы. Выигрывает тот, кто сотрёт последнюю букву. **40.Проект. Для игры, описанной в предыдущей задаче, напишите программу, которая определяет, кто выиграет при безошибочной игре: первый игрок (который делает первый ход) или второй. Интересный сайт docs.python.org/3/library/string.html — описание встроенных функ- ций Python для обработки строк §6 Массивы (списки) Ключевые слова: • массив • список • индекс элемента • значение элемента заполнение массива вывод массива ввод массива 46
Массивы (списки) §6 Что такое массив? В программах, с которыми мы работали раньше, было всего несколько переменных. Каждой из них мы давали своё имя, и никаких сложно- стей при этом не возникало. Объёмы данных, которые обрабатывают современные компьютеры, огромны: количество значений измеряется миллионами и миллиарда- ми. Если каждую из этих переменных называть своим именем, очень легко запутаться, и работать с таким набором данных очень неудобно. Допустим, мы хотим сложить значения 1000 ячеек с именами al, а2, ..., al ООО. Для этого нужно будет написать очень длинный оператор присваивания: sum = al + а2 + . . . + alOOO Учтите, что компьютер не понимает многоточий, поэтому нам при- дётся перечислить все 1000 имён переменных. Серьёзная проблема возникнет при решении этой задачи, если коли- чество данных заранее неизвестно (например, они поступают по ком- пьютерной сети). Мы не знаем, сколько переменных потребуется, а поэтому не можем написать команду суммирования. Для того чтобы было удобно работать с большим количеством дан- ных, обычно дают общее имя группе ячеек, которая называется масси- вом. Массив — это группа данных одного типа, расположенных в памяти друг за другом и имеющих общее имя. К элементу массива можно обращаться по его номеру (индексу). Перед тем как использовать массив, надо присвоить ему имя, опре- делить тип входящих в массив переменных {элементов массива) и их количество. По этим сведениям компьютер вычислит, сколько места требуется для хранения массива, и выделит в памяти нужное число ячеек. Для работы с массивами необходимо научиться: • выделять память нужного размера под массив; • записывать данные в нужную ячейку; • читать данные из ячейки массива. Массивы в языке Python В языке Python нет такой структуры данных, как «массив». Вместо этого для хранения группы однотипных (и не только однотипных!) объектов используют списки — объекты типа list. В отличие от массивов список — это динамическая структура, его размер можно изменять во время выполнения программы (удалять и добавлять элементы), при этом все операции по управлению памятью берёт на себя транслятор. 47
1 Программирование на языке Python Далее, говоря о списках, мы будем использовать слово «массив», по- тому что чаще всего списки в языке Python используются именно в роли массива, т. е. для хранения однотипных значений. Создание массива Массив (список) можно создать перечислением элементов через запя- тую в квадратных скобках, например так: А = [1, 3, 4, 23, 5] print( type(А) ) Такая программа выведет: <class 'list'> Это означает, что массив — объект типа list. Массив можно составить не только из чисел, но и из данных любых типов, например из символьных строк: А = ["Вася", "Петя", "Коля", "Маша", "Даша"] Массивы можно «складывать» с помощью знака +, например при- ведённый выше числовой массив можно было построить сложением нескольких списков: А = [1, 3] + [4, 23] + [5] Сложение одинаковых массивов заменяется умножением *. Оператор А = [0]*10 создаёт новый массив из 10 элементов (выделяет для них место в памяти) и заполняет их нулями. Длина массива (количество элементов в нём) определяется с помо- щью функции len: N = len(А) Таким образом, в любой момент массив «знает» свой размер. Иногда размер массива хранят в отдельной переменной: N = 10 А = [0]*N В этом случае очень просто переделать программу для работы с масси- вом другого размера: достаточно просто изменить значение N в первой строке программы. Обращение к элементу массива Каждый элемент массива имеет свой номер (индекс). Используя индекс, можно сразу обратиться к любому элементу массива. Поэтому говорят, что массив — это структура данных с произвольным доступом. 48
Массивы (списки) §6 Индекс — это значение, которое указывает на конкретный элемент массива. Нумерация элементов массивов (и элементов символьных строк) в Python всегда начинается с нуля, второй по счёту элемент имеет но- мер 1 и т. д. Для того чтобы обратиться к элементу массива (прочитать или изме- нить его значение), нужно записать имя массива и в квадратных скоб- ках — (рис. 1.7) индекс нужного элемента, например А [2] (рис. 1.7). Индексы О 1 2^ 3 4 3 2 7 0 5 А[0] А[1] А[2] А[3] А[4] Обращение к элементу Рис. 1.7 Индексом может быть также целое значение переменной или ариф- метического выражения. Например, для массива на рис. 1.7 про- грамма i = 1 print(A[i], A[i+1], A[3*i+1], A[A[3*i]]) выведет то же самое, что и программа print(A[l], А[2], А[4], А[0]) Индексом может быть даже значение элемента массива. Для послед- него элемента вывода запись A[A[3*i]] означает, что нужно взять значение A[3*i] при i = 1 и использовать его как индекс нужного элемента. Получаем: i = 1 => 3*i = 3 => A[3*i] = А[3] = 0 => A[A[3*i] ] = А[0] = 3. Как и для символьных строк, для массивов можно использовать отрицательные индексы элементов, при этом отсчёт ведётся с конца массива. Например, А[-1] — это последний элемент, а А[-2] — пред- последний. Для получения соответствующего положительного индекса к отрицательному нужно прибавить длину массива. При обращении к элементу массива с несуществующим индексом происходит серьёзная ошибка — выход за границы массива, и про- грамма завершается аварийно. Например, для массива на рис. 1.7 (он имеет длину 5) правильные значения индексов — от -5 до 4. 49
1 Программирование на языке Python Перебор элементов массива Далее везде будем считать, что N — это текущий размер массива А, с которым мы работаем, т. е. то значение, которое возвращает вызов функции 1еп( А ). Перебор элементов состоит в том, что мы в цикле просматриваем все элементы массива и, если нужно, выполняем с каждым из них некото- рую операцию. Для этого удобнее всего использовать цикл по перемен- ной, значение которой изменяется от минимального до максимального индекса. Для массива из N элементов цикл выглядит так: for i in range(N): ... # работаем c A[i] Здесь вместо многоточия можно добавлять операторы, которые рабо- тают с элементом A[i] (в том числе и изменяют его). Мы видим, что благодаря использованию массива нам достаточ- но описать, что делать с одним элементом, и поместить эти действия внутрь цикла, перебирающего значения индексов. Если бы мы при- меняли простые переменные, то нам пришлось бы описывать необхо- димые действия для каждого элемента (правда, при этом цикл бы не понадобился). Программа for i in range(N): A[i] = i заполняет массив целыми числами от 0 до N-1 (это как раз та последовательность, которую строит функция range). Здесь мы пред- полагаем, что память для N элементов массива уже выделена, иначе обращение к несуществующему элементу вызовет ошибку и аварийное завершение программы. Теперь заполним массив первыми N натуральными числами в обрат- ном порядке: в первый элемент массива должно быть записано число N, во второй — число N-1, а в последний — единица. Сначала запишем в развёрнутом виде (без цикла) операторы, кото- рые должны быть выполнены: А [ 0 ] = N А[1] = N-1 A[N-1] = 1 Теперь составим цикл, в котором значение, присваиваемое очередно- му элементу, пока обозначим через X: for i in range(N): A[i] = X Величина X должна изменяться при переходе к следующему элемен- ту. Её начальное значение равно N, конечное значение равно 1, с каж- 50
Массивы (списки) §6 дым повторением цикла оно уменьшается на 1. Можно записать цикл так: X = N for i in range(N): A[i] = X X -= 1 А можно его значительно упростить, заметив, что при увеличении индекса элемента i на единицу значение X уменьшается, причём тоже на единицу. Поэтому сумма i + X остаётся постоянной! Для первого элемента мы должны получить i + X = N. Выразив X из этого уравнения, находим, что элемент с номером i принимает значе- ние N - i, поэтому цикл можно записать так: for i in range(N): A[i] = N - i Теперь предположим, что массив заполнен, и попробуем увеличить все его элементы на единицу. Это значит, что нужно заменить значе- ние элемента A[i] на A[i]+1: for i in range(N): A[i] += 1 Генераторы Методы заполнения массивов, использующие цикл, работают в боль- шинстве языков программирования. В языке Python есть особые возможности, которые позволяют решать многие задачи кратко и надёжно. Например, две операции — создание и заполнение массива — мож- но объединить в одну с помощью генератора — выражения, напомина- ющего цикл: А = [i for i in range(N)] Как вы знаете, цикл for i in range (N) перебирает все значения i от 0 до N-1. Выражение перед словом for (в данном случае — i) — это то, что записывается в очередной элемент массива для каждого i. В приведённом примере массив заполняется значениями, которые по- следовательно принимает переменная i, т. е. при N = 10 мы построим такой массив: А = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Тот же результат можно получить, если использовать функцию list для того, чтобы создать список из данных, которые получаются с помощью функции range: А = list ( range ( N ) ) 51
1 Программирование на языке Python Для заполнения массива квадратами этих чисел можно использовать такой генератор: А = [i*i for i in range(N)] В конце записи генератора можно добавить условие отбора. В этом случае в массив включаются лишь те элементы, перебираемые в цик- ле, которые удовлетворяют этому условию. Например, следующий ге- нератор составляет массив из всех чисел в диапазоне от 0 до 99, кото- рые делятся на 7: А = [i for i in range(100) if i % 7 == 0] Обратите внимание, что длина этого массива будет меньше 100, и цикл for i in range(100): print( A[i] ) приведёт к ошибке — выходу за границы массива. Вывод массива Массив — это набор элементов, поэтому во многих языках програм- мирования нельзя вывести массив одной командой. Однако в языке Python такая возможность есть: print(А) В этом случае весь массив выводится в квадратных скобках, его эле- менты разделяются запятыми. Можно вывести элементы массива на экран по одному, используя цикл перебора: for i in range(len(А)): print( A[i], end=" " ) Параметр end определяет, что после ввода каждого элемента добав- ляется пробел, а не символ перехода на новую строку. Удобно записывать такой цикл несколько иначе: for х in А: print( х, end=" " ) Здесь не используются переменная-индекс i и функция len, а про- сто перебираются все элементы массива. На каждой итерации цикла в переменную х заносится значение очередного элемента массива (в по- рядке возрастания индексов). Такой цикл перебора очень удобен, если не нужно изменять значения элементов массива. В языке Python существует ещё один замечательный способ вывода всех элементов массива через пробел (без скобок): print( *А ) 52
Массивы (списки) §6 Знак * перед именем массива означает, что нужно преобразовать массив в набор отдельных значений, т. е. для массива А = [1, 2, 3, 4, 5] эта команда сработает так же, как и print (1, 2, 3, 4, 5 ) Ввод массива с клавиатуры Иногда небольшие массивы вводятся с клавиатуры. В простейшем случае мы просто строим цикл, который выполняет оператор ввода отдельно для каждого элемента массива: for i in range(N): A[i] = int ( input () ) Напомним, что если какую-то из введённых строк не удастся преоб- разовать в целое число, программа завершится с ошибкой. Вместо цикла можно использовать генератор, который сразу создаёт массив и заполняет его введёнными числами: А = [int( input() ) for i in range(N)] Здесь при каждом повторении цикла строка, введённая пользовате- лем, преобразуется в целое число с помощью функции int, и это чис- ло добавляется к массиву. При этом пользователь вводит данные «вслепую», т. е. программа не подсказывает ему, значение какого элемента вводится в данный мо- мент. Значительно удобнее, если перед вводом появляется сообщение с подсказкой: for i in range(N): print ( "A[{}]=".format(i), end="" ) A[i] = int ( input () ) В этом примере перед вводом очередного элемента массива на экран выводится подсказка. Например, при вводе элемента с индексом 3 бу- дет выведено: А[3]= и курсор (приглашение к вводу) будет мигать справа от знака =. Заполнение массива случайными числами Иногда нужно заполнить массив случайными числами (например, опре- делить случайные координаты клеток с призами или препятствиями в игре). Для работы со случайными (точнее, псевдослучайными) числами нужно подключить (импортировать) модуль random, из которого нам будет нужна одна функция — randint. Эта функция генерирует слу- 53
1 Программирование на языке Python чайное целое число в заданном диапазоне. Вначале подключим эту функцию: from random import randint Если массив уже создан, используем цикл по переменной: for i in range(N): A[i] = randint( 20, 100 ) To же самое можно сделать с помощью генератора: А = [randint( 20, 100 ) for i in range(N)] Генератор создаёт массив из N элементов и заполняет его слу- чайными числами из отрезка [20, 100]. Выводы • Массив — это группа переменных одного типа, расположенных в памяти друг за другом и имеющих общее имя. Массивы использу- ют для того, чтобы было удобно работать с большим количеством данных. • Индекс элемента массива — это значение, которое указывает на конкретный элемент массива. Нумерация элементов массива в Python начинается с нуля. • К элементу массива можно в любой момент обратиться по индек- су. Массив — это структура данных с произвольным доступом. • При обращении к элементу массива индекс записывают в квадрат- ных скобках. Это может быть число, имя переменной целого типа или арифметическое выражение, результат которого — целое число. • Для перебора элементов массива удобно использовать цикл по пе- ременной, которая изменяется от минимального до максимального значения индекса. Вопросы и задания 1. Объясните разницу между терминами «индекс элемента массива» и «значение элемента массива». 2. Объясните действие операторов. print( А[3] ) А[3] = 5 А[1] = А[2] + 2*А[3] 3. Определите, что выведет фрагмент программы для массива на рис. 1.7. i = 1 А[2] = A[i] + 2*A[i-l] + A[2*i] print( A[2] + 2*A[4] ) 54
Массивы (списки) §6 4. Найдите ошибки в фрагменте программы. А = [5, 4, 3, 2, 1] х = 5 print( А[х-7] ) А[х+4] = А[х-1] + А[2*х] В чём заключаются ошибки? 5. Некоторые языки программирования разрешают обращаться к эле- ментам за пределами массива (программа не завершается аварий- но). Обсудите достоинства и недостатки такого решения. 6. Почему размер массива лучше вводить как переменную, а не запи- сывать как число? 7. Определите, что выведет фрагмент программы. А = [4, 3, 0, 2, 1] print(А[0]) print(А[А[0]]) print(А[А[А[0]]]) print(А[А[А[А [ 0]]]]) print(А[А[А[А[А[0]]]]]) 8. Определите, какие значения окажутся в массиве после выполнения фрагмента программы. А = [5, 4, 3, 2, 1] N = len ( А ) for i in range(N): A[i] += i 9. Напишите фрагмент программы, который умножит все элементы массива на 2. 10. Напишите фрагмент программы, который умножит первый элемент массива на 1, второй — на 2, третий — на 3 и т. д. 11. Напишите фрагмент программы, который заполнит массив нуля- ми и единицами так, чтобы они чередовались: сначала 0, потом 1, потом снова 0 и т. д. 12. Заполните все элементы массива значением X, введённым с клави- атуры. 13. Заполните массив последовательными натуральными числами, начиная со значения X, введённого с клавиатуры. Первый элемент должен быть равен X, второй — X + 1 и т. д. 14. Заполните массив целыми числами в обратном порядке, начиная со значения X, введённого с клавиатуры. Последний элемент дол- жен быть равен X, предпоследний — X - 1 и т. д. 15. Заполните массив степенями числа 2 (от 21 до 2W). *16. Заполните массив степенями числа 2, начиная с конца, так чтобы последний элемент массива был равен 1, а каждый предыдущий был в 2 раза больше следующего. 55
1 Программирование на языке Python *17. С клавиатуры вводится целое число X. Заполните массив, состо- ящий из нечётного числа элементов, целыми числами, так чтобы средний элемент массива был равен X, слева от него элементы были расположены по возрастанию, а справа — по убыванию. Со- седние элементы различаются на единицу. Например, при X = 3 массив из 5 элементов заполняется так: 1 2 3 2 1. 18. Заполните массив случайными целыми числами на отрезке [20; 100] и выведите на экран. 19. Массив из 22 элементов требуется заполнить случайными целыми числами на отрезке [10; 30]. Будут ли в массиве одинаковые эле- менты? Почему? 20. С клавиатуры вводятся целые значения X и Y (не гарантируется, что X < У). Заполните массив случайными целыми числами на отрезке, ограниченном значениями X и У. 21. Массив введён следующим образом: А = [1, 2, 3, 4, 5] При каких значениях х программа завершится аварийно? a) print ( А[х+3] ) б) for i in range ( 3 ) : A[i+x] = A[i] в) for i in range ( x-2 ): A[i] = 2*(x - i) r) for i in range ( 4 ) : A [ i + 1 ] = A [ i ] + x д) for i in range ( x+1, x+6 ) : A [ i ] = x * x e) for i in range ( 5 ) : A[i+2] = x + i 22. Чему будут равны элементы массива А = [1, 2, 3, 4, 5] после выполнения цикла? Здесь N — длина массива. a) for i in range ( N ) : A[i] = A[i]*A[i] 6) for i in range ( N-l ): A[i] = A[i+1] в) for i in range ( N-l ): A[i+1] = A[i] r) for i in range ( N-l, 0, -1 ) : A[i] = A[i-1] д) for i in range ( 1, N ) : A[i] = A[i-1] + 1 e) for i in range ( 1, N ) : A[i] = A[i-1]*2 56
Алгоритмы обработки массивов §7 23. Дан фрагмент программы: j = 1 for i in range(N): if A[i] == A[j] : j = i При каком условии после выполнения этого фрагмента значение переменной j будет равно: а) 1; б) 4; в) N? 24. Введите с клавиатуры значения элементов массива, увеличьте каж- дый элемент в 2 раза и выведите полученный массив на экран. 25. Введите с клавиатуры значения элементов массива и увеличьте на 5 значения всех элементов массива, кроме первого и последнего. 26. В массиве чётное число элементов. Введите с клавиатуры значения элементов массива и выполните две операции: а) увеличьте на единицу значения всех элементов в первой полови- не массива; б) увеличьте в 2 раза значения всех элементов во второй половине массива. *27. Введите с клавиатуры значения элементов массива и найдите их среднее арифметическое. §7 Алгоритмы обработки массивов Ключевые слова’. • сумма элементов массива • подсчёт элементов Сумма элементов массива Представьте себе, что в массиве записаны зарплаты сотрудников фир- мы и требуется найти общую сумму, которая будет им выплачена. Для этого нужно сложить все числа, которые находятся в массиве. Для того чтобы накапливать сумму, введём отдельную переменную, назовём её summa. Классический способ решения этой задачи — вы- полнить перебор всех элементов массива в цикле. На каждой итерации цикла к значению переменной summa прибавляется значение очередно- го элемента массива. summa = О for i in range(N): summa += A[i] print( summa ) 57
Программирование на языке Python Покажем, как работает этот алгоритм для массива А: 0 12 3 4 5 2 8 3 1 Рис. 1.8 Выполним трассировку («ручную прокрутку») программы. Запишем в таблице выполняемые команды (операторы) и изменение всех пере- менных (сам массив А при этом не меняется): Оператор i summa summa = 0 0 i = 0 0 summa += А[0] 5 i += 1 1 summa += A [ 1 ] 7 i += 1 2 summa += A[2] 15 i += 1 3 summa += A[3] 18 i += 1 4 summa += A [ 4 ] 19 Фоном выделены команды, которые выполняются автоматически в цикле по переменной: в строке 2 переменной i присваивается началь- ное значение, а в строках 4, 6, 8 и 10 после очередного повторения цикла значение этой переменной увеличивается на единицу. Поскольку массив А не изменяется, сложить все его элементы удоб- но с помощью такого цикла: summa = О for х in А: summa += х print ( summa ) Заметим, что при этом не нужно хранить размер массива в перемен- ной. Более того, в языке Python есть встроенная функция sum, которая сразу считает сумму элементов массива, так что задача решается в одну строку: print ( sum(А) ) 58
Алгоритмы обработки массивов §7 Произведение элементов массива вычисляется аналогично: вспомога- тельная переменная умножается на значение очередного элемента. На- чальное значение этой вспомогательной переменной должно быть рав- но 1: product = 1 for х in А: product *= х print ( product ) Подсчёт элементов массива, удовлетворяющих условию Во многих задачах нужно найти в массиве все элементы, удовлетворя- ющие заданному условию, и как-то их обработать, например подсчи- тать их количество. Для подсчёта элементов используется переменная- счётчик, назовём её count. Перед началом цикла в счётчик записывается ноль (ни одно- го нужного элемента ещё не найдено). Если на очередной итерации цикла найден новый элемент, значение счётчика увеличивается на единицу. Подсчитаем количество элементов массива с чётными значениями. Условие «элемент A[i] — чётный» можно сформулировать иначе: «оста- ток от деления A[i] на 2 равен нулю». Получаем такую программу: count = О for i in range (N) : if A[i] % 2 == 0: count += 1 # увеличить счётчик print( count ) Другой вариант цикла позволяет избавиться от переменной i: count = 0 for х in А: if х % 2 == 0: count += 1 print ( count ) Кроме того, можно сначала построить новый массив, выделив в него все нужные (чётные) элементы, а потом подсчитать его длину с помо- щью стандартной функции 1еп: В = [х for х in А if х % 2 == 0] print( len(В) ) Массив В строится с помощью генератора, в него включаются все элементы х из исходного массива А, для которых выполняется условие чётности. 59
1 Программирование на языке Python Теперь усложним задачу. В массиве записан рост каждого члена баскетбольной команды в сантиметрах. Требуется найти средний рост игроков, которые выше 180 см (предполагаем, что хотя бы один такой игрок есть). Средний рост — это среднее арифметическое, т. е. «сум- марный рост» интересующих нас игроков (тех, которые выше 180 см), разделённый на их количество. Для решения задачи нам нужно считать и сумму, и количество эле- ментов массива, которые больше 180: count = О surrana = О for х in А: if х > 180: count += 1 surrana += x print ( summa/count ) Обратите внимание, что в теле условного оператора находятся две команды (увеличение счётчика и увеличение суммы). Решение «в стиле Python» предполагает построение нового масси- ва из нужных элементов массива А и использование двух встроенных функций: В = [х for х in A if х > 180] print ( sum(В)/len(В) ) Особенности копирования списков в Python Предположим, что в программе создан список А, например, так: А = [1, 2, 3] При этом в переменной А хранится ссылка на список, т. е. адрес этого списка в памяти. Оператор В = А копирует в переменную в тот же самый адрес. Теперь две переменные, Айв, будут ссылаться на один и тот же список (рис. 1.9, а). А[0] = 0 Поэтому при изменении списка А будет одновременно изменяться и список В, ведь фактически это один и тот же список, к которому можно обращаться по двум разным именам. На рисунке 1.9, б показа- на ситуация после выполнения оператора А[0]=0. 60
Алгоритмы обработки массивов §7 Эту особенность Python нужно учитывать при работе со списками. Если нам нужна именно копия списка (а не ещё одна ссылка на него), можно использовать срез, включающий все элементы: В = А[: ] Теперь А и В — это независимые списки, и изменение одного из них не меняет второй (рис. 1.10). Рис. 10 Вместо среза можно было вызвать функцию сору из модуля сору: inport сору А = [1, 2, 3] В = сору.сору( А ) Это так называемая «поверхностная» копия — она не создаёт пол- ную копию, если список содержит какие-то изменяемые объекты, на- пример другие списки. Для полного копирования используется функ- ция de ер с ору из того же модуля: inport сору А = [1, 2, 3] В = сору.deepcopy( А ) Выводы • Задачи суммирования, перемножения, подсчёта элементов массива решаются с помощью цикла, в котором перебираются все элементы. • Для вычисления суммы элементов массива используется вспомо- гательная переменная, в которой накапливается сумма. Начальное значение этой переменной равно нулю. • При вычислении произведения начальное значение вспомогатель- ной переменной должно быть равно 1. • При подсчёте элементов, удовлетворяющих условию, использует- ся переменная-счётчик, которая увеличивается на 1 каждый раз, когда найден новый подходящий элемент. Её начальное значение должно быть равно нулю. • Решения «в стиле Python» получаются проще за счёт использо- вания генераторов и стандартных функций обработки массивов. Часто они связаны с построением вспомогательного массива. • Оператор присваивания В = А для списков в Python создаёт не копию списка А, а вторую ссылку на тот же список. Независимую копию списка можно построить с помощью среза: В = А [: ]. 61
1 Программирование на языке Python Вопросы и задания 1. Для массива на рис. 1.8 выполните трассировку программы и опре- делите, какое значение будет выведено. summa = О for i in range(N): if A[i] % 2 == 0: summa += A[i] print( summa ) 2. Измените условие отбора в программе из предыдущего задания так, чтобы при обработке массива на рис. 1.8 переменной summa получи- лось число 9. 3. Напишите цикл, с помощью которых можно найти в переменной product произведение положительных элементов массива. 4. Напишите программу, которая определяет, есть ли в массиве хотя бы одно чётное значение. Предложите два способа решения и срав- ните их. 5. Напишите программу, которая определяет, есть ли в массиве хотя бы одно число, которое делится одновременно на 7 и на 5. Предло- жите два способа решения и сравните их. 6. Поликарп написал программу, которая вычисляет среднее арифмети- ческое положительных элементов массива. К сожалению, она работа- ет неверно. Найдите ошибку: summa = 0 for i in range(N): if A[i] > 0: summa += A[i] print( summa/N ); 7. Что произойдёт с массивом А = [ 1, 2, 3, 4 ] длины N = 4 при выполнении фрагмента программы? for i in range(N-1): A[i] = A[i+1] 8. Что произойдёт с массивом А = [1, 2, 3, 4] длины N = 4 при выполнении фрагмента программы? for i in range(N-1): A[i+1] = A[i] 9. Напишите программу, которая заполняет массив из 20 элементов случайными числами на отрезке [-10; 10] и находит: а) сумму положительных элементов массива; б) сумму чётных положительных элементов массива; в) суммы элементов в первой и во второй половинах массива (отдельно); г) произведение ненулевых элементов; 62
Алгоритмы обработки массивов §7 д) количество отрицательных элементов массива; е) количество элементов с чётными и нечётными значениями (отдельно). 10. Напишите программу, которая заполняет массив случайными чис- лами на отрезке [1000; 2000] и находит: а) количество элементов массива, в десятичной записи которых вторая с конца цифра (число десятков) чётная; б) количество элементов массива, которые делятся на 3 и не делятся на 5; в) количество элементов массива, у которых последние две цифры одинаковые; г) сумму элементов массива, у которых число десятков (вторая цифра справа) больше, чем число единиц (младшая цифра). 11. В массиве записаны символьные строки («слова»). Напишите про- грамму, которая выводит: а) количество слов, начинающихся с буквы «А»; б) количество слов, длина которых — чётное число; в) все слова, состоящие из трёх символов; г) все слова, в которых есть символ @; д) все слова, состоящие из четырёх символов, в которых есть буква «щ»; е) все слова, которые начинаются и заканчиваются на одну и ту же букву; ж) все слова, состоящие только из цифр 0 и 1 («битовые це- почки»); з) все слова, которые можно преобразовать в целое число. 12. Заполните массив случайными целыми числами на отрезке [1; 100] и подсчитайте отдельно среднее арифметическое всех эле- ментов, которые меньше 50, и среднее арифметическое всех эле- ментов, которые больше или равны 50. * 13. В массиве чётное число элементов. Напишите программу, которая меняет местами пары соседних элементов: А[0] с А[1], А[2] с А[3] и т. д. * 14. Напишите программу, которая в массиве с чётным количеством элементов меняет местами пары соседних элементов, кроме перво- го и последнего (А[1] с А[2], А[3] с А[4] и т. д.). * *15.Напишите программу, которая находит в массиве все простые числа и строит из них новый массив. Используйте логическую функцию isPrime, которая возвращает истинное значение, если переданное ей число простое. * *16. Напишите программу, которая находит в массиве все числа Фибоначчи и строит из них новый массив. Используйте логи- ческую функцию, которая возвращает истинное значение, если переданное ей число — это число Фибоначчи. 63
1 Программирование на языке Python **17.Проект. Напишите программу, которая играет с человеком в следующую игру. Задан набор слов, в котором ни одно слово не совпадает с началом другого. Игра начинается с пустой строки. Игроки по очереди приписывают в конец строки по одной бук- ве так, чтобы полученная строка совпадала с началом одного из заданных слов. Выигрывает тот, кто первым составит слово из набора. **18.Проект. Для игры, описанной в предыдущей задаче, напишите программу, которая определяет, кто выиграет при безошибочной игре: первый игрок (который делает первый ход) или второй. §8 Поиск в массивах Ключевые слова: • линейный поиск • максимальный элемент Линейный поиск Очень часто требуется найти в массиве заданное значение или сооб- щить, что его там нет. Для этого нужно просмотреть все элемен- ты массива с первого до последнего. Как только будет найден эле- мент, равный заданному значению X, надо завершить поиск и вывести результат. Такой алгоритм называется линейным поиском. Кажется очевидным такое решение: i = О while А [ i ] != X: i += 1 print ( "А[{}]={}".format(i, X) ) Оно хорошо работает, если нужный элемент в массиве есть, одна- ко приведёт к ошибке, если такого элемента нет, — получится за- цикливание и выход за границы массива. Поэтому в условие нужно добавить ещё одно ограничение: i < N, где через N обозначен размер массива. Если после окончания цикла это условие будет нарушено, то поиск был неудачным — элемента нет: i = О while i < N and A[i] != X: i += 1 if i < N: print( "A[{}] = {}".format(i, X) ) else: print( "He нашли!" ) 64
Поиск в массивах §8 Отметим одну тонкость. При i >= N элемента А [ i ] в массиве нет. В этом случае проверка условия А [ i ] ! =Х приведёт к выходу за грани- цы массива, и программа завершится аварийно. К счастью, этого можно избежать. Если первая часть сложного условия с операцией and ложна, то вторая часть не проверяется1* — уже понятно, что всё условие тоже ложно. Поэтому в сложном усло- вии i < N and А [ i ] ! = X сначала нужно записать именно отношение i < N. Если это условие ложно, цикл сразу завершится. Возможен ещё один поход к решению этой задачи. Используя цикл по переменной, можно перебрать все элементы массива и досрочно завершить цикл, если найдено требуемое значение: nX = -1 for i in range(N): if A[i] == X: nX = i break if nX >= 0: print ( "A[{}]={}".format(nX, X) ) else: print ( "He нашли!" ) Для выхода из цикла используется оператор break, номер найденно- го элемента сохраняется в переменной пХ. Если её значение осталось равным —1 (не изменилось в ходе выполнения цикла), то в массиве нет элемента, равного X. Последний пример можно упростить, используя особые возможности цикла for в языке Python: for i in range(N): if A[i] == X: print ( "A[{}] = {}".format(i, X) ) break else: print ( "He нашли!" ) Здесь мы выводим результат сразу, как только нашли нужный эле- мент, а не после цикла. Слово else после цикла for начинает блок, который выполняется при нормальном завершении цикла (без при- менения break). Таким образом, сообщение «Не нашли!» будет вы- ведено только тогда, когда условие в операторе if ни разу не выпол- нится. * * Во многих современных языках (например, в Python, C++, JavaScript, PHP) такое поведение гарантировано стандартом. 65
1 Программирование на языке Python В языке Python возможен и другой способ решения этой задачи, использующий встроенные методы для массивов (списков). Оператор in определяет, есть ли нужный элемент в массиве. Он возвращает логи- ческое значение (True или False): if X in А: print( "Нашли!" ) else: print ( "He нашли!" ) А метод index возвращает индекс первого найденного элемента, рав- ного X: nX = A.index( X ) Поэтому поиск элемента, равного X, и определение его индекса мож- но записать так: if X in А: nX = A.index( X ) print( "А[{}] = {}".format(i, X) ) else: print( "He нашли!" ) Поиск максимального элемента в массиве Представьте себе, что вы по очереди заходите в N комнат, в каждой из которых лежит арбуз. Вес арбузов такой, что вы можете унести только один арбуз. Возвращаться в ту комнату, где вы уже побывали, нельзя. Как выбрать самый большой арбуз? Итак, вы вошли в первую комнату. По-видимому, нужно забрать лежа- щий в ней арбуз. Действительно, вдруг он самый большой? А вернуться сюда вы уже не сможете. С этим первым арбузом идёте во вторую комна- ту и сравниваете, какой арбуз больше — тот, который у вас в руках, или новый. Если новый больше, берёте его, а старый оставляете во второй комнате. Теперь в любом случае у вас в руках оказывается самый боль- шой арбуз из первых двух комнат. Действуя так же и в остальных ком- натах, вы гарантированно выберете самый большой арбуз из всех. На этой идее основан и поиск максимального элемента в массиве. Для хранения максимального элемента используем целочисленную пе- ременную М. Будем в цикле просматривать все элементы массива один за другим. Если очередной элемент массива больше, чем максималь- ный из предыдущих (находящийся в переменной М), запомним новое значение максимального элемента в переменной М. На языке Python этот алгоритм запишется так: for i in range(N): if A[i] > M: M = A[i] print( M ) 66
Поиск в массивах §8 Остаётся решить, каким должно быть начальное значение М. Во-первых, сначала можно записать в переменную М значение, заве- домо меньшее, чем значение любого из элементов массива. Например, если в массиве записаны натуральные числа, можно записать в М ноль (максимальный элемент всегда будет больше нуля). Если же содержимое массива неизвестно, можно сразу записать в м значение А [ 0 ] (сразу взять первый арбуз), а цикл перебора начать со второго по счёту элемента, А [ 1 ]: М = А[0] for i in range(1, N): if A[i] > M: M = A[i] print( M ) Более аккуратно смотрится вариант с другим типом цикла for: М = А[0] for х in А: if х > М: М = х print( М ) Теперь найдём номер максимального элемента. Казалось бы, нужно ввести ещё одну переменную пМах для хранения номера, сначала запи- сать в неё 0 (считаем первый элемент максимальным) и затем, когда найдём новый максимальный элемент, запомнить его номер в перемен- ной пМах: М = А[0] пМах = О for i in range (1, N) : if A[i] > M: M = A[i] nMax = i print ( "A[{}]={}".format(пМах, M) ) Однако это не самый лучший вариант. В этой программе можно обойтись без одной из переменных. По номеру элемента i можно всегда определить значение этого эле- мента: оно равно А [ i ]. Поэтому достаточно хранить только номер мак- симального элемента пМах, а его значение заменить на А[пМах]: пМах = О for i in range (1, N) : if A[i] > A[nMax]: nMax = i print ( "A[{}] = {}".format(nMax, A[nMax]) ) 67
1 Программирование на языке Python Для поиска максимума можно использовать и встроенные функции Python: сначала найти максимальный элемент, а потом его индекс с помощью функции index: М = max( А ) nMax = A.index( М ) print( "А[{}]={}".format(nMax, М) ) В этом случае фактически придётся выполнить два прохода по мас- сиву. Однако такой вариант работает быстрее, чем «рукописный» цикл с одним проходом, потому что встроенные функции написаны на язы- ке С и подключаются в виде готового машинного кода, а не выполня- ются относительно медленным интерпретатором Python. Максимальный элемент, удовлетворяющий условию Более сложная задача — найти максимальное значение не из всех эле- ментов массива, а только из тех, которые удовлетворяют некоторому условию. Если вернуться к примеру с поиском арбуза: в некоторых комнатах лежат не арбузы, а дыни, но нужно найти именно самый большой арбуз. Это значит, что мы выбираем новый элемент, если он: а) подходит нам по условию отбора и б) больше, чем максимальный, найденный до этого. Рассмотрим конкретную задачу: найти максимальный из отрицатель- ных элементов массива. Самое сложное — определить, каким должно быть начальное значение М. «Очевидное» решение М = А[0] for х in А: if х < 0 and х > М: М = х print( М ) работает не всегда. В этом легко убедиться на примере массива на рис. 1.11. 0 12 3 4 5 I 8 3 -1 Рис. 1.11 Выполнив ручную прокрутку этой программы (сделайте это самостоя- тельно), мы обнаружим, что найти максимальный отрицательный эле- мент не удалось. Дело в том, что элемент А[0] — положительный, он оказывается больше всех подходящих элементов, и программа выводит его как (неверный) результат! 68
Поиск в массивах §8 Попытаемся исправить программу. Сначала предположим, что значе- ния элементов массива ограничены, т. е. принадлежат некоторому от- резку [а, &]. В этом случае достаточно выбрать любое начальное значе- ние М, меньшее, чем а. При этом все элементы массива будут больше этого числа, и когда при переборе встретится первое подходящее чис- ло, значение М будет изменено. Например, при а = -1000 можно ис- пользовать такой вариант: М = -1001 # любое, меньше -1000 for х in А: if х < 0 and х > М: М = х if М == -1001: print ( "Нет отрицательных!" ) else: print( М ) Если ни одного отрицательного элемента в массиве нет, то условие в теле цикла не выполнится ни разу. Поэтому в переменной М останется начальное значение -1001. Эту задачу можно решить иначе, не используя информацию о диа- пазоне значений элементов массива. Сначала запишем в переменную М значение А[0]. Оно может быть и неподходящим (положительным). Пусть при переборе мы нашли отрицательный элемент х массива. Будем заменять значение М на х, если выполняется одно из двух усло- вий: 1) М > 0 (нашли самый первый подходящий элемент); 2) х > М (нашли новый максимум). Получается такая программа: М = А[0] for х in А: if х < 0: if М >=0 or х > М: М = х if М >= 0: print ( "Нет отрицательных!" ) else: print( М ) Если в переменной М осталось неотрицательное значение, это значит, что отрицательных элементов в массиве нет. Есть и ещё один вариант — использовать счётчик отрицательных элементов (в программе он называется count): 69
1 Программирование на языке Python count = О for х in А: if х < 0: if count == 0 or x > M: M = x count += 1 if count == 0: print( "Нет отрицательных!" ) else: print( M ) Если мы нашли отрицательный элемент и счётчик равен 0 (это пер- вый отрицательный элемент!), выполняется условие во втором услов- ном операторе, и в переменную М записывается начальное значение х. Обратите внимание, что цикл for х in А не позволяет определить индекс найденного максимального отрицательного элемента в массиве. Приёмы, с которыми мы сейчас познакомились, работают практичес- ки во всех языках программирования. Решение «в стиле Python» получается значительно короче: мы сна- чала отбираем все отрицательные элементы в новый массив, а затем находим в нём максимальное значение. В = [х for х in A if х < 0] if В: print( max(В) ) else: print ( "Нет таких!" ) Здесь условие в операторе if В выполняется тогда, когда массив В непустой, т. е. len (В) > 0. Индекс максимального отрицательного эле- мента в исходном массиве можно найти с помощью метода index: М = max( В ) nmax = A.index( М ) Выводы • Линейный поиск — это перебор всех элементов массива до тех пор, пока не будет найден нужный элемент или не закончится массив. • При поиске максимального элемента используется вспомогатель- ная переменная, в которой хранится максимальное из значений уже просмотренных элементов массива. • При поиске максимального из элементов, удовлетворяющих усло- вию, можно сначала отобрать все подходящие элементы в новый массив. 70
Поиск в массивах §8 • При решении задач обработки массивов в Python лучше исполь- зовать встроенные функции и методы, так как они написаны на языке С и работают быстрее, чем код на Python. Вопросы и задания 1. Заполните массив случайными целыми числами на отрезке [0; 4] и выведите на экран номера всех элементов, равных значению X (оно вводится с клавиатуры). 2. Алиса начала программу поиска максимального элемента строкой М = A[N-1] Закончите программу Алисы (не используя функцию max). 3. Елисей начал программу поиска минимального элемента строкой М = A[N//2] Закончите программу Елисея (не используя функцию min). 4. Объясните, почему при поиске максимального элемента и его номера можно не запоминать само значение максимального элемента. 5. Фёдор написал программу для поиска минимального элемента в мас- сиве А. Вот её фрагмент: М = 1000 for х in А: if х < М: И = х print( М ) Определите, в каких случаях такая программа найдёт правиль- ное значение минимального элемента, а в каких — неправильное. Исправьте ошибки в алгоритме. 6. Степан написал программу для поиска индекса максимального чёт- ного элемента в массиве А. Вот её фрагмент: м = о for i in range(len(А)): if A[i] % 2 == 0 and A[i] > M: M = A[i] nMax = i print( nMax ) Определите, в каких случаях такая программа найдёт правиль- ное значение минимального элемента, а в каких — неправильное. Исправьте ошибки в алгоритме. 7. Напишите программу, которая заполняет массив из 20 элементов случайными числами на отрезке [50; 150] и находит в нём мини- мальный и максимальный элементы и их индексы. 8. Напишите программу, которая находит в массиве минимальный из чётных элементов и его индекс. Если в массиве нет чётных элемен- тов, нужно вывести ответ «нет». 71
1 Программирование на языке Python 9. Измените программы в последнем пункте параграфа так, чтобы най- ти не только значение, но и ин дек максимального отрицательного элемента в исходном массиве. Указание: здесь удобно применить функцию enumerate. Используя дополнительные источники, выяс- ните, как она работает. 10. Напишите программу, которая находит в массиве минимальный положительный элемент. Если в массиве нет положительных эле- ментов, нужно вывести ответ «нет». 11. Напишите программу, которая находит в массиве максимальный элемент, десятичная запись которого оканчивается на 3, и его ин- декс. Если таких элементов нет, нужно вывести ответ «нет». 12. Напишите программу, которая находит в массиве минимальный элемент, который делится на 7, и его индекс. Если таких элемен- тов нет, нужно вывести ответ «нет». 13. Напишите программу, которая находит в массиве максимальный и минимальный из элементов, которые кратны 7 и заканчивают- ся на цифру 2. Если таких элементов нет, нужно вывести ответ «нет». *14. Напишите программу, которая заполняет массив случайными трёх- значными числами и находит в нём элемент с наибольшей суммой цифр, и его индекс. 15. Введите с клавиатуры значения элементов массива и найдите два элемента, которые меньше всех остальных. Если в массиве есть несколько равных минимальных элементов, нужно найти первые два из них. *16. Напишите программу, которая за один проход по массиву нахо- дит три его различных элемента, которые меньше всех остальных («три минимума»). 17. Напишите программу, которая получает с клавиатуры значения элементов массива и выводит количество элементов, имеющих максимальное значение. *18. Заполните массив случайными числами на отрезке [10; 12] и най- дите самую длинную последовательность стоящих рядом одинако- вых элементов. 19. Напишите программу, которая заполняет массив из 20 элементов случайными числами на отрезке [100; 200] и находит в нём пару соседних элементов, сумма которых минимальна. 20. Напишите программу, которая заполняет массив из 20 элементов случайными числами на отрезке [-100; 100] и находит в каждой половине массива пару соседних элементов, сумма которых макси- мальна. 72
Используем массивы §9 §9 Используем массивы Ключевые слова: • массив • константа • рефакторинг • инициализация • глобальные переменные • случайный выбор • обработчик события • метка Игра «Стрельба по тарелкам» В этом параграфе мы напишем игру, в которой для хранения данных нам понадобятся массивы. Справа налево через экран летят «тарелки», которые изображаются как круги. Игрок должен щёлкнуть мышью по движущемуся кругу, при каждом попадании счёт увеличивается. Данные кругов-тарелок мы будем хранить в массивах. Все фигуры, нарисованные с помощью графической библиотеки Python, — это объ- екты, которые «помнят» свои координаты. Поэтому нам нужно толь- ко сохранить ссылки на эти объекты. Для этой цели заведём массив (пока пустой): plates = [] Количество тарелок обозначим как MAX_PLATES: MAX_PLATES =10 В других языках программирования для этой цели используются константы — неизменяемые величины, которым присвоены имена. К сожалению, в языке Python нет констант, поэтому мы ввели пере- менную, но договариваемся не менять её значение во время работы программы. Теперь можно создать объекты-тарелки, разместив их случайным образом на холсте (по умолчанию его ширина равна 500 пикселей, а высота — 600 пикселей). inport random from graph inport * MAX_PLATES =10 plates = [] for i in range(MAX_PLATES): r = random.randint( 10, 30 ) x = random.randint( r, 500-r ) у = random.randint( r, 600-r ) brushColor ( "blue" ) p = circle( x, y, r ) plates.append( p ) run () 73
1 Программирование на языке Python В начале программы импортируем модуль random (для работы со случайными числами) и все функции из модуля graph. В цикле определяем случайный радиус тарелки г (от 10 до 30 пик- селей), а также случайные координаты х и у — так, чтобы тарелка оказалась в пределах экрана. Затем устанавливаем синий цвет заливки, рисуем круг-тарелку и запоминаем её код в переменной р. Используя этот код, можно ме- нять свойства тарелки, например её координаты или цвет. В послед- ней строке цикла сохраняем код новой тарелки в массиве plates. Для этого используется метод append, добавляющий новый элемент в конец массива. Нашу программу уже можно запустить, но она просто нарисует начальную картинку, никакого движения не будет. Рефакторинг Давайте сразу проведём рефакторинг — попытаемся улучшить структу- ру программы так, чтобы её было легче понимать и изменять. То, что мы уже запрограммировали, — это начальные действия, которые программисты называют инициализацией. Поэтому выделим их в отдельную процедуру init, так что вся основная программа будет состоять из одного вызова процедуры init. import random from graph import * MAX_PLATES =10 plates = [] def init () : for i in range(MAX_PLATES) : r = random.randint( 10, 30 ) x = random.randint( r, 500-r ) у = random.randint( r, 600-r ) brushColor( "blue" ) p = circle ( x, y, r ) plates.append( p ) init() run() Константу MAX_PLATES и массив plates мы оставили глобальными^ для того, чтобы к ним могли обращаться все процедуры нашей про- граммы. Теперь избавимся от «магических чисел» в теле цикла: 10, 30, 500, 600. Первые два — это минимальный и максимальный радиусы таре- Вообще говоря, нужно избегать использования глобальных переменных, но при программировании на языке Python без них часто сложно обойтись. 74
Используем массивы §9 лок, следующие два — ширина и высота холста. Введём их как гло- бальные переменные в самом начале программы: WIDTH = 500 HEIGHT = 600 MIN_R =10 MAX_R =30 Для того чтобы можно было легко изменять размеры графического окна и холста, в начало процедуры init мы добавим ещё две коман- ды: windowsize меняет размеры окна, a canvasSize — размеры хол- ста для рисования: windowsize( WIDTH, HEIGHT ) canvasSize( WIDTH, HEIGHT ) Раскрасим тарелки разными цветами. Для этого введём массив используемых цветов: COLORS = ["blue", "red", "yellow", "green"] а перед рисованием тарелки будем выбирать цвет случайным образом из этого массива с помощью функции choice: color = random.choice( COLORS ) brushColor ( color ) В результате получилась такая программа: import random from graph import * WIDTH = 500 HEIGHT = 600 MAX_PLATES =10 MIN_R =10 MAX_R =30 COLORS = ["blue", "red", "yellow", "green"] plates = [] def init () : windowSize ( WIDTH, HEIGHT ) canvasSize( WIDTH, HEIGHT ) for i in range(MAX_PLATES): r = random.randint( MIN_R, MAX_R ) x = random.randint( r, WIDTH-r ) у = random.randint( r, HEIGHT-r ) color = random.choice( COLORS ) brushColor( color ) p = circle ( x, y, r ) plates.append( p ) init () run () 75
1 Программирование на языке Python Движение Итак, в массиве plates у нас записаны коды всех объектов-тарелок. С помощью этих кодов мы можем управлять тарелками, например перемещать их. Перемещение всех тарелок на 5 пикселей влево можно оформить в виде процедуры: def update () : for р in plates: moveObjectBy( p, -5, 0 ) В цикле перебираются все тарелки, для каждой из них вызывается процедура moveObjectBy — «сдвинуть объект на...». Первый аргумент процедуры — код объекта, который мы двигаем. Второй и третий — смещение по осям х и у. В нашем случае х-координата уменьшается на 5 (сдвиг влево на 5 пикселей), а //-координата не меняется. Теперь нужно сделать так, чтобы эта процедура вызывалась с не- которым периодом, скажем, 50 миллисекунд — тогда все тарелки «поедут» влево. Для этого используется команда onTimer, которой передаётся имя процедуры — обработчика события — и интервал в миллисекундах, через который её нужно вызывать: onTimer( update, 50 ) После этих изменений тарелки начнут двигаться. Но мы заметим, что они уходят влево за край окна и пропадают. Дело в том, что мы никак не отслеживаем выход за границы холста и не предпринимаем никаких мер. Что можно сделать? Один из вариантов — «зациклить» движение, т. е. сделать так, чтобы тарелка, улетевшая влево, появлялась вновь с другой стороны холста. Будем считать, что тарелка улетела, если она целиком вышла за границы экрана, т. е. х-координаты всех её точек стали меньше или равны нулю. В графической библиотеке graph есть функция coords, которая воз- вращает четыре числа — координаты левого верхнего и правого ниж- него углов объекта. Изменим процедуру update: def update(): for p in plates: moveObjectBy( p, -5, 0 ) xl, yl, x2, y2 = coords( p ) if x2 < 0: moveObjectBy( p, WIDTH+x2-xl, 0 ) Передвинув очередную тарелку, мы определяем её координаты и, если правый нижний угол вышел за границы экрана, добавляем к х-координате значение WIDTH — ширину экрана плюс ширину тарелки, равную х2-х1. В результате тарелка «выплывает» с другой стороны экрана. 76
Используем массивы §9 Меняем скорости В последнем варианте программы появилось «магическое число» -5, которое определяет скорость движения (сейчас все тарелки движутся с одной скоростью). Давайте теперь сделаем так, чтобы тарелки имели случайную скорость, скажем, от 1 до 5 пикселей за один шаг модели- рования. Определим в начале программы две константы: минимальную и максимальную скорости: MIN_V = 1 MAX_V = 5 Скорости нужно где-то хранить, выделим для этого новый массив vel (от английского velocity — скорость движения): vel = [] Заполнять этот массив будем в процедуре init: в цикл добавим строки, которые для каждой тарелки вычисляют случайную скорость v и добавляют её в массив vel: v = random.randint( MIN_V, MAX_V ) vel.append( v ) Итак, скорости мы определили, и они хранятся в массиве vel. Теперь нужно в процедуре update использовать сохранённое значе- ние скорости для выбранной тарелки вместо «магического числа» -5. Перебор вида for р in plates: уже не подходит, потому что нам нужен ещё и номер тарелки, чтобы добраться до её скорости в массиве vel. В языке Python есть функция enumerate (по-английски — перечислить), которая позволяет выпол- нить перебор так, как надо: for i, р in enumerate(plates): При каждом повторении цикла определяются две переменные: в пе- ременную i записывается номер тарелки, а в переменную р — её код. Поэтому процедуру update можно переписать так: def update () : for i, p in enumerate ( plates ) : moveObjectBy( p, -vel[i], 0 ) xl, yl, x2, y2 = coords( p ) if x2 < 0: moveObjectBy( p, WIDTH+x2-xl, 0 ) Вместо «магического числа» -5 мы написали -vel[i] — это зна- чение скорости i-й тарелки со знаком минус (движение влево, r-координата уменьшается). 77
1 Программирование на языке Python Бьём тарелки Следующая задача — «поймать» щёлчок по тарелке, увеличить счёт и убрать разбитую тарелку. Для этого в основной программе установим свой обработчик события «щелчок мышью» (по-английски — mouse click): onMouseClick( mouseClick ) Процедуру mouseClick нам предстоит написать. В ней мы перебира- ем в цикле все тарелки из массива plates и, если щёлчком мышью мы попали по очередной тарелке, убираем её с экрана: def mouseClick( event ) : for p in plates: if hit( event.x, event.y, p ): removePlate( p ) После щелчка мышью процедура mouseClick получает блок данных event, два значения в котором — это координаты мыши. Обратить- ся к ним можно с помощью точечной записи (так же, как к мето- дам обработки символьных строк): event. х — это х-координата мыши, a event .у — её ^-координата. Такие блоки данных, содержащие несколько переменных, называются структурами или записями. Координаты мыши передаются функции hit (она пока не написана), которая сравнивает их с координатами очередной тарелки р. Функция hit — логическая, она вернёт значение True («да»), если в момент щелчка курсор мыши оказался на тарелке, и False («нет»), если игрок не попал по тарелке. Если щёлкнули по тарелке, нужно убрать её с экрана и из массива plates, это будет делать процедура removePlate (она тоже пока не написана). Теперь построим логическую функцию hit. Для упрощения будем считать, что мы попали по тарелке, если координаты мыши находятся внутри прямоугольника, ограничивающего объект-тарелку. Мы можем получить координаты прямоугольника с помощью функции coords из графической библиотеки: def hit( х, у, р ) : xl, yl, х2, у2 = coords( р ) return ( xl <= х <= х2 ) and ( yl <= у <= у2 ) Функция coords возвращает четыре числа: первые два — это коор- динаты левого верхнего угла, третье и четвёртое — координаты правого нижнего угла. Наша функция hit возвращает истинное значение, если х-координата мыши находится на отрезке [хх; х2], а у-координата — на отрезке [z^; z/2]. Осталось написать процедуру removePlate, которая удаляет тарелку. Она должна выполнить две операции: удалить объект с холста (с по- мощью процедуры deleteObject графической библиотеки) и удалить 78
Используем массивы §9 из массива plates ссылку на разбитую тарелку (для этого применим метод remove): def removePlate ( р ) : deleteObject( p ) plates.remove( p ) Теперь все процедуры готовы, можно запускать и проверять про- грамму. Показываем счёт Для того чтобы показать счёт в окне программы, нужно расположить там метку — текстовую надпись, которой можно управлять из про- граммы. В основную программу добавим команду: scoreLabel = label( "Счёт: 0", 10, 10 ) Функция label принимает три аргумента: первый — текст надписи, следующие два — координаты левого верхнего угла надписи. Функция возвращает код метки, который мы запишем в глобальную перемен- ную scoreLabel. Через эту переменную мы будем управлять меткой, увеличивая счёт, когда будет разбита очередная тарелка. Но счёт нужно где-то хранить. Заведём для этого глобальную переменную score (по-английски — счёт), в самом начале она должна быть равна нулю: score = 0 Счёт нужно увеличивать тогда, когда игрок попадёт по тарелке. Поэтому добавляем в процедуру mousedick вызов ещё одной процеду- ры incScore, которую мы сейчас напишем: def mouseClick( event ): for p in plates: if hit( event.x, event.y, p ): removePlate( p ) incScore() У процедуры incScore две задачи: увеличить значение глобальной переменной score и изменить текст метки scoreLabel: def incScore () : global score score += 1 scoreLabel["text"] = "Счёт: " + str (score) Команда global говорит о том, что мы собираемся изменять зна- чение глобальной переменной score. Для изменения текста надписи мы обращаемся к свойству text метки scoreLabel, записывая назва- 79
1 Программирование на языке Python ние свойства в квадратных скобках. Функция str преобразует число в символьную строку. Получившаяся программа вполне работоспособна, хотя её можно ещё значительно улучшить. Сделайте это самостоятельно. Выводы • В массивах можно хранить свойства группы однотипных объектов (например, скорости тарелок). • Все постоянные значения (константы), которые можно использо- вать для настройки программы, удобно вводить в начале програм- мы и обозначать именами. • Инициализация — это действия, которые выполняются один раз в начале работы программы. • Состояние программы хранится в глобальных переменных. • Для выбора одного из элементов массива случайным образом используется функция choice (из модуля random). • Анимация выполняется в обработчике события onTimer (из моду- ля graph), который вызывается с заданным интервалом. • Обработчик щелчка мышью устанавливается процедурой onMouseClick (из модуля graph). При вызове он получает блок данных, в котором записаны координаты мыши. • Функция enumerate выполняет перебор элементов массива и воз- вращает пары: индекс — значение очередного элемента. • С помощью команды label (из модуля graph) добавляется мет- ка — управляемая текстовая надпись. Вопросы и задания 1. Зачем нужны константы? Какие проблемы могут возникнуть при работе с константами в языке Python? 2. Почему нужно избавляться от «магических чисел» в программе? 3. Какой метод проектирования программ применялся при разработке программы в этом параграфе? 4. Объясните, как сделан в параграфе «перескакивание» тарелки на правый край холста. 5. Как вы думаете, что произойдёт, если удалить строку global score из процедуры incScore (в параграфе)? 6. Как ускорить движение тарелок? Предложите два способа и срав- ните их. 7. Какие изменения нужно внести в программу из параграфа, чтобы тарелки летели слева направо? Сверху вниз? 80
Матрицы §10 8. Какие изменения нужно внести в программу из параграфа, чтобы тарелки не заходили в область метки со счётом? 9. Доработайте программу из параграфа так, чтобы функция hit реа- гировала на щелчок именно по кругу, а не по охватывающему пря- моугольнику. 10. Доработайте программу из параграфа так, чтобы попадание в мень- шую по размеру тарелку давало большее увеличение счёта. 11. Составьте список недостатков программы, которая рассмотрена в тексте параграфа. Попытайтесь исправить их. §10 Матрицы Ключевые слова: • матрица • строка • столбец • вложенный цикл • квадратная матрица • главная диагональ • побочная диагональ • перебор элементов • перестановка строк (столбцов) Что такое матрица? Многие программы работают с данными, организованными в виде таб- лиц. Например, при составлении программы для игры в крестики- нолики нужно запоминать состояние каждой клетки квадратной доски. Пустым клеткам присвоим код -1, клетке, где стоит нолик, — код О, а клетке с крестиком — код 1. Тогда информация о состоянии поля записывается в виде таблицы (рис. 1.12). -1 0 1 -1 0 1 0 1 -1 0 12 о 1 2 Рис. 1.12 Такие таблицы называются матрицами или двумерными массивами. Матрица — это прямоугольная таблица, составленная из элементов одного типа (чисел, строк и т. д.). О 81
1 Программирование на языке Python Каждый элемент матрицы, в отличие от линейного массива, имеет два индекса — номер строки и номер столбца. Это похоже на коор- динаты пикселя растрового рисунка или точки на плоскости. На ри- сунке 1.12 серым фоном выделен элемент, находящийся на пересече- нии строки 1 и столбца 2, он обозначается в программе как А[1] [2]. Нумерация строк и столбцов, как и индексов одномерных массивов, в языке Python начинается с нуля. Создание матрицы Поскольку в Python нет массивов, нет и матриц в классическом понимании. Для того чтобы работать с таблицами, используют списки. Двумерная таблица хранится как «список списков» — список, каждый элемент которого тоже представляет собой список. Например, таблицу, показанную на рис. 1.12, можно записать так: А = [ [-1, 0, 1] , [-1, 0, 1], [О, 1, -1]] или в одну строку: А = [ [-1, 0, 1] , [-1, 0, 1] , [0, 1, -1] ] Конечно, первый способ более нагляден. Иногда нужно создать в памяти матрицу заданного размера, запол- ненную некоторыми начальными значениями, например нулями1*. Пер- вая мысль — использовать такой алгоритм, использующий операцию повторения *: N = 3 М = 2 # неверное создание матрицы! row = [0]*М # создаём список-строку длиной М А = [row]*N # создаём массив (список) из N строк Однако этот способ работает неверно из-за особенностей языка Python. Например, если после этого выполнить присваивание А[0][0] = 1 мы увидим, что все элементы столбца 0, т. е. А[0] [0], А[1] [0] и т. д. стали равны 1. Дело в том, что матрица — это список ссылок на списки-строки (список адресов строк). При выполнении оператора row = [0]*М Для работы с матрицами и решения сложных вычислительных задач лучше использовать пакет NumPy (www.numpy.org), обсуждение которого выходит за рамки пособия. 82
Матрицы §10 компилятор создаёт в памяти одну единственную строку, дующий оператор а затем сле- А = [row]*N устанавливает на эту (рис. 1.13). единственную строку все ссылки в массиве А А Рис. 1.13 Естественно, что когда мы меняем элемент с индексом 0 в строке 0 (он выделен фоном на рис. 1.13), меняются и все элементы с индексом 0 во всех строках. Для создания полноценной матрицы нам нужно как-то заставить транслятор создать все строки в памяти как разные объекты. Для это- го сначала построим пустой список, а потом будем в цикле добавлять к нему (с помощью метода append) новые строки, состоящие из нулей: А = [] for i in range(N): A.append( [0]*M ) или так, с помощью генератора: А = [[0]*М for i in range(N)] Теперь все строки расположены в разных областях памяти (рис. 1.14). Каждому элементу матрицы можно присвоить любое значение. Поскольку индексов два, для заполнения матрицы нужно использовать вложенный цикл. Далее будем считать, что существует матрица А, состоящая из N строк и м столбцов, a i и j — целочисленные переменные, обознача- ющие индексы строки и столбца. В этом примере матрица заполняется случайными числами из отрезка [20; 80]: 83
1 Программирование на языке Python import random А = [ [ 0]*М for i in range(N)] for i in range(N): for j in range(M): A[i][j] = random.randint( 20, 80 ) Вывод матрицы на экран Простейший способ вывода матрицы — с помощью одного вызова функции print: print( А ) В этом случае матрица выводится в одну строку, что не очень удоб- но. Поскольку человек воспринимает матрицу как таблицу, лучше и на экран выводить её в виде таблицы. Для этого можно написать такую процедуру (в классическом стиле): def printMatrix( А ): for i in range(len(A)): for j in range(len(A[i])): print( "{:4d}". format(A[i] [j]), end="" ) print() Здесь i — это номер строки, a j — номер столбца; len (A) — это число строк в матрице, a len(A[i]) — число элементов в строке i (совпадает с числом столбцов). После вывода очередного элемента матрицы мы не делаем перевод строки на экране (end=""), а после вывода всей строки — делаем с по- мощью вызова функции print. Можно написать такую функцию и «в стиле Python»: def printMatrix( А ) : for row in A: for x in row: print ( "{:4}".format(x), end="" ) print () Первый цикл перебирает все строки в матрице; каждая из них по очереди попадает в переменную row. Затем внутренний цикл перебира- ет все элементы этой строки и выводит их на экран. Перебор элементов матрицы Каждый элемент матрицы имеет два индекса, поэтому для перебора всех элементов нужно использовать вложенный цикл. Обычно внешний цикл перебирает индексы строк, а внутренний — индексы столбцов. Вот так можно найти сумму всех элементов матрицы: 84
Матрицы summa = О for i in range(N): for j in range(M): summa += A[i][j] print ( summa ) Эту задачу можно красиво решить «в стиле Python»: summa = О for row in А: summa += sum( row ) print( summa ) Здесь в цикле перебираются все строки матрицы А, каждая из них по очереди записывается в переменную row. В теле цикла сумма эле- ментов очередной строки прибавляется к значению переменной summa. Квадратные матрицы На практике (например, при решении шахматных задач) часто при- ходится работать с квадратными матрицами, у которых количество строк и количество столбцов одинаковое. Для квадратной матрицы используют понятия «главная диаго- наль» (выделенные клетки на рис. 1.15, а) и «побочная диагональ» (рис. 1.15, б). На рисунке 1.15, в выделена главная диагональ и все элементы под ней. Пусть матрица А содержит N строк и столько же столбцов. Главная диагональ — это элементы А[0][0], А[1][1], ..., A[N-1] [N-1], т. е. элементы, у которых номер строки равен номеру столбца. Для перебо- ра этих элементов нужен один цикл: for i in range(N): # работаем c A[i][i] Элементы побочной диагонали — это А[0] [N-1], A[l] [N-2], ..., A[N-1] [0]. Заметим, что сумма номеров строки и столбца для каждого из этих элементов равна N-1, поэтому получаем такой цикл перебора: for i in range(N): # работаем c A[i][N-l-i]
Программирование на языке Python Для обработки всех элементов на главной диагонали и под ней (рис. 1.15, в) нужен вложенный цикл: номер строки будет меняться от О до N-1, а номер столбца для каждой строки i — от 0 до i: for i in range(N): for j in range(i+1): # работаем c A[i] [j] Чтобы переставить столбцы матрицы, достаточно одного цикла. Например, переставим столбцы с индексами 2 и 4, используя вспомо- гательную переменную temp: for i in range (N) : temp = A[i][2] A[i] [2] = A[i] [4] A[i] [4] = temp или, используя множественное присваивание в Python: for i in range(N): A[i][2],A[i][4] = A[i][4],A[i][2] Переставить две строки можно вообще без цикла, учитывая, что A[i] — это ссылка на список элементов строки i. Поэтому достаточно просто переставить ссылки. Оператор А[0] , А [3] = А[3] ,А[0] переставляет строки матрицы с индексами 0 и 3. Для того чтобы создать копию строки с индексом i, нельзя делать так: R = A[i] # это неверно! потому что при этом мы получим новую ссылку на существующую строку. Вместо этого нужно создать копию в памяти с помощью среза, включающего все элементы строки: R = A[i][:] Построить копию столбца с индексом j несколько сложнее, так как матрица расположена в памяти по строкам. В этой задаче удобно использовать генератор: С = [row[j] for row in A] В цикле перебираются все строки матрицы А, они по очереди попадают в переменную row. Генератор выбирает из каждой строки элемент с индексом j и составляет список из этих значений. С помощью генератора легко выделить в отдельный массив элемен- ты главной диагонали: D = [A[i][i] for i in range(N)] 86
Матрицы §10 Выводы • Матрица — это прямоугольная таблица, составленная из элемен- тов одного типа (чисел, строк и т. д.). • Каждый элемент матрицы имеет два индекса — номера строки и столбца. Нумерация строк и столбцов в языке Python начинается с нуля. • Для перебора всех элементов матрицы требуется вложенный цикл: в одном цикле перебираются индексы строк, во втором — индек- сы столбцов. • Главная диагональ квадратной матрицы — это элементы, у кото- рых индекс строки равен индексу столбца. • Побочная диагональ квадратной матрицы — это элементы А[0][ЛГ-1], А[1][ЛГ-2], ..., A[N - 1][0]. Вопросы и задания 1. Сравните понятия «массив» и «матрица». 2. Как вы думаете, можно ли считать, что первый индекс элемента матрицы — это номер столбца, а второй — номер строки? Что от этого изменится? © 3. Почему суммирование элементов на главной диагонали квадратной матрицы требует одиночного цикла, а суммирование элементов под главной диагональю — вложенного? 4. Напишите фрагмент программы, который вычисляет количество ненулевых элементов матрицы в переменной count. 5. Напишите фрагмент программы, который вычисляет сумму всех элементов главной диагонали квадратной матрицы в переменной summa. 6. Напишите фрагмент программы, который вычисляет количество ненулевых элементов побочной диагонали квадратной матрицы. 7. Напишите фрагмент программы, который вычисляет среднее ариф- метическое элементов квадратной матрицы, находящихся на глав- ной диагонали и под ней. 8. Напишите программу, которая находит максимальный элемент на главной диагонали квадратной матрицы. 9. Напишите программу, которая находит максимальный элемент матрицы и его индексы (номера строки и столбца). 10. Напишите программу, которая находит минимальный из чётных положительных элементов матрицы. Учтите, что таких элементов в матрице может и не быть. 11. Напишите программу, которая выводит на экран строку матрицы, сумма элементов которой наибольшая. 12. Напишите программу, которая выводит на экран столбец матрицы, сумма элементов которого наименьшая. 87
1 Программирование на языке Python 13. Напишите программу, которая заполняет матрицу из N строк и М столбцов нулями и единицами в шахматном порядке. 14. Напишите программу, которая заполняет матрицу из N строк и N столбцов нулями и единицами так, что все элементы выше глав- ной диагонали равны нулю, а остальные — единице. 15. Напишите программу, которая заполняет матрицу из N строк и N столбцов нулями и единицами так, что все элементы выше побоч- ной диагонали равны нулю, а остальные — единице. 16. Напишите программу, которая заполняет матрицу размером 7x7 случайными числами, а затем записывает в элементы, отмеченные на рисунках фоном, число 0. ными числами по спирали и змейкой, как на рисунках. те её транспонирование: так называется процедура, в результате 88
Сложность алгоритмов которой строки матрицы становятся столбцами, а столбцы — стро- элемент и удаляет строку и столбец, в которых он расположен. 23. Заполните матрицу из N строк и М столбцов случайными двоич- ными значениями (каждый элемент может быть равен 0 или 1) и добавьте к ней ещё один столбец (столбец чётности) так, чтобы количество единиц в каждой строке было чётным. * *24.Проект. Напишите программу, которая играет с человеком в крестики-нолики на поле размером 3x3. * *25. Проект. Напишите программу, которая обходит квадратную шах- матную доску заданного размера ходом коня. Предварительно узнайте из дополнительных источников про правило Вайсдорфа. * *26.В матрице, содержащей N строк и М столбцов, записана карта островного государства Лимония (нули обозначают море, а едини- цы — сушу). Все острова имеют форму прямоугольника. Напи- шите программу, которая по готовой карте определяет количество островов. Интересный сайт numpy.org — пакет для научных исследований в Python, содержа- щий быстрые алгоритмы обработки матриц §11 Сложность алгоритмов Ключевые слова: временная сложность пространственная сложность время работы алгоритма асимптотическая сложность линейная сложность квадратичная сложность 89
Программирование на языке Python Как сравнивать алгоритмы? Для решения большинства задач известны разные алгоритмы решения. Поэтому возникает вопрос: как выбрать лучший? И ещё один важный вопрос: способен ли современный компьютер за приемлемое время най- ти решение задачи? Например, в игре в шахматы возможно лишь ко- нечное количество позиций и, значит, только конечное количество раз- личных партий. Значит, теоретически можно перебрать все возможные партии и выяснить, кто побеждает при правильной игре — белые или чёрные. Однако количество вариантов настолько велико, что современ- ные компьютеры не могут выполнить такой перебор за приемлемое время. Что мы хотим от алгоритма? Во-первых, чтобы он работал как мож- но быстрее. Во-вторых, чтобы объём необходимой памяти был как можно меньше. В-третьих, чтобы алгоритм был как можно понят- нее — это упрощает отладку программы. К сожалению, эти требования противоречивы, и в серьёзных задачах редко удается найти алгоритм, который был бы лучше остальных по всем показателям. Часто говорят о временнбй сложности алгоритма (быстродействии) и пространственной сложности, которая определяется объёмом необ- ходимой памяти. Временем работы алгоритма называется количество элементарных операций Т, выполненных исполнителем. Элементарной операцией мы будем считать действие, которое не раз- бивается на более простые действия (например, присваивание, сложе- ние, умножение). Такой подход позволяет оценивать именно качество алгоритма, а не свойства исполнителя (например, быстродействие компьютера, на кото- ром выполняется алгоритм). При этом мы считаем, что время выпол- нения всех элементарных операций одинаково. Как правило, величина Т будет существенно зависеть от объёма исходных данных: поиск в списке из 10 элементов завершится гораз- до быстрее, чем в списке из 10000 элементов. Поэтому время работы алгоритма зависит от размера входных данных N. Например, для ал- горитмов обработки массивов в качестве размера N используют дли- ну массива. Функция T(N) называется временнбй сложностью алго- ритма. Пространственная сложность — это зависимость объёма занимаемой памяти от размера данных N. Для ускорения работы некоторых ал- горитмов нужно использовать дополнительную память, которая может быть намного больше, чем память для хранения исходных данных. Поскольку память постоянно дешевеет, а быстродействие компью- теров растёт медленно, более важна временная сложность алгоритмов. Пространственную сложность мы дальше рассматривать не будем. 90
Сложность алгоритмов §11 Примеры Определим временную сложность различных алгоритмов обработки мас- сива А длины N и квадратной матрицы размером N х N. Пример 1. Вычислить сумму первых трёх элементов массива (при N>3). Решение этой задачи содержит всего один оператор: summa = А[0] + А[1] + А[2] Этот алгоритм включает две операции сложения и одну операцию присваивания (записи значения в память), поэтому его сложность T(N) = 3 не зависит от размера массива вообще. Пример 2. Вычислить сумму всех элементов массива. В этой задаче уже не обойтись без цикла: summa = О for х in А: summa += х Здесь выполняется N операций сложения и N 4- 1 операций присва- ивания, поэтому его сложность T(N) = 2N 4- 1 возрастает линейно с увеличением длины массива. Пример 3. Найти сумму элементов квадратной матрицы А размером N х N. Стандартный алгоритм использует вложенный цикл: summa = О for i in range(N): for j in range(N): summa += A[i][j] Если не считать «расходы» на организацию циклов (т. е. операции с переменными i и j), здесь выполняется N2 операций сложения и №4-1 операций присваивания. По результатам этих примеров можно сделать выводы: • простой цикл, в котором количество итераций пропорционально № — это алгоритм линейной сложности (T(N) — линейная функ- ция от N); • вложенный цикл, в котором количество итераций внешнего и внутреннего цикла пропорционально N, — это алгоритм квадра- тичной сложности (T(N) — квадратичная функция от N). Что такое асимптотическая сложность? Допустим, что нужно сделать выбор между несколькими алгоритмами, которые имеют разную сложность. Какой из них лучше, т. е. работает 91
1 Программирование на языке Python быстрее? Оказывается, ответ на этот вопрос зависит от размера масси ва данных. Сравним три алгоритма, сложность которых: T\(N) = 10 000 • ЛГ, T2(N) = 100 • N2, T3(N) = N3. Построим эти зависимости на графике (рис. 1.16). При N < 100 получаем T3(N) < T2(N) < при N = 100 количест- во операций для всех трёх алгоритмов совпадает, а при больших N имеем T3(N) > T2(N) > T^N). Обычно в теоретической информатике при сравнении алгоритмов используется их асимптотическая сложность, т. е. скорость роста количества операций при больших значениях N. Запись O(N) (читается «О большое от 7V») обозначает линейную сложность алгоритма. Это значит, что, начиная с некоторого значения N = Nq, количество операций будет меньше, чем с • N, где с — некото- рая постоянная: T(N) <c-N для N>Nq. При увеличении размера данных в 10 раз объём вычислений алго- ритма с линейной сложностью увеличивается тоже примерно в 10 раз. Пусть, например, T(N) = 2N + 1, как в алгоритме поиска суммы элементов массива. Очевидно, что при этом T(N) < 3N для всех N > 1, поэтому алгоритм имеет линейную сложность. Многие известные алгоритмы имеют квадратичную сложность O(N2). Это значит, что сложность алгоритма при больших N меньше, чем с • N2: T(N) < с • N2 для N > ЛГ0. Если размер данных увеличивается в 10 раз, то количество опера- ций (и время выполнения такого алгоритма) увеличивается примерно в 100 раз. Пример алгоритма с квадратичной сложностью — вычисление суммы элементов квадратной матрицы. 92
Сложность алгоритмов §11 Алгоритм имеет асимптотическую сложность если найдётся такая постоянная с, что, начиная с некоторого N = No, выполняется условие T(N) < с • f(N). О Это значит, что график функции с • f(N) проходит выше, чем график функции T(N), по крайней мере, при N > No (рис. 1.17). Если количество операций не зависит от размера данных, то гово- рят, что асимптотическая сложность алгоритма — 0(1), т. е. количе- ство операций меньше некоторой постоянной при любых N. Существует также немало алгоритмов с кубической сложностью, O(N3). При больших значениях N алгоритм с кубической сложностью требует большего количества вычислений, чем алгоритм со сложно- стью O(N2), а тот, в свою очередь, работает дольше, чем алгоритм с линейной сложностью. Однако при небольших значениях N всё может быть наоборот, это зависит от постоянной с для каждого из алгорит- мов. Известны и алгоритмы, для которых количество операций растёт быстрее, чем любой многочлен, например как O(2N) или даже O(Nl), где NI — это факториал числа N (произведение всех натуральных чи- сел от 1 до N). Они встречаются чаще всего в задачах поиска опти- мального (наилучшего) варианта, которые решаются только методом полного перебора. Самая известная задача такого типа — это задача коммивояжёра (бродячего торговца), который должен посетить по од- ному разу каждый из N городов и вернуться в начальную точку. Для него нужно выбрать маршрут, при котором стоимость поездки (или общая длина пути) будет минимальной. В таблице на рис. 1.18 показано примерное время работы алгорит- мов, имеющих разную временную сложность, при N = 100 на компью- тере с быстродействием 1 миллиард операций в секунду. 93
1 Программирование на языке Python T(N) Время выполнения N 100 нс N2 10 мс N3 0,001 с 2n 1013 лет N\ 10141 лет Рис. 1.18 Выводы • Временем работы алгоритма называется количество элементарных операций Т, выполненных исполнителем. • Временная сложность алгоритма обычно зависит от объёма исход- ных данных N, например от размера массива. • Пространственная сложность — это объём памяти, необходимой для работы алгоритма. • Простой цикл, в котором количество итераций пропорционально N, — это алгоритм линейной сложности. • Вложенный цикл, в котором количество итераций внешнего и внутреннего циклов пропорционально А, — это алгоритм квадра- тичной сложности. • Алгоритм имеет асимптотическую сложность если найдёт- ся такая постоянная с, что, начиная с некоторого N = No, выпол- няется условие T(N) < с • f(N). • Линейная сложность означает, что при увеличении размера масси- ва в К" раз количество операций увеличивается примерно в К раз. • Квадратичная сложность означает, что при увеличении размера массива в К раз количество операций увеличивается примерно в К2 раз. Вопросы и задания 1. Какие критерии используются для оценки качества алгоритмов? 2. Почему скорость работы алгоритма оценивается не временем выпол- нения, а количеством элементарных операций? 3. Как учитывается размер данных при оценке быстродействия алго- ритма? 4. Временная сложность алгоритма определяется функцией T(N) = 2N3. Во сколько раз увеличится время работы алгоритма, если размер данных N увеличится в 10 раз? 94
Сложность алгоритмов §11 5. Для быстрой сортировки массива из 2V элементов с помощью ал- горитма Семёна требуется N вспомогательных массивов, каждый из которых содержит N элементов. Как изменится объём нужной дополнительной памяти, если N увеличится в 10 раз? 6. Вычислите количество операций (считая сравнения и присваивание значений переменным) при выполнении фрагмента программы. а = 8 Ь = 15 if а < b: с = 2*а + Ъ else: с = 2*Ь + а; 7. Вычислите количество операций при выполнении фрагмента про- граммы. а = О Ь = 1 for i in range(N): a += b*b b += 1 8. В каких случаях алгоритм, имеющий асимптотическую сложность O(N2), может работать быстрее, чем алгоритм с асимптотической сложностью O(N)? 9. Оцените асимптотическую сложность алгоритмов: а) вычисления произведения первого и последнего элементов мас- сива; б) вычисления суммы элементов первой половины массива; в) нахождения минимального и максимального элементов массива; г) определения количества положительных элементов массива; д) определения количества нулевых элементов квадратной матрицы; е) поиска всех делителей числа ЛГ. Считайте, что массив содержит N элементов, а матрица имеет раз- мер N х N. 10. Определите любые подходящие значения с и No, такие что T(N) < С' N для N > No, для алгоритмов с линейной асимптотичес- кой сложностью: a) T(N) < 12N - 8; б) T(N) < 7N + 5. 11. Определите любые подходящие значения с и No, такие что T(N) < с • N2 для N > No, для алгоритмов с квадратичной асимптоти- ческой сложностью: а) Т(ЛГ) < 5№ - 3N - 8; б) T(N) < 7N2 + 5ЛГ. 12. Определите асимптотическую сложность алгоритмов, для которых известно количество операций: a) T(N) = 5 • W + 6; в) T(N) = 2 • N3 + 100; б) T(N) = 3 • N2 + 2 • N + 19; г) T(N) = 4 • 2N + 25 • N1S + 8. 95
1 Программирование на языке Python 13. Алгоритм обработки массива имеет асимптотическую сложность O(N2), где N — длина массива. Во сколько раз увеличится время выполнения алгоритма, если длина массива увеличится в 5 раз? *14. Алгоритм обработки массива имеет асимптотическую сложность O(2W), где N — длина массива. Как увеличится время выполне- ния алгоритма, если длина массива увеличится на 5 элементов? В 5 раз? 15. Юный программист Григорий поспорил с учителем, что сможет с помощью компьютера решить сложную задачу перебора вариантов к завтрашнему уроку. Дома он определил временную сложность алгоритма: T(N) = 2N. Для какого наибольшего значения N сможет Григорий решить задачу за сутки, если его компьютер выполняет 1 миллиард операций в секунду? 96
Глава 2 ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ C++ В этой главе мы продолжим знакомство со вторым языком програм- мирования — C++, который начали изучать в прошлом году. Основ- ной теоретический материал был изложен ранее, при изучении языка Python, здесь мы будем рассматривать решения аналогичных задач с помощью C++ и отличие этих решений от Python. §12 Процедуры Ключевые слова: • процедура • локальная переменная • интерфейс • глобальная переменная • реализация • область видимости • параметр • передача по ссылке Процедура — это подпрограмма, которая не возвращает результата, но выполняет некоторые действия. Зачем нужны процедуры? Сложная программа может состоять из сотен тысяч и даже миллионов строк кода. Разбираться в такой про- грамме очень трудно, как, например, трудно читать длинный сплош- ной текст, не разделённый на главы, абзацы или пункты. В сложной программе некоторые операции (группы строк кода) по- вторяются. Каждой такой группе можно дать название и вызывать её по имени. Это сократит длину программы и облегчит работу с ней, ведь все изменения нужно будет вносить только в одном месте. По- этому большие программы обязательно разделяют на подпрограммы — процедуры и функции. Полезно и то, что подпрограмму, разработанную в одном проекте, можно использовать в других проектах. Такое повторное использова- ние кода сейчас — это общепринятый подход, сокращающий сроки создания новых программ. Простая процедура Начнём с процедуры, которая выводит на экран строку из знаков «минус». В следующей программе процедура выделена фоном: 97
2 Программирование на языке C++ #include <iostream> using namespace std; void printLine() { cout « "-------------" « endl; } int main() { printLine(); cin.get(); } Напомним, что в первой строке мы подключаем библиотеку iostream, которая позволяет работать с потоками ввода и вывода. Вторая строка программы говорит, что мы будем использовать про- странство имён std, в котором определены поток ввода cin (для вво- да данных с клавиатуры) и поток вывода cout (для вывода на экран). Далее идёт процедура с именем printLine. Слева от имени проце- дуры видим слово void, по-английски — пустой, отсутствующий. Это означает, что подпрограмма не возвращает никакого результата (на- пример, числа), т. е. это именно процедура, а не функция. Скобки после имени обязательны, но внутри скобок ничего нет — эта проце- дура не принимает никаких параметров, т. е. её работой никак нель- зя управлять. Тело процедуры заключается в фигурные скобки. Ниже процедуры записана основная программа — функция с име- нем main. Напомним, что в языке С+-I- основная программа всегда называется main (по-английски — главная), и именно с этой функ- ции начинается выполнение программы при запуске. Слово int перед именем говорит о том, что программа может передать операционной системе целое число — результат своей работы, по которому мож- но определить, была ли ошибка во время работы программы или всё было в порядке. Основная программа состоит из двух команд: вызова процедуры printLine и ожидания нажатия на любую клавишу (для того, чтобы не закрылось окно программы, пока мы не рассмотрели результат её работы). В этом примере процедура расположена выше основной программы. Поэтому, когда компилятор встречает вызов процедуры в основной программе, он уже «знает» о процедуре и может проверить, правиль- но ли мы к ней обращаемся. Почти всегда удобно размещать процедуру ниже основной програм- мы или даже вообще вынести её в другой файл, как делают в боль- ших программах. Тогда получается, что транслятор «не знает», что такая процедура есть в программе, и выдаст ошибку в той строке, где 98
Процедуры §12 она вызывается. Но выход есть — выше основной программы процеду- ру нужно объявить: #include <iostream> using namespace std; void printLine(); // объявление процедуры int main() { printLine(); cin.get (); } void printLine() { cout << "-----------" << endl; } Объявление (прототип) процедуры — это заголовок процедуры, который заканчивается точкой с запятой. Объявление — это всё, что нужно знать для использования процедуры. Оно определяет интерфейс — способ об- мена данными между процедурой printLine и вызывающими процедура- ми и функциями. Оно говорит компилятору, что: 1) в нашей программе действительно есть подпрограмма с именем printLine; 2) это процедура, она не возвращает никакого значения (void); 3) у неё нет параметров. Компилятору достаточно этой информации для того, чтобы прове- рить правильность вызова процедуры. Сама процедура (её определение, или реализация) может находить- ся где угодно — ниже основной программы или даже в другом фай- ле. Конечно, при окончательной сборке программы процедура должна быть найдена и подключена к исполняемому файлу. Процедуры с параметрами Теперь попробуем управлять процедурой, передавая ей аргумент - количество знаков «минус», которые нужно вывести. Напомним, что данные, передаваемые в процедуру, называют аргументами. Они за- писываются в переменные, указанные в заголовке процедуры. Такие переменные называются параметрами. Так же, как и в языке Python, добавляем имя параметра в скобки в заголовок процедуры, но в С++ нужно ещё указать тип параметра: void printLine( int n ) { } 99
2 Программирование на языке C++ Служебное слово int говорит о том, что процедура принимает цело- численное значение. Тип параметра важен: если мы передадим проце- дуре вещественное значение (которое можно преобразовать в целое, но с потерей дробной части), например: printLine( 4.5 ); компилятор выдаст предупреждение (англ, warning) и передаст в про- цедуру только целую часть числа (4). Если же мы попытаемся пере- дать символьную строку: printLine( "Кот Леопольд" ); компилятор выведет сообщение об ошибке, и программа не запустится. А вот один символ удастся передать: printLine( 'А' ); Дело в том, что все символы хранятся в памяти в виде числовых кодов, код латинской прописной буквы «А» — 65. Поэтому процедура получит целое число 65 (хотя вряд ли это будет иметь смысл). Теперь займёмся телом процедуры: нам нужно вывести ровно п зна- ков «минус». Операции умножения строки на число в языке C++ нет, поэтому одним оператором вывода уже не обойтись, и мы используем цикл: void printLine( int n ) { for( int i = 0; i < n; i++ ) cout « cout « endl; } Символы выводятся по одному, переменная i в цикле изменяется от О до п-1, увеличиваясь на единицу с каждым шагом (i++). Обратим внимание на переменную i. Это временная переменная, она объявляется внутри процедуры и называется локальной переменной. Другим процедурам и функциям она неизвестна. Более того, эта пере- менная объявлена прямо в заголовке цикла, поэтому она вдобавок ещё и локальная для цикла, т. е. она «живёт» лишь тогда, когда работает цикл. Если обратиться к ней после цикла (например, вывести в вы- ходной поток cout), компилятор сообщит об ошибке. Несколько параметров Давайте немного улучшим процедуру: сделаем так, чтобы можно было изменять не только длину строки, но и символы, из которых она стро- ится. Для этого добавим в процедуру ещё один параметр, который можно назвать symbol: 100
Процедуры §1 void printLine( char symbol, int n ) { for( int i = 0; i < n; i++ ) cout « symbol; cout « endl; } Теперь при вызове процедуры нужно сначала указать символ (в апо- строфах, а не в двойных кавычках!), а затем — количество этих сим- волов: printLine ( ' = 10 ); Если тип параметра symbol изменить с char (один символ) на string (символьная строка), можно будет строить цепочки из последо- вательностей, например: printLine ( "=!=", 10 ); Напомним, что символьные строки записываются в двойных кавычках. Чем больше параметров у процедуры, тем больше разных задач она может решать, но тем сложнее понимать, что она делает, и легче сде- лать ошибку. Поэтому не рекомендуется использовать в процедуре больше 3-4 параметров. Локальные и глобальные переменные Все переменные, которые объявлены в процедуре, — это её локальные переменные. Остальные процедуры и функции, а также основная про- грамма не могут к ним обратиться (изменить или прочитать значение). Область видимости локальной переменной — это блок, ограниченный фигурными скобками, внутри которого она объявлена. Основная программа в C++ — это тоже функция, поэтому все пе- ременные, объявленные внутри main, — это локальные переменные функции main. Пусть в некоторой компьютерной игре переменная state (по-английски — состояние) хранит состояние игры (например, 0 — пауза, 1 — игра идёт, 2 — игра закончена). Мы хотим изменить со- стояние игры в процедуре changestate (по-английски — изменить состояние) и делаем это так: #include <iostream> using namespace std; void changestate() { state =1; // ошибка компияции } // здесь нет переменной state! 101
2 Программирование на языке С++ int main() { int state =0; // локальная переменная в main changestate() ; cout « state; cin.get (); } Эта программа ошибочна, она не пройдёт трансляцию. Дело в том, что переменная state — это локальная переменная основной програм- мы (функции main), и в процедуре changestate она недоступна. В таком случае надо объявить глобальную переменную — доступную основной программе и всем подпрограммам. Делается это вне всех про- цедур и функций. #include <iostream> using namespace std; int state =0; // это глобальная переменная void changestate() { state = 1; } int main() { changestate() ; cout << state; cin.get(); } Такая программа успешно выполнится и выведет на экран число 1. Переменная state находится в глобальной, области видимости и доступна всем процедурам и функциям. В процедуре можно объявить переменную с таким же именем, как и у глобальной переменной. Например, объявим дополнительно локаль- ную переменную state в процедуре changestate: void changestate () { int state = 0; state =1; // меняем локальную переменную } Теперь процедура изменит значение локальной переменной, а при запуске программа выведет значение глобальной переменной state: оно по-прежнему будет равно 0, а не 1. 102
Процедуры §12 Отметим, что в языке C++ во все глобальные переменные при стар- те программы автоматически записываются нулевые значения (а в логические переменные — значение false, эквивалентное нулю). Все глобальные переменные при старте программы обнуляются. О Как же в такой процедуре «добраться» до глобальной переменной? В общем, не так сложно: нужно поставить два двоеточия перед име- нем переменной: ::state =1; // меняем глобальную переменную Но лучше не давать локальной и глобальной переменным одинаковые имена, чтобы не запутать себя и тех, кто будет разбираться в вашей программе. Теперь уберём объявление переменной state из процедуры changestate, внесём его в основную программу и не будем присваи- вать начальное значение: int main() { int state; // ничего не присваиваем, // не надо так! changestate(); cout « state; cin.get (); } Результат выполнения программы (число на экране) может оказать- ся разным на разных компьютерах и даже для разных компиляторов. Дело в том, что процедура changestate изменит значение глобаль- ной переменной, ведь локальной-то у неё нет! Основная программа вы- ведёт значение своей локальной переменной с тем же именем, а это значение не определено — там находится «мусор». Вот этот «мусор» мы и получим на экран. К сожалению, большинство компиляторов не обнаруживают эту ошибку и разрешают «наступать на грабли». Поэтому лучше не давать локальной и глобальной переменным одинаковые имена. Чтобы не было путаницы, можно, например, имена всех глобальных переменных начинать с прописной буквы. Желательно, чтобы процедуры и функции не зависели от глобаль- ных переменных, или, по крайней мере, не изменяли их. Глобальны- ми часто делают не переменные, а константы (постоянные значения), которые объявляются со служебным словом const: const int WIDTH = 800; const int HEIGHT = 600; 103
2 Программирование на языке C++ Эти значения ни одна процедура или функция изменить не сможет. По соглашению имена констант обычно записывают прописными бук- вами. Чем меньше область видимости переменных в программе, тем мень- ше будет «непонятных» ошибок, особенно в больших программах. Глобальные переменные используются только в тех редких случаях, когда без них никак не обойтись. Бблыпая часть всех используемых переменных — это локальные переменные процедур и функций. Если локальная переменная нужна только в цикле, лучше объявить её прямо в заголовке цикла. Кроме того, язык C++ позволяет объ- являть локальные переменные для блока, ограниченного фигурными скобками. Например, в процедуре void printSquareArea( int side ) { { // начало блока int area = side*side; cout << area; // всё правильно! } // конец блока cout << area; // ошибка! нет переменной area! } локальная переменная area «живёт» только внутри блока, в котором она объявлена. Попытка обратиться к ней за пределами блока приво- дит к ошибке. Процедуры, изменяющие аргументы Во многих алгоритмах требуется менять местами значения двух переменных, например вначале а = 1 и Ь = 2, и нужно получить а = 2 и b = 1. Напишем процедуру, которая (вроде бы) выполняет нужные дейст- вия: void exchange( int х, int у ) { int temp = x; x = у; у = temp; } Вызвав такую процедуру: int а = 1, Ь = 2 ; exchange( а, b ); cout << "а=" << а << " Ь=" << Ь; мы обнаружим, что никакой замены не произошло: а=1 Ь=2 104
Процедуры §12 Дело в том, что при вызове процедуры или функции создаются ко- пии переданных ей аргументов. Эти копии (х и у) после создания ста- новятся самостоятельными локальными переменными, не связанными с переменными а и Ь основной программы. Процедура работает именно с новыми переменными х и у, не подозревая об исходных переменных. После завершения работы процедуры эти копии уничтожаются, а ис- ходные переменные сохраняют свои прежние значения. Таким образом, изменение копий никак не повлияет на значения переменных а и b в основной программе. Чтобы решить эту проблему, можно использовать другой метод — передачу данных по ссылке. Немного меняется заголовок процедуры: после названия типа данных ставится знак & (этот символ называется «амперсанд», найдите его на клавиатуре): void exchange( int& х, int& у ) { } В этом случае процедура фактически получает не копии значений переменных а и Ь, а их адреса в памяти. Используя эти адреса, она работает с переменными основной программы и изменяет их. Если какой-то аргумент передаётся по ссылке, при вызове проце- дуры на этом месте можно указывать только имя переменной, но не число и не арифметическое выражение (числа, константы и арифмети- ческие выражения не имеют адреса). Например, такие вызовы запре- щены: exchange( 5, b ); exchange( а, Ь+2 ); Вообще, использовать передачу данных по ссылке не рекомендуется. Дело в том, что, во-первых, процедура изменяет данные, которые ей не «принадлежат» (даёт «побочный эффект»!). Во-вторых, этот факт очень сложно обнаружить при чтении программы — нужно смотреть на заго- ловок процедуры, больше нигде упоминания о ссылке не увидеть. Рассмотрим ещё один метод передачи данных в процедуру — по адресу, который работает не только в языке C++, но и в языке С. Он наглядно показывает, что происходит «за ширмой» при передаче дан- ных по ссылке. Вот такой вариант функции exchange тоже правильно выполняет обмен значений двух целых переменных: void exchange( int* рх, int* ру ) { int temp = *px; *px = *py; *py = temp; } 105
Программирование на языке C++ Эта процедура получает не значения параметров, а их адреса в спе- циальных переменных, которые называются указателями. Признак указателя — это знак * после названия типа. Здесь int* — это указа- тель на целую переменную, т. е. переменная, содержащая адрес другой (целой) переменной. Чтобы было понятно, что мы работаем с указате- лями, часто имена таких переменных начинаются с буквы р (от англ. pointer — указатель). Знак * перед указателем означает, что мы обращаемся к той пере- менной, адрес которой содержит указатель (эта операция называется разыменование указателя). Например, *рх — это значение перемен- ной, адрес которой записан в указателе рх. Перестановка двух значений в процедуре происходит так же, как и в варианте с передачей данных по ссылке, но мы «достаём» нужные значения через указатели рх и ру. При вызове нужно передать этой процедуре адреса исходных пере- менных. В языке C++ (как и в С) для получения адреса используется специальный оператор &: exchange ( &а, &Ь ); В отличие от варианта с передачей по ссылке здесь при вызове сра- зу ясно, что процедуре передаются адреса переменных, и их значения, скорее всего, изменяются в процедуре. Процедуры в графике С процедурами в языке C++ мы уже работали в прошлом году, используя библиотеку ТХ Library1*, разработанную преподавателем МФТИ И. Р. Дединским. Применим её для построения такого рисунка, как на рис. 2.1 (см. цветной рисунок на обороте обложки). Все треугольники на рис. 2.1 прямоугольные и имеют одинаковые размеры. Напишем процедуру drawTriangle, которая рисует один тре- угольник, так чтобы вызвать её три раза в основной программе: г* Библиотеку ТХ Library можно загрузить с главной страницы сайта И. Р. Дедин- ского ded32.ru или txlib.ru.
Процедуры int main() { txCreateWindow( 600, 400 ); drawTriangle ( ... ); drawTriangle ( ... ); drawTriangle ( ... ); } Вместо многоточий нужно будет записать данные каждого треугольни- ка, но мы пока не определили, что это будут за данные. Займёмся разработкой процедуры, рисующей треугольник. По рисун- ку видно, что у этих треугольников различаются: 1) координаты углов; 2) цвета заливки. Эти данные и нужно передавать в процедуру как аргументы. А вот размеры всех треугольников одинаковы, поэтому передавать их в про- цедуру мы не будем: определим их как константы прямо в процедуре. Поскольку треугольники равны и одинаково ориентированы (могут быть получены один из другого в результате параллельного переноса), не нужно включать в число параметров координаты всех вершин, до- статочно одной базовой точки. Удобно выбрать за базовую точку левый нижний (прямой) угол, обо- значим его координаты через (х, у). Теперь через эти переменные мож- но выразить координаты всех остальных углов (рис. 2.2). (х, у - Я) w Рис. 2.2 Кроме координат базовой точки х и у процедуре нужно передать цвет заливки — параметр типа COLORREF: void drawTriangle(int х, int у, COLORREF color) { const int H = 60; const int W = 100; txLine ( x, y, x, y-H ) ; txLine ( x, y-H, x+W, у ); txLine( x+W, y, x, у ); txSetFillColor ( color ); txFloodFill ( x+W/4, y-H/4 ); } 107
2 Программирование на языке C++ Размеры треугольника, Н и W, мы ввели как константы. Они не нужны основной программе, поэтому введены именно внутри процеду- ры, как локальные величины. Цвет заливки устанавливается командой txSetFillColor, а сама заливка выполняется командой txFloodFill. Это связано с тем, что треугольник не будет залит автоматически, ведь он строится из отдель- ных линий, а не как единая фигура (например, замкнутая ломаная). Чтобы залить уже построенный замкнутый контур, нужно, чтобы вся его внутренняя часть была одного цвета. После этого вызываем процедуру txFloodFill, передав ей координаты любой точки внутри области. При выбранной форме треугольника точка с координатами (x+W/4, у-Н/4) гарантированно находится внутри него (рис. 2.3). (х + W/4, у - Н/4) Н] (х, у) W Рис. 2.3 При вызове процедуры нужно передать ей координаты базовой точ- ки очередного треугольника (их легко определить по рис. 2.1) и цвет заливки. Получается такая основная программа: int main () { txCreateWindow( 600, 400 ); drawTriangle( 20, 100, TX_BLUE ); drawTriangle( 120, 100, TX_LIGHTGREEN ); drawTriangle( 120, 160, TX_RED ); } Выводы • Заголовок процедуры на языке C++ начинается словом void — это означает, что она не возвращает никакого результата. • Локальные переменные объявляются внутри подпрограмм. Пере- менные, объявленные в основной программе (функции main), недоступны другим подпрограммам. • Глобальные переменные объявляются вне всех подпрограмм и вне основной программы. Они доступны всем процедурам и функци- ям. При запуске программы значения всех глобальных перемен- ных автоматически обнуляются. • Если не присвоить начальные значения локальным переменным (не инициализировать их), то в них при запуске программы оста- нется «мусор», использовать эти значения бессмысленно. 108
Процедуры §12 • Переменная, объявленная в заголовке цикла, — это локальная переменная цикла (а не всей подпрограммы). • Параметры в заголовке подпрограммы перечисляются через запя- тую, для каждого указывается его тип. • Если после названия типа параметра поставить знак &, происходит передача данных по ссылке: процедуре передаётся адрес переменной вызывающей программы, и она может изменять эту переменную. • Если переменную вызывающей программы нужно изменять в про- цедуре, можно использовать передачу данных по адресу (по указа- телю). При вызове нужно передавать адреса переменных, их мож- но получить с помощью оператора &. • Служебное слово const перед названием типа переменной обозна- чает константу — неизменяемую величину, которая имеет имя. Вопросы и задания 1. Михаил решил написать процедуру, которая вводит с клавиатуры все исходные данные для работы программы. Обсудите достоинства и недостатки такого решения. 2. Напишите процедуру с параметром п, которая выводит прямоуголь- ный треугольник из символов с катетами размера п. г м п Ч ММ МММ мммм 3. Напишите процедуру с параметром п, которая выводит ёлочку из символов высотой п. м п- мом момом момомом м 4. Определите, что общего у треугольников, из которых состоит рисунок, и чем они различаются (см. цветные рисунки на обороте обложки). Напишите процедуру, с помощью которой можно нарисовать все эти треугольники, и программу, которая строит весь рисунок. 109
Программирование на языке C++ 5. Напишите процедуру, которая умеет рисовать на экране сороконож- ку с разным количеством ножек; количество ножек определяет дли- ну сороконожки (см. цветные рисунки на обороте обложки). 6. Напишите процедуру с параметрами, которая рисует домики разных размеров и цветов (см. цветные рисунки на обороте обложки). 7. Напишите процедуру с параметром п, которая рисует орнамент, состоящий из п повторений узора. 8. В процедуру из предыдущей задачи введите параметр, который позволяет изменять размер узора (расстояние между линиями). 9. Найдите в дополнительных источниках узор, который вам нравит- ся. Напишите процедуру с параметром п, которая рисует орнамент, состоящий из п повторений этого узора. Интересный сайт ded32.ru (txlib.ru) — сайт профессиональной проектной работы по информатике для школьников; там же расположена библиотека ТХ Library §13 Рекурсия Ключевые слова'. • рекурсивная процедура • фрактал • базовые объекты условие остановки анимация Рекурсивные процедуры Как вы помните, рекурсивная процедура — это процедура, вызываю- щая сама себя. 110
Рекурсия §13 Рекурсивное решение задачи состоит из двух частей. Нам нужно определить: 1) правило, по которому решение исходной задачи сводится к реше- нию более простой задачи того же типа; 2) условие остановки рекурсии — построение базовых объектов. Например, программа, которая решает задачу «Ханойские башни» (см. § 3) с помощью рекурсивной процедуры, выглядит так: #include <iostream> using namespace std; void Hanoi( int n, int k, int m ) { if ( n <= 0 ) return; int p = 6 - k - m; Hanoi(n-1, k, p); cout << k << " -> " << m << endl; Hanoi( n-1, p, m ) ; } int main() { Hanoi ( 4, 1, 3 ); cin.get (); } Видно, что она очень похожа на аналогичную программу, которую мы составили в § 3 на языке Python. Все отличия — только в оформ- лении. В языке C++ для каждого параметра нужно указать его тип, условие в условном операторе заключается в скобки, и т. д. Программа, которая выводит двоичное представление числа (см. § 3), также мало изменилась: #include <iostream> using namespace std; void printBin( int n ) { if (n <= 0) return; printBin(n / 2); cout << n % 2; } int main() { printBin ( 90 ); cin.get (); } 111
2 Программирование на языке С++ Дерево Пифагора Чтобы продемонстрировать возможности рекурсии, напишем процедуру для рисования фрактала «дерево Пифагора». На рисунке 2.4 показаны два дерева Пифагора — пятиуровневое (а) и девятиуровневое (б). Рис. 2.4 n-уровневое дерево Пифагора размера L состоит из ствола длиной L и двух (п — 1)-уровневых деревьев Пифагора размера k • L, где k < 1 — постоянная. Значение k определяет, во сколько раз уменьшается раз- мер ствола на следующем уровне. Для деревьев на рис. 2.4 выбрано значение k = 0,7. Деревья следующего уровня примыкают к концу ствола под углом ±45° относительно направления ствола. Очевидно, что это рекурсивное определение: дерево Пифагора опре- деляется через базовый элемент (отрезок-ствол) и два дерева Пифагора меньшего размера. Определим параметры, которые должна принимать рекурсивная про- цедура pythagorasTree {Pythagoras — это имя древнегреческого мате- матика Пифагора на латинском языке). Это будут: 1) координаты точки, где начинается ствол («корня»), (х, у); 2) длина ствола L; 3) угол наклона ствола, angle (по-английски — угол); 4) количество уровней, levels (по-английски — уровни). Будем считать, что дерево может быть повёрнуто под любым углом, поэтому рассмотрим задачу перехода на следующий уровень в общем виде (для произвольного угла) — рис. 2.5. Рис. 2.5 112
Рекурсия §13 Если известны координаты (х, z/) и длина ствола L, несложно опре- делить координаты второго конца ствола, хг и уг: хг = х + L • cos а, уг = у - L- sin а. Стволы двух деревьев следующего уровня будут направлены под углами а - 45° и а + 45°. В итоге получается такая процедура: void pythagorasTree( float х, float у, float L, float angle, int levels ) { if ( levels <= 0 ) return; const float k = 0.7; const float anglel = angle - 45; const float angle2 = angle + 45; float xl = x + L*cos( angle*M_PI/180 ); float yl = у - L*sin( angle*M_PI/180 ); txLine ( x, y, xl, yl ); pythagorasTree( xl, yl, k*L, anglel, levels-1 ); pythagorasTree( xl, yl, k*L, angle2, levels-1 ); } В начале процедуры сразу проверяем, нужно ли что-то делать. Если количество оставшихся уровней равно нулю (или меньше — в резуль- тате ошибки), процедура не выполняет никаких действий, рекурсия заканчивается. Далее вводим константу к и определяем два угла — направления стволов деревьев следующего уровня. По приведённым выше формулам вычисляются координаты второго конца ствола (xl, yl), и затем процедура вызывает сама себя дважды. Меняются все параметры: новые деревья выходят из точки (xl, yl), их размер равен k*L, меняются углы, количество оставшихся уровней уменьшается на 1. Обратите внимание, что значение угла наклона ствола передаётся в процедуру в градусах (так нам удобнее). Но функциям sin и cos нужно передать значение угла в радианах. Поэтому при вызове этих функций угол переводится из градусов в радианы: умножается на константу я (М_Р1) и делится на 180°. Чтобы не терять точность за счёт округлений, все аргументы (кроме количества уровней) передаются как вещественные числа. Посмотрев на исходный код функции txLine, вы обнаружите, что эта функция округляет переданные ей вещественные координаты до ближайших це- лых чисел. Сделав небольшие изменения внутри процедуры pythagorasTree, можно рисовать самые разные деревья. 113
2 Программирование на языке С++ Анимация Пока наше дерево стоит неподвижно, но совсем несложно сделать так, чтобы его ветки двигались (как от ветра). Что для этого нужно? Достаточно случайным образом менять углы между ветками при рекурсивных вызовах процедуры. Поскольку угол в нашей программе — это вещественное число, мы напишем (и будем использовать) функцию, которая возвращает случайное вещественное число в заданном интервале1*: float uniform( float a, float Ь ) { return а + (b - a)*rand()/RAND_MAX; } Теперь заменим те строки в процедуре, где вычисляются углы наклона деревьев следующего уровня, — добавим в них случайную составляющую (изменения выделены фоном): const float anglel = angle - 45 + uniform( -5, 5 ); const float angle2 = angle + 45 + uniform( -5, 5 ) ; Пока мы научились только рисовать дерево, в котором ветки имеют случайный наклон. Теперь нужно сделать анимацию. Проще всего через небольшой интервал времени стирать всё изображение в окне и перерисовывать дерево заново. При этом вызовы функции uniform вернут уже другие случайные (точнее — псевдослу- чайные) числа, и наклон веток изменится — мы увидим их «шевеле- ние». На псевдокоде можно записать цикл так: while ( /* не нажата клавиша Escape */ ) { // стираем изображение // рисуем дерево // задержка на 50 мс } Очистку экрана выполняет процедура txClear (она заливает холст выбранным цветом фона), а задержку («засыпание» программы) — процедура txSleep, обе они входят в библиотеку ТХ Library. Нажатие клавиши можно «поймать» с помощью функции GetAsyncKeyState. Получается такая основная программа: int main() { const int width = 800, height = 600; x* В языке Python такая функция уже есть в модуле random, а здесь нам при- шлось написать её самостоятельно, используя функцию rand из стандартной библиотеки. 114
Рекурсия §13 txCreateWindow( width, height ); txSetFillColor( TX_WHITE ); txSetColor( TX_BLACK ); const float L = 200, angle = 90; const int levels = 10; while( not GetAsyncKeyState( VK_ESCAPE ) ) { txClear(); pythagorasTree( width/2, height, L, angle, levels ); txSleep (50); } } Сразу после создания окна мы установили белый цвет заливки (txSetFillColor) и чёрный цвет линий (txSetColor). Вообразите, насколько сложно было бы построить такую анимацию без использования рекурсии. Выводы • Рекурсивные процедуры в C++ строятся по тем же правилам, что и в языке Python. • Один из способов создания анимации — очистка экрана и полная перерисовка картинки при каждом повторении цикла. Вопросы и задания 1. Напишите рекурсивную процедуру для перевода числа в шестнадца- теричную систему счисления. * 2. Постройте кривую Коха. Информацию об этой кривой найдите в дополнительных источниках. * 3. Постройте фрактал — кривую Гильберта. Информацию об этой кри- вой найдите в дополнительных источниках. * 4. Постройте фрактал — кривую Серпинского. Информацию об этой кривой найдите в дополнительных источниках. 5. Измените процедуру рисования дерева Пифагора (из параграфа) так, чтобы последние уровни («листья») были нарисованы другим цветом. 6. Добавьте в процедуру рисования дерева Пифагора (из параграфа) ещё один параметр branchAngle - угол отклонения ветвей следую- щего уровня от ствола (в приведённой программе он равен 45°). 7. Измените процедуру рисования дерева Пифагора (из параграфа) так, чтобы из каждого узла выходило не по две, а по три ветки под разными углами. 115
2 Программирование на языке C++ 8. В предыдущей задаче попробуйте сделать так, чтобы количество веток в развилке выбиралось случайно. 9. Как изменить программу рисования дерева Пифагора (из парагра- фа), чтобы ветки дерева наклонялись вправо (ветер слева)? 10. Добавьте в программу анимации дерева Пифагора (из параграфа) управление с клавиатуры — нажатия на клавиши-стрелки меняют силу или направление ветра, размеры дерева. Используйте функцию GetAsyncKeyState из стандартной библиотеки Windows, символь- ные обозначения клавиш найдите в справочной системе (поищите названия клавиш VK_LEFT, VK_RIGHT — и найдёте остальные). 11. Проект. Постройте один из следующих фракталов. Интересные сайты cppstudio.com — программирование на C++ для начинающих cplusplus.com — сайт, посвящённый языку C++ 116
Функция §14 §14 Функция Ключевые слова,'. • функция • результат функции • параметры • рекурсивная функция • вызов функции Функции в C++ Функция — это подпрограмма, которая возвращает результат (чис- ло, символьную строку и др.). Заголовок процедуры начинался словом void (ничего не возвращает), а для функции вместо void записывают тип результата, например: float average( int a, int b ) { float avg = (a + b) / 2; return avg; } Эта функция с именем average принимает два целочисленных пара- метра (с именами а и Ь), и возвращает вещественный результат (типа float). Как и в Python, результат функции определяется оператором return. В нашем примере он возвращает значение локальной переменной avg, которое равно среднему арифметическому значений а и Ь. Если попробовать запустить программу с такой функцией и посчи- тать с её помощью среднее арифметическое чисел 2 и 5, мы увидим ответ 3, а не 3,5. Дело в том, что операция деления целых чисел в языке C++ всегда даёт целое число, несмотря на то что мы объявили переменную avg вещественной (float). Чтобы исправить ошибку, достаточно добавить нулевую дробную часть делителю 2, сделав его вещественным: avg = (а + b) / 2.0; Тогда получится, что сумма целых чисел делится на вещественное чис- ло, поэтому частное тоже получается вещественным. В принципе, функцию можно вызывать так же, как и процедуру: average ( 5, 9 ); но при этом её значение потеряется. Вызов без сохранения результата используется тогда, когда результат нас не интересует. Например, за- глянув в исходный код библиотеки ТХ Library, вы обнаружите, что многие команды, которые мы раньше использовали как процедуры, на самом деле — функции. Они возвращают логическое значение (типа bool). Если оно истинное (true), работа завершена удачно, если лож- ное (false) — произошла ошибка, например, из-за того, что мы забы- ли создать окно рисования. 117
2 Программирование на языке C++ Если мы уверены, что функция отработает правильно (или если ошибка при её работе ни на что не влияет), мы можем вызвать такую функцию как процедуру, не проверяя её результат. Но в професси- ональных программах так не делают — код проверки и обработки ошибок обычно составляет значительную часть всего кода программы. Результат работы функции можно сохранить в переменной: float sred = average ( 5, 9 ); Вместо вызова функции в операторе присваивания компилятор под- ставляет результат этого вызова — то значение, которое эта функция вернёт. Результат работы функции можно сразу вывести на экран: cout << average ( 3, 8 ); Функции можно передавать не только аргументы-константы, но так- же значения переменных и арифметических выражений: int а = 5, Ь = 7; float sred = average ( a, b+8 ); Функция average возвращает вещественное число, поэтому вызовы этой функции можно применять везде, где можно использовать веще- ственное число, в том числе в арифметических выражениях, условных операторах и циклах. Например: с = 2*average( х, у ) + z; if( average (a, b) > 4 ) cout « "Опасность! Задраить люки!"; while( average(a, b) < х ) а += 1; Как и в операторе присваивания, вместо вызова функции компиля- тор подставляет то значение, которое возвращает функция. Вы уже знаете, что основная программа в языке C++ — это тоже функция. Поэтому она может вернуть результат: int main() { return 0; // программа завершилась успешно } Результат работы функции main — целое число, которое сообща- ет операционной системе, успешно ли завершилась программа (в этом случае нужно вернуть 0) или произошла ошибка (тогда нужно вернуть ненулевое значение — код ошибки, и описать его в документации к своей программе). 118
Функция §14 Мы никогда не ставили оператор return в конце основной програм- мы (функции main). В этом случае, согласно стандарту языка C++, основная программа возвращает 0. Но такое исключение допускает- ся только для main. Если не поставить оператор return в какой-либо другой функции, то она вернёт неопределённое («мусорное») число, и это будет очень неприятная ошибка. Согласно терминологии языка C++, все подпрограммы называются функциями. Процедура считается особым видом функции, которая воз- вращает значение типа void, т. е. «ничего», «пусто». Примеры функций Напишем свою функцию max, которая выбирает максимальное из двух целых чисел: int max( int a, int b ) { int maximum; if ( a > b ) maximum = a; else maximum = b; return maximum; } To же самое можно записать и короче: int max( int a, int b ) { if( a > b ) return a; else return b; } Заметим, что для определения максимума из вещественных чисел в C++ нужно строить другую функцию, эта не подойдёт. При вызове с вещественными аргументами она приведёт их к типу int, т. е. отбро- сит дробную часть. Одна функция может вызывать другую. Например, можно составить функцию max4, которая возвращает наибольшее из четырёх чисел, используя готовую функцию max: int max4( int a, int b, int c, int d ) { return max( max(a, b), max(c, d) ); } А вот функция, которая вычисляет сумму цифр натурального числа (мы писали её раньше на языке Python): int sumDigits( int N ) { int summa = 0, digit; 119
2 Программирование на языке C++ while (N != 0) { digit = N % 10; summa += digit; N /= 10; } return summa; } Логические функции Логическая функция должна вернуть логическое значение — результат типа bool. В C++ истинное значение обозначается как true, а лож- ное — как false. Напишем простую функцию, которая определяет, верно ли, что переданное ей число — это число Фибоначчи. Последовательность Фибоначчи Fn определяется с помощью рекурсии: 1) F1 = F2= 1, 2) Fn = Fn_x + Fn2 для n >2. Наша функция принимает целое число и возвращает true, если это число Фибоначчи. bool isFibonacci ( int N ) { } Будем строить числа Фибоначчи, пока очередное число не станет больше или равно N. Если при этом N будет равно последнему получен- ному числу Фибоначчи, то функция вернёт true, а если будет меньше его, то false. Для построения чисел Фибоначчи применим циклический алгоритм, который работает намного быстрее, чем рекурсия: bool isFibonacci( int N ) { if( N == 1 ) return true; int f2 = 1, fl = 1; int Fib = fl + f2; while ( N > Fib ) { f2 = fl; fl = Fib; Fib = fl + f2; } return (N == Fib) ; } 120
Функция §14 Первая строка — это обработка особого случая: N = 1 — это точно число Фибоначчи. В переменных fl и f2 хранятся, соответственно, предыдущие эле- менты последовательности: Fn l и Fn_2, а в переменной Fib мы вычис- ляем новое число Фибоначчи, складывая два предыдущих. Цикл работает, пока очередное полученное число Фибоначчи не ста- нет больше или равно N. Теперь остаётся сравнить значения N и Fib. Оператор return (N == Fib); можно записать в развёрнутой форме: if ( N == Fib ) return true; else return false; Результат, который возвращает логическая функция, можно исполь- зовать во всех условиях как обычное логическое значение. Например, так: if( isFibonacci (а) ) cout << "Это число Фибоначчи!"; Если вывести на экран логическое значение — результат логической функции, — мы увидим 0 вместо false и 1 вместо true. Это говорит о том, что значение переменной типа bool хранится как целое число, но может принимать только значения 0 или 1. Рекурсивные функции Напишем рекурсивную функцию, которая вычисляет наибольший общий делитель (НОД) двух натуральных чисел с помощью алгоритма Евклида. Алгоритм Евклида. Чтобы найти НОД двух натуральных чисел, нужно заменять большее число остатком от деления большего на меньшее, пока одно из чисел не станет равно нулю. Тогда второе число и есть их НОД. В соответствии с этим алгоритмом построим такую функцию: int NOD( int a, int b ) { if( a == 0 or b == 0 ) return a + b; if( a > b ) return NOD( a % b, b ); else return NOD( a, b % a ); } 121
2 Программирование на языке C++ Первая строка — это условие окончания рекурсии (условие выхода). Если одно из чисел стало равно нулю, то второе число можно найти как сумму а + Ь, это и есть НОД. Далее мы определяем, какое число больше, и заменяем его на оста- ток от деления большего числа на меньшее. Можно упростить эту функцию, избавившись от второго условного оператора. Если бы всегда выполнялось условие а > Ь, делать выбор было бы не нужно. При рекурсивном вызове мы будем передавать сна- чала Ь, а потом — а % Ь (это значение всегда меньше, чем Ь): int NOD( int a, int b ) { if( a == 0 or b == 0 ) return a + b; else return NOD( b, a % b ); } Осталось убедиться, что функция будет работать, если передать ей данные в неправильном порядке (а < Ь). В этом случае а % b = а, так что первый рекурсивный вызов сведётся к простой перестановке значе- ний а и Ь. Выводы • В языке C++ все подпрограммы называются функциями. • Тип возвращаемого значения указывается в заголовке функции. При этом для процедуры указывается тип void. • Логическая функция возвращает логическое значение: результат типа bool (true или false). • Рекурсивная функция вызывает сама себя, напрямую или через другие функции. Вопросы и задания 1. Как по тексту функции определить тип значения, которое она воз- вращает? 2. Какой недостаток, на ваш взгляд, имеет эта функция? int cube( int х ) { int cubeX = x*x*x; cout « cubeX << endl; return cubeX; } 3. Напишите функцию readNumberln, которая запрашивает у пользова- теля целое число на заданном отрезке. Например, при вызове int n = readNumberln(1, 6) 122
Функция §14 функция должна вернуть целое число из отрезка [1; 6]. Если поль- зователь вводит неправильные числа, функция выдаёт сообщение об ошибке и запрашивает ввод повторно, пока он не введёт подхо- дящее число. 4. Напишите рекурсивную функцию numberOfDigits, которая вычис- ляет количество цифр числа. 5. Напишите рекурсивную функцию numberOfOnes, которая вычисля- ет количество единиц в двоичной записи числа. 6. Напишите функцию withParityBit, которая принимает неотрица- тельное целое число из отрезка [0; 127] и изменяет его, добавляя к двоичной записи числа справа бит чётности так, чтобы коли- чество единиц в двоичной записи стало чётным. Используйте ре- зультат выполнения предыдущего задания. Например, для числа 13 = 1Ю12 нужно добавить справа бит, равный единице, тогда ко- личество единиц станет чётным (4): 110112 = 27. 7. Напишите логическую функцию isAutomorph, которая возвраща- ет истинное значение, если переданное ей число автоморфное (по- следние цифры его квадрата совпадают с самим числом, например 252 = 625). 8. Напишите логические функции notAnd и notOr, которые возвра- щают результаты операций НЕ-И и HE-ИЛИ над двумя логиче- скими значениями. *9. Напишите логическую функцию imp, которая возвращает резуль- тат операции «импликация» над двумя логическими значениями. Вот её таблица истинности: а ь imp(a, b) false false true false true true true false false true true true *10. Напишите логическую функцию хог, которая возвращает резуль- тат операции «исключающее ИЛИ» над двумя логическими значе- ниями. Вот её таблица истинности: a b xor(a, b) false false false false true true true false true true true false 123
2 Программирование на языке C++ 11. Напишите функцию NOK, которая возвращает наименьшее общее кратное двух натуральных чисел. Используйте уже известную функцию NOD (из параграфа). 12. Напишите нерекурсивную версию функции, которая определяет наибольший общий делитель двух натуральных чисел. Сравните рекурсивное и нерекурсивное решения. Какое из них вам больше нравится? Обсудите этот вопрос в классе. *13.Напишите логическую функцию isPrime, которая возвращает зна- чение «истина», если переданное ей число простое (делится только на само себя и на единицу). Интересный сайт informatics.mccme.ru — сайт для подготовки к олимпиадам по ин- форматике с автоматической проверкой решений §15 Символьные строки Ключевые слова: • символьная строка • удаление символов • длина строки • вставка символов • сцепление строк • поиск подстроки • выход за границы строки • замена подстроки • подстрока • преобразование типов Что такое символьная строка? Символьная строка — это последовательность символов, которая рас- сматривается как единый объект. В языке C++ строка относится к типу string (по-английски — строка) и объявляется так: string s; По умолчанию строка после создания пустая, т. е. не содержит ни одного символа. Новое значение записывается в строку с помощью оператора присваивания: s = "паровоз"; или сразу при объявлении1): string s = "пароход"; i) Для того чтобы программа правильно выводила русские буквы, достаточно подключить библиотеку ТХ Library, добавив в начало программы команду #include "TXLib.h" 124
Символьные строки §15 Строковая константа в языке C++ всегда заключается в двойные кавычки. А вот так создаются строки из одинаковых символов: string chain ( 5,'-'); // (1) chain = string( 10, '+' ); // (2) Число в скобках — это количество повторений, а второе значение — символ, который повторяется. В строке 1 строится новая символь- ная строка chain из пяти знаков «минус», а в строке 2 её значение заменяется на цепочку из десяти знаков «плюс». Строки можно вводить из входного потока cin: cin >> s; Однако у такого способа есть важная особенность: все символы после первого пробела не будут записаны в строку. Для того чтобы ввести строку с пробелами, применяется функция getline: getline ( cin, s ); Первый аргумент функции (cin) — это поток, откуда происходит ввод данных, а второй — строка, в которую нужно поместить резуль- тат ввода. Функция должна изменить строку s, следовательно, строка передаётся в функцию по ссылке (это можно проверить, посмотрев на прототип функции getline в документации). Вывод строки в выходной поток cout выполняется обычным спосо- бом: cout << s; Для определения длины строки s используется точечная запись: s.size(). Символьная строка в языке C++ — это не простой тип дан- ных (как, например, int или float), а объект, который имеет свои свойства и методы. Метод — функция для обработки данных объекта. Запись s. size () означает, что метод size применяется к объекту s. Метод size определён для объектов типа string, вызов его для переменных других типов в большинстве случаев приведёт к ошибке. Этот метод возвращает целое число — количество символов строки, его можно сохранить в целочисленной переменной: int n = s.size (); Сравнение строк Строки можно сравнивать между собой так же, как числа. Например, проверим равенство двух строк: string password; getline( cin, password ) ; if( password == "sEzAm" ) cout << "Слушаюсь и повинуюсь!"; else cout << "No pasaran!"; 125
2 Программирование на языке C++ Операции сравнения «меньше» и «больше» работают так же, как и в языке Python: string si = "паровоз"; string s2 = "пароход"; cout << (si < s2); Этот фрагмент программы выводит 1, так как строка si «меньше», чем s2, и условие si < s2 истинно. Сравнение происходит по ко- дам первых различных символов («в» < «г»); более короткая строка меньше, чем все начинающиеся с таких же символов более длинные строки. Сцепление строк Оператор «+» используется для «сложения» (объединения, сцепления) строк, эта операция называется конкатенацией. Например: string be = "Будь, как"; string name = "Петя"; string goodBoy = be + " " + name + "!"; В результате в переменную goodBoy будет записано значение "Будь, как Петя!" А вот операции «умножения» строки на число в языке С++ (в отличие от Python) нет. Обращение к символам Каждый символ строки имеет свой номер (индекс). Нумерация, так же как и в языке Python, начинается с нуля. К любому символу можно обратиться по индексу, записав его после имени строки в квадратных скобках: string hello = "Привет!"; cout << hello[1] « endl; // p cout << hello[5] « hello [2] « "к"; // тик В отличие от языка Python, отрицательные индексы использовать нельзя, при этом происходит выход за границы строки с непредсказу- емым результатом. Компилятор выводит предупреждение о возможной ошибке, но не блокирует операцию, позволяя даже изменять символы строки с отрицательными индексами: hello[-5] = 'ж'; // так делать не надо! Однако при этом мы меняем содержимое памяти вне строки. Это мо- жет вызвать ошибку обращения к памяти и аварийное завершение про- граммы, а может стать скрытой ошибкой программы на долгие годы. Такой же печальный результат мы получим при обращении к символу с 126
Символьные строки §15 положительным индексом за пределами строки, например к hello[255]. Такое «неопределённое поведение» (англ, undefined behavior) очень за- трудняет отладку программы. Поэтому нужно обязательно включать в программу проверки на выход за границы массивов и строк. Перебор всех символов Поскольку к символу можно обращаться по индексу, для перебора всех символов удобно использовать цикл по переменной, которая «про- бегает» все возможные значения индексов. Пусть нужно вывести в столбик коды всех символов строки с име- нем capital: string capital = "Moscow"; Её длину можно найти с помощью метода size, индекс перво- го символа равен 0, а индекс последнего — на единицу меньше, чем capital. size (). Таким образом, все допустимые индексы можно пере- брать в цикле по переменной: for( int i = 0; i < capital.size(); i++ ) cout << capital[i] « « int( capital[i] ) « endl; Символ хранится в памяти как числовой код, поэтому в результате преобразования символа в целое число функцией int мы получаем как раз этот код и выводим его на экран. В современных версиях C++ (начиная с C++11) можно обойтись без переменной i: string capital = "Moscow"; for( char sym: capital ) cout << sym « « int(sym) « endl; Запись for (char sym: capital) напоминает цикл, который в язы- ке Python записывается как for sym in capital: все символы строки capital по очереди оказываются во временной переменной sym. В отличие от Python строка в C++ — это изменяемый объект. Поэтому можно изменять отдельные (и даже все!) символы строки пря- мо в той области памяти, где строка была размещена при объявлении. Следующая программа вводит строку с клавиатуры, заменяет в ней все буквы «э» на буквы «е» и выводит полученную строку на экран: string s; getline( cin, s ); for( int i = 0; i < s.size(); i++ ) if( s[i] == 'э' ) s[i] = 'e'; cout << s; 127
2 Программирование на языке C++ В отличие от аналогичной программы на языке Python здесь не вы- деляется новая область памяти при каждом изменении символа, поэто- му программа работает очень быстро. В версии С+-1-11 можно переписать цикл из последнего фрагмента в краткой форме: for( char& sym: s ) if( sym == 'э' ) sym = 'e'; Обратите внимание, что теперь временная переменная sym объявлена как ссылка на символ (char&), поэтому её можно использовать и для изменения символов строки s. Подстрока Для того чтобы выделить часть строки (подстроку, англ, substring), в C++ применяется метод substr, который тоже вызывается с помощью точечной записи. Следующий фрагмент копирует в строку si пять символов строки s, начиная с символа с индексом 3: string s = "0123456789"; string si = s.substr( 3, 5 ); cout « si « endl; // "34567" Два аргумента при вызове метода — это индекс начального символа и количество символов. Если второй параметр при вызове метода substr не указан, в новую строку копируются все символы до конца строки. Например: string s = "0123456789"; string si = s.substr ( 3 ); // "3456789" Удаление и вставка Для удаления части строки нужно вызвать метод erase, указав индекс первого удаляемого символа и число символов, которые нужно удалить. string s = "0123456789"; s.erase ( 3, 6 ); // "0129" В строке s остаётся значение "0129" (удаляются 6 символов, начи- ная с элемента s [ 3 ]). Обратите внимание, что метод erase изменяет строку, для которой он вызывается. Если второе значение (длина удаляемой части) не указано, удаляют- ся все символы до конца строки: string s = "0123456789"; s.erase ( 3 ); // "012" Метод clear (по-английски — очистить) очищает строку — удаляет все символы и освобождает занимаемую ими память. После этого метод 128
Символьные строки §15 size вернёт значение 0, а логический метод empty (по-английски — пустой) — истинное значение: s.clear (); if ( s. empty () ) // или if (s.size() == 0) cout « "Строка пуста!" << endl; Для вставки символов служит метод insert, которому передают встав- ляемый фрагмент и индекс символа, с которого начинается вставка: string s = "0123456789"; s.insert ( 3, "ABC" ); // "012АВС3456789" Метод insert тоже изменяет строку, для которой он вызывается. При вставке символов длина строки увеличивается. Поиск в символьных строках В языке C++ для поиска символа и подстроки используется метод find. Эта функция возвращает индекс первого найденного символа, а при поиске подстроки — индекс первого вхождения подстроки. Если образец не найден, возвращается специальное значение string: :npos (от англ, по position — нет позиции). Преобразование этого значения к типу int даёт -1. В следующем примере мы ищем в строке s первый символ ' с': string s = "Здесь был Вася."; int n = s.find('c'); if( n != string::npos ) cout << "'c': s[" « n << "]" << endl; else cout << "Нет 'c'. " << endl; Точно так же можно искать не только отдельный символ, но и под- строку: string s = "Здесь был Вася."; int n = s.find ( "Вася" ); if( n != string::npos ) cout << "'Вася' начинается с s[" << п << II ]II . else cout << "Не нашли Васю."; Если нужно искать не с самого начала строки, методу find можно передать второй аргумент — индекс символа, с которого следует начи- нать поиск: string s = "Здесь был Вася."; int n = s.find('с', 6); // 12 Для поиска с конца строки применяют метод г find (от англ, reverse find — искать в обратную сторону) — он ищет последнее вхождение образца в строку. 129
2 Программирование на языке C++ string s = "Здесь был Вася!"; int n = s. rfind ( 'с' ); //12 Замена Встроенный метод replace (по-английски — заменить) позволяет заме- нить часть строки на заданную подстроку. Например, чтобы заменить два слэша в записи даты на точки, тре- буются два вызова: string date = "12/02/2018"; date.replace (2, 1, // "12.02/2018" date.replace (5, 1, // "12.02.2018" Первый аргумент при вызове метода replace — это индекс первого заменяемого символа, второй — количество заменяемых символов, а третий — строка, которую нужно поставить на их место. Готового метода, который выполняет замену всех вхождений задан- ной подстроки на другую подстроку, в C++ нет. Разберём ещё один пример. Пусть в строке fileName записано полное имя файла с расширением: string fileName = "example.com"; Требуется заменить имя файла на spam, оставив то же (неизвестное заранее) расширение. Сначала найдём индекс последней точки: int pos = fileName.rfind; а затем применим метод replace: fileName.replace(0, pos, "spam"); // "spam.com" Преобразования «строка — число» В языке C++ для преобразования строки (типа string) в число можно использовать функции atoi (преобразует символьную строку в целое число) и at of (преобразует символьную строку в вещественное число). Они находятся в библиотеке cstdlib, поэтому сначала нужно подклю- чить эту библиотеку: #include <cstdlib> Функции atoi и atof пришли в C++ из языка С, поэтому они используют другую форму представления строк — последовательность символов с кодом 0 на конце (англ, null-terminated strings). Для пере- вода строки типа string в такой формат нужно вызвать метод-функ- циюг) c_str: В стандарте С++11 вместо atoi и atof можно использовать функции stoi и stof, которые не требуют такого преобразования и работают с объектами типа string. 130
Символьные строки §15 string s = "12.345бе-бе-бе6789"; int n = atoi ( s.c_str() ); // 12 float x = atof ( s.c_str() ); // 12.345 Функции atoi и atof останавливают преобразование на том симво- ле, который не соответствует нужному типу данных. В этом примере мы записываем целое число в переменную п, поэтому функция atoi останавливает декодирование на точке, разделяющей целую и дробную части. А декодирование вещественного числа в функции atof остано- вится на первой букве «б». Обратное преобразование (из числа в строку) в C++ выполняется с помощью потоков. Но, в отличие от уже известного потока cout, нам нужен поток, который выводит результат в строку. Подключаем библиотеку sstream и создаём поток вывода в строку: ♦include <sstream> int main () { ostringstream temp; } Здесь temp — это объект типа ostringstream — вспомогательный вы- ходной поток, который можно затем преобразовать в строку, используя метод str: int N = 123; temp << N; string s = temp.str(); // "123" Для перевода следующего числа поток нужно очистить, записав в него пустую строку: temp.str( "" ); // очистка потока При выводе вещественных чисел в поток удобно использовать, как и для потока cout, команды-манипуляторы из библиотеки iomanip, которые позволяют задать и общее число позиций на вывод числа, и точность (количество знаков в дробной части числа). В начале программы подключим библиотеку iomanip: ♦include <iomanip> Для вывода чисел можно использовать формат с фиксированной запятой (fixed): double X = 123.456123; temp « fixed << setprecision (3) « setw(10) << X; s = temp.str (); // " 123.456" 131
2 Программирование на языке C++ или научный формат записи чисел (scientific): temp.str("") ; // очистка потока temp « scientific << setprecision (6) << setw(13) << X; s = temp.str(); // "1.234561E+002" Напомним, что манипулятор setw устанавливает общее количество позиций для вывода значения, a setprecision — количество знаков в дробной части вещественного числа. В стандартной библиотеке С++11 есть удобная функция to_string, которая преобразует различные данные в символьную строку: int N = 123; string sN = to_string( N ); // 123 double X = 123.45; string sX = to_string( X ); // 123.450000 Символьные строки в функциях Символьные строки могут передаваться процедурам и функциям как аргументы. Функции могут возвращать символьные строки как резуль- тат. Построим процедуру, которая заменяет в строке s все вхожде- ния слова-образца wOld на слово-замену wNew (здесь wOld и wNew — это имена параметров, а выражение «слово wOld» означает «значение параметра wOld»). Сначала разработаем алгоритм решения задачи. В первую очередь в голову приходит такая идея: while( /* слово wOld есть в строке s */ ) { // удалить слово wOld из строки // вставить на это место слово wNew } Однако такой алгоритм работает неверно, если слово wOld входит в состав wNew. Например, с помощью этого алгоритма не удастся заме- нить "12" на "А12В" (покажите самостоятельно, что это приведёт к зацикливанию). Чтобы избежать подобных проблем, попробуем накапливать резуль- тат в другой символьной строке result, удаляя из исходной строки s уже обработанную часть. Строка result в начале работы алгоритма пустая. Если слово wOld не удалось найти в строке s, вся строка приписы- вается в конец строки result, и работа алгоритма заканчивается. Пусть в строке s обнаружено слово wOld (рис. 2.6, а). 132
Символьные строки §15 a) result wOld s 6) result s Рис. 2.6 Теперь нужно выполнить следующие действия: 1) ту часть строки s, которая стоит слева от образца, «прицепить» в конец строки result (рис. 2.6, б); 2) «прицепить» в конец строки result слово-замену wNew (рис. 2.6, в); 3) удалить из строки s начальную часть, включая найденное слово- образец (рис. 2.6, г). Далее все эти операции, начиная с поиска слова wOld, повторяются до тех пор, пока строка s не станет пустой. В таблице приведён протокол работы этого алгоритма для строки "12.12.12", в которой нужно заменить "12" на "А12В”: Результат result Рабочая строка s П fl ”12.12.12” "А12В" ”.12.12” "А12В.А12В" ” . 12” "А12В.А12В.А12В" fl и Начнём составлять процедуру: void replaceAll( strings s, string wOld, string wNew ) { string result = int lengthOld = wOld.sizeO; Процедура будет изменять переданную ей строку s, поэтому передаём этот параметр по ссылке (strings). Объявляем локальные переменные: 133
2 Программирование на языке C++ result — строка-результат, которую будем собирать; lengthOld — длина строки-образца wOld. Основные операции выполняются в цикле, который работает, пока длина оставшейся части строки больше нуля: while( s.size() > 0 ) { int posOld = s.find(wOld); // (1) if ( posOld == string::npos ) break; // (2) result += s.substr(0, posOld) + wNew; // (3) s.erase (0, posOld + lengthOld); // (4) } В цикле вводим локальную переменную posOld — позицию строки образца, — и пытаемся найти этот образец (строка (1)). Если образец не найден (всё уже заменили), то выходим из цикла (строка (2)). Ина- че добавляем к строке result все символы слева от образца и слово- замену (строка (3)). В строке (4) удаляем из строки s обработанную часть. После окончания работы цикла строка s может быть непустой (если произошёл выход из цикла по оператору break), поэтому нужно допи- сать в конец строки result всю оставшуюся часть строки s: result += s; В конце процедуры записываем в строку s полученную строку- результат: s = result; Отметим, что локальная переменная result выйдет из области видимости и будет уничтожена при выходе из процедуры. Приведём полную версию процедуры: void replaceAll( strings s, string wOld, string wNew ) { string result = int lengthOld = wOld.size(); while ( s.sizeO > 0 ) { int posOld = s.find( wOld ); if( posOld == string::npos ) break; result += s.substr ( 0, posOld ) + wNew; s.erase( 0, posOld + lengthOld ); } result += s; s = result; } 134
Символьные строки §15 Вызвать эту процедуру можно так: string s = "(12.12.12)"; replaceAll( s, "12", "А12В" ); cout « s; // "(A12B.A12B.A12B)" Полученную процедуру несложно превратить в функцию, причём в таком виде использовать её станет заметно удобнее. Для этого изменим заголовок, указав тип возвращаемого значения string вместо void, и отменим передачу строки s по ссылке, убрав знак &: string replaceAll( string s, string wOld, string wNew ) Кроме того, последнюю строку (s = result) нужно заменить на опера- тор возврата значения: return result; Вызывать функцию тоже нужно по-другому, записывая её результат в переменную типа string: s = replaceAll( s, "12", "А12В" ); Такой вариант лучше, чем использование процедуры, потому что по вызову процедуры трудно догадаться, что она изменяет переданную ей строку. Рекурсивный перебор В § 5 мы рассматривали рекурсивную процедуру перебора всех сим' вольных строк заданной длины в некотором алфавите. Приведём ана логичную программу на C++: void allWords( string word, string alphabet, int К ) { if( К < 1 ) { cout << word << endl; return; } for( int i = 0; i < alphabet.size() ; i++ ) allWords( word + alphabet[i], alphabet, K-l ); } int main() { string letters = "ABB"; int wordLength = 3; allWords( letters, wordLength ); cin.get (); } 135
2 Программирование на языке C++ Выводы • Символьные строки в C++ относятся к типу string. Это изменяе- мые объекты: можно изменить любые символы строки непосредст- венно в той области памяти, которая была выделена при записи нового значения в строку. • Нумерация символов строки начинается с нуля. Обращение к сим- волам за пределами строки (например, с отрицательными индекса- ми) не запрещено, но является серьёзной ошибкой и может при- вести к непредсказуемым результатам. • Строка «знает» свой размер, он определяется с помощью метода size. • Оператор + выполняет соединение символьных строк (конкатена- цию, сцепление). • Для работы со строками используют методы, которые вызывают- ся с помощью точечной записи: substr — получение подстроки, erase — удаление символов, insert — вставка символов, find — поиск символа или подстроки, и др. • Строку можно преобразовать в целое или вещественное число для того, чтобы затем выполнять с ним вычисления. Число можно преобразовать в символьную строку. Вопросы и задания 1. Используя только операции выделения подстроки и сцепления строк, постройте из строки string cyb = "кибернетика"; как можно больше слов русского языка. Постарайтесь использовать наименьшее возможное число операций. Проверьте ваши решения с помощью программы. 2. Попробуйте вызвать методы erase и insert как функции, запишите их результаты в новые символьные строки. Сделайте выводы. 3. Используя дополнительные источники, выясните, как использовать методы replace и append для символьных строк. 4. Алексею нужно проверить, верно ли, что zuzya — пустая строка. Предложите два способа решения этой задачи. 5. Напишите процедуру printlnWidth, которая принимает длинный текст text и ширину поля вывода width и выводит текст в поле этой ширины. Если очередное слово не помещается в границы поля, оно переносится на следующую строку. 6. Доработайте процедуру из предыдущего задания так, чтобы она выполняла выравнивание по ширине. Для этого нужно добавить пробелы между словами. 136
Символьные строки §15 7. Напишите функцию extractFileName, которая выделяет из полно- го адреса файла имя файла. Например, при передаче строки D: \ DEV\CPP\sharik.exe мы должны получить результат sharik.exe. Предусмотрите обработку ошибок. 8. Напишите функцию extractFileDir, которая извлекает из полно- го адреса файла название каталога. Например, из адреса D:\DEV\ CPP\sharik.exe нужно извлечь название каталога D:\DEV\CPP. Предусмотрите обработку ошибок. 9. Напишите функцию centerstring, которая принимает символь- ную строку s и ширину поля вывода width и выравнивает строку по центру поля, добавляя слева пробелы. 10. Напишите функцию noDoubleSpace, которая принимает символь- ную строку s и удаляет из нее все двойные пробелы (оставляя одиночные). 11. Напишите функцию trim, которая принимает символьную строку s и удаляет из неё все пробелы в начале и в конце строки. 12. Доработайте процедуру рекурсивного перебора из параграфа так, чтобы она определяла количество построенных слов. Предложите разные варианты решения задачи и сравните их. 13. Напишите логическую функцию, которая проверяет правильность битовой цепочки — символьной строки, состоящей только из сим- волов 'О' и '1'. Функция должна возвращать значение true («истина»), если количество единиц в цепочке чётное, и false («ложь»), если нечётное. Предусмотрите обработку ошибок. 14. Напишите функцию bin2dec, которая переводит число из двоич- ной системы счисления в десятичную. Исходное число записано в виде символьной строки. 15. Напишите функцию hex 2 de с, которая переводит число из шест- надцатеричной системы счисления в десятичную. Исходное число записано в виде символьной строки. *16. Напишите функцию base2dec, которая переводит число из систе- мы счисления с основанием п (2 < п < 36) в десятичную систему. Исходное число записано в виде символьной строки. *17. Напишите функцию base2base, которая переводит число из систе- мы счисления с основанием п (2 < и < 36) в систему счисления с основанием т (2 < т < 36). Исходное число и результат хранятся как символьные строки. 18. Напишите функцию, которая обрабатывает строку, содержащую фамилию, имя и отчество человека (каждая пара слов разделе- на одним пробелом) и возвращает новую строку, в которой запи- саны инициалы и через пробел — фамилия. Например, из стро- ки "Семёнов Андрей Иванович" должна получиться строка "А.И. Семёнов". Предусмотрите обработку ошибок. 137
2 Программирование на языке C++ 19. Как вы думаете, можно ли построить цикл в функции replaceAll (из параграфа) как бесконечный цикл? while( true ) { } Обоснуйте ваш ответ. 20. Напишите функцию numberOfWords, которая определяет количе- ство слов в строке. Слова разделены пробелами, причём в нача- ле строки, в конце строки и между словами может быть сколько угодно пробелов. 21. Напишите функцию strReverse, которая «переворачивает» введён- ное слово, т. е. переставляет буквы так, чтобы первая буква стала последней, вторая — предпоследней, а последняя — первой. 22. Напишите логическую функцию isPalindrom, которая принима- ет символьную строку и проверяет, является ли она палиндромом (палиндром, или слово-перевёртыш, читается одинаково в обоих направлениях, например слово «казак»). 23. Напишите логическую функцию isProperDate, которая возвраща- ет значение true («истина»), если переданная её символьная за- пись даты — правильная, и false («ложь»), если неправильная. Например "05.12.2018" — это правильная дата, а "35.3.2018", "15.23.2028” и "08.3.20.18” — неправильные. Входная строка содержит только цифры и точки. 24. Напишите функцию, которая вычисляет разницу между двумя значениями времени. Время хранится как символьная строка, сна- чала — часы (2 цифры), через двоеточие — секунды (тоже 2 циф- ры), например "23:59". Если второе время меньше, чем первое, имеется в виду начало следующих суток. 25. Напишите функцию dec2roman, которая переводит число из деся- тичной системы счисления в римскую. Предусмотрите обработку ошибок. 26. Напишите функцию roman2dec, которая переводит число из рим- ской системы счисления в десятичную. Предусмотрите обработку ошибок. 27. Напишите функцию calc, которая вычисляет арифметическое выражение, состоящее из натуральных чисел и знаков «+» и «-», записанное в виде символьной строки, например "100+25-12- 34+8 9". Предусмотрите обработку ошибок. *28.Доработайте решение предыдущей задачи так, чтобы можно было вычислять выражения, содержащие скобки. *29. Доработайте решение предыдущей задачи так, чтобы можно было вычислять выражения, содержащие знаки умножения и деления. 138
2 Программирование на языке C++ 19. Как вы думаете, можно ли построить цикл в функции replaceAll (из параграфа) как бесконечный цикл? while( true ) { } Обоснуйте ваш ответ. 20. Напишите функцию numberOfWords, которая определяет количе- ство слов в строке. Слова разделены пробелами, причём в нача- ле строки, в конце строки и между словами может быть сколько угодно пробелов. 21. Напишите функцию strReverse, которая «переворачивает» введён- ное слово, т. е. переставляет буквы так, чтобы первая буква стала последней, вторая — предпоследней, а последняя — первой. 22. Напишите логическую функцию isPalindrom, которая принима- ет символьную строку и проверяет, является ли она палиндромом (палиндром, или слово-перевёртыш, читается одинаково в обоих направлениях, например слово «казак»). 23. Напишите логическую функцию isProperDate, которая возвраща- ет значение true («истина»), если переданная её символьная за- пись даты — правильная, и false («ложь»), если неправильная. Например "05.12.2018" — это правильная дата, а "35.3.2018", "15.23.2028" и "08.3.20.18" — неправильные. Входная строка содержит только цифры и точки. 24. Напишите функцию, которая вычисляет разницу между двумя значениями времени. Время хранится как символьная строка, сна- чала — часы (2 цифры), через двоеточие — секунды (тоже 2 циф- ры), например "23:59". Если второе время меньше, чем первое, имеется в виду начало следующих суток. 25. Напишите функцию dec2roman, которая переводит число из деся- тичной системы счисления в римскую. Предусмотрите обработку ошибок. 26. Напишите функцию roman2dec, которая переводит число из рим- ской системы счисления в десятичную. Предусмотрите обработку ошибок. 27. Напишите функцию calc, которая вычисляет арифметическое выражение, состоящее из натуральных чисел и знаков «+» и «-», записанное в виде символьной строки, например "100+25-12- 34+89". Предусмотрите обработку ошибок. *28. Доработайте решение предыдущей задачи так, чтобы можно было вычислять выражения, содержащие скобки. *29.Доработайте решение предыдущей задачи так, чтобы можно было вычислять выражения, содержащие знаки умножения и деления. 138
Массивы §16 * 30.Проект. Напишите процедуру tabLogic, которая принимает сим- вольную строку, содержащую логическое выражение, и строит его таблицу истинности. В выражении можно использовать перемен- ные А, В, С и D, а также знаки «|» (ИЛИ, логическое сложение), «&» (И, логическое умножение) и «!» (НЕ, отрицание). Предусмот- рите обработку ошибок. * 31. Проект. Напишите функцию, которая находит сумму двух боль- ших чисел, записанных в двух символьных строках. Например, при обработке строк "13489123452314" и "23187651237856123" должна получиться строка "23201140361308437". * 32.Проект. Напишите функцию, которая вычисляет сумму двух больших чисел, записанную в символьной строке. Например, при обработке строки "13489123452314+23187651237856123" должна получиться строка "23201140361308437". * *33.Доработайте функцию из предыдущего задания так, чтобы она могла складывать произвольное количество чисел (а не только два). * *34.Доработайте функцию из предыдущего задания так, чтобы она правильно обрабатывала и строки со знаками вычитания. * *35. Проект. Напишите функцию, которая находит произведение двух больших чисел, записанных в двух символьных строках. Например, при обработке строк "1238762346582346" и "9078623478" должна получиться строка "11246256923344859455919388". * *36.Проект. Напишите функцию, которая находит квадратный корень из большого натурального числа (предполагается, что корень — тоже натуральное число). Например, при обработке строки "1534539031169393153683756117187998756" должна получиться строка "1238765123487658234". * 37.Проект. Напишите программу для игры в «Поле чудес»: ком- пьютер загадывает слово, человек пытается его отгадать по одной букве (или назвать слово сразу). У игрока есть 8 попыток. §16 Массивы Ключевые слова: • массив • заполнение массива • индекс элемента • вывод массива • значение элемента • ввод массива • константа 139
2 Программирование на языке C++ Массивы в C++ В отличие от языка Python в C++ есть классические массивы, в кото- рых все элементы однотипны и размещаются в памяти рядом. Для обращения к отдельному элементу используют индекс. Нумера- ция элементов начинается, как и в Python, с нуля, т. е. первый по счёту элемент имеет индекс 0. Поскольку элементы размещены в па- мяти последовательно, по возрастанию их индексов, легко вычислить адрес любого элемента. Например, пусть каждый элемент занимает 4 байта. Тогда адрес элемента с индексом 15 вычисляется как адрес начала массива плюс 15 блоков по 4 байта (всего 60 байт). Чтобы использовать массив в C++, надо его предварительно объ- явить — присвоить ему имя, определить тип входящих в массив пе- ременных {элементов массива) и их количество. По этим сведениям компьютер вычислит, сколько места требуется для хранения массива, и выделит в памяти нужное число ячеек. Такие массивы называют- ся статическими — память для них выделяется при объявлении, их размеры изменить невозможно. Массивы объявляются почти так же, как и обычные переменные, только после имени массива в квадратных скобках указывают количе- ство элементов: int А [ 5 ] ; double velocities[8]; bool proper [10]; char syms[80]; // массив символов Поскольку индексы элементов массива всегда начинаются с нуля, в массиве из пяти элементов последний элемент будет иметь индекс 4 (на единицу меньше размера массива). Массивы можно строить из данных любых типов. Объявленный выше массив syms состоит из отдельных символов. Это не символьная строка, он существенное отличается от переменной типа string. Ни при каких условиях мы не сможем изменить его размер: он всегда бу- дет хранить ровно 80 символов. Это не единый объект-строка, а просто набор символов, поэтому он «не знает» своей длины, для него нельзя использовать те же методы (size, erase и др.), которые мы применя- ли для строк типа string. Количество байт памяти, выделенное под массив, можно вычислить с помощью оператора sizeof: int А[5 ] ; cout << sizeof( А ) ; Так как все элементы массива занимают одинаковое место в памяти, его размер (количество элементов) можно определить, разделив размер всего массива на размер одного элемента: int N = sizeof( А )/sizeof( А[0] ); 140
2 Программирование на языке C++ Массивы в C++ В отличие от языка Python в C++ есть классические массивы, в кото- рых все элементы однотипны и размещаются в памяти рядом. Для обращения к отдельному элементу используют индекс. Нумера- ция элементов начинается, как и в Python, с нуля, т. е. первый по счёту элемент имеет индекс 0. Поскольку элементы размещены в па- мяти последовательно, по возрастанию их индексов, легко вычислить адрес любого элемента. Например, пусть каждый элемент занимает 4 байта. Тогда адрес элемента с индексом 15 вычисляется как адрес начала массива плюс 15 блоков по 4 байта (всего 60 байт). Чтобы использовать массив в C++, надо его предварительно объ- явить — присвоить ему имя, определить тип входящих в массив пе- ременных (элементов массива) и их количество. По этим сведениям компьютер вычислит, сколько места требуется для хранения массива, и выделит в памяти нужное число ячеек. Такие массивы называют- ся статическими — память для них выделяется при объявлении, их размеры изменить невозможно. Массивы объявляются почти так же, как и обычные переменные, только после имени массива в квадратных скобках указывают количе- ство элементов: int А [ 5 ] ; double velocities[8]; bool proper[10]; char syms[80]; // массив символов Поскольку индексы элементов массива всегда начинаются с нуля, в массиве из пяти элементов последний элемент будет иметь индекс 4 (на единицу меньше размера массива). Массивы можно строить из данных любых типов. Объявленный выше массив syms состоит из отдельных символов. Это не символьная строка, он существенное отличается от переменной типа string. Ни при каких условиях мы не сможем изменить его размер: он всегда бу- дет хранить ровно 80 символов. Это не единый объект-строка, а просто набор символов, поэтому он «не знает» своей длины, для него нельзя использовать те же методы (size, erase и др.), которые мы применя- ли для строк типа string. Количество байт памяти, выделенное под массив, можно вычислить с помощью оператора sizeof: int А [ 5 ] ; cout << sizeof( А ) ; Так как все элементы массива занимают одинаковое место в памяти, его размер (количество элементов) можно определить, разделив размер всего массива на размер одного элемента: int N = sizeof( А )/sizeof( А[0] ); 140
Массивы §16 Поскольку размер массива невозможно изменить во время работы программы, его удобно задать как константу и затем везде использо- вать эту константу: const int MAX_PERSON = 5; int salary[MAX_PERSON]; Теперь, если в новой версии программы нужно поменять размер массива, достаточно изменить только значение константы MAX_PERSON. После объявления локального массива (в основной программе или в функции) все его элементы содержат «мусор», и использовать их бес- смысленно. Заполнить массив можно сразу при объявлении, перечис- лив значения его элементов в фигурных скобках: int А[5] = {0, 3, 12, 45, 67}; Если количество значений меньше, чем количество элементов массива, остальные заполняются нулями, а если больше — компилятор сообщит об ошибке. Рекомендуется всегда определять начальные значения для элементов массива (инициализировать их), например, заполняя массив нулями: int cardNumbers[25] = {}; // все нули! Хотя все глобальные массивы в C++ (как и остальные глобальные переменные) автоматически заполняются нулями, считается хорошим стилем явно присваивать им начальные значения. Когда определены начальные значения всех элементов, размер мас- сива можно не указывать, транслятор подсчитает его сам: int А[] = {0, 3, 12, 45, 67}; string friends[] = {"Вася", "Петя ", "Маша"}; Здесь в массиве А будет выделена память для пяти элементов типа int, а в массиве friends — для трёх элементов типа string. Обращение к элементу массива Для того чтобы обратиться к элементу массива (прочитать или изме- нить его значение), надо записать имя массива и затем в квадратных скобках — индекс нужного элемента, например А [ 2 ]. Индексом может быть также значение арифметического выражения, результат которого — целое число. Например, для массива int А[] = {3, 2, 7, 0, 5}; программа int i = 1; cout « A[i] « A[i+1] « A[3*i+1] « A[A[3*i]]; выведет то же самое, что и программа cout « А[1] « А[2] « А[4] « А[0]; 141
2 Программирование на языке C++ В записи A[A[3*i]] индекс нужного элемента сам является элемен- том массива. При i = 1 получаем: A[A[3*i] ] = А[А[3]] = А[0] = 3. В языке C++ разрешено обращаться к элементу массива с несущест- вующим индексом, но при этом происходит серьёзная ошибка — вы- ход за границы массива. Результаты этой ошибки часто непредсказу- емы: при чтении данных мы получим неправильное значение, а при записи изменим значение какой-то ячейки памяти, которая не принад- лежит массиву. Нередко в этом случае по ошибке меняется значение некоторой локальной переменной, поэтому программа может получить неверные результаты и даже завершиться аварийно. Поиск и исправ- ление ошибок обращения к памяти очень сложны и обычно требуют значительного времени. Перебор элементов массива Далее везде будем считать, что N — это размер массива, который раз- мещён в памяти, например, так (с заполнением нулями): const int N = 10; int A[N] = {}; // все элементы равны 0 Для перебора элементов обычно используют цикл по переменной, которая изменяется от минимального до максимального индекса, т. е. от 0 до N - 1: for( int i = 0; i < N; i++ ) { A[i] = i; // здесь что-то делаем c A[i] } Эта программа заполняет массив целыми числами от 0 до N - 1. В современных версиях языка (начиная с С++11) для перебора элементов массива можно использовать цикл, аналогичный циклу for х in А в языке Python: for( int& х: А ) х = 1; Здесь все элементы массива заполняются единицами. Заметим, что переменная х — это ссылка (int&) на очередной элемент массива, с помощью которой этот элемент можно изменять. Если не писать знак &, то переменная х будет независимой переменной, никак не связанной с массивом. Например, такой цикл никак не изменит массив: for( int х: А ) х = 0; 142
Массивы §16 Вывод массива Можно попытаться вывести массив так же, как и в Python: cout << А; Такая команда выполнится, но результат будет неожиданным, напри- мер таким: 0x22f3f0 Прежде всего, нужно понять, что мы увидели. Символы Ох говорят о том, что после них записано число в шестнадцатеричной системе счис- ления. На самом деле, это адрес памяти, с которого начинается массив. Чтобы вывести значения всех элементов массива, придётся использо- вать цикл: for( int i = 0; i < N; i++ ) cout « A[i] « " После вывода каждого элемента добавляется пробел, иначе все зна- чения будут выведены вплотную, как одно длинное число (если в мас- сиве были числа). Тот же цикл в краткой форме (С+-Ы1): for( int х: А ) cout « х << " Здесь переменную х можно не делать ссылкой, потому что элементы массива изменять не нужно. Ввод массива с клавиатуры Для ввода массива с клавиатуры тоже используется цикл: for( int i = 0; i < N; i++ ) cin >> A[i]; В C+-1-11 можно использовать такой вариант: for( int& x: A ) cin » x; Перед вводом очередного элемента полезно вывести сообщение с под- сказкой: for( int i = 0; i < N; i++ ) { cout « "A[" « i « cin >> A[i]; } Заполнение массива случайными числами Для заполнения массива случайными числами удобно использовать стандартный генератор псевдослучайных чисел из библиотеки random. 143
2 Программирование на языке C++ Сначала нужно подключить саму библиотеку: #include <random> Функция rand возвращает случайное целое число на отрезке [0; RAND_MAX], где RAND_MAX — это константа, определённая в библио- теке random. Для получения случайного числа на отрезке [а; Ь] можно написать свою функцию: int randint ( int a, int b ) { return a + rand() % (b-a+1); } Взяв остаток от деления результата функции rand на Ь-а+1, мы получим число на отрезке [0; b-а], а затем добавлением а переходим к нужному отрезку [а; Ь]. Для заполнения массива используется цикл: for( int i = 0; i < N; i++ ) A[i] = randint (20, 100); Начиная с версии C++11, можно использовать такую форму: for( int& x: A ) x = randint (20, 100); Алгоритмы обработки массивов Стандартные операции с массивами программируются почти так же, как и в языке Python. Например, следующий фрагмент находит сумму элементов массива А: int sum = 0; for( int i = 0; i < N; i++ ) sum += A[i]; cout << sum; Для подсчёта элементов массива, удовлетворяющих условию, исполь- зуется цикл и условный оператор: int count = 0 ; for( int i = 0; i < N; i++ ) if( 0 <= A[i] and A[i] <= 255 ) count++; cout << count; Выводы • Массив в C++ — это группа переменных одного типа, располо- женных в памяти друг за другом и имеющих общее имя. К эле- ментам массива можно обращаться по индексу. 144
Массивы §16 • Массивы в C++ нужно объявлять, при этом сразу выделяется память для массива. Такие массивы называются статическими. • Размер массива удобно вводить как константу. • Нумерация элементов массива в C++ начинается с нуля. • При объявлении можно задать начальные значения элементов мас- сива, перечислив их в фигурных скобках. Если начальные зна- чения локального массива не заданы, в его ячейках содержится «мусор». • В C++ разрешается обращаться к ячейкам за пределами масси- ва, используя неправильные индексы, но это может привести к серьёзным ошибкам. • Ввод, вывод и обработка элементов массива выполняются с помо- щью цикла. Вопросы и задания 1. Почему для задания размера массива лучше использовать констан- ту, а не записывать везде размер числом? 2. Напишите фрагмент программы, который заполняет массив из N элементов первыми N натуральными числами в обратном порядке (А[0] = N, А[1] = N-1, ..., А[Х-1] = 1). 3. Заполните массив первыми N числами Фибоначчи. *4. Заполните массив первыми N простыми числами. 5. Найдите и исправьте все ошибки в приведённом фрагменте про- граммы, которая вычисляет сумму элементов массива. int А[] = {1, 2, 3, 4, 5, 6, 7}; int i, sum; while ( i < 10 ) sum += A[i] ; 6. С клавиатуры вводятся целые значения X и Y (X < У). Заполни- те массив случайными вещественными числами на полуинтервале [X; У). 7. Запишите циклы, приведённые в конце параграфа, с использовани- ем возможностей С++11. 8. В программе объявлен массив int А[] = {5, 4, 3, 2, 1}; Чему будут равны элементы этого массива после выполнения цикла? a) for ( int i = 0; i < N; i++ ) A[i] += A[i]; 6) for ( int i = 0; i < N-1; i++ ) A[i] += A[i+1]; в) for ( int i = 0; i < N-1; i++ ) A[i+1] += A[i]; r) for ( int i = N-1; i > 0; i-- ) A [i] += A[i-1]; 145
2 Программирование на языке C++ д) for ( int i = 1; i < N; i++ ) A[i] += A[i-1] + 1; e) for ( int i = 1; i < N; i++ ) A [i] += A[i-1]; 9. Дан фрагмент программы: int k = 0; for( int i = 1; i < N; i++ ) iff A[i] != A[i-1] ) k = i; Какое условие обязательно должно быть истинным, чтобы после выполнения этого фрагмента значение переменной к было равно: а) 0; б) 2; в) N? Для каждого случая приведите пример массива А. 10. Решите задачи 7-20 из § 8 на языке C++. 11. Напишите программу, которая выполняет реверс массива, т. е. пе- реставляет его элементы так, чтобы первый по счёту элемент стал последним, второй — предпоследним, а последний — первым. 12. Напишите программу, которая выполняет циклический сдвиг мас- сива влево: каждый элемент, кроме самого первого, переходит на место предыдущего, а первый — на место последнего. 13. Напишите программу, которая выполняет циклический сдвиг эле- ментов массива вправо аналогично предыдущему заданию (послед- ний элемент нужно переместить в самое начало массива). 14. В двух массивах, X и Y, записаны координаты сундуков с сокрови- щами, а в массиве money — количество золотых монет в каждом сундуке. Пират находится с точке с координатами X = 0, Y = 0 и до захода солнца может добраться до всех сундуков внутри круга радиуса 200 м. Напишите программу, которая определяет, сколько монет сможет собрать пират. * 15. В массиве di st записаны расстояния от игрока до опасных существ. Существо считается угрожающим, если расстояние до него меньше, чем 100. Напишите программу, которая определяет, угрожают ли игроку какие-нибудь существа. * 16. В массиве dist записаны расстояния от игрока до опасных существ. Существо считается угрожающим, если расстояние до него меньше, чем 100. Напишите программу, которая отбирает в новый массив индексы угрожающих существ. * 17. В массиве dist записаны расстояния от игрока до опасных существ, а в массиве enemies - коды этих существ. Напишите программу, которая выводит код самого опасного существа (того, которое ближе всего к игроку). * 18. В массиве dist записаны расстояния от игрока до опасных существ, а в массиве enemies - коды этих существ. Напиши- те программу, которая сортирует массив dist по степени угро- зы (по возрастанию расстояния), сохраняя связь массивов dist и enemies. 146
Используем массивы §17 §17 Используем массивы Ключевые слова’. • массив • константа • инициализация • глобальные переменные • обработчик события Игра «Стрельба по тарелкам» В этом параграфе мы перепишем на языке C++ игру «Стрельба по тарелкам», которую раньше сделали на языке Python (см. § 9). В начале программы нужно подключить библиотеки: ТХ Library и random (для работы со случайными числами): #include "TXLib.h" #include <random> Введём константы, такие же как и в программе на Python: const int WIDTH = 500, HEIGHT = 600; const int MAX_PLATES = 10; const int MIN_V = 1, MAX_V = 5; const int MIN_R = 10, MAX_R =' 30; Данные кругов-тарелок мы будем хранить в массивах. В отличие от графической библиотеки Python фигуры, нарисованные нами на экра- не, — это не векторные фигуры (не объекты), они не «помнят» ни своих координат, ни размеров. Все эти данные придётся хранить в массивах. Эти массивы нужно сделать глобальными, чтобы к ним мог- ли обращаться все процедуры и функции: int rPlate[MAX_PLATES], xPlate[MAX_PLATES], yPlate[MAX_PLATES], velPlate[MAX_PLATES]; COLORREF color[MAX_PLATES]; В массивах rPlate, xPlate, yPlate и velPlate будем хранить соот- ветственно радиусы, х-координаты, 1/-координаты и скорости движения тарелок, а в массиве color — цвета заливки. Сразу введём две функции для работы со случайными числами, которые будем использовать в программе: 147
2 Программирование на языке C++ int randint( int a, int b ) { return a + rand() % (b-a+1) ; } COLORREF randColor() { return RGB(randint(0, 255), randint(0, 255), randint(0, 255)); } Функция randint возвращает случайное целое число на заданном отрезке (мы уже её использовали), а функция randColor — случай- ный цвет, у которого все три составляющие в модели RGB (красная, зелёная и синяя) выбираются случайно на отрезке [0; 255]. Теперь можно написать процедуру, которая выполняет инициализа- цию — заполнение массивов начальными значениями: void init() { for( int i = 0; i < MAX_PLATES; i++ ) { rPlate[i] = randint( MIN_R, MAX_R ); xPlate[i] = randint( rPlate[i], WIDTH-rPlate[i] ); yPlate[i] = randint( rPlate[i], HEIGHT-rPlate[i] ); velPlate[i] = randint( MIN_V, MAX_V ); color[i] = randColor(); } } Следующая задача — нарисовать тарелки на экране. Для этого напишем ещё одну процедуру: void drawPlates () { f or ( int i = 0; i < MAX_PLATES; i++ ) { txSetFillColor( color[i] ); txCircle ( xPlate[i], yPlate[i], rPlate[i] ); } } Отметим, что эта процедура, так же, как и процедура init, использует глобальные массивы. Поскольку цвет заливки у всех тарелок разный, мы устанавливаем его на каждом шаге цикла (для каждой следующей тарелки) командой txSetFillColor. Команда txCircle рисует круг, первые два её параметра — координаты центра круга, а третий — его радиус. 148
Используем массивы §17 Анимацию будем делать так же, как и в § 13, каждый раз очищая весь холст и рисуя все круги заново. Получается такая основная про- грамма: int main() { txCreateWindow( WIDTH, HEIGHT ); init (); while ( not GetAsyncKeyState ( VK_ESCAPE ) ) { txSetFillColor ( TX_BLACK ); txClear (); drawPlates (); txSleep (50); } } Но тарелки пока не двигаются. Движение Для того чтобы тарелки двигались справа налево, нужно на каждом шаге цикла уменьшать их х-координаты на некоторое значение, кото- рое мы называли скоростью. Скорости тарелок различаются и хранят- ся в массиве velPlate. Напишем процедуру movePlates, которая двигает тарелки: void movePlates() { for( int i = 0; i < MAX_PLATES; i++ ) { xPlate[i] -= velPlate[i]; if( xPlate[i] < -rPlate[i] ) xPlate[i] += WIDTH + 2*rPlate[i]; } } Изменив х-координату очередной тарелки, мы проверяем, не вышла ли тарелка из области холста. Если вышла, то «перекидываем» тарелку на правую сторону холста, добавляя к х-координате ширину холста плюс диаметр этой тарелки. Вызов этой процедуры нужно добавить в тело цикла сразу после вы- зова txSleep. Бьём тарелки В первую очередь нужно определить, нажата ли кнопка мыши. Для этого в теле цикла вызовем функцию txMouseButons из библиотеки ТХ Library. Она возвращает 0, если ни одна кнопка не нажата, 1 — 149
2 Программирование на языке C++ если нажата левая кнопка мыши, 2 — если нажата правая, и 3 — если нажаты обе кнопки1*. if( txMouseButtons() > 0 ) ... // нажата какая-то кнопка мыши Событие нужно обработать, для этого мы напишем процедуру onMouseClick, которую будем вызывать при щелчке: if( txMouseButtons() > 0 ) onMouseClick ( txMouseX(), txMouseYO ); Этой процедуре передаются координаты курсора мыши. Мы опре- деляем их с помощью функций txMouseX и txMouseY из библиотеки ТХ Library. Давайте подумаем, что нужно делать, если щёлкнули по тарелке. Самый простой вариант — просто сделать радиус этой тарелки равным нулю. Процедура onMouseClick может выглядеть так: void onMouseClick( int х, int у ) { for ( int i = 0; i < MAX_PLATES; i++ ) if ( hit(x, y, i) ) { rPlate[i] = 0; break; } } Логическая функция hit определяет, верно ли, что мы щёлкнули мышью по тарелке с номером i. Если попали по тарелке, меняем её радиус на 0 и выходим из цикла. Один щелчок уничтожает только одну тарелку. В функции hit вычислим расстояние от центра тарелки до точки, где щёлкнули мышью (по теореме Пифагора), и вернём значение true («истина»), если это расстояние меньше, чем радиус тарелки: bool hit( int х, int у, int plateNo ) { float distX = xPlate[plateNo] - x; float distY = yPlate[plateNo] - y; float distance = hypot( distX, distY ); return distance < rPlate[plateNo]; } Стандартная функция hypot, которую мы вызвали, вычисляет гипотенузу прямоугольного треугольника по двум его катетам. То есть hypot ( distX, distY ) — это то же самое, что и sqrt( distX*distX + distY*distY ) . Для определения состояния мыши можно также использовать функцию GetAsyncKeyState с параметрами MK_LEFT (левая кнопка мыши) и MK_RIGHT (правая кнопка). 150
Используем массивы §17 Показываем счёт Для того чтобы показать количество набранных очков, нужно это зна- чение где-то хранить. Удобно завести для этого глобальную перемен- ную score: int score = 0; Увеличивать эту переменную нужно внутри функции onMouseClick: тогда, когда мы зафиксировали щелчок по тарелке. Для вывода данных на холст применим функцию txTextOut (из библиотеки ТХ Library), которой нужно передать координаты левого верхнего угла текста и символьную строку для вывода. Поэтому воз- никает задача представить всё, что мы хотим вывести, в виде символь- ной строки. Сделаем это через потоки, как в § 15. Подключим в нача- ле программы библиотеку sstream: #include <sstream> и объявим в основной программе временную переменную — поток для вывода в строку с именем temp: ostringstream temp; Теперь в основной цикл сразу после вызова процедуры drawPlates добавим такой код: temp.str( ; // (1) temp << "Счёт: " << score; // (2) txTextOut( 10, 10, temp.str().c_str() ); // (3) В строке (1) мы очищаем поток, записывая в него пустую строку, в строке (2) добавляем данные в поток, а в строке (3) выводим полу- ченную строку на экран. Как мы уже говорили, первые два аргумента функции txTextOut — это координаты левого верхнего угла текста, а третий — символьная строка. Но эта строка должна быть в формате языка С — ссылка на область памяти с кодом 0 на конце. Поэтому мы сначала получаем строку типа string из потока, применив метод str, а потом уже эту строку переводим в нужный формат с помощью метода c_str. Можно было записать эти операции в развёрнутом виде: string tempString = temp.str(); txTextOut ( 10, 10, tempString.c_str() ); Итак, мы построили начальную версию простой игры. Доработать её вы можете самостоятельно. Тут есть много возможностей для развития и улучшения. 151
2 Программирование на языке C++ Выводы • В программах на языке C++ свойства группы объектов удобно хранить в массивах. • Для работы с мышью в библиотеке ТХ Library используются функции txMouseButtons (определяет, нажаты ли кнопки мыши), txMouseX и txMouseY (возвращают координаты курсора мыши). • Функция txTextOut позволяет вывести данные на холст. Перед этим нужно представить их в виде символьной строки в формате языка С (с кодом 0 в конце строки). Вопросы и задания 1. Как вы думаете, что произойдёт, если убрать команду txSetFillColor (из параграфа) перед вызовом txClear? Проверьте свою версию с помощью программы. 2. Что изменится, если удалить оператор break в процедуре onMouseClick (из параграфа)? 3. Представьте, что изображения двух тарелок на экране наложились друг на друга. Изучите программу из параграфа и определите, ка- кая из них будет удалена, если игрок попал щелчком в их общую область. 4. Как изменить процедуру onMouseClick (из параграфа) так, чтобы при наложении тарелок удалялась верхняя тарелка (см. предыдущее задание). 5. Как ускорить движение тарелок? Предложите два способа и сравни- те их. 6. Какие изменения нужно внести в программу, чтобы тарелки летели слева направо? Сверху вниз? 7. Какие изменения нужно внести в программу, чтобы тарелки не за- ходили в область метки со счётом? 8. Выполните рефакторинг программы из параграфа: вынесите вывод счёта в отдельную процедуру. 9. Составьте список недостатков программы, которая рассмотрена в тексте параграфа. Попытайтесь исправить их. 152
Матрицы §18 §18 Матрицы Ключевые слова: • матрица • квадратная матрица • строка • главная диагональ • столбец • побочная диагональ • перебор элементов • перестановка строк • вложенный цикл Что такое матрица? Матрица в языке C++ — это двумерный массив, т. е. массив, каждый элемент которого имеет два индекса — индекс строки и индекс столбца. Матрицы, как и массивы, нужно объявлять, указывая тип данных и два размера (они должны быть числами или константами): const int N = 3, М = 4; int A[N][М]; При объявлении можно (и даже нужно!) сразу задать начальные значе- ния для элементов матрицы, например: int А[3][4] = { {1, 2, 3, 4}, {5, 6, 7}, {9} }; Значения элементов каждой строки взяты в фигурные скобки, кроме того, все данные матрицы охвачены ещё одной парой фигурных ско- бок. Для двух последних строк задано меньше элементов, чем выделе- но памяти — остальные будут заполнены нулями. Если элементы массивов нужно просто обнулить, вместо начальных значений можно просто поставить пару пустых фигурных скобок: double weight[10][12] = {}; bool isGood[N][2] = {}; Рекомендуется всегда задавать начальные значения для элементов матриц. Если этого не сделать, в ячейках оказывается «мусор», кото- рый может привести к серьёзным ошибкам во время выполнения про- граммы. В языке C++ все строки «обычных» матриц (таких, как определён- ные выше) обязательно должны быть одинаковой длины. В принципе можно построить и матрицу, в которой строки будут иметь разную длину, но это не так просто. Нумерация строк и столбцов матрицы начинается с нуля. Для обращения к элементу матрицы используются две пары квадратных скобок: в первой записывают индекс строки, во второй — индекс столбца, например А [ 3 ] [ 2 ]. 153
2 Программирование на языке C++ Размещение матрицы в памяти В языке C++ матрица хранится в памяти по строкам. Это значит, что сначала размещаются все ячейки самой первой строки (с индексом 0), затем — второй, и т. д. На рисунке 2.7 показано размещение в памяти матрицы А, состоящей из трёх строк и трёх столбцов. Процессор при обработке массивов и матриц использует кэширо- вание — читает данные из памяти блоками в быстродействующую кэш-память. Если при выполнении следующей команды нужные дан- ные уже есть в кэш-памяти, они загружаются в процессор оттуда, а не из относительно медленной оперативной памяти. Поэтому об- работка элементов матрицы по строкам, т. е. в том же порядке, в котором они расположены в памяти, может ускорить вычисления в 5-6 раз. Заполнение матрицы Для того чтобы присвоить значения всем элементам матрицы, нужен вложенный цикл. В одном цикле перебираются все индексы строк, а во втором — все индексы столбцов. Далее будем предполагать, что в программе объявлена матрица А, состоящая из N строк и М столбцов: const int N = 3, М = 2; int A[N][М] = {}; Заполнение этой матрицы случайными числами на отрезке [20; 80] выполняется так: for( int i = 0; i < N; i++ ) for( int j =0; j < M; j++ ) A[i][j] = randint( 20, 80 ); Здесь мы использовали функцию randint, которая была написана ранее. Обратите внимание, что матрица заполняется по строкам: сначала индекс столбца j проходит все возможные значения, и только потом 154
Матрицы §18 меняется индекс строки i. Как мы обсудили выше, такая последова- тельность перебора элементов ускоряет вычисления. Вывод матрицы на экран Вывести матрицу на экран одной командой, как в Python, не удастся. Для этого придётся использовать вложенный цикл: for( int i = 0; i < N; i++ ) { for( int j =0; j < M; j++ ) cout << setw(4) << A[i][j]; cout << endl; } Каждое значение выводится в четырёх позициях, после вывода оче- редной строки матрицы происходит переход на новую строку на экране (с помощью команды endl). Таким образом, если все значе- ния элементов матрицы занимают не более трёх знаков, матрица вы- водится ровными столбиками. Обработка матриц Каждый элемент матрицы имеет два индекса, поэтому для перебора всех элементов нужно использовать вложенный цикл. Вот так можно найти сумму всех элементов матрицы: for( int 1=0; i < N; i++ ) for( int j =0; j < M; j++ ) sum += A[i] [ j] ; Во многих практических задачах встречаются квадратные матри- цы, у которых количество строк и количество столбцов одинаковые. Пусть матрица А содержит N строк и столько же столбцов. Для перебора всех элементов главной диагонали (у которых индексы стро- ки и столбца совпадают) нужен один цикл: for( int i = 0; i < N; i++ ) { ... // работаем c A[i][i] } Аналогичным циклом перебираются элементы второй (побочной) диагонали, для которых сумма индексов строки и столбца постоянна и равна N-1: for( int i = 0; i < N; i++ ) { ... // работаем c A[i][N-l-i] } 155
2 Программирование на языке C++ Чтобы обработать все элементы на главной диагонали и под ней, нужен вложенный цикл: номер строки будет меняться от 0 до N-1, а номер столбца для каждой строки i — от 0 до i: for( int i = 0; i < N; i++ ) for( int j = 0; j <= i; j++ ) { ... // работаем c A[i][j] } Чтобы переставить строки или столбцы матрицы, достаточно одно- го цикла. Например, поменяем местами строки с индексами 2 и 4, используя вспомогательную переменную temp: for( int j = 0; j < M; j++ ) { int temp = A[2] [j] ; A[2] [j] = A[4] [j] ; A[4][j] = temp; } Выводы • Матрица в языке C++ — это двумерный массив, каждый элемент которого имеет два индекса. Место в памяти для матрицы выде- ляется при объявлении. • Все строки матрицы содержат одинаковое количество элементов. • Нумерация строк и столбцов матрицы в языке C++ начинается с нуля. • Для перебора всех элементов матрицы используется вложенный цикл. Вопросы и задания 1. Запишите фрагмент программы, который меняет местами столбцы с индексами 3 и 5 в матрице, состоящей из N строк и М столбцов (М > 5). 2. Заполните матрицу случайным образом нулями и единицами. Счи- тая, что каждая строка матрицы — это двоичная запись некоторого числа, найдите сумму этих чисел и выведите её на экран в десятич- ной системе. Например, строки матрицы int А[3] [4] = { {1, 0, 0, 1}, {0, 0, 1, 1}, {1, 1, 0, 1} }; кодируют числа Ю012 = 9, 112 = 3, 1Ю12 = 13. Их сумма равна 25. 3. Объявите матрицу, состоящую из трёх строк и восьми столбцов. Вве- дите с клавиатуры три числа на отрезке [0; 255] и заполните строки матрицы двоичными кодами этих чисел (см. предыдущую задачу). 156
Матрицы §18 4. Заполните матрицу случайным образом нулями и единицами. Клет- ка с единицей обозначает бактерию, у неё может быть до восьми соседей (эти клетки отмечены серыми точками): Подсчитайте: а) у скольких бактерий нет соседей; б) у скольких бактерий два или три соседа; в) у скольких бактерий больше трёх соседей; г) сколько есть пустых клеток, у которых ровно три соседа. 5. В предыдущей задаче заполните матрицу нулями и единицами так, чтобы: а) единиц было больше, чем нулей, примерно в два раза; б) нулей было больше, чем единиц, примерно в два раза. 6. В задаче 4 найдите на поле: а) количество вертикальных цепочек, состоящих не менее чем из трёх бактерий; б) количество блоков из четырёх бактерий, окружённых пустыми клетками: в) количество блоков специальной формы (эта форма называется «улей»): 7. Проект. Узнайте из дополнительных источников про игру «Жизнь» математика Джона Конвея и попробуйте написать свою программу, которая моделирует эту игру. 157
2 Программирование на языке С++ *8. Проект. Узнайте из дополнительных источников, как строятся лабиринты. Напишите программу, которая строит лабиринт — мат- рицу, состоящую из нулей и единиц, где единица обозначает стен- ку, а ноль — проход. *9. Проект. Дана матрица, в которой закодирован лабиринт (см. пре- дыдущее задание). Левый верхний угол — это вход в лабиринт, правый нижний — выход из лабиринта. Робот может переходить в соседние клетки (слева, справа, внизу и сверху, но не по диа- гонали). Напишите программу, которая находит кратчайший путь Робота от входа лабиринта к выходу и показывает его на экране. Почитайте в дополнительных источниках про волновой алгоритм. 158
ПРИЛОЖЕНИЕ УПРАВЛЕНИЕ ВЕРСИЯМИ Системы управления версиями Зачем это нужно? Многие начинающие программисты сталкиваются с ситуацией, когда в результате улучшения работавшей программы она перестаёт работать и вернуть обратно сделанные изменения не удаётся. Конечно, выход есть: каждый раз перед тем, как в программу будут внесены серьёзные изменения, можно создавать полную копию всех файлов в отдельном каталоге. В этом случае вы всегда сможете вернуться к любому сохранённому варианту программы. А для того, чтобы не забыть, где какая версия и какие в ней были сделаны из- менения, нужно вести список изменений в отдельном файле. Это до- статочно утомительная работа, которая требует большой аккуратности. Если сохранённых версий много, легко запутаться в них и забыть, чем же они отличаются друг от друга. Чтобы автоматизировать работу с архивом версий, придумали специ- альные программные системы — системы, управления версиями (англ. VCS: version control systems). Чаще всего их применяют при разработ- ке программ и веб-сайтов. Системы управления версиями умеют: • хранить сколько угодно версий программы; • вести журнал, в котором сохраняется комментарий для каждой сохранённой версии; • восстанавливать любую из сохранённых версий; • хранить несколько вариантов («веток») разработки программы; • объединять изменения, сделанные в нескольких ветках. Чем-то они напоминают «машину времени», которая даёт возмож- ность перемещаться по истории изменений в программе — сначала в прошлое, а потом обратно в настоящее. Современные системы управления версиями решают ещё одну серьёзную проблему. Большинство программ в наше время разрабаты- ваются целыми командами программистов. И нужно, чтобы все они могли работать одновременно и независимо друг от друга. Организо- вать это непросто, ведь каждый работает на своём компьютере, и в то же время все они используют один и тот же набор файлов. 159
Приложение Представим себе, что файлы проекта, с которым работает команда, хранятся на сервере сети. Если один из разработчиков начинает изме- нять какой-то файл прямо на сервере, этот файл блокируется так, что остальные не могут его изменить. В таких условиях совместная работа становится почти невозможной. Значительно удобнее, когда каждый из программистов использу- ет (и изменяет) свою копию файлов. Но потом нужно как-то объеди- нить все изменения, сделанные на разных компьютерах. Делать это вручную крайне сложно, а современные системы управления версиями успешно справляются и с этой задачей. При загрузке каждой новой версии программы в базе данных сохра- няется информация о том, кто обновлял файлы и какие именно изме- нения были сделаны. Поэтому с помощью системы управления версия- ми всегда можно восстановить историю событий. Представьте себе, что компания хочет доработать свой веб-сайт. При этом во время внесения и отладки всех изменений прежняя версия сайта должна надёжно работать. Система управления версиями позво- ляет создать новую «ветку», в которой разработчик будет вносить ис- правления. И только после окончания отладки изменённые файлы из этой ветки добавляются в основной набор файлов, которые размещают- ся на сайте. Все эти достоинства сделали системы управления версиями неза- менимыми инструментами для командной разработки программ, веб- сайтов, документов. Какие бывают системы управления версиями? Самые простые системы управления версиями — локальные. В них все данные хранятся на одном компьютере. Локальные системы создают базу данных, куда записывают «снимки» (полные копии) каждой вер- сии или изменения, которые были сделаны по сравнению с предыду- щей версией. Но для командной работы локальные системы не походят. Дей- ствительно, если каждый программист работает над своей версией, как потом свести вместе все изменения? В этом случае нужно, чтобы основная база данных хранилась на сервере в сети. Так появились централизованные системы управления версиями (CVS, Subversion). В них есть центральный сервер, который хранит базу данных со все- ми версиями проекта. Компьютеры-клиенты обращаются к нему и скачивают файлы, когда нужно. Но тут всё зависит от сервера: если с ним что-то случится, база будет потеряна. Если связь нарушит- ся, работать дальше будет тяжело, восстановить предыдущую версию невозможно. Решить эту проблему несложно — достаточно хранить всю базу вер- сий на каждом компьютере-клиенте. Это и было сделано в распреде- лённых системах (Git, Mercurial). При каждом скачивании на ком- 160
Управление версиями пьютер разработчика загружается полная копия всех данных — все сохранённые версии проекта. После этого программист может работать только на своём компьютере, не обращаясь к серверу. Затем, когда он внесёт все необходимые изменения в локальную версию, новые фай- лы будут загружены на сервер. Когда другие члены команды будут об- новлять файлы проекта на своих компьютерах, они получат версию с самыми последними изменениями. Git Распределённая система управления версиями Git была разработана Линусом Торвальдсом, создателем операционной системы GNU/Linux с открытым исходным кодом и свободной лицензией. Система получи- лась быстрой и надёжной, она позволяет работать со многими вариан- тами («ветками») программы в большой команде разработчиков. Ядро (основная часть) системы Git — это утилиты, которые запу- скаются из командной строки. Все настройки программ хранятся в текстовых файлах, которые называются файлами конфигурации. Такой подход позволяет использовать с системой Git различные графические оболочки. База данных проекта в Git называется репозиторием (от англ. repository — хранилище). Репозиторий можно организовать в любом каталоге, по умолчанию он создаётся как скрытый каталог с именем .git. При любых изменениях в репозитории создаются новые файлы. Фай- лы, уже записанные в хранилище, никогда не изменяются. Основные приёмы работы с Git Начало работы Прежде всего, нужно установить на свой компьютер программу Git. Если вы работаете в операционной системе Linux, используйте коман- ду install пакетного менеджера, например для Ubuntu: apt-get install git В операционной системе Windows нужно установить набор программ с сайта gitforwindows.org. В контекстное меню Проводника добавляют- ся две команды: • Git Bash here — запустить командный процессор bash в текущем каталоге; • Git GUI here — запустить графическую оболочку. Далее во всех примерах мы будем использовать командный процес- сор (рис. 1). Так можно работать во всех операционных системах и легче понять, что же происходит на самом деле. 161
Приложение ф MING W32:/e/gtt/tert-git <___________________________ Jalxll $ cd e:\git E kp@kot MINGW32 /e/git Sis i ру-test/ test-git/ kp@kot MINGW32 /е/git I $ cd test-git । kpOkot MINGW32 /е/git/test-git (master) S git status i On branch master Untracked files: (use "git add <file>..." to include in what will be committed) I .gitignore , tmp/ I ttt.txt nothing added to commit but untracked files present (use "git add” to track) kp@kot MINGW32 /е/git/test-git (master) Рис. 1 Сначала делаем глобальные настройки — задаём имя пользователя и адрес электронной почты: git config —global user.name "j-bond" git config —global user.email "jbond@mail.ru" Эти данные будут сохраняться вместе с каждой версией, добавленной в репозиторий. Затем нужно определить, какие символы используются в текстовых файлах для перехода на новую строку (они разные в различных опера- ционных системах). Для Windows эти команды настройки такие: git config —global core. autocrlf true git config --global core. safecrlf true Для того чтобы создать репозиторий в некотором каталоге, нужно сначала перейти в этот каталог в окне командного процессора. Напри- мер, для перехода в каталог C:\projects\game в Windows нужно ввести команду cd c:\projects\game Далее выполним команду создания репозитория: git init Теперь в текущем каталоге появился скрытый каталог .git, где хранится репозиторий, готовый к работе. 162
Управление версиями Операции с файлами С точки зрения Git, файлы в любом проекте могут находиться в трёх состояниях: изменённом, подготовленном к записи и сохранённом. Файлы, состояние которых одинаково, образуют группы («области»): • рабочую область, в которой вы изменяете файлы; • индекс, или область подготовленных файлов (staging area) — фай- лы, готовые к записи в репозиторий; • репозиторий, в котором хранятся «снимки» всех файлов проекта. Файлы переводятся из одной области в другую с помощью команд Git. Покажем цикл работы с файлами на простом примере. Пусть в проекте используются два файла — game. ру и enemy. ру. Сразу после создания этих файлов Git обнаружит их, но пока (без команды) с ними работать не будет. Состояние проекта можно посмо- треть по команде git status которая выведет имена новых файлов красным цветом под заголовком Untracked files (файлы, которые не отслеживаются). Добавим оба файла к набору файлов, изменения в которых нужно отслеживать: git add game.py git add enemy.py Каждый из этих файлов программа индексирует — вычисляет для него контрольную сумму (хэш), которая соответствует состоянию фай- ла. Формула для построения контрольной суммы выбрана так, чтобы при малейшем изменении данных в файле эта сумма изменялась. Та- ким образом, для того чтобы определить, изменился ли файл, доста- точно рассчитать его новую контрольную сумму и сравнить с той, что была запомнена ранее. Можно добавить в индекс сразу все файлы из текущего каталога и всех подкаталогов, указав точку (стандартное обозначение текущего ка- талога) вместо имени файла: git add . Файлы, которые не изменялись после добавления в индекс, находят- ся в области подготовленных файлов, они готовы к записи в архив. Область подготовленных файлов работает как «черновик», в котором вы собираете очередную версию проекта перед отправкой в репозито- : рий. ( Операцию записи из индекса в репозиторий часто называют комми- том по названию команды commit (по-английски — совершить, зафик- 1 сировать): git commit -m "Some changes..." 163
Приложение По этой команде в архиве сохраняется новая версия, включающая все файлы, подготовленные к записи. После ключа -т (от англ, mes- sage — сообщение) в кавычках записывают сообщение, которое будет храниться в журнале для этой версии программы. Обычно в нём крат- ко описывают изменения, внесённые в проект. Далее мы можем изменять файлы в рабочей области. Все обновлён- ные файлы нужно подготовить с помощью команды git add, а затем сохранить новую версию проекта командой git commit («сделать ком- мит>). Можно объединить обе операции: одной командой записать все изме- нённые файлы в индекс и сразу же сохранить новую версию в архиве (её тоже называют коммитом): git commit -a -m "Great changes!" He все файлы нужны Временные и вспомогательные файлы (например, файлы с расширени- ем .bak) нет смысла хранить в репозитории. Чтобы Git не обращала на них внимания, нужно создать рядом с каталогом .git текстовый файл .gitignore и записать в него имена или маски всех файлов, ко- торые не нужно добавлять в репозиторий11. Например, если создать та- кой файл .gitignore: tmp/ *. bak uaua.conf то Git будет игнорировать все файлы в подкаталоге tmp, все файлы с расширением .bak и файл uaua.conf. Восстановление версии Предположим, что у нас есть архив версий Git и потребовалось вос- становить один из сохранённых «снимков» проекта. Команда git log показывает журнал сохранённых версий. Для каждой из версий выво- дятся такие данные: commit 21cd8411b05ae3341177690be09443a3785c62f9 Author: j-bond <jbond@mail.ru> Date: Tue Apr 17 20:45:37 2018 +0300 Some changes... Маска — это шаблон для отбора группы файлов, который может содержать специальные символы: * (любое количество любых символов) и ? (один любой символ). 164
Управление версиями После слова commit записана контрольная сумма (хэш) сохранённого проекта, затем — имя пользователя и его электронная почта (мы вво- дили эти данные при настройке). В следующих строках — дата и тот комментарий, который был задан при сохранении этой версии проекта командой git commit. Допустим, что по дате и комментарию мы нашли нужную версию, которую хотим восстановить. Самый «жёсткий» (англ, hard) способ восстановления такой: git reset —hard 21cd8 В конце команды записаны первые несколько символов контрольной суммы (обычно хватает 5-6 знаков, чтобы можно было однозначно определить нужную версию). При этом файлы в рабочей области заменяются на файлы из архи- ва, а все изменения, сделанные после сохранения этой версии, будут потеряны. Второй вариант — использовать ту же команду reset без ключа --hard: git reset 21cd8 Эта команда удаляет из репозитория версии, сохранённые после дан- ной, но не меняет файлы в рабочей области. Индекс будет пустой, т. е. в области подготовленных файлов не будет никаких файлов. Можно также временно переключиться на выбранную версию: git checkout 21cd8 При этом файлы в рабочей области заменяются на файлы восстанов- ленной версии, но все «снимки» в репозитории сохранятся. Вернуться к последней сохранённой версии можно по команде git checkout master Работа с удалённым архивом Если над проектом работают несколько разработчиков, то все файлы нужно хранить в сети. Такой архив называется удалённым (в отличие от локального, расположенного на компьютере разработчика). Существует несколько веб-сервисов, где можно работать с файла- ми, используя Git. Наиболее популярная площадка — GitHub (github. com) — бесплатно предоставляет место для проектов с открытым ис- ходным кодом. Это значит, что ваши проекты смогут увидеть все же- лающие. Можно создать и закрытый (частный) проект, но в этом слу- чае за хранение данных нужно будет заплатить. Сайт Bitbucket (bitbucket.org) тоже поддерживает Git. Кроме того, на нём можно бесплатно размещать закрытые проекты, в которых уча- ствуют не более пяти разработчиков. 165
Приложение Итак, если вы хотите хранить архив в Интернете, вам нужно за- регистрироваться на одном из сайтов, поддерживающих работу с Git, например на GitHub. После регистрации вы сможете создавать соб- ственные репозитории «с нуля», а также копировать открытые репо- зитории других разработчиков (эта операция по-английски называется fork — разветвление). При работе с удалённым хранилищем используют следующие основ- ные операции: • клонирование удалённого репозитория (создание локальной копии на компьютере пользователя); • отправка изменений, сохранённых пользователем на своём ком- пьютере, в удалённый репозиторий; • загрузка изменений из удалённого репозитория. Представим себе, что в команду пришёл новый программист. Для работы ему нужно скачать себе все файлы проекта. Пусть проект раз- мещён на сайте GitHub пользователем vasya и репозиторий называется vpr. клонирования этого репозитория нужно выполнить команду git clone https://github.com/vasya/vpr.git Если команда выполнится успешно, в текущем каталоге будет создан каталог с именем проекта (vpr) и в него будет загружен весь репози- торий (все сохранённые версии проекта!). Таким образом, на компьюте- ре создаётся полная копия удалённого репозитория. Для удалённого репозитория, с которым вы будете работать, можно создать псевдоним, чтобы не писать каждый раз длинный адрес: git remote add gh https://github.com/vasya/vpr.git Эта команда создаёт для указанного репозитория псевдоним gh. Уви- деть все псевдонимы можно с помощью команды git remote -v Предположим, вы изменили файлы и сохранили новую версию в локальном репозитории. Теперь нужно отправить эти изменения в уда- лённое хранилище. Делается это так: git push -u gh master После ключа -u записывается псевдоним (gh), и этот псевдоним свя- зывается с веткой (вариантом) master — это главная ветка разработ- ки программы. Система Git запоминает эту связь, и в следующий раз можно использовать её краткую форму: git push Если с проектом работают другие разработчики, они тоже вносят изменения в файлы. Обновить файлы на своём компьютере из удалён- ного репозитория можно с помощью команды git pull 166
Управление версиями На рисунке 2 показаны основные команды Git и соответствующие им перемещения данных. Ветки Ветка — это отдельная линия развития программы. Пока вы не используете специальные команды для работы с ветками, в репозито- рии хранится единственная ветка с именем master (по-английски — хозяин, главный), к которой относятся все коммиты. Эту ветку создаёт команда git init. Можно считать, что коммиты в одной ветке вытянуты в одну линию (образуют линейный список) — рис. 3. master Рис. 3 Команда git branch -а показывает все ветки в репозитории (по-английски branch — ветка). Для случая на рис. 3 мы увидим только одно слово master. Пусть теперь нам нужно исправить ошибку в программе, и для это- го мы создаём git branch Теперь концы новую ветку с именем bugfix: bugfix обеих веток совпадают (рис. 4). master bugfix Рис. 4 167
Приложение Мы сказали системе Git, что в этом месте будет развилка, т. е. на основе коммита 3 будет построено два новых коммита (в разных вет- ках), каждый из которых дальше будет развиваться самостоятельно. Посмотрев список веток, мы увидим, что их теперь две, но активная ветка — это по-прежнему ветка master (она обозначена звёздочкой): bugfix * master Это значит, что именно в ветку master будет добавлен следующий «снимок». Сделаем активной новую ветку: git checkout bugfix Теперь новые коммиты будут добавляться в ветку bugfix. Мы можем переключаться и на главную ветку, в результате на обеих могут по- явиться новые коммиты (на рис. 5 показано по одному новому комми- ту в каждой ветке). master bugfix Рис. 5 Когда ошибка найдена и исправлена, нужно добавить все изменения из ветки bugfix в главную ветку master. Для этого сделаем активной ветку master и выполним команду git merge bugfix После этого в ветке master автоматически создаётся новый «сни- мок», в котором объединяются изменения из коммитов 5 и 6 (рис. 6). master bugfix Рис. 6 168
Управление версиями Вообще говоря, дальше две ветки могут снова развиваться самосто- ятельно. Но чаще всего вторая ветка уже не нужна, и её можно уда- лить командой git branch -d bugfix Графические оболочки Некоторым пользователям неудобно работать с помощью командной строки. Поэтому было создано много графических оболочек, в которых можно выполнить те же действия, используя мышь. Самая известная из таких оболочек — GitHub Desktop, которую можно скачать на сайте desktop.github.com для разных операционных систем. В установочный пакет Git для Windows входит ещё одна гра- фическая оболочка — Git GUI. Изучить их вы можете самостоятельно. Интересные сайты gitforwindows.org — программа Git для Windows desktop.github.com — графическая оболочка для работы с Git и GitHub try.github.io — онлайн-тренажёр для знакомства с Git githowto.com/ru — интерактивный учебник по Git git-scm.com/book/ru/v2 — онлайн-учебник по Git github.com — платформа для совместной разработки проектов с использованием Git bitbucket.org — платформа для совместной разработки проектов с использованием Git learn.javascript.ru/screencast/git — видеокурс по Git 169
ОГЛАВЛЕНИЕ Предисловие.......................................................3 Глава 1. Программирование на языке Python.........................5 § 1. Проектирование программ..................................5 Этапы создания программ..................................5 Методы проектирования программ...........................7 Интерфейс и реализация...................................8 Документирование программы...............................8 Выводы...................................................9 §2 . Процедуры...............................................10 Подпрограммы: процедуры и функции.......................10 Простая процедура.......................................11 Процедуры с параметрами.................................12 Локальные и глобальные переменные.......................14 Выводы..................................................15 §3 . Рекурсия................................................17 Что такое рекурсия?.....................................18 Ханойские башни.........................................18 Пример..................................................20 Фракталы................................................22 Выводы..................................................24 §4 . Функции.................................................25 Что такое функция?......................................25 Примеры функций.........................................27 Логические функции......................................28 Рекурсивные функции.....................................28 Выводы..................................................29 170
Оглавление §5. Символьные строки............................................32 Что такое символьная строка?...............................32 Сравнение строк............................................32 Сложение и умножение.......................................33 Обращение к символам.......................................33 Перебор всех символов......................................34 Срезы......................................................35 Удаление и вставка.........................................36 Встроенные методы..........................................36 Поиск в символьных строках.................................37 Замена.....................................................38 Преобразования «строка — число»............................38 Символьные строки в функциях...............................39 Рекурсивный перебор........................................40 Выводы.....................................................41 §6. Массивы (списки).............................................46 Что такое массив?..........................................47 Массивы в языке Python.....................................47 Создание массива...........................................48 Обращение к элементу массива...............................48 Перебор элементов массива..................................50 Генераторы.................................................51 Вывод массива..............................................52 Ввод массива с клавиатуры..................................53 Заполнение массива случайными числами......................53 Выводы.....................................................54 § 7. Алгоритмы обработки массивов................................57 Сумма элементов массива....................................57 Подсчёт элементов массива, удовлетворяющих условию.........59 Особенности копирования списков в Python...................60 Выводы.....................................................61 §8. Поиск в массивах.............................................64 Линейный поиск.............................................64 Поиск максимального элемента в массиве.....................66 Максимальный элемент, удовлетворяющий условию..............68 Выводы.....................................................70 §9. Используем массивы...........................................73 Игра «Стрельба по тарелкам»................................73 Рефакторинг................................................74 Движение...................................................76 Меняем скорости............................................77 171
Оглавление Бьём тарелки.............................................78 Показываем счёт..........................................79 Выводы...................................................80 § 10. Матрицы.................................................81 Что такое матрица?.......................................81 Матрицы..................................................81 Вывод матрицы на экран...................................84 Перебор элементов матрицы................................84 Квадратные матрицы.......................................85 Выводы...................................................87 § 11. Сложность алгоритмов....................................89 Как сравнивать алгоритмы?................................90 Примеры..................................................91 Что такое асимптотическая сложность?.....................91 Выводы...................................................94 Глава 2. Программирование на языке C++...........................97 §12. Процедуры...............................................97 Простая процедура........................................97 Процедуры с параметрами..................................99 Несколько параметров....................................100 Локальные и глобальные переменные.......................101 Процедуры, изменяющие аргументы.........................104 Выводы..................................................108 §13 . Рекурсия...............................................110 Рекурсивные процедуры....................................ПО Дерево Пифагора.........................................112 Анимация................................................114 Выводы..................................................115 §14 . Функция................................................117 Функции в C++...........................................117 Примеры функций.........................................119 Логические функции......................................120 Рекурсивные функции.....................................121 Выводы..................................................122 §15 . Символьные строки......................................124 Что такое символьная строка?............................124 Сравнение строк.........................................125 Сцепление строк.........................................126 Обращение к символам....................................126 172
Оглавление Перебор всех символов....................................127 Подстрока................................................128 Удаление и вставка.......................................128 Поиск в символьных строках...............................129 Замена...................................................130 Преобразования «строка — число»..........................130 Символьные строки в функциях.............................132 Рекурсивный перебор......................................135 Выводы...................................................136 §16 . Массивы.................................................139 Массивы в C++............................................140 Обращение к элементу массива.............................141 Перебор элементов массива................................142 Вывод массива............................................143 Ввод массива с клавиатуры................................143 Заполнение массива случайными числами....................143 Алгоритмы обработки массивов.............................144 Выводы...................................................144 §17 . Используем массивы.....................................147 Игра «Стрельба по тарелкам»..............................147 Движение.................................................149 Бьём тарелки.............................................149 Показываем счёт..........................................151 Выводы...................................................152 § 18. Матрицы................................................153 Что такое матрица?.......................................153 Размещение матрицы в памяти..............................154 Заполнение матрицы.......................................154 Вывод матрицы на экран...................................155 Обработка матриц.........................................155 Выводы...................................................156 Приложение Управление версиями...............................................159 Системы управления версиями..............................159 Зачем это нужно?.........................................159 Какие бывают системы управления версиями?................160 Git......................................................161 Основные приёмы работы с Git.............................161 Начало работы............................................161 Операции с файлами.......................................163 173
Оглавление Не все файлы нужны..........................................164 Восстановление версии.......................................164 Работа с удалённым архивом..................................165 Ветки.......................................................167 Графические оболочки........................................169 174
Учебное издание Поляков Константин Юрьевич ПРОГРАММИРОВАНИЕ Python. С 4-4- Часть 2 Учебное пособие Ведущий редактор О. А. Полежаева Концепция внешнего оформления В. А. Андрианов Художественный редактор Н. А. Новак Технический редактор Е. В, Денюкова Корректор Е. Н, Клишина Компьютерная верстка: В. А. Носенко (124-)
Подписано в печать 06.09.2018 Формат 84x108/16. Усл. печ. л. 18,48 Тираж 3 000 экз. Заказ № м7021. ООО «БИНОМ. Лаборатория знаний» 127473, Москва, ул. Краснопролетарская, д. 16, стр. 3, тел. (495)181-53-44, e-mail: binom@Lbz.ru http://Lbz.ru, http://metodist.Lbz.ru Отпечатано в филиале «Смоленский полиграфический комбинат» ОАО «Издательство «Высшая школа». 214020, г. Смоленск, ул. Смольянинова, 1 Тел.: +7 (4812) 31-11-96. Факс: +7 (4812) 31-31-70 E-mail: spk@smolpk.ru http://www.smolpk.ru