专题讨论2

数据结构专题讨论2:树与查找

----表达式树(网安2411班于鸿硕、吴承桓小组)


一、题目分析与思路设计

求解本题,小组分别从用表达式树和用栈二种解决方法进行分析。通过对计算表达式进行分析,我们发现在解决表达式储存和计算问题中,两种解决方法都需要先用栈把中缀表达式转化为后缀表达式,而后分别存入不同数据结构体进行计算,遂有如下设计:

//伪代码:


//     表达式树法:


// 定义表达式树节点结构
结构体 TreeNode {
    字符串 val     // 节点值,可以是数字或运算符
    TreeNode* left  // 左子树指针
    TreeNode* right // 右子树指针
}

// 判断字符是否为数字
函数 isDigit(字符 ch) 返回 布尔值:
    返回 ch >= '0' 且 ch <= '9'

// 获取运算符优先级
函数 getPriority(字符 op) 返回 整型:
    如果 op 是 '+' 或 '-' 则 返回 1
    如果 op 是 '*' 或 '/' 则 返回 2
    返回 0  // 其他字符(如括号)

// 中缀表达式转后缀表达式
函数 infixToPostfix(字符串 infix) 返回 字符串:
    初始化 opStack 为字符栈
    初始化 postfix 为空字符串

    对于 infix 中的每个字符 ch:
        如果 ch 是空格 则 跳过

        如果 isDigit(ch) 为真:
            postfix 追加 ch
        否则 如果 ch 是 '(':
            opStack.push(ch)
        否则 如果 ch 是 ')':
            当 opStack 非空 且 栈顶不是 '(':
                postfix 追加 opStack.top()
                opStack.pop()
            opStack.pop() // 弹出 '('
        否则: // 处理运算符
            当 opStack 非空 且 getPriority(opStack.top()) >= getPriority(ch):
                postfix 追加 opStack.top()
                opStack.pop()
            opStack.push(ch)

    // 弹出栈中剩余运算符
    当 opStack 非空:
        postfix 追加 opStack.top()
        opStack.pop()

    返回 postfix

// 根据后缀表达式构建表达式树
函数 buildTree(字符串 postfix) 返回 TreeNode*:
    初始化 nodeStack 为 TreeNode* 栈

    对于 postfix 中的每个字符 ch:
        如果 isDigit(ch) 为真:
            创建新节点 newNode(ch)
            nodeStack.push(newNode)
        否则: // 处理运算符
            创建新节点 newNode(ch)
            newNode->right = nodeStack.top()
            nodeStack.pop()
            newNode->left = nodeStack.top()
            nodeStack.pop()
            nodeStack.push(newNode)

    如果 nodeStack 非空 则 返回 nodeStack.top()
    否则 返回 NULL

// 递归计算表达式树的值
函数 calculate(TreeNode* root) 返回 整型:
    如果 root 为 NULL 则 返回 0

    如果 root 是叶节点 (左右子节点均为NULL):
        将 root->val 转换为整数 num
        返回 num

    left = calculate(root->left)
    right = calculate(root->right)

    根据 root->val 的值:
        如果是 "+": 返回 left + right
        如果是 "-": 返回 left - right
        如果是 "*": 返回 left * right
        如果是 "/": 返回 left / right

    返回 0 // 默认值

// 主程序
函数 main() 返回 
            
            
//     直接用栈法:
            
            
输入:后缀表达式 postfix
输出:表达式计算结果
初始化:空栈 operand_stack

对 postfix 中的每个字符 token:
    if token 是数字:
        将 token 转换为数值 num
        将 num 压入 operand_stack 栈
    else if token 是运算符(+、-、*、/):
        弹出 operand_stack 栈顶元素作为右操作数 right
        弹出 operand_stack 栈顶元素作为左操作数 left
        switch token:
            case '+':
                result = left + right
            case '-':
                result = left - right
            case '*':
                result = left * right
            case '/':
                if right == 0:
                    抛出除零错误
                result = left / right
        将 result 压入 operand_stack 栈

返回 operand_stack 的栈顶元素(即最终计算结果)

二、主要运行过程流程图

graph TD; A[输入表达式]-->B[中缀表达式通过栈转化为后缀形式]; B-->|遍历后缀表达式|c["逢操作数入栈、逢运算符出栈两个操作数作为孩子结点"]; c-->g[将操作符节点入栈]; g-->z["表达式树构建完成,返回根节点"] z-->e["递归计算"]; e-->f[计算结果]; B-->|直接用栈|e;

三、代码及运行结果

1.表达树法:

#include <iostream>
#include <stack>
#include <string>

using namespace std;

// 表达式树节点结构
struct TreeNode {
    string val;     // 节点值,可以是数字或运算符
    TreeNode* left;  // 左子树指针
    TreeNode* right; // 右子树指针

    // 构造函数
    TreeNode(string x) {
        val = x;
        left = NULL;
        right = NULL;
    }
};

// 判断字符是否为数字
bool isDigit(char ch) {
    return ch >= '0' && ch <= '9';
}

// 获取运算符优先级
// 返回值越大表示优先级越高
int getPriority(char op) {
    if (op == '+' || op == '-') return 1;  // 加减法优先级较低
    if (op == '*' || op == '/') return 2;  // 乘除法优先级较高
    return 0;  // 其他字符(如括号)优先级最低
}

// 中缀表达式转后缀表达式(逆波兰表示法)
// 参数:infix - 中缀表达式字符串
// 返回:对应的后缀表达式字符串
string infixToPostfix(const string& infix) {
    stack<char> opStack;  // 运算符栈
    string postfix;       // 存储后缀表达式结果

    // 遍历中缀表达式每个字符
    for (int i = 0; i < infix.size(); i++) {
        char ch = infix[i];
        if (ch == ' ') continue;  // 跳过空格

        // 处理数字
        if (isDigit(ch)) {
            postfix += ch;  // 数字直接加入后缀表达式
        }
        // 处理左括号
        else if (ch == '(') {
            opStack.push(ch);  // 左括号直接入栈
        }
        // 处理右括号
        else if (ch == ')') {
            // 弹出栈顶元素直到遇到左括号
            while (!opStack.empty() && opStack.top() != '(') {
                postfix += opStack.top();
                opStack.pop();
            }
            opStack.pop();  // 弹出左括号但不加入后缀表达式
        }
        // 处理运算符
        else {
            // 弹出栈顶优先级不低于当前运算符的所有运算符
            while (!opStack.empty() && getPriority(opStack.top()) >= getPriority(ch)) {
                postfix += opStack.top();
                opStack.pop();
            }
            opStack.push(ch);  // 当前运算符入栈
        }
    }

    // 弹出栈中剩余的所有运算符
    while (!opStack.empty()) {
        postfix += opStack.top();
        opStack.pop();
    }

    return postfix;
}

// 根据后缀表达式构建表达式树
// 参数:postfix - 后缀表达式字符串
// 返回:表达式树的根节点指针
TreeNode* buildTree(const string& postfix) {
    stack<TreeNode*> nodeStack;  // 节点栈,用于构建表达式树

    // 遍历后缀表达式每个字符
    for (int i = 0; i < postfix.size(); i++) {
        char ch = postfix[i];

        // 处理数字
        if (isDigit(ch)) {
            // 创建数字节点并入栈
            nodeStack.push(new TreeNode(string(1, ch)));
        }
        // 处理运算符
        else {
            // 创建运算符节点
            TreeNode* node = new TreeNode(string(1, ch));
            // 运算符的右操作数是栈顶元素
            node->right = nodeStack.top();
            nodeStack.pop();
            // 运算符的左操作数是新的栈顶元素
            node->left = nodeStack.top();
            nodeStack.pop();
            // 将运算符节点入栈
            nodeStack.push(node);
        }
    }

    // 栈顶元素即为表达式树的根节点
    return nodeStack.empty() ? NULL : nodeStack.top();
}

// 递归计算表达式树的值
// 参数:root - 表达式树的根节点指针
// 返回:表达式的计算结果
int calculate(TreeNode* root) {
    if (root == NULL) return 0;  // 空树返回0

    // 如果是叶节点(数字节点)
    if (root->left == NULL && root->right == NULL) {
        int num = 0;
        // 将字符串转换为整数
        for (int i = 0; i < root->val.size(); i++) {
            num = num * 10 + (root->val[i] - '0');
        }
        return num;
    }

    // 递归计算左子树的值
    int left = calculate(root->left);
    // 递归计算右子树的值
    int right = calculate(root->right);

    // 根据运算符计算左右子树的结果
    if (root->val == "+") return left + right;
    if (root->val == "-") return left - right;
    if (root->val == "*") return left * right;
    if (root->val == "/") return left / right;

    return 0;  // 默认返回0
}

int main() {
    string expr = "(3+5)*(10-4)";  // 测试表达式
    string postfix = infixToPostfix(expr);  // 转换为后缀表达式
    TreeNode* root = buildTree(postfix);    // 构建表达式树
    int result = calculate(root);           // 计算结果

    // 输出信息
    cout << "表达式: " << expr << endl;
    cout << "后缀表达式: " << postfix << endl;
    cout << "计算结果: " << result << endl;

    return 0;
}

运行截图:

2.直接用栈法:

// 栈方法计算后缀表达式的值
double evaluatePostfix(const vector<char>& postfix) {
    stack<double> operands;
    for (char c : postfix) {
        if (isdigit(c)) {
            operands.push(c - '0');
        } else if (isOperator(c)) {
            double right = operands.top();
            operands.pop();
            double left = operands.top();
            operands.pop();
            switch (c) {
                case '+':
                    operands.push(left + right);
                    break;
                case '-':
                    operands.push(left - right);
                    break;
                case '*':
                    operands.push(left * right);
                    break;
                case '/':
                    if (right == 0) {
                        cerr << "Error: Division by zero!" << endl;
                        return 0;
                    }
                    operands.push(left / right);
                    break;
            }
        }
    }
    return operands.top();
}

int main() {
    double stackResult = evaluatePostfix(postfix);
    cout << "Stack method result: " << stackResult << endl;
}

四、分析与结论

在实验过程中,我们发现:

传统的计算表达式的方法(即直接用栈法)

可以通过一个栈实现该计算功能,代码相对简洁简单,占用内存空间略为轻量,不需要递归遍历使得计算效率更高。但是结构不够直观,用户无法对表达式进行分析和修改,同时只适用于加减乘除基本操作。

复杂度分析:

  • 时间复杂度
    • 中缀转后缀:遍历表达式一次,时间复杂度为 (O(n))。
    • 计算后缀表达式:遍历后缀表达式一次,时间复杂度为 (O(n))。
    • 综上,总时间复杂度为 (O(n) + O(n) = O(n))。
  • 空间复杂度
    • 中缀转后缀:栈中最多存储 (O(n)) 个运算符(如大量左括号场景)。
    • 计算后缀表达式:栈中最多存储 (O(n/2)) 个操作数(如交替出现操作数和运算符),仍为 (O(n))。
    • 综上,空间复杂度为 (O(n))。

表达式树方法

表达式树能直观地向用户表示表达式的结构,便于理解和分析,甚至拓展至修改、化简和求导等高级操作(可直接操作树节点),也能够处理含括号和复杂运算符优先级的表达式。但是实现偏复杂,需要大量代码和额外内存空间支持两个数据结构。

复杂度分析:

  • 时间复杂度
    • 中缀转后缀:遍历表达式一次,时间复杂度为 (O(n))。
    • 构建表达式树:遍历后缀表达式一次,每个元素处理一次,时间复杂度为 (O(n))。
    • 计算表达式树值:后序遍历每个节点一次,时间复杂度为 (O(n))。
    • 综上,总时间复杂度为 (O(n) + O(n) + O(n) = O(n))。
  • 空间复杂度
    • 中缀转后缀:栈中最多存储 (O(n)) 个运算符(如大量左括号场景)。
    • 构建表达式树:存储 n 个节点(每个表达式元素对应一个节点),空间复杂度为 (O(n))。
    • 计算表达式树值:递归调用栈深度最大为树高(最坏 (O(n)),如退化为链表的树)。
    • 综上,空间复杂度主要由存储树节点决定,为 (O(n))。

最后,我们对比列出如下表格:

方面 表达式树 直接用栈
时间复杂度 构建(O (n)) + 计算(O (n)) 转后缀(O (n)) + 计算(O (n))
空间复杂度 O (n)(存储整棵树) O (n)(栈空间,无需存储树)
内存占用 略微大(两个数据结构) 略微小(一个栈)
适用户性 结构清晰 用户不可见其结构
可拓展性 可进行修改、化简及其他高级操作

综上所分析,表达式树具有操作拓展性高的显著优势,可以在计算中实时根据需要进行修改和调整,可应用于更多种实际场景


五、讨论

经资料查阅整合,表达式树除了计算表达式求值还有下列功能:

  • 代码生成:在编译器和代码生成器中,表达式树可以作为中间表示形式。将高级语言中的表达式转换为表达式树后,再根据目标平台的指令集和规范,将表达式树生成对应的机器码或其他目标代码。这样可以方便地对表达式进行优化和生成高效的代码。
  • 语法分析与验证:在编译过程中,词法分析器和语法分析器将输入的源代码转换为表达式树,以此来检查表达式的语法结构是否正确。例如,在检查一个复杂的数学表达式或逻辑表达式是否符合语法规则时,表达式树能够清晰地展示表达式的结构,帮助发现语法错误。
  • 数据查询与筛选:在数据库查询语句的处理中,表达式树可以用来表示查询条件。例如,对于查询语句SELECT * FROM students WHERE age > 18 AND grade = 'A',可以将其转换为表达式树来进行解析和处理,以便数据库系统能够根据表达式树来执行相应的查询操作,高效地筛选出符合条件的数据。
  • 函数式编程与 Lambda 表达式处理:在函数式编程语言中,表达式树常用于表示 Lambda 表达式或其他函数式构造。它使得对函数的操作和转换更加方便,例如可以对函数进行组合、柯里化等操作,还可以在运行时动态地生成和修改函数。
  • 优化与变换:基于表达式树的结构,可以对表达式进行各种优化操作。例如,常量折叠、公共子表达式消除、代数化简等。通过分析表达式树的节点和结构,识别出可以优化的部分,并进行相应的变换,以提高表达式的计算效率或代码的执行效率。
  • 可视化与调试:表达式树可以被可视化,以帮助开发人员理解复杂表达式的结构和执行流程。在调试过程中,通过查看表达式树的状态和节点值,可以更容易地发现表达式求值过程中出现的问题,有助于快速定位和解决错误。
posted @ 2025-04-22 11:46  KinthYu  阅读(29)  评论(0)    收藏  举报