把字符串表达式转换成表达式树再输出
背景介绍
在计算机科学中,任何算术表达式都可以用一种名为表达式树 (Expression Tree) 的二叉树结构来表示。树的叶子节点是操作数(数字或变量),而非叶子节点是操作符。这种树形结构能够清晰地展示运算的先后顺序和优先级。
你的任务是编写一个程序,将给定的、包含加减和括号的字符串表达式,转换为一棵表达式树,并以先序遍历的顺序输出树的所有节点。
表达式元素定义
输入的表达式 calExpression 由以下三种元素组成:
-
操作数 (Operands):
- 所有操作数都是变量名。
- 变量名仅由全小写英文字母组成,长度不超过 10 个字符。
-
操作符 (Operators):
- 只包含
+(加法) 和-(减法) 两种双目运算符。
- 只包含
-
括号 (Parentheses):
(和)用于改变运算的优先级。
运算规则
- 优先级:
+和-具有相同的运算优先级。 - 结合性: 当运算符优先级相同时,遵循从左向右的计算顺序(左结合)。例如,
a-b+c应被解析为(a-b)+c。
任务要求
-
解析输入的中缀表达式字符串
calExpression。 -
根据运算规则(优先级、结合性、括号)构建对应的二叉表达式树。
-
对构建好的树进行先序遍历(Pre-order Traversal)。
- 先序遍历的顺序是:根节点 -> 左子树 -> 右子树。
-
返回一个字符串数组,其中包含按先序遍历顺序访问到的所有节点的值。
输入格式
-
一个字符串参数
calExpression。1 <= calExpression.length < 1000。- 仅包含小写字母和
+,-,(,)字符。 - 用例保证输入的字符串是一个合法的运算表达式。
输出格式
- 一个字符串数组(或列表),每个元素的值为节点中的操作数或者运算符。
样例
输入样例 1
"x+(a+pi-xn)+eps"
输出样例 1
["+", "+", "x", "-", "+", "a", "pi", "xn", "eps"]
样例 1 解释
-
表达式分析: 根据括号和左结合规则,表达式
x+(a+pi-xn)+eps的最终计算顺序等同于(x + ((a+pi)-xn)) + eps。 -
构建表达式树:
-
最外层的计算是
(...) + eps,所以根节点是+,右子节点是eps。 -
左子树对应
x + (a+pi-xn),其根节点是+,左子节点是x。 -
右子树对应
a+pi-xn,等价于(a+pi)-xn,其根节点是-,右子节点是xn。 -
再往下,左子树对应
a+pi,其根节点是+,左孩子是a,右孩子是pi。 -
最终形成的树结构如下:
+ / \ + eps / \ x - / \ + xn / \ a pi
-
-
先序遍历 (根 -> 左 -> 右):
- 从最顶层的
+开始。 - 然后遍历其左子树
(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; // 表示括号不匹配
}
}