Làm thế nào để thực hiện các thuật toán sắp xếp cổ điển trong C ++ hiện đại?


331

Các std::sortthuật toán (và anh em họ của mình std::partial_sortstd::nth_element) từ Thư viện chuẩn C ++ là trong hầu hết các trường một sự pha trộn phức tạp và hybrid của các thuật toán sắp xếp cơ bản hơn , chẳng hạn như sắp xếp chọn, sắp xếp chèn, nhanh chóng sắp xếp, sắp xếp hợp nhất, hoặc loại heap.

Có rất nhiều câu hỏi ở đây và trên các trang web chị em như https://codereview.stackexchange.com/ liên quan đến lỗi, độ phức tạp và các khía cạnh khác của việc triển khai các thuật toán sắp xếp cổ điển này. Hầu hết các triển khai được cung cấp bao gồm các vòng lặp thô, sử dụng các thao tác chỉ mục và các loại cụ thể và thường không tầm thường để phân tích về tính chính xác và hiệu quả.

Câu hỏi : làm thế nào các thuật toán sắp xếp cổ điển được đề cập ở trên có thể được thực hiện bằng C ++ hiện đại?

  • không có vòng lặp thô , nhưng kết hợp các khối xây dựng thuật toán của Thư viện tiêu chuẩn từ<algorithm>
  • Giao diện lặp và sử dụng các mẫu thay vì thao tác chỉ mục và các loại cụ thể
  • Kiểu C ++ 14 , bao gồm Thư viện tiêu chuẩn đầy đủ, cũng như các bộ giảm nhiễu cú pháp như auto, bí danh mẫu, bộ so sánh trong suốt và lambdas đa hình.

Ghi chú :

  • để tham khảo thêm về việc triển khai các thuật toán sắp xếp, hãy xem Wikipedia , Rosetta Code hoặc http://www.sorting-alerskyms.com/
  • theo quy ước của Sean Parent (slide 39), một vòng lặp thô fordài hơn so với thành phần của hai hàm với một toán tử. Vì vậy f(g(x));, f(x); g(x);hoặc f(x) + g(x);không phải là các vòng lặp thô, và cũng không phải là các vòng lặp trong selection_sortinsertion_sortbên dưới.
  • Tôi theo thuật ngữ của Scott Meyers để biểu thị C ++ 1y hiện tại đã là C ++ 14 và để biểu thị cả C ++ 98 và C ++ 03 là C ++ 98, vì vậy đừng kích thích tôi vì điều đó.
  • Như được đề xuất trong các nhận xét của @Mehrdad, tôi cung cấp bốn triển khai dưới dạng Ví dụ trực tiếp ở cuối câu trả lời: C ++ 14, C ++ 11, C ++ 98 và Boost và C ++ 98.
  • Câu trả lời chỉ được trình bày dưới dạng C ++ 14. Nếu có liên quan, tôi biểu thị sự khác biệt về cú pháp và thư viện trong đó các phiên bản ngôn ngữ khác nhau.

8
Sẽ thật tuyệt khi thêm thẻ Faq C ++ vào câu hỏi, mặc dù nó sẽ yêu cầu mất ít nhất một trong số những người khác. Tôi sẽ đề nghị loại bỏ các phiên bản (vì đây là một câu hỏi chung về C ++, với việc triển khai có sẵn trong hầu hết các phiên bản với một số điều chỉnh).
Matthieu M.

@TemplateRex Vâng, về mặt kỹ thuật, nếu không phải là Câu hỏi thường gặp thì câu hỏi này quá rộng (đoán - Tôi đã không downvote). Btw. công việc tốt, nhiều thông tin hữu ích, cảm ơn :)
BartoszKP 15/07/14

Câu trả lời:


388

Khối xây dựng thuật toán

Chúng tôi bắt đầu bằng cách lắp ráp các khối xây dựng thuật toán từ Thư viện chuẩn:

#include <algorithm>    // min_element, iter_swap, 
                        // upper_bound, rotate, 
                        // partition, 
                        // inplace_merge,
                        // make_heap, sort_heap, push_heap, pop_heap,
                        // is_heap, is_sorted
#include <cassert>      // assert 
#include <functional>   // less
#include <iterator>     // distance, begin, end, next
  • các công cụ lặp như không phải thành viên std::begin()/ std::end()cũng như std::next()chỉ có sẵn từ C ++ 11 trở lên. Đối với C ++ 98, người ta cần phải tự viết những thứ này. Có những sự thay thế từ Boost.Range in boost::begin()/ boost::end()và từ Boost.Utility in boost::next().
  • các std::is_sortedthuật toán chỉ có sẵn cho C ++ 11 và xa hơn nữa. Đối với C ++ 98, điều này có thể được thực hiện dưới dạng std::adjacent_findvà một đối tượng hàm viết tay. Boost.Alacticm cũng cung cấp boost::algorithm::is_sortedmột thay thế.
  • các std::is_heapthuật toán chỉ có sẵn cho C ++ 11 và xa hơn nữa.

Goodies thực tế

C ++ 14 cung cấp các bộ so sánh trong suốt của dạng std::less<>hoạt động đa hình trên các đối số của chúng. Điều này tránh phải cung cấp loại lặp. Điều này có thể được sử dụng kết hợp với các đối số khuôn mẫu hàm mặc định của C ++ 11 để tạo ra một quá tải duy nhất để sắp xếp các thuật toán lấy <so sánh và các thuật toán có đối tượng hàm so sánh do người dùng định nghĩa.

template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

Trong C ++ 11, người ta có thể định nghĩa bí danh mẫu có thể sử dụng lại để trích xuất loại giá trị của trình vòng lặp, điều này làm tăng thêm sự lộn xộn nhỏ cho chữ ký của thuật toán sắp xếp:

template<class It>
using value_type_t = typename std::iterator_traits<It>::value_type;

template<class It, class Compare = std::less<value_type_t<It>>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

Trong C ++ 98, người ta cần viết hai lần quá tải và sử dụng typename xxx<yyy>::typecú pháp dài dòng

template<class It, class Compare>
void xxx_sort(It first, It last, Compare cmp); // general implementation

template<class It>
void xxx_sort(It first, It last)
{
    xxx_sort(first, last, std::less<typename std::iterator_traits<It>::value_type>());
}
  • Một đặc điểm cú pháp khác là C ++ 14 tạo điều kiện bao bọc các bộ so sánh do người dùng định nghĩa thông qua lambdas đa hình (với autocác tham số được suy ra như các đối số khuôn mẫu hàm).
  • C ++ 11 chỉ có lambdas đơn hình, yêu cầu sử dụng bí danh mẫu ở trên value_type_t.
  • Trong C ++ 98, một trong hai cần phải viết một hàm đối tượng độc lập hoặc dùng đến các tiết std::bind1st/ std::bind2nd/ std::not1kiểu cú pháp.
  • Boost.Bind cải thiện điều này với boost::bind_1/ _2cú pháp giữ chỗ.
  • C ++ 11 và hơn thế nữa cũng có std::find_if_not, trong khi C ++ 98 cần std::find_ifcó một std::not1đối tượng chức năng xung quanh.

Phong cách C ++

Không có phong cách C ++ 14 thường được chấp nhận. Dù tốt hơn hay tồi tệ hơn, tôi theo sát bản dự thảo Hiệu quả hiện đại C ++ của Scott Meyers và GotW đã được tân trang lại của Herb Sutter . Tôi sử dụng các khuyến nghị phong cách sau đây:

  • Khuyến nghị "Hầu như luôn luôn tự động" của Herb Sutter và khuyến nghị "Ưu tiên tự động cho các khai báo loại cụ thể" của Scott Meyers , trong đó sự ngắn gọn là không thể vượt qua, mặc dù sự rõ ràng của nó đôi khi bị tranh cãi .
  • "Phân biệt (){}khi tạo đối tượng" của Scott Meyers và luôn chọn cách khởi tạo giằng {}thay vì khởi tạo cũ được ngoặc đơn tốt ()(để giải quyết các vấn đề phân tích rõ nhất trong mã chung).
  • Scott Meyers "Thích khai báo bí danh cho typedefs" . Đối với các mẫu, đây là điều bắt buộc và sử dụng nó ở mọi nơi thay vì typedeftiết kiệm thời gian và thêm tính nhất quán.
  • Tôi sử dụng một for (auto it = first; it != last; ++it)mẫu ở một số nơi, để cho phép kiểm tra bất biến vòng lặp cho các phạm vi con đã được sắp xếp. Trong mã sản xuất, việc sử dụng while (first != last)và một ++firstnơi nào đó bên trong vòng lặp có thể tốt hơn một chút.

Lựa chọn sắp xếp

Lựa chọn sắp xếp không thích ứng với dữ liệu theo bất kỳ cách nào, vì vậy thời gian chạy của nó luôn luônO(N²). Tuy nhiên, sắp xếp lựa chọn có đặc tính giảm thiểu số lượng giao dịch hoán đổi . Trong các ứng dụng có chi phí trao đổi vật phẩm cao, việc sắp xếp lựa chọn rất tốt có thể là thuật toán được lựa chọn.

Để triển khai nó bằng Thư viện chuẩn, sử dụng nhiều lần std::min_elementđể tìm phần tử tối thiểu còn lại và iter_swaphoán đổi nó vào vị trí:

template<class FwdIt, class Compare = std::less<>>
void selection_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const selection = std::min_element(it, last, cmp);
        std::iter_swap(selection, it); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

Lưu ý rằng selection_sortphạm vi đã được xử lý được [first, it)sắp xếp là bất biến vòng lặp của nó. Các yêu cầu tối thiểu là các trình vòng lặp chuyển tiếp , so với std::sortcác trình vòng lặp truy cập ngẫu nhiên.

Chi tiết bỏ qua :

  • lựa chọn sắp xếp có thể được tối ưu hóa với một thử nghiệm sớm if (std::distance(first, last) <= 1) return;(hoặc cho các vòng lặp chuyển tiếp / hai chiều if (first == last || std::next(first) == last) return;:).
  • đối với các trình lặp hai chiều , phép thử trên có thể được kết hợp với một vòng lặp trong khoảng thời gian [first, std::prev(last)), bởi vì phần tử cuối cùng được đảm bảo là phần tử còn lại tối thiểu và không yêu cầu trao đổi.

Sắp xếp chèn

Mặc dù nó là một trong những thuật toán sắp xếp cơ bản với O(N²)thời gian trong trường hợp xấu nhất, sắp xếp chèn là thuật toán được lựa chọn khi dữ liệu gần như được sắp xếp (vì nó thích nghi ) hoặc khi kích thước sự cố nhỏ (vì nó có chi phí thấp). Vì những lý do này và vì nó cũng ổn định , nên sắp xếp chèn thường được sử dụng làm trường hợp cơ sở đệ quy (khi kích thước bài toán nhỏ) cho các thuật toán sắp xếp phân chia và chinh phục trên cao hơn, chẳng hạn như sắp xếp hợp nhất hoặc sắp xếp nhanh.

Để triển khai insertion_sortvới Thư viện chuẩn, liên tục sử dụng std::upper_boundđể tìm vị trí mà phần tử hiện tại cần đến và sử dụng std::rotateđể dịch chuyển các phần tử còn lại lên trên trong phạm vi đầu vào:

template<class FwdIt, class Compare = std::less<>>
void insertion_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const insertion = std::upper_bound(first, it, *it, cmp);
        std::rotate(insertion, it, std::next(it)); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

Lưu ý rằng insertion_sortphạm vi đã được xử lý được [first, it)sắp xếp là bất biến vòng lặp của nó. Sắp xếp chèn cũng hoạt động với các vòng lặp phía trước.

Chi tiết bỏ qua :

  • sắp xếp chèn có thể được tối ưu hóa với thử nghiệm sớm if (std::distance(first, last) <= 1) return;(hoặc cho các vòng lặp chuyển tiếp / hai chiều if (first == last || std::next(first) == last) return;:) và một vòng lặp trong khoảng thời gian [std::next(first), last), bởi vì phần tử đầu tiên được đảm bảo đúng vị trí và không yêu cầu xoay.
  • đối với các trình vòng lặp hai chiều , tìm kiếm nhị phân để tìm điểm chèn có thể được thay thế bằng tìm kiếm tuyến tính ngược bằng std::find_if_notthuật toán của Thư viện chuẩn .

Bốn ví dụ trực tiếp ( C ++ 14 , C ++ 11 , C ++ 98 và Boost , C ++ 98 ) cho đoạn dưới đây:

using RevIt = std::reverse_iterator<BiDirIt>;
auto const insertion = std::find_if_not(RevIt(it), RevIt(first), 
    [=](auto const& elem){ return cmp(*it, elem); }
).base();
  • Đối với các đầu vào ngẫu nhiên, điều này đưa ra O(N²)so sánh, nhưng điều này cải thiện để O(N)so sánh cho các đầu vào gần như được sắp xếp. Tìm kiếm nhị phân luôn sử dụng O(N log N)so sánh.
  • Đối với phạm vi đầu vào nhỏ, địa phương bộ nhớ tốt hơn (bộ đệm, tìm nạp trước) của tìm kiếm tuyến tính cũng có thể chi phối tìm kiếm nhị phân (tất nhiên người ta nên kiểm tra điều này).

Sắp xếp nhanh chóng

Khi được thực hiện cẩn thận, sắp xếp nhanh là mạnh mẽ và có O(N log N)độ phức tạp dự kiến, nhưng với O(N²)độ phức tạp trong trường hợp xấu nhất có thể được kích hoạt với dữ liệu đầu vào được chọn bất lợi. Khi không cần một loại ổn định, sắp xếp nhanh là một loại mục đích chung tuyệt vời.

Ngay cả đối với các phiên bản đơn giản nhất, sắp xếp nhanh cũng phức tạp hơn một chút khi thực hiện bằng Thư viện chuẩn so với các thuật toán sắp xếp cổ điển khác. Cách tiếp cận bên dưới sử dụng một vài tiện ích lặp để xác định phần tử ở giữa của phạm vi đầu vào [first, last)làm trục, sau đó sử dụng hai lệnh gọi std::partition(để O(N)) phân vùng ba chiều phạm vi đầu vào thành các phân đoạn của các phần tử nhỏ hơn, bằng, và lớn hơn trục đã chọn, tương ứng. Cuối cùng, hai phân đoạn bên ngoài có các phần tử nhỏ hơn và lớn hơn trục được sắp xếp theo cách đệ quy:

template<class FwdIt, class Compare = std::less<>>
void quick_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;
    auto const pivot = *std::next(first, N / 2);
    auto const middle1 = std::partition(first, last, [=](auto const& elem){ 
        return cmp(elem, pivot); 
    });
    auto const middle2 = std::partition(middle1, last, [=](auto const& elem){ 
        return !cmp(pivot, elem);
    });
    quick_sort(first, middle1, cmp); // assert(std::is_sorted(first, middle1, cmp));
    quick_sort(middle2, last, cmp);  // assert(std::is_sorted(middle2, last, cmp));
}

Tuy nhiên, sắp xếp nhanh là khá khó để có được chính xác và hiệu quả, vì mỗi bước trên phải được kiểm tra cẩn thận và tối ưu hóa cho mã mức sản xuất. Cụ thể, đối với O(N log N)độ phức tạp, trục phải dẫn đến một phân vùng cân bằng của dữ liệu đầu vào, không thể được đảm bảo chung cho một O(1)trục, nhưng có thể được đảm bảo nếu đặt trục làm O(N)trung vị của phạm vi đầu vào.

Chi tiết bỏ qua :

  • việc thực hiện ở trên đặc biệt dễ bị tổn thương đối với các đầu vào đặc biệt, ví dụ: nó có O(N^2)độ phức tạp đối với đầu vào " ống nội tạng " 1, 2, 3, ..., N/2, ... 3, 2, 1(vì phần giữa luôn lớn hơn tất cả các phần tử khác).
  • Lựa chọn trục giữa của 3 trong số các yếu tố được chọn ngẫu nhiên từ các bộ bảo vệ phạm vi đầu vào so với các đầu vào gần như được sắp xếp mà độ phức tạp sẽ giảm điO(N^2).
  • Phân vùng 3 chiều (tách các phần tử nhỏ hơn, bằng và lớn hơn trục) như được hiển thị bởi hai lệnh gọistd::partitionkhông phải làO(N)thuật toánhiệu quả nhấtđể đạt được kết quả này.
  • đối với các trình vòng lặp truy cập ngẫu nhiên , O(N log N)độ phức tạp được đảm bảo có thể đạt được thông qua lựa chọn trục trung bình bằng cách sử dụng std::nth_element(first, middle, last), theo sau là các cuộc gọi đệ quy đến quick_sort(first, middle, cmp)quick_sort(middle, last, cmp).
  • tuy nhiên, sự đảm bảo này phải trả giá, bởi vì hệ số không đổi của O(N)độ phức tạp std::nth_elementcó thể đắt hơn so với O(1)độ phức tạp của trục trung bình 3 theo sau là một O(N)cuộc gọi đến std::partition(một chuyển tiếp đơn thân thiện với bộ đệm dữ liệu).

Hợp nhất sắp xếp

Nếu việc sử dụng O(N)thêm không gian không đáng lo ngại, thì sắp xếp hợp nhất là một lựa chọn tuyệt vời: đó là thuật toán sắp xếp ổn định duy nhất O(N log N).

Thật đơn giản để thực hiện bằng thuật toán tiêu chuẩn: sử dụng một vài tiện ích lặp để xác định giữa phạm vi đầu vào [first, last)và kết hợp hai phân đoạn được sắp xếp đệ quy với std::inplace_merge:

template<class BiDirIt, class Compare = std::less<>>
void merge_sort(BiDirIt first, BiDirIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;                   
    auto const middle = std::next(first, N / 2);
    merge_sort(first, middle, cmp); // assert(std::is_sorted(first, middle, cmp));
    merge_sort(middle, last, cmp);  // assert(std::is_sorted(middle, last, cmp));
    std::inplace_merge(first, middle, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

Hợp nhất sắp xếp yêu cầu các trình lặp hai chiều, nút cổ chai là std::inplace_merge. Lưu ý rằng khi sắp xếp danh sách được liên kết, sắp xếp hợp nhất chỉ O(log N)cần thêm không gian (cho đệ quy). Thuật toán thứ hai được triển khai std::list<T>::sorttrong Thư viện chuẩn.

Sắp xếp đống

Heap sort rất đơn giản để thực hiện, thực hiệnO(N log N)sắp xếp tại chỗ, nhưng không ổn định.

Vòng lặp đầu tiên, O(N)giai đoạn "heapify", đưa mảng vào thứ tự heap. Vòng lặp thứ hai, O(N log Ngiai đoạn "sắp xếp", liên tục trích xuất tối đa và khôi phục thứ tự heap. Thư viện tiêu chuẩn làm cho điều này cực kỳ đơn giản:

template<class RandomIt, class Compare = std::less<>>
void heap_sort(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    lib::make_heap(first, last, cmp); // assert(std::is_heap(first, last, cmp));
    lib::sort_heap(first, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

Trong trường hợp bạn coi đó là "gian lận" để sử dụng std::make_heapstd::sort_heap, bạn có thể đi sâu hơn một cấp và tự viết các chức năng đó theo std::push_heapstd::pop_heaptương ứng:

namespace lib {

// NOTE: is O(N log N), not O(N) as std::make_heap
template<class RandomIt, class Compare = std::less<>>
void make_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last;) {
        std::push_heap(first, ++it, cmp); 
        assert(std::is_heap(first, it, cmp));           
    }
}

template<class RandomIt, class Compare = std::less<>>
void sort_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = last; it != first;) {
        std::pop_heap(first, it--, cmp);
        assert(std::is_heap(first, it, cmp));           
    } 
}

}   // namespace lib

Thư viện chuẩn chỉ định cả hai push_heappop_heapđộ phức tạp O(log N). Tuy nhiên, lưu ý rằng vòng lặp bên ngoài phạm vi [first, last)dẫn đến O(N log N)độ phức tạp make_heap, trong khi std::make_heapchỉ có O(N)độ phức tạp. Đối với sự O(N log N)phức tạp tổng thể của heap_sortnó không thành vấn đề.

Chi tiết bỏ qua : O(N)thực hiệnmake_heap

Kiểm tra

Dưới đây là bốn ví dụ trực tiếp ( C ++ 14 , C ++ 11 , C ++ 98 và Boost , C ++ 98 ) kiểm tra tất cả năm thuật toán trên nhiều loại đầu vào (không có nghĩa là toàn diện hoặc nghiêm ngặt). Chỉ cần lưu ý sự khác biệt rất lớn trong LOC: C ++ 11 / C ++ 14 cần khoảng 130 LOC, C ++ 98 và Boost 190 (+ 50%) và C ++ 98 hơn 270 (+ 100%).


13
Mặc dù tôi không đồng ý với việc bạn sử dụngauto (và nhiều người không đồng ý với tôi), tôi rất thích thấy các thuật toán thư viện tiêu chuẩn được sử dụng tốt. Tôi muốn xem một số ví dụ về loại mã này sau khi xem bài nói chuyện của Sean Parent. Ngoài ra, tôi không có ý tưởng nào std::iter_swaptồn tại, mặc dù nó có vẻ lạ đối với tôi <algorithm>.
Joseph Mansfield

32
@sbabbi Toàn bộ thư viện chuẩn dựa trên nguyên tắc các trình lặp rẻ tiền để sao chép; nó vượt qua chúng bằng giá trị, ví dụ. Nếu sao chép một trình vòng lặp không rẻ, thì bạn sẽ gặp vấn đề về hiệu suất ở mọi nơi.
James Kanze

2
Bài đăng tuyệt vời. Về phần gian lận của [std ::] make_heap. Nếu std :: make_heap bị coi là gian lận, thì std :: push_heap. Tức là gian lận = không thực hiện hành vi thực tế được xác định cho cấu trúc heap. Tôi sẽ tìm thấy nó có hướng dẫn bao gồm cả đẩy_heap.
Thuyền trưởng Hươu cao cổ

3
@gnzlbg Các khẳng định bạn có thể nhận xét, tất nhiên. Thử nghiệm sớm có thể được gửi theo thẻ cho mỗi loại lặp, với phiên bản hiện tại để truy cập ngẫu nhiên và if (first == last || std::next(first) == last). Tôi có thể cập nhật nó sau. Việc triển khai nội dung trong phần "chi tiết bị bỏ qua" nằm ngoài phạm vi của câu hỏi, IMO, vì chúng chứa các liên kết đến toàn bộ bản thân Q & As. Thực hiện các thói quen sắp xếp từ thực là khó!
TemplateRex

3
Bài đăng tuyệt vời. Mặc dù, tôi đã lừa dối với quicksort của bạn bằng cách sử dụng nth_elementtheo ý kiến ​​của tôi. nth_elementđã thực hiện một nửa quicksort (bao gồm bước phân vùng và đệ quy trên một nửa bao gồm phần tử thứ n mà bạn quan tâm).
sellibitze

14

Một số nhỏ và khá thanh lịch ban đầu được tìm thấy trên đánh giá mã . Tôi nghĩ rằng nó là giá trị chia sẻ.

Sắp xếp đếm

Mặc dù nó khá chuyên biệt, đếm sắp xếp là một thuật toán sắp xếp số nguyên đơn giản và thường có thể thực sự nhanh chóng với điều kiện các giá trị của các số nguyên để sắp xếp không quá xa nhau. Có lẽ lý tưởng nếu người ta cần sắp xếp một bộ sưu tập một triệu số nguyên được biết là từ 0 đến 100 chẳng hạn.

Để thực hiện một loại đếm rất đơn giản, hoạt động với cả số nguyên có dấu và không dấu, người ta cần tìm các phần tử nhỏ nhất và lớn nhất trong bộ sưu tập để sắp xếp; sự khác biệt của chúng sẽ cho biết kích thước của mảng đếm được phân bổ. Sau đó, một lần thứ hai đi qua bộ sưu tập được thực hiện để đếm số lần xuất hiện của mọi yếu tố. Cuối cùng, chúng tôi ghi lại số lượng yêu cầu của mỗi số nguyên trở lại bộ sưu tập ban đầu.

template<typename ForwardIterator>
void counting_sort(ForwardIterator first, ForwardIterator last)
{
    if (first == last || std::next(first) == last) return;

    auto minmax = std::minmax_element(first, last);  // avoid if possible.
    auto min = *minmax.first;
    auto max = *minmax.second;
    if (min == max) return;

    using difference_type = typename std::iterator_traits<ForwardIterator>::difference_type;
    std::vector<difference_type> counts(max - min + 1, 0);

    for (auto it = first ; it != last ; ++it) {
        ++counts[*it - min];
    }

    for (auto count: counts) {
        first = std::fill_n(first, count, min++);
    }
}

Mặc dù nó chỉ hữu ích khi phạm vi của các số nguyên được sắp xếp là nhỏ (thường không lớn hơn kích thước của bộ sưu tập để sắp xếp), làm cho việc đếm sắp xếp chung chung hơn sẽ làm cho nó chậm hơn trong các trường hợp tốt nhất của nó. Nếu phạm vi không được biết là nhỏ, một thuật toán khác như sắp xếp cơ số , ska_sort hoặc lây lan có thể được sử dụng thay thế.

Chi tiết bỏ qua :

  • Chúng ta có thể đã vượt qua giới hạn của phạm vi các giá trị được thuật toán chấp nhận làm tham số để hoàn toàn thoát khỏi lần std::minmax_elementchuyển đầu tiên qua bộ sưu tập. Điều này sẽ làm cho thuật toán nhanh hơn nữa khi các giới hạn phạm vi nhỏ hữu ích được biết đến bằng các phương tiện khác. (Nó không phải là chính xác; đi qua một hằng số 0 đến 100 vẫn còn nhiều hơn một đường chuyền thêm hơn một triệu các yếu tố để phát hiện ra rằng các giới hạn thực sự là 1 đến 95. Thậm chí 0-1000 sẽ có giá trị nó; sự các phần tử phụ được viết một lần bằng 0 và đọc một lần).

  • Phát triển countstrên bay là một cách khác để tránh vượt qua đầu tiên riêng biệt. Nhân đôi countskích thước mỗi lần tăng phải cho thời gian khấu hao O (1) cho mỗi phần tử được sắp xếp (xem phân tích chi phí chèn bảng băm để chứng minh rằng số mũ tăng trưởng là chìa khóa). Phát triển cuối cùng cho một cái mới maxthật dễ dàng với std::vector::resizeviệc thêm các phần tử zero mới. Thay đổi minnhanh chóng và chèn các phần tử zero mới ở phía trước có thể được thực hiện std::copy_backwardsau khi phát triển vectơ. Sau đó std::fillđể không các yếu tố mới.

  • Các countsvòng lặp increment là một biểu đồ. Nếu dữ liệu có khả năng lặp lại cao và số lượng thùng nhỏ, thì có thể không kiểm soát được nhiều mảng để giảm tắc nghẽn phụ thuộc dữ liệu tuần tự hóa của cửa hàng / tải lại vào cùng một thùng. Điều này có nghĩa là số đếm nhiều hơn về 0 khi bắt đầu và nhiều lần lặp lại ở cuối, nhưng đáng giá trên hầu hết các CPU cho ví dụ của chúng tôi về hàng triệu từ 0 đến 100 số, đặc biệt là nếu đầu vào có thể được sắp xếp (một phần) và có những bước chạy dài cùng số.

  • Trong thuật toán trên, chúng tôi sử dụng một min == maxkiểm tra để trả về sớm khi mọi phần tử có cùng giá trị (trong trường hợp đó bộ sưu tập được sắp xếp). Thay vào đó, thực sự có thể kiểm tra đầy đủ xem bộ sưu tập đã được sắp xếp chưa trong khi tìm các giá trị cực trị của bộ sưu tập mà không lãng phí thêm thời gian (nếu lần đầu tiên vẫn bị tắc nghẽn bộ nhớ với công việc bổ sung là cập nhật tối thiểu và tối đa). Tuy nhiên, một thuật toán như vậy không tồn tại trong thư viện tiêu chuẩn và việc viết một thuật toán sẽ tẻ nhạt hơn so với việc viết phần còn lại của việc sắp xếp chính nó. Nó được để lại như một bài tập cho người đọc.

  • Vì thuật toán chỉ hoạt động với các giá trị nguyên, các xác nhận tĩnh có thể được sử dụng để ngăn người dùng mắc lỗi loại rõ ràng. Trong một số bối cảnh, một sự thay thế với std::enable_if_tcó thể được ưu tiên.

  • Trong khi C ++ hiện đại thì tuyệt vời, C ++ trong tương lai có thể còn tuyệt vời hơn: các ràng buộc có cấu trúc và một số phần của Ranges TS sẽ làm cho thuật toán trở nên sạch hơn.


@TemplateRex Nếu có thể lấy một đối tượng so sánh tùy ý, nó sẽ làm cho việc sắp xếp sắp xếp một loại so sánh và các loại so sánh không thể có trường hợp xấu nhất tốt hơn O (n log n). Sắp xếp đếm có trường hợp xấu nhất là O (n + r), điều đó có nghĩa là dù sao nó cũng không thể là một loại so sánh. Các số nguyên có thể được so sánh nhưng thuộc tính này không được sử dụng để thực hiện sắp xếp (nó chỉ được sử dụng trong std::minmax_elementđó chỉ thu thập thông tin). Thuộc tính được sử dụng là thực tế là số nguyên có thể được sử dụng như chỉ số hoặc giá trị bù và chúng có thể tăng lên trong khi bảo toàn thuộc tính sau.
Morwenn

Ranges TS thực sự rất đẹp, ví dụ: vòng lặp cuối cùng có thể kết thúc counts | ranges::view::filter([](auto c) { return c != 0; })để bạn không phải kiểm tra nhiều lần về số lượng khác không bên trong fill_n.
TemplateRex

(Tôi đã tìm thấy lỗi chính tả trong small một ratherappart- tôi có thể giữ chúng cho đến khi chỉnh sửa liên quan đến reggae_sort không?)
greybeard

@greybeard Bạn có thể làm bất cứ điều gì bạn muốn: p
Morwenn

Tôi nghi ngờ rằng việc phát triển nhanh chóng counts[]sẽ là một chiến thắng so với việc vượt qua đầu vào minmax_elementtrước khi lập biểu đồ. Đặc biệt đối với trường hợp sử dụng trong trường hợp lý tưởng, đầu vào rất lớn với nhiều lần lặp lại trong một phạm vi nhỏ, bởi vì bạn sẽ nhanh chóng phát triển countsđến kích thước đầy đủ của nó, với một vài sai lệch chi nhánh hoặc nhân đôi kích thước. (Tất nhiên, biết một phạm vi đủ nhỏ trong phạm vi sẽ cho phép bạn tránh minmax_elementquét tránh kiểm tra giới hạn bên trong vòng lặp biểu đồ.)
Peter Cordes
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.