Điểm của việc sử dụng danh sách trên các vectơ, trong C ++ là gì?


32

Tôi đã chạy 3 thử nghiệm khác nhau liên quan đến danh sách và vectơ C ++.

Những người có vectơ tỏ ra hiệu quả hơn, ngay cả khi có rất nhiều sự chèn vào ở giữa.

Do đó, câu hỏi: trong trường hợp nào danh sách có ý nghĩa hơn vectơ?

Nếu hầu hết các vectơ có vẻ hiệu quả hơn và xem xét các thành viên của họ giống nhau như thế nào, thì những lợi thế nào còn lại cho danh sách?

  1. Tạo N số nguyên và đặt chúng vào một thùng chứa để container vẫn được sắp xếp. Việc chèn đã được thực hiện một cách ngây thơ, bằng cách đọc từng phần tử một và chèn cái mới ngay trước cái lớn hơn đầu tiên.
    Với một danh sách, thời gian đi qua mái nhà khi kích thước tăng, so với vectơ.

  2. Chèn N số nguyên vào cuối container.
    Đối với danh sách và vectơ, thời gian tăng theo cùng một độ lớn, mặc dù nó nhanh hơn 3 lần với vectơ.

  3. Chèn N số nguyên vào một thùng chứa.
    Bắt đầu hẹn giờ.
    Sắp xếp vùng chứa bằng list.sort cho danh sách và std :: sort cho vectơ. Đồng hồ bấm giờ.
    Một lần nữa, thời gian tăng theo cùng một thứ tự cường độ, nhưng trung bình nhanh hơn 5 lần với các vectơ.

Tôi có thể tiếp tục thực hiện các bài kiểm tra và tìm ra một vài ví dụ trong đó danh sách sẽ chứng minh tốt hơn.

Nhưng kinh nghiệm chung của các bạn khi đọc tin nhắn này có thể cung cấp câu trả lời hiệu quả hơn.

Bạn có thể đã gặp các tình huống trong đó danh sách thuận tiện hơn để sử dụng hoặc thực hiện tốt hơn?



1
Đây cũng là một tài nguyên tốt khác về chủ đề: stackoverflow.com/a/2209564/8360 , hầu hết các hướng dẫn C ++ mà tôi đã nghe là sử dụng vector theo mặc định, chỉ liệt kê nếu bạn có lý do cụ thể.
Zachary Yates

Cảm ơn bạn. Tuy nhiên tôi không đồng ý với hầu hết những gì được nói trong câu trả lời yêu thích. Hầu hết những ý tưởng định sẵn này đã bị vô hiệu bởi các thí nghiệm của tôi. Người này đã không thực hiện bất kỳ bài kiểm tra nào và áp dụng lý thuyết rộng rãi được dạy trong sách hoặc ở trường.
Marek Stanley

1
listlẽ sẽ tốt hơn nếu bạn loại bỏ nhiều yếu tố. Tôi không tin vectorsẽ trả lại bộ nhớ cho hệ thống cho đến khi toàn bộ vectơ bị xóa. Ngoài ra, hãy nhớ rằng bài kiểm tra số 1 của bạn không chỉ kiểm tra thời gian chèn. Đó là một thử nghiệm kết hợp tìm kiếm và chèn. Đó là việc tìm ra nơi để chèn vào nơi listchậm. Chèn thực tế sẽ nhanh hơn vector.
Gort Robot

3
Nó rất điển hình đến nỗi câu hỏi này được mô tả về hiệu suất (thời gian chạy), hiệu suất và chỉ hiệu suất. Đây dường như là một điểm mù của rất nhiều lập trình viên - họ tập trung vào khía cạnh này và quên rằng có hàng tá các khía cạnh khác thường rất quan trọng hơn nhiều.
Doc Brown

Câu trả lời:


34

Câu trả lời ngắn gọn là các trường hợp dường như rất ít và xa. Có lẽ có một vài mặc dù.

Một là khi bạn cần lưu trữ một số lượng nhỏ các đối tượng lớn - đặc biệt là các đối tượng quá lớn đến mức không thực tế để phân bổ không gian cho một vài trong số chúng. Về cơ bản, không có cách nào ngăn chặn một vectơ hoặc deque phân bổ không gian cho các đối tượng bổ sung - đó là cách chúng được xác định (nghĩa là chúng phải phân bổ thêm không gian để đáp ứng các yêu cầu phức tạp của chúng). Nếu bạn không thể cho phép không gian được phân bổ thêm, thì đó std::listcó thể là thùng chứa tiêu chuẩn duy nhất đáp ứng nhu cầu của bạn.

Một cách khác là khi / nếu bạn lưu trữ một trình vòng lặp đến một điểm "thú vị" trong danh sách trong một khoảng thời gian dài và khi bạn thực hiện chèn và / hoặc xóa, bạn (gần như) luôn luôn làm điều đó từ một vị trí bạn đã có một trình vòng lặp, vì vậy bạn không đi qua danh sách để đi đến điểm bạn sẽ thực hiện thao tác chèn hoặc xóa. Rõ ràng điều tương tự cũng áp dụng nếu bạn làm việc với nhiều hơn một vị trí, nhưng vẫn có kế hoạch lưu trữ một trình vòng lặp đến từng nơi bạn có khả năng làm việc, vì vậy bạn hầu hết thao tác các điểm bạn có thể tiếp cận trực tiếp và chỉ hiếm khi đi qua danh sách để có được đến những điểm đó.

Để biết ví dụ về cái đầu tiên, hãy xem xét một trình duyệt web. Nó có thể giữ một danh sách các Tabđối tượng được liên kết , với mỗi đối tượng tab đại diện trên tab mở trong trình duyệt. Mỗi tab có thể là vài chục megabyte dữ liệu (nhiều hơn, đặc biệt là nếu có liên quan đến video). Số lượng tab mở điển hình của bạn có thể dễ dàng ít hơn một chục và 100 có thể gần với mức cực cao.

Để biết ví dụ về phần hai, hãy xem xét một trình xử lý văn bản lưu trữ văn bản dưới dạng danh sách các chương được liên kết, mỗi chương có thể chứa danh sách các đoạn (nói) được liên kết. Khi người dùng đang chỉnh sửa, họ thường sẽ tìm một vị trí cụ thể nơi họ sẽ chỉnh sửa và sau đó thực hiện một số lượng công việc hợp lý tại vị trí đó (hoặc bên trong đoạn đó, dù sao đi nữa). Vâng, họ sẽ chuyển từ đoạn này sang đoạn khác bây giờ và một lần nữa, nhưng trong hầu hết các trường hợp, đó sẽ là một đoạn gần nơi họ đã làm việc.

Thỉnh thoảng (những thứ như tìm kiếm và thay thế toàn cầu) cuối cùng bạn đi qua tất cả các mục trong tất cả các danh sách, nhưng điều đó khá hiếm và ngay cả khi bạn làm, có lẽ bạn sẽ thực hiện đủ công việc tìm kiếm trong một mục trong danh sách, thời gian để duyệt qua danh sách gần như không quan trọng.

Lưu ý rằng trong một trường hợp điển hình, điều này cũng có khả năng phù hợp với tiêu chí đầu tiên - một chương chứa một số đoạn khá nhỏ, mỗi đoạn có thể khá lớn (ít nhất là tương đối với kích thước của các con trỏ trong nút, và như vậy). Tương tự như vậy, bạn có một số lượng chương tương đối nhỏ, mỗi chương có thể là vài kilobyte hoặc hơn.

Điều đó nói rằng, tôi phải thừa nhận rằng cả hai ví dụ này có lẽ hơi khó hiểu và trong khi một danh sách được liên kết có thể hoạt động hoàn hảo cho cả hai, thì có lẽ nó cũng sẽ không mang lại lợi thế lớn trong cả hai trường hợp. Ví dụ, trong cả hai trường hợp, việc phân bổ thêm không gian trong một vectơ cho một số trang / tab (trống) hoặc một số chương trống dường như không phải là vấn đề thực sự.


4
+1, nhưng: Trường hợp đầu tiên biến mất khi bạn sử dụng con trỏ, mà bạn nên luôn luôn sử dụng với các đối tượng lớn. Danh sách liên kết không phù hợp với ví dụ thứ hai; mảng riêng cho tất cả các hoạt động khi chúng ngắn.
amara

2
Trường hợp đối tượng lớn hoàn toàn không hoạt động. Sử dụng một std::vectorcon trỏ sẽ hiệu quả hơn sau đó tất cả các đối tượng nút danh sách được liên kết.
Winston Ewert

Có nhiều cách sử dụng cho các danh sách được liên kết - chỉ là chúng không phổ biến như các mảng động. Bộ đệm LRU là một cách sử dụng phổ biến của danh sách được liên kết.
Charles Salvia

Ngoài ra, std::vector<std::unique_ptr<T>>có thể là một lựa chọn tốt.
Ded repeatator

24

Theo chính Bjarne Stroustrup, vectơ phải luôn là bộ sưu tập mặc định cho chuỗi dữ liệu. Bạn có thể chọn danh sách nếu bạn muốn tối ưu hóa để chèn và xóa các yếu tố, nhưng thông thường bạn không nên. Các chi phí của danh sách là sử dụng bộ nhớ và truyền tải chậm.

Ông nói về điều này trong bài trình bày này .

Vào khoảng 0 giờ 44 phút, ông nói về các vectơ so với các danh sách nói chung.

Vấn đề nhỏ gọn. Các vectơ nhỏ gọn hơn danh sách. Và mô hình sử dụng dự đoán là vấn đề rất lớn. Với các vectơ, bạn phải loại bỏ rất nhiều yếu tố nhưng bộ nhớ cache thực sự rất tốt ở đó. ... Danh sách không có quyền truy cập ngẫu nhiên. Nhưng khi bạn duyệt qua một danh sách, bạn tiếp tục truy cập ngẫu nhiên. Có một nút ở đây và nó đi đến nút đó, trong bộ nhớ. Vì vậy, bạn thực sự đang truy cập ngẫu nhiên vào bộ nhớ của bạn và bạn đang tối đa hóa các lỗi bộ nhớ cache của mình, điều này hoàn toàn ngược lại với những gì bạn muốn.

Vào khoảng 1:08, anh ta được đưa ra một câu hỏi về vấn đề này.

Những gì chúng ta nên thấy rằng chúng ta cần một chuỗi các yếu tố. Và chuỗi các phần tử mặc định trong C ++ là vector. Bây giờ, vì đó là nhỏ gọn và hiệu quả. Thực hiện, ánh xạ đến phần cứng, vấn đề. Bây giờ, nếu bạn muốn tối ưu hóa để chèn và xóa - bạn nói, 'tốt, tôi không muốn phiên bản mặc định của chuỗi. Tôi muốn một chuyên ngành, đó là một danh sách '. Và nếu bạn làm điều đó, bạn nên biết đủ để nói, 'Tôi chấp nhận một số chi phí và một số vấn đề, như truyền tải chậm và sử dụng nhiều bộ nhớ hơn'.


1
bạn có phiền khi viết ngắn gọn những gì được nói trong bài thuyết trình mà bạn liên kết đến "vào khoảng 0h44 và 1:08" không?
gnat

2
@gnat - chắc chắn rồi. Tôi đã cố gắng trích dẫn những thứ có ý nghĩa riêng biệt, và điều đó không cần bối cảnh của các slide.
Pete

11

Nơi duy nhất mà tôi thường sử dụng danh sách là nơi tôi cần xóa các phần tử và không làm mất hiệu lực các trình vòng lặp. std::vectorlàm mất hiệu lực tất cả các trình vòng lặp khi chèn và xóa. std::listđảm bảo rằng các trình vòng lặp cho các phần tử hiện có vẫn còn hiệu lực sau khi chèn hoặc xóa.


4

Ngoài các câu trả lời khác đã được cung cấp, danh sách có một số tính năng nhất định không tồn tại trong vectơ (vì chúng sẽ cực kỳ tốn kém.) Các hoạt động ghép và ghép là quan trọng nhất. Nếu bạn thường xuyên có một loạt các danh sách cần được nối hoặc hợp nhất với nhau, một danh sách có lẽ là một lựa chọn tốt.

Nhưng nếu bạn không cần thực hiện các thao tác này thì có lẽ là không.


3

Việc thiếu bộ nhớ cache / thân thiện với trang vốn có của các danh sách được liên kết có xu hướng khiến chúng gần như bị loại bỏ hoàn toàn bởi rất nhiều nhà phát triển C ++ và với sự biện minh tốt ở dạng mặc định đó.

Danh sách liên kết vẫn có thể là tuyệt vời

Các danh sách được liên kết có thể là tuyệt vời khi được hỗ trợ bởi một bộ phân bổ cố định mang lại cho họ địa phương không gian mà họ vốn đã thiếu.

Ví dụ, nơi chúng nổi trội là chúng ta có thể chia một danh sách thành hai danh sách, ví dụ, bằng cách lưu trữ một con trỏ mới và thao tác một hoặc hai con trỏ. Chúng ta có thể di chuyển các nút từ danh sách này sang danh sách khác trong thời gian không đổi bằng thao tác con trỏ đơn thuần và một danh sách trống có thể chỉ đơn giản có chi phí bộ nhớ của một headcon trỏ.

Bộ gia tốc lưới đơn giản

Như một ví dụ thực tế, hãy xem xét một mô phỏng hình ảnh 2D. Nó có một màn hình cuộn với bản đồ trải rộng 400x400 (160.000 ô lưới) được sử dụng để tăng tốc những thứ như phát hiện va chạm giữa hàng triệu hạt di chuyển xung quanh ở mỗi khung hình (chúng tôi tránh các cây bốn lá ở đây vì chúng thực sự có xu hướng hoạt động kém hơn với mức độ này dữ liệu động). Cả một loạt các hạt liên tục di chuyển xung quanh ở mọi khung hình, nghĩa là chúng liên tục di chuyển từ một ô này sang ô khác.

Trong trường hợp này, nếu mỗi hạt là một nút danh sách liên kết đơn, mỗi ô lưới có thể bắt đầu như một headcon trỏ trỏ tới nullptr. Khi một hạt mới được sinh ra, chúng ta chỉ cần đặt nó vào ô lưới mà nó cư trú bằng cách đặt headcon trỏ của ô đó để trỏ đến nút hạt này. Khi một hạt di chuyển từ một ô lưới sang ô tiếp theo, chúng ta chỉ cần thao tác con trỏ.

Điều này có thể hiệu quả hơn rất nhiều so với việc lưu trữ 160.000 vectorscho mỗi ô lưới và đẩy lùi và xóa từ giữa mọi lúc trên cơ sở từng khung hình.

danh sách :: danh sách

Điều này là cho các danh sách liên kết đơn, xâm nhập, đơn lẻ được hỗ trợ bởi một phân bổ cố định mặc dù. std::listđại diện cho một danh sách liên kết đôi và nó có thể không nhỏ gọn khi trống như một con trỏ đơn (thay đổi theo cách thực hiện của nhà cung cấp), cộng với việc thực hiện phân bổ tùy chỉnh theo std::allocatorhình thức là một điều khó khăn.

Tôi phải thừa nhận tôi không bao giờ sử dụng listbất cứ điều gì. Nhưng danh sách liên kết có thể vẫn còn tuyệt vời! Tuy nhiên, chúng không tuyệt vời vì những lý do mà mọi người thường muốn sử dụng chúng, và không tuyệt vời trừ khi chúng được hỗ trợ bởi một công cụ phân bổ cố định rất hiệu quả, giúp giảm thiểu rất nhiều lỗi bắt buộc và lỗi bộ nhớ cache liên quan.


1
Có một danh sách liên kết đơn tiêu chuẩn kể từ C ++ 11 , std::forward_list.
sharyex

2

Bạn nên xem xét kích thước của các yếu tố trong container.

int vector phần tử rất nhanh vì hầu hết dữ liệu nằm trong bộ đệm CPU (và các hướng dẫn SIMD có thể được sử dụng để sao chép dữ liệu).

Nếu kích thước của phần tử lớn hơn thì kết quả của phép thử 1 và 3 có thể thay đổi đáng kể.

Từ một so sánh hiệu suất rất toàn diện :

Điều này rút ra kết luận đơn giản về việc sử dụng từng cấu trúc dữ liệu:

  • Số crunching: sử dụng std::vectorhoặcstd::deque
  • Tìm kiếm tuyến tính: sử dụng std::vectorhoặcstd::deque
  • Chèn / Xóa ngẫu nhiên:
    • Kích thước dữ liệu nhỏ: sử dụng std::vector
    • Kích thước phần tử lớn: sử dụng std::list(trừ khi chủ yếu để tìm kiếm)
  • Kiểu dữ liệu không tầm thường: sử dụng std::listtrừ khi bạn cần bộ chứa đặc biệt để tìm kiếm. Nhưng đối với nhiều sửa đổi của container, nó sẽ rất chậm.
  • Đẩy về phía trước: sử dụng std::dequehoặcstd::list

(như một lưu ý phụ std::dequelà một cấu trúc dữ liệu được đánh giá rất thấp).

Từ quan điểm thuận tiện std::listcung cấp đảm bảo rằng các trình vòng lặp không bao giờ bị vô hiệu khi bạn chèn và loại bỏ các yếu tố khác. Nó thường là một khía cạnh quan trọng.


2

Theo tôi, lý do nổi bật nhất để sử dụng danh sách là vô hiệu hóa trình lặp : nếu bạn thêm / xóa các phần tử vào một vectơ, tất cả các con trỏ, tham chiếu, các trình lặp mà bạn giữ cho các phần tử cụ thể của vectơ này có thể bị vô hiệu và dẫn đến các lỗi tinh vi .. hoặc lỗi phân đoạn.

Đây không phải là trường hợp với danh sách.

Các quy tắc chính xác cho tất cả các thùng chứa tiêu chuẩn được đưa ra trong bài đăng StackOverflow này .


0

Nói tóm lại, không có lý do chính đáng để sử dụng std::list<>:

  • Nếu bạn cần một container chưa sắp xếp, std::vector<>quy tắc.
    (Xóa các phần tử bằng cách thay thế chúng bằng phần tử cuối cùng của vectơ.)

  • Nếu bạn cần một container được sắp xếp, std::vector<shared_ptr<>>quy tắc.

  • Nếu bạn cần một chỉ số thưa thớt, std::unordered_map<>quy tắc.

Đó là nó.

Tôi thấy rằng chỉ có một tình huống mà tôi có xu hướng sử dụng một danh sách được liên kết: Khi tôi có các đối tượng có sẵn cần được kết nối theo một cách nào đó để thực hiện một số logic ứng dụng bổ sung. Tuy nhiên, trong trường hợp đó tôi không bao giờ sử dụng std::list<>, thay vào đó tôi sử dụng một con trỏ tiếp theo (thông minh) bên trong đối tượng, đặc biệt là vì hầu hết các trường hợp sử dụng đều dẫn đến một cây thay vì danh sách tuyến tính. Trong một số trường hợp, cấu trúc kết quả là một danh sách được liên kết, trong những trường hợp khác, nó là một cây hoặc biểu đồ chu kỳ có hướng. Mục đích chính của các con trỏ này là luôn xây dựng cấu trúc logic, không bao giờ quản lý các đối tượng. Chúng tôi có std::vector<>cho điều đó.


-1

Bạn cần chỉ ra cách bạn thực hiện các thao tác chèn trong bài kiểm tra đầu tiên của bạn. Bài kiểm tra thứ hai và thứ ba của bạn, vector sẽ dễ dàng giành chiến thắng.

Việc sử dụng danh sách đáng kể là khi bạn phải hỗ trợ xóa các mục trong khi lặp. Khi vectơ được sửa đổi, tất cả các trình vòng lặp (có khả năng) không hợp lệ. Với một danh sách, chỉ có một trình vòng lặp cho phần tử bị loại bỏ là không hợp lệ. Tất cả các trình vòng lặp khác vẫn còn hiệu lực.

Thứ tự sử dụng điển hình cho các container là vector, deque, sau đó liệt kê. Sự lựa chọn của container thường dựa trên Push_back chọn vector, pop_front chọn deque, chèn danh sách chọn.


3
khi xóa các mục trong khi lặp lại, tốt hơn là sử dụng một vectơ và chỉ cần tạo một vectơ mới cho kết quả
amara

-1

Một yếu tố tôi có thể nghĩ đến là khi một vectơ phát triển, bộ nhớ trống sẽ bị phân mảnh khi vectơ phân bổ bộ nhớ của nó và phân bổ một khối lớn hơn nhiều lần. Đây không phải là một vấn đề với danh sách.

Điều này ngoài thực tế là số lượng lớn push_backkhông có dự trữ cũng sẽ gây ra một bản sao trong mỗi lần thay đổi kích thước khiến nó không hiệu quả. Chèn vào giữa tương tự gây ra sự di chuyển của tất cả các yếu tố sang phải, và thậm chí còn tồi tệ hơn.

Tôi không biết đây có phải là một mối quan tâm lớn hay không, nhưng đó là lý do được đưa ra cho tôi trong công việc của tôi (phát triển trò chơi di động), để tránh các vectơ.


1
không, vector sẽ sao chép và điều đó rất tốn kém. Nhưng đi qua danh sách được liên kết (để tìm ra nơi cần chèn) cũng tốn kém. Chìa khóa thực sự là để đo lường
Kate Gregory

@KateGregory Ý tôi là ngoài điều đó, hãy để tôi chỉnh sửa cho phù hợp
Karthik T

3
Đúng, nhưng tin hay không (và hầu hết mọi người không tin) chi phí bạn đã không đề cập, đi qua danh sách được liên kết để tìm nơi chèn NGOÀI RA những bản sao đó (đặc biệt là nếu các yếu tố nhỏ (hoặc có thể di chuyển được, bởi vì sau đó chúng là di chuyển)) và vector thường (hoặc thậm chí thường) nhanh hơn. Tin hay không.
Kate Gregory
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.