Để tránh quên, tạm viết lại một số khái niệm vào đây để các bạn đỡ mất công tra, đồng thời thống nhất được các định nghĩa.
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 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 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.
Độ ư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:
1 | Xác định mức ưu tiên toán tử nằm trong chuỗi biểu thức (toantu)
|
3 | if (toantu := "*" OR toantu := "/" OR toantu == "%") |
5 | if (toantu := " " OR toantu == "-") |
Định dạng lại biểu thức Infix trước khi chuyển đổiCá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 yêu cầu đị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ử gọi là các
token.
Các phương thức kiểm tra toán tử và toán hạngTrong 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. Ví dụ viết
ab phải hiểu là a*b, viết
123 không được hiểu là 1*2*3.
1 | Chương_trình_con_Nhận_dạng_toán_tử_tên_là IsOperator(string str) |
3 | Trả_về_đúng_khi_str_thuộc_một_trong_(str, @" |-|*|/|%"); |
5 | Chương_trình_con_nhận_dạng_toán_hạng_là_chữ IsOperand(string str) |
7 | Trả_về_đúng_khi_str_chứa_ký_tự_đầu_tiên_trong_(str, @"^d $|^([a-z]|[A-Z])$"); |
Chuyển biểu thức Infix sang PostfixLý 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à đưa phần lấy đó
nối thêm 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ử xét các trường hợp:
- 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à nối thêm vào 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.</blockquote>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 / |