К настоящему моменту у нас должно быть сформировано первое понимание того, как устроены структуры данных во время выполнения программы: это совокупность объектов, связанных ссылками. Пришло время посмотреть на управляющие структуры, определяющие порядок, в котором будут применяться операторы к этим объектам.
Возможно, вы знакомы с известной пародией на то, как учат инженеров.
Как способ кипячения воды эта техника не особенно эффективна, но дан прекрасный пример комбинирования некоторых из фундаментальных управляющих структур.
Полезно вспомнить обсуждение контрактов в предыдущих лекциях. Здесь мы отмечаем, что переход к случаю 1 из случая 2 возможен — что явно подчеркнуто при описании случая 2, — поскольку первый шаг случая 2 гарантирует выполнение предусловия случая 1 (вода холодная). Предусловия и другие контракты будут играть большую роль в получении правильных структур управления.
В этом примере в непринужденной манере устанавливается подходящий контекст изучения управляющих структур: они задают приемы решения задачи. Программа дает решение задачи; каждый вид управляющей структуры отражает частичную стратегию поиска решения задачи.
Задача всегда будет ставиться так: начиная с известного свойства K, требуется достичь некоторой цели G. В примере: свойство K — чайник с водой, цель G — вскипятить воду. Стратегии, обеспечиваемые управляющими структурами, состоят в редуцировании (приведении) задачи к нескольким более простым задачам. Вот пример.
Техника рекурсии, достаточно важная, чтобы посвятить ей отдельную лекцию, может быть применена, если можно сконструировать решение задачи из одного или нескольких решений той же самой задачи, но примененной к данным меньшего размера.
Так как программирование предназначено для решения задач, полезно изучать эти и другие структуры с этих позиций.
Для каждой из управляющих структур будем последовательно анализировать:
Управляющие структуры определяют порядок операций в процессах, выполняемых компьютером. Такие процессы называются алгоритмами. Это одна из фундаментальных концепций информатики. Вам уже наверняка приходилось встречаться с этим термином, поскольку даже популярная пресса обсуждает такие темы, как "криптографические алгоритмы". Для изучения управляющих структур нам понадобится более точное определение.
В общих терминах алгоритм — это описание процесса вычислений, достаточное, чтобы его могла выполнить машина (для нас в роли машины выступает компьютер), осуществление процесса на любых входных данных без дальнейших инструкций. Нам известно много различных алгоритмов. Сложите два числа:
687 + 42 = 729
Для решения этой задачи вы примените правила, скорее всего, не думая явно об этих правилах.
Сложение двух целых чисел
Процесс состоит из нескольких шагов, каждый шаг выполняется над определенной позицией чисел. Позиция для первого шага задается самой правой цифрой обоих чисел. Для каждого следующего шага его позиция - непосредственно слева от предыдущей позиции. У каждого шага есть перенос. Начальный перенос равен нулю. На каждом шаге пусть m - это цифра первого числа, стоящая в позиции данного шага, а n - это соответствующая цифра второго числа в той же позиции. Если какое-либо число не имеет цифр в данной позиции, то значение (m или n) равно нулю.
На каждом шаге процесс выполняет следующее:
Процесс заканчивается, когда в позиции шага нет цифр у обоих чисел и перенос равен нулю.
Этап 3 основан на предположении: "s не может быть больше 19". Без него процесс не имел бы смысла, так как мы хотим писать в результат цифру за цифрой. Чтобы гарантировать корректность алгоритма, нам нужно доказать, что это свойство выполняется на каждом шаге процесса. Действительно, m и n являются цифрами, не могут быть больше 9, а их сумма не больше 18. Так как перенос не больше 1, s не больше 19. Это пример инвариантного свойства, концепции, которую будем изучать детально при рассмотрении циклов.
Будучи менее точной, чем принятый стандарт публикации алгоритмов, предыдущая спецификация все же более пунктуальна, чем большинство предписаний, которые нам приходится использовать с различной степенью успеха в обычной жизни. Вот пример инструкции на трех языках на пакете куриного супа с овощами.
На немецком и французском языках инструкция говорит: "Залейте овощи одним литром холодной воды, добавьте две столовые ложки масла и соль". Рецепт не точен, поскольку "столовые ложки" бывают разными, и сколько нужно соли, тоже не указано. Что более удивительно, так это отсутствие ключевой инструкции: если вы хотите получить результат, то без огня не обойтись. Только итальянская версия упоминает эту деталь — "Готовьте в соответствии с указанным временем", что придает смысл рисунку.
Такие инструкции ориентированы на человека, его интерпретацию; отсутствие явно заданных шагов не представляет проблемы, поскольку большинству пользователей ясно, что еду не приготовишь без кипячения и что картинка задает время приготовления супа (даже я догадался). Но то, что годится для кухонных рецептов, недостаточно для алгоритмов. Вы должны специфицировать каждую операцию, каждую деталь процесса, и их нужно специфицировать в форме, не оставляющей двусмысленностей, никакой свободы для предположений.
Для алгоритмов в противоположность неформальным рецептам справедливы свойства, задаваемые следующим определением.
Алгоритм является спецификацией процесса, действующей на (возможно пустом) множестве данных и удовлетворяющей следующим пяти правилам.
А1. Спецификация определяет применимое множество данных.
А2. Спецификация определяет множество элементарных действий, из которых строятся все шаги процесса.
А3. Спецификация определяет возможный порядок или порядки, в которых процесс может выполнять свои действия.
А4. Спецификация элементарных действий (правило А2) и предписанный порядок (правило А3) основаны на точно определенных соглашениях, позволяя процессу выполняться автоматически, без вмешательства человека. Гарантируется, что на одном и том же множестве данных результат будет одинаковым для двух автоматов, следующих тем же соглашениям.
А5. Для любого множества данных, для которых процесс применим (по правилу А1), гарантируется завершение процесса после выполнения конечного числа шагов алгоритма.
Вышеприведенный метод сложения целых обладает требуемыми свойствами:
А1: описывает процесс, применимый к некоторым данным, и специфицирует вид этих данных: два целых числа, записанные в десятичной нотации;
А2: процесс основан на хорошо определенных базисных действиях: установить значение, равное нулю или известному числу, сложить три числа (цифры), сравнить число с 10;
A3: описание задает порядок выполнения базисных действий;
А4: правило является точным. Этой точности должно быть достаточно для любых двух людей, понимающих и применяющих алгоритм одинаковым способом. Хотя, как отмечалось, оно может быть недостаточно точным для других целей;
А5: для любых применимых данных — два числа в десятичной нотации — процесс завершится после конечного числа шагов. Это интуитивно ясно, но должно быть тщательно проверено. Мы увидим, как это делается, показав, что выражение M — step + 1 является вариантом.
В определении правила A3 упоминается "порядок или порядки" шагов. Последовательный и детерминированный алгоритм определяет единственный порядок шагов для любого возможного выполнения. Но это не единственная возможность.
В этой книге будут рассматриваться только последовательные детерминированные алгоритмы.
В свете приведенного определения алгоритма хотелось бы понять, что же отличает алгоритмы от программ? Базисные концепции одни и те же.
Иногда говорят, что разница лежит в уровне абстракции: программа предполагает выполнение на специальной машине, в то время как алгоритм задает абстрактное определение процесса вычислений независимо от любого вычислительного устройства. Это имело некоторый смысл несколько десятилетий назад, когда программы записывались в кодах конкретного компьютера. Алгоритмы тогда служили для выражения сущности программ: вычислительный процесс описывался независимо от любого компьютера. Но та точка зрения не применима сегодня.
Практическое описание алгоритмов часто опускает специфицирование некоторых деталей, таких как выбор структур данных, которые программа не может опустить, так как без этого невозможна компиляция и выполнение. Эта практика, кажется, обосновывает довод о более высоком уровне абстракции алгоритмов в сравнении с программами. Но для алгоритмов это только полезное соглашение, облегчающее их публикацию. В описаниях, предназначенных для публикации, снижаются требования к точности (условие А4), но каждый читатель понимает, что для получения алгоритма в настоящем смысле необходимо восстановить опущенные детали.
Так что мы не можем полагать, что уровень абстракции отличает алгоритмы от программ. Более важны другие два отличия — или нюанса.
Algorithms + Data Structures = Programs. На русском языке: "Алгоритмы + Структуры данных = Программы"
ОО-подход к конструированию ПО отводит центральную роль данным, более точно — типам объектов: классам. Каждый алгоритм присоединен к некоторому классу. Eiffel применяет это правило без исключений: каждый алгоритм, который вы пишете, появляется как метод (feature) некоторого класса. Этот подход оправдан по соображениям качества создаваемого ПО, которое будем анализировать в последующих лекциях. Как следствие, в этой книге мы будем изучать алгоритмы и аспекты данных в тесном взаимодействии.
Структуры управления, рассматриваемые в этой лекции, являются одним из примеров алгоритмических концепций, не связанных напрямую с конкретным видом структуры данных.