Олександр Рудик

Зворотний польський запис

1. Означення і алгоритм породження

      Звичною формою запису виразів є інфіксна, коли знак бінарної операції записують між позначеннями операндів цієї операції, наприклад, a + b. Розглянемо запис знаків операцій після позначень операндів, тобто постфіксний запис, наприклад, a b +. Такий запис має також назву зворотного польського, бо його запропонував польський логік Ян Лукасевич. Далі словосполучення: «зворотний польський запис» позначатимемо ЗПЗ. Позначення для функції традиційно записують перед аргументами. Природно такий запис назвати префіксним. При описі ЗПЗ переважно обмежуються перетворенням інфіксного запису у ЗПЗ. Далі розглянуто узагальнення — перетворення у ЗПЗ арифметичних виразів з традиційним записом функції: аргументи записано після позначення функції у дужках через кому.

Означення 1. Зворотний польський запис (ЗПЗ) — це:

      Можна дати еквівалентне означення зворотного польського запису через рекурсивно задану операцію P перетворення звичайного інфіксного запису у зворотний польський запис.
      Нехай
x — позначення сталої чи змінної;
X , Y, X1 , X2, ... , Xn — інфіксні записи;
⊚ — знак бінарної операції (наприклад, один зі знаків арифметичних дій: +, – , ∙ або /);
f — позначення функції n змінних.
      Маємо:
(P1)     P (x) = x;
(P2)     P (XY ) = P (X ) P (Y ) ⊚;
(P3)     P ( f (X1 , X2 , ... , Xn )) = P (X1 ) P (X2 ) ... P (Xn ) f .
      Наприклад, P (ab + c/de) = a bc d / + e – .
      Зворотний польський запис має чудові властивості, які перетворюють її на ідеальну проміжну ланку при трансляції коду програми.
      Обчислення виразу, записаного в зворотному польському записі, можна проводити шляхом однократного перегляду ЗПЗ.
      Зворотний польський запис виразу з арифметичними діями та піднесенням до степеня можна отримати, дотримуючись алгоритму, запропонованого Дейкстpою. Для цього запроваджують поняття стекового пріоритету символів (див. табл. 1, у якій ^ є позначенням для піднесення у степінь).

Таблиця 1
Пріоритет операцій для отримання зворотного польського запису

Пріоритет Операція
0 (
1 )
2 + або –
3 ∙ або /
4 ^

      Проглядаючи послідовність символів інфіксного запису, операнди записуємо у вихідний файл у порядку зустрічі, а знаки операцій і дужки заносимо у стек, дотримуючись таких правил:

Приклад виконання такого алгоритму подано таблицею 2.

Таблиця 2
Процес отримання зворотного польського запису
виразу (a + b) ∙ (c + d) – e

Cимвол1234567891011121314
Введення(a+b)*(c+d)-e
Стан стека((+(+(*(*(*+(*+(**--
Виведенняab+cd+*e-

      Подамо програму мовою Turbo Pascal для отримання зворотного польського запису.

{$I-}
const v=['a'..'e','g'..'z','0'..'9','.',
         'A'..'E','G'..'Z'];
      ns_=1000;                        {розмір стеку}
var   fin,                             {вхідний файл}
      fout: text;                     {вихідний файл}
        ch: char;                   {зчитаний символ}
        ns,               {кількість елементів стеку}
        is: word;         {лічильник елементів стеку}
      code: integer;      {код перетворення
                            числа аргументів функції}
s: array [0..1000] of string;        {елементи стеку
                                у зворотному порядку}
p: array [0..1000] of byte;   {пріоритет елементів s}

            {Визначення пріоритету бінарної операції}
function prior(ch: char): byte;  BEGIN
                        case ch of
                '(': prior:=0;
                ')': prior:=1;
            '+','-': prior:=2;
            '*','/': prior:=3;
                '^': prior:=4; end END;
                                               BEGIN
assign(fin, 'in.txt');    reset(fin);
assign(fout,'out.txt');  rewrite(fout);
ns:=0;
REPEAT read(fin,ch);
CASE ch of
' ':;                         {ігнорування прогалини}

','                                          : begin
            if s[ns]<>'(' then
               repeat write(fout,s[ns],' '); dec(ns)
                until s[ns]='('                  end;

                   {Запис числа або позначення змінної}
'a'..'e','g'..'z','.','0'..'9',
'A'..'E','G'..'Z':                             begin
                               write(fout,ch);
             repeat read (fin, ch);
                    if   ch in v then write(fout,ch);
              until not (ch in v);
             write (fout,' ');
             case ch of
               '+','-','*','/','^'           : begin
                  if ns=0 then                 begin
                     ns:=1;
                     s[1]:=ch;
                     p[1]:=prior(ch)             end
               else                            begin
       {виштовхування операцій з неменшим пріоритетом}
                    p[0]:=prior(ch);
                    while (ns>0) and
                          (p[ns]>=p[0]) do     begin
                      write(fout,s[ns],' ');
                      dec(ns)                    end;
                    inc(ns);
                    p[ns]:=p[0];
                    s[ns]:=ch                end end;
               ',',')'                       : begin
                  if s[ns]<>'(' then
                     repeat write(fout,s[ns],' ');
                            dec(ns)
                     until s[ns]='(';
                     if ch=')' then dec(ns)  end end
                                                 end;
'('                                          : begin
                  inc(ns);  s[ns]:=ch;  p[ns]:=0 end;

          {бінарні операції, записані між операндами}
'+','-','*','/','^'                          : begin
        if   ns=0 then     begin
             ns:=1;
             s[1]:=ch;
             p[1]:=prior(ch) end
        else                                   begin
       {виштовхування операцій з неменшим пріоритетом}
             p[0]:=prior(ch);
             while (ns>0) and (p[ns]>=p[0]) do begin
                   write(fout,s[ns],' ');
                   dec(ns)                       end;
            inc(ns);
            p[ns]:=p[0];
            s[ns]:=ch                        end end;
            {початок запису функції у форматі:
          f<кількість аргументів>_<літери або цифри>}
'f','F'                                      : begin
            s[0]:=ch;
            repeat read(fin,ch);  s[0]:=s[0]+ch;
             until ch='_';
            val(copy(s[0],2,length(s[0])-2),p[0],code);
            p[0]:=p[0]+4;
            if code<>0 then           begin
              writeln(fout);
              writeln(fout,'See ',s[0]);
              close(fout);
              halt                      end;
            repeat read(fin,ch);  s[0]:=s[0]+ch;
             until ch='(';
            delete(s[0],length(s[0]),1);
            while (ns>0) and (p[ns]>=p[0]) do  begin
                  write(fout,' ',s[ns]);
                  dec(ns)                        end;
            inc(ns);  s[ns]:=s[0];  p[ns]:=p[0];
            inc(ns);  s[ns]:='(';   p[ns]:=0     end;
')'                                          : begin
            repeat write(fout,s[ns],' ');
                   dec(ns)
             until s[ns]='(';
            dec(ns)                          end END
UNTIL seekeoln(fin);
                                 {вивільнення стеку}
for is:=ns downto 2 do write(fout,s[is],' ');
writeln(fout,s[ns]);  close(fout);  close(fin)   END.

2. Породження дерева і виконання обчислень

      З кожним зворотним польським записом Z природним чином пов'язане деяке орієнтоване дерево T(Z), в якому кожна вершина поставлена у взаємно однозначну відповідність до фраґменту зворотного польського запису таким чином:

  1. Позначенню x сталої або змінної відповідає вершина ⓧ, з якої не виходить жодної дуги (таку вершину називають листком);

  2. Бінарній операції ⊚ відповідає вершина, з якої виходить дві дуги до тих вершин Ⓧ і Ⓨ, що відповідають операндам X і Y цієї операції;

  3. Функції f відповідає вершина ⓕ, з якої виходить дуга до вершини Ⓧ, що відповідає аргументу X цієї функції.

      Інакше кажучи, відображення T, як і P, задано рекурсивно:

(T1)    T(x) = ⓧ;

(T2)    T(X Y ⊚) =;

(T3)    T(X f ) =.

      Зауваження 1. Для функції n змінних правило (T3) потрібно замінити на таке, у якому з вершинивиходить n дуг.

      Обчислення згідно зі зворотним польським записом Z має наочне тлумачення у термінах отриманого дерева T(Z): поступова заміна листків і дуг, що ведуть у ці листки з однієї вершини на листок, якому приписують результат. Отже, для обчислення згідно зі зворотним польським записом достатньо здійснити таке.

  1. Побудувати дерево згідно з правилами (T1), (T2) і (Т3).

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

      Побудова в оперативній пам'ятi ПК орієнтованого дерева, що відповідає ЗПЗ, з використанням динамічного розподілу пам'яті вимагає детального опису алгоритму, який українською мовою далі не подано. Зате подано прокоментований код програми мовою Turbo Pascal для обчислення арифметичного виразу сталих величин (присутні лише бінарні операції — арифметичні дії). Кінці дуг з однаковим кінцем поділено на ліві й праві нащадки за місцем в інфіксному записі (див. поля l, r типу b, запровадженого для опису вершин дерева). Для аналізу коду програми рекомендуємо будувати графічні ілюстрації дій щодо створення нових вершин і дуг дерева на етапі його побудови та перетворення дерева на етапі обчислення величини арифметичного виразу. Обхід листків розпочато з того, для якого шлях від кореня був весь час до лівого нащадка, поки не потрапили у листок.

      Подану далі програму можна удосконалити на випадок використання функцій багатьох змінних після визначення послідовностей символів, що позначатимуть функції 1, 2, 3 і т.і. змінних. Якщо буде достатньо лише символів, то істотних змін структура програми щодо обчислення виразу не зазнає. Інакше доведеться або використовувати умовні оператори, або вкладати оператори case один в інший. При цьому тип поля a типу запису b потрібно буде змінити, наприклад, на string. Змін має зазнати:

{Створення бiнарного дерева для виконання арифметичних дiй
                     згiдно зі зворотним польським записом}

type p=^b;  b=record       {вершина дерева)
              n: real;     {число}
              a: char;     {символ арифметичної дiї (' ' для числа)}
              l,r,         {вказiвники на операнди  (NIL для числа)}
              prev: p end; {вказiвник на попереднiй запис}
var cp,                    {попередня вершина}
    cl,                    {її лiвий  нащадок}
    cr: p;                 {її правий нащадок}
    i,                     {вхiдний файл, що містить рядки-ЗПЗ}
    o: text;               {вихiдний файл, що міститиме рядки-результати}
    num: boolean;          {чи останнє дане є числом?}
     st: string;           {послiдовнiсть символiв числа}
      s: char;             {останнiй зчитаний символ}
      r: real;             {останнє число}
   code: integer;          {код перетворення рядка}                 BEGIN
assign(o,'TREE.SOL');  rewrite(o);
assign(i,'TREE.DAT');  reset(i);               while not eof (i) do begin
new(cp);  cp^.prev:=nil;  cp^.a:=' ';
                                 num:=true;    while not eoln(i) do begin
read(i,s);  while s=' ' do read(i,s);                      case s of
'0','1','2','3','4','5','6','7','8','9','.':                        begin
{Цифри числа}            st:='';  while (s<>' ') and not eoln(i) do begin
                                                 st:=st+s;  read(i,s) end;
{Знаходження числа}                val(st,r,code);  if code<>0 then begin
            writeln(o,'Є некоректний запис числа.');  close(o);  halt end;
{Продовжуємо заповнювати числами}                       if num then begin
new(cl);  cl^.a:=' ';  cl^.l:=nil;  cl^.r:=nil;  cl^.prev:=cp;  cp^.l:=cl;
new(cr);  cr^.a:=' ';  cr^.l:=nil;  cr^.r:=nil;  cr^.prev:=cp;  cp^.r:=cr;
          cl^.n:=r;    cp:=cr                                         end
{Вставка вершини мiж коренем i cp}           else if cp^.a=' ' then begin
new(cr);  cr^.a:=' ';  cr^.l:=cp^.r;  cr^.prev:=cp;  cp^.r^.prev:=cr;
                       cp^.r:=cr;     cp:=cr;
new(cr);  cr^.a:=' ';  cr^.l:=nil;  cr^.r:=nil;  cr^.prev:=cp;  cp^.r:=cr;
new(cl);  cl^.a:=' ';  cl^.l:=nil;  cl^.r:=nil;  cl^.prev:=cr;  cr^.l:=cl;
          cl^.n:=r;    cp:=cr;
new(cr);  cr^.a:=' ';  cr^.l:=nil;  cr^.r:=nil;  cr^.prev:=cp;  cp^.r:=cr;
                       cp:=cr                                         end
                                                               else begin
{Новий корiнь дерева}                cl:=cp;
new(cp);  cp^.a:=' ';  cp^.l:=cl;  cl^.prev:=cp;  cp^.prev:=nil;
new(cr);  cr^.a:=' ';  cp^.r:=cr;  cr^.prev:=cp;  cp:=cr;
new(cl);  cl^.a:=' ';  cp^.l:=cl;  cl^.prev:=cp;  cl^.r:=nil;  cl^.l:=nil;
new(cr);  cr^.a:=' ';  cp^.r:=cr;  cr^.prev:=cp;  cr^.r:=nil;  cr^.l:=nil;
                                                    cl^.n:=r;  cp:=cr end;
                                          num:=true                   end;
'+','-','*',':','/':                                                begin
                     if num then begin
cp:=cp^.prev^.prev;  cp^.a:=s;
cp^.r^.n:=cp^.r^.l^.n;  num:=false;
dispose(cp^.r^.l);  cp^.r^.l:=nil;
dispose(cp^.r^.r);  cp^.r^.r:=nil  end else cp^.a:=s;
if cp^.prev<>nil then cp:=cp^.prev                            end end end;
            
           {Обчислення виразу за створеним бiнарним деревом}
repeat  while cp^.l^.l<>nil do cp:=cp^.l;
      if (cp^.r^.l=nil) and (cp^.r^.r=nil) and
         (cp^.l^.l=nil) and (cp^.l^.r=nil) then        begin
                                               case cp^.a of
        '+':                     cp^.n:=cp^.l^.n+cp^.r^.n;
        '-':                     cp^.n:=cp^.l^.n-cp^.r^.n;
        '*':                     cp^.n:=cp^.l^.n*cp^.r^.n;
    ':','/': if cp^.r^.n<>0 then cp^.n:=cp^.l^.n/cp^.r^.n
                            else                       begin
        writeln(o,'Вираз не iснує, бо є дiлення на 0.');
                                         close(o);  halt end;
                                                  else begin
        writeln(o,'Є символ, що не є цифрою або знаком ',
              'арифметичної дiї.');  close(o);  halt end end;
        dispose(cp^.l);  cp^.l:=nil;
        dispose(cp^.r);  cp^.r:=nil;
        if cp^.prev<>nil then cp:=cp^.prev               end
   else cp:=cp^.r;
until (cp^.r=nil) and (cp^.l=nil) and (cp^.prev=nil);
                                            {Запис вiдповiдi}
writeln(o,cp^.n:20:4);  readln(i);  dispose(cp)          end;
                                     close(i);  close(o) END.