Có cách nào thực tế để cấu trúc nút được liên kết là bất biến không?


26

Tôi quyết định viết một danh sách liên kết đơn, và có kế hoạch đi vào để làm cho cấu trúc nút liên kết bên trong không thay đổi.

Tôi chạy vào một snag mặc dù. Nói rằng tôi có các nút được liên kết sau (từ các addhoạt động trước ):

1 -> 2 -> 3 -> 4

và nói rằng tôi muốn nối thêm a 5.

Để làm điều này, vì nút 4là bất biến, tôi cần tạo một bản sao mới 4, nhưng thay thế nexttrường của nó bằng một nút mới chứa a 5. Vấn đề là bây giờ, 3là tham khảo cái cũ 4; một mà không có phụ lục 5. Bây giờ tôi cần sao chép 3và thay thế nexttrường của nó để tham chiếu 4bản sao, nhưng bây giờ 2là tham chiếu 3...

Hay nói cách khác, để thực hiện nối thêm, toàn bộ danh sách dường như cần được sao chép.

Những câu hỏi của tôi:

  • Suy nghĩ của tôi có đúng không? Có cách nào để thực hiện nối thêm mà không sao chép toàn bộ cấu trúc không?

  • Rõ ràng "Java hiệu quả" chứa phần giới thiệu:

    Các lớp học nên bất biến trừ khi có một lý do rất chính đáng để khiến chúng biến đổi ...

    Đây có phải là một trường hợp tốt cho tính đột biến?

Tôi không nghĩ rằng đây là một bản sao của câu trả lời được đề xuất vì tôi không nói về danh sách đó; điều đó rõ ràng phải có thể thay đổi để phù hợp với giao diện (mà không làm gì đó như giữ danh sách mới bên trong và truy xuất nó thông qua một getter. Mặc dù vậy, ngay cả khi điều đó sẽ yêu cầu một số đột biến; nó sẽ chỉ được giữ ở mức tối thiểu). Tôi đang nói về việc liệu nội bộ của danh sách có phải là bất biến hay không.


Tôi sẽ nói có - các bộ sưu tập bất biến hiếm hoi trong Java là những bộ có các phương thức đột biến được ghi đè để ném hoặc các CopyOnWritexxxlớp cực kỳ đắt tiền được sử dụng cho đa luồng. Không ai mong đợi các bộ sưu tập sẽ thực sự bất biến (mặc dù nó tạo ra một số điều
kỳ quặc

9
(Nhận xét về trích dẫn.) Điều đó, một trích dẫn tuyệt vời, thường bị hiểu lầm rất nhiều. Tính không thay đổi nên được áp dụng cho ValueObjectlợi ích của nó , nhưng nếu bạn đang phát triển bằng Java, việc sử dụng tính bất biến ở những nơi mà tính đột biến được dự đoán là không thực tế. Hầu hết thời gian, một lớp học có những khía cạnh nhất định nên bất biến, và những khía cạnh nhất định cần có thể thay đổi dựa trên yêu cầu.
rwong

2
liên quan (có thể là một bản sao): Đối tượng bộ sưu tập tốt hơn là bất biến? "Không có chi phí hiệu năng, bộ sưu tập này (lớp LinkedList) có thể được giới thiệu là bộ sưu tập không thay đổi không?"
gnat

Tại sao bạn sẽ tạo một danh sách liên kết bất biến? Lợi ích lớn nhất của danh sách được liên kết là tính dễ thay đổi, bạn sẽ tốt hơn với một mảng chứ?
Pieter B

3
Bạn có thể vui lòng giúp tôi hiểu yêu cầu của bạn? Bạn muốn có một danh sách bất biến, mà bạn muốn đột biến? Đó không phải là một mâu thuẫn sao? Bạn có ý nghĩa gì với "nội bộ" của danh sách? Bạn có nghĩa là các nút được thêm trước đó không nên sửa đổi sau này mà chỉ cho phép nối thêm vào nút cuối cùng trong danh sách?
null

Câu trả lời:


46

Với danh sách bằng các ngôn ngữ chức năng, bạn hầu như luôn làm việc với đầu và đuôi, phần tử đầu tiên và phần còn lại của danh sách. Việc chuẩn bị phổ biến hơn nhiều bởi vì, như bạn phỏng đoán, việc nối thêm yêu cầu sao chép toàn bộ danh sách (hoặc các cấu trúc dữ liệu lười biếng khác không giống với danh sách được liên kết).

Trong các ngôn ngữ bắt buộc, việc nối thêm là phổ biến hơn nhiều, bởi vì nó có xu hướng cảm thấy tự nhiên hơn về mặt ngữ nghĩa và bạn không quan tâm đến việc vô hiệu hóa các tham chiếu đến các phiên bản trước của danh sách.

Như một ví dụ về lý do tại sao việc chuẩn bị trước không yêu cầu sao chép toàn bộ danh sách, hãy xem xét bạn có:

2 -> 3 -> 4

Chuẩn bị một 1cung cấp cho bạn:

1 -> 2 -> 3 -> 4

Nhưng lưu ý rằng không có vấn đề gì nếu người khác vẫn giữ tham chiếu 2là người đứng đầu danh sách của họ, vì danh sách này là bất biến và các liên kết chỉ đi một chiều. Không có cách nào để nói 1là thậm chí còn ở đó nếu bạn chỉ có một tài liệu tham khảo 2. Bây giờ, nếu bạn thêm một 5danh sách vào một trong hai danh sách, bạn sẽ phải tạo một bản sao của toàn bộ danh sách, vì nếu không nó cũng sẽ xuất hiện trong danh sách khác.


Ahh, điểm tốt về lý do tại sao việc chuẩn bị trước không yêu cầu một bản sao; Tôi đã không nghĩ về điều đó.
Carcigenicate

17

Bạn đã đúng, việc nối thêm yêu cầu sao chép toàn bộ danh sách nếu bạn không muốn thay đổi bất kỳ nút nào tại chỗ. Vì chúng ta cần đặt nextcon trỏ của nút thứ hai (bây giờ), trong một cài đặt không thay đổi sẽ tạo một nút mới, và sau đó chúng ta cần đặt nextcon trỏ của nút thứ ba đến cuối cùng, v.v.

Tôi nghĩ rằng vấn đề cốt lõi ở đây không phải là bất biến, cũng không phải là appendhoạt động không được khuyến khích. Cả hai đều hoàn toàn tốt trong lĩnh vực tương ứng của họ. Trộn chúng là xấu: Giao diện tự nhiên (hiệu quả) cho một danh sách không thay đổi được nhấn mạnh thao tác ở phía trước danh sách, nhưng đối với các danh sách có thể thay đổi, việc xây dựng danh sách bằng cách nối tiếp các mục từ đầu đến cuối là điều tự nhiên hơn.

Vì vậy, tôi khuyên bạn nên quyết định: Bạn có muốn giao diện phù du hay giao diện bền bỉ không? Do hầu hết các hoạt động tạo ra một danh sách mới và để lại một phiên bản chưa sửa đổi có thể truy cập (liên tục) hoặc bạn viết mã như thế này (phù du):

list.append(x);
list.prepend(y);

Cả hai lựa chọn đều ổn, nhưng việc triển khai sẽ phản ánh giao diện: Cấu trúc dữ liệu liên tục được hưởng lợi từ các nút bất biến, trong khi một phù du cần có khả năng biến đổi bên trong để thực sự thực hiện được hiệu suất mà nó thực hiện. java.util.Listvà các giao diện khác là phù du: Việc thực hiện chúng trong một danh sách bất biến là không phù hợp và trên thực tế là một mối nguy hiểm về hiệu suất. Các thuật toán tốt trên các cấu trúc dữ liệu có thể thay đổi thường khá khác với các thuật toán tốt trên các cấu trúc dữ liệu bất biến, do đó, việc thay đổi cấu trúc dữ liệu bất biến thành một cấu trúc dữ liệu có thể thay đổi (hoặc ngược lại) mời các thuật toán xấu.

Trong khi một danh sách dai dẳng có một số nhược điểm (không phụ thêm hiệu quả), nhu cầu này không phải là một vấn đề nghiêm trọng khi lập trình chức năng: Nhiều thuật toán có thể được xây dựng một cách hiệu quả bằng cách thay đổi những suy nghĩ và sử dụng các chức năng bậc cao như maphay fold(để đặt tên cho hai người tương đối nguyên thủy ), hoặc bằng cách trả trước nhiều lần. Hơn nữa, không ai bắt buộc bạn chỉ sử dụng cấu trúc dữ liệu này: Khi những người khác (phù du, hoặc kiên trì nhưng tinh vi hơn) phù hợp hơn, hãy sử dụng chúng. Tôi cũng nên lưu ý rằng danh sách liên tục có một số lợi thế cho khối lượng công việc khác: Chúng chia sẻ đuôi của chúng, có thể bảo tồn bộ nhớ.


4

Nếu bạn có một danh sách liên kết đơn, bạn sẽ làm việc với mặt trước nếu nó nhiều hơn so với mặt sau.

Các ngôn ngữ chức năng như prolog và haskel cung cấp các cách dễ dàng để có được phần tử phía trước và phần còn lại của mảng. Nối vào mặt sau là thao tác O (n) với các bản sao mỗi nút.


1
Tôi đã sử dụng Haskell. Afaik, nó cũng phá vỡ một phần của vấn đề bằng cách sử dụng sự lười biếng. Tôi đang thực hiện bổ sung vì tôi nghĩ đó là những gì được mong đợi bởi Listgiao diện (mặc dù tôi có thể sai ở đó). Tôi không nghĩ rằng bit thực sự trả lời câu hỏi mặc dù. Toàn bộ danh sách sẽ vẫn cần được sao chép; nó sẽ chỉ làm cho quyền truy cập vào phần tử cuối cùng được thêm nhanh hơn vì không cần phải truyền tải đầy đủ.
Carcigenicate

Tạo một lớp theo một giao diện không khớp với cấu trúc / thuật toán dữ liệu cơ bản là một lời mời đến sự đau đớn và không hiệu quả.
quái vật ratchet

JavaDocs sử dụng rõ ràng từ "chắp thêm". Bạn đang nói tốt hơn là bỏ qua điều đó vì lợi ích của việc thực hiện?
Carcigenicate

2
@Carcigenicate no Tôi đang nói rằng thật sai lầm khi thử và khớp một danh sách liên kết đơn lẻ với các nút bất biến vào khuôn củajava.util.List
ratchet freak

1
Với sự lười biếng sẽ không có một danh sách liên kết nữa; không có danh sách, do đó không có đột biến (chắp thêm). Thay vào đó là một số mã tạo ra mục tiếp theo theo yêu cầu. Nhưng nó không thể được sử dụng ở mọi nơi mà danh sách được sử dụng. Cụ thể, nếu bạn viết một phương thức trong Java dự kiến ​​sẽ thay đổi một danh sách được truyền vào dưới dạng đối số, thì danh sách đó phải có thể thay đổi ở vị trí đầu tiên. Cách tiếp cận của trình tạo yêu cầu tái cấu trúc hoàn chỉnh (sắp xếp lại) mã để làm cho nó hoạt động - và để có thể loại bỏ danh sách một cách vật lý.
rwong

4

Như những người khác đã chỉ ra, bạn đúng rằng một danh sách liên kết đơn bất biến đòi hỏi phải sao chép toàn bộ danh sách khi thực hiện một thao tác nối thêm.

Thường thì bạn có thể sử dụng cách giải quyết khi thực hiện thuật toán của mình về các conshoạt động (trả trước), sau đó đảo ngược danh sách cuối cùng một lần. Điều này vẫn cần phải sao chép danh sách một lần, nhưng chi phí phức tạp là tuyến tính theo chiều dài của danh sách, trong khi bằng cách sử dụng lặp đi lặp lại, bạn có thể dễ dàng có được độ phức tạp bậc hai.

Danh sách khác biệt (xem ví dụ ở đây ) là một thay thế thú vị. Một danh sách khác biệt bao bọc một danh sách và cung cấp một hoạt động chắp thêm trong thời gian không đổi. Về cơ bản, bạn làm việc với trình bao bọc miễn là bạn cần nối thêm, sau đó chuyển đổi trở lại danh sách khi bạn kết thúc. Điều này tương tự như những gì bạn làm khi bạn sử dụng một StringBuilderchuỗi để xây dựng một chuỗi và cuối cùng nhận được kết quả là một String(không thay đổi!) Bằng cách gọi toString. Một sự khác biệt là a StringBuilderlà có thể thay đổi, nhưng một danh sách khác biệt là bất biến. Ngoài ra, khi bạn chuyển đổi một danh sách khác biệt trở lại danh sách, bạn vẫn phải xây dựng toàn bộ danh sách mới, nhưng, một lần nữa, bạn chỉ cần thực hiện việc này một lần.

Sẽ khá dễ dàng để thực hiện một DListlớp bất biến cung cấp giao diện tương tự như của Haskell Data.DList.


4

Bạn phải xem video tuyệt vời này từ 2015 React conf của nhà sáng tạo của Immutable.js Lee Byron. Nó sẽ cung cấp cho bạn các gợi ý và cấu trúc để hiểu cách thực hiện một danh sách bất biến hiệu quả mà không trùng lặp nội dung. Các ý tưởng cơ bản là: - miễn là hai danh sách sử dụng các nút giống nhau (cùng giá trị, cùng một nút tiếp theo), cùng một nút được sử dụng - khi các danh sách bắt đầu khác nhau, một cấu trúc được tạo tại nút phân kỳ, giữ các con trỏ tới nút cụ thể tiếp theo của mỗi danh sách

Hình ảnh này từ một hướng dẫn phản ứng có thể rõ ràng hơn tiếng Anh bị hỏng của tôi:nhập mô tả hình ảnh ở đây


2

Nó không hoàn toàn là Java, nhưng tôi khuyên bạn nên đọc bài viết này về một cấu trúc dữ liệu bền vững được lập chỉ mục nhưng được lập chỉ mục được viết bằng Scala:

http://www.codecommit.com/blog/scala/imâying-ipersistent-vector-in-scala

Vì nó là cấu trúc dữ liệu Scala, nên nó cũng có thể được sử dụng từ Java (với độ dài hơn một chút). Nó dựa trên cấu trúc dữ liệu có sẵn trong Clojure và tôi chắc chắn rằng có một thư viện Java "bản địa" hơn cũng cung cấp nó.

Ngoài ra, một lưu ý khi xây dựng cấu trúc dữ liệu bất biến: thứ bạn thường muốn là một loại "Builder" nào đó, cho phép bạn "biến đổi" cấu trúc dữ liệu "đang thi công" của mình bằng cách nối các phần tử vào nó (trong một luồng); sau khi bạn hoàn thành việc nối thêm, bạn gọi một phương thức trên đối tượng "đang xây dựng" , .build()hoặc .result()"xây dựng" đối tượng, cung cấp cho bạn cấu trúc dữ liệu bất biến mà sau đó bạn có thể chia sẻ một cách an toàn.


1
Có một câu hỏi về các cổng Java của PersistentVector tại stackoverflow.com/questions/15997996/
mẹo

1

Một cách tiếp cận đôi khi có thể hữu ích là có hai lớp đối tượng giữ danh sách - một đối tượng danh sách chuyển tiếp có finaltham chiếu đến nút đầu tiên trong danh sách được liên kết chuyển tiếp và không finaltham chiếu ban đầu (khi không -null) xác định một đối tượng danh sách ngược giữ các mục tương tự theo thứ tự ngược lại và đối tượng danh sách ngược có finaltham chiếu đến mục cuối cùng của danh sách được liên kết ngược và tham chiếu không cuối cùng ban đầu (khi không -null) xác định một đối tượng trong danh sách chuyển tiếp giữ các mục tương tự theo thứ tự ngược lại.

Chuẩn bị một mục vào danh sách chuyển tiếp hoặc nối thêm một mục vào danh sách ngược sẽ chỉ yêu cầu tạo một nút mới liên kết đến nút được xác định bởi finaltham chiếu và tạo một đối tượng danh sách mới cùng loại với finaltham chiếu ban đầu đến nút mới đó.

Việc thêm một mục vào danh sách chuyển tiếp hoặc chuẩn bị vào danh sách ngược sẽ yêu cầu phải có một danh sách loại ngược lại; lần đầu tiên được thực hiện với một danh sách cụ thể, nó sẽ tạo một đối tượng mới thuộc loại đối diện và lưu trữ một tham chiếu; lặp lại hành động nên sử dụng lại danh sách.

Lưu ý rằng trạng thái bên ngoài của một đối tượng danh sách sẽ được coi là giống nhau cho dù tham chiếu của nó đến danh sách loại ngược lại là null hoặc xác định danh sách thứ tự ngược lại. Sẽ không cần phải thực hiện bất kỳ biến nào final, ngay cả khi sử dụng mã đa luồng, vì mọi đối tượng danh sách sẽ có một finaltham chiếu đến một bản sao hoàn chỉnh của nội dung của nó.

Nếu mã trên một luồng tạo và lưu trữ một bản sao của danh sách bị đảo ngược và trường mà bản sao đó được lưu vào bộ đệm không dễ bay hơi, thì có thể mã trên một luồng khác có thể không thấy danh sách được lưu trong bộ nhớ cache, nhưng chỉ có tác dụng phụ từ đó sẽ là các luồng khác sẽ cần phải làm thêm việc xây dựng một bản sao đảo ngược khác của danh sách. Bởi vì hành động như vậy sẽ làm giảm hiệu quả kém nhất, nhưng sẽ không ảnh hưởng đến tính chính xác và bởi vì volatilecác biến tạo ra suy giảm hiệu quả của chính chúng, nên sẽ tốt hơn nếu biến đó không biến động và chấp nhận khả năng hoạt động dự phòng không thường xuyê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.