Дерево выражений Nightmare с чрезмерно ограниченным классом

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

Первая команда/функция, getNodes, принимает строку, представляющую префиксное выражение, используя целые числа со знаком и четыре операции +, -, * и /, и создает соответствующий связанный список токенов с нулевым завершением, используя класс Node, с токенами, связанными через "правильный" указатель.

Вторая команда/функция, getTree, берет аналогичную строку, передает ее в getNodes и повторно связывает результирующие узлы в дерево выражений.

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

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

#ifndef EXPTREE_H_
#define EXPTREE_H_

using namespace std;

enum Ops{ADD, SUB, MUL, DIV, NUM};

class Node {
   private:
        int num;
        Ops op;
        Node *left, *right;

    public:
        friend Node *getNodes(string d);
        friend Node *getTree(string d);
        friend int evaluate (string);
    };

int evaluate(string d);
Node *getNodes(string d);
Node *getTree(string d);
#endif

Единственные библиотеки, которые можно использовать, это

#include <iostream>
#include <vector>
#include <string>
#include "exptree.h" 

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

Вот программа драйвера, которую я дал им на основе их спецификаций.

#include <iostream>
#include <string>
#include "exptree.h"
using namespace std;
void test(string s, int target) {
    int result = evaluate(s);
    if (result == target)
        cout << s << " correctly evaluates to " << target << endl;
    else
        cout << s << "(" << result 
             << ") incorrectly evaluates to " << target << endl;
}
int main() {
    test("42", 42);
    test("* - / 4 2 1 42", 42);
    test("* - / -4 +2 -1 2", -2);
    test("* - / -4 +2 -1 2            ", -2);
    test("* 9 6", 54);
    return 0;
}

Сможете ли вы написать три функции как можно элегантнее, чтобы решить эту кошмарную проблему?


person Tom Murphy    schedule 04.04.2011    source источник
comment
Я решил эту же проблему (как и все) на одном из моих занятий год назад или около того. Мы решили эту проблему, сделав оценку полиморфной. Узлы умножения будут умножать свои левые и правые, узлы значений будут просто возвращать свое значение и так далее. Сделал программу самописной :) Я понимаю, что это противоречит вашему ограничению Write only three functions, но было бы интересно их познакомить.   -  person Mike Bailey    schedule 04.04.2011
comment
Гарантировано ли, что токены разделены пробелами, как в ваших примерах?   -  person Null Set    schedule 04.04.2011
comment
Единственные библиотеки, которые можно использовать, это <string>? В противном случае сложно определить функции ;-)   -  person Steve Jessop    schedule 04.04.2011
comment
-1: Должен ли ответ компилироваться? Программа драйвера не будет. string в exptree.h не находится в глобальном пространстве имен к моменту включения exptree.h в программу-драйвер.   -  person Robᵩ    schedule 04.04.2011
comment
@rob Ничего себе, единственная проблема заключается в том, чтобы включить <string> и using namespace std или префикс strings. Подумаешь?   -  person pajton    schedule 05.04.2011
comment
Вы правы, подразумевалась строка, так как она необходима как тип параметра. Вы также правы, также требуется использование пространства имен std. Мои студенты решили избежать полиморфизма или более чем одного типа узла, используя добавление NUM как часть перечисляемого типа операций.   -  person Tom Murphy    schedule 05.04.2011


Ответы (3)


Функции getNodes и getTree было бы довольно тривиально написать при таких ограничениях, поэтому я просто перешел к интересной части. Вы, естественно, рекурсивно оцениваете дерево выражений, но здесь это невозможно, потому что функция eval принимает только строку. Конечно, вы можете преобразовать оставшееся дерево в префиксное выражение и рекурсивно вызвать eval, но это было бы просто глупо.

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

#include <iostream>
#include <vector>
#include <string>
using namespace std;
#include "exptree.h" 

int evaluate(string d){
    Node* tree = getTree(d);
   //convert tree to postfix for simpler evaluation
    vector<Node*> node_stack;
    node_stack.push_back(tree);
    Node postfix_head;
    Node* postfix_tail = &postfix_head;
    while(node_stack.size() > 0){
        Node* place = node_stack.back();
        if(place->left == 0){
             if(place->right == 0){
                 postfix_tail->right = place;
                 node_stack.pop_back();
             } else {
                 node_stack.push_back(place->right);
                 place->right = 0;
             }
        } else {
            node_stack.push_back(place->left);
            place->left = 0;
        }
    }
   //evaluate postfix
    Node* place = postfix_head.right;
    vector<int> stack;
    while(place != 0){
        if(place->op != NUM){
            int operand_a, operand_b;
            operand_b = stack.back();
            stack.pop_back();
            operand_a = stack.back();
            stack.pop_back();
            switch(place->op){
                case ADD:
                    stack.push_back(operand_a + operand_b);
                    break;
                case SUB:
                    stack.push_back(operand_a - operand_b);
                    break;
                case MUL:
                    stack.push_back(operand_a * operand_b);
                    break;
                case DIV:
                    stack.push_back(operand_a / operand_b);
                    break;
            }
        } else {
            stack.push_back(place->num);
        }
        place = place->right;
    }
    return stack.back();
}
person Null Set    schedule 04.04.2011
comment
Это хорошее начало решения, очень прямое. Сегмент дал сбой, когда я запустил его в строке operand_b = stack.back();. Похоже, ваш стек опустел. getnodes легко написать. getTree требует некоторой доработки. - person Tom Murphy; 05.04.2011

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

Node* relink(Node* start) // builds a tree; returns the following node
{
    if (start->op == NUM)
    {
        Node* result = start->right;
        start->left = start->right = NULL;
        return result;
    }
    else
    {
        start->left = start->right;
        start->right = relink(start->left);
        return relink(start->right);
    }
}

Node* getTree(string d)
{
    Node* head = getNodes(d);
    relink(head);
    return head;
}

Я мог бы реализовать рекурсию, используя явный стек (реализованный std::vector), но это уродливо и неясно (если только вы не хотите, чтобы студенты практиковали именно это).

person anatolyg    schedule 04.04.2011
comment
Легко писать с двумя дополнительными функциями, допускающими рекурсию. Конечно, я не хочу, чтобы мои ученики создавали явный стек. Когда я решил это, я был удивлен, насколько красивым было решение. Я тоже ожидал, что использование явного стека будет уродливым. Я ошибался. Я провел своих студентов через мое решение сегодня. На самом деле это было довольно полезно. Я не очень заинтересован в том, чтобы мои ученики практиковали; Я хочу, чтобы они решали разумные проблемы; для них это было неприемлемо, но я подумал, что людям из stackoverflow это может понравиться. - person Tom Murphy; 05.04.2011

Для чего это стоит, вот решение, которое я закодировал непосредственно перед тем, как опубликовать вопрос

#include <iostream>
#include <vector>
#include "exptree.h"
using namespace std;

Node *getNodes(string s) {
    const int MAXINT =(int)(((unsigned int)-1) >> 1), MININT = -MAXINT -1;
    Node *list;
    int sign, num;

    s += " ";                                                   // this simplifies a lot of logic, allows trailing white space to always close off an integer
    list = (Node *) (num = sign = 0);
    for (int i=0; i<s.size(); ++i) {
        char c = s[i];                                          // more efficient and cleaner reference to the current character under scrutiny
        if (isdigit(c)) {
            if (sign == 0) sign = 1;                            // if sign not set, then set it. A blank with a sign==0 now signifies a blank that can be skipped
            num = 10*num + c - '0';
        } else if (((c=='+') || (c=='-')) && isdigit(s[i+1])) { // another advantage of adding blank to string above so don't need a special case
            sign = (c=='+') ? 1 : -1;
        } else if ( !isspace(c) &&  (c != '+')  && (c != '-')  && (c != '*')  && (c != '/')) {
            cout << "unexpected character " << c << endl;
            exit(1);
        } else if  (!isspace(c) || (sign != 0)) {                                                       // have enough info to create next Node
            list->left = (list == 0) ? (list = new Node) : (list->left->right = new Node);              // make sure left pointer of first Node points to last Node
            list->left->right = 0;                                                                      // make sure list is still null terminated
            list->left->op = (c=='+' ? ADD : (c=='-' ? SUB : (c=='*' ? MUL : (c=='/' ? DIV : NUM))));   // choose right enumerated type
            list->left->num = (list->left->op==NUM) ? sign*num : MININT;                                // if interior node mark number for evaluate function
            num = sign = 0;                                                                             // prepare for next Node
        }
    }
    return list;
}

Node *getTree(string s) {
    Node *nodes = getNodes(s), *tree=0, *root, *node;
    vector<Node *> stack;

    if (nodes == 0) return tree;
    root = tree = nodes;
    nodes = nodes->right;
    for (node=nodes; node != 0; node=nodes) {
        nodes = nodes->right;
        if (root->op != NUM) {              // push interior operator Node on stack til time to point to its right tree
            stack.push_back(root);
            root = (root->left = node);     // set interior operator Node's left tree and prepare to process that left tree
        } else {                            
            root->left = root->right = 0;   // got a leaf number Node so finish it off
            if (stack.size() == 0) break;
            root = stack.back();            // now pop operator Node off the stack
            stack.pop_back();
            root = (root->right = node);    // set its left tree and prepare to process that left tree
        }
    }
    if ((stack.size() != 0) || (nodes != 0)) {
        cout << "prefix expression has missing or extra terms" << endl;
        exit(1);
    }
    return tree;
}

int evaluate(string s) {
    // MININT is reserved value signifying operator waiting for a left side value, low inpact since at edge of representable integers
    const int MAXINT =(int)(((unsigned int)-1) >> 1), MININT = -MAXINT -1;
    Node *tree = getTree(s);
    vector<Node *> stack;
    int v = 0;                              // this is value of a leaf node (a number) or the result of evaluating an interior node
    if (tree == 0) return v;
    do {
        v = tree->num;
        if (tree->op != NUM) {
            stack.push_back(tree);
            tree = tree->left;              // prepare to process the left subtree      
        } else while (stack.size() != 0) {  // this while loop zooms us up the right side as far as we can go (till we come up left side or are done)
            delete tree;                    // done with leaf node or an interior node we just finished evaluating
            tree = stack.back();            // get last interior node from stack
            if (tree->num == MININT) {      // means returning up left side of node, so save result for later
                tree->num = v;
                tree = tree->right;         // prepare to evaluate the right subtree
                break;                      // leave the "else while" for the outer "do while" which handles evaluating an expression tree
            } else {                        // coming up right side of an interior node (time to calculate)
                stack.pop_back();           // all done with interior node
                v = tree->op==ADD ? tree->num+v : (tree->op==SUB ? tree->num-v : (tree->op==MUL ? tree->num*v : tree->num/v)) ;
            }
        }
    } while (stack.size() != 0);
    return v;
}
person Tom Murphy    schedule 19.06.2011
comment
list = (Node *) (num = sign = 0); это определенно НЕ красиво! - person Xeo; 19.06.2011
comment
Думаю, красота в глазах смотрящего. Это был компактный способ инициализировать три вещи нулями. Размещение связанного кода на экране — тоже приятная вещь. - person Tom Murphy; 19.06.2011
comment
Красота также в глазах компилятора ;). Однострочники ‹ предупреждения. - person Chuck Claunch; 03.11.2014