Danh sách liên kết nên luôn luôn có một con trỏ đuôi?


11

Sự hiểu biết của tôi...

Ưu điểm:

  • Chèn ở cuối là O (1) thay vì O (N).
  • Nếu danh sách là Danh sách được liên kết gấp đôi, thì việc xóa khỏi cuối cũng là O (1) thay vì O (N).

Bất lợi:

  • Chiếm một lượng bộ nhớ phụ không đáng kể: 4-8 byte .
  • Người thực hiện phải theo dõi đuôi.

Nhìn vào những ưu điểm và nhược điểm này, tôi không thể hiểu tại sao Danh sách được liên kết sẽ tránh sử dụng con trỏ đuôi. Có thiếu điều gì không?


1
một con trỏ đuôi là 4-8 byte (tùy thuộc vào hệ thống 32 hoặc 64 bit)
ratchet freak

1
Có vẻ như bạn đã tóm tắt khá nhiều rồi.
Robert Harvey

@RobertHarvey Tôi đang nghiên cứu cấu trúc dữ liệu ngay bây giờ và tôi không biết thực tiễn tốt nhất là gì. Vì vậy, những gì tôi viết là ấn tượng của tôi, nhưng những gì tôi đang hỏi là liệu chúng có đúng không. Nhưng cảm ơn bạn đã làm rõ!
Adam Zerner

7
"Thực hành tốt nhất" là thuốc phiện của quần chúng . Kỷ niệm thực tế là bạn vẫn có khả năng suy nghĩ cho chính mình.
Robert Harvey

Cảm ơn liên kết @RobertHarvey - Tôi thích điểm đó! Tôi chắc chắn có một cách tiếp cận lợi ích chi phí xem xét các chi tiết cụ thể của tình huống.
Adam Zerner

Câu trả lời:


7

Bạn đã đúng, một con trỏ đuôi không bao giờ bị tổn thương và chỉ có thể giúp đỡ. Tuy nhiên, có một tình huống mà người ta không cần một con trỏ đuôi nào cả.

Nếu một người đang sử dụng một danh sách được liên kết để thực hiện một ngăn xếp, thì không cần phải có một con trỏ đuôi bởi vì người ta có thể đảm bảo rằng tất cả các truy cập, chèn và loại bỏ xảy ra ở đầu. Điều đó đang được nói rằng người ta có thể sử dụng một danh sách liên kết đôi với một con trỏ đuôi bởi vì đó là cách triển khai tiêu chuẩn trong thư viện hoặc nền tảng và bộ nhớ là rẻ, nhưng người ta không cần nó.


9

Danh sách liên kết là rất phổ biến liên tục và bất biến. Trong thực tế, trong các ngôn ngữ lập trình chức năng, việc sử dụng này có mặt khắp nơi. Con trỏ đuôi phá vỡ cả hai tính chất đó. Tuy nhiên, nếu bạn không quan tâm đến tính bất biến hoặc sự bền bỉ, có rất ít nhược điểm để bao gồm một con trỏ đuôi.


3
Bạn có thể giải thích lý do tại sao họ phá vỡ sự bền bỉ và bất biến?
Adam Zerner

Vui lòng thêm mối quan tâm thân thiện với bộ nhớ cache
Basilevs 6/11/2015

Nhìn vào ví dụ của tôi từ câu hỏi này . Nếu bạn chỉ làm việc từ đầu danh sách, và nó là bất biến, bạn có thể chia sẻ đuôi. Nếu bạn sử dụng một con trỏ đuôi, bạn không thể sử dụng kỹ thuật này để chia sẻ và duy trì tính bất biến.
Karl Bielefeldt

Trên thực tế với tính bất biến, một con trỏ đuôi bên cạnh vô dụng vì điều duy nhất bạn có thể làm với nó là xem phần tử cuối cùng là gì. Mọi thứ khác cần phải làm việc từ đầu.
ratchet freak

0

Tôi hiếm khi sử dụng một con trỏ đuôi cho các danh sách được liên kết và có xu hướng sử dụng các danh sách liên kết đơn thường xuyên hơn trong đó một kiểu chèn / pop giống như ngăn xếp và loại bỏ (hoặc chỉ loại bỏ thời gian tuyến tính từ giữa). Đó là bởi vì trong các trường hợp sử dụng thông thường của tôi, con trỏ đuôi thực sự đắt tiền, giống như làm cho danh sách liên kết đơn thành một danh sách liên kết đôi là đắt tiền.

Thông thường việc sử dụng trường hợp phổ biến của tôi cho danh sách liên kết đơn có thể lưu trữ hàng trăm ngàn danh sách được liên kết chỉ chứa một vài nút danh sách. Tôi cũng thường không sử dụng con trỏ cho danh sách liên kết. Tôi sử dụng các chỉ mục vào một mảng thay vì các chỉ mục có thể là 32 bit, ví dụ, chiếm một nửa không gian của con trỏ 64 bit. Tôi cũng thường không phân bổ các nút danh sách một lần và thay vào đó, chỉ sử dụng một mảng lớn để lưu trữ tất cả các nút và sau đó sử dụng các chỉ mục 32 bit để liên kết các nút lại với nhau.

Ví dụ, hãy tưởng tượng một trò chơi video sử dụng lưới 400x400 để phân vùng một triệu hạt di chuyển xung quanh và bật ra khỏi nhau để tăng tốc phát hiện va chạm. Trong trường hợp đó, một cách khá hiệu quả để lưu trữ đó là lưu trữ 160.000 danh sách liên kết đơn, chuyển thành 160.000 số nguyên 32 bit trong trường hợp của tôi (~ 640 kilobyte) và một chi phí nguyên 32 bit cho mỗi hạt. Bây giờ khi các hạt di chuyển xung quanh trên màn hình, tất cả những gì chúng ta phải làm là cập nhật một vài số nguyên 32 bit để di chuyển một hạt từ ô này sang ô khác, như vậy:

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

... với nextchỉ mục ("con trỏ") của nút hạt đóng vai trò là chỉ mục cho hạt tiếp theo trong ô hoặc hạt tự do tiếp theo để lấy lại nếu hạt đã chết (về cơ bản là thực thi cấp phát danh sách miễn phí bằng chỉ số):

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

Việc loại bỏ thời gian tuyến tính khỏi một ô không thực sự là một chi phí vì chúng ta đang xử lý logic hạt bằng cách lặp qua các hạt trong một ô, do đó, một danh sách liên kết đôi sẽ chỉ thêm chi phí không có ích tất cả trong trường hợp của tôi cũng giống như một cái đuôi sẽ không có lợi cho tôi cả.

Một con trỏ đuôi sẽ tăng gấp đôi mức sử dụng bộ nhớ của lưới cũng như tăng số lần bỏ lỡ bộ đệm. Nó cũng yêu cầu chèn để yêu cầu một nhánh để kiểm tra xem danh sách có trống không thay vì không có nhánh. Làm cho nó trở thành một danh sách liên kết đôi sẽ tăng gấp đôi chi phí danh sách của mỗi hạt. 90% thời gian tôi sử dụng các danh sách được liên kết, đó là cho các trường hợp như thế này, và do đó, một con trỏ đuôi thực sự sẽ tương đối tốn kém để lưu trữ.

Vì vậy, 4-8 byte thực sự không tầm thường trong hầu hết các bối cảnh mà tôi sử dụng danh sách được liên kết ở vị trí đầu tiên. Chỉ muốn gắn chip vào đó vì nếu bạn đang sử dụng cấu trúc dữ liệu để lưu trữ một khối lượng phần tử, thì 4-8 byte có thể không phải lúc nào cũng không đáng kể. Tôi thực sự sử dụng các danh sách được liên kết để giảm phân bổ số lượng bộ nhớ và số lượng bộ nhớ cần thiết, ngược lại, lưu trữ 160.000 mảng động phát triển cho lưới có sử dụng bộ nhớ nổ (thường là một con trỏ cộng với ít nhất hai số nguyên cho mỗi ô lưới cùng với phân bổ heap cho mỗi ô lưới trái ngược với chỉ một phân bổ heap và số nguyên cho mỗi ô).

Tôi thường thấy nhiều người tiếp cận với các danh sách được liên kết vì độ phức tạp liên tục của họ liên quan đến việc loại bỏ trước / giữa và chèn trước / giữa khi LL thường là một lựa chọn kém trong những trường hợp do thiếu liên tục chung. Trong đó các LL rất đẹp đối với tôi từ quan điểm hiệu năng là khả năng chỉ cần di chuyển một yếu tố từ danh sách này sang danh sách khác bằng cách thao tác một vài con trỏ và có thể đạt được cấu trúc dữ liệu có kích thước thay đổi mà không cần bộ cấp phát bộ nhớ có kích thước thay đổi (kể từ đó mỗi nút có kích thước đồng nhất, chúng ta có thể sử dụng danh sách miễn phí, vd). Nếu mỗi nút danh sách đang được phân bổ riêng cho phân bổ mục đích chung, thì thông thường khi danh sách được liên kết sẽ bị giảm giá trị so với các lựa chọn thay thế và đó là '

Thay vào đó, tôi đề nghị rằng trong hầu hết các trường hợp danh sách được liên kết đóng vai trò tối ưu hóa rất hiệu quả so với các lựa chọn đơn giản, các hình thức hữu ích nhất thường được liên kết đơn lẻ, chỉ cần một con trỏ đầu và không yêu cầu cấp phát bộ nhớ cho mục đích chung nút và thay vào đó thường có thể chỉ là bộ nhớ nhóm đã được phân bổ cho mỗi nút (từ một mảng lớn đã được phân bổ trước, ví dụ). Ngoài ra, mỗi SLL thường sẽ lưu trữ một số lượng rất nhỏ các phần tử trong các trường hợp đó, như các cạnh được kết nối với một nút biểu đồ (nhiều danh sách được liên kết nhỏ so với một danh sách được liên kết lớn).

Cũng đáng lưu ý rằng chúng ta có một khối lượng DRAM ngày nay nhưng đó là loại bộ nhớ chậm thứ hai có sẵn. Chúng tôi vẫn ở mức 64 KB mỗi lõi khi nói đến bộ đệm L1 với các dòng bộ đệm 64 byte. Kết quả là, những khoản tiết kiệm byte nhỏ đó thực sự có thể quan trọng trong một khu vực quan trọng về hiệu năng như sim hạt ở trên khi được nhân lên hàng triệu lần nếu điều đó có nghĩa là sự khác biệt giữa việc lưu trữ gấp đôi số nút vào một dòng bộ đệm hay không, ví dụ

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.