Bootstrapping cấu trúc cây ngón tay


16

Sau khi làm việc với 2-3 cây ngón tay khá lâu, tôi đã bị ấn tượng bởi tốc độ của chúng trong hầu hết các hoạt động. Tuy nhiên, một vấn đề tôi gặp phải là chi phí lớn liên quan đến việc tạo ra một cây ngón tay lớn ban đầu. Bởi vì tòa nhà được định nghĩa là một chuỗi các hoạt động nối, cuối cùng bạn sẽ xây dựng một số lượng lớn các cấu trúc cây ngón tay không cần thiết.

Do tính chất phức tạp của 2-3 cây ngón tay, tôi thấy không có phương pháp trực quan nào để khởi động chúng và tất cả các tìm kiếm của tôi đều trống rỗng. Vì vậy, câu hỏi là, làm thế nào bạn có thể đi về bootstrapping một cây 2-3 ngón tay với chi phí tối thiểu?

Để được rõ ràng: đưa ra một chuỗi có độ dài n đã biết tạo ra biểu diễn cây ngón tay của S với các thao tác tối thiểu.SnS

Cách ngây thơ để thực hiện là các cuộc gọi liên tiếp đến hoạt động khuyết điểm (trong tài liệu là toán tử ' '). Tuy nhiên, điều này sẽ tạo ra n cấu trúc cây ngón tay riêng biệt đại diện cho tất cả các lát của S cho [ 1 .. i ] .nS[1 ..Tôi]


1
Liệu cây Finger: một cấu trúc dữ liệu đa năng đơn giản của Hinze và Paterson có cung cấp câu trả lời không?
Dave Clarke

@ Tôi thực sự đã triển khai bài báo của họ và họ không giải quyết việc tạo hiệu quả.
jbondeson

Tôi đã tìm ra nhiều.
Dave Clarke

Bạn có thể nói rõ hơn một chút về ý của bạn khi "xây dựng" trong trường hợp này không? Đây có phải là một mở ra?
jbapple

@jbapple - Tôi đã chỉnh sửa để rõ ràng hơn, xin lỗi vì sự nhầm lẫn.
jbondeson

Câu trả lời:


16

GHC của Data.Sequence's replicatechức năng xây dựng một fingertree trong thời gian và không gian, nhưng điều này được kích hoạt bằng cách biết các yếu tố mà đi vào phía rìa bên phải của cây ngón tay từ get-go. Thư viện này được viết bởi các tác giả của bài báo gốc trên 2-3 cây ngón tay.Ôi(lgn)

Nếu bạn muốn xây dựng một cây ngón tay bằng cách nối lặp đi lặp lại, bạn có thể giảm mức sử dụng không gian thoáng qua trong khi xây dựng bằng cách thay đổi biểu diễn của các gai. Các gai trên 2-3 cây ngón tay được lưu trữ khéo léo dưới dạng danh sách liên kết đơn được đồng bộ hóa. Nếu, thay vào đó, bạn lưu trữ các gai dưới dạng deques, có thể tiết kiệm không gian khi nối cây. Ý tưởng là ghép hai cây có cùng chiều cao cần không gian bằng cách sử dụng lại các gai của cây. Khi ghép 2-3 cây ngón tay như mô tả ban đầu, các gai bên trong cây mới không còn có thể được sử dụng như hiện trạng.Ôi(1)

"Các đại diện hoàn toàn chức năng của danh sách sắp xếp có thể điều chỉnh được" của Kaplan và Tarjan mô tả cấu trúc cây ngón tay phức tạp hơn. Bài viết này (trong phần 4) cũng thảo luận về một công trình tương tự như gợi ý deque mà tôi đã thực hiện ở trên. Tôi tin rằng cấu trúc mà họ mô tả có thể ghép hai cây có chiều cao bằng nhau trong thời gian và không gian . Để xây dựng cây ngón tay, điều này có đủ tiết kiệm không gian cho bạn?Ôi(1)

Lưu ý: Việc họ sử dụng từ "bootstrapping" có nghĩa là một cái gì đó hơi khác so với việc bạn sử dụng ở trên. Chúng có nghĩa là lưu trữ một phần của cấu trúc dữ liệu bằng cách sử dụng phiên bản đơn giản hơn của cùng cấu trúc.


Một ý tưởng rất thú vị. Tôi sẽ phải xem xét điều này và xem sự đánh đổi sẽ là gì đối với cấu trúc dữ liệu tổng thể.
jbondeson

Ý tôi là có hai ý tưởng trong câu trả lời này: (1) Ý tưởng sao chép (2) Nối nhanh hơn cho các cây có kích thước gần bằng nhau. Tôi nghĩ rằng ý tưởng sao chép có thể xây dựng cây ngón tay trong không gian thừa rất ít nếu đầu vào là một mảng.
jbapple

Vâng, tôi đã thấy cả hai. Xin lỗi tôi đã không bình luận về cả hai. Trước tiên tôi đang xem xét mã sao chép - mặc dù tôi chắc chắn sẽ mở rộng kiến ​​thức về Haskell của mình cho đến khi nó đi. Lúc đầu, có vẻ như nó có thể giải quyết hầu hết các vấn đề tôi gặp phải, miễn là bạn có quyền truy cập ngẫu nhiên nhanh chóng. Các concat nhanh có thể là một giải pháp chung hơn một chút trong trường hợp không có quyền truy cập ngẫu nhiên.
jbondeson

10

Riffing trên câu trả lời tuyệt vời của jbapple liên quan đến replicate, nhưng sử dụng replicateA( replicateđược xây dựng trên) thay vào đó, tôi đã đưa ra những điều sau đây:

--Unlike fromList, one needs the length explicitly. 
myFromList :: Int -> [b] -> Seq b
myFromList l xs = flip evalState xs $ Seq.replicateA l go
    where go = do
           (y:ys) <- get
            put ys
            return y

myFromList(trong một phiên bản hơi hiệu quả hơn) đã được xác định và sử dụng trong nội tại Data.Sequenceđể xây dựng cây ngón tay mà là kết quả của các loại.

Nói chung, trực giác cho replicateAđơn giản. replicateAđược xây dựng trên đỉnh của ứng dụngTree hàm . applicativeTreelấy một mảnh của cây có kích thước mvà tạo ra một cây cân đối có chứa các nbản sao của cái này. Các trường hợp cho ntối đa 8 (một Deepngón tay) được mã hóa cứng. Bất cứ điều gì ở trên này, và nó tự gọi đệ quy. Phần tử "ứng dụng" chỉ đơn giản là nó xen kẽ việc xây dựng cây với các hiệu ứng luồng xuyên qua, chẳng hạn như, trong trường hợp của đoạn mã trên, trạng thái.

Các gochức năng, được nhân rộng, đơn giản chỉ là một hành động mà được tình trạng hiện tại, bật một yếu tố ra khỏi đầu, và thay thế phần còn lại. Trên mỗi lệnh gọi, do đó, bước tiếp xuống danh sách được cung cấp làm đầu vào.

Một số ghi chú cụ thể hơn

main = print (length (show (Seq.fromList [1..10000000::Int])))

Trong một số thử nghiệm đơn giản, điều này mang lại một sự đánh đổi hiệu suất thú vị. Hàm chính ở trên chạy gần như thấp hơn 1/3 với myFromList so vớifromList . Mặt khác, myFromListsử dụng một đống không đổi 2MB, trong khi tiêu chuẩn fromListsử dụng lên tới 926 MB. 926 MB đó phát sinh từ việc cần phải giữ toàn bộ danh sách trong bộ nhớ cùng một lúc. Trong khi đó, giải pháp với myFromListkhả năng tiêu thụ cấu trúc theo kiểu lười biếng phát trực tuyến. Vấn đề với kết quả tốc độ từ thực tế là myFromListphải thực hiện gấp đôi số lần phân bổ (do kết quả của việc xây dựng / phá hủy cặp đơn nguyên nhà nước) nhưfromList. Chúng ta có thể loại bỏ những phân bổ đó bằng cách chuyển sang một đơn vị trạng thái được chuyển đổi CPS, nhưng điều đó dẫn đến việc giữ bộ nhớ nhiều hơn bất cứ lúc nào, bởi vì mất sự lười biếng đòi hỏi phải đi qua danh sách theo cách không phát trực tuyến.

Mặt khác, nếu thay vì buộc toàn bộ chuỗi bằng một chương trình, tôi chuyển sang chỉ trích xuất phần đầu hoặc phần tử cuối cùng, myFromList ngay lập tức đưa ra một chiến thắng lớn hơn - trích xuất phần tử đầu gần như ngay lập tức và trích xuất phần tử cuối cùng là 0,8 giây . Trong khi đó, với tiêu chuẩn fromList, trích xuất phần đầu hoặc phần tử cuối có giá ~ 2,3 giây.

Đây là tất cả các chi tiết, và là hệ quả của sự tinh khiết và lười biếng. Trong một tình huống có đột biến và truy cập ngẫu nhiên, tôi sẽ tưởng tượng replicategiải pháp này hoàn toàn tốt hơn.

Tuy nhiên, nó đặt ra câu hỏi liệu có cách nào để viết lại applicativeTreenhư vậy mà myFromListhiệu quả hơn không. Vấn đề là, tôi nghĩ rằng các hành động áp dụng được thực hiện theo một thứ tự khác với cây được di chuyển tự nhiên, nhưng tôi chưa hoàn toàn làm việc thông qua cách thức này hoạt động, hoặc nếu có cách nào để giải quyết vấn đề này.


4
(1) Thú vị. Này trông giống như các cách chính xác để làm nhiệm vụ này. Tôi ngạc nhiên khi biết rằng điều này chậm hơn so với fromListkhi toàn bộ chuỗi bị ép buộc. (2) Có thể câu trả lời này quá nặng về mã và phụ thuộc vào ngôn ngữ cho cstheory.stackexchange.com. Sẽ thật tuyệt nếu bạn có thể thêm một lời giải thích cách replicateAhoạt động theo cách độc lập với ngôn ngữ.
Tsuyoshi Ito

9

Trong khi bạn kết thúc với một số lượng lớn các cấu trúc ngón tay trung gian, chúng chia sẻ phần lớn cấu trúc của chúng với nhau. Cuối cùng, bạn phân bổ tối đa gấp đôi bộ nhớ so với trường hợp lý tưởng hóa và phần còn lại được giải phóng với bộ sưu tập đầu tiên. Sự không triệu chứng của điều này là tốt như họ có thể nhận được, vì bạn cần một đầu ngón tay chứa đầy n giá trị cuối cùng.

Bạn có thể xây dựng ngón tay bằng cách sử dụng Data.FingerTree.replicatevà họ sử dụng FingerTree.fmapWithPosđể tra cứu các giá trị của bạn trong một mảng đóng vai trò của chuỗi hữu hạn của bạn hoặc sử dụng traverseWithPosđể bóc chúng ra khỏi danh sách hoặc thùng chứa có kích thước đã biết khác.

Ôi(đăng nhậpn)Ôi(n)Ôi(đăng nhậpn)

Ôi(đăng nhậpn)replicateAmapAccumL

TL; DR Nếu tôi phải làm điều này, có lẽ tôi sẽ sử dụng:

rep :: (Int -> a) -> Int -> Seq a 
rep f n = mapWithIndex (const . f) $ replicate n () 

và chỉ số vào một mảng có kích thước cố định tôi chỉ muốn cung cấp (arr !)cho fở trên.

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.