Haskell: Danh sách, Mảng, Vectơ, Chuỗi


230

Tôi đang học Haskell và đọc một vài bài viết về sự khác biệt về hiệu suất của danh sách Haskell và (chèn ngôn ngữ của bạn).

Là một người học tôi rõ ràng chỉ sử dụng danh sách mà không cần suy nghĩ về sự khác biệt hiệu suất. Gần đây tôi đã bắt đầu điều tra và tìm thấy nhiều thư viện cấu trúc dữ liệu có sẵn trong Haskell.

Ai đó có thể vui lòng giải thích sự khác biệt giữa Danh sách, Mảng, Vectơ, Chuỗi mà không đi sâu vào lý thuyết khoa học máy tính về cấu trúc dữ liệu không?

Ngoài ra, có một số mẫu phổ biến mà bạn sẽ sử dụng một cấu trúc dữ liệu thay vì một cấu trúc dữ liệu khác không?

Có bất kỳ dạng cấu trúc dữ liệu nào khác mà tôi đang thiếu và có thể hữu ích không?


1
Hãy xem câu trả lời này về danh sách so với mảng: stackoverflow.com/questions/8196667/haskell-arrays-vs-lists Các vectơ có hiệu suất tương tự như mảng, nhưng API lớn hơn.
Grzegorz Chrupała

Sẽ rất vui khi thấy Data.Map được thảo luận ở đây. Đây có vẻ như là một cấu trúc dữ liệu hữu ích đặc biệt là cho dữ liệu đa chiều.
Martin Capodici

Câu trả lời:


339

Danh sách nhạc rock

Cho đến nay, cấu trúc dữ liệu thân thiện nhất cho dữ liệu tuần tự trong Haskell là Danh sách

 data [a] = a:[a] | []

Danh sách cung cấp cho bạn ϴ (1) khuyết điểm và khớp mẫu. Các thư viện chuẩn, và cho rằng quan trọng là khúc dạo đầu, có đầy đủ các chức năng danh sách hữu ích mà nên xả rác mã của bạn ( foldr, map, filter). Danh sách là persistant , aka hoàn toàn chức năng, mà là rất tốt đẹp. Danh sách Haskell không thực sự là "danh sách" vì chúng có tính cưỡng chế (các ngôn ngữ khác gọi các luồng này) nên những thứ như

ones :: [Integer]
ones = 1:ones

twos = map (+1) ones

tenTwos = take 10 twos

làm việc tuyệt vời Cấu trúc dữ liệu vô hạn đá.

Danh sách trong Haskell cung cấp một giao diện giống như các trình lặp trong các ngôn ngữ bắt buộc (vì sự lười biếng). Vì vậy, nó có ý nghĩa rằng chúng được sử dụng rộng rãi.

Mặt khác

Vấn đề đầu tiên với các danh sách là để lập chỉ mục vào chúng (!!)mất (k), điều này gây khó chịu. Ngoài ra, các phụ lục có thể chậm ++, nhưng mô hình đánh giá lười biếng của Haskell có nghĩa là những điều này có thể được coi là khấu hao hoàn toàn, nếu chúng xảy ra.

Vấn đề thứ hai với danh sách là họ có địa phương dữ liệu kém. Bộ xử lý thực sự phải chịu các hằng số cao khi các đối tượng trong bộ nhớ không được đặt cạnh nhau. Vì vậy, trong C ++ std::vectorcó "snoc" nhanh hơn (đặt các đối tượng ở cuối) so với bất kỳ cấu trúc dữ liệu danh sách liên kết thuần túy nào tôi biết, mặc dù đây không phải là cấu trúc dữ liệu bền vững nên ít thân thiện hơn danh sách của Haskell.

Vấn đề thứ ba với danh sách là chúng có hiệu quả không gian kém. Bunches của con trỏ thêm đẩy lưu trữ của bạn (bởi một yếu tố không đổi).

Trình tự là chức năng

Data.Sequencelà nội bộ dựa trên cây ngón tay (tôi biết, bạn không muốn biết điều này) có nghĩa là chúng có một số đặc tính tốt

  1. Hoàn toàn chức năng. Data.Sequencelà một cấu trúc dữ liệu hoàn toàn bền bỉ.
  2. Darn nhanh chóng truy cập vào đầu và cuối của cây. (1) (khấu hao) để lấy phần tử đầu tiên hoặc cuối cùng hoặc nối thêm cây. Tại các danh sách điều là nhanh nhất, Data.Sequencenhiều nhất là chậm liên tục.
  3. (Log n) truy cập vào giữa chuỗi. Điều này bao gồm chèn các giá trị để tạo chuỗi mới
  4. API chất lượng cao

Mặt khác, Data.Sequencekhông làm được gì nhiều cho vấn đề cục bộ dữ liệu và chỉ hoạt động cho các bộ sưu tập hữu hạn (nó ít lười hơn danh sách)

Mảng không dành cho người yếu tim

Mảng là một trong những cấu trúc dữ liệu quan trọng nhất trong CS, nhưng chúng không phù hợp lắm với thế giới chức năng thuần túy lười biếng. Mảng cung cấp quyền truy cập (1) vào giữa bộ sưu tập và các yếu tố địa phương / hằng số dữ liệu đặc biệt tốt. Nhưng, vì chúng không phù hợp lắm với Haskell, nên chúng rất khó sử dụng. Thực tế có vô số kiểu mảng khác nhau trong thư viện chuẩn hiện tại. Chúng bao gồm các mảng hoàn toàn bền bỉ, mảng có thể thay đổi cho đơn vị IO, mảng có thể thay đổi cho đơn vị ST và các phiên bản không được đóng hộp ở trên. Để biết thêm hãy xem wiki haskell

Vector là một mảng "tốt hơn"

Các Data.Vectorgói cung cấp tất cả sự tốt lành mảng, trong một mức độ và sạch API cao hơn. Trừ khi bạn thực sự biết những gì bạn đang làm, bạn nên sử dụng những thứ này nếu bạn cần mảng như hiệu suất. Tất nhiên, một số cảnh báo vẫn được áp dụng - mảng có thể thay đổi như cấu trúc dữ liệu chỉ không chơi tốt trong các ngôn ngữ lười biếng thuần túy. Tuy nhiên, đôi khi bạn muốn hiệu suất O (1) đó và Data.Vectorcung cấp cho bạn trong một gói có thể sử dụng được.

Bạn có những lựa chọn khác

Nếu bạn chỉ muốn danh sách có khả năng chèn hiệu quả vào cuối, bạn có thể sử dụng danh sách khác biệt . Ví dụ tốt nhất về danh sách làm tăng hiệu suất có xu hướng đến từ [Char]đó khúc dạo đầu có bí danh là String. Chardanh sách là thuận tiện, nhưng có xu hướng chạy theo thứ tự chậm hơn 20 lần so với chuỗi C, vì vậy hãy sử dụng Data.Texthoặc rất nhanh Data.ByteString. Tôi chắc chắn có những thư viện theo định hướng trình tự khác mà tôi không nghĩ đến ngay bây giờ.

Phần kết luận

90 +% thời gian tôi cần một bộ sưu tập tuần tự trong danh sách Haskell là cấu trúc dữ liệu phù hợp. Danh sách giống như các trình vòng lặp, các hàm tiêu thụ danh sách có thể dễ dàng được sử dụng với bất kỳ cấu trúc dữ liệu nào khác bằng cách sử dụng các toListhàm mà chúng đi kèm. Trong một thế giới tốt hơn, khúc dạo đầu sẽ hoàn toàn tham số về loại thùng chứa mà nó sử dụng, nhưng hiện đang []nằm trong thư viện chuẩn. Vì vậy, sử dụng danh sách (hầu hết) mọi nơi chắc chắn là ổn.
Bạn có thể nhận được các phiên bản tham số đầy đủ của hầu hết các hàm danh sách (và rất cao để sử dụng chúng)

Prelude.map                --->  Prelude.fmap (works for every Functor)
Prelude.foldr/foldl/etc    --->  Data.Foldable.foldr/foldl/etc
Prelude.sequence           --->  Data.Traversable.sequence
etc

Trong thực tế, Data.Traversableđịnh nghĩa một API phổ biến hơn hoặc ít hơn trên bất kỳ thứ gì "liệt kê như".

Tuy nhiên, mặc dù bạn có thể giỏi và chỉ viết mã tham số đầy đủ, hầu hết chúng ta đều không và sử dụng danh sách ở mọi nơi. Nếu bạn đang học, tôi thực sự khuyên bạn nên làm quá.


EDIT: Căn cứ vào ý kiến tôi nhận ra tôi không bao giờ giải thích khi sử dụng Data.Vectorvs Data.Sequence. Mảng và vectơ cung cấp các hoạt động lập chỉ mục và cắt cực nhanh, nhưng về cơ bản là cấu trúc dữ liệu (bắt buộc). Các cấu trúc dữ liệu chức năng thuần túy thích Data.Sequence[]cho phép tạo ra các giá trị mới từ các giá trị cũ một cách hiệu quả như thể bạn đã sửa đổi các giá trị cũ.

  newList oldList = 7 : drop 5 oldList

không sửa đổi danh sách cũ và nó không phải sao chép nó. Vì vậy, ngay cả khi oldListcực kỳ dài, "sửa đổi" này sẽ rất nhanh. Tương tự

  newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence 

sẽ tạo ra một chuỗi mới với một newValuefor ở vị trí của phần tử 3000 của nó. Một lần nữa, nó không phá hủy chuỗi cũ, nó chỉ tạo ra một chuỗi mới. Nhưng, nó thực hiện điều này rất hiệu quả, lấy O (log (min (k, kn)) trong đó n là độ dài của chuỗi và k là chỉ số bạn sửa đổi.

Bạn không thể dễ dàng làm điều này với VectorsArrays. Chúng có thể được sửa đổi nhưng đó là sửa đổi bắt buộc thực sự, và vì vậy không thể được thực hiện bằng mã Haskell thông thường. Điều đó có nghĩa là các hoạt động trong Vectorgói thực hiện sửa đổi như thế nào snocconsphải sao chép toàn bộ vectơ để mất O(n)thời gian. Ngoại lệ duy nhất này là bạn có thể sử dụng phiên bản có thể thay đổi ( Vector.Mutable) bên trong STđơn nguyên (hoặc IO) và thực hiện tất cả các sửa đổi của mình giống như bạn làm trong một ngôn ngữ bắt buộc. Khi bạn đã hoàn tất, bạn "đóng băng" vector của mình để chuyển sang cấu trúc bất biến mà bạn muốn sử dụng với mã thuần.

Cảm giác của tôi là bạn nên mặc định sử dụng Data.Sequencenếu một danh sách không phù hợp. Data.VectorChỉ sử dụng nếu kiểu sử dụng của bạn không liên quan đến việc thực hiện nhiều sửa đổi hoặc nếu bạn cần hiệu suất cực cao trong các đơn vị ST / IO.

Nếu tất cả các cuộc nói chuyện này của các STđơn vị đang khiến bạn bối rối: tất cả lý do nhiều hơn để gắn bó với tinh khiết nhanh và đẹp Data.Sequence.


45
Một cái nhìn sâu sắc mà tôi đã nghe là các danh sách về cơ bản có cấu trúc điều khiển giống như cấu trúc dữ liệu trong Haskell. Và điều này có ý nghĩa: nơi bạn sẽ sử dụng kiểu vòng lặp C cho một ngôn ngữ khác, bạn sẽ sử dụng một [1..]danh sách trong Haskell. Danh sách cũng có thể được sử dụng cho những điều thú vị như quay lui. Suy nghĩ về chúng như các cấu trúc điều khiển (loại) thực sự giúp hiểu được cách chúng được sử dụng.
Tikhon Jelvis

21
Câu trả lời tuyệt vời. Khiếu nại duy nhất của tôi là "Chuỗi là chức năng" đang nhấn mạnh chúng một chút. Trình tự là awesomesauce chức năng. Một phần thưởng khác cho họ là tham gia và chia tách nhanh (log n).
Dan Burton

3
@DanBurton Hội chợ. Tôi đã có thể nhấn mạnh Data.Sequence. Cây ngón tay là một trong những phát minh tuyệt vời nhất trong lịch sử điện toán (Guibas có lẽ sẽ nhận được giải thưởng Turing một ngày nào đó) và Data.Sequencelà một triển khai tuyệt vời và có API rất tiện dụng.
Philip JF

3
"UseData.Vector chỉ khi kiểu sử dụng của bạn không liên quan đến việc thực hiện nhiều sửa đổi hoặc nếu bạn cần hiệu suất cực cao trong các đơn vị ST / IO .." Từ ngữ thú vị, bởi vì nếu bạn đang thực hiện nhiều sửa đổi (như lặp đi lặp lại (100 nghìn lần) phát triển 100k yếu tố), sau đó bạn làm cần ST / IO Vector để có được hiệu suất chấp nhận được,
misterbee

4
Các mối quan tâm về vectơ (thuần túy) và sao chép được giảm bớt một phần bởi phản ứng tổng hợp luồng, ví dụ: điều này: import qualified Data.Vector.Unboxed as VU; main = print (VU.cons 'a' (VU.replicate 100 'b'))biên dịch thành một phân bổ 404 byte (101 ký tự) trong Core: hpaste.org/65015
FunctorSalad
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.