Làm cách nào tôi có thể chọn hiệu quả bộ chứa Thư viện Chuẩn trong C ++ 11?


135

Có một hình ảnh nổi tiếng (cheat sheet) được gọi là "Lựa chọn container C ++". Đó là biểu đồ dòng chảy để chọn thùng chứa tốt nhất cho việc sử dụng mong muốn.

Có ai biết nếu đã có phiên bản C ++ 11 của nó không?

Đây là cái trước: Lựa chọn container eC ++


6
Chưa bao giờ thấy điều này trước đây. cảm ơn!
WeaselFox

6
@WeaselFox: Nó đã là một phần của C ++ - Faq ở đây trên SO.
Alok Lưu

4
C ++ 11 chỉ giới thiệu một loại container thực sự mới: container unordered_X. Bao gồm chúng sẽ chỉ làm hỏng bảng đáng kể, vì có một số cân nhắc khi quyết định liệu bảng băm có phù hợp hay không.
Nicol Bolas

13
James nói đúng, có nhiều trường hợp sử dụng vector hơn những gì bảng hiển thị. Lợi thế của địa phương dữ liệu vượt trội trong nhiều trường hợp là thiếu hiệu quả trong một số hoạt động (sớm hơn C ++ 11). Tôi không thấy biểu đồ điện tử rất hữu ích ngay cả đối với c ++ 03
David Rodríguez - dribeas

33
Điều này thật dễ thương, nhưng tôi nghĩ rằng việc đọc bất kỳ sách giáo khoa phổ biến nào về cấu trúc dữ liệu sẽ khiến bạn rơi vào trạng thái, theo đó bạn không chỉ có thể phát minh lại sơ đồ này trong vài phút, mà còn biết nhiều thứ hữu ích hơn mà sơ đồ này che đậy.
Andrew Tomazos

Câu trả lời:


97

Không phải là tôi biết, tuy nhiên nó có thể được thực hiện bằng văn bản tôi đoán. Ngoài ra, biểu đồ hơi tắt, vì listnói chung không phải là một container tốt như vậy, và cũng không forward_list. Cả hai danh sách là các container rất chuyên biệt cho các ứng dụng thích hợp.

Để xây dựng một biểu đồ như vậy, bạn chỉ cần hai hướng dẫn đơn giản:

  • Chọn ngữ nghĩa trước
  • Khi có nhiều lựa chọn, hãy chọn cách đơn giản nhất

Lo lắng về hiệu suất thường là vô dụng lúc đầu. Các cân nhắc O lớn chỉ thực sự có tác dụng khi bạn bắt đầu xử lý một vài ngàn (hoặc nhiều hơn) các mặt hàng.

Có hai loại container lớn:

  • Container liên kết : họ có một findhoạt động
  • Container Sequence đơn giản

và sau đó bạn có thể xây dựng nhiều adapter trên đầu trang của họ: stack, queue, priority_queue. Tôi sẽ để các bộ điều hợp ra khỏi đây, chúng đủ chuyên dụng để có thể nhận ra.


Câu 1: Liên kết ?

  • Nếu bạn cần dễ dàng tìm kiếm bằng một phím, thì bạn cần một thùng chứa kết hợp
  • Nếu bạn cần sắp xếp các phần tử, thì bạn cần một thùng chứa kết hợp được đặt hàng
  • Nếu không, nhảy đến câu hỏi 2.

Câu 1.1: Đặt hàng ?

  • Nếu bạn không cần một đơn đặt hàng cụ thể, hãy sử dụng một unordered_container, nếu không hãy sử dụng đối tác được đặt hàng truyền thống của nó.

Câu 1.2: Khóa riêng ?

  • Nếu khóa tách biệt với giá trị, hãy sử dụng a map, nếu không thì sử dụngset

Câu 1.3: Bản sao ?

  • Nếu bạn muốn giữ các bản sao, hãy sử dụng một multi, nếu không thì không.

Thí dụ:

Giả sử rằng tôi có một vài người có ID duy nhất được liên kết với họ và tôi muốn lấy dữ liệu của một người từ ID của mình một cách đơn giản nhất có thể.

  1. Tôi muốn một findchức năng, do đó, một container kết hợp

    1.1. Tôi không thể quan tâm ít hơn về trật tự, do đó, một unordered_container

    1.2. Khóa (ID) của tôi tách biệt với giá trị được liên kết, do đó, mộtmap

    1.3. ID là duy nhất, do đó không trùng lặp nên leo vào.

Câu trả lời cuối cùng là : std::unordered_map<ID, PersonData>.


Câu 2: Bộ nhớ ổn định ?

  • Nếu các phần tử phải ổn định trong bộ nhớ (nghĩa là chúng không nên di chuyển xung quanh khi bản thân thùng chứa được sửa đổi), sau đó sử dụng một số list
  • Nếu không, nhảy đến câu hỏi 3.

Câu 2.1: Cái nào ?

  • Giải quyết cho a list; a forward_listchỉ hữu ích cho dấu chân bộ nhớ ít hơn.

Câu 3: Kích thước động ?

  • Nếu vùng chứa có kích thước đã biết (tại thời gian biên dịch) kích thước này sẽ không bị thay đổi trong suốt quá trình của chương trình các thành phần có thể xây dựng mặc định hoặc bạn có thể cung cấp danh sách khởi tạo đầy đủ (sử dụng { ... }cú pháp), sau đó sử dụng array. Nó thay thế mảng C truyền thống, nhưng với các chức năng thuận tiện.
  • Nếu không, nhảy đến câu hỏi 4.

Câu 4: Kết thúc kép ?

  • Nếu bạn muốn có thể loại bỏ các mục từ cả phía trước và phía sau, sau đó sử dụng a deque, nếu không sử dụng a vector.

Bạn sẽ lưu ý rằng, theo mặc định, trừ khi bạn cần một thùng chứa kết hợp, lựa chọn của bạn sẽ là một vector. Hóa ra đó cũng là khuyến nghị của Sutter và Stroustrup .


5
+1, nhưng với một số lưu ý: 1) arraykhông yêu cầu loại có thể xây dựng mặc định; 2) chọn multis không phải là quá nhiều về việc sao chép được cho phép mà nhiều hơn về việc giữ chúng có vấn đề hay không (bạn có thể đặt các bản sao vào trong các multithùng chứa, điều đó chỉ xảy ra khi chỉ có một bản duy nhất).
R. Martinho Fernandes

2
Ví dụ là một chút tắt. 1) chúng ta có thể "tìm" (không phải hàm thành viên, "<Thuật toán>" một) trên một thùng chứa không liên kết, 1.1) nếu chúng ta cần tìm "hiệu quả" và unordered_ sẽ là O (1) chứ không phải O (1) log n).
BlakBat

4
@BlakBat: map.find(key)ngon miệng hơn nhiều so với std::find(map.begin(), map.end(), [&](decltype(map.front()) p) { return p.first < key; }));mặc dù vậy, điều quan trọng, về mặt ngữ nghĩa, đó findlà chức năng thành viên chứ không phải là chức năng từ <algorithm>. Đối với O (1) so với O (log n), nó không ảnh hưởng đến ngữ nghĩa; Tôi sẽ xóa "hiệu quả" khỏi ví dụ và thay thế bằng "dễ dàng".
Matthieu M.

"Nếu các yếu tố phải ổn định trong bộ nhớ ... thì hãy sử dụng một số danh sách" ... hmmm, tôi nghĩ dequecó thuộc tính này không?
Martin Ba

@MartinBa: Có và không. Trong một dequeyếu tố chỉ ổn định nếu bạn đẩy / bật ở hai đầu; nếu bạn bắt đầu chèn / xóa ở giữa thì tối đa N / 2 phần tử được xáo trộn để lấp đầy khoảng trống được tạo.
Matthieu M.

51

Tôi thích câu trả lời của Matthieu, nhưng tôi sẽ trình bày lại sơ đồ như sau:

Khi nào KHÔNG sử dụng std :: vector

Theo mặc định, nếu bạn cần một thùng chứa các công cụ, sử dụng std::vector. Do đó, mọi container khác chỉ được chứng minh bằng cách cung cấp một số chức năng thay thế std::vector.

Người xây dựng

std::vectoryêu cầu nội dung của nó có thể di chuyển được, vì nó cần có khả năng xáo trộn các vật phẩm xung quanh. Đây không phải là một gánh nặng khủng khiếp đối với nội dung (lưu ý rằng các nhà xây dựng mặc định là không bắt buộc , nhờ emplacevà vv). Tuy nhiên, hầu hết các container khác không yêu cầu bất kỳ nhà xây dựng cụ thể nào (một lần nữa, nhờ emplace). Vì vậy, nếu bạn có một đối tượng mà bạn hoàn toàn không thể thực hiện một hàm tạo di chuyển, thì bạn sẽ phải chọn một đối tượng khác.

A std::dequesẽ là sự thay thế chung, có nhiều thuộc tính của std::vector, nhưng bạn chỉ có thể chèn vào hai đầu của deque. Chèn ở giữa yêu cầu di chuyển. Một std::listnơi không có yêu cầu về nội dung của nó.

Nhu cầu Bools

std::vector<bool>không phải. Vâng, nó là tiêu chuẩn. Nhưng đó không phải là một vectorcách hiểu thông thường, vì các hoạt động std::vectorthường cho phép bị cấm. Và nó chắc chắn không chứa bools .

Do đó, nếu bạn cần vectorhành vi thực tế từ một thùng chứa bools, bạn sẽ không lấy nó từ đó std::vector<bool>. Vì vậy, bạn sẽ phải thực hiện do a std::deque<bool>.

Đang tìm kiếm

Nếu bạn cần tìm các phần tử trong một thùng chứa và thẻ tìm kiếm không thể chỉ là một chỉ mục, thì bạn có thể cần phải từ bỏ std::vectorđể ủng hộ setmap. Lưu ý từ khóa " có thể "; một sắp xếp std::vectorđôi khi là một sự thay thế hợp lý. Hoặc Boost.Container's flat_set/map, thực hiện sắp xếp std::vector.

Hiện tại có bốn biến thể trong số này, mỗi biến thể có nhu cầu riêng.

  • Sử dụng mapkhi thẻ tìm kiếm không giống với mục bạn đang tìm kiếm. Nếu không thì sử dụng a set.
  • Sử dụng unorderedkhi bạn có rất nhiều vật phẩm trong container và hiệu suất tìm kiếm hoàn toàn cần phải có O(1), hơn là O(logn).
  • Sử dụng multinếu bạn cần nhiều mục để có cùng một thẻ tìm kiếm.

Đặt hàng

Nếu bạn cần một thùng chứa các mặt hàng để luôn được sắp xếp dựa trên một hoạt động so sánh cụ thể, bạn có thể sử dụng a set. Hoặc multi_setnếu bạn cần nhiều mặt hàng để có cùng giá trị.

Hoặc bạn có thể sử dụng một sắp xếp std::vector, nhưng bạn sẽ phải giữ nó được sắp xếp.

Ổn định

Khi lặp và tham chiếu bị vô hiệu đôi khi là một mối quan tâm. Nếu bạn cần một danh sách các mục, như bạn có các trình lặp / con trỏ tới các mục đó ở nhiều nơi khác nhau, thì std::vectorcách tiếp cận vô hiệu hóa có thể không phù hợp. Bất kỳ hoạt động chèn có thể gây ra vô hiệu, tùy thuộc vào kích thước và công suất hiện tại.

std::listcung cấp một bảo đảm chắc chắn: một trình vòng lặp và các tham chiếu / con trỏ liên quan của nó chỉ bị vô hiệu khi chính mục đó bị xóa khỏi vùng chứa. std::forward_listlà có nếu bộ nhớ là một mối quan tâm nghiêm trọng.

Nếu đó là một bảo đảm quá mạnh, std::dequecung cấp một bảo đảm yếu hơn nhưng hữu ích. Kết quả không hợp lệ từ các phần chèn vào giữa, nhưng phần chèn vào phần đầu hoặc phần đuôi chỉ gây ra sự vô hiệu của các trình vòng lặp , không phải là các con trỏ / tham chiếu đến các mục trong vùng chứa.

Hiệu suất chèn

std::vector chỉ cung cấp chèn giá rẻ vào cuối (và thậm chí sau đó, nó trở nên đắt tiền nếu bạn thổi công suất).

std::listlà đắt về hiệu suất (mỗi mục mới được chèn có giá phân bổ bộ nhớ), nhưng nó phù hợp . Nó cũng cung cấp khả năng đôi khi không thể thiếu để xáo trộn các mặt hàng xung quanh mà hầu như không mất chi phí hiệu năng, cũng như để trao đổi các mặt hàng với các std::listcontainer khác cùng loại mà không làm giảm hiệu suất. Nếu bạn cần xáo trộn mọi thứ xung quanh nhiều , hãy sử dụng std::list.

std::dequecung cấp chèn / loại bỏ thời gian liên tục ở đầu và đuôi, nhưng chèn ở giữa có thể khá tốn kém. Vì vậy, nếu bạn cần thêm / xóa những thứ từ phía trước cũng như phía sau, std::dequecó thể là những gì bạn cần.

Cần lưu ý rằng, nhờ vào ngữ nghĩa di chuyển, std::vectorhiệu suất chèn có thể không tệ như trước đây. Một số triển khai đã triển khai một hình thức sao chép mục dựa trên ngữ nghĩa di chuyển (cái gọi là "swaptimization"), nhưng bây giờ việc di chuyển là một phần của ngôn ngữ, nó bắt buộc theo tiêu chuẩn.

Không phân bổ động

std::arraylà một thùng chứa tốt nếu bạn muốn phân bổ động ít nhất có thể. Nó chỉ là một trình bao bọc xung quanh một mảng C; điều này có nghĩa là kích thước của nó phải được biết tại thời điểm biên dịch . Nếu bạn có thể sống với điều đó, sau đó sử dụng std::array.

Điều đó đang được nói, sử dụng std::vectorreserveing một kích thước sẽ làm việc tốt cho một giới hạn std::vector. Bằng cách này, kích thước thực tế có thể khác nhau và bạn chỉ nhận được một cấp phát bộ nhớ (trừ khi bạn thổi dung lượng).


1
Chà, tôi cũng thích câu trả lời của bạn rất nhiều :) WRT sắp xếp một vectơ được sắp xếp, ngoài ra std::sort, còn std::inplace_mergecó một điều thú vị là dễ dàng đặt các phần tử mới (thay vì một cuộc gọi std::lower_bound+ std::vector::insert). Rất vui được tìm hiểu về flat_setflat_map!
Matthieu M.

2
Bạn cũng không thể sử dụng một vectơ với các kiểu căn chỉnh 16 byte. Cũng là một thay thế tốt cho vector<bool>vector<char>.
Nghịch đảo

@Inverse: "Bạn cũng không thể sử dụng một vectơ với các loại được căn chỉnh 16 byte." Nói ai? Nếu std::allocator<T>không hỗ trợ căn chỉnh đó (và tôi không biết tại sao lại không), thì bạn luôn có thể sử dụng công cụ cấp phát tùy chỉnh của riêng mình.
Nicol Bolas

2
@Inverse: C ++ 11 std::vector::resizecó tình trạng quá tải không lấy giá trị (nó chỉ lấy kích thước mới; mọi phần tử mới sẽ được xây dựng mặc định tại chỗ). Ngoài ra, tại sao các trình biên dịch không thể căn chỉnh chính xác các tham số giá trị, ngay cả khi chúng được tuyên bố là có sự liên kết đó?
Nicol Bolas

1
bitsetcho bool nếu bạn biết kích thước trước en.cppreference.com/w/cpp/utility/bitset
bentervader

25

Đây là phiên bản C ++ 11 của sơ đồ trên. [ban đầu được đăng mà không ghi công cho tác giả ban đầu của nó, Mikael Persson ]


2
@NO_NAME Wow, tôi rất vui khi ai đó bận tâm trích dẫn một nguồn.
gạch dưới

1

Đây là một vòng quay nhanh, mặc dù nó có thể cần làm việc

Should the container let you manage the order of the elements?
Yes:
  Will the container contain always exactly the same number of elements? 
  Yes:
    Does the container need a fast move operator?
    Yes: std::vector
    No: std::array
  No:
    Do you absolutely need stable iterators? (be certain!)
    Yes: boost::stable_vector (as a last case fallback, std::list)
    No: 
      Do inserts happen only at the ends?
      Yes: std::deque
      No: std::vector
No: 
  Are keys associated with Values?
  Yes:
    Do the keys need to be sorted?
    Yes: 
      Are there more than one value per key?
      Yes: boost::flat_map (as a last case fallback, std::map)
      No: boost::flat_multimap (as a last case fallback, std::map)
    No:
      Are there more than one value per key?
      Yes: std::unordered_multimap
      No: std::unordered_map
  No:
    Are elements read then removed in a certain order?
    Yes:
      Order is:
      Ordered by element: std::priority_queue
      First in First out: std::queue
      First in Last out: std::stack
      Other: Custom based on std::vector????? 
    No:
      Should the elements be sorted by value?
      Yes: boost::flat_set
      No: std::vector

Bạn có thể nhận thấy rằng điều này khác rất nhiều so với phiên bản C ++ 03, chủ yếu là do tôi thực sự không thích các nút được liên kết. Các thùng chứa nút được liên kết thường có thể được đánh bại trong hiệu suất bởi một thùng chứa không được liên kết, ngoại trừ trong một vài tình huống hiếm gặp. Nếu bạn không biết những tình huống đó là gì và có quyền truy cập để tăng cường, đừng sử dụng các thùng chứa nút được liên kết. (std :: list, std :: slist, std :: map, std :: multimap, std :: set, std :: multiset). Danh sách này tập trung chủ yếu vào các thùng chứa nhỏ và trung bình, bởi vì (A) 99,99% so với những gì chúng ta xử lý trong mã và (B) Số lượng lớn các phần tử cần thuật toán tùy chỉnh, không phải các thùng chứa khác nhau.

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.