Здравствуйте прошла курсы на тему Алгоритмы С++. Но не пришел сертификат и не доступен.Где и как можно его скаачат? |
Абстрактные типы данных
Примеры клиентов, использующих ATД стека
В последующих главах мы увидим огромное количество применений стеков. А сейчас в качестве вводного примера рассмотрим применение стеков для вычисления арифметических выражений. Например, предположим, что требуется найти значение простого арифметического выражения с операциями умножения и сложения целых чисел наподобие
5 * ( ( ( 9 + 8 ) * ( 4 * 6 ) ) + 7 )
При вычислении необходимо сохранять промежуточные результаты: например, если сначала вычисляется 9 + 8, придется сохранить результат 17 на время вычисления 4*6. Стек магазинного типа представляет собой идеальный механизм для сохранения промежуточных результатов в таких вычислениях.
Начнем с рассмотрения более простой задачи, где выражение, которое необходимо вычислить, имеет другую форму: знак операции стоит после двух своих аргументов, а не между ними. Как будет показано ниже, любое арифметическое выражение может быть представлено в такой форме, которая называется постфиксной, в отличие от инфиксной - обычной формы записи арифметических выражений. Вот постфиксное представление выражения из предыдущего абзаца:
5 9 8 + 4 6 * * 7 + *
Форма записи, обратная постфиксной, называется префиксной или польской записью (так как ее придумал польский логик Лукашевич).
При инфиксной записи чтобы отличить, например, выражение
5 * ( ( ( 9 + 8 ) * ( 4 * 6 ) ) + 7 )
от выражения
( ( 5 * 9 ) + 8 ) * ( ( 4 * 6 ) + 7 )
требуются скобки; но в постфиксной (или префиксной) записи скобки не нужны. Чтобы понять, почему это так, можно рассмотреть следующий процесс преобразования постфиксного выражения в инфиксное: все группы из двух операндов со следующим за ними знаком операции заменяются их инфиксными эквивалентами и заключаются в круглые скобки - они означают, что этот результат может рассматриваться как операнд. То есть, группы a b * и a b + заменяются соответственно на группы (a * b) и (a + b). Затем то же самое преобразование выполняется с полученным выражением, и весь процесс продолжается до тех пор, пока не будут обработаны все операции. Вот шаги преобразования для нашего случая:
5 9 8 + 4 6 * * 7 + *
5 ( 9 + 8 ) ( 4 * 6 ) * 7 + *
5 ( ( 9 + 8 ) * ( 4 * 6 ) ) 7 + *
5 ( ( ( 9 + 8 ) * ( 4 * 6 ) ) + 7 ) *
( 5 * ( ( ( 9 + 8 ) * ( 4 * 6 ) ) + 7 ) )
Таким способом в постфиксном выражении можно определить все операнды, связанные с любой операцией, поэтому необходимость в применении скобок отпадает.
А с помощью стека можно выполнить эти операции и вычислить значение любого постфиксного выражения (см. рис. 4.2 рис. 4.2). Перемещаясь слева направо, мы интерпретируем каждый операнд как команду "занести операнд в стек", а каждый знак операции - как команды "извлечь из стека два операнда, выполнить операцию и занести результат в стек". Программа 4.5 является реализацией этого процесса на языке C++. Обратите внимание, что, поскольку АТД стека создан в виде шаблона, один и тот же код пригоден и для создания стека целых чисел в этой программе, и стека символов в программе 4.6.
Постфиксная запись и стек магазинного типа обеспечивают естественный способ организации ряда вычислительных процедур. В некоторых калькуляторах и языках программирования метод вычислений явно базируется на постфиксной записи и стековых операциях - при выполнении любой операции ее аргументы извлекаются из стека, а результат возвращается в стек.
Примером такого языка является язык PostScript, с помощью которого напечатана данная книга. Это завершенный язык программирования, в котором программы пишутся в постфиксном виде и интерпретируются с помощью внутреннего стека, в точности как в программе 4.5. Мы не можем осветить здесь все аспекты этого языка (см. раздел ссылок), но он достаточно прост, чтобы мы рассмотрели некоторые реальные программы и оценили полезность постфиксной записи и абстракции стека магазинного типа. Например, строка 5 9 8 add 4 6 mul mul 7 add mul является PostScript-программой! Программа на языке PostScript состоит из операций (таких, как add и mul) и операндов (например, целые числа). Программа на этом языке интерпретируется так же, как в программе 4.5 - слева направо. Если встречается операнд, он заносится в стек; если встречается знак операции, из стека извлекаются операнды для этой операции (если они нужны), а затем результат (если он есть) заносится в стек.
Эта последовательность операций демонстрирует использование стека для вычисления постфиксного выражения 5 9 8 + 4 6 * * 7 + *. Выражение обрабатывается слева направо и если встречается число, оно заносится в стек; если же встречается знак операции, то эта операция выполняется с двумя верхними числами стека, и результат опять заносится в стек.
Программа 4.5. Вычисление постфиксного выражения
Эта программа-клиент для стека магазинного типа считывает любое постфиксное выражение с операциями умножения и сложения целых чисел, затем вычисляет это выражение и выводит полученный результат. Промежуточные результаты она хранит в стеке целых чисел; при этом предполагается, что интерфейс из программы 4.4 реализован в файле STACK.cxx как шаблон класса.
Когда встречаются операнды, они заносятся в стек; когда встречаются знаки операций, из стека извлекаются два верхних элемента, с ними выполняется данная операция, и результат снова заносится в стек. Порядок выполнения двух операций pop() для сложений и умножений в языке C++ не определен, а код для некоммутативных операций, таких как вычитание или деление, был бы более сложным.
В программе неявно предполагается, что целые числа и знаки операций ограничены какими-нибудь другими символами (скажем, пробелами), но программа не проверяет корректность входных данных. Последний оператор if и цикл while выполняют вычисление наподобие функции atoi языка C++, которая преобразует строки в коде ASCII в целые числа, пригодные для вычислений. Когда встречается новая цифра, накопленный результат умножается на 10, и к нему прибавляется эта цифра.
#include <iostream.h> #include <string.h> #include "STACK.cxx" int main(int argc, char *argv[]) { char *a = argv[1]; int N = strlen(a); STACK<int> save(N); for (int i = 0; i < N; i++) { if (a[i] == '+') save.push(save.pop() + save.pop()); if (a[i] == '*') save.push(save.pop() * save.pop()); if ((a[i] >= '0') && (a[i] <= '9')) save.push(0); while ((a[i] >= '0') && (a[i] <= '9')) save.push(10*save.pop() + (a[i++]-'0')); } cout << save.pop() << endl; }
Таким образом, на рис. 4.2 полностью описан процесс выполнения этой программы: после выполнения программы в стеке остается число 2 07 5.
В языке PostScript имеется несколько примитивных функций, которые служат инструкциями для абстрактного графопостроителя; а кроме них, можно определять и собственные функции. Эти функции вызываются с аргументами, расположенными в стеке, таким же способом, как и любые другие функции. Например, следующий код на языке PostScript 0 0 moveto 144 hill 0 72 moveto 72 hill stroke соответствует последовательности действий "вызвать функцию moveto с аргументами 0 и 0, затем вызвать функцию hill с аргументом 144" и т.д. Некоторые операции относятся непосредственно к самому стеку.
Например, операция dup дублирует элемент в верхушке стека; поэтому, например, код 144 dup 0 rlineto 60 rotate dup 0 rlineto означает следующую последовательность действий: вызвать функцию rlineto с аргументами 144 и 0, затем вызвать функцию rotate с аргументом 60, затем вызвать функцию rlineto с аргументами 144 и 0 и т.д. В PostScript-программе, показанной на рис. 4.3, определяется и используется функция hill. Функции в языке PostScript подобны макросам: строка /hill { A } def делает имя hill эквивалентным последовательности операций внутри фигурных скобок. На рис. 4.3 показан пример PostScript-программы, в которой определяется функция и вычерчивается простая диаграмма.
В верхней части рисунка приведена диаграмма, а в нижней - PostScript-программа, формирующая эту диаграмму. Программа является постфиксным выражением, в котором используются встроенные функции moveto, rlineto, rotate, stroke и dup, а также определяемая пользователем функция hill (см. текст). Графические команды являются инструкциями графопостроителю: команда moveto устанавливает перо в заданную позицию страницы (координаты даются в пунктах, равных 1/72 дюйма); команда rlineto перемещает перо в новую позицию, координаты которой задаются относительно текущей позиции (тем самым к пройденному пути добавляется очередной участок); команда rotate изменяет направление движения пера (поворачивает влево на заданное число градусов); а команда stroke вычерчивает пройденный путь.
В данном контексте наш интерес к языку PostScript объясняется тем, что этот широко используемый язык программирования основан на абстракции стека магазинного типа. Вообще-то в аппаратных средствах многих компьютеров реализованы основные стековые операции, поскольку они являются естественным воплощением механизма вызова функций: при входе в процедуру текущее состояние программной среды сохраняется - заносится в стек; при выходе из процедуры состояние программной среды восстанавливается - извлекается из стека. Как будет показано в "Рекурсия и деревья" , эта связь между магазинными стеками и программами, которые организованы в виде функций, обращающихся к другим функциям, является основной парадигмой вычислительного процесса.
Данная последовательность демонстрирует использование стека для преобразования инфиксного выражения (5*(((9+8)*(4*6))+7)) в постфиксную форму 5 9 8 + 4 6 * * 7 + *. Выражение обрабатывается слева направо: если встречается число, оно записывается в выходной поток; если встречается левая скобка, она игнорируется; если встречается знак операции, он заносится в стек; и если встречается правая скобка, то в выходной поток выталкивается знак операции, находящийся на верхушке стека.
Возвращаясь к первоначальной задаче, отметим, что, как видно из рис. 4.4, магазинный стек можно также использовать для преобразования инфиксного арифметического выражения с круглыми скобками в постфиксную форму. При выполнении этого преобразования знаки операций заносятся в стек, а сами операнды просто передаются в выходной поток программы. Правая скобка показывает, что два последних числа на выходе программы являются аргументами операции, знак которой занесен в стек последним. Поэтому этот знак операции извлекается из стека и также передается в выходной поток программы.
Программа 4.6 представляет собой реализацию этого процесса. Обратите внимание, что аргументы в постфиксном выражении расположены в том же самом порядке, что и в инфиксном выражении. Любопытно, что левые скобки в инфиксном выражении не нужны. Однако они необходимы, если существуют операции, имеющие разное количество операндов (см. упражнение 4.14).
Программа 4.6. Преобразование из инфиксной формы в постфиксную
Эта программа является еще одним примером программы-клиента для стека магазинного типа. В данном случае стек содержит символы. Для преобразования (A+B) в постфиксную форму A B + левая скобка игнорируется, символ A записывается в выходной поток, знак + запоминается в стеке, символ B записывается в выходной поток, а затем при обнаружении правой скобки знак + извлекается из стека и записывается в выходной поток.
#include <iostream.h> #include <string.h> #include "STACK.cxx" int main(int argc, char *argv[]) { char *a = argv[1]; int N = strlen(a); STACK<char> ops(N); for (int i = 0; i < N; i++) { if (a[i] == ')') cout << ops.pop() << " "; if ((a[i] == '+') || (a[i] == '*')) ops.push(a[i]); if ((a[i] >= '0') && (a[i] <= '9')) cout << a[i] << " "; } cout << endl; }
Помимо того, что алгоритм, разработанный в данном разделе для вычисления инфиксных выражений, предоставляет два разных примера использования абстракции стека, он и сам по себе является упражнением по абстракциям. Во-первых, входные данные преобразуются в промежуточное представление (постфиксное выражение). Во-вторых, для интерпретации и вычисления этого выражения имитируется работа абстрактной машины, функционирующей на основе стека. В целях эффективности и мобильности эта схема применяется во многих современных компиляторах: задача компиляции программы на C++ для конкретного компьютера разбивается на две задачи с промежуточным представлением между ними. Поэтому задача трансляции программы отделяется от задачи выполнения этой программы, точно так же, как это делалось в данном разделе. В разделе 5.7 "Рекурсия и деревья" будет показано похожее, но другое промежуточное представление.
Это приложение также демонстрирует достоинства абстрактных типов данных и шаблонов C++ . Здесь не просто используются два разных стека: один из них содержит объекты типа char (знаки операций), а другой - объекты типа int (операнды). С помощью АТД в виде шаблона класса, определенного в программе 4.4, можно даже объединить две рассмотренных клиентских программы в одну (см. упражнение 4.19). Несмотря на привлекательность этого решения, учтите, что оно может и не быть оптимальным: ведь различные реализации могут отличаться своей производительностью, так что не стоит априори считать, что одна и та же реализация будет хорошо работать в обоих случаях. Вообще-то наш главный интерес - реализации и их производительность, и сейчас мы приступим к рассмотрению этих вопросов применительно к стеку магазинного типа.
Упражнения
-
4.12. Преобразуйте в постфиксное выражение
( 5 * ( ( 9 * 8 ) + ( 7 * ( 4 + 6 ) ) ) ) .
-
4.13. Таким же способом, как на
рис.
4.2, покажите содержимое стека при вычислении программой 4.5 выражения
59*8746+*213*+*+*.
- 4.14. Расширьте программы 4.5 и 4.6 таким образом, чтобы они обрабатывали операции - (вычитание) и / (деление).
- 4.15. Расширьте решение упражнения 4.14 таким образом, чтобы оно включало унарные операции - (смена знака) и $ (извлечение квадратного корня). Кроме того, измените механизм абстрактного стека в программе 4.5 так, чтобы можно было использовать числа с плавающей точкой. Например, имея в качестве исходного выражение
(-(-1) + $((-1) * (-1)-(4 * (-1)))) / 2 программа должна выдать число 1.618034.
- 4.16. Напишите на языке PostScript программу которая вычерчивает следующую фигуру:
- 4.17. Методом индукции докажите, что программа 4.5 правильно вычисляет любое постфиксное выражение.
- 4.18. Напишите программу, которая преобразует постфиксное выражение в инфиксное, используя стек магазинного типа.
- 4.19. Объедините программы 4.5 и 4.6 в один модуль, в котором будут использоваться два разных АТД: стек целых чисел и стек (знаков) операций.
-
4.20. Напишите компилятор и интерпретатор для языка программирования, в котором каждая программа состоит из одного арифметического выражения. Выражению может предшествовать ряд операторов присваивания с арифметическими выражениями, состоящими из целых чисел и переменных, обозначаемых одиночными строчными буквами. Например, получив входные данные
(x = 1)
(y = (x + 1))
(((x + y) * 3) + (4 * x))
программа должна вывести число 13.