把字符串表达式转换成表达式树再输出

222 阅读6分钟

把字符串表达式转换成表达式树再输出

背景介绍

在计算机科学中,任何算术表达式都可以用一种名为表达式树 (Expression Tree) 的二叉树结构来表示。树的叶子节点是操作数(数字或变量),而非叶子节点是操作符。这种树形结构能够清晰地展示运算的先后顺序和优先级。

你的任务是编写一个程序,将给定的、包含加减和括号的字符串表达式,转换为一棵表达式树,并以先序遍历的顺序输出树的所有节点。

表达式元素定义

输入的表达式 calExpression 由以下三种元素组成:

  1. 操作数 (Operands):

    • 所有操作数都是变量名。
    • 变量名仅由全小写英文字母组成,长度不超过 10 个字符。
  2. 操作符 (Operators):

    • 只包含 + (加法) 和 - (减法) 两种双目运算符。
  3. 括号 (Parentheses):

    • () 用于改变运算的优先级。

运算规则

  • 优先级: +- 具有相同的运算优先级。
  • 结合性: 当运算符优先级相同时,遵循从左向右的计算顺序(左结合)。例如,a-b+c 应被解析为 (a-b)+c

任务要求

  1. 解析输入的中缀表达式字符串 calExpression

  2. 根据运算规则(优先级、结合性、括号)构建对应的二叉表达式树。

  3. 对构建好的树进行先序遍历(Pre-order Traversal)。

    • 先序遍历的顺序是:根节点 -> 左子树 -> 右子树
  4. 返回一个字符串数组,其中包含按先序遍历顺序访问到的所有节点的值。


输入格式

  • 一个字符串参数 calExpression

    • 1 <= calExpression.length < 1000
    • 仅包含小写字母和 +, -, (, ) 字符。
    • 用例保证输入的字符串是一个合法的运算表达式。

输出格式

  • 一个字符串数组(或列表),每个元素的值为节点中的操作数或者运算符。

样例

输入样例 1

"x+(a+pi-xn)+eps"

输出样例 1

["+", "+", "x", "-", "+", "a", "pi", "xn", "eps"]

样例 1 解释

  1. 表达式分析: 根据括号和左结合规则,表达式 x+(a+pi-xn)+eps 的最终计算顺序等同于 (x + ((a+pi)-xn)) + eps

  2. 构建表达式树:

    • 最外层的计算是 (...) + eps,所以根节点是 +,右子节点是 eps

    • 左子树对应 x + (a+pi-xn),其根节点是 +,左子节点是 x

    • 右子树对应 a+pi-xn,等价于 (a+pi)-xn,其根节点是 -,右子节点是 xn

    • 再往下,左子树对应 a+pi,其根节点是 +,左孩子是 a,右孩子是 pi

    • 最终形成的树结构如下:

            +
           / \
          +   eps
         / \
        x   -
           / \
          +   xn
         / \
        a   pi
      
  3. 先序遍历 (根 -> 左 -> 右):

    • 从最顶层的 + 开始。
    • 然后遍历其左子树 (x + ...),遇到 +
    • 再遍历其左子树 x
    • 再遍历其右子树 ((a+pi)-xn),遇到 -
    • ...以此类推,最终得到的访问顺序即为输出结果。

输入样例 2

"length+length"

输出样例 2

["+", "length", "length"]

解释: 不同位置的操作数可以用同一个变量名。


输入样例 3

"((x))"

输出样例 3

["x"]

解释: 一个单独的操作数也是合法的表达式;多余的括号会被解析掉,最终只剩下操作数节点。

import java.util.*;

class Solution {
    // TreeNode 内部类定义保持不变
    static class TreeNode {
        String val;
        TreeNode left;
        TreeNode right;
        public TreeNode(String val) {
            this.val = val;
        }
    }

    /**
     * 主入口方法,生成表达式树并返回其先序遍历结果。
     *
     * @param calExpression 输入的算术表达式字符串
     * @return 表达式树的先序遍历结果列表
     */
    public List<String> genCalTree(String calExpression) {
        // 调用构建树的方法
        TreeNode root = buildTree(calExpression);
        // 调用优化后的先序遍历方法
        return getTreeInPreOrder(root);
    }

    /**
     * 【优化】先序遍历的公共接口。
     *
     * @param root 树的根节点
     * @return 包含先序遍历结果的列表
     */
    public List<String> getTreeInPreOrder(TreeNode root) {
        List<String> resultList = new ArrayList<>();
        // 调用一个辅助方法来执行递归,并将结果填充到 resultList 中
        getTreeInPreOrderHelper(root, resultList);
        return resultList;
    }

    /**
     * 【优化】高效的先序遍历辅助方法。
     * 它接收一个 List 作为参数,直接将遍历结果添加到这个 List 中,避免了创建大量临时对象。
     *
     * @param node       当前遍历的树节点
     * @param resultList 用于收集结果的列表
     */
    private void getTreeInPreOrderHelper(TreeNode node, List<String> resultList) {
        if (node == null) {
            return;
        }
        resultList.add(node.val); // 访问根节点
        getTreeInPreOrderHelper(node.left, resultList); // 递归遍历左子树
        getTreeInPreOrderHelper(node.right, resultList); // 递归遍历右子树
    }

    /**
     * 【修正】构建表达式树的核心方法。
     * 为了修复索引管理问题,我们不再传递 startIndex,而是让每个 buildTree 调用都处理一个完整的表达式(或子表达式)。
     *
     * @param calExpression 当前要处理的表达式字符串
     * @return 构建好的表达式树的根节点
     */
    private TreeNode buildTree(String calExpression) {
        Deque<TreeNode> eleStk = new LinkedList<>();  // 操作数栈
        Deque<Character> signStk = new LinkedList<>();// 操作符栈
        StringBuilder ele = new StringBuilder();      // 用于拼接多位数字或变量名

        for (int i = 0; i < calExpression.length(); i++) {
            char c = calExpression.charAt(i);
            if ((c == '+') || (c == '-')) {
                // 遇到操作符前,如果 ele 中有内容,先将其作为操作数节点入栈
                if (ele.length() > 0) {
                    eleStk.push(new TreeNode(ele.toString()));
                    ele.setLength(0); // 清空 ele
                }
                signStk.push(c); // 操作符入栈
            } else if (c == '(') {
                // ---【核心点】---
                // 1. 找到与当前 '(' 匹配的 ')' 的位置
                int matchingParenIndex = findMatchingParen(calExpression, i);
                // 2. 提取括号内的子表达式
                String subExpression = calExpression.substring(i + 1, matchingParenIndex);
                // 3. 递归调用 buildTree 处理这个独立的子表达式
                eleStk.push(buildTree(subExpression));
                // 4. 将主循环的索引 i 直接跳到 ')' 的位置,下一次循环将从 ')' 之后开始
                i = matchingParenIndex;
            } else if (c == ')') {
                // 在这个实现中,右括号在 findMatchingParen 中被处理,
                // 主循环的 i 会跳过它,所以理论上不会在这里遇到 ')'。
                // 保留此分支以增加健壮性,它标志着子表达式的结束(虽然在此逻辑中由 break 实现)。
                // 这里可以什么都不做,或者抛出异常表示括号不匹配。
            } else {
                // 如果是数字或字母,则追加到 ele
                ele.append(c);
            }
        }
        // 循环结束后,处理最后一个操作数(如果存在)
        if (ele.length() > 0) {
            eleStk.push(new TreeNode(ele.toString()));
        }

        // 从栈中的内容构建树
        return buildTreeByStk(eleStk, signStk);
    }

    /**
     * 辅助方法:从操作数栈和操作符栈构建表达式树。
     *
     * @param eleStk  操作数栈 (TreeNode)
     * @param signStk 操作符栈 (Character)
     * @return 构建好的子树的根节点
     */
    private TreeNode buildTreeByStk(Deque<TreeNode> eleStk, Deque<Character> signStk) {
        // 基准情况:没有操作符了,操作数栈中剩下的唯一节点就是根
        if (signStk.isEmpty()) {
            return eleStk.isEmpty() ? null : eleStk.pop();
        }
        // 递归构建:
        // 1. 弹出一个操作符作为根
        TreeNode root = new TreeNode(String.valueOf(signStk.pop()));
        // 2. 从操作数栈弹出一个节点作为右孩子(因为栈是后进先出)
        root.right = eleStk.pop();
        // 3. 递归调用,用剩余的栈内容构建左子树
        root.left = buildTreeByStk(eleStk, signStk);
        return root;
    }

    /**
     * 辅助方法:查找与给定左括号匹配的右括号的索引。
     * 正确处理嵌套括号。
     *
     * @param s          表达式字符串
     * @param startIndex 左括号 '(' 的索引
     * @return 匹配的右括号 ')' 的索引;如果找不到则返回 -1。
     */
    private int findMatchingParen(String s, int startIndex) {
        int balance = 1; // 括号平衡计数器,遇到'('加1,遇到')'减1
        for (int i = startIndex + 1; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                balance++;
            } else if (s.charAt(i) == ')') {
                balance--;
            }
            // 当 balance 变为 0 时,说明找到了匹配的右括号
            if (balance == 0) {
                return i;
            }
        }
        return -1; // 表示括号不匹配
    }
}