Algoritmik yapı taşları
Standart Kütüphane'den algoritmik yapı taşlarını birleştirerek başlıyoruz:
#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
- üye olmayan
std::begin()
/ std::end()
gibi yineleyici araçları std::next()
yalnızca C ++ 11 ve sonrası için kullanılabilir. C ++ 98 için, bunları kendisinin yazması gerekir. Orada yerine de Boost.Range gelmektedir boost::begin()
/ ' boost::end()
ve de Boost.Utility dan boost::next()
.
std::is_sorted
Algoritma 11 ve öbür C ++ için kullanılabilir. C ++ 98 için bu std::adjacent_find
ve elle yazılmış bir fonksiyon nesnesi açısından uygulanabilir . Algoritma ayrıca bir boost::algorithm::is_sorted
a sağlar .
std::is_heap
Algoritma 11 ve öbür C ++ için kullanılabilir.
Sözdizimsel hediyeler
C ++ 14, argümanları üzerinde polimorfik olarak hareket eden formun şeffaf karşılaştırıcılarını sağlar std::less<>
. Bu, bir yineleyici türü sağlamak zorunda kalmaz. Bu, karşılaştırma olarak alınan ve kullanıcı tanımlı bir karşılaştırma işlevi nesnesi olan algoritmaları sıralamak için tek bir aşırı yük oluşturmak üzere C ++ 11'in varsayılan işlev şablonu bağımsız değişkenleriyle birlikte kullanılabilir .<
template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});
C ++ 11'de, sıralama algoritmalarının imzalarına küçük bir karmaşa ekleyen bir yineleyicinin değer türünü ayıklamak için yeniden kullanılabilir bir şablon diğer adı tanımlanabilir :
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'de, birinin iki aşırı yük yazması ve ayrıntılı typename xxx<yyy>::type
sözdizimini kullanması gerekir
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>());
}
- Başka bir sözdizimsel özellik, C ++ 14'ün kullanıcı tanımlı karşılaştırıcıları polimorfik lambdalar (
auto
fonksiyon şablonu argümanları gibi çıkarılmış parametrelerle) ile sarmayı kolaylaştırmasıdır .
- C ++ 11 sadece yukarıdaki şablon takma adının kullanılmasını gerektiren monomorfik lambdalara sahiptir
value_type_t
.
- C ++ 98'de, bağımsız bir işlev nesnesi yazmak veya sözdizimi
std::bind1st
/ std::bind2nd
/ std::not1
türünün sözdizimine başvurmak gerekir .
- Boost.Bind bunu
boost::bind
ve _1
/ _2
placeholder sözdizimi ile geliştirir .
- C ++ 11 ve ötesi de vardır
std::find_if_not
, oysa C ++ 98'in bir fonksiyon nesnesi etrafında ihtiyacı std::find_if
vardır std::not1
.
C ++ Stili
Henüz genel olarak kabul edilebilir bir C ++ 14 stili yoktur. Daha iyisi ya da daha kötüsü, Scott Meyers'in tasarı Etkili Modern C ++ ve Herb Sutter'in yenilenmiş GotW'sini yakından takip ediyorum . Aşağıdaki stil önerilerini kullanıyorum:
- Herb Sutter'in "Neredeyse Her Zaman Otomatik" ve Scott Meyers'ın " açıklığı bazen tartışılsa da, kısalık eşsizdir .
- Scott Meyers'ın " Nesneleri ayırın
()
ve {}
oluştururken" ve {}
iyi eski parantez başlangıcı yerine sürekli olarak hazır başlatmayı seçin ()
(genel koddaki tüm en sinir bozucu ayrıştırma sorunlarını yan adımlamak için).
- Scott Meyers'ın "typedefs için takma ad bildirimlerini tercih et" . Şablonlar için bu bir zorunluluktur ve bunu her yerde kullanmak
typedef
zaman kazandırır ve tutarlılık kazandırır.
for (auto it = first; it != last; ++it)
Zaten sıralanmış alt aralıklar için döngü değişmez kontrol izin vermek için bazı yerlerde bir desen kullanın . Üretim kodunda, döngü içinde ve içinde while (first != last)
bir ++first
yerlerde kullanım biraz daha iyi olabilir.
Seçim sıralaması
Seçim sıralaması verilere hiçbir şekilde uyum sağlamaz, bu nedenle çalışma zamanı her zaman olurO(N²)
. Ancak, seçim sıralaması takas sayısını en aza indirme özelliğine sahiptir. Öğeleri değiştirme maliyetinin yüksek olduğu uygulamalarda, seçim sıralaması çok iyi bir seçim algoritması olabilir.
Standart Kitaplığı kullanarak uygulamak için std::min_element
, kalan minimum öğeyi bulmak ve iter_swap
yerine takmak için tekrar tekrar kullanın :
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
Önceden işlenmiş aralığın [first, it)
döngü değişmezi olarak sıralandığını unutmayın . Minimum gereksinimler, rasgele erişim yineleyicilerine kıyasla ileristd::sort
yineleyicilerdir.
Ayrıntılar atlandı :
- seçim sıralaması erken bir testle
if (std::distance(first, last) <= 1) return;
(veya ileri / çift yönlü yineleyiciler için) optimize edilebilir if (first == last || std::next(first) == last) return;
.
- için çift yönlü tekrarlayıcılara , yukarıdaki test aralığı boyunca bir döngü ile kombine edilebilir
[first, std::prev(last))
son eleman en az kalan eleman olması sağlanır ve bir takas gerektirmediğinden,.
Ekleme sıralaması
En O(N²)
kötü zamana sahip temel sıralama algoritmalarından biri olmasına rağmen , ekleme sıralaması , veriler neredeyse sıralandığında ( uyarlanabilir olduğu için ) veya sorun boyutu küçük olduğunda (düşük ek yükü olduğu için) tercih edilen algoritmadır . Bu nedenlerden dolayı ve aynı zamanda kararlı olduğu için, birleştirme sıralaması veya hızlı sıralama gibi daha yüksek havai bölme ve fethetme sıralama algoritmaları için ekleme sıralaması genellikle özyinelemeli temel durum (sorun boyutu küçük olduğunda) olarak kullanılır.
insertion_sort
Standart Kitaplık ile uygulamak std::upper_bound
için, geçerli öğenin gitmesi gereken konumu bulmak için art arda kullanın std::rotate
ve kalan öğeleri giriş aralığında yukarı kaydırmak için kullanın :
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
Önceden işlenmiş aralığın [first, it)
döngü değişmezi olarak sıralandığını unutmayın . Ekleme sıralaması, ileri yineleyicilerle de çalışır.
Ayrıntılar atlandı :
- yerleştirme sıralaması erken bir test
if (std::distance(first, last) <= 1) return;
(veya ileri / çift yönlü yineleyiciler için if (first == last || std::next(first) == last) return;
) ve aralık üzerinde bir döngü ile optimize edilebilir [std::next(first), last)
, çünkü ilk elemanın yerinde olması garanti edilir ve bir döndürme gerektirmez.
- için çift yönlü iterators , ekleme noktasını bulmak için ikili arama bir ile değiştirilebilir ters lineer arama Standart Kütüphanesi kullanarak
std::find_if_not
algoritması.
Aşağıdaki parça için dört Canlı Örnek ( C ++ 14 , C ++ 11 , C ++ 98 ve 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();
- Rastgele girişler için bu
O(N²)
karşılaştırmalar sağlar, ancak bu O(N)
neredeyse sıralanmış girdiler için karşılaştırmaları geliştirir . İkili arama her zaman O(N log N)
karşılaştırmalar kullanır .
- Küçük giriş aralıkları için, doğrusal aramanın daha iyi bellek konumu (önbellek, önceden getirme) de ikili aramaya hâkim olabilir (elbette bunu test etmeliyiz).
Hızlı sıralama
Dikkatli bir şekilde uygulandığında, hızlı sıralama sağlamdır ve O(N log N)
karmaşıklığı beklenir, ancak O(N²)
tersine seçilmiş giriş verileriyle tetiklenebilecek en kötü durum karmaşıklığına sahiptir. Kararlı bir sıralama gerekli olmadığında, hızlı sıralama mükemmel bir genel amaçlı sıralamadır.
En basit sürümler için bile, hızlı sıralama Standart Kütüphane'yi kullanarak diğer klasik sıralama algoritmalarından daha karmaşıktır. Kullanımları birkaç yineleyici programları aşağıdaki yaklaşım bulmak için orta eleman giriş aralığının [first, last)
ardından, eksen olarak iki çağrı kullanımı std::partition
(vardır O(N)
daha küçük olan elemanların parça halinde üç yönlü bölümü) giriş aralığı, e eşit, ve sırasıyla seçilen pivottan daha büyüktür. Son olarak, pivottan daha küçük ve daha büyük elemanlara sahip iki dış segment tekrar tekrar sıralanır:
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));
}
Bununla birlikte, yukarıdaki adımların her birinin üretim seviyesi kodu için dikkatlice kontrol edilmesi ve optimize edilmesi gerektiğinden, hızlı sıralama doğru ve verimli olmak için oldukça zordur. Özellikle, O(N log N)
karmaşıklık için, O(1)
pivot, genel olarak bir pivot için garanti edilemeyen, ancak pivotu O(N)
giriş aralığının medyanı olarak ayarladığında garanti edilebilecek olan, giriş verilerinin dengeli bir bölümüyle sonuçlanmalıdır .
Ayrıntılar atlandı :
- yukarıdaki uygulama özellikle özel girdilere karşı savunmasızdır, örneğin
O(N^2)
" organ borusu " girişi için karmaşıklığa sahiptir 1, 2, 3, ..., N/2, ... 3, 2, 1
(çünkü orta kısım her zaman diğer tüm elemanlardan daha büyüktür).
- Giriş aralığı korumalarından rastgele seçilen elemanlardan medyan-3- pivot seçimi, karmaşıklığın aksi takdirde bozulacağı neredeyse sıralı girişlere karşı koruma sağlar
O(N^2)
.
- İki çağrıda gösterildiği gibi 3-yollu bölümleme (pivottan daha küçük, ona eşit ve daha büyük elemanları ayırmak)bu sonucu elde etmek
std::partition
için en etkiliO(N)
algoritmadeğildir.
- için rastgele erişim yineleyicileri , garantili bir
O(N log N)
karmaşıklık ile elde edilebilir ortalama dönme seçimi ile std::nth_element(first, middle, last)
yinelemeli çağrıları, ardından quick_sort(first, middle, cmp)
ve quick_sort(middle, last, cmp)
.
- Bununla birlikte, bu garantinin bir maliyeti vardır, çünkü
O(N)
karmaşıklığının sabit faktörü, medyan-3 pivotun karmaşıklığından std::nth_element
daha pahalı olabilir, O(1)
ardından bir O(N)
çağrı std::partition
(önbellek dostu tek ileri geçiş) veri).
Sıralamayı birleştir
O(N)
Ek alan kullanmak endişe etmiyorsa, birleştirme sıralaması mükemmel bir seçimdir: tek kararlı O(N log N)
sıralama algoritmasıdır.
Standart algoritmalar kullanarak uygulamak kolaydır: giriş aralığının ortasını bulmak için birkaç yineleyici yardımcı programını kullanın [first, last)
ve yinelenen şekilde sıralanan iki segmenti aşağıdakilerle birleştirin 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));
}
Birleştirme sıralama çift yönlü yineleyiciler gerektirir, darboğaz std::inplace_merge
. Bağlantılı listeleri sıralarken, birleştirme sıralamasının yalnızca O(log N)
fazladan alan gerektirdiğini (yineleme için) unutmayın. İkinci algoritma std::list<T>::sort
Standart Kütüphane'de tarafından uygulanır .
Öbek sıralaması
Öbek türünün uygulanması kolaydır,O(N log N)
yerinde bir tür gerçekleştirir, ancak kararlı değildir.
İlk döngü olan O(N)
"heapify" aşaması diziyi yığın sırasına sokar. İkinci döngü olan O(N log N
"sortdown" aşaması, tekrar tekrar maksimumu çıkarır ve yığın sırasını geri yükler. Standart Kütüphane bunu son derece basitleştirmektedir:
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));
}
Kullanmanın "aldattığını" düşünürseniz std::make_heap
ve std::sort_heap
bir seviye daha derine inebilir ve bu işlevleri sırasıyla std::push_heap
ve açısından kendiniz yazabilirsiniz 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
Standart Kütüphane hem belirtir push_heap
ve pop_heap
karmaşıklık olarak O(log N)
. Not ancak aralığı üzerinde, dış döngü bu [first, last)
sonuçların O(N log N)
karmaşıklık make_heap
, oysa std::make_heap
işlem sadece O(N)
karmaşıklığı. Çünkü genel O(N log N)
karmaşıklığı heap_sort
önemli değil.
Detaylar atlanmıştır : O(N)
uygulama içindemake_heap
Test yapmak
Beş algoritmanın çeşitli girişler üzerinde test edilmesini sağlayan dört Canlı Örnek ( C ++ 14 , C ++ 11 , C ++ 98 ve Boost , C ++ 98 ) (ayrıntılı veya titiz olması amaçlanmamıştır). Sadece LOC'deki büyük farklılıklara dikkat edin: C ++ 11 / C ++ 14'ün 130 LOC, C ++ 98 ve Boost 190 (+% 50) ve C ++ 98'in 270'ten (+% 100) fazla olması gerekiyor.