[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)

文章目录

  • 一、问题背景
  • 二、原理分析
    • 1 后缀表达式计算结果
    • 2 中缀表达式转后缀表达式
  • 三、准备数据结构
    • 1 栈
    • 2 队列
  • 四、设计计算器
    • 1 提取字符串的数字
    • 2 进栈处理
    • 3 后缀计算
    • 4 输出结果

一、问题背景

【复杂计算器】:使用C/C++编程,从字符串中获取表达式,比如9+(3-1)×3+10÷2,并完成计算。

在之前,由于笔者才疏学浅,掌握知识甚少,完成某次任务【复杂计算器】时,利用了三个栈,而且还是使用的STL中的栈的实现,极大的增大了内存的开销。
于是,这次算是一次补票:利用栈和队列重新实现复杂计算器。

设想是:

  • 首先,我们输入中缀表达式的四则运算表达式
  • 将中缀表达式转换为后缀表达式,并存入队列之中
  • 将队列中的后缀表达式取出,并计算结果

二、原理分析

此处参考为程杰作者的《大话数据结构[溢彩加强版]》
[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第1张图片

1 后缀表达式计算结果

为了解释后缀表达式的好处,我们先来看看,计算机如何应用后缀表达式计算出最终的结果 20 的。
后缀表达式:9 3 1-3 *+10 2 /+

规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。

  1. 初始化一个空栈。此栈用来对要运算的数字进出使用。

    [C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第2张图片
    2. 后缀表达式中前三个都是数字,所以 931 进栈。

    [C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第3张图片
    3. 接下来是 -,所以将栈中的 1 出栈作为减数,3 出栈作为被减数,并运算 3-1 得到 2,再将 2 进栈。

    [C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第4张图片
    4. 接着是数字 3 进栈

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第5张图片

以此类推,直到表达式遍历完成,得到结果:

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第6张图片
最后出栈:
[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第7张图片

2 中缀表达式转后缀表达式

那么,我们是如何由中缀表达式得到后缀表达式的呢?

我们把平时所用的标准四则运算表达式,即9+(3-1)×3+10÷2叫做中缀表达式。

我们现在将中缀表达式转换为后缀表达式。
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终得到后缀表达式为止。

  1. 初始化一空栈,用来对符号进出栈使用。

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第8张图片
2. 第一个字符是数字 9,输出 9,后面是符号 +,进栈。

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第9张图片

  1. 第三个字符是 (,依然是符号,因其只是左括号,还未配对,故进栈。

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第10张图片

  1. 第四个字符是数字 3,输出,总表达式为 9 3,接着是 -,进栈。

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第11张图片
5. 接下来是数字 1,输出,总表达式为 9 3 1,后面是符号 ,此时,我们需要去匹配此前的 ( ,所以栈顶依次出栈,并输出,直到 ( 出栈为止。此时左括号上方只有 - ,因此输出 - 。总的输出表达式为 9 3 1-

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第12张图片

  1. 紧接着是符号 ×,因为此时的栈顶符号为 +,优先级低于 ×,因此不输出, *进栈。接着是数字 3,输出,总的表达式为 9 3 1 – 3

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第13张图片

  1. 之后是符号 + ,此时当前栈顶元素 * 比这个 + 的优先级高,因此栈中元素出栈并输出 (没有比 + 更低的优先级,所以全部出栈),总输出表达式为 9 3 1-3 * +。然后将当前这个符号 + 进栈。也就是说,前 6 张图的栈底的 + 是指中缀表达式中开头的 9 后面那个 + ,而左图中的栈底 (也是栈顶)的 + 是指 9+ (3-1)×3+ 中的最后一个 +

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第14张图片
8. 紧接着数字 10,输出,总表达式变为 9 3 1-3 *+10。后是符号 ÷,所以 /进栈。如右图所示。

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第15张图片

  1. 最后一个数字 2,输出,总的表达式为 9 3 1 – 3 *+10 2

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第16张图片

  1. 因为已经到最后,所以将栈中符号全部出栈并输出。最终输出的后缀表达式结果为 9 3 1 – 3 *+10 2 /+

[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)_第17张图片

三、准备数据结构

我们为计算器编写一个栈和队列,这里笔者使用的C++,仅仅只使用了部分C++的类的特性,方便封装和整合,核心代码部分,C语言也可以食用。

1 栈

定义两个类型,分别表示数字和符号:

	typedef double ElemType;
    typedef char OpType;

我们使用链式结构进行设计我们的结构:
先定义好节点,这里我们为每个节点的数据域声明两个类型,再使用is_data进行判断是数字还是符号:

struct Node {
        ElemType data = 0.0;
        OpType op = 0;
        bool is_data = true;
        Node *next = nullptr;
    };

其他部分和一般链栈无异,仅仅用了一些C++的构造函数,重载函数,错误检查等功能,C语言修改多增加不重名函数也可以实现。
这里直接上源码:

LinkStack.h

//
// Created by Whisky on 2023/2/9.
//
#ifndef DATA_STRUCTURE_LINKSTACK_H
#define DATA_STRUCTURE_LINKSTACK_H
#include 

struct LinkStack {
public:
    typedef double ElemType;
    typedef char OpType;
private:
    struct Node {
        ElemType data = 0.0;
        OpType op = 0;
        bool is_data = true;
        Node *next = nullptr;
    };
    void check();
public:
    void push(const ElemType &);
    void push(const OpType &);
    void pop();
    void clear();
    Node *top = nullptr;
    unsigned int size = 0;
};

#endif //DATA_STRUCTURE_LINKSTACK_H

LinkStack.cpp

//
// Created by Whisky on 2023/2/9.
//

#include "LinkStack.h"
#include 
#include 

void LinkStack::push(const LinkStack::ElemType &e) {
    Node *p = static_cast<Node *>(malloc(sizeof (Node)));
    p->is_data = true;
    p->data = e;
    p->next = top;
    top = p;
    ++size;
}

void LinkStack::push(const LinkStack::OpType &e) {
    Node *p = static_cast<Node *>(malloc(sizeof (Node)));
    p->is_data = false;
    p->op = e;
    p->next = top;
    top = p;
    ++size;
}

void LinkStack::pop() {
    check();
    Node *p = top;
    top = top->next;
    free(p);
    --size;
}

void LinkStack::check() {
    try {
        if (top == nullptr)
            throw std::underflow_error("the stack is empty");
    }
    catch (std::exception &err)
    {
        std::cerr << err.what() << "\n";
        assert(0);
    }
}

void LinkStack::clear() {
    while (top != nullptr) {
        Node *p =top;
        top = top->next;
        free(p);
    }
}

2 队列

我们的队列的节点的数据域也设计为两种类型:

struct Node {
        ElemType data = 0;
        OpType op = 0;
        bool is_data = true;
        Node *next = nullptr;
    };

其他与一般链式队列无异。
这里也直接上源码:

LinkQueue.h

//
// Created by Whisky on 2023/2/9.
//

#ifndef DATA_STRUCTURE_LINKQUEUE_H
#define DATA_STRUCTURE_LINKQUEUE_H
#include 

struct LinkQueue {
public:
    typedef double ElemType;
    typedef char OpType;
    unsigned int length();
    LinkQueue() { front = rear = &node; };
    void push(const ElemType &);
    void push(const OpType &);
    void pop();
private:
    struct Node {
        ElemType data = 0;
        OpType op = 0;
        bool is_data = true;
        Node *next = nullptr;
    };
    Node node;
public:
    Node *front = nullptr;
    Node *rear = nullptr;

};
#endif //DATA_STRUCTURE_LINKQUEUE_H

LinkQueue.cpp

//
// Created by Whisky on 2023/2/9.
//

#include "LinkQueue.h"
#include 

void LinkQueue::push(const LinkQueue::ElemType &e) {
    Node *p = static_cast<Node *>(malloc(sizeof (Node)));
    assert(!(p == nullptr));//申请失败
    p->data = e;
    p->is_data = true;
    p->next = nullptr;
    rear->next = p;
    rear = p;
    ++node.data;
}

void LinkQueue::push(const LinkQueue::OpType &o) {
    Node *p = static_cast<Node *>(malloc(sizeof (Node)));
    assert(!(p == nullptr));//申请失败
    p->op = o;
    p->is_data = false;
    p->next = nullptr;
    rear->next = p;
    rear = p;
    ++node.data;
}

void LinkQueue::pop() {
    assert(!(front == rear));//空了
    Node *p = front->next;
    ElemType tmp = p->data;
    front->next = p->next;
    if (rear == p)
        rear = front;
    free(p);
    --node.data;
}

unsigned int LinkQueue::length() {
    return node.data;
}

四、设计计算器

完成了这基本数据结构的搭建,我们也可以开始设计计算器了。

我们首先定义大致结构,后续功能根据实际编程添加:

class Calculator {
public:
    Calculator(std::istream &);
    Calculator() = default;
    void input(std::istream &);
    void print();
private:
    LinkStack cal_stack;
    LinkQueue cal_queue;
};

计算器利用input成员函数得到表达式,并使用print成员函数输出结果。
为了方便使用我们类的程序员使用,我们添加了重载构造函数,用来直接input。而且input的参数为std::istream &基本IO类,这让用户可以选择多种输入方式(文件、命令框、string字符串流等等)。

Calculator::Calculator(std::istream &is) {
    input(is);
}

1 提取字符串的数字

从IO获取一行数据并存入字符串:

	std::string line;
    getline(is, line);

我们通过遍历字符串,对每个字符进行判断,连续数字就进行移位加合,遇到小数点就利用向右移位加合(这里的移位都是针对10进制的),遇到我们需要的字符就进行处理:

for (auto it = line.begin(); it != line.end() && running; ++it)
{
      double sum = 0.0;
       bool float_flag = false;
       float sum2 = 0.0;
       double power = 0.1;
       while (isdigit(*it) || (*it == '.'))
       {
           if (isdigit(*it)) {
               if (float_flag) {
                   sum2 += power * ((*it) - '0');
                   power *= 0.1;
               }
               else sum = sum * 10 + (*it) - '0';
           }
           else {
               float_flag = true;
           }
           is_dight = true;
           ++it;
           if  (it == line.end())
               running = false;
       }
       sum += sum2;//整数加小数部分
       if (is_dight)
       {
           //sum为数字,等待处理
           is_dight = false;
       }
       if (*it == '+'|| *it == '-'|| *it == '*'||
           *it == '/'|| *it == '('|| *it == ')')
       {
          //*it为操作符,等待处理
       }
}

2 进栈处理

规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终得到后缀表达式为止。

按照之前原理中的方法,我们把得到的数和符号利用栈的特性处理,把结果放进队列。

判断优先级:

#define PRI_HIGH 2
#define PRI_LOW 1

int priority(char ch) {
    if (ch == '*' || ch == '/')
        return PRI_HIGH;
    else
        return PRI_LOW;
}

进栈处理并输出到队列:

void Calculator::input(std::istream &is) {
    std::string line;
    getline(is, line);
    bool is_dight = false;
    bool running = true;
    bool begin_flag = true;
    for (auto it = line.begin(); it != line.end() && running; ++it)
    {
        double sum = 0.0;
        bool float_flag = false;
        float sum2 = 0.0;
        double power = 0.1;
        while (isdigit(*it) || (*it == '.'))
        {
            if (isdigit(*it)) {
                if (float_flag) {
                    sum2 += power * ((*it) - '0');
                    power *= 0.1;
                }
                else sum = sum * 10 + (*it) - '0';
            }
            else {
                float_flag = true;
            }
            is_dight = true;
            ++it;
            if  (it == line.end())
                running = false;
        }
        sum += sum2;//整数加小数部分
        if (is_dight)
        {
            cal_queue.push(sum);//数字直接输出进队列
            is_dight = false;
        }
        if (*it == '+'|| *it == '-'|| *it == '*'||
            *it == '/'|| *it == '('|| *it == ')')//符号借用栈的特性解决
        {
            if (cal_stack.top == nullptr || begin_flag)
            {
                begin_flag = false;
                cal_stack.push(*it);//第一个直接放入栈
            }
            else
            {
                if (*it == ')' || *it == '(')
                {
                    if (*it == '(')
                        begin_flag = true;//左括号之后的符号直接放入栈,避免与左括号比较
                    cal_stack.push(*it);
                }
                else if (priority(*it) > priority(cal_stack.top->op))//符号优先级高于栈顶符号
                    cal_stack.push(*it);
                else {
                	//判断符号与栈顶符号的优先级,优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出(以括号为一个层级),并将当前符号进栈
                    while (cal_stack.top != nullptr)
                    {
                    	if (cal_stack.top->op == '(')
                            break;
                        cal_queue.push(cal_stack.top->op);
                        cal_stack.pop();
                    }
                    cal_stack.push(*it);
                }
            }
            if (*it == ')')
            {
            //遇到右括号,去匹配左括号,同时把栈里面的符号都输出
                while (true)
                {
                    char tmp = cal_stack.top->op;
                    if (tmp != '(' && tmp != ')')
                        cal_queue.push(tmp);
                    cal_stack.pop();
                    if (tmp == '(')
                        break;
                }
            }
        }
    }
    //剩下的依次出栈
    while (cal_stack.size){
        cal_queue.push(cal_stack.top->op);
        cal_stack.pop();
    }
}

3 后缀计算

规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。

用于计算的函数(这里没有处理除数为0的情况,大家可以自行补充):

double cal_operator(double a, char c, double b)
{
    switch (c) {
        case '+':
            return a + b;
        case '-':
            return a - b;
        case '*':
            return a * b;
        case '/':
            return a / b;
        default:
            return 0;
    }
}

后缀计算则非常简单了,遇到数字进栈,遇到符号就运算(注意谁减谁或者谁除谁):

void Calculator::cal() {
    while (cal_queue.length() != 0)
    {
        if (cal_queue.front->next->is_data)//数字进栈
        {
            cal_stack.push(cal_queue.front->next->data);
        }
        else {//符号运算
            char &op = cal_queue.front->next->op;
            double tmp1, tmp2;
            tmp1 = cal_stack.top->data;cal_stack.pop();
            tmp2 = cal_stack.top->data;cal_stack.pop();
            tmp2 =  cal_operator(tmp2, op, tmp1);
            cal_stack.push(tmp2);//运算完再进栈
        }
        cal_queue.pop();
    }
}

4 输出结果

输出结果就是出栈就行了:

void Calculator::print_stack() {
    while (cal_stack.top)
    {
        if (cal_stack.top->is_data)
            std::cout << cal_stack.top->data << " ";
        else
            std::cout << cal_stack.top->op << " ";
        cal_stack.pop();
    }
    std::cout << std::endl;
}

虽然其实写一个cal_stack.top->data就行(因为最后栈里只有一个元素)。这些是笔者用于测试时,用于输出整个栈的代码。




结尾附上完整代码:

Calculator.h

//
// Created by Whisky on 2023/2/9.
//

#ifndef DATA_STRUCTURE_CALCULATOR_H
#define DATA_STRUCTURE_CALCULATOR_H
#include 
#include "LinkStack.h"
#include "LinkQueue.h"

class Calculator {
public:
    Calculator(std::istream &);
    Calculator() = default;
    void input(std::istream &);
    void print();
    void print_queue();
private:
    void cal();
    void print_stack();

#define PRI_HIGH 2
#define PRI_LOW 1
    LinkStack cal_stack;
    LinkQueue cal_queue;
    friend int priority(char);
};
int priority(char ch);

#endif //DATA_STRUCTURE_CALCULATOR_H

Calculator.cpp

//
// Created by Whisky on 2023/2/9.
//

#include "Calculator.h"

int priority(char ch) {
    if (ch == '*' || ch == '/')
        return PRI_HIGH;
    else
        return PRI_LOW;
}

double cal_operator(double a, char c, double b)
{
    switch (c) {
        case '+':
            return a + b;
        case '-':
            return a - b;
        case '*':
            return a * b;
        case '/':
            return a / b;
        default:
            return 0;
    }
}

void Calculator::cal() {
    while (cal_queue.length() != 0)
    {
        if (cal_queue.front->next->is_data)
        {
            cal_stack.push(cal_queue.front->next->data);
        }
        else {
            char &op = cal_queue.front->next->op;
            double tmp1, tmp2;
            tmp1 = cal_stack.top->data;cal_stack.pop();
            tmp2 = cal_stack.top->data;cal_stack.pop();
            tmp2 =  cal_operator(tmp2, op, tmp1);
            cal_stack.push(tmp2);
        }
        cal_queue.pop();
    }
}

void Calculator::print_queue() {
    while (cal_queue.front->next != nullptr) {
        if (cal_queue.front->next->is_data)
            std::cout << cal_queue.front->next->data << " ";
        else
            std::cout << cal_queue.front->next->op << " ";
        cal_queue.pop();
    }
    std::cout << std::endl;
}

void Calculator::print_stack() {
    while (cal_stack.top)
    {
        if (cal_stack.top->is_data)
            std::cout << cal_stack.top->data << " ";
        else
            std::cout << cal_stack.top->op << " ";
        cal_stack.pop();
    }
    std::cout << std::endl;
}

void Calculator::input(std::istream &is) {
    std::string line;
    getline(is, line);
    bool is_dight = false;
    bool running = true;
    bool begin_flag = true;
    for (auto it = line.begin(); it != line.end() && running; ++it)
    {
        double sum = 0.0;
        bool float_flag = false;
        float sum2 = 0.0;
        double power = 0.1;
        while (isdigit(*it) || (*it == '.'))
        {
            if (isdigit(*it)) {
                if (float_flag) {
                    sum2 += power * ((*it) - '0');
                    power *= 0.1;
                }
                else sum = sum * 10 + (*it) - '0';
            }
            else {
                float_flag = true;
            }
            is_dight = true;
            ++it;
            if  (it == line.end())
                running = false;
        }
        sum += sum2;
        if (is_dight)
        {
            cal_queue.push(sum);
            is_dight = false;
        }
        if (*it == '+'|| *it == '-'|| *it == '*'||
            *it == '/'|| *it == '('|| *it == ')')
        {
            if (cal_stack.top == nullptr || begin_flag)
            {
                begin_flag = false;
                cal_stack.push(*it);
            }
            else
            {
                if (*it == ')' || *it == '(')
                {
                    if (*it == '(')
                        begin_flag = true;
                    cal_stack.push(*it);
                }
                else if (priority(*it) > priority(cal_stack.top->op))
                    cal_stack.push(*it);
                else {
                    while (cal_stack.top != nullptr)
                    {
                        if (cal_stack.top->op == '(')
                            break;
                        cal_queue.push(cal_stack.top->op);
                        cal_stack.pop();
                    }
                    cal_stack.push(*it);
                }
            }
            if (*it == ')')
            {
                while (true)
                {
                    char tmp = cal_stack.top->op;
                    if (tmp != '(' && tmp != ')')
                        cal_queue.push(tmp);
                    cal_stack.pop();
                    if (tmp == '(')
                        break;
                }
            }
        }
    }
    while (cal_stack.size){
        cal_queue.push(cal_stack.top->op);
        cal_stack.pop();
    }

}

void Calculator::print() {
    cal();
    print_stack();
}

Calculator::Calculator(std::istream &is) {
    input(is);
}

.

你可能感兴趣的:(记录一次编程,c++,c语言,算法,数据结构,开发语言)