::Главная страница ::Delphi/Паскаль :: Статьи
В этом выпуске речь пойдет о вычислении выражений, заданных строкой.

В этом выпуске речь пойдет о вычислении выражений, заданных строкой. Этот алгоритм основан на бинарном дереве и является одним из самых быстрых. Смысл алгоритма в том, что каждая операция проводится над одним или двумя числами. Например: (1+2)*3 - умножается сумма на 3, сумма - складываются 1 и 2. 1+2+3 тоже можно представить, как (1+2)+3, то есть сумму суммы 1 и 2 с 3. Таким образом, каждая операция выполняется над одной или двумя операциями (число здесь рассматривается как операция, возвращающая значение этого числа). Можно написать функцию (назовем ее Count), которая бы выполняла заданную операцию с двумя заданными подоперациями. Причем, чтобы вычислить значения подопераций, эта функция будет вызывать себя же. Например: Count(1+2) = Count(1) + Count(2). Или: Count(1*2+3*4) = Count(1*2) + Count(3*4) = Count(Count(Count(1) * Count(2)) + Count(Count(3) * Count(4))). Если значение выражения нужно посчитать несколько раз (подставляя разный x), удобно один раз разобрать выражение по бинарному дереву, а потом уже с большой скорость вычислять. Для этого нужно создать рекуррентную структуру, то есть каждая операция содержит одну или две подоперации. В свою очередь каждая подоперация содержит свои подоперации и т.д.

Теперь о реализации. Тип TOperation определя операцию (+, -, *, ...). Запись TTreeTop - это и есть операция с подоперациями. Конструкция case позволяет для разных операций хранить разные поля, не затрачивая лишней памяти. Если операция - число, в записи хранится это число, если x - хранится адрес переменной x. Такой подход позволяет наиболее удобно подсталять различные значения x. OperStr - это строковые аналоги операций (плюс - '+', синус - 'sin' и т.д.). OperPr - это приоритет операций. LastOper - последняя операция данного приоритета. FillTree ищет справа налево операцию с наименьшем приоритетом. Эта операция должна находиться вне скобок. Поэтому заведен счетчик открытых-закрытых скобок, а в начале процедуры из строки удаляются лишние скобки. Как работает Count было рассказано выше. Единственное, что о чем здесь нужно еще сказать, это об особенности дебаггера Delphi. Когда Вы запускаете программу из-под Delphi, дебаггер, несмотря на предусмотренную Вами обработку исключений try-except, при каждой ошибке выдает свое сообщение. Эта проблема исчезает, когда Вы запускаете программу другим способом (например, из Проводника). Также существует возможность выключить эту функцию. Для этого нужно выключить флажок Integrating Debugging (в Delphi3 он находится Tools|Enviroment Options|Preferences|Debugging, а в Delphi5 Tools| Option|Debugger Options).

Button1 строит график y=f(x) - f(x) задается в Edit1. Button2 проводит миллион вычислений f(x) и выводит затраченное время в заголовок окна.

Скачать необходимые для компиляции файлы проекта можно на program.dax.ru. Достапны проекты для различных версий Delphi.

uses Math;

{ Эта функция скопирована из модуля SysUtils пятого
  Delphi. Необходима в более ранних версиях Delphi: }
function AnsiSameText(const S1, S2: string): Boolean;
begin
  result := CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE,
    PChar(S1), length(S1), PChar(S2), length(S2)) = 2;
end;

type
  pdouble = ^double;
  TOperation = (opValue, opX, opUnMinus, opPlus, opMinus, opMultiply, opDivide,
    opPower, opSin, opCos, opTg, opArcsin, opArccos, opArctg,
    opInt, opAbs);
  PTreeTop = ^TTreeTop;
  TTreeTop = record
    case operation: TOperation of
      opValue: (v: double);
      opX: (xaddr: pdouble);
      opPlus, opMinus, opMultiply, opDivide, opPower: (left, right: PTreeTop);
      opUnMinus, opSin, opCos, opTg, opArcsin, opArccos, opArctg, opInt, opAbs: (arg: PTreeTop);
  end;

const
  OperStr: array [opPlus..opAbs] of string = ('+', '-', '*', '/', '^',
    'sin', 'cos', 'tg', 'arcsin', 'arccos', 'arctg', 'int', 'abs');
  OperPr: array [opUnMinus..opAbs] of integer = (1, 1, 1, 2, 2,
    3, 4, 4, 4, 4, 4, 4, 4, 4);
  LastOper: array [1..5] of TOperation = (opX, opMinus, opDivide,
    opPower, opAbs);

// Удаление лишних скобок, например: "((1+2)+3)" -> "(1+2)+3"
procedure DeleteBrackets(var s: string);
var i, br, min: integer;
begin
  if length(s) <= 0 then Exit;
  s := trim(s); // Удаление из начала и конца строки пробелов
  min := length(s); // Заведомо больше
  br := 0;
  for i := 1 to length(s) do
    case s[i] of
      '(': inc(br);
      ')': dec(br);
      else if (s[i] <> ' ') and (min > br) then min := br;
    end;
  // Удаление из строки первых и последних min символов
  s := trim(copy(s, min + 1, length(s) - min * 2));
end;

procedure FillTree(top: PTreeTop; s: string; XAddr: pdouble);
var
  op, oper: TOperation;
  i, code, MinPrior, OperPos, br: integer;
begin
  DeleteBrackets(s); // Удаление лишних скобок
  oper := opValue;
  OperPos := 1;
  MinPrior := 5;
  br := 0;
  // Поиск операции:
  for i := length(s) downto 1 do
    case s[i] of
      ')': inc(br);
      '(': dec(br);
      else if br = 0 then { Если количество открытых скобок равно
                            количеству закрытых скобок: }
        for op := opUnMinus to LastOper[MinPrior] do begin
          { Сравнение куска текста с операцией без учета регистра: }
          if (AnsiSameText(copy(s, i, length(OperStr[op])),
            OperStr[op]))
          then begin
            MinPrior := OperPr[op];
            oper := op;
            OperPos := i;
            break;
          end;
      end;
    end;
  // Если нужно - из минуса сделать унарный минус:
  if (oper = opMinus) and (OperPos = 1) then oper := opUnMinus;
  case oper of
    // операции одного аргумента:
    opUnMinus, opSin..opInt:
    begin
      new(top^.arg);
      inc(OperPos, length(OperStr[oper]));
      FillTree(top^.arg, copy(s, OperPos, length(s) - OperPos + 1), XAddr);
    end;
    // операции двух аргументов:
    opPlus, opMinus, opMultiply, opDivide, opPower:
    begin
      new(top^.left);
      FillTree(top^.left, copy(s, 1, OperPos - 1), XAddr);
      new(top^.right);
      inc(OperPos, length(OperStr[oper]));
      FillTree(top^.right, copy(s, OperPos, length(s) - OperPos + 1), XAddr);
    end;
    else begin
      if LowerCase(s) = 'x' then begin // x
        oper := opX;
        top^.operation := opX;
        top^.XAddr := XAddr;
      end else begin // Число
        oper := opValue;
        top^.operation := opValue;
        val(s, top^.v, code);
      end;
    end;
  end;
  top^.operation := oper;
end;

// Освобождение памяти:
procedure FreeTree(top: PTreeTop);
begin
  case top^.operation of
    opPlus, opMinus, opMultiply, opDivide, opPower: begin
      FreeTree(top^.left);
      FreeTree(top^.right);
    end;
    opUnMinus, opSin, opCos, opTg, opArcsin, opArccos,
      opArctg, opInt, opAbs: FreeTree(top^.arg);
  end;
  dispose(top);
end;

// Вычисление выражения:
function Count(top: PTreeTop; var error: boolean): double; register;
var y: double;
begin
  try
    case top^.operation of
      opX: result := top^.XAddr^;
      opUnMinus: result := -Count(top^.arg, error);
      opPlus: result := Count(top^.left, error) + Count(top^.right, error);
      opMinus: result := Count(top^.left, error) -
        Count(top^.right, error);
      opMultiply: result := Count(top^.left, error) *
        Count(top^.right, error);
      opDivide: result := Count(top^.left, error) /
        Count(top^.right, error);
      opPower: begin
        y := Count(top^.right, error);
        if abs(round(y) - y) < 1E-10
          then result := IntPower(Count(top^.left, error), round(y))
          else result := Power(Count(top^.left, error), y);
      end;
      opSin: result := sin(Count(top^.arg, error));
      opCos: result := cos(Count(top^.arg, error));
      opTg: result := tan(Count(top^.arg, error));
      opArcsin: result := arcsin(Count(top^.arg, error));
      opArccos: result := arccos(Count(top^.arg, error));
      opArctg: result := arctan(Count(top^.arg, error));
      opInt: result := int(Count(top^.arg, error));
      opAbs: result := abs(Count(top^.arg, error));
      else result := top^.v;
    end;
  except // Проихошла ошибка:
    result := 0;
    error := true;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
const
  left = -15; // Наименьший x
  right = 15; // Наибольший x
  k = 10; // Колиство точек на пиксел экрана
var
  x, y: double;
  i: integer;
  error: boolean;
  top: PTreeTop;
  Scale: single;
begin
  //5*cos(2*x)+12*sin(2*x)-12*sin(x)-18*cos(x)+17
  new(top); // Выделение памяти под главную вершину
  FillTree(top, Edit1.Text, @x); // Заполнение бинарного дерева
  Scale := Image1.Width / (right - left); // Вычисление масштаба
  // Вывод осей:
  with Image1.Canvas do begin
    Rectangle(0, 0, Image1.Width, Image1.Height);
    MoveTo(round(-left * Scale), Image1.Height);
    LineTo(round(-left * Scale), 0);
    MoveTo(0, Image1.Height div 2);
    LineTo(Image1.Width, Image1.Height div 2);
  end;
  error := false;
  for i := round(left * Scale * k) to round(right * Scale * k) do begin
    x := i / Scale / k; { В ячейку памяти с x
                          кладем значение аргумента }
    y := Count(top, error); // Вычисление выражения
    if error // Если ошибка:
      then error := false // Восстановить значение флага
      // Иначе рисуется точка:
      else Image1.Canvas.Pixels[(round(i / k - left * Scale)),
        round(Image1.Height / 2 - y * Scale)] := clRed;
  end;
  FreeTree(top);
end;

{$HINTS OFF} { Поскольку это тестирующая процедура,
               предупреждения здесь не нужны }
procedure TForm1.Button2Click(Sender: TObject);
var
  x, y: double;
  i: integer;
  error: boolean;
  top: PTreeTop;
  t: cardinal;
begin
  new(top); // Выделение памяти под главную вершину
  FillTree(top, Edit1.Text, @x); // Заполнение бинарного дерева
  t := GetTickCount; // Засечение времени
  for i := 1 to 1000000 do
    y := Count(top, error); // Вычисление выражения
  Form1.Caption := IntToStr(GetTickCount - t); // Вывод времени
  FreeTree(top); // Освобождение памяти
end;
{$HINTS ON}



Полезные мелочи
Для сравнения двух строк без учета регистра удобно использовать AnsiSameText. Эта функция корректно работает с различными языками. В Delphi3,4 AnsiSameText не существует, поэтому необходимо описывать ее, как это сделано в приведенной выше программе. Пример:
procedure TForm1.Button1Click(Sender: TObject);
begin
  if AnsiSameText(Edit1.Text, Edit2.Text)
    then Form1.Caption := 'same'
    else Form1.Caption := 'different';
end;



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

Всего доброго,
Даниил Карапетян.
Тематические ссылки
Ваша ссылка Ваша ссылка

Обмен кнопками, ведение статистики, реклама.