Reverse Polish Notation
Trang 1Reverse Polish Notation - Thuật toán tính giá trị biểu thức
Ngô Minh Đức
Reverse Polish Notation (ký pháp nghịch đảo Ba Lan) là một loại ký pháp toán học dùng
để biểu thị biểu thức đại số, rất thuận lợi trong giải thuật tính giá trị biểu thức, có nghĩa là bạn nhập vào một chuỗi, chẳng hạn ″(1+2)*3″, chương trình sẽ tính ra kết qủa bằng 9 Reverse Polish Notation (gọi tắt là RPN) có hai loại, tiền tố (preffix) và hậu tố (suffix), trong bài này tôi chỉ đề cập đến dạng suffix.
Để hiểu rõ hơn và dễ dàng tiếp cận với thuật toán này trước tiên chúng ta xét một ví dụ đơn giản sau đây:
Chẳng hạn xét biểu thức sau:
(1 + 2) x 3 thì dạng RPN - hậu tố - của biểu thức trên là: 1 2 + 3 x
Chương trình có thể tính toán rất dễ dàng với biểu thức dạng RPN Bạn để ý trong biểu thức RPN không còn dấu ngoặc nữa, khi tính toán chỉ cần duyệt các toán tử từ trái sang phải và thực hiện với các toán hạng đứng trước nó
Đây là mô tả qúa trình tính toán biểu thức RPN trên:
Bước 1: Duyệt đến dấu +, ngừng lại.
Bước 2: Cộng hai toán hạng đứng trước nó: 1+2=3.
Bước 3: Xóa các phần tử 1, 2, + thay bằng số 3
Quay lại bước 1: Duyệt đến dấu x, ngừng lại
Quay lại bước 2: Nhân hai toán hạng đứng trước nó: 3x3=9
Quay lại bước 3: Thay các phần tử 3, 3, x bởi số 9 Chỉ còn một phần tử nên kết thúc Kết qủa chính là phần tử duy nhất còn lại: Đó là số 9
Do đó vấn đề chỉ còn là chuyển biểu thức từ dạng thông thường sang dạng RPN Sau đây tôi sẽ trình bày giải thuật chuyển đổi dưới hai phần
1 Phần cơ bản.
Giải thuật này do các bạn trên TTVNOnline (www.ttvnol.com) và Diễn Đàn Tin Học (www.diendantinhoc.com) cung cấp
Xin các bạn chú ý một điều là: các phần tử trong biểu thức được chia ra làm hai loại: toán hạng (số) và toán tử (bao gồm dấu và hàm)
Trong các loại toán tử, chúng ta cần lưu ý đến toán tử cộng trừ một ngôi (unary
plus/minus) Đây là một loại toán tử chỉ tác dụng lên một toán hạng đứng trước nó, khác với toán tử cộng trừ bình thường tác dụng lên hai toán hạng đứng trước nó
ví dụ: -2 + 5 thì ″-″ là toán tử một ngôi, ″+″ là toán tử bình thường
Để xác định ″+″, ″-″ là một ngôi hay hai ngôi, khi duyệt biểu thức ta chỉ cần xem phần tử đứng trước nó là số hay là một toán tử khác
Mức ưu tiên của các toán tử (từ nhỏ đến lớn): (, +, -, *, /, ^, + một ngôi, - một ngôi
Thuật toán:
Ta sử dụng hai stack (ngăn xếp): rpnStack dùng để lưu dạng RPN, oprStack dùng để tạm lưu các toán tử trong qúa trình xử lý Đọc lần lượt từ đầu đến cuối biểu thức để xử lý theo từng loại:
Dấu mở ngoặc: đưa vào oprStack
Toán hạng: đưa vào rpnStack
Trang 2Toán tử: Trong trường hợp này ta phải xét toán tử đang ở trên cùng (được đưa vào cuối cùng) trong oprStack: nếu mức ưu tiên cao hơn toán tử đang xét thì chuyển toán tử đó sang rpnStack; tiếp tục làm như vậy cho đến khi được toán tử có mức ưu tiên nhỏ hơn hoặc bằng toán tử đang xét; cuối cùng đưa toán tử đang xét vào oprStack
Dấu đóng ngoặc: lần lượt chuyển các toán tử được lưu trong oprStack sang rpnStack; cho đến khi gặp dấu mở ngoặc thì dừng lại và xóa dấu mở ngoặc đó khỏi oprStack
Lưu ý: Khi viết chương trình chỉ duyệt được từng ký tự, do đó phải thêm phần nhận biết
nhóm ký tự hợp thành một số (hoặc một tên biến)
Ví dụ: 564 + 4, khi bắt đầu duyệt từ ký tự ″5″ sẽ nhận biết luôn số ″564″ và nhảy đến ký tự thứ tư
Đây là những bước cơ bản của thuật toán, sau khi chuyển đổi xong, bạn đọc từ đầu đến cuối (từ dưới lên trên) trong rpnStack sẽ thu được dạng RPN
Sau khi thu được dạng RPN, cách tính toán như sau:
Tìm toán tử đầu tiên trong rpnStack và áp dụng tính với các toán hạng đứng trước nó Sau
đó, xóa toán tử và các toán hạng đã tính, thay bằng kết qủa tính được
Tiếp tục cho đến khi nào không còn toán tử để tính nữa, kết qủa của biểu thức chính là phần tử đầu tiên của rpnStack
2 Phần mở rộng.
Trên đây chỉ là các bước cơ bản, tùy theo sự khéo léo của bạn mà có thể cải tiến giải thuật
để tính được những biểu thức phức tạp hơn Tôi xin trình bày một số mở rộng về giải thuật
để tính phân số, hỗn số và tính hàm nhiều tham số (chẳng hạn max(1,2,3) =3)
* Tính phân số, hỗn số
Ta đặt thêm hai toán tử, gọi là oprFraction (dấu phân số, giả sử là ″~″) và oprMixed (dấu hỗn số)
oprMixed chỉ là một toán tử ″ảo″ được thêm vào cho thuận lợi trong qúa trình xử lý
Độ ưu tiên của toán tử: (, +, -, *, /, ~, ^, + một ngôi, - một ngôi)
Trước khi đưa toán tử phân số vào rpnStack ta cần xét phần tử trên cùng trong rpnStack như sau:
Nếu cũng là toán tử phân số: gộp toán tử này và toán tử phân số đang xét thành toán tử hỗn số
Nếu là toán tử hỗn số: cho chương trình báo lỗi
Trong trường hợp còn lại: đưa toán tử phân số vào rpnStack bình thường
Thêm phần xử lý dấu phân số và dấu hỗn số vào thủ tục tính toán như sau:
Dấu phân số: gọi hàm khởi tạo fraction (gọi chung cho phân số/hỗn số) từ hai toán hạng đứng trước
Dấu hỗn số: gọi hàm khởi tạo fraction từ ba toán hạng đứng trước
Fraction có thể được quy định theo kiểu String (vd ″1~3″, ″17~18″,″1~1~2″.…)
Trong khi tính toán với fraction nên đổi hết ra phân số (chỉ gồm tử và mẫu), đến khi xuất kết qủa mới đổi thành hỗn số hoặc giữ nguyên tùy theo lựa chọn của người dùng
Lúc này, khi thực hiện các phép toán + - * /, có thể làm như sau (theo kiểu máy tính Casio):
Ta xét hai toán hạng của phép tính:
Nếu gồm một số lẻ thập phân: đổi hết ra số thập phân (nếu toán tử còn lại là phân số) và tính
Nếu chỉ gồm số nguyên và phân số: tính theo phân số
Bạn phải tự viết một module để xử lý phân số (cộng, trừ, nhân, chia, khởi tạo, )
Để xử lý loại biểu thức phức tạp hơn (chẳng hạn (1~3)~2 = 1~6) thì cần phải thêm một số
Trang 3bước nữa Bạn có thể liên hệ với tôi để nhận đựơc mã nguồn.
* Hàm nhiều tham số
Phần này trình bày cách xử lý các hàm nhiều tham số, chẳng hạn như max(1,2,3) hay uscln(5,6,12,30),v.v…
Ta đặt thêm một toán tử gọi là oprComma (dấu phẩy), độ ưu tiên chỉ đứng trên dấu mở ngoặc (áp chót) Trong khi chuyển đổi cũng xử lý với oprComma như những toán tử khác
Để mô tả thuật giải, ta xét ví dụ sau: max(1,2,3)
Sau công đoạn chuyển đổi ta có: 1 2 3 , , max
Khi gặp dấu ″,″ ta gộp hai toán hạng đứng trước nó vào một mảng
Như vậy sau bước 1 ta có: 1(2,3), max (ký hiệu (2,3) là chỉ mảng)
Gặp dấu ″,″ tiếp theo ta gộp luôn phần tử đầu tiên vào phần tử ″mảng″ thứ hai:
Như vậy sau bước 2 ta có (1,2,3) max
Do đó hàm ″max″ đã trở nên được thực hiện trên một tham số duy nhất, tham số đó lại là một mảng các đối số Để tính toán được ta phải xây dựng các hàm xử lý trên mảng đối số
Lưu ý: Cách làm này chưa hay và không thích hợp với những trình biên dịch như Turbo Pascal Bạn có thể liên hệ với tôi theo địa chỉ: attilathehunvn @yahoo.com để nhận được
mã nguồn (bằng VB và VB NET) Tôi mô tả giải thuật này trong một class, gọi là
″Evaluator″, bao gồm một thủ tục chính là Eval() Bạn có thể gọi thủ tục này từ bất kỳ module nào, chẳng hạn Eval(″1~3+2″) sẽ cho kết qủa là ″2~1~3″
Class này được áp dụng trong chương trình Quick Calculator 1.0 của tác giả Rất mong được các bạn giúp đỡ để hoàn thiện thêm giải thuật