Algorithm – Chuyển biểu thức trung tố sang tiền tố và hậu tố bằng Stack

Các biểu thức đại số được sử dụng hằng ngày đều được biểu diễn dưới dạng trung tố (infix). Cách biểu diễn này rất dễ hiểu với con người vì hầu hết các toán tử (+, -, *, /) đều là toán tử hai ngôi và chúng phân cách giữa hai toán hạng với nhau. Tuy nhiên đối với máy tính, để tính được giá trị của một biểu thức đại số theo dạng này không đơn giản như ta vẫn làm. Để khắc phục điều đó, máy tính cần chuyển cách biểu diễn các biểu thức đại số từ trung tố sang một dạng khác là tiền tố hoặc hậu tố. Trong bài này tôi sẽ giới thiệu kĩ thuật để giúp máy tính thực hiện điều này thông qua ngôn ngữ minh họa C#.

Thế nào là biểu thức tiền tố, trung tố và hậu tố

Trong đoạn giới thiệu trên có lẽ bạn cũng hình dung được thế nào là biểu thức trung tố, hiểu đơn giản tức là toán tử sẽ được đặt giữa hai toán hạng, dĩ nhiên đây phải là toán tử hai ngôi. Vậy dựa vào vị trí của của toán tử, liệu ta có thể biểu diễn biểu thức đại số dưới dạng khác? Câu trả lời là được, và như đã nói, ta có ba cách là biểu thức tiền tố (prefix), trung tố (infix) và hậu tố (postfix). Hãy xem một chút giới thiệu về cách biểu diễn biểu thức tiền tố và hậu tố để hiểu rõ hơn về chúng.

-       Prefix: Biểu thức tiền tố được biểu diễn bằng cách đặt toán tử lên trước các toán hạng. Cách biểu diễn này còn được biết đến với tên gọi “ký pháp Ba Lan” do nhà toán học Ba Lan Jan Łukasiewicz phát minh năm 1920. Với cách biểu diễn này, thay vì viết x+y như dạng trung tố, ta sẽ viết +xy. Tùy theo độ ưu tiên của toán tử mà chúng sẽ được sắp xếp khác nhau, bạn có thể xem một số ví dụ ở phía sau phần giới thiệu này.

-       Postfix: Ngược lại với cách Prefix, tức là các toán tử sẽ được đặt sau các toán hạng. Cách biểu diễn này được gọi là “ký pháp nghịch đảo Ba Lan” hoặc được viết tắt là RPN (Reverse Polish notation), được phát minh vào khoảng giữa thập kỷ 1950 bởi một triết học gia và nhà khoa học máy tính Charles Hamblin người Úc.

 

Một số ví dụ:

Infix Prefix Postfix
x+y +xy xy+
x+y-z -+xyz xy+z -
x+y*z +x*yz xyz*+
x+(y-z) +x-yz xyz-+

 

Phương pháp chuyển từ biểu thức trung tố sang tiền tố và hậu tố

Có hai cách để chuyển một biểu thức từ trung tố sang hai loại còn lại đó là dùng:

-       Stack

-       Expression Tree (cây biểu thức)

Việc dùng Stack phổ biến hơn có ưu điểm là dễ cài đặt, đơn giản còn dùng Expression Tree sẽ giúp việc chuyển đổi được dễ hiểu và trực quan hơn tuy nhiên lại mất thời gian cài đặt. Trong bài viết này tôi sẽ chỉ trình bày kĩ thuật sử dụng Stack, kĩ thuật dùng Expression Tree sẽ được giới thiệu trong bài viết sau.

Việc cài đặt các thuật toán chuyển đổi này bằng C# có một lợi điểm là C# hỗ trợ sẵn các collection, ngoài ra chúng còn có khả năng truy vấn và lọc dữ liệu nếu như bạn dùng C# 3 trở lên. Bên cạnh đó kĩ thuật Regular Expression cũng được sử dụng để làm giảm các câu lệnh so sánh và thao tác chuỗi.

Độ ưu tiên của các toán tử

Một trong những điều quan trọng trước khi bắt đầu là phải tính toán được độ ưu tiên của các toán tử trong biểu thức nhập vào. Để đơn giản ta chỉ xét các toán tử hai ngôi và thường dùng bao gồm: multiply (+),subtract (-), multiply (*), divide (/), Modulo (%). Theo đó các toán tử “*, /, %” có cùng độ ưu tiên và cao hơn hai toán tử “+, -”. Như vậy ta có phương thức lấy độ ưu tiên toán tử như sau:

public static int GetPriority(string op)
{
    if (op == "*" || op == "/" || op == "%")
        return 2;
    if (op == "+" || op == "-")
        return 1;
    return 0;
}

Định dạng lại biểu thức Infix trước khi chuyển đổi

 

Các biểu thức Infix khi nhập vào có thể dư thừa các khoảng trắng, các kí tự không phù hợp hoặc viết sai cú pháp. Phần kiểm tra cú pháp bạn có thể làm riêng hoặc để luôn trong các thuật toán chuyển đổi. Trong phần này tôi chỉ trình bày việc định dạng để các phần tử của biểu thức (toán tử, toán hạng) phải được phân cách với nhau bằng một khoảng trắng. Các phần tử này tôi sẽ gọi là một token.

public static void FormatExpression(ref string expression)
{
    expression = expression.Replace(" ", "");
    expression = Regex.Replace(expression, @"\+|\-|\*|\/|\%|\^|\)|\(", delegate(Match match)
    {
        return " " + match.Value + " ";
    });
    expression = expression.Replace("  ", " ");
    expression = expression.Trim();
}

Như bạn thấy tôi đầu tiên tôi loại bỏ hết khoảng trắng khỏi chuỗi expression sau đó sử dụng phương thức Replace của Regular expression để tìm kiếm các toán tử và các dấu ngoặc đơn để thêm khoảng trắng vào hai đầu mỗi kí tự tìm thấy.

Phương thức Replace() tôi sử dụng trên có tham số thứ 3 là một delegate kiểu MatchEvaluator để xử lý các kết quả tìm được trong chuỗi. Cách viết trên tôi sử dụng kĩ thuật anonymous method, bạn có thể dùng lambda expression để viết và sử dụng phương thức Format() của lớp String để mã lệnh ngắn gọn hơn như sau:

expression = Regex.Replace(expression, @"\+|\-|\*|\/|\%|\^|\)|\(", match =>
    String.Format(" {0} ", match.Value)
);

Hai dòng sau dùng để cắt các khoảng trắng nằm sát nhau và các khoảng trắng nằm ở hai đầu chuỗi. Nguyên nhân có các khoảng trắng liền nhau là do trong biểu thức có thể sẽ xuất hiện các toán tử và dấu ngoặc đơn nằm sát nhau.

Các phương thức kiểm tra toán tử và toán hạng

 

Trong thuật toán chuyển đổi này ta cần có các phương thức kiểm tra xem một thành phần của chuỗi có phải là toán tử hoặc toán hạng không. Thay vì sử dụng các cấu trúc if hoặc switch dài dòng và bất tiện khi phát triển, ta sẽ dùng Regex để kiểm tra.

Ngoài ra vì chuỗi nhập vào là một biểu thức đại số, nên các toán hạng ta sẽ xét không chỉ là các chữ số mà còn có chữ cái từ a-z và A-Z.

Có một quy tắc nữa là khi dùng chữ cái thì chỉ cho phép duy nhất một chữ cái đại diện cho một toán hạng, còn khi dùng chữ số thì có thể nhiều chữ số ghép thành một toán hạng.

private static bool IsOperator(string str)
{
    return Regex.Match(str, @"\+|\-|\*|\/|\%").Success;
}
public static bool IsOperand(string str)
{
    return Regex.Match(str, @"^\d+$|^([a-z]|[A-Z])$").Success;
}

Chuyển biểu thức Infix sang Postfix

Lý do tôi trình bày thuật toán chuyển sang postfix trước vì thuật toán này phổ biến và dễ cài đặt hơn, bạn cũng sẽ thấy sự tương đồng của thuật toán này với thuật toán chuyển từ infix sang prefix khi tôi trình bày ở phần sau.

Thuật toán để chuyển một biểu thức Infix sang dạn Prefix:

Đọc từng token trong biểu thức infix từ trái qua phải, với mỗi token ta thực hiện các bước sau:

-       Nếu là toán hạng: cho ra output.

-       Nếu là dấu mở ngoặc “(“: cho vào stack

-       Nếu là dấu đóng ngoặc “)”: lấy các toán tử trong stack ra và cho vào output cho đến khi gặp dấu mở ngoặc “(“. (Dấu mở ngoặc cũng phải được đưa ra khỏi stack)

-       Nếu là toán tử:

  • Chừng nào ở đỉnh stack là toán tử và toán tử đó có độ ưu tiên lớn hơn hoặc bằng toán tử hiện tại thì lấy toán tử đó ra khỏi stack và cho ra output.
  • Đưa toán tử hiện tại vào stack

Sau khi duyệt hết biểu thức infix, nếu trong stack còn phần tử thì lấy các token trong đó ra và cho lần lượt vào output.

Hãy xem ví dụ sau để hiểu rõ hơn thuật toán này.

Chúng ta sẽ chuyển biểu thức A*B+C*((D-E)+F)/G từ dạng Infix sang dạng Postfix:

Token Stack Output
A {Empty} A
* * A
B * A B
+ + A B *
C + A B * C
* + * A B * C
( + * ( A B * C
( + * ( ( A B * C
D + * ( ( A B * C D
- + * ( ( - A B * C D
E + * ( ( - A B * C D E
) + * ( A B * C D E -
+ + * ( + A B * C D E -
F + * ( + A B * C D E - F
) + * A B * C D E - F +
/ + / A B * C D E - F + *
G + / A B * C D E - F + * G
{Empty} A B * C D E - F + * G / +

Dựa theo thuật toán trên, ta cài đặt một phương thức tương ứng trên C#.

public static string Infix2Postfix(string infix)
{
    FormatExpression(ref infix);

    IEnumerable<string> str = infix.Split(' ');
    Stack<string> stack = new Stack<string>();
    StringBuilder postfix = new StringBuilder();

    foreach (string s in str)
    {
        if (IsOperand(s))
            postfix.Append(s).Append(" ");
        else if (s == "(")
            stack.Push(s);
        else if (s == ")")
        {
            string x = stack.Pop();
            while (x != "(")
            {
                postfix.Append(x).Append(" ");
                x = stack.Pop();
            }
        }
        else// IsOperator(s)
        {
            while (stack.Count > 0 && GetPriority(s) <= GetPriority(stack.Peek()))
                postfix.Append(stack.Pop()).Append(" ");
            stack.Push(s);
        }
    }

    while (stack.Count > 0)
        postfix.Append(stack.Pop()).Append(" ");

    return postfix.ToString();
}

Chuyển biểu thức Infix sang Prefix

 

Không có nhiều sự khác biệt giữa việc chuyển từ Infix sang Prefix với Infix sang Postfix, chủ yếu là sự thay đổi thứ tự duyệt từ phải sang trái thay vì từ trái sang phải. Và thay vì duyệt theo hướng ngược lại như thế bạn có thể thực hiện một chuyển đổi nhỏ để đảo ngược biểu thức nhập vào.

Chú ý là tôi sử dụng “đảo ngược biểu thức” thay vì ”đảo ngược chuỗi”, việc đảo ngược này phải giữ nguyên được giá trị của các toán hạng, ví dụ bạn không thể đảo ngược 12 thành 21 được. Hơn nữa vì chuỗi đã đảo ngược nên các dấu ngoặc đơn cũng phải được hiểu ngược lại. Cụ thể ta có ví dụ sau:

Chuyển biểu thức Infix A*B+C*((D-E)+F)/G sang dạng Prefix

Đầu tiên ta đảo ngược biểu thức trên thành G/)F+)E-D((*C+B*A, sau đó ta thực hiện các bước trong thuật toán sau:

Đọc từng token trong biểu thức infix từ trái qua phải, với mỗi token ta thực hiện các bước sau:

-       Nếu là toán hạng: cho ra output.

-       Nếu là dấu đóng ngoặc “)“: cho vào stack

-       Nếu là dấu mở ngoặc “(”: lấy các toán tử trong stack ra và cho vào output cho đến khi gặp dấu đóng ngoặc “)“. (Dấu đóng ngoặc cũng phải được đưa ra khỏi stack)

-       Nếu là toán tử:

  • Chừng nào ở đỉnh stack là toán tử và toán tử đó có độ ưu tiên lớn hơn toán tử hiện tại thì lấy toán tử đó ra khỏi stack và cho ra output.
  • Đưa toán tử hiện tại vào stack

Sau khi duyệt hết biểu thức infix, nếu trong stack còn phần tử thì lấy các token trong đó ra và cho lần lượt vào output. Cuối cùng là đảo ngược biểu thức một lần nữa và ta sẽ thu được kết quả.

public static string Infix2Prefix(string infix)
        {
            List<string> prefix = new List<string>();
            Stack<string> stack = new Stack<string>();

            FormatExpression(ref infix);

            IEnumerable<string> enumer = infix.Split(' ').Reverse();

            foreach (string s in enumer)
            {
                if (IsOperand(s))
                    prefix.Add(s);
                else if (s == ")")
                    stack.Push(s);
                else if (s == "(")
                {
                    string x = stack.Pop();
                    while (x != ")")
                    {
                        prefix.Add(x);
                        x = stack.Pop();
                    }
                }
                else// if (IsOperator(s))
                {
                    while (stack.Count > 0 && GetPriority(s) < GetPriority(stack.Peek()))
                        prefix.Add(stack.Pop());
                    stack.Push(s);
                }
            }

            while (stack.Count > 0)
                prefix.Add(stack.Pop());

            StringBuilder str = new StringBuilder();
            for (int i = prefix.Count - 1; i >= 0; i--)
            {
                str.Append(prefix[i]).Append(" ");
            }

            return str.ToString();
   }

Ngoài cách viết lại nguyên một phương thức chuyển đổi như trên, ta có thể sử dụng lại phương thức Infix2Postfix() để chuyển đổi sau khi đảo ngược biểu thức infix, đồng thời đảo ngược hai kí tự “(“ và “)” cho nhau.

Một số điểm lưu ý

 

Một số biểu thức prefix và postfix có thể được tạo ra khác nhau nhưng thực chất giá trị của chúng là bằng nhau và biểu thức infix ban đầu cũng là tương đương nhau. Nguyên nhân là một biểu thức đại số có nhiều cách để để tính giá trị khi độ ưu tiên của các toán tử là bằng nhau:

Ví dụ bạn có thể tính x+y-z bằng cách nhóm chúng lại thành

 

(x+y)-z (1)

hoặc

 

x+(y-z) (2)

Thông thường chúng ta ưu tiên cách tính số (1) từ trái qua.

Điểm tạo nên sự khác biệt là cách chúng ta so sánh độ ưu tiên của các toán tử, ví dụ thay vì viết dấu “<” trong đoạn mã này, ta có thể sửa thành “<=”, giá trị của chúng vẫn không thay đổi:

//…
while (stack.Count > 0 && GetPriority(s) < GetPriority(stack.Peek()))

prefix.Add(stack.Pop());

stack.Push(s);

//…

Kiểm tra kết quả

Để kiểm tra kết quả có chính xác không, bạn có thể dùng một dịch vụ chuyển đổi trực tuyến tại địa chỉ http://scriptasylum.com/tutorials/infix_postfix/infix_postfix.html. Trang web cũng sẽ thực hiện tính toán giá trị của biểu thức sau khi chuyển đổi. Như kết quả của biểu thức dưới đây là 111.

infix2postfix_online

Ngoài ra bạn cũng có thể dùng chương trình Y2 Expression Converter Demo của tôi viết để minh họa việc chuyển đổi từ infix sang prefix và postfix.

Y2 Expression Converter Demo 1.0.0

Xem giới thiệu và tải Demo+Soucecode tại đây

http://yinyangit.wordpress.com

About these ads

24 thoughts on “Algorithm – Chuyển biểu thức trung tố sang tiền tố và hậu tố bằng Stack

  1. Pingback: Ký pháp nghịch đảo Balan và tính giá trị biểu thức « y5k72pk

  2. Pingback: Algorithm – Tạo và sử dụng cây biểu thức (expression tree) « Nguyễn Ngọc Vạn's Blog

  3. Pingback: Algorithm – Tính giá trị của biểu thức tiền tố và hậu tố « Nguyễn Ngọc Vạn's Blog

  4. Pingback: Viết code chuẩn như thế nào? | ► Trang chủ

  5. Pingback: Chuyển biểu thức từ dạng trung tố sang dạng hậu tố sử dụng Stack | Thanh's Blog

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s