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

информатика_2 / Программирование_С

.pdf
Скачиваний:
87
Добавлен:
16.03.2016
Размер:
2.31 Mб
Скачать

if (// числитель > знаменателя)

{

а = // числитель; b = //знаменатель;

}

else

{

b = // числитель; a = // знаменатель;

}; do

{

q = int(a / b); r = a – q*b; a = b;

b = r; }while (r > 0);

}

Используя функцию reduce, мы можем написать другую функцию equal, которая определяет, равны или нет между собой числа r1 и r2. Если они равны (то есть равны одновременно и числители и знаменатели), то функция equal возвращает значение 1; в противном случае возвращается ноль. Ввиду простоты кода детальное обсуждение не приводится.

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

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

Напомним, что (a/b) (c/d) = (a*c) / (b*d), и именно этой формулой удобнее пользоваться. Однако, поскольку числа (a*c) и (b*d) могут быть большими, перед завершением программы умножения мы сократим результат до несократимой дроби

3.2.2.Задачи на стеках:

3.2.2.1.Постфиксная, префиксная и инфиксная записи

3.2.2.1.1.Базовые определения и примеры Рассмотрим сумму чисел А и В. Будем говорить о применении операции «+» к

операндам А и В и будем записывать сумму как А+В. Такое традиционное представление называется инфиксной записью. Для представления суммы чисел

211

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

+А В префиксная запись А В + постфиксная запись

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

Рассмотрим еще несколько примеров. Вычисление выражения А+В*С, записанное в стандартной инфиксной записи, требует знания того, какая из двух операций выполняется первой. В случае + или * известно, что умножение выполнится раньше сложения (при отсутствии скобок). Следовательно, выражение А+В* С интерпретируется как А+(В*С). Мы говорим, что умножение имеет более высокий приоритет, чем сложение. Предположим, что мы хотим записать выражение А+В*С в постфиксной записи. Учитывая правила приоритетности операций, мы сначала преобразуем ту часть выражения, которая

вычисляется

первой, — умножение. Выполняя преобразования поэтапно,

получим

 

А+(В*С)

скобки для выделения

А+(ВC*)

преобразование операции умножения

А(ВC*)+

преобразование операции сложения

АВC*+

постфиксная форма

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

(А+В)*С

инфиксная форма

(АВ+)*С

преобразование операции сложения

(АВ+)C*

преобразование операции умножения

АВ+С*

постфиксная форма

В приведенном примере сложение преобразуется перед умножением из-за наличия скобок. В преобразовании выражения (А+В)*С к (АВ+)*С, А и В являются операндами, а + является оператором. В преобразовании выражения (АВ+)*С к (АВ+)С* (АВ+) и С являются операндами, а * является операцией. Если известна приоритетность выполнения операций, то правила преобразования инфиксного представления в постфиксное просты.

Примеры преобразования инфиксного представления в постфиксное.

Инфиксное

Постфиксное

представление

представление

А+В

АВ+

А+В–С

АВ+С -

(А+В)*(С–D)

АВ+СD–*

А В*С–D+E/F/(G+Н)

АВ C*D–ЕF/GН+/+

212

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

Инфиксное

Постфиксное

представление

представление

А+В

+АВ

А+В–С

–+АВС

(А+В)*(С–D)

*+АВ–СD

А В*С–D+E/F/(G+Н)

+–*АВCD/ЕF+GН

Отметим, что префиксная форма для сложного выражения не является зеркальным отображением постфиксной формы, что видно на втором примере (А+В–С). В дальнейшем мы будем в основном рассматривать постфиксные преобразования.

Очевидное отличие постфиксной формы от всех остальных заключается в том, что она не содержит скобок. Рассмотрим два выражения: А+(В*С) и (А+В)*С. Если в первом из двух выражений скобки не являются обязательными [согласно преобразованию А+В*С=А+(В*С)], то во втором они необходимы во избежание путаницы с первым случаем. Постфиксные формы для этих выражений есть

Инфиксная

Постфиксная

форма

форма

А+(В*С)

АВC*+

(А+В)*С

АВ+C*

В обоих преобразованных выражениях скобки отсутствуют. Внимательное рассмотрение этих преобразований говорит о том, что порядок операций в постфиксных выражениях определяет действительный порядок операций при вычислении выражения, делая скобки ненужными. Можнот возразить, что постфиксная форма, возможно, и выглядит проще, однако ее трудно вычислять. Например, если в рассмотренных выше примерах А=3, В=4 и С=5, то откуда мы знаем, что 345*+ равно 23, а 34+5* равно 35?

3.2.2.1.2. Вычисление выражения, записанного в постфиксной форме Ответ на этот вопрос дает алгоритм вычисления выражения, записанного в

постфиксной форме. Каждая операция в постфиксной строке ссылается к предшествующим двум операндам этой строки. (Разумеется, один из этих двух операндов может быть результатом выполнения предыдущей операции.) Предположим, что всякий раз, когда мы прочитываем операнд, мы записываем его в стек. Когда мы достигаем операции, то относящиеся к ней операнды будут двумя верхними элементами стека. Затем можно извлечь эти два операнда и осуществить над ними указанную операцию, а затем записать результат в стек. Тем самым он станет доступен в качестве операнда применительно к следующей операции. Приведенный ниже алгоритм вычисляет выражение в постфиксной записи именно таким способом.

//инициализировать стек s, обнулив его

//выполнять просмотр входной строки, считывая за один раз по одному

213

//элементу в переменную symb

{

while ( // во входной строке еще имеются непросмотренные символы)

{

// symb = следующий считанный символ if (symb есть операнд)

push(s,symb);

еlsе

{

seсорег = рор(s);

ореr1=рор(s);

// value=результат применения symb к орег1 и sесореr push(s, value);

}

}

result=рор(s);

}

Пример. Предположим, что нам необходимо вычислить следующее выражение, представленное в постфиксной нотации:

623+ –382/+ *2 3+

Покажем содержимое стека операндов s и переменных symb, ореr 1, sесорег и value после каждого очередного шага цикла. Вершина стека расположена справа.

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

Symb

oper1

oper2

value

s

6

 

 

 

6

2

 

 

 

6, 2

3

 

 

 

6, 2, 3

+

2

3

5

6, 5

-

6

5

1

1

3

6

5

1

1, 3

8

6

5

1

1, 3, 8

2

6

5

1

1, 3, 8, 2

/

8

2

4

1, 3, 4

+

3

4

7

1, 7

*

1

7

7

7

2

1

7

7

7, 2

 

7

2

49

49

3

7

2

49

49, 3

+

49

3

52

52

214

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

3.2.2.1.4. Преобразование выражения из инфиксной записи в постфиксную Мы уже упоминали о том, что части выражения, заключенные в скобки на

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

Рассмотрим два выражения в инфиксной форме: А+В*С и (А+В)*С и соответствующие им постфиксные формы АВС*+ и АВ+С*. В каждом случае порядок следования операндов в этих формах совпадает с порядком следования операндов в исходных выражениях. При просмотре первого выражения (А+В*С) первый операнд А может быть сразу же помещен в постфиксное выражение. Очевидно, что символ « + » не может быть помещен в это выражение до тех пор, пока туда не будет помещен второй, еще не просмотренный операнд. Следовательно, он (т. е. символ « + ») должен быть сохранен, а впоследствии извлечен и помещен в соответствующую позицию. После просмотра операнда В этот символ записывается вслед за операндом А. К этому моменту просмотренными оказываются уже два операнда. Что мешает извлечь и разместить символ «+»? Разумеется, ответ на этот вопрос заключается в том, что за символом «+» следует символ «*», имеющий более высокий приоритет. Во втором выражении наличие скобок обусловливает выполнение операции «+» в первую очередь. Вспомним, что в отличие от инфиксной формы в постфиксной записи операция, появляющаяся первой в строке, выполняется первой.

Так как при преобразовании инфиксной формы в постфиксную правила приоритета играют существенную роль, для их учета введем функцию prcd(operl, secoper), где operl и secoper — символы, обозначающие операции. Эта функция возвращает значение TRUE, если operl имеет более высокий приоритет, чем secoper, и operl располагается слева от secoper в бесскобочном выражении, представленном в инфиксной форме. В противном случае функция prcd(operl, secoper) возвращает значение FALSE. Например, значения функций prcd(«*», « + ») и prcd(« + », «+»)—«истина», a prcd(« + », «*») — «ложь». Рассмотрим теперь макет алгоритма для преобразования строки, представленной в инфиксной форме и не содержащей скобок, в постфиксную строку. Поскольку мы считаем, что во входной строке скобки отсутствуют, единственным признаком порядка выполнения операций является их приоритет.

{

//установить в постфиксную строку « »

//обнулить стек с именем opstk

while ( // на входе еще имеются символы)

{

215

read symb;

if (symb есть операнд)

//добавить символ к постфиксной строке else //символ есть операция

{

while (empty (stack) )==FALSE) and(prcd (stacktop (opstk) ,symb) ==

TRUE)

smbtp = pop (opstk);

//smbtp имеет приоритет больший, чем symb, поэтому она //может быть добавлена к постфиксной строке

//добавить smbtp к постфиксной строке

//в этой точке либо opstk пуст, либо symb имеет приоритет над //stacktop (opstk). Мы не можем поместить symb в постфиксную //строку до тех пор, пока не считаем следующую операцию, //которая может иметь более высокий приоритет.

// Следовательно, мы должны сохранить symb push(opstk.symb);

}

}

//к этому моменту строка оказывается просмотренной целиком. Мы

//должны поместить оставшиеся в стеке операции в постфиксную строку

while (empty (opstk)= FALSE) do

{

smbtp=pop(opstk);

// добавить smbtp к постфиксной строке

}

}

Проверьте алгоритм со строками «A*B + C*D» и «А+В* *C D E» (где prcd(« », « »)=FALSE) и убедитесь в том, что он выполняется правильно. Отметим, что в любой момент операция в стеке имеет более низкий приоритет, чем все операции перед ним. Это обусловлено тем, что изначально пустой стек удовлетворяет данному условию и операция помещается в стек только в том случае, если находящаяся в данный момент в вершине стека операция имеет более низкий приоритет, чем считываемая.

Какие изменения должны быть внесены в алгоритм для обеспечения возможности работы со скобками? Ответ на этот вопрос прост. После считывания открывающей скобки она записывается в стек. Это может быть выполнено путем установки правила prcd(op, «(»)=FALSE для любого символа операции, отличного от символа правой (закрывающей) скобки. Мы также определим prcd(«(»,op) =FALSE для того, чтобы символ операции, появляющийся после левой скобки, записывался в стек.

После считывания закрывающей скобки все операции вплоть до первой, открывающей скобки должны быть прочитаны из стека и помещены в постфиксную строку. Это можно сделать путем установки prcd (ор.,«)») =TRUE

216

для всех операций ор, отличных от левой скобки. После считывания этих операций из стека и закрытия открывающей скобки необходимо выполнить следующую операцию. Открывающая скобка должна быть удалена из стека и отброшена вместе с закрывающей скобкой. Обе скобки не помещаются затем ни в постфиксную строку, ни в стек. Установим функцию prcd («(»,«)») равной FALSE. Это гарантирует нам то, что при достижении открывающей скобки вложенный цикл while будет пропущен, а открывающая скобка не будет помещена в постфиксную строку.

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

prcd («(»,op)= FALSE для любой операции ор prcd(op,«(»)=FALSE для любой операции ор, отличной от «)» prcd (op,«)»)= TRUE для любой операции ор, отличной от «(»

prcd («)»,ор)= неопределенно для любой операции ор, (попытка сравнения двух указанных операций означает ошибку)

Пример: А+В*С

Строка

Symb

Постфиксн

optsk

ая строка

1

А

А

 

2

+

А

+

3

В

АВ

+

4

*

АВ

+*

5

С

ABC

+*

6

 

ABC*

+

7

 

АВС*

+

Приводится содержимое symb, постфиксной строки и opstk после просмотра каждого символа. Вершина opstk находится справа.

Строки 1, 3 и 5 соответствуют просмотру операнда таким образом, что символ (symb) немедленно помещается в постфиксную строку. В строке 2 была обнаружена операция, а стек оказался пустым, поэтому операция помещается в стек. В строке 4 приоритет нового символа (*) больше, чем приоритет символа, расположенного в вершине стека (+), поэтому новый символ помещается в стек. На 6-м и 7-м шагах входная строка пуста, поэтому из стека считываются элементы, которые затем помещаются в постфиксную строку.

Теперь можно написать программу, считывающую строку в инфиксной форме

ивычисляющую ее числовое значение.

3.2.2.1.5.Анализ скобочного выражения на корректность

Интересным примером использования стека может служить программа анализа скобочного выражения на корректность. Программа проверяет соответствие строки символов обычным математическим правилам записи скобочных выражений; используются скобки трех видов: ( ), [ ] и { }. Очевидно, запись корректна, если:

количества открывающих и закрывающих скобок каждого типа совпадают;

217

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

Это дает нам принцип работы программы:

читаем строку посимвольно; помещаем все открывающие скобки в стек; при обнаружении закрывающей скобки достаем из стека лежащую сверху

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

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

{

//char * откр_скобки = {'(', '[', ‘{’};

//char * закр_скобки = {')', ']', '}'}; result = true;

//ввод строки

while (// есть символы и result == true)

{

if (// символ == откр_скобка) // положить ее в стек;

if (// символ == закр_скобка) if (// стек_пуст)

result =false; else

{

//взять из стека последнюю скобку

//она должна быть открывающей if ( // скобки не парные)

result =false;

}

}

}

Здесь переменная результата result изначально устанавливается в true, г при первом же обнаружении некорректности — в false. Некорректность возникает либо при отсутствии в стеке какой-либо закрывающей скобки для обнаруженной открывающей, либо при несовпадении типов скобок

Напишите программу, считывающую строку со скобочным выражением и проверяющую его на корректность.

3.2.2.1.6. Анализ строки на наличие палиндрома Напишите программу, определяющую, является ли введенная строка символов

палиндромом (то есть читается ли она одинаково слева направо и справа налево). Используйте для решения структуру стека.

218

3.2.3. Задачи на списках

3.2.3.1 .Задача Джозефуса

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

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

3.2.3.2. Элементы заголовка

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

pt_buf = pt_buf –> next, где pt_buf изначально равен указателю на начало списка pt_lst. Список будет пройден целиком, если выполнено равенство pt_buf == pt_lst.

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

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

3.2.3.1.1. Сложение длинных положительных целых чисел при помощи циклических списков

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

219

длину. Предположим, что мы работаем с положительными целыми числами произвольной длины и нам необходима функция, которая вычисляет сумму двух таких чисел. Для сложения двух чисел их цифры просматриваются справа налево и соответствующие пары складываются друг с другом, возможно, с переносом, полученным при сложении предыдущей пары. Это предполагает хранение длинных целых чисел в списке с размещением их цифр справа налево так, что первый элемент списка содержит последнюю значащую цифру (крайнюю правую), а последний элемент содержит первую значащую цифру (крайнюю левую). Однако для экономии пространства мы будем хранить по пять цифр в каждом элементе. Мы можем объявить набор элементов следующим образом

(рис. 3.8):

Рис. 3.8. Длинное целое число, представленное в виде циклического списка

Воспользуемся циклическими списками с заголовками. Заголовок списка отличается от остальных элементов наличием в поле данных структуры значения, например, –1. Целое число 459763497210698463 будет храниться в списке так, как это показано на.

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

то перенос Carry есть INТ(Summ/100000).

Пять младших значащих цифр Summ есть тогда соответственно Summ – 100000*Carry. По достижению конца списка перенос передается к оставшимся цифрам в другом списке.

3.2.3.1.2. Сложение длинных целых чисел Используя двунаправленные связанные списки, можно складывать длинные

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

220

Соседние файлы в папке информатика_2