एल्गोरिदमिक बिल्डिंग ब्लॉक
हम मानक लाइब्रेरी से एल्गोरिथम बिल्डिंग ब्लॉक्स को असेंबल करके शुरू करते हैं:
#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
- नॉन-मेंबर
std::begin()
/ std::end()
साथ ही साथ इट्रेटर टूल std::next()
केवल C ++ 11 और उसके बाद के संस्करण के रूप में उपलब्ध हैं। C ++ 98 के लिए, इन्हें स्वयं लिखने की आवश्यकता है। Boost.Range से boost::begin()
/ में boost::end()
, और Boost.Utility में से विकल्प हैं boost::next()
।
std::is_sorted
एल्गोरिथ्म सी ++ के लिए 11 और उससे आगे ही उपलब्ध है। C ++ 98 के लिए, इसे std::adjacent_find
और हाथ से लिखी गई फ़ंक्शन ऑब्जेक्ट के संदर्भ में लागू किया जा सकता है । Boost.Algorithm boost::algorithm::is_sorted
एक विकल्प के रूप में भी प्रदान करता है ।
std::is_heap
एल्गोरिथ्म सी ++ के लिए 11 और उससे आगे ही उपलब्ध है।
सिंथेटिक नेकियां
सी ++ 14 फॉर्म के पारदर्शी तुलनात्मक रूप प्रदान करता है std::less<>
जो उनके तर्कों पर बहुरूपिए रूप से कार्य करते हैं। यह इट्रेटर के प्रकार प्रदान करने से बचा जाता है। इसका उपयोग C ++ 11 के डिफ़ॉल्ट फ़ंक्शन टेम्प्लेट तर्कों के साथ संयोजन के लिए एक अधिभार बनाने के लिए किया जा सकता है जो <
तुलनात्मक रूप से लेने वाले एल्गोरिदम के लिए है और जिनके पास उपयोगकर्ता-परिभाषित तुलना फ़ंक्शन फ़ंक्शन है।
template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});
C ++ 11 में, कोई पुन: प्रयोज्य टेम्प्लेट उर्फ को इट्रेटर के मान प्रकार को परिभाषित करने के लिए परिभाषित कर सकता है जो सॉर्ट एल्गोरिदम के हस्ताक्षर में मामूली अव्यवस्था जोड़ता है:
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{});
C ++ 98 में, एक को दो अधिभार लिखने और क्रिया typename xxx<yyy>::type
सिंटैक्स का उपयोग करने की आवश्यकता है
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>());
}
- एक और वाक्यविन्यास अच्छाई यह है कि C ++ 14 पॉलीमॉर्फिक लैम्ब्डा के माध्यम से उपयोगकर्ता-परिभाषित तुलनित्रों को लपेटने की सुविधा देता है (
auto
फ़ंक्शन टेम्पलेट तर्कों जैसे मापदंडों के साथ कटौती की जाती है)।
- C ++ 11 में केवल मोनोमोर्फिक लैम्ब्डा है, जिसे उपरोक्त टेम्पलेट उर्फ के उपयोग की आवश्यकता है
value_type_t
।
- C ++ 98 में, किसी को या तो स्टैंडअलोन फ़ंक्शन ऑब्जेक्ट लिखने या वर्बोस
std::bind1st
/ std::bind2nd
/ std::not1
प्रकार के सिंटैक्स का सहारा लेने की आवश्यकता होती है।
- Boost.Bind इसे
boost::bind
और _1
/ _2
प्लेसहोल्डर सिंटैक्स के साथ सुधारता है ।
- सी ++ 11 और उससे आगे भी है
std::find_if_not
, जबकि सी ++ 98 जरूरत है, std::find_if
एक साथ std::not1
एक समारोह वस्तु के आसपास।
सी ++ शैली
आम तौर पर स्वीकार्य C ++ 14 शैली अभी तक नहीं है। बेहतर या बदतर के लिए, मैं स्कॉट मेयर्स के प्रभावी मॉडर्न सी ++ और हर्ब सटर के संशोधित गॉटडब्ल्यू का बारीकी से पालन करता हूं । मैं निम्नलिखित शैली सिफारिशों का उपयोग करता हूं:
- हर्ब सटर की "लगभग ऑलवेज ऑटो" और स्कॉट मेयर्स की "विशिष्ट प्रकार की घोषणाओं के लिए ऑटो को प्राथमिकता दें" सिफारिश, जिसके लिए संक्षिप्तता नायाब है, हालांकि इसकी स्पष्टता कभी-कभी विवादित होती है ।
- स्कॉट मेयर्स की "भेद
()
और {}
जब वस्तुएं बनाते हैं" और {}
अच्छे पुराने पेरेंटाइज्ड इनिशियलाइज़ेशन के बजाय लगातार ब्रेस-इनिशियलाइज़ेशन चुनते हैं ()
(जेनेरिक कोड में सभी सबसे वैक्सिंग-पार्स मुद्दों को साइड-स्टेप करने के लिए)।
- स्कॉट मेयर्स की "टाइपराइफ के लिए अन्य उपनामों को प्राथमिकता दें" । टेम्प्लेट्स के लिए यह वैसे भी बहुत जरूरी है, और
typedef
समय बचाने के बजाय हर जगह इसका उपयोग करना और स्थिरता जोड़ता है।
- मैं
for (auto it = first; it != last; ++it)
कुछ स्थानों में एक पैटर्न का उपयोग करता हूं , ताकि पहले से छांटे गए उप-श्रेणियों के लिए लूप इनवेरिएंट चेकिंग की अनुमति मिल सके। उत्पादन कोड में, लूप के अंदर while (first != last)
और ++first
कहीं का उपयोग थोड़ा बेहतर हो सकता है।
चयन छांटना
चयन प्रकार किसी भी तरह से डेटा के अनुकूल नहीं होता है, इसलिए इसका रनटाइम हमेशा होता हैO(N²)
। हालांकि, चयन प्रकार में स्वैप की संख्या को कम करने की संपत्ति है। उन अनुप्रयोगों में जहां स्वैपिंग आइटम की लागत अधिक है, चयन प्रकार बहुत अच्छी तरह से पसंद का एल्गोरिदम हो सकता है।
मानक लाइब्रेरी का उपयोग करके इसे लागू std::min_element
करने के लिए, शेष न्यूनतम तत्व को खोजने के लिए बार-बार उपयोग करें, और iter_swap
इसे जगह में स्वैप करें:
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));
}
}
ध्यान दें कि selection_sort
पहले से ही संसाधित सीमा [first, it)
इसकी लूप इनवेरिएंट के रूप में क्रमबद्ध है। न्यूनतम आवश्यकताएं पुनरावृत्तियों के लिए हैं , std::sort
यादृच्छिक अभिगमकर्ताओं की तुलना में ।
विवरण छोड़ा गया :
- चयन प्रकार को प्रारंभिक परीक्षा
if (std::distance(first, last) <= 1) return;
(या आगे / द्विदिश पुनरावृत्तियों के लिए if (first == last || std::next(first) == last) return;
) के साथ अनुकूलित किया जा सकता है ।
- के लिए द्विदिश iterators , ऊपर परीक्षण अंतराल पर एक पाश के साथ जोड़ा जा सकता है
[first, std::prev(last))
क्योंकि पिछले तत्व न्यूनतम शेष तत्व होने की गारंटी है और एक स्वैप की आवश्यकता नहीं है,।
सम्मिलन सॉर्ट
यद्यपि यह O(N²)
सबसे खराब समय के साथ प्राथमिक सॉर्टिंग एल्गोरिदम में से एक है , सम्मिलन सॉर्ट पसंद का एल्गोरिथ्म है जब डेटा लगभग सॉर्ट किया जाता है (क्योंकि यह अनुकूली है ) या जब समस्या का आकार छोटा होता है (क्योंकि इसमें कम ओवरहेड होता है)। इन कारणों के लिए, और क्योंकि यह भी स्थिर है , सम्मिलन प्रकार का उपयोग अक्सर उच्च ओवरहेड डिवाइड और-विजयी सॉर्टिंग एल्गोरिदम जैसे मर्ज सॉर्ट या त्वरित सॉर्ट के लिए पुनरावर्ती बेस केस (जब समस्या का आकार छोटा होता है) के रूप में किया जाता है।
insertion_sort
मानक लाइब्रेरी के साथ कार्यान्वित करने के लिए, बार-बार std::upper_bound
उस स्थान को खोजने के लिए उपयोग करें जहां वर्तमान तत्व को जाने की आवश्यकता है, और std::rotate
शेष तत्वों को इनपुट रेंज में ऊपर की ओर स्थानांतरित करने के लिए उपयोग करें:
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));
}
}
ध्यान दें कि insertion_sort
पहले से ही संसाधित सीमा [first, it)
इसकी लूप इनवेरिएंट के रूप में क्रमबद्ध है। सम्मिलन क्रम आगे चलने वाले के साथ भी काम करता है।
विवरण छोड़ा गया :
- सम्मिलन सॉर्ट को प्रारंभिक परीक्षण
if (std::distance(first, last) <= 1) return;
(या फॉरवर्ड / बिडायरेक्शनल पुनरावृत्तियों:) if (first == last || std::next(first) == last) return;
और अंतराल पर एक लूप के साथ अनुकूलित किया जा सकता है [std::next(first), last)
, क्योंकि पहला तत्व जगह में रहने की गारंटी देता है और इसे घुमाने की आवश्यकता नहीं होती है।
- के लिए द्विदिश iterators , द्विआधारी खोज सम्मिलन बिंदु को खोजने के लिए एक साथ बदला जा सकता रिवर्स रैखिक खोज स्टैंडर्ड लाइब्रेरी का उपयोग कर
std::find_if_not
एल्गोरिथ्म।
नीचे दिए गए टुकड़े के लिए चार लाइव उदाहरण ( C ++ 14 , C ++ 11 , C ++ 98 और Boost , C ++ 98 ):
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();
- यादृच्छिक आदानों के लिए यह
O(N²)
तुलना देता है , लेकिन यह O(N)
लगभग छांटे गए इनपुट के लिए तुलना में सुधार करता है । बाइनरी सर्च हमेशा O(N log N)
तुलना का उपयोग करता है ।
- छोटी इनपुट श्रेणियों के लिए, रैखिक खोज की बेहतर मेमोरी लोकलिटी (कैश, प्रीफेटिंग) एक द्विआधारी खोज पर भी हावी हो सकती है (किसी को भी इसका परीक्षण करना चाहिए)।
जल्दी से सुलझाएं
जब ध्यान से लागू किया जाता है, तो त्वरित सॉर्ट मजबूत होता है और इससे O(N log N)
जटिलता की उम्मीद होती है, लेकिन O(N²)
सबसे खराब स्थिति जटिलता के साथ प्रतिकूल रूप से चुने गए इनपुट डेटा के साथ शुरू हो सकती है। जब एक स्थिर प्रकार की आवश्यकता नहीं होती है, तो त्वरित सॉर्ट एक उत्कृष्ट सामान्य-उद्देश्य सॉर्ट है।
यहां तक कि सबसे सरल संस्करणों के लिए, अन्य क्लासिक सॉर्टिंग एल्गोरिदम की तुलना में स्टैंडर्ड लाइब्रेरी का उपयोग करने के लिए त्वरित सॉर्ट काफी जटिल है। का उपयोग करता है कुछ इटरेटर उपयोगिताओं नीचे दृष्टिकोण का पता लगाने का मध्य तत्व इनपुट रेंज के [first, last)
धुरी के रूप में, तो दो के लिए कॉल का उपयोग std::partition
(जो कर रहे हैं O(N)
तत्वों है कि तुलना में छोटे होते के क्षेत्रों में इनपुट रेंज तीन तरह विभाजन के लिए), के बराबर, और क्रमशः चयनित धुरी से बड़ा है। अंत में धुरी से छोटे और बड़े तत्वों वाले दो बाहरी खंडों को पुन: क्रमबद्ध किया जाता है:
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));
}
हालांकि, त्वरित सॉर्ट सही और कुशल पाने के लिए मुश्किल है, क्योंकि उपरोक्त प्रत्येक चरण को सावधानीपूर्वक जांचना और उत्पादन स्तर कोड के लिए अनुकूलित करना है। विशेष रूप से, O(N log N)
जटिलता के लिए, धुरी को इनपुट डेटा के संतुलित विभाजन में परिणाम करना पड़ता है, जिसे सामान्य रूप से O(1)
धुरी के लिए गारंटी नहीं दी जा सकती है , लेकिन जिसे गारंटी दी जा सकती है यदि कोई धुरी O(N)
इनपुट रेंज के मध्य के रूप में सेट करता है ।
विवरण छोड़ा गया :
- उपरोक्त कार्यान्वयन विशेष रूप से विशेष इनपुट के लिए कमजोर है, उदाहरण
O(N^2)
के लिए " ऑर्गन पाइप " इनपुट के लिए इसकी जटिलता है 1, 2, 3, ..., N/2, ... 3, 2, 1
(क्योंकि मध्य हमेशा अन्य सभी तत्वों से बड़ा होता है)।
- लगभग 3 तरह के इनपुट के खिलाफ इनपुट रेंज गार्डसे बेतरतीब ढंग से चुने गए तत्वों से माध्यिका -3 की धुरी चयनजिसके लिए जटिलता अन्यथा बिगड़ जाएगी
O(N^2)
।
- 3- कॉल पार्टीटिंग (तत्वों को छोटे से अलग, धुरी के बराबर और बड़े से अलग) जैसा कि दो कॉल द्वारा दिखाया गया है,इस परिणाम को प्राप्त करने के लिए
std::partition
सबसे कुशलO(N)
एल्गोरिथ्मनहीं है।
- के लिए रैंडम एक्सेस iterators , एक गारंटीकृत
O(N log N)
जटिलता के माध्यम से प्राप्त किया जा सकता मंझला धुरी चयन का उपयोग कर std::nth_element(first, middle, last)
, करने के लिए पुनरावर्ती कॉल के बाद quick_sort(first, middle, cmp)
और quick_sort(middle, last, cmp)
।
- यह गारंटी, हालांकि, लागत पर आती है, क्योंकि
O(N)
कॉम्प्लेक्स की निरंतरता का कारक एक मंझला -३ पिवट std::nth_element
की O(1)
जटिलता की तुलना में अधिक महंगा हो सकता है, जिसके बाद O(N)
कॉल के लिए कॉल किया जा सकता है std::partition
(जो कि कैश-फ्रेंडली सिंगल फॉरवर्ड पास है आँकड़े)।
मर्ज़ सॉर्ट
यदि O(N)
अतिरिक्त स्थान का उपयोग करना कोई चिंता का विषय नहीं है, तो मर्ज सॉर्ट एक उत्कृष्ट विकल्प है: यह एकमात्र स्थिर O(N log N)
सॉर्टिंग एल्गोरिदम है।
मानक एल्गोरिदम का उपयोग करके इसे लागू करना सरल है: इनपुट रेंज के मध्य का पता लगाने [first, last)
और दो पुनरावर्ती सॉर्ट किए गए सेगमेंट को एक के साथ संयोजित करने के लिए कुछ इटरेटर उपयोगिताओं का उपयोग करें 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));
}
मर्ज सॉर्ट के लिए द्विदिश चलने वाले, टोंटी होने की आवश्यकता होती है std::inplace_merge
। ध्यान दें कि लिंक की गई सूचियों को सॉर्ट करते समय, मर्ज सॉर्ट के लिए केवल O(log N)
अतिरिक्त स्थान (पुनरावर्तन के लिए) की आवश्यकता होती है । बाद के एल्गोरिथ्म को std::list<T>::sort
मानक पुस्तकालय में लागू किया गया है।
ढेर बनाएं और छांटें
हीप सॉर्ट लागू करने के लिए सरल है,O(N log N)
इन-प्लेस सॉर्ट करता है, लेकिन स्थिर नहीं है।
पहला लूप, O(N)
"हीपाइफाई" चरण, सरणी को ढेर क्रम में रखता है। दूसरा लूप, O(N log 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));
}
यदि आप उपयोग करने के लिए इसे "धोखा" मानते हैं std::make_heap
और std::sort_heap
, आप क्रमशः एक स्तर पर जा सकते हैं और उन कार्यों को क्रमशः std::push_heap
और के संदर्भ में लिख सकते हैं std::pop_heap
:
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
मानक पुस्तकालय दोनों push_heap
और pop_heap
जटिलता के रूप में निर्दिष्ट करता है O(log N)
। नोट हालांकि सीमा पर बाहरी पाश है कि [first, last)
में परिणाम O(N log N)
के लिए जटिलता make_heap
है, जबकि std::make_heap
केवल O(N)
जटिलता। समग्र O(N log N)
जटिलता के लिए heap_sort
यह कोई फर्क नहीं पड़ता।
विवरण छोड़ा गया : का O(N)
कार्यान्वयनmake_heap
परिक्षण
यहां चार लाइव उदाहरण ( C ++ 14 , C ++ 11 , C ++ 98 और Boost , C ++ 98 ) सभी पांच एल्गोरिदम का परीक्षण विभिन्न प्रकार के इनपुटों पर किया गया है (जिसका अर्थ संपूर्ण या कठोर नहीं है)। LOC में भारी अंतर पर ध्यान दें: C ++ 11 / C ++ 14 को लगभग 130 LOC, C ++ 98 और Boost 190 (+ 50%) और C ++ 98 270 (+ 100%) से अधिक की आवश्यकता है।