Evaluate Reverse Polish Notation
Bạn nhận được một biểu thức số học ở dạng ký pháp Ba Lan ngược, dưới dạng một mảng (array) các token. Mỗi toán tử đứng ngay sau hai toán hạng của nó, vì vậy 3 4 + có nghĩa là 3 + 4 và 3 4 + 2 * có nghĩa là (3 + 4) * 2, không cần dấu ngoặc đơn. Mỗi token là một số nguyên hoặc một trong các toán tử +, -, * và /.
Trả về giá trị của biểu thức. Phép chia cắt phần thập phân về phía 0: 7 / 2 là 3 và -7 / 2 là -3.
Hàm
- tokensstring-array
- các số và toán tử của biểu thức, theo thứ tự
- Trả vềinteger
- giá trị của biểu thức
Ràng buộc
1 ≤ tokens.length ≤ 104- Mỗi token là
+,-,*,/hoặc một số nguyên từ-200đến200được viết ở dạng thập phân, có dấu trừ ở đầu nếu là số âm. tokenslà một biểu thức theo ký pháp Ba Lan ngược (reverse Polish notation) hợp lệ.- Không xảy ra phép chia cho 0, và mọi giá trị trung gian cũng như giá trị cuối cùng đều lớn hơn
-231và nhỏ hơn231.
Ví dụ
- Đầu vào
- tokens = ["8", "3", "-", "4", "*"]
- Đầu ra
- 20
- Giải thích
- Phép trừ
-áp dụng lần lượt cho 8 rồi đến 3, nên cho kết quả là 5, không phải -5. Sau đó, phép nhân*nhân 5 với 4, cho kết quả là 20.
- Đầu vào
- tokens = ["6", "2", "9", "3", "/", "-", "*"]
- Đầu ra
- -6
- Giải thích
/dùng hai giá trị gần nhất: 9 chia cho 3 bằng 3. Sau đó,-tính 2 trừ 3, bằng -1, và*nhân 6 với -1.
- Đầu vào
- tokens = ["10", "-7", "2", "/", "+"]
- Đầu ra
- 7
- Giải thích
- Token
-7là một số, không phải toán tử. -7 chia cho 2 bằng -3.5, khi cắt cụt về phía 0 sẽ thành -3, không phải -4, và 10 cộng -3 bằng 7.
Câu hỏi mở rộng
Bạn có thể viết lại biểu thức bằng ký hiệu thông thường, chẳng hạn như (3 + 4) * 2, chỉ thêm dấu ngoặc đơn ở những chỗ làm thay đổi ý nghĩa không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Đọc các token từ trái sang phải. Khi gặp một toán tử, nó áp dụng lên hai giá trị nào? Hãy xem thứ tự tạo ra các giá trị đó.
Một toán tử luôn áp dụng cho hai giá trị gần nhất mà chưa có toán tử nào sử dụng, và kết quả của nó trở thành một giá trị mới cho các toán tử tiếp theo. “Giá trị gần nhất chưa được sử dụng” chính là thứ mà ngăn xếp cung cấp cho bạn.
Đẩy từng số vào ngăn xếp. Khi gặp toán tử, lấy toán hạng bên phải ra trước rồi lấy toán hạng bên trái ra, kết hợp chúng theo thứ tự đó và đẩy kết quả vào ngăn xếp. Khi hết token, ngăn xếp còn lại một giá trị: đáp án. Hãy bảo đảm phép chia làm tròn về 0.
Lời giải
Mỗi toán tử áp dụng cho hai giá trị ngay trước nó, và một trong hai giá trị đó có thể là kết quả của một toán tử đứng trước. Vì vậy, một ngăn xếp giá trị sẽ tính biểu thức chỉ trong một lượt. Những điểm dễ nhầm là thứ tự toán hạng với - và /, phân biệt toán tử - với số -7, và phép chia lấy phần nguyên.
Thu gọn toán tử đầu tiên, lặp lại
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Tìm toán tử nằm ngoài cùng bên trái. Không có toán tử nào đứng trước nó, vì vậy hai token đứng trước nó là các số, tức là các toán hạng của nó. Thay ba token đó bằng kết quả của chúng. Các toán tử phía sau sẽ thấy giá trị đó ở vị trí của phần vừa thay thế, nên biểu thức vẫn giữ nguyên ý nghĩa. Lặp lại cho đến khi chỉ còn một số.
Mỗi lượt, hãy tìm lại từ đầu. Với 5,000 số theo sau là 4,999 toán tử, toán tử đầu tiên nằm khoảng giữa dãy trong mỗi lượt, nên riêng các lần tìm kiếm đã đọc khoảng 1.25 × 10^7 token.
Thuật toán
- Sao chép các token vào một danh sách mà bạn có thể thay đổi.
- Quét từ đầu đến toán tử đầu tiên, ở vị trí
k. - Áp dụng toán tử đó cho các số ở vị trí
k-2(bên trái) vàk-1(bên phải). - Thay ba token ở
k-2,k-1vàkbằng kết quả. - Lặp lại cho đến khi còn một token, rồi trả về token đó dưới dạng số.
def evalRPN(tokens):
items = list(tokens)
while len(items) > 1:
# Find the first operator. Everything to its left is a plain number.
k = 0
while items[k] not in ("+", "-", "*", "/"):
k += 1
left, right, op = int(items[k - 2]), int(items[k - 1]), items[k]
if op == "+":
value = left + right
elif op == "-":
value = left - right
elif op == "*":
value = left * right
else:
value = int(left / right) # int() truncates toward zero
# Replace the three tokens "left right op" with the number they stand for.
items[k - 2:k + 1] = [str(value)]
return int(items[0])Một lượt duyệt với ngăn xếp giá trị
Ý tưởng
Giữ các giá trị ở bên trái toán tử trên ngăn xếp thay vì đọc lại chúng. Một số được đưa vào ngăn xếp. Toán tử lấy ra hai giá trị, kết hợp chúng rồi đưa kết quả vào ngăn xếp, nơi kết quả chờ toán tử tiếp theo như mọi giá trị khác. Ngăn xếp luôn chứa các giá trị của những phần hoàn chỉnh đã đọc được cho đến lúc đó, theo đúng thứ tự, nên toán tử luôn áp dụng lên hai giá trị trên cùng. Giá trị trên cùng là toán hạng bên phải vì nó được tạo ra sau cùng.
Trong 6 2 9 3 / - *, / lấy ra 3 rồi 9 và đưa 3 vào ngăn xếp, - lấy ra 3 rồi 2 và đưa -1 vào ngăn xếp, còn * đưa 6 × -1 = -6 vào ngăn xếp. Mỗi token được xử lý một lần: thời gian O(n) và bộ nhớ O(n).
Thuật toán
- Bắt đầu với một ngăn xếp trống.
- Với mỗi token là một số, đẩy giá trị của nó vào ngăn xếp.
- Với mỗi toán tử, lấy toán hạng bên phải ra trước, rồi lấy toán hạng bên trái ra.
- Tính
left op right, làm tròn về 0 khi chia bằng/, rồi đẩy kết quả vào ngăn xếp. - Sau token cuối cùng, trả về giá trị duy nhất trên ngăn xếp.
def evalRPN(tokens):
stack = [] # values of the parts read so far, the newest on top
for token in tokens:
if token in ("+", "-", "*", "/"):
# The right operand was pushed last, so it comes off first.
right = stack.pop()
left = stack.pop()
if token == "+":
stack.append(left + right)
elif token == "-":
stack.append(left - right)
elif token == "*":
stack.append(left * right)
else:
# int() truncates toward zero; // would round -7 / 2 down to -4.
stack.append(int(left / right))
else:
stack.append(int(token))
return stack[0]
Lỗi thường gặp và trường hợp biên
- Đổi chỗ các toán hạng. Lần pop đầu tiên là toán hạng bên phải:
["3", "5", "-"]cho kết quả -2, còn["2", "9", "/"]cho kết quả 0, không phải 4. - Nhận diện toán tử dựa trên ký tự đầu tiên.
-7bắt đầu bằng dấu trừ nhưng lại là một số, vì vậy hãy so sánh toàn bộ token. - Làm tròn xuống thay vì cắt phần thập phân.
//của Python,/của Ruby,%/%của R vàmath.floorcủa Lua biến-7 / 2thành -4 và-1 / 3thành -1. - In ra
-0. Trong JavaScript và Lua,0 * -5vàMath.trunc(-1 / 3)cho kết quả âm không. Hãy cộng 0 vào giá trị cuối cùng.
Câu hỏi thường gặp4
Độ phức tạp thời gian của Evaluate Reverse Polish Notation là bao nhiêu?
Lời giải dùng ngăn xếp chạy trong thời gian O(n) với n token: mỗi số được đẩy vào một lần, còn mỗi toán tử thực hiện hai lần lấy ra và một lần đẩy vào. Ngăn xếp chứa tối đa khoảng n/2 giá trị, nên độ phức tạp không gian là O(n).
Vì sao ký pháp Ba Lan ngược không cần dấu ngoặc đơn?
Trong ký hiệu thông thường, 3 + 4 * 2 cần có quy tắc ưu tiên hoặc dấu ngoặc để xác định phép toán nào được thực hiện trước. Trong ký pháp Ba Lan ngược, toán tử luôn áp dụng cho hai giá trị đứng ngay trước nó, nên thứ tự token quyết định kết quả: 3 4 2 * + bằng 11 và 3 4 + 2 * bằng 14.
Làm thế nào để chia và cắt phần thập phân về 0 trong Python?
Dùng int(a / b). Toán tử // làm tròn xuống, nên -7 // 2 là -4, còn int(-7 / 2) là -3. Phép chia số thực đủ chính xác với các giá trị 32 bit. Với số nguyên lớn hơn, hãy chia các giá trị tuyệt đối bằng // rồi thêm lại dấu.
Làm thế nào để chuyển một biểu thức thông thường thành ký pháp Ba Lan ngược?
Thuật toán shunting-yard thực hiện việc này trong một lượt bằng một ngăn xếp chứa các toán tử. Các số được chuyển thẳng đến đầu ra. Trước khi đưa một toán tử vào ngăn xếp, hãy chuyển mọi toán tử đang nằm trên ngăn xếp có độ ưu tiên cao hơn hoặc bằng nó đến đầu ra. Dấu ngoặc đóng chuyển các toán tử ra ngoài cho đến dấu ngoặc mở tương ứng. Cuối cùng, chuyển phần còn lại đến đầu ra.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def evalRPN(tokens):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
tokens = ["8", "3", "-", "4", "*"]
Đầu ra mong đợi
20