JMU专题讨论:表达式树
问题1:用二叉树存储、计算表达式的主要思路
问题2:用二叉树存储、计算表达式主要过程流程图
问题3:从空间复杂度和时间复杂度两个方面来分析使用表达式树与直接使用栈来计算的差别
查询资料:表达式树有什么用?
linq就是表达式树的最重要价值的体现
linq是一种语言集成查询,定义了大约40个查询操作符,更方便的利于我们查找语法
引入正题,我们先来回顾一下表达式树的定义及其性质:
一、表达式树的叶子节点是操作数,加常数或变量名字,而其他的结点为操作符
如图这就是一个简单的表达式树,他的叶子节点都为操作数,他的非叶子节点为操作符
用二叉树存储、计算表达式
用二叉树存储表达式
二、举例说明
下面给出一种算法来把后缀表达式转变成表达式树。我们一次一个符号地读入表达式。如果符号是操作数,那么就建立一个单结点树并将它推入栈中。如果符号是操作符,那么就从栈中弹出两棵树T1和T2(T1先弹出)并形成一棵新的树,该树的根就是操作符,它的左、右儿子分别是T2和T1。然后将指向这颗树的指针压入栈中。
下面来看一个例子。设输入为ab+cde+**
前两个符号是操作数,因此创建两棵单结点树并将指向它们的指针压入栈中。
接着,"+"被读入,因此指向两棵树的指针被弹出,形成一棵新的树,并将指向它的指针压入栈中。
然后,c,d和e被读入,在单个结点树创建后,指向对应的树的指针被压入栈中。
接下来读入"+"号,因此两棵树合并。
继续进行,读入"*"号,因此,弹出两棵树的指针合并形成一棵新的树,"*"号是它的根。
最后,读入一个符号,两棵树合并,而指向最后的树的指针被留在栈中。
主要过程流程图:
主要思路:
中序遍历对于我们虽然更加熟悉,但是为了在计算机内的计算方便,我们选择后序遍历对表达式计算,所以将获得的中缀表达式通过后序遍历转化为后缀表达式存储
也就是将表达式转化为表达式树。
函数的定义
struct Node{
char ch; //结点存储数字或者运算符
Node* lchild;
Node* rchild;
};
class BinaryTree {
private:
Node* root;
public:
Node* getroot(); //获得根节点
BinaryTree(); //构造函数
void destoryTree(Node* bt); //销毁树
void preOrder(Node* bt); //前序遍历
void inOrder(Node* bt); //中序遍历
void postOrder(Node* bt); //后序遍历
};
Node* BinaryTree::getroot() {
return this->root;
}
函数的实现
Node* BinaryTree::getroot() {
return this->root;
}
BinaryTree::BinaryTree() {
this->root = new Node;
root->ch = '0';
root->lchild = nullptr;
root->rchild = nullptr;
}
void BinaryTree::preOrder(Node* bt) {
if (bt == nullptr)
return;
else {
cout << bt->ch;
preOrder(bt->lchild);
preOrder(bt->rchild);
}
}
void BinaryTree::inOrder(Node* bt) { //用中缀形式输出表达式
if (bt == nullptr)
return;
else {
if (!isNumber(bt->ch))
cout << "("; //添加左括号
inOrder(bt->lchild);
cout << bt->ch;
inOrder(bt->rchild);
if (!isNumber(bt->ch))
cout << ")"; //添加右括号
}
}
void BinaryTree::postOrder(Node* bt) {
if (bt == nullptr)
return;
else {
postOrder(bt->lchild);
postOrder(bt->rchild);
cout << bt->ch;
}
}
void BinaryTree::destoryTree(Node* bt) {
Node* lTree = bt->lchild;
Node* rTree = bt->rchild;
delete bt;
if(lTree != nullptr)
destoryTree(lTree);
if(rTree != nullptr)
destoryTree(rTree);
return;
}
判断字符是否是数字的函数和判断运算符优先级的的函数
bool isNumber(char ch) {
return (ch > 47 && ch < 58);
}
bool isPrior(char a, char b) {
//1.任何运算符优先级高于(2.当a是乘除且b是加减时算优先级高
return (b == '(' || ((a == '*' || a == '/') && (b == '+' || b == '-')));
}
要用二叉树储存表达式,首先需要将中缀表达式转化为逆波兰式,这里只实现了简单的功能,即数字只可以是0~9。
std::queue<char> toRPN(char* array) {
int i = 0;
int cnt = 0; //字符个数
while (array[i++] != '\0') //记录输入了几个字符
cnt++;
std::stack<char> stk; //转化时用栈储存暂时数据
std::queue<char> RPN; //RPN队列用于储存最后的逆波兰式
i = 0; //重置i
for (i = 0; i < cnt; i++) {
if (isNumber(array[i])) //c是数字时直接入队
RPN.push(array[i]);
else if (array[i] == '(') //c是(时入栈
stk.push(array[i]);
else if (array[i] == ')') { //c是)时,将()中的字符逐个出栈入队
while (stk.top() != '(') {
RPN.push(stk.top());
stk.pop();
}
stk.pop(); //最后将(出栈
}
else { //c是运算符时
if (!stk.empty() && !isPrior(array[i], stk.top())) { //栈不空且当前运算符优先级低时
while (!isPrior(array[i], stk.top())) { //循环出栈直到当前运算符优先级高于栈顶
RPN.push(stk.top());
stk.pop();
if (stk.empty()) //栈空时停止出栈
break;
}
}
stk.push(array[i]); //栈为空或当前运算符优先级高时直接入栈,否则循环后入栈
}
}
while (!stk.empty()) { //将栈中剩下的字符入队
RPN.push(stk.top());
stk.pop();
}
return RPN;
}
将toRPN返回的队列转化成二叉树:
BinaryTree creatTree(std::queue<char> RPN) {
BinaryTree* TreeArray = new BinaryTree[RPN.size()]; //把每个字符储存为一棵只有根的树
int cnt = RPN.size(); //字符数
std::stack<BinaryTree> NodeStk; //结点栈
for (int i = 0; i < cnt; i++) {
TreeArray[i].getroot()->ch = RPN.front(); //出队字符赋给当前结点
if (!isNumber(RPN.front())) { //c是数字时直接入栈,否则进行如下操作
//逆波兰式的第一个数字是左操作数,第二个数字是右操作数
TreeArray[i].getroot()->rchild = NodeStk.top().getroot(); //栈顶的结点作为右孩子
NodeStk.pop();
TreeArray[i].getroot()->lchild = NodeStk.top().getroot(); //然后作为左孩子
NodeStk.pop();
}
NodeStk.push(TreeArray[i]); //所有循环后栈中只剩一个结点
RPN.pop();
}
BinaryTree tree = NodeStk.top(); //将最后的结点作为根结点构造树
NodeStk.pop();
return tree;
}
用二叉树计算表达式:
计算时,使用二叉树的后续遍历,先算左子树的值,再算右子树的值,然后根据自己的操作符进行计算,再返回给上一层
代码如下:
public class Calculator {
public static void main(String[] args) {
// TODO Auto-generated method stub
Calculator ca=new Calculator();
ca.calculator();
}
public void calculator()
{
int a=new TreeNode("9-2*2/1+2*2+5*6/3-2+4").calculate();
System.out.print(a);
}
class TreeNode{
char value;
TreeNode left;
TreeNode right;
public TreeNode(String expression)
{
char[] exc = expression.toCharArray();
int length = exc.length;
int index=0;
if(length>1)
{
for(int i=length-1;i>=0;i--)
{
if(exc[i]=='*'||exc[i]=='/')
{
index = i;
}else
if(exc[i]=='+'||exc[i]=='-')
{
index = i;
break;
}
}
StringBuilder sbleft = new StringBuilder();
StringBuilder sbright = new StringBuilder();
for(int i=0;i<index;i++)
{
sbleft.append(exc[i]);
}
for(int i=index+1;i<length;i++)
{
sbright.append(exc[i]);
}
left=new TreeNode(sbleft.toString());
right= new TreeNode(sbright.toString());
}
value = exc[index];
}
public int calculate()
{
int result=0;
if(left==null && right ==null)
{
result =Integer.parseInt(Character.toString(value));
}else
{
int leftResult =left.calculate();
int rightResult=right.calculate();
switch(value){
case '+':
result=leftResult + rightResult;
break;
case '-':
result=leftResult - rightResult;
break;
case '*':
result=leftResult * rightResult;
break;
case '/':
result=leftResult / rightResult;
break;
default: break;
}
}
return result;
}
}
}
从时间复杂度和空间复杂度来分析表达式树与直接使用栈计算表达式的差别
表达式树 | 直接用栈 | |
时间复杂度 | O(n) | O(n) |
空间复杂度 | O (n) | O(n) |
-
时间复杂度:语句总的执行次数与问题规模n的函数表达式
一般算法时间复杂度的计算方法:1、用常数1取代运行时间中所有的加法常数
2、在修改后的运行次数函数中,只保留最高阶 -
空间复杂度:函数中创建对象的个数关于问题的规模函数表达式
一般算法时间复杂度的计算方法:创建变量的个数 -
递归算法的时间复杂度和空间复杂度
- 时间复杂度=递归的总次数*每次递归函数里边执行的系数
- 空间复杂度:递归的深度*每次栈帧的个数