Ưu tiên của hàm trong thuật toán Shunting-yard


9

Tôi đang làm việc thông qua thuật toán Shunting-yard , như được mô tả bởi wikipedia.

Mô tả thuật toán khi giao dịch với các toán tử như sau:

Nếu mã thông báo là toán tử, o1, thì:

trong khi có mã thông báo toán tử, o2, ở đầu ngăn xếp toán tử và

o1 is left-associative and its precedence is less than or equal to
that of o2, or

o1 is right associative, and has precedence less than that of o2,

sau đó bật o2 khỏi ngăn xếp toán tử, vào hàng đợi đầu ra;

đẩy o1 lên ngăn xếp toán tử.

Tuy nhiên, họ đưa ra ví dụ sau:

Đầu vào: sin max 2 3 / 3 * 3.1415

Khi thuật toán chạm vào /mã thông báo, mô tả về những gì sẽ xảy ra như sau:

Token |        Action       |   Output (in RPN) |   Operator Stack
...
/     | Pop token to output | 2 3 max           | / sin 
...

Họ đang bật mã thông báo chức năng maxra stackvà đưa vào queue. Theo thuật toán của họ, điều này dường như có nghĩa là mã thông báo hàm vừa là toán tử, vừa có độ ưu tiên thấp hơn /toán tử.

Không có lời giải thích nào về việc liệu đây có phải là trường hợp hay không. Vì vậy, đối với Shunting-yardthuật toán, ưu tiên của hàm là gì? Là chức năng phải hay trái liên kết? Hoặc là wikipedia chỉ là không đầy đủ / không chính xác?

Câu trả lời:


5

Tôi tin rằng câu trả lời trực tiếp chỉ đơn giản là các hàm không phải là toán tử. Từ trang bạn đã liên kết:

Nếu mã thông báo là mã thông báo chức năng, sau đó đẩy nó lên ngăn xếp.

Đây là tất cả những gì nó cần nói, vì trường hợp hàm (tiền tố thành postfix) đơn giản hơn nhiều so với trường hợp toán tử (infix to postfix).

Đối với các câu hỏi tiếp theo: Các khái niệm về quyền ưu tiên và kết hợp chỉ cần thiết vì sự mơ hồ kế thừa trong bất kỳ biểu thức nào có nhiều toán tử infix. Mã thông báo chức năng đã sử dụng ký hiệu tiền tố, vì vậy đơn giản là họ không gặp phải vấn đề đó. Bạn không cần phải biết liệu sinhoặc maxcó "ưu tiên cao" để tìm ra rằng maxcần phải được đánh giá đầu tiên; nó đã rõ ràng từ thứ tự của các mã thông báo. Đó là lý do tại sao máy tính thích ký hiệu pre / postfix để bắt đầu và tại sao chúng ta có thuật toán này để chuyển đổi infix thành pre / postfix.

Bạn cần phải có một số loại quy tắc về nơi các đối số của hàm bắt đầu và kết thúc khi không có dấu ngoặc đơn, vì vậy bạn có thể nói rằng các hàm "được ưu tiên" hơn các toán tử hoặc ngược lại. Nhưng không giống như các toán tử infix, một quy tắc nhất quán duy nhất cho tất cả các hàm là đủ để làm cho các tác phẩm của chúng hoàn toàn không rõ ràng.


Thuật toán của họ là chính xác, sau đó; đó là ví dụ của họ không đúng Ký hiệu infix phải bao gồm dấu ngoặc đơn bao bọc các chức năng:sin( max( 2 3) / 3 * 3.1415)
MirroredFate

Tôi không chắc chắn nếu tôi gọi nó là không chính xác, nhưng đây là một lập luận mạnh mẽ ủng hộ các ngôn ngữ yêu cầu dấu ngoặc đơn và dấu phẩy xung quanh tất cả các lệnh gọi hàm.
Ixrec

Tôi nghĩ rằng nó không chính xác vì không thể phân tích cú pháp bằng cách sử dụng thuật toán như họ mô tả.
MirroredFate

@Ixrec Tôi không thấy dòng "Nếu mã thông báo là mã thông báo chức năng, sau đó đẩy nó lên ngăn xếp." trên trang Wikipedia. Có thể được chỉnh sửa bởi bây giờ. Nhưng bạn có nghĩa là tôi có thể coi một hàm giống như một số trong thuật toán?
Abhinav

3

Có hai trường hợp khác nhau để xem xét, tùy thuộc vào cú pháp ngôn ngữ của bạn. Nếu ngôn ngữ của bạn sử dụng dấu ngoặc đơn để biểu thị ứng dụng chức năng (ví dụ f(2+1)) thì quyền ưu tiên là không liên quan. Hàm nên được đẩy lên ngăn xếp và bật ra sau (ví dụ ở trên, kết quả là 2 1 + f). Ngoài ra, bạn có thể coi hàm là một giá trị và xuất nó ngay lập tức và xuất ra một hoạt động gọi hàm sau dấu ngoặc đơn (nếu không được xử lý giống như bất kỳ dấu ngoặc đơn nào khác), ví dụ f 2 1 + $, $hoạt động gọi hàm là gì.

Tuy nhiên, nếu ngôn ngữ của bạn không sử dụng dấu ngoặc đơn để biểu thị lời gọi hàm, mà thay vào đó, đặt đối số trực tiếp sau hàm mà không có dấu câu đặc biệt nào (ví dụ f 2 + 1), như trường hợp của ví dụ Wikipedia, thì mọi thứ phức tạp hơn một chút. Lưu ý rằng biểu thức tôi vừa đưa ra một ví dụ không rõ ràng: f được áp dụng cho 2 và 1 được thêm vào kết quả hay chúng ta thêm 2 và 1 với nhau và sau đó gọi f với kết quả?

Một lần nữa, có hai cách tiếp cận. Bạn có thể chỉ cần đẩy hàm vào ngăn xếp toán tử khi bạn gặp nó và gán cho nó bất cứ thứ gì bạn muốn. Đây là cách tiếp cận đơn giản nhất, và rõ ràng là những gì ví dụ được trích dẫn đã làm. Có những vấn đề thực tế, tuy nhiên. Thứ nhất, làm thế nào để bạn xác định một chức năng? Nếu bạn có một bộ hữu hạn thì thật dễ dàng, nhưng nếu bạn có các hàm do người dùng xác định, điều này có nghĩa là trình phân tích cú pháp của bạn cần phản hồi quá mức vào môi trường của bạn, điều này có thể trở nên lộn xộn nhanh chóng. Và làm thế nào để bạn xử lý các chức năng với nhiều đối số?

Cảm giác của tôi là đối với kiểu cú pháp này, sử dụng các hàm làm các giá trị được xử lý bởi một toán tử ứng dụng hàm có ý nghĩa hơn nhiều. Sau đó, bạn có thể chỉ cần tiêm toán tử ứng dụng bất cứ khi nào bạn đọc một giá trị và điều cuối cùng bạn đọc cũng là một giá trị, vì vậy bạn không cần bất kỳ cách đặc biệt nào để biết định danh nào là hàm. Bạn cũng có thể làm việc với các biểu thức trả về các hàm (khó hoặc không thể với kiểu hoạt động của hàm). Và điều này có nghĩa là bạn có thể sử dụng currying để xử lý nhiều hàm đối số, đây là một sự đơn giản hóa lớn so với việc cố gắng xử lý chúng trực tiếp.

Điều duy nhất bạn cần quyết định sau đó là ưu tiên của ứng dụng chức năng là gì. Sự lựa chọn tùy thuộc vào bạn, nhưng trong mọi ngôn ngữ tôi đã sử dụng hoạt động như thế này, nó đã là toán tử ràng buộc mạnh nhất trong ngôn ngữ và đã được kết hợp đúng. (Biến thể thú vị duy nhất là Haskell, cũng như có phiên bản ràng buộc mạnh được mô tả, cũng có một từ đồng nghĩa với ký hiệu $là toán tử ràng buộc yếu nhất trong ngôn ngữ, cho phép các biểu thức muốn f 2 + 1áp dụng f đến 2 và f $ 2 + 1áp dụng nó cho toàn bộ phần còn lại của biểu thức)


3

Tôi đã triển khai "các chức năng trong sân shunting" sau khi đọc suy nghĩ ban đầu của Dijkstra (Trang 7-11 trong bài viết về trình biên dịch Algol 60, https://ir.cwi.nl/pub/9251 ) và cần một giải pháp mạnh mẽ, tôi đã làm như sau:

phân tích cú pháp:

  • Đẩy mô tả chức năng
  • Đẩy một dấu ngoặc đơn bắt đầu bên trái "[" giống như bắt đầu dấu ngoặc đơn phụ của nó.
  • Đọc một chuỗi danh sách đối số cân bằng "(" đến ")" từ đầu vào
  • Đẩy cái này vào luồng mã thông báo đầu ra
  • Đẩy một khung bên phải cuối cùng "]" giống như "khung đóng bù" của anh ấy

Infix-to-postfix (sân shunting):

  • Thêm ngăn xếp khác, ngăn xếp hàm, giống như ngăn xếp toán tử
  • Khi quét tên hàm, đẩy thông tin hàm đến ngăn xếp hàm
  • Khi nhìn thấy một khung bên phải của đối số kết thúc, hãy đưa ngăn xếp hàm vào đầu ra

Hoạt động hoàn hảo trong thử nghiệm mạnh mẽ và các kịch bản phức tạp. Trong ứng dụng của tôi (một bộ mở rộng biểu thức chứa đối số dòng lệnh), tôi hỗ trợ các hàm đa đối số và dấu phẩy "," để phân tách chúng và các luồng này trong toàn bộ quá trình.

Các ví dụ trông giống như "sqrt (3 ^ 2 + 4 ^ 2)" trở thành "3 2 ^ 4 2 ^ + sqrt" và cuối cùng là "5", đó là những gì chương trình nghĩ là đối số. Đó là bignum, vì vậy "" nhị thức (64, 32) / gcd (nhị thức (64, 32), nhị thức (63, 31)) "==> những điều lớn ==>" 2 "là hữu ích." 123456 ^ 789 " là 40.173 chữ số và thời gian hiển thị "Assessment = 0,000390 giây" trên MacBookPro của tôi, thật nhanh chóng.

Tôi cũng sử dụng điều này để mở rộng dữ liệu trong các bảng và thấy rằng tiện dụng. Dù sao, đây là mẹo của tôi trên con đường của tôi để xử lý cẩn thận các lệnh gọi hàm, nhiều đối số và lồng nhau sâu trong bối cảnh di chuyển sân của Dijkstra. Chỉ cần làm điều đó ngày hôm nay từ suy nghĩ độc lập. Không biết có cách nào tốt hơn không.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.