Danh sách liên kết hữu ích trong những trường hợp nào?


110

Hầu hết những lần tôi thấy mọi người cố gắng sử dụng danh sách liên kết, đối với tôi nó dường như là một lựa chọn kém (hoặc rất kém). Có lẽ sẽ hữu ích khi khám phá các trường hợp mà danh sách liên kết có phải là một lựa chọn tốt hay không về cấu trúc dữ liệu.

Lý tưởng nhất là các câu trả lời sẽ giải thích rõ các tiêu chí sử dụng trong việc chọn cấu trúc dữ liệu và cấu trúc dữ liệu nào có khả năng hoạt động tốt nhất trong các trường hợp cụ thể.

Chỉnh sửa: Tôi phải nói rằng, tôi khá ấn tượng không chỉ bởi số lượng, mà còn chất lượng của các câu trả lời. Tôi chỉ có thể chấp nhận một, nhưng có hai hoặc ba cái nữa mà tôi phải nói là đáng để chấp nhận nếu điều gì đó tốt hơn một chút không có ở đó. Chỉ có một vài (đặc biệt là người mà tôi đã chấp nhận) chỉ ra những tình huống mà một danh sách được liên kết mang lại lợi thế thực sự. Tôi nghĩ Steve Jessop xứng đáng được đề cập một cách danh giá vì đã đưa ra không chỉ một, mà là ba câu trả lời khác nhau, tất cả đều tôi thấy khá ấn tượng. Tất nhiên, mặc dù nó chỉ được đăng dưới dạng nhận xét chứ không phải câu trả lời, tôi nghĩ blog của Neil cũng rất đáng đọc - không chỉ mang tính thông tin mà còn khá thú vị.


34
Câu trả lời cho đoạn thứ hai của bạn mất khoảng một học kỳ.
Seva Alekseyev

2
Đối với ý kiến ​​của tôi, hãy xem punchlet.wordpress.com/2009/12/27/letter-the-fourth . Và vì đây có vẻ là một cuộc khảo sát, nó có lẽ phải là CW.

1
@Neil, thật tuyệt, mặc dù tôi nghi ngờ CS Lewis sẽ chấp thuận.
Tom

@Neil: Tôi đoán là một cuộc khảo sát. Hầu hết đó là một nỗ lực để xem liệu ai đó có thể đưa ra câu trả lời có cơ sở mà tôi ít nhất có thể mua là hợp lý. @Seva: vâng, đọc lại, tôi làm câu cuối chung chung hơn một chút so với dự định ban đầu.
Jerry Coffin,

2
@Yar Mọi người (bao gồm cả tôi, tôi rất tiếc phải nói) được sử dụng để triển khai danh sách được liên kết không có con trỏ trong các ngôn ngữ như FORTRAN IV (không có khái niệm về con trỏ), giống như họ đã làm với cây. Bạn đã sử dụng mảng thay vì bộ nhớ "thực".

Câu trả lời:


40

Chúng có thể hữu ích cho các cấu trúc dữ liệu đồng thời. (Hiện có một mẫu sử dụng trong thế giới thực không đồng thời bên dưới - sẽ không có nếu @Neil không đề cập đến FORTRAN. ;-)

Ví dụ: ConcurrentDictionary<TKey, TValue>trong .NET 4.0 RC sử dụng danh sách được liên kết để xâu chuỗi các mục băm vào cùng một nhóm.

Cấu trúc dữ liệu cơ bản cho ConcurrentStack<T>cũng là một danh sách được liên kết.

ConcurrentStack<T>là một trong những cấu trúc dữ liệu đóng vai trò là nền tảng cho Nhóm luồng mới , (về cơ bản, các "hàng đợi" cục bộ được triển khai dưới dạng ngăn xếp). (Cấu trúc hỗ trợ chính khác là ConcurrentQueue<T>.)

Đến lượt mình, Thread Pool mới cung cấp cơ sở cho việc lập lịch làm việc của Thư viện song song nhiệm vụ mới .

Vì vậy, chúng chắc chắn có thể hữu ích - một danh sách được liên kết hiện đang đóng vai trò là một trong những cấu trúc hỗ trợ chính của ít nhất một công nghệ mới tuyệt vời.

(Một danh sách được liên kết đơn lẻ tạo nên sự lựa chọn hấp dẫn không có khóa - nhưng không phải chờ - trong những trường hợp này, vì các hoạt động chính có thể được thực hiện với một CAS duy nhất (+ thử lại). Trong môi trường GC-d hiện đại - chẳng hạn như Java và .NET - vấn đề ABA có thể dễ dàng tránh được. Chỉ cần bọc các mục bạn thêm vào các nút mới tạo và không sử dụng lại các nút đó - hãy để GC thực hiện công việc của nó. Trang về vấn đề ABA cũng cung cấp việc triển khai khóa- ngăn xếp miễn phí - thực sự hoạt động trong .Net (& Java) với Nút (GC-ed) giữ các mục.)

Chỉnh sửa : @Neil: thực ra, những gì bạn đã đề cập về FORTRAN nhắc nhở tôi rằng cùng một loại danh sách liên kết có thể được tìm thấy trong cấu trúc dữ liệu được sử dụng nhiều nhất và bị lạm dụng trong .NET: .NET chung Dictionary<TKey, TValue>.

Không phải một, mà nhiều danh sách liên kết được lưu trữ trong một mảng.

  • Nó tránh thực hiện nhiều phân bổ (de) nhỏ trên các lần chèn / xóa.
  • Việc tải bảng băm ban đầu khá nhanh, vì mảng được lấp đầy tuần tự (hoạt động rất tốt với bộ nhớ cache của CPU).
  • Chưa kể rằng một bảng băm chuỗi là tốn kém về bộ nhớ - và "thủ thuật" này cắt giảm "kích thước con trỏ" trên x64.

Về cơ bản, nhiều danh sách liên kết được lưu trữ trong một mảng. (một cho mỗi nhóm được sử dụng.) Một danh sách miễn phí các nút có thể sử dụng lại được "đan xen" giữa chúng (nếu có lần xóa). Một mảng được cấp phát khi bắt đầu / khi khởi động lại và các nút của chuỗi được giữ trong đó. Ngoài ra còn có một con trỏ miễn phí - một chỉ mục trong mảng - theo sau việc xóa. ;-) Vì vậy, - tin hay không - kỹ thuật FORTRAN vẫn tồn tại. (... và không nơi nào khác, ngoài một trong những cấu trúc dữ liệu .NET được sử dụng phổ biến nhất ;-).


2
Trong trường hợp bạn đã bỏ lỡ, đây là nhận xét của Neil: "Mọi người (bao gồm cả tôi, tôi xin lỗi phải nói) đã sử dụng để triển khai danh sách được liên kết không có con trỏ bằng các ngôn ngữ như FORTRAN IV (không có khái niệm về con trỏ), giống như họ đã làm với cây . Bạn đã sử dụng mảng thay vì bộ nhớ "thực". "
Andras Vass

Tôi nên thêm rằng phương pháp tiếp cận "danh sách được liên kết trong một mảng" trong trường hợp Dictionarytiết kiệm đáng kể hơn trong .NET: nếu không mỗi nút sẽ yêu cầu một đối tượng riêng biệt trên heap - và mọi đối tượng được phân bổ trên heap đều có một số chi phí. ( vi.csharp-online.net/Common_Type_System%E2%80%94Object_Layout )
Andras Vass

Cũng tốt khi biết rằng mặc định của C ++ std::listkhông an toàn trong bối cảnh đa luồng không có khóa.
Mooing Duck

49

Danh sách được liên kết rất hữu ích khi bạn cần thực hiện nhiều lần chèn và xóa, nhưng không phải tìm kiếm quá nhiều, trên một danh sách có độ dài tùy ý (không xác định tại thời điểm biên dịch).

Tách và nối danh sách (được liên kết hai chiều) rất hiệu quả.

Bạn cũng có thể kết hợp danh sách liên kết - ví dụ: cấu trúc cây có thể được thực hiện dưới dạng danh sách liên kết "dọc" (quan hệ cha / con) kết nối với nhau danh sách liên kết ngang (anh chị em).

Sử dụng danh sách dựa trên mảng cho những mục đích này có những hạn chế nghiêm trọng:

  • Thêm một mục mới có nghĩa là mảng phải được phân bổ lại (hoặc bạn phải phân bổ nhiều không gian hơn mức bạn cần để cho phép tăng trưởng trong tương lai và giảm số lượng phân bổ lại)
  • Loại bỏ các mục để lại không gian lãng phí hoặc yêu cầu phân bổ lại
  • chèn các mục vào bất kỳ đâu ngoại trừ phần cuối bao gồm (có thể phân bổ lại và) sao chép nhiều dữ liệu lên một vị trí

5
Vì vậy, câu hỏi đặt ra là, khi nào bạn cần thực hiện nhiều thao tác chèn và xóa ở giữa một chuỗi, nhưng không có nhiều tra cứu trong danh sách theo thứ tự? Việc duyệt qua một danh sách được liên kết thường đắt hơn hoặc đắt hơn sao chép một mảng, vì vậy mọi thứ bạn nói về việc xóa và chèn các mục trong mảng cũng không tốt cho việc truy cập ngẫu nhiên trong danh sách. Bộ nhớ đệm LRU là một ví dụ mà tôi có thể nghĩ đến, bạn cần phải loại bỏ ở giữa rất nhiều, nhưng bạn không bao giờ cần xem lại danh sách.
Steve Jessop

2
Thêm vào danh sách liên quan đến việc phân bổ bộ nhớ cho mọi phần tử bạn thêm vào. Điều này có thể liên quan đến một cuộc gọi hệ thống sẽ rất tốn kém. Việc thêm vào một mảng chỉ yêu cầu một lệnh gọi như vậy nếu mảng phải được phát triển. Trên thực tế, trong hầu hết các ngôn ngữ (chính xác là vì những lý do này), mảng là cấu trúc dữ liệu được ưa thích và các danh sách hầu như không được sử dụng.

1
Giả sử cái nào? Rõ ràng là việc phân bổ đó nhanh chóng đáng kinh ngạc - thường yêu cầu thêm kích thước đối tượng vào một con trỏ. Tổng chi phí cho GC là thấp? Lần trước, tôi đã thử đo nó trên một ứng dụng thực, điểm mấu chốt là Java đang thực hiện tất cả công việc khi bộ xử lý không hoạt động, vì vậy đương nhiên nó không ảnh hưởng nhiều đến hiệu suất hiển thị. Trong tiêu chuẩn CPU bận rộn, rất dễ khiến Java khó chịu và nhận được thời gian phân bổ trong trường hợp xấu nhất. Tuy nhiên, đây là cách đây nhiều năm và việc thu gom rác theo thế hệ đã làm giảm đáng kể tổng chi phí của GC kể từ đó.
Steve Jessop

1
@Steve: Bạn đã sai khi phân bổ là "giống nhau" giữa danh sách và mảng. Mỗi khi bạn cần cấp phát bộ nhớ cho một danh sách, bạn chỉ cần cấp phát một khối nhỏ - O (1). Đối với một mảng, bạn phải cấp phát một khối mới đủ lớn cho toàn bộ danh sách, sau đó sao chép toàn bộ danh sách - O (n). Để chèn vào một vị trí đã biết trong danh sách, bạn cập nhật một số con trỏ cố định - O (1), nhưng để chèn vào một mảng và sao chép bất kỳ mục nào sau đó lên một vị trí để nhường chỗ cho việc chèn - O (n). Có nhiều trường hợp do đó mảng kém hiệu quả hơn nhiều so với LL.
Jason Williams

1
@Jerry: Tôi hiểu. Quan điểm của tôi là một phần lớn chi phí của việc phân bổ lại mảng không phải là cấp phát bộ nhớ , đó là nhu cầu sao chép toàn bộ nội dung mảng vào bộ nhớ mới. Để chèn vào mục 0 của một mảng, bạn phải sao chép toàn bộ nội dung mảng lên một vị trí trong bộ nhớ. Tôi không nói mảng là xấu; chỉ là có những tình huống không cần truy cập ngẫu nhiên và khi chèn / xóa / liên kết lại LLs thực sự không đổi theo thời gian thực sự được ưu tiên hơn.
Jason Williams

20

Danh sách được liên kết rất linh hoạt: Với việc sửa đổi một con trỏ, bạn có thể thực hiện một thay đổi lớn, trong đó hoạt động tương tự sẽ rất kém hiệu quả trong danh sách mảng.


Có thể thúc đẩy lý do tại sao sử dụng một danh sách mà không phải một tập hợp hoặc bản đồ?
Patrik

14

Mảng là cấu trúc dữ liệu mà Danh sách được Liên kết thường được so sánh với nhau.

Danh sách được liên kết thông thường hữu ích khi bạn phải thực hiện nhiều sửa đổi đối với chính danh sách trong khi các mảng hoạt động tốt hơn danh sách khi truy cập phần tử trực tiếp.

Dưới đây là danh sách các thao tác có thể được thực hiện trên danh sách và mảng, được so sánh với chi phí hoạt động tương đối (n = độ dài danh sách / mảng):

  • Thêm một phần tử:
    • trên danh sách, bạn chỉ cần cấp phát bộ nhớ cho phần tử mới và chuyển hướng con trỏ. O (1)
    • trên mảng bạn phải định vị lại mảng. Trên)
  • Xóa một phần tử
    • trên danh sách bạn chỉ chuyển hướng con trỏ. O (1).
    • trên mảng, bạn dành O (n) thời gian để định vị lại mảng nếu phần tử cần xóa không phải là phần tử đầu tiên hoặc cuối cùng của mảng; nếu không, bạn chỉ có thể định vị lại con trỏ đến đầu mảng hoặc giảm độ dài mảng
  • Nhận một phần tử ở một vị trí đã biết:
    • trên danh sách, bạn phải di chuyển danh sách từ phần tử đầu tiên đến phần tử ở vị trí cụ thể. Trường hợp tệ nhất: O (n)
    • trên mảng, bạn có thể truy cập phần tử ngay lập tức. O (1)

Đây là một so sánh cấp thấp của hai cấu trúc dữ liệu phổ biến và cơ bản này và bạn có thể thấy rằng danh sách hoạt động tốt hơn trong các tình huống mà bạn phải thực hiện nhiều sửa đổi đối với danh sách đó (xóa hoặc thêm phần tử). Mặt khác, mảng hoạt động tốt hơn danh sách khi bạn phải truy cập trực tiếp vào các phần tử của mảng.

Từ quan điểm của việc phân bổ bộ nhớ, danh sách tốt hơn vì không cần phải có tất cả các phần tử bên cạnh nhau. Mặt khác, có (ít) chi phí lưu trữ các con trỏ đến phần tử tiếp theo (hoặc thậm chí đến phần tử trước đó).

Biết những khác biệt này là điều quan trọng đối với các nhà phát triển để lựa chọn giữa danh sách và mảng trong triển khai của họ.

Lưu ý rằng đây là so sánh giữa danh sách và mảng. Có các giải pháp tốt cho các vấn đề được báo cáo ở đây (ví dụ: SkipLists, Dynamic Arrays, v.v.). Trong câu trả lời này, tôi đã tính đến cấu trúc dữ liệu cơ bản mà mọi lập trình viên nên biết.


Điều này hơi đúng đối với việc triển khai tốt các danh sách và việc triển khai các mảng một cách tồi tệ. Hầu hết các triển khai mảng đều phức tạp hơn nhiều so với mức bạn cấp cho chúng. Và tôi không nghĩ rằng bạn hiểu việc phân bổ bộ nhớ động có thể tốn kém như thế nào.

Câu trả lời này không bao gồm chương trình của một khóa học Đại học về cấu trúc dữ liệu. Đây là bản so sánh được viết có tính đến các danh sách và mảng được liên kết, được triển khai theo cách bạn, tôi và hầu hết mọi người biết. Mảng mở rộng hình học, Danh sách bỏ qua, v.v. là những giải pháp mà tôi biết, tôi sử dụng và tôi nghiên cứu nhưng điều đó sẽ cần một lời giải thích sâu hơn và điều đó sẽ không phù hợp với câu trả lời ngăn xếp.
Andrea Zilio

1
"Từ quan điểm của việc phân bổ bộ nhớ, danh sách tốt hơn vì không cần phải có tất cả các phần tử bên cạnh nhau." Ngược lại, các thùng chứa liền kề sẽ tốt hơn chúng giữ các phần tử bên cạnh nhau. Trên máy tính hiện đại, dữ liệu cục bộ là vua. Tất cả những gì nhảy xung quanh trong bộ nhớ sẽ giết chết hiệu suất bộ nhớ cache của bạn và dẫn đến các chương trình chèn một phần tử tại một vị trí ngẫu nhiên (hiệu quả) hoạt động nhanh hơn với một mảng động chẳng hạn như C ++ std::vectorso với với một danh sách được liên kết chẳng hạn như C ++ std::list, đơn giản là vì đi qua danh sách quá đắt.
David Stone

@DavidStone Có thể tôi không đủ rõ ràng, nhưng với câu đó, tôi đang đề cập đến thực tế là bạn không cần phải có không gian liền kề để lưu trữ các phần tử của mình. Cụ thể là nếu bạn muốn lưu trữ một thứ gì đó không quá nhỏ và bạn có bộ nhớ khả dụng hạn chế, bạn có thể không có đủ dung lượng trống liền kề để lưu trữ dữ liệu của mình, nhưng bạn có thể phù hợp với dữ liệu của mình bằng cách sử dụng danh sách (mặc dù bạn sẽ có chi phí của con trỏ ... cả vì không gian mà chúng chiếm và các vấn đề về hiệu suất mà bạn đã đề cập). Tôi có lẽ nên cập nhật câu trả lời của mình để làm cho nó rõ ràng hơn.
Andrea Zilio

4

Danh sách liên kết đơn là một lựa chọn tốt cho danh sách miễn phí trong bộ cấp phát ô hoặc nhóm đối tượng:

  1. Bạn chỉ cần một ngăn xếp, vì vậy một danh sách được liên kết đơn lẻ là đủ.
  2. Mọi thứ đã được chia thành các nút rồi. Không có chi phí phân bổ cho một nút danh sách xâm nhập, miễn là các ô đủ lớn để chứa một con trỏ.
  3. Một vectơ hoặc deque sẽ áp đặt chi phí của một con trỏ trên mỗi khối. Điều này rất quan trọng vì khi bạn tạo heap lần đầu tiên, tất cả các ô đều miễn phí, vì vậy đó là chi phí trả trước. Trong trường hợp xấu nhất, nó tăng gấp đôi yêu cầu bộ nhớ cho mỗi ô.

Vâng, đã đồng ý. Nhưng có bao nhiêu lập trình viên thực sự đang tạo ra những thứ như vậy? Hầu hết chỉ đơn giản là thực hiện lại những gì std :: list, vv cung cấp cho bạn. Và thực sự "xâm nhập" thường có một ý nghĩa hơi khác so với những gì bạn đã đưa ra - rằng mỗi phần tử danh sách có thể chứa một con trỏ tách biệt với dữ liệu.

1
Bao nhiêu? Nhiều hơn 0, ít hơn một triệu ;-) Câu hỏi của Jerry là "sử dụng tốt các danh sách", hay "sử dụng tốt các danh sách mà mọi lập trình viên sử dụng hàng ngày", hay điều gì đó ở giữa? Tôi không biết bất kỳ tên nào khác ngoài "xâm nhập" cho nút danh sách được chứa bên trong đối tượng là một phần tử danh sách - cho dù là một phần của liên hợp (trong điều kiện C) hay không. Điểm 3 chỉ áp dụng trong các ngôn ngữ cho phép bạn làm điều đó - C, C ++, trình hợp dịch tốt. Java xấu.
Steve Jessop

4

Danh sách được liên kết đôi là một lựa chọn tốt để xác định thứ tự của một bản đồ băm cũng xác định thứ tự trên các phần tử (LinkedHashMap trong Java), đặc biệt khi được sắp xếp theo lần truy cập cuối cùng:

  1. Nhiều chi phí bộ nhớ hơn một vectơ hoặc deque liên quan (2 con trỏ thay vì 1), nhưng hiệu suất chèn / xóa tốt hơn.
  2. Không có chi phí phân bổ, vì bạn vẫn cần một nút cho mục nhập băm.
  3. Vị trí của tham chiếu không phải là vấn đề bổ sung so với vector hoặc deque của con trỏ, vì bạn phải kéo từng đối tượng vào bộ nhớ theo cách nào đó.

Chắc chắn, bạn có thể tranh luận về việc liệu bộ nhớ đệm LRU có phải là một ý tưởng tốt ngay từ đầu hay không, so với một thứ gì đó phức tạp và dễ điều chỉnh hơn, nhưng nếu bạn chuẩn bị có một bộ nhớ cache, thì đây là một cách triển khai khá ổn. Bạn không muốn thực hiện xóa-từ-giữa-và-thêm-vào-cuối trên một vectơ hoặc deque ở mỗi lần truy cập đọc, nhưng di chuyển một nút đến đuôi thường là tốt.


4

Danh sách được liên kết là một trong những lựa chọn tự nhiên khi bạn không thể kiểm soát nơi dữ liệu của mình được lưu trữ, nhưng bạn vẫn cần bằng cách nào đó đi từ đối tượng này sang đối tượng tiếp theo.

Ví dụ: khi triển khai theo dõi bộ nhớ trong C ++ (thay thế mới / xóa), bạn cần một số cấu trúc dữ liệu điều khiển theo dõi con trỏ nào đã được giải phóng, mà bạn hoàn toàn cần phải tự thực hiện. Giải pháp thay thế là định vị tổng thể và thêm danh sách liên kết vào đầu mỗi đoạn dữ liệu.

Bởi vì bạn luôn biết rằng bạn đang ở đâu trong danh sách khi lệnh xóa được gọi, bạn có thể dễ dàng loại bỏ bộ nhớ trong O (1). Ngoài ra, thêm một đoạn mới vừa bị lỗi nằm trong O (1). Đi bộ danh sách là rất hiếm khi cần thiết trong trường hợp này, vì vậy chi phí O (n) không phải là một vấn đề ở đây (dù sao đi bộ một cấu trúc là O (n)).


3

Chúng hữu ích khi bạn cần đẩy, bật và xoay tốc độ cao và đừng bận tâm đến việc lập chỉ mục O (n).


Bạn đã bao giờ bận tâm đến thời gian của danh sách liên kết C ++ so với (giả sử) một deque?

@Neil: Không thể nói rằng tôi có.
Ignacio Vazquez-Abrams

@Neil: nếu C ++ đã cố tình phá hoại lớp danh sách liên kết của nó để làm cho nó chậm hơn bất kỳ vùng chứa nào khác (không xa sự thật), thì điều đó có liên quan gì đến một câu hỏi bất khả tri về ngôn ngữ? Một danh sách liên kết xâm nhập vẫn là một danh sách được liên kết.
Steve Jessop

@Steve C ++ là một ngôn ngữ. Tôi không thể thấy làm thế nào nó có thể có biến động. Nếu bạn đang gợi ý rằng các thành viên của Ủy ban C ++ bằng cách nào đó đã phá hoại các danh sách được liên kết (về mặt logic phải chậm đối với nhiều hoạt động), thì hãy nêu tên những kẻ có tội!

3
Đó không thực sự là sự phá hoại - các nút danh sách bên ngoài có lợi thế của chúng, nhưng hiệu suất không phải là một trong số đó. Tuy nhiên, chắc chắn mọi người đều nhận thức được khi đánh đổi thứ mà bạn biết, đó là điều khá khó khăn để tìm ra cách sử dụng tốt std::list. Một danh sách xâm nhập không phù hợp với triết lý C ++ về các yêu cầu tối thiểu đối với các phần tử vùng chứa.
Steve Jessop

3

Danh sách liên kết đơn là cách triển khai rõ ràng kiểu dữ liệu "danh sách" phổ biến trong các ngôn ngữ lập trình chức năng:

  1. Việc thêm vào head rất nhanh (append (list x) (L))(append (list y) (L))có thể chia sẻ hầu hết tất cả dữ liệu của họ. Không cần copy-on-write bằng một ngôn ngữ không ghi. Các lập trình viên chức năng biết cách tận dụng điều này.
  2. Việc thêm vào đuôi rất tiếc là chậm, nhưng bất kỳ cách triển khai nào khác cũng vậy.

Để so sánh, một vectơ hoặc deque thường sẽ chậm được thêm vào ở một trong hai đầu, yêu cầu (ít nhất là trong ví dụ của tôi về hai phần bổ sung khác nhau) phải lấy một bản sao của toàn bộ danh sách (vectơ) hoặc khối chỉ mục và khối dữ liệu được thêm vào (deque). Trên thực tế, có thể có điều gì đó được nói ở đó cho deque trong danh sách lớn cần thêm vào đuôi vì một số lý do, tôi không đủ thông tin về lập trình chức năng để đánh giá.


3

Một ví dụ về cách sử dụng tốt cho danh sách liên kết là nơi các phần tử danh sách rất lớn, tức là. đủ lớn để chỉ một hoặc hai có thể vừa trong bộ nhớ đệm CPU cùng một lúc. Tại thời điểm này, lợi thế mà các vùng chứa khối liền kề như vectơ hoặc mảng để lặp lại có ít nhiều bị vô hiệu hóa và lợi thế về hiệu suất có thể có nếu nhiều lần chèn và xóa diễn ra trong thời gian thực.


2

Từ kinh nghiệm của tôi, việc triển khai ma trận thưa thớt và đống fibonacci. Danh sách được liên kết cung cấp cho bạn nhiều quyền kiểm soát hơn đối với cấu trúc tổng thể đối với cấu trúc dữ liệu như vậy. Mặc dù tôi không chắc liệu ma trận thưa thớt có được triển khai tốt nhất bằng cách sử dụng danh sách được liên kết hay không - có lẽ có một cách tốt hơn, nhưng nó thực sự giúp tìm hiểu chi tiết của ma trận thưa thớt bằng cách sử dụng danh sách liên kết trong CS chưa tốt nghiệp :)


1

Có hai phép toán bổ sung cho nhau là O (1) trên danh sách và rất khó thực hiện trong O (1) trong các cấu trúc dữ liệu khác - loại bỏ và chèn một phần tử từ vị trí tùy ý, giả sử bạn cần duy trì thứ tự của các phần tử.

Bản đồ băm rõ ràng có thể thực hiện chèn và xóa trong O (1) nhưng sau đó bạn không thể lặp lại các phần tử theo thứ tự.

Với thực tế ở trên, bản đồ băm có thể được kết hợp với một danh sách được liên kết để tạo bộ đệm LRU tiện lợi: Một bản đồ lưu trữ một số lượng cố định các cặp khóa-giá trị và loại bỏ khóa ít được truy cập gần đây nhất để nhường chỗ cho các cặp khóa mới.

Các mục trong bản đồ băm cần có con trỏ đến các nút danh sách được liên kết. Khi truy cập vào bản đồ băm, nút danh sách được liên kết được hủy liên kết khỏi vị trí hiện tại của nó và được chuyển đến đầu danh sách (O (1), yay cho danh sách được liên kết!). Khi cần loại bỏ phần tử ít được sử dụng gần đây nhất, phần tử từ phần cuối của danh sách cần được loại bỏ (một lần nữa O (1) giả sử bạn giữ con trỏ đến nút đuôi) cùng với mục nhập bản đồ băm được liên kết (vì vậy các liên kết ngược từ danh sách cho bản đồ băm là cần thiết.)


1

Hãy xem xét rằng một danh sách được liên kết có thể rất hữu ích trong việc triển khai theo phong cách Thiết kế theo hướng miền của một hệ thống bao gồm các phần liên kết với nhau bằng sự lặp lại.

Một ví dụ mà bạn nghĩ đến có thể là nếu bạn đang làm mẫu cho một chuỗi treo. Nếu bạn muốn biết mức độ căng thẳng trên bất kỳ liên kết cụ thể nào, giao diện của bạn có thể bao gồm một getter cho trọng lượng "rõ ràng". Việc triển khai sẽ bao gồm một liên kết yêu cầu liên kết tiếp theo của nó về trọng lượng biểu kiến ​​của nó, sau đó thêm trọng lượng của chính nó vào kết quả. Bằng cách này, toàn bộ chiều dài xuống dưới cùng sẽ được đánh giá bằng một lệnh gọi từ khách hàng của chuỗi.

Là một người đề xuất mã đọc như ngôn ngữ tự nhiên, tôi thích cách lập trình viên hỏi một liên kết chuỗi rằng nó đang mang trọng lượng bao nhiêu. Nó cũng giữ mối quan tâm của việc tính toán những đứa trẻ của các thuộc tính trong ranh giới của việc thực hiện liên kết, loại bỏ nhu cầu về dịch vụ tính toán trọng lượng chuỗi ".


1

Một trong những trường hợp hữu ích nhất mà tôi tìm thấy đối với danh sách được liên kết hoạt động trong các trường quan trọng về hiệu suất như xử lý lưới và hình ảnh, công cụ vật lý và raytracing là khi sử dụng danh sách được liên kết thực sự cải thiện vị trí của tham chiếu và giảm phân bổ heap và đôi khi thậm chí còn giảm sử dụng bộ nhớ so với các lựa chọn thay thế đơn giản.

Bây giờ điều đó có vẻ giống như một oxymoron hoàn chỉnh mà các danh sách được liên kết có thể làm tất cả những điều đó vì chúng nổi tiếng là thường làm ngược lại, nhưng chúng có một thuộc tính duy nhất là mỗi nút danh sách có kích thước cố định và các yêu cầu về căn chỉnh mà chúng ta có thể khai thác để cho phép chúng sẽ được lưu trữ liên tục và được xóa trong thời gian không đổi theo những cách mà những thứ có kích thước thay đổi không thể.

Do đó, chúng ta hãy sử dụng một trường hợp mà chúng ta muốn thực hiện tương tự như việc lưu trữ một chuỗi có độ dài thay đổi chứa một triệu chuỗi con có độ dài thay đổi lồng nhau. Một ví dụ cụ thể là một lưới được lập chỉ mục lưu trữ một triệu đa giác (một số hình tam giác, một số hình tứ giác, một số hình ngũ giác, một số hình lục giác, v.v.) và đôi khi đa giác bị xóa khỏi bất kỳ vị trí nào trong lưới và đôi khi đa giác được xây dựng lại để chèn một đỉnh vào một đa giác hiện có hoặc loại bỏ một. Trong trường hợp đó, nếu chúng ta lưu trữ một triệu nhỏ std::vectors, thì cuối cùng chúng ta sẽ phải đối mặt với việc phân bổ đống cho mỗi vectơ đơn lẻ cũng như việc sử dụng bộ nhớ có khả năng bùng nổ. Một triệu người nhỏ SmallVectorscó thể không gặp phải vấn đề này nhiều trong các trường hợp phổ biến, nhưng sau đó bộ đệm được phân bổ trước của chúng không được phân bổ riêng biệt theo đống có thể vẫn gây ra việc sử dụng bộ nhớ bùng nổ.

Vấn đề ở đây là một triệu std::vectorphiên bản sẽ cố gắng lưu trữ một triệu thứ có độ dài thay đổi. Những thứ có độ dài thay đổi có xu hướng muốn phân bổ heap vì chúng không thể được lưu trữ liên tục và bị loại bỏ một cách hiệu quả trong thời gian không đổi (ít nhất là theo cách đơn giản mà không có trình phân bổ rất phức tạp) nếu chúng không lưu trữ nội dung của mình ở nơi khác trên heap.

Nếu, thay vào đó, chúng tôi làm điều này:

struct FaceVertex
{
    // Points to next vertex in polygon or -1
    // if we're at the end of the polygon.
    int next;
    ...
};

struct Polygon
{
     // Points to first vertex in polygon.
    int first_vertex;
    ...
};

struct Mesh
{
    // Stores all the face vertices for all polygons.
    std::vector<FaceVertex> fvs;

    // Stores all the polygons.
    std::vector<Polygon> polys;
};

... thì chúng tôi đã giảm đáng kể số lượng phân bổ heap và bỏ sót bộ nhớ cache. Thay vì yêu cầu phân bổ heap và bộ nhớ cache bắt buộc có khả năng bỏ lỡ cho mỗi đa giác mà chúng tôi truy cập, giờ đây chúng tôi chỉ yêu cầu phân bổ heap đó khi một trong hai vectơ được lưu trữ trong toàn bộ lưới vượt quá dung lượng của chúng (chi phí phân bổ). Và trong khi nỗ lực để đi từ đỉnh này sang đỉnh tiếp theo vẫn có thể khiến phần chia sẻ bộ nhớ cache của nó bị bỏ lỡ, nó vẫn thường ít hơn nếu mỗi đa giác lưu trữ một mảng động riêng biệt vì các nút được lưu trữ liền kề và có khả năng là một đỉnh lân cận có thể được truy cập trước khi loại bỏ (đặc biệt khi xem xét rằng nhiều đa giác sẽ thêm các đỉnh của chúng cùng một lúc, điều này làm cho phần sư tử của các đỉnh đa giác tiếp giáp hoàn hảo).

Đây là một ví dụ khác:

nhập mô tả hình ảnh ở đây

... nơi các ô lưới được sử dụng để tăng tốc va chạm hạt-hạt, chẳng hạn như 16 triệu hạt chuyển động trong mỗi khung hình. Trong ví dụ về lưới hạt đó, bằng cách sử dụng danh sách được liên kết, chúng ta có thể di chuyển một hạt từ ô lưới này sang ô lưới khác chỉ bằng cách thay đổi 3 chỉ số. Xóa khỏi một vectơ và đẩy trở lại một vectơ khác có thể đắt hơn đáng kể và giới thiệu nhiều phân bổ đống hơn. Các danh sách được liên kết cũng làm giảm bộ nhớ của một ô xuống 32-bit. Một vectơ, tùy thuộc vào việc triển khai, có thể định vị trước mảng động của nó đến điểm mà nó có thể mất 32 byte cho một vectơ trống. Nếu chúng ta có khoảng một triệu ô lưới, đó là một sự khác biệt.

... và đây là nơi tôi tìm thấy danh sách được liên kết hữu ích nhất hiện nay và đặc biệt tôi nhận thấy sự đa dạng "danh sách được liên kết được lập chỉ mục" hữu ích vì các chỉ số 32 bit giảm một nửa yêu cầu bộ nhớ của các liên kết trên máy 64 bit và chúng ngụ ý rằng các nút được lưu trữ liền kề trong một mảng.

Thường thì tôi cũng kết hợp chúng với danh sách miễn phí được lập chỉ mục để cho phép xóa và chèn liên tục ở bất kỳ đâu:

nhập mô tả hình ảnh ở đây

Trong trường hợp đó, nextchỉ mục sẽ trỏ đến chỉ mục miễn phí tiếp theo nếu nút đã bị xóa hoặc chỉ mục được sử dụng tiếp theo nếu nút chưa bị xóa.

Và đây là trường hợp sử dụng số một mà tôi tìm thấy cho các danh sách được liên kết những ngày này. Ví dụ, khi chúng ta muốn lưu trữ một triệu dãy con có độ dài thay đổi, mỗi dãy 4 phần tử (nhưng đôi khi các phần tử bị xóa và thêm vào một trong các dãy con này), danh sách được liên kết cho phép chúng ta lưu trữ 4 triệu các nút danh sách được liên kết liền kề thay vì 1 triệu vùng chứa được phân bổ từng vùng riêng lẻ: một vectơ khổng lồ, tức là, không phải một triệu cái nhỏ.


0

Tôi đã sử dụng danh sách được liên kết (thậm chí là danh sách được liên kết kép) trong một ứng dụng C / C ++. Đây là trước .NET và thậm chí stl.

Tôi có thể sẽ không sử dụng danh sách liên kết bây giờ bằng ngôn ngữ .NET bởi vì tất cả mã truyền tải bạn cần đều được cung cấp cho bạn thông qua các phương thức mở rộng Linq.

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.