Những cấu trúc dữ liệu nào bạn có thể sử dụng để bạn có thể loại bỏ và thay thế O (1)? Hoặc làm thế nào bạn có thể tránh các tình huống khi bạn cần cấu trúc nói?
ST
đơn vị trong Haskell thực hiện điều này một cách xuất sắc.
Những cấu trúc dữ liệu nào bạn có thể sử dụng để bạn có thể loại bỏ và thay thế O (1)? Hoặc làm thế nào bạn có thể tránh các tình huống khi bạn cần cấu trúc nói?
ST
đơn vị trong Haskell thực hiện điều này một cách xuất sắc.
Câu trả lời:
Có một loạt các cấu trúc dữ liệu khai thác sự lười biếng và các thủ thuật khác để đạt được thời gian cố định hoặc thậm chí (đối với một số trường hợp hạn chế, chẳng hạn như hàng đợi ) cập nhật thời gian liên tục cho nhiều loại vấn đề. Luận án tiến sĩ "Cấu trúc dữ liệu chức năng thuần túy" của Chris Okasaki và cuốn sách cùng tên là một ví dụ điển hình (có lẽ là cuốn chính đầu tiên), nhưng lĩnh vực này đã phát triển kể từ đó . Các cấu trúc dữ liệu này thường không chỉ đơn thuần là chức năng trong giao diện, mà còn có thể được thực hiện bằng Haskell thuần túy và các ngôn ngữ tương tự, và hoàn toàn bền bỉ.
Ngay cả khi không có bất kỳ công cụ tiên tiến nào, các cây tìm kiếm nhị phân cân bằng đơn giản cung cấp các cập nhật theo thời gian logarit, do đó bộ nhớ có thể thay đổi có thể được mô phỏng với điều tồi tệ nhất là làm chậm logarit.
Có những lựa chọn khác, có thể được coi là gian lận, nhưng rất hiệu quả liên quan đến nỗ lực thực hiện và hiệu suất trong thế giới thực. Ví dụ, các loại tuyến tính hoặc các loại duy nhất cho phép cập nhật tại chỗ làm chiến lược triển khai cho ngôn ngữ thuần túy về mặt khái niệm, bằng cách ngăn chương trình giữ giá trị trước đó (bộ nhớ sẽ bị thay đổi). Đây là ít chung hơn so với cấu trúc dữ liệu liên tục: Ví dụ: bạn không thể dễ dàng xây dựng nhật ký hoàn tác bằng cách lưu trữ tất cả các phiên bản trước đó của trạng thái. Nó vẫn là một công cụ mạnh mẽ, mặc dù AFAIK chưa có sẵn trong các ngôn ngữ chức năng chính.
Một tùy chọn khác để giới thiệu trạng thái có thể thay đổi một cách an toàn vào một cài đặt chức năng là ST
đơn nguyên trong Haskell. Nó có thể được thực hiện mà không bị đột biến và chặn các unsafe*
chức năng, nó hoạt động như thể nó chỉ là một trình bao bọc ưa thích xung quanh việc chuyển một cấu trúc dữ liệu liên tục ngầm (xem State
). Nhưng do một số mánh khóe hệ thống thực thi lệnh đánh giá và ngăn chặn thoát, nó có thể được thực hiện một cách an toàn với đột biến tại chỗ, với tất cả các lợi ích hiệu suất.
Một cấu trúc có thể thay đổi giá rẻ là ngăn xếp đối số.
Hãy xem cách tính giai thừa theo kiểu SICP điển hình:
(defn fac (n accum)
(if (= n 1)
accum
(fac (- n 1) (* accum n)))
(defn factorial (n) (fac n 1))
Như bạn có thể thấy, đối số thứ hai để fac
được sử dụng như một bộ tích lũy có thể thay đổi để chứa sản phẩm thay đổi nhanh n * (n-1) * (n-2) * ...
. Tuy nhiên, không có biến đổi có thể nhìn thấy được và không có cách nào để vô tình thay đổi bộ tích lũy, ví dụ từ một luồng khác.
Tất nhiên, đây là một ví dụ hạn chế.
Bạn có thể nhận được danh sách liên kết không thay đổi với nút thay thế giá rẻ (và bằng cách mở rộng bất kỳ phần nào bắt đầu từ đầu): bạn chỉ cần tạo điểm đầu mới đến cùng nút tiếp theo như đầu cũ đã làm. Điều này hoạt động tốt với nhiều thuật toán xử lý danh sách (bất cứ điều gìfold
dựa trên ).
Bạn có thể có được hiệu suất khá tốt từ các mảng kết hợp dựa trên HAMTs . Theo logic, bạn nhận được một mảng kết hợp mới với một số cặp giá trị khóa được thay đổi. Việc thực hiện có thể chia sẻ hầu hết các dữ liệu phổ biến giữa các đối tượng cũ và mới được tạo. Đây không phải là O (1); thông thường bạn nhận được một cái gì đó logarit, ít nhất là trong trường hợp xấu nhất. Mặt khác, cây bất biến thường không phải chịu bất kỳ hình phạt hiệu suất nào so với cây đột biến. Tất nhiên, điều này đòi hỏi một số chi phí bộ nhớ, thường là xa cấm.
Một cách tiếp cận khác dựa trên ý tưởng rằng nếu một cái cây rơi trong rừng và không ai nghe thấy nó, thì nó không cần phải tạo ra âm thanh. Đó là, nếu bạn có thể chứng minh rằng một chút trạng thái đột biến không bao giờ rời khỏi một phạm vi cục bộ nào đó, bạn có thể biến đổi dữ liệu trong đó một cách an toàn.
Clojure có các quá độ có thể thay đổi 'bóng' của các cấu trúc dữ liệu bất biến không bị rò rỉ ra ngoài phạm vi cục bộ. Clean sử dụng Uniques để đạt được một cái gì đó tương tự (nếu tôi nhớ chính xác). Rust giúp làm những việc tương tự với con trỏ độc đáo được kiểm tra tĩnh.
ref
và ràng buộc chúng trong một phạm vi nhất định. Xem IORef
hoặc STRef
. Và dĩ nhiên, có những TVar
s và MVar
s tương tự nhau nhưng với ngữ nghĩa đồng thời lành mạnh (stm cho TVar
s và mutex dựa trên MVar
s)
Những gì bạn đang hỏi là một chút quá rộng. O (1) loại bỏ và thay thế từ vị trí nào? Người đứng đầu một chuỗi? Cái đuôi? Một vị trí độc đoán? Cấu trúc dữ liệu để sử dụng phụ thuộc vào những chi tiết đó. Điều đó nói rằng, 2-3 Cây ngón tay dường như là một trong những cấu trúc dữ liệu bền vững linh hoạt nhất hiện có:
Chúng tôi trình bày 2-3 cây ngón tay, một biểu diễn chức năng của các chuỗi liên tục hỗ trợ truy cập vào các đầu trong thời gian không đổi được khấu hao, và nối và tách trong logarit thời gian theo kích thước của mảnh nhỏ hơn.
(...)
Hơn nữa, bằng cách xác định thao tác phân tách ở dạng chung, chúng tôi có được cấu trúc dữ liệu mục đích chung có thể phục vụ như một chuỗi, hàng đợi ưu tiên, cây tìm kiếm, hàng đợi tìm kiếm ưu tiên và hơn thế nữa.
Nói chung các cấu trúc dữ liệu liên tục có hiệu suất logarit khi thay đổi các vị trí tùy ý. Điều này có thể hoặc không phải là một vấn đề, vì hằng số trong thuật toán O (1) có thể cao và sự chậm lại logarit có thể được "hấp thụ" vào một thuật toán tổng thể chậm hơn.
Quan trọng hơn, các cấu trúc dữ liệu liên tục giúp cho việc lập luận về chương trình của bạn dễ dàng hơn và đó luôn luôn là chế độ hoạt động mặc định của bạn. Bạn nên ưu tiên các cấu trúc dữ liệu liên tục bất cứ khi nào có thể và chỉ sử dụng cấu trúc dữ liệu có thể thay đổi một khi bạn đã định hình và xác định rằng cấu trúc dữ liệu liên tục là một nút cổ chai hiệu năng. Bất cứ điều gì khác là tối ưu hóa sớm.