簡單實用算法——計算數學表達式


算法概述

變量定義: str-數學表達式
注:數學表達式的數值支持小數,符號只支持+ - * / ( )這幾種。

計算原理::先將數學表達式的字符串(中綴表達式)轉化為后綴表達式,然后計算后綴表達式的值。
注:為了運算結果的精度,運算過程中統一使用decimal類型的數據。

例:輸入表達式"10*1.1/(2+8)+1.1+2.2-4.3",輸出結果“0.1”。

算法代碼(C#)

代碼如下:

//測試算法
class Program
{
    static void Main(string[] args)
    {
        string str = "10*1.1/(2+8)+1.1+2.2-4.3";
        decimal res = Calculator.Calculate(str);
        Console.WriteLine(str+"="+res);
        Console.ReadLine();
    }        
}

/// <summary>
/// 計算數學表達式,基於后綴表達式的實現,可使用 + - * / ( ) 運算符
/// </summary>
class Calculator 
{
    /// <summary>
    /// 計算數學表達式的值
    /// </summary>
    /// <param name="str">數學表達式</param>
    /// <returns></returns>
    public static decimal Calculate(string str)
    {            
        try
        {
            Queue<string> queue = CreateRPN(str);
            decimal res = ParseRPN(queue);
            return res;
        }
        catch (OverflowException)
        {
            throw new Exception("數據過大導致計算溢出");
        }
        catch (Exception)
        {
            throw new Exception("無法計算錯誤的表達式");
        }
            
    }

    //生成后綴表達式
    private static Queue<string> CreateRPN(string str)
    {
        //臨時存儲+ - * / ( 符號的棧
        Stack<char> stack = new Stack<char>();
        //存儲后綴表達式的隊列
        Queue<string> queue = new Queue<string>();
        for (int i = 0; i < str.Length; )
        {
            //如果是空格直接跳過
            if (str[i] == ' ')
            {
                i++;
                continue;
            }
            else if ((str[i] >= '0' && str[i] <= '9') || (str[i] == '.'))
            {
                //當前數
                decimal cur = 0;
                //小數標識
                bool isDecimal = false;
                //小數位數
                int num = 0;
                //特別要注意i < s.length這個條件
                while (i < str.Length && ((str[i] >= '0' && str[i] <= '9') || (str[i] == '.')))
                {
                    if (str[i] == '.')
                    {
                        isDecimal = true;
                    }
                    else
                    {
                        if (!isDecimal)
                        {
                            cur = cur * 10 + str[i] - '0';
                        }
                        else
                        {
                            num++;
                            cur = cur + ((decimal)(str[i] - '0')) / (decimal)(Math.Pow(10, num));
                        }
                    }
                    i++;
                }
                queue.Enqueue(cur.ToString());
            }
            else if (str[i] == ')')
            {
                //如果是 " )"那么需要彈出棧中的操作符號,並且把它加入到后綴表達式的隊列中
                //一直到遇到符號棧中的 " ( " 為止
                while (stack.Count != 0 && stack.Peek() != '(')
                {
                    queue.Enqueue(stack.Pop() + "");
                }
                stack.Pop();
                i++;
            }
            else
            {
                //可能是 +  -  *  / 這些符號或者是左括號
                //這個時候需要判斷符號棧中的棧頂元素與當前遍歷到的字符的優先級的問題
                while (stack.Count != 0 && Compare(stack.Peek(), str[i]) < 0)
                {
                    queue.Enqueue(stack.Pop() + "");
                }
                stack.Push(str[i]);
                i++;
            }
        }
        while (stack.Count != 0)
        {
            queue.Enqueue(stack.Pop() + "");
        }
        return queue;
    }

    //處理符號優先級
    private static int Compare(char peek, char c)
    {
        if (peek == '(' || c == '(') return 1;
        if (c == '+' || c == '-') return -1;
        if (c == '*' && (peek == '*' || peek == '/')) return -1;
        if (c == '/' && (peek == '*' || peek == '/')) return -1;
        return 1;
    }

    //解析后綴表達式
    private static decimal ParseRPN(Queue<string> queue)
    {
        //結果棧
        Stack<decimal> res = new Stack<decimal>();
        while (queue.Count != 0)
        {
            String t = queue.Dequeue();
            if (t.Equals("+") || t.Equals("-") || t.Equals("*") || t.Equals("/"))
            {
                decimal a = res.Pop();
                decimal b = res.Pop();
                decimal result = Calculate(b, a, t);
                res.Push(result);
            }
            else
            {
                res.Push(decimal.Parse(t));
            }
        }
        return res.Pop();
    }

    //基本運算單元
    private static decimal Calculate(decimal a, decimal b, String t)
    {
        //計算
        if (t.Equals("+"))
        {
            return a + b;
        }
        else if (t.Equals("-"))
        {
            return a - b;
        }
        else if (t.Equals("*"))
        {
            return a * b;
        }
        else
        {
            return a / b;
        }
    }
}

注:上面的代碼簡單擴展一下即可支持更復雜的運算符

算法實現

中綴表達式轉化為后綴表達式規則:從左到右遍歷中綴表達式的每個數字和符號,若是數字就輸出,即成為后綴表達式的一部分;若是符號,則判斷其與棧頂符號的優先級,是右括號或優先級低於找頂符號(乘除優先加減)則棧頂元素依次出找並輸出,並將當前符號進棧,一直到最終輸出后綴表達式為止。

例:中綴表達式“9+(3-1)3+10/2”轉化為后綴表達式“9 3 1-3+ 10 2/+”。

后綴表達式的計算過程規則:從左到右遍歷表達式的每個數字和符號,遇到是數字就進棧,遇到是符號,就將處於棧頂兩個數字出棧,進行運算,運算結果進棧,一直到最終獲得結果。

擴展:使用DataTable.Compute計算

一種更簡單的方法,使用DataTable.Compute計算數學表達式,代碼如下:

//測試算法
static void Main(string[] args)
{
    Console.WriteLine(Calculate("10*1.1/(2+8)+1.1+2.2-4.3"));
    Console.WriteLine(Calculate(double.MaxValue+"+"+double.MaxValue));  
    Console.ReadLine();
}

/// <summary>
/// 計算數學表達式的值
/// </summary>
/// <param name="str">數學表達式</param>
/// <returns>計算結果</returns>
private static double Calculate(string str)
{
    try
    {
        DataTable dt = new DataTable();
        double result = double.Parse(dt.Compute(str, "").ToString());
        return result;
    }
    catch (OverflowException)
    {
        throw new Exception("數據過大導致計算溢出");
    }
    catch (Exception)
    {
        throw new Exception("無法計算錯誤的表達式");
    }            
}

*注:DataTable.Compute計算的結果有decimal、double兩種(已測試出的),個人猜測在decimal取值范圍內的運算不會發生double運算的精度損失,但計算結果只能用范圍較大的double類型表示。

目前來看,DataTable.Compute計算數學表達式的適用范圍更全面一些

擴展:使用SQL計算數學表達式

可以通過執行SQL語句得到數學表達式的結果,SQL語句如下:

string strSQL="SELECT "+"10*1.1/(2+8)+1.1+2.2-4.3";

使用SQL語句的好處是可以計算含有開方、平方等更高級運算的數學表達式,最簡單、方便的是使用使用SQLite(沒有數據庫的通訊開銷)數據庫來計算數學表達式,代碼如下:

/// <summary>
/// 計算給定的表達式
/// </summary>
/// <param name="expr">表達式</param>
/// <returns></returns>
public static object SQLiteCompute(string expr)
{            
    expr = expr.Replace("/", "*1.0/");
    expr = expr.Replace("[", "(");
    expr = expr.Replace("]", ")");
    expr = expr.Replace("{", "(");
    expr = expr.Replace("}", ")");            

    string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DataBass");
    if (!Directory.Exists(path)) Directory.CreateDirectory(path);

    string connStr = Path.Combine(path, "ComputeEngine.db");
    if (!File.Exists(connStr)) File.Create(connStr).Close();

    using (SQLiteConnection conn = new SQLiteConnection("Data Source=" + connStr))
    {
        if (conn.State != ConnectionState.Open)
            conn.Open();
        var cmd = new SQLiteCommand();
        
        cmd.Parameters.Clear();
        cmd.Connection = conn;
        cmd.CommandText = "SELECT " + expr;
        cmd.CommandType = CommandType.Text;
        cmd.CommandTimeout = 30;                
        return cmd.ExecuteScalar();
    }
}

使用方法:

static void Main(string[] args)
{
    string result = SQLiteCompute("sqrt(1+2)/[4+(1+1)/3]").ToString();
    Console.WriteLine(result);

    Console.ReadKey();
}

計算結果:

0.371153744479045

關於然后使用SQLite數據庫,可以參考C#中SQLite的使用及工具類

參考資料

堆棧實現計算數學表達式——CSDN
接觸后綴表達式(逆波蘭表示法)——Veda
將中綴表達式轉化為后綴表達式——Veda
圖解后綴表達式的計算過程——Veda
C#里如何計算一個表達式的值——CSDN


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM