2015-05-15 24 views
10

Tôi hiện đang học Haskell và tôi tò mò về những điều sau đây:Danh sách thao tác hiệu quả trong Haskell

Nếu tôi thêm một yếu tố vào một danh sách trong Haskell, Haskell trả về một (? Hoàn toàn) danh sách mới, và không thao tác bản gốc.

Bây giờ, giả sử tôi có danh sách một triệu phần tử và tôi nối thêm một phần tử vào cuối. Haskell có "sao chép" toàn bộ danh sách (1 triệu phần tử) và thêm phần tử vào bản sao đó không? Hoặc là có một "thủ thuật" gọn gàng đang diễn ra đằng sau hậu trường để tránh sao chép toàn bộ danh sách?

Và nếu không có "mẹo", thì quy trình sao chép các danh sách lớn không đắt như tôi nghĩ?

Trả lời

8

Tùy thuộc vào cấu trúc dữ liệu bạn đang sử dụng. Nếu bạn đang sử dụng các danh sách Haskell bình thường, chúng sẽ tương tự như một danh sách liên kết điển hình được thực hiện trong C hoặc C++. Với cấu trúc này, phụ thêm là độ phức tạp O (n), trong khi phần trước là O (1) phức tạp. Nếu bạn cố gắng nối thêm một triệu phần tử, thì sẽ mất thời gian O (500000500000) (O (1) + O (2) + O (3) + ... + O (1000000)) khoảng 500000500000 hoạt động. Điều này là bất kể ngôn ngữ bạn đang sử dụng, Haskell, C, C++, Python, Java, C#, hoặc thậm chí Assembler. Tuy nhiên, nếu bạn sử dụng cấu trúc như Data.Sequence.Seq, thì nó sử dụng cấu trúc thích hợp bên trong để cung cấp thêm (1) chi phí và phụ thêm, nhưng chi phí là nó có thể chiếm nhiều RAM hơn một chút. Tất cả các cấu trúc dữ liệu có sự cân bằng, mặc dù, nó tùy thuộc vào bạn mà bạn muốn sử dụng. Ngoài ra, bạn cũng có thể sử dụng Data.Vector.Vector hoặc Data.Array.Array, cả hai đều cung cấp các mảng bộ nhớ liền kề, cố định, nhưng nối thêm và đắt tiền vì bạn phải sao chép toàn bộ mảng vào vị trí mới trong RAM. Lập chỉ mục là O (1), và ánh xạ hoặc gấp trên một trong các cấu trúc này sẽ nhanh hơn nhiều vì các mảng của mảng có thể vừa với bộ nhớ cache CPU của bạn cùng một lúc, trái ngược với danh sách liên kết hoặc chuỗi có phần tử nằm rải rác RAM của bạn.

Haskell "sao chép" toàn bộ danh sách (1 triệu phần tử) và thêm phần tử vào bản sao đó?

Không nhất thiết, trình biên dịch có thể xác định xem nó là an toàn để vừa có sự thay đổi next con trỏ giá trị cuối cùng của điểm theo giá trị mới thay vì danh sách rỗng, hoặc nếu nó không an toàn có thể cần thiết để sao chép toàn bộ danh sách . Tuy nhiên, những vấn đề này vốn có là cấu trúc dữ liệu chứ không phải ngôn ngữ. Nói chung, tôi sẽ nói rằng danh sách của Haskell tốt hơn C danh sách liên kết vì trình biên dịch có khả năng phân tích khi điều này an toàn hơn lập trình viên, và trình biên dịch C sẽ không thực hiện phân tích này, chúng chỉ làm chính xác như chúng được nói.

+1

Tôi đồng ý với những gì bạn nói nhưng Big O ký hiệu của bạn là không đúng. O (500000500000) == O (1) == thời gian không đổi (xem http://en.wikipedia.org/wiki/Big_O_notation#Multiplication_by_a_constant). Chắc chắn, bạn có thể tranh luận rằng nếu bạn cố gắng "nối thêm một triệu phần tử" thì nó luôn chạy trong O (1) vì không có biến nào còn lại và thao tác "nối thêm một triệu lần" thực sự chạy trong thời gian không đổi. Nhưng tôi không nghĩ đó là điều bạn muốn nói. –

+0

@ JohannesWeiß Tốt hơn? – bheklilr

+0

Vâng, @bheklilr, cảm ơn :) –

3

Khi sử dụng danh sách, phụ phí quá đắt và danh sách phải được sao chép, mặc dù không phải là phần tử. Ngoài ra, việc thêm tiền tố còn rẻ vì giá trị mới chỉ trỏ đến danh sách gốc.

Hãy thêm "third" vào ["first", "second"]: danh sách mới là (:) "first" ((:) "second" ((:) "third" [])). Do đó, hàm khởi tạo đầu tiên phải là một hàm mới làm đối số thứ hai phải là một giá trị mới như ... Các chuỗi không được nhân đôi. Danh sách mới trỏ đến cùng một chuỗi trong bộ nhớ.

Lưu ý rằng trong trường hợp giá trị cũ bị loại bỏ, trình biên dịch có thể quyết định sử dụng lại nó thay vì cấp phát bộ nhớ cho các giá trị mới và thu gom rác cũ. Trong mọi trường hợp, phụ thêm sẽ được thực hiện trong O (n) vì nó cần phải tìm thấy kết thúc của nó.

Bây giờ nếu chương trình của bạn đang thêm rất nhiều vào danh sách, bạn có thể muốn sử dụng cấu trúc dữ liệu khác để có thể nối thêm vào O (1) chẳng hạn như DList tạo thành gói dlist. (https://hackage.haskell.org/package/dlist-0.5/docs/Data-DList.html)

+0

các phụ lục không phải là vấn đề. không có gì ngăn cản các danh sách đang được thực hiện với các phần tử của chúng được lưu trữ trong một mảng được phân bổ trước lớn, cộng với vị trí 'start' và' end'. cả 'xs' và' xs ++ [a] 'đều có thể sử dụng cùng một mảng. thậm chí các phần bổ sung không phải là vấn đề nếu chúng ta bắt đầu ở giữa, hoặc sử dụng các danh sách (/ mảng) của các khối mảng (con trỏ đến). nó là * insertions * có vấn đề. 'case xs of (a: as) ...' sẽ chỉ tạo 'as = (start + 1, end, array)' từ 'xs = (bắt đầu, kết thúc, mảng)', đằng sau hậu trường. –

8

Đây là một câu hỏi phức tạp đáng ngạc nhiên, vì hai tính năng của Haskell và GHC:

  1. đánh giá Lazy
  2. Danh sách fusion

Danh sách tổng hợp có nghĩa là trong một số trường hợp, GHC có thể viết lại mã xử lý danh sách vào một vòng lặp không phân bổ các ô danh sách. Vì vậy, tùy thuộc vào ngữ cảnh mà nó được sử dụng, cùng một mã có thể không phải trả thêm chi phí.

Đánh giá lười biếng có nghĩa là nếu kết quả của một thao tác không được tiêu thụ, thì bạn không phải trả chi phí tính toán nó. Vì vậy, ví dụ, đây là giá rẻ, bởi vì bạn chỉ phải xây dựng mười yếu tố đầu tiên của danh sách:

example = take 10 ([1..1000000] ++ [1000001]) 

Trong thực tế, trong mã rằng take 10 có thể kết hợp được với danh sách append, do đó, nó giống như chỉ [1..10].

Nhưng hãy giả sử rằng chúng tôi đang tiêu thụ tất cả các thành phần của tất cả các danh sách mà chúng tôi thực hiện và trình biên dịch không kết hợp hoạt động danh sách của chúng tôi. Bây giờ cho câu hỏi của bạn:

Nếu tôi thêm phần tử vào Danh sách trong Haskell, Haskell trả về danh sách mới (đầy đủ?) Và không thao tác bản gốc. Bây giờ chúng ta hãy nói rằng tôi có một danh sách của một triệu yếu tố và tôi nối thêm một phần tử ở cuối. Haskell có "sao chép" toàn bộ danh sách (1 triệu phần tử) và thêm phần tử vào bản sao đó không? Hoặc là có một "thủ thuật" gọn gàng đang diễn ra đằng sau hậu trường để tránh sao chép toàn bộ danh sách?

Có các thủ thuật để tránh sao chép toàn bộ danh sách, nhưng bằng cách nối vào cuối danh sách, bạn sẽ đánh bại chúng. Điều cần hiểu là các cấu trúc dữ liệu chức năng thường được thiết kế sao cho các hoạt động "sửa đổi" chúng sẽ khai thác chia sẻ cấu trúc để tái sử dụng càng nhiều cấu trúc cũ càng tốt. Vì vậy, ví dụ, phụ thêm hai danh sách có thể được định nghĩa như thế này:

(++) :: [a] -> [a] -> [a] 
[] ++ ys = ys 
(x:xs) ++ ys = x : xs ++ ys 

Nhìn vào định nghĩa này, bạn có thể nói rằng danh sách ys sẽ được tái sử dụng trong kết quả. Vì vậy, nếu chúng ta có xs = [1..3], ys = [4..5]xs ++ ys, tất cả các đánh giá đầy đủ và giữ lại trong bộ nhớ cùng một lúc, nó sẽ trông giống như bộ nhớ khôn ngoan này:

  +---+---+ +---+---+ +---+---+ 
     xs = | 1 | -----> | 2 | -----> | 3 | -----> [] 
      +---+---+ +---+---+ +---+---+ 

      +---+---+ +---+---+ 
     ys = | 4 | -----> | 5 | -----> [] 
      +---+---+ +---+---+  
      ^
      | 
      +------------------------------------+ 
                | 
      +---+---+ +---+---+ +---+---+ | 
xs ++ ys = | 1 | -----> | 2 | -----> | 3 | -------+ 
      +---+---+ +---+---+ +---+---+ 

Đó là chặng đường dài để nói điều này: nếu bạn làm xs ++ ys , và nó không hợp nhất, và bạn tiêu thụ toàn bộ danh sách, sau đó sẽ tạo ra một bản sao của xs nhưng tái sử dụng bộ nhớ cho ys.

Nhưng bây giờ chúng ta hãy nhìn lại chút này của câu hỏi của bạn:

Bây giờ chúng ta hãy nói rằng tôi có một danh sách của một triệu yếu tố và tôi thêm một yếu tố ở cuối. Haskell có "sao chép" toàn bộ danh sách (1 triệu phần tử) và thêm phần tử vào bản sao đó không?

Điều đó sẽ giống như [1..1000000] ++ [1000001] và có, nó sẽ sao chép cả triệu yếu tố. Nhưng mặt khác, [0] ++ [1..1000000] sẽ chỉ sao chép [0]. Quy tắc chung là:

  • Thêm phần tử ở đầu danh sách hiệu quả nhất.
  • Việc thêm các phần tử vào cuối danh sách thường không hiệu quả, đặc biệt nếu bạn lặp đi lặp lại.

Các giải pháp chung để phân loại này của vấn đề là:

  1. Sửa đổi thuật toán của bạn để bạn sử dụng danh sách theo mô hình truy cập chúng hỗ trợ một cách hiệu quả.
  2. Không sử dụng danh sách; sử dụng một số cấu trúc dữ liệu chuỗi khác có hiệu quả hỗ trợ mẫu truy cập bạn cần cho sự cố trong tầm tay. Một câu trả lời nêu danh sách khác biệt, nhưng những người khác đáng nói là:
+0

Đẹp! Tôi không biết về chia sẻ cấu trúc. – Robin

Các vấn đề liên quan