Wprowadzenie
Biblioteka standardowa C++ (potocznie, aczkolwiek niepoprawnie zwana STLem) to nie tylko kontenery, ale także algorytmy.
Algorytmy z biblioteki standardowej są tym co warto znać, będąc więcej niż początkującym programistą C++. Standardy kodowania wielu firm programistycznych zalecają używanie algorytmów zamiast "gołych pętli". Ponadto np. certyfikaty znajomości programowania, m.in.
C++ Certified Professional Programmer – CPP (udzielany przez
C++ Institute), kładą bardzo duży nacisk na biegłe władanie tymi algorytmami.
W dobie AI do kodowania nadal znajomość algorytmów jest pożądana, gdyż z moich obserwacji AI samo z siebie ich nie stosuje, raczej trzeba mu napisać, że znoobił i powinien użyć algorytmów, wtedy przyzna nam racje i zrobi co sugerujemy, ale wpierw musimy znać algorytmy.
Dlaczego algorytmy lepsze od pętli
Jak to powiedział Św. Jan Paweł II: "Człowiek nie jest w stanie pracować, gdy nie widzi sensu pracy, gdy sens ten przestaje być dla niego przejrzysty, gdy zostaje mu niejako przesłonięty", tak więc poniżej zestawiam argumenty za stosowaniem algorytmów:
Skąd nazwa STL i czemu jest niepoprawna
C++ na początku nie posiadał kontenerów oraz algorytmów, dopiero
Alexander Stepanov, pracujący w
HP Labs, ok. 1992 roku dokonał ich implementacji i to się nazywało
STL (Standard Template Library), następnie Stepanov zaprezentował bibliotekę komitetowi
ANSI/
ISO C++, a jego prace w większości weszły do standardu C++98, od tej pory jednak w każdym kolejnym standardzie biblioteka standardowa jest poszerzana. Poprawnym określeniem jest obecnie "
biblioteka standardowa" (bardziej szczegółowa argumentacja w książce "
Wysoce wydajny C++").
Jak się uczyć algorytmów
Istnieje strona referencyjna zawierająca
wykaz algorytmów dostępnych w bibliotece standardowej C++, ale ta lista mimo iż jest idealnym miejscem do sprawdzenia szczegółów, to jednak jest ona ciężka w nauce dla nieznających tych algorytmów. Zwłaszcza, że C++ się zmienia i pojawiają się nie tylko implementacje kolejnych algorytmów, ale też mechanizmy wpływające na prawie wszystkie algorytmy m.in. "
Execution policies".
Niegdyś wspaniałym źródłem wiedzy o algorytmach jest książka
STL w praktyce Scotta Meyersa - jednakże problemem jest to, że książka ta jest sprzed
C++11, przez co wiele informacji się zdezaktualizowało (nawet mimo iż C++ trzyma olbrzymią kompatybilność wsteczną to jednak pewne mechanizmy wyleciały).
Rozpoczynamy naukę algorytmów
Poniżej moje przemyślenia jak się ich uczyć, oraz zestawienia tabelaryczne wraz z tym co ważne. Poniższy opis nie zastąpi szczegółowej dokumentacji na stronie
CppReference, ale pozwoli ogarniać algorytmy, oraz być referencją do szybkiego sprawdzenia czy przypominania.
Warunki wstępne
Algorytmy są poniekąd zwieńczeniem biblioteki standardowej C++, wymagane jest płynne posługiwanie się C++, szablonami, wyrażeniami lambda, jak również kontenerami. Jeśli nie spełniasz tych kryteriów uzupełnij przed przejściem dalej.
Podział algorytmów
Algorytmy (w bazowej formie) dzielą się na kilka grup:
Powyższe można jeszcze podzielić bardziej szczegółowo np. (są to grupy wg uznania, nie standardu, można więc podzielić inaczej):
Sposoby korzystania z algorytmów w różnych standardach C++
Zasadniczo skupię się w tym artykule na najbardziej "tradycyjnej" formie korzystania z algorytmów, czyli przez podawanie zakresów, jest to przykładowo forma:
vector v = { 1, 2, 3, 4, 5 };
auto print =[ ]( auto e ) { cout << e << endl; };
std::for_each( v.begin(), v.end(), print );
Dlaczego zakres
Algorytmy są na tyle ogólne, że są w stanie współpracować nie tylko z kontenerami, ale też z tablicami, oraz też zakresami (ang.
ranges , którym poświęciłem
oddzielny artykuł). Stąd będę używał ogólnego określenia w tym artykule "
zakres" - bo określa to wszystko z czym algorytmy współpracują, co prawda najczęściej będę pod spodem używał kontenerów do demonstracji algorytmów.
Od
C++20 istnieje wersja bazująca na zakresach (ang. ranges), w której zamiast pary iteratorów podaje się dane (np. kontener lub tablicę o statycznym rozmiarze), a reszta jest niemalże analogiczna. Sugeruję jednak zrozumieć podstawową wersję na początku. Więcej o zakresach w innym
artykule mojego autorstwa. Poza tym algorytmy oparte na zakresach często są wolniejsze, oraz mają pewne wady (opisane w
wystąpieniu Nico Josuttis). Przykład użycia wersji opartej na zakresach:
std::ranges::for_each( v, print );
Jest to jednakże mechanizm znacznie bardziej zaawansowany niż tylko inny zestaw argumentów. Dodam też, że zestaw algorytmów z
std::ranges
ma algorytmy, których nie ma w
std::
np.
std::ranges::contains
,
std::ranges::contains_subrange
,
std::ranges::starts_with
,
std::ranges::ends_with
.
W
C++17 wprowadzili do algorytmów możliwość sprecyzowania tzw.
execution policy, czyli jak algorytmy mają być wykonywane (teoretycznie można wskazać wielowątkowe wykonanie, czy wykonanie na GPU), te algorytmy mają jako pierwszy argument obiekt polityki wykonania (execution policy) z nagłówka
< execution >
:
Przykład użycia:
std::for_each( std::execution::par, v.begin(), v.end(), print );
Zanim się zachwycisz: pułapki execution policy
Krótki kubeł zimnej wody dla każdego, kto sądzi, że wstawienie
std::execution::par
przy dowolnym algorytmie automatycznie da przyspieszenie:
Kiedy więc używać
par
/
par_unseq
? Tylko gdy: (1) dane są duże (setki tysięcy elementów i więcej), (2) operacja jest ciężka obliczeniowo (nie prosty sort), (3) sprawdziłeś benchmarkiem, że naprawdę jest szybciej, (4) callable jest thread-safe. Bez pomiaru — nie warto.
Jaki kawałek algorytmów będzie przedstawiony
Wszystkich algorytmów jest około stu, każdy z nich ma wiele wersji, które się zmieniały między standardami C++ (dochodziły nowsze wersje, lub się robiły bardziej
constexpr
), dlatego postaram się przedstawić najważniejsze wersje, z uwzględnieniem wymagań, pewnych pułapek, często złożoności obliczeniowej, oraz też standardu C++, jednakże będą to skrótowe informacje.
Algorytmy niemodyfikujące Cheetsheet
Poniżej lista wszystkich algorytmów określonych jako niemodyfikujące. Co prawda, jak ktoś chce to może przy ich pomocy dokonać pewnych zmian (np. jeśli ww.
std::for_each
jako funkcje dostanie coś modyfikującego elementy, a elementy nie będą
const
).
Poniżej są wymienione algorytmy, niektóre z nich nie mają podanych wymagań, jednakże wymagania "oczywiste" są:
Najpierw szybki indeks do przypomnienia — pełny opis ze składnią, złożonością i uwagami jest pod tabelką (parametry skrócone:
f/
l =
first/
last,
f1/
l1/
f2/
l2 =
first1/
last1/
first2/
last2 funkcje/predykaty wg liczby argumentów: g1 = funkcja jednoargumentowa, g2 = funkcja wieloargumentowa, p1 = predykat jednoargumentowy, p2 = predykat dwuargumentowy — generator bezargumentowy (gen) nie występuje w tej sekcji, pojawi się przy algorytmach modyfikujących). Dopisek ", c2?" oznacza opcjonalny parametr porównujący dwa elementy — czyli że istnieją dwie wersje: jedna z domyślnym
operator ==
, a druga z możliwością wstrzyknięcia własnego "porównywacza". Typ zwracany podany jest po dwukropku;
it
oznacza, że zwraca iterator.
Poniższa tabela zawiera pewne informacje w sposób bardziej skrótowy, a tym samym niezgodnym z sygnaturą czy typami w C++
Podane
O(N) to zawsze przypadek pesymistyczny — samo duże O z definicji jest górnym ograniczeniem, więc już oznacza "co najwyżej rzędu N", nigdy "zawsze dokładnie N" (to dopiero byłoby
Θ(N)). W algorytmach wyszukujących (
all_of
/
any_of
/
none_of
,
find
*,
search
*,
adjacent_find
,
mismatch
,
equal
) realny czas pracy bywa krótszy, bo kończą się wcześniej, gdy wynik jest już przesądzony — fachowo nazywa się to
short-circuit (wcześniejsze zakończenie), NIE "leniwe wartościowanie" (lazy evaluation to inny mechanizm — odraczanie obliczenia wartości do momentu, gdy jest naprawdę potrzebna, a nie przerywanie pętli). Najlepszy przypadek to wtedy
Ω(1). Wyjątek:
for_each
,
for_each_n
,
count
,
count_if
zawsze odwiedzają każdy element z osobna — tu
O(N) jest ścisłe, czyli
Θ(N).
| algorytm |
standard |
opis |
for_each( f, l, g1 )->g1 |
C++98 |
Wywołaj g1 dla każdego elementu i zwróć g1 |
for_each_n( f, n, g1 )->it |
C++17 |
Wywołaj g1 na pierwszych n elementach |
all_of( f, l, p1 )->bool |
C++11 |
Czy p1 prawdziwy dla wszystkich elementów? |
any_of( f, l, p1 )->bool |
C++11 |
Czy p1 prawdziwy dla co najmniej jednego elementu? |
none_of( f, l, p1 )->bool |
C++11 |
Czy wszystkie elementy nie spełniają p1? |
find( f, l, value )->it |
C++98 |
Znajdź pozycje pierwszego elementu równego value albo l |
find_if( f, l, p1 )->it |
C++98 |
Znajdź pozycje pierwszego elementu spełniającego p1 albo l |
find_if_not( f, l, p1 )->it |
C++11 |
Znajdź pozycje pierwszego elementu niespełniającego p1 albo l |
find_end( f1, l1, f2, l2, c2 ? )->it |
C++98 |
Znajdź pozycje ostatniego wystąpienie drugiego [f2,l2) zakresu w pierwszym [f1,l1) |
find_first_of( f1, l1, f2, l2, c2 ? )->it |
C++98 |
Znajdź pierwszą pozycje dowolnego elementu z zakresu [f2, l2] |
adjacent_find( f, l, c2 ? )->it |
C++98 |
Znajdź początek pierwszej pary równych sąsiednich elementów |
count( f, l, value )->ptrdiff |
C++98 |
Ile elementów równych value |
count_if( f, l, p1 )->ptrdiff |
C++98 |
Ile elementów spełnia p1 |
mismatch( f1, l1, f2, c2 ? )->pair < it1, it2 > mismatch( f1, l1, f2, l2, c2 ? )->pair < it1, it2 > |
C++98 / C++14 |
Pierwsza para różniących się elementów z dwóch zakresów |
equal( f1, l1, f2, c2 ? )->bool equal( f1, l1, f2, l2, c2 ? )->bool |
C++98 / C++14 |
Czy dwa zakresy są identyczne |
search( f1, l1, f2, l2, c2 ? )->it
search( f1, l1, f2, l2, searcher )->it |
C++98 / C++17 |
Znajdź pierwsze wystąpienie drugiego zakresu w pierwszym |
search_n( f, l, n, value, c2 ? )->it |
C++98 |
Znajdź pozycje pierwszych n kolejnych elementów równych value |
std::for_each (też w dokumentacji:
for_each)
Złożoność:
O(N) (ściśle:
Θ(N) —
f
jest wywoływane dokładnie N razy, bez wyjątku)
template < class InputIt, class UnaryFunc >
UnaryFunc for_each( InputIt first, InputIt last, UnaryFunc f );
Wykonuje funkcję
f
dla każdego elementu zakresu —
f
może być funktorem, lambdą, ale też zwykłą funkcją (bez wrappera typu
ptr_fun
). Z założenia nie modyfikuje elementów, ale jeśli podamy funktor modyfikujący, a zakres nie będzie stały (nie const-iteratory), modyfikacja się uda. Zwraca podaną funkcję/funktor — dzięki temu można odzyskać stan funktora po przejściu całego zakresu.
std::for_each_n (C++17)
Złożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class Size, class UnaryFunc >
InputIt for_each_n( InputIt first, Size n, UnaryFunc f );
Stosuje
f
do pierwszych
n
elementów od
first
(
n
ujemne to UB). W odróżnieniu od
for_each
zwraca iterator
first + n
, nie obiekt
f
— nie da się więc odzyskać stanu funktora. Przydatne, gdy nie mamy pod ręką
last
albo liczy się tylko określona liczba początkowych elementów.
std::all_of /
std::any_of /
std::none_of (C++11)
Złożoność: O(N) w najgorszym przypadku (Θ(N) — patrz uwaga o short-circuit wyżej), Θ(1) w najlepszym
template < class InputIt, class UnaryPred >
bool all_of( InputIt first, InputIt last, UnaryPred predicate );
bool any_of( InputIt first, InputIt last, UnaryPred predicate );
bool none_of( InputIt first, InputIt last, UnaryPred predicate );
Sprawdzają, czy predykat jest prawdziwy: dla wszystkich / dla przynajmniej jednego / dla żadnego elementu.
Zachowanie na PUSTYM zakresie to nie przypadek ani UB, tylko logika pustego kwantyfikatora (
vacuous truth):
all_of
i
none_of
zwracają
true
,
any_of
zwraca
false
.
std::find /
std::find_if /
std::find_if_not (też w dokumentacji:
find /
find_if)
Złożoność: O(N) w najgorszym przypadku (Θ(N)), Θ(1) w najlepszym (patrz uwaga o short-circuit wyżej)
InputIt find( InputIt first, InputIt last, const T & value );
InputIt find_if( InputIt first, InputIt last, UnaryPred predicate ); InputIt find_if_not( InputIt first, InputIt last, UnaryPred predicate );
find
zwraca pierwszy element równy
value
;
find_if
pierwszy element, dla którego predykat zwraca
true
;
find_if_not
— dla którego zwraca
false
. Każdy z nich zwraca
last
, gdy nic nie pasuje.
std::find_end (też w dokumentacji:
find_end)
Złożoność:
O(S·(N−S+1)) w najgorszym przypadku (to gwarancja standardu dla wersji bez searchera, nie dowiedziona optymalna granica —
algorytmy typu KMP potrafią to zrobić w
O(N+S), więc Θ tu nie przypisuję)
template < class ForwardIt1, class ForwardIt2 >
ForwardIt1 find_end( ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2 );
template < class ForwardIt1, class ForwardIt2, class BinaryPred >
ForwardIt1 find_end( ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2, BinaryPred pred );
Wyszukuje ostatnie wystąpienie podzakresu wyznaczonego przez
first2, last2
w zakresie źródłowym
first1, last1
. Zwraca iterator na początek znalezionej podsekwencji, lub
last1
, gdy nie znaleziono.
To nie jest kwestia "kierunku szukania", tylko tego, KTÓRE dopasowanie ma zostać zwrócone — zawsze ostatnie. Standard wymaga jedynie
ForwardIterator
(implementacja może więc iść w przód, pamiętając ostatnie trafienie); złożoność to co najwyżej S·(N−S+1) porównań, gdzie S i N to długości obu zakresów. Implementacja MOŻE (nie musi) przeszukiwać od końca do początku, jeśli iteratory są co najmniej
BidirectionalIterator
— bywa to szybsze w praktyce, ale nie zmienia złożoności pesymistycznej.
std::find_first_ofZłożoność:
O(N1·N2) w najgorszym przypadku (ściśle:
Θ(N1·N2)), gdzie N1 = długość
first1, last1
, N2 = długość
first2, last2
template < class InputIt, class ForwardIt >
InputIt find_first_of( InputIt first1, InputIt last1, ForwardIt first2, ForwardIt last2 );
template < class InputIt, class ForwardIt, class BinaryPred >
InputIt find_first_of( InputIt first1, InputIt last1, ForwardIt first2, ForwardIt last2, BinaryPred pred );
Zwraca pierwszy element zakresu
first1, last1
, który jest równy któremukolwiek z elementów zakresu
first2, last2
(domyślnie wg
operator ==
).
std::adjacent_findZłożoność: O(N) w najgorszym przypadku (Θ(N)), Θ(1) w najlepszym
template < class ForwardIt >
ForwardIt adjacent_find( ForwardIt first, ForwardIt last );
template < class ForwardIt, class BinaryPred >
ForwardIt adjacent_find( ForwardIt first, ForwardIt last, BinaryPred pred );
Przeszukuje zakres do momentu znalezienia pierwszej pary sąsiadujących, równych sobie elementów. Zwraca iterator na pierwszy z tej pary (lub
last
, gdy nie znaleziono).
std::count /
std::count_if (też w dokumentacji:
count /
count_if)
Złożoność: O(N) (ściśle: Θ(N) — zawsze dokładnie N porównań/wywołań, patrz uwaga wyżej)
template < class InputIt, class T >
typename std::iterator_traits < InputIt >::difference_type
count( InputIt first, InputIt last, const T & value );
template < class InputIt, class UnaryPred >
typename std::iterator_traits < InputIt >::difference_type
count_if( InputIt first, InputIt last, UnaryPred predicate );
Zliczają liczbę elementów równych
value
/ spełniających predykat. Zawsze dokładnie N porównań/wywołań predykatu — w odróżnieniu od
find *
nie przerywają się wcześniej, bo muszą przejść cały zakres. Zwracają
iterator_traits < InputIt >::difference_type
(zwykle
ptrdiff_t
), nie
int
— dla naprawdę wielkich zakresów to istotne.
std::mismatchZłożoność: O(N) w najgorszym przypadku (Θ(N)), Θ(1) w najlepszym
template < class InputIt1, class InputIt2 >
std::pair < InputIt1, InputIt2 > mismatch( InputIt1 first1, InputIt1 last1, InputIt2 first2 );
template < class InputIt1, class InputIt2, class BinaryPred >
std::pair < InputIt1, InputIt2 > mismatch( InputIt1 first1, InputIt1 last1, InputIt2 first2, BinaryPred p );
template < class InputIt1, class InputIt2 >
std::pair < InputIt1, InputIt2 > mismatch( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2 );
template < class InputIt1, class InputIt2, class BinaryPred >
std::pair < InputIt1, InputIt2 > mismatch( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPred p );
Zwraca pozycje z obu zakresów, w których elementy pierwszy raz się różnią.
Co dokładnie zwraca, gdy nie znaleziono rozbieżności? W wersji klasycznej (bez
last2
) — jeśli WSZYSTKO się zgadza na całej długości
first1 .. last1
, para to
( last1, first2 + distance( first1, last1 ) )
. W wersji bezpiecznej (z
last2
, od C++14) porównywanie zatrzymuje się na krótszym z obu zakresów — jeśli ten krótszy się wyczerpie bez rozbieżności, zwrócona para wskazuje jego koniec i odpowiadającą pozycję w drugim zakresie (różna długość obu zakresów sama w sobie liczy się więc jako "mismatch" w tym sensie, że iteracja się zatrzymuje, mimo że porównane fragmenty były identyczne).
std::equal (też w dokumentacji:
equal)
Złożoność: O(N) w najgorszym przypadku (Θ(N)), Θ(1) w najlepszym
template < class InputIt1, class InputIt2 >
bool equal( InputIt1 first1, InputIt1 last1, InputIt2 first2 );
template < class InputIt1, class InputIt2, class BinaryPred >
bool equal( InputIt1 first1, InputIt1 last1, InputIt2 first2, BinaryPred p );
template < class InputIt1, class InputIt2 >
bool equal( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2 );
template < class InputIt1, class InputIt2, class BinaryPred >
bool equal( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPred p );
Sprawdza, czy dwa zakresy są sobie równe element po elemencie.
Wersja klasyczna (bez
last2
) zakłada, że drugi zakres ma co najmniej tyle elementów co
first1 .. last1
— jeśli nie, to UB, stąd bezpieczniejsza wersja z C++14. To nie to samo, co
ranges::equal
ani "po prostu"
operator ==
na kontenerach — choć np.
std::vector
ma własny
operator ==
, który pod spodem korzysta z tej samej idei.
std::search (też w dokumentacji:
search)
Złożoność: O(N·S) w najgorszym przypadku (to gwarancja standardu dla wersji bez searchera, nie dowiedziona optymalna granica — stąd searchery niżej: KMP/Boyer-Moore potrafią O(N+S)), gdzie S = długość szukanego podciągu, N = długość zakresu źródłowego
template < class ForwardIt1, class ForwardIt2 >
ForwardIt1 search( ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2 );
template < class ForwardIt1, class ForwardIt2, class BinaryPred >
ForwardIt1 search( ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2, BinaryPred pred );
Wyszukuje podciąg wyznaczony przez
first2, last2
w zakresie źródłowym
first1, last1
. Zwraca pozycję jego początku, lub
last1
, gdy nie znaleziono.
Od
C++17 search
ma też przeciążenie przyjmujące obiekt
Searcher
(nagłówek
< functional >
) zamiast pary iteratorów:
std::default_searcher
(opakowuje klasyczny
search
),
std::boyer_moore_searcher
i
std::boyer_moore_horspool_searcher
— implementują klasyczne algorytmy wyszukiwania wzorca w tekście i przy długich haystackach/needlach potrafią być zauważalnie szybsze, kosztem wymogu
RandomAccessIterator
i przygotowania wewnętrznych tablic pomocniczych:
auto it = std::search( text.begin(), text.end(), std::boyer_moore_searcher( pattern.begin(), pattern.end() ) );
std::search_nZłożoność: O(N) (ściśle: Θ(N) — patrz dymek niżej, dlaczego NIE O(N·count))
template < class ForwardIt, class Size, class T >
ForwardIt search_n( ForwardIt first, ForwardIt last, Size count, const T & value );
template < class ForwardIt, class Size, class T, class BinaryPred >
ForwardIt search_n( ForwardIt first, ForwardIt last, Size count, const T & value, BinaryPred pred );
Szuka pierwszego wystąpienia
count
kolejnych (sąsiadujących) elementów równych
value
. Zwraca iterator na początek znalezionej podsekwencji, lub
last
, gdy nie znaleziono; dla
count <= 0
zwraca
first
. Przydatne np. do szukania "trzech kolejnych zer" w sekwencji.
Złożoność to O(N), NIE O(N·count) — mimo że naiwna implementacja (dla każdej pozycji sprawdź kolejne count elementów) sugerowałaby to drugie. Standard gwarantuje co najwyżej N porównań, bo dobra implementacja nie zaczyna sprawdzania od nowa po nieudanym dopasowaniu, tylko przeskakuje od razu za miejsce, gdzie seria się urwała.
Algorytmy modyfikujące
Omówione wyżej algorytmy niemodyfikujące odpowiadały na pytania „czy", „ile" i „gdzie" — żaden z nich nie zmienia zawartości zakresu (a przynajmniej nie taki jest jego zamysł, o czym była mowa przy
for_each
). Poniżej zestaw algorytmów, które albo modyfikują zakres źródłowy w miejscu, albo tworzą zmodyfikowaną kopię pod innym adresem. Grupuję je tak, jak w zestawieniu na początku artykułu.
Podział danych (partitioning)
"Partycjonowanie" to przegrupowanie zakresu tak, by elementy spełniające predykat znalazły się przed elementami go niespełniającymi (bez pełnego sortowania) — to dokładnie to, co robi krok "partition" algorytmu
quicksort, i to jest chyba najlepsza analogia do zapamiętania tej grupy.
std::partition /
std::stable_partitionZłożoność: O(N) (ściśle: Θ(N) — standard wymaga dokładnie N zastosowań predykatu)
template < class ForwardIt, class UnaryPred >
ForwardIt partition( ForwardIt first, ForwardIt last, UnaryPred p );
template < class BidirIt, class UnaryPred >
BidirIt stable_partition( BidirIt first, BidirIt last, UnaryPred p );
Przenoszą elementy spełniające
p
na początek zakresu, resztę na koniec. Zwracają iterator na pierwszy element drugiej grupy (punkt podziału). Różnica:
partition
nie gwarantuje zachowania względnej kolejności elementów w obrębie każdej z grup (i wymaga tylko
ForwardIterator
),
stable_partition
tę kolejność zachowuje (kosztem wymogu
BidirectionalIterator
i potencjalnie gorszej złożoności, gdy brakuje dodatkowej pamięci).
Analogicznie do
remove
/
unique
—
partition
nie usuwa niczego z kontenera, tylko przestawia elementy. Jeśli zamiast pełnego podziału potrzebujesz tylko usunąć elementy niespełniające warunku, prościej użyć idiomu erase-remove z
remove_if
(patrz sekcja "Usuwanie"). Złożoność
stable_partition
: dokładnie N zastosowań predykatu przy wystarczającej dodatkowej pamięci, w przeciwnym razie
O(N log N) zamian (ale wciąż
O(N) zastosowań predykatu) — sama implementacja może np. rekurencyjnie dzielić zakres na pół.
std::partition_copy (C++11)
Złożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class OutputIt1, class OutputIt2, class UnaryPred >
std::pair < OutputIt1, OutputIt2 >
partition_copy( InputIt first, InputIt last,
OutputIt1 d_first_true, OutputIt2 d_first_false, UnaryPred p );
Nie modyfikuje źródła (wystarczy
InputIterator
) — kopiuje elementy spełniające
p
do
d_first_true
, resztę do
d_first_false
. Zwraca parę iteratorów końcowych obu miejsc docelowych. Zakresy docelowe nie mogą nachodzić na zakres źródłowy ani na siebie nawzajem.
std::is_partitioned /
std::partition_point (C++11)
Złożoność:
is_partitioned
:
O(N) w najgorszym przypadku (
Θ(N)),
Θ(1) w najlepszym /
partition_point
:
Θ(log N) — liczba kroków szukania binarnego zależy tylko od N, nie od danych
template < class InputIt, class UnaryPred >
bool is_partitioned( InputIt first, InputIt last, UnaryPred p );
template < class ForwardIt, class UnaryPred >
ForwardIt partition_point( ForwardIt first, ForwardIt last, UnaryPred p );
is_partitioned
sprawdza (liniowo, w jednym przejściu), czy w zakresie wszystkie elementy spełniające
p
poprzedzają te niespełniające.
partition_point
zakłada, że zakres JEST już tak podzielony (to założenie, nie coś co sprawdza — niespełnienie to UB) i szuka punktu podziału szukaniem binarnym, analogicznie do
lower_bound
poniżej — stąd
O(log N) porównań, choć samych "kroków" iteratora może być
O(N) przy iteratorach nie będących
RandomAccessIterator
.
Sortowanie
std::sort (też w dokumentacji:
sort)
Złożoność: O(N log N) w najgorszym przypadku — i ściśle Θ(N log N): to zarazem znane dolne ograniczenie (Ω(N log N)) dla DOWOLNEGO sortowania opartego na porównaniach, więc lepiej być nie może
template < class RandomIt > void sort( RandomIt first, RandomIt last );
template < class RandomIt, class Compare > void sort( RandomIt first, RandomIt last, Compare comp );
Wymaga
RandomAccessIterator
(stąd nie zadziała bezpośrednio na
std::list
— tam trzeba użyć metody składowej
list::sort()
). NIE jest stabilny — elementy "równe" wg komparatora mogą zmienić względną kolejność.
Komparator musi spełniać strict weak ordering — w szczególności
comp( a, a )
musi być fałszywe. Najczęstszy błąd zaczynających: komparator
<=
zamiast
<
dla sortowania rosnącego — to złamanie strict weak ordering i UB (w praktyce bywa segfault, pętla nieskończona, albo błędny wynik, w zależności od implementacji).
Przed C++11 standard gwarantował złożoności "w przybliżeniu N log N" tylko dla przeciętnego przypadku, dopuszczając implementacje czystego quicksortu (podatne na
O(N²) w pesymistycznym przypadku, np. dla już posortowanych danych ze złym wyborem pivota). Od C++11 wymagane jest
O(N log N) w najgorszym przypadku, co w praktyce wymusza
introsort (
quicksort z przełączeniem na
heapsort po przekroczeniu limitu głębokości rekursji) — tak działają np. implementacje GCC/libstdc++ i Clang/libc++.
std::stable_sortZłożoność:
O(N log N) przy wystarczającej dodatkowej pamięci (ściśle:
Θ(N log N), z tego samego powodu co
sort
wyżej) — bez dodatkowej pamięci
O(N log²N) (to gwarancja standardu, nie potwierdzona jako optymalna dolna granica, więc zostaje przy samym O)
template < class RandomIt > void stable_sort( RandomIt first, RandomIt last );
template < class RandomIt, class Compare > void stable_sort( RandomIt first, RandomIt last, Compare comp );
Jak
sort
, ale zachowuje względną kolejność elementów "równych" wg komparatora — kluczowe przy sortowaniu wielokluczowym (np. najpierw sortujesz po nazwisku, potem stabilnie po dziale — w efekcie w obrębie działu kolejność alfabetyczna nazwisk przetrwa). Implementacja typowo korzysta z merge sort, stąd potrzeba dodatkowej pamięci dla gwarancji
O(N log N); bez niej degraduje do
O(N log²N).
std::partial_sort /
std::partial_sort_copyZłożoność: O(N log M), M = liczba sortowanych elementów
template < class RandomIt > void partial_sort( RandomIt first, RandomIt middle, RandomIt last );
template < class RandomIt, class Compare > void partial_sort( RandomIt first, RandomIt middle, RandomIt last, Compare comp );
template < class InputIt, class RandomIt >
RandomIt partial_sort_copy( InputIt first, InputIt last, RandomIt d_first, RandomIt d_last );
template < class InputIt, class RandomIt, class Compare >
RandomIt partial_sort_copy( InputIt first, InputIt last, RandomIt d_first, RandomIt d_last, Compare comp );
partial_sort
sortuje tylko fragment od
first
do
middle
(wyłącznie), tak by zawierał M najmniejszych elementów całego zakresu we właściwej kolejności — reszta (od
middle
do
last
) pozostaje w nieokreślonej kolejności. Klasyczne zastosowanie: "top-K najlepszych wyników" bez sortowania całości.
partial_sort_copy
robi to samo, ale nie modyfikuje źródła (wystarczy
InputIterator
) i pisze do osobnego zakresu docelowego — liczba faktycznie posortowanych elementów to
min( last - first, d_last - d_first )
, zwracany iterator wskazuje na koniec zapisanego fragmentu.
std::nth_elementZłożoność: O(N) przeciętnie (average case — to jest kluczowe zastrzeżenie: standard NIE gwarantuje O(N) w najgorszym przypadku, więc Θ(N) tu nie przypisuję; przy złośliwie dobranych danych i naiwnym wyborze piwota implementacja mogłaby teoretycznie działać wolniej)
template < class RandomIt > void nth_element( RandomIt first, RandomIt nth, RandomIt last );
template < class RandomIt, class Compare > void nth_element( RandomIt first, RandomIt nth, RandomIt last, Compare comp );
Po wywołaniu element na pozycji
nth
to dokładnie ten element, który znalazłby się tam po pełnym posortowaniu — wszystkie elementy przed nim są
<= * nth
, wszystkie po nim
>= * nth
(ale BEZ dalszego uporządkowania w obrębie tych dwóch grup). To Quickselect — średnio liniowe, znacznie szybsze niż pełne sortowanie, gdy potrzebujesz np. tylko mediany albo k-tego percentyla.
std::is_sorted /
std::is_sorted_until (C++11)
Złożoność: O(N) w najgorszym przypadku (Θ(N) — pełne posortowanie wymaga sprawdzenia wszystkich par), Θ(1) w najlepszym (naruszenie zaraz na początku)
template < class ForwardIt > bool is_sorted( ForwardIt first, ForwardIt last );
template < class ForwardIt, class Compare > bool is_sorted( ForwardIt first, ForwardIt last, Compare comp );
template < class ForwardIt > ForwardIt is_sorted_until( ForwardIt first, ForwardIt last );
template < class ForwardIt, class Compare > ForwardIt is_sorted_until( ForwardIt first, ForwardIt last, Compare comp );
is_sorted
zwraca
bool
;
is_sorted_until
zwraca iterator na pierwszy element psujący porządek (lub
last
, gdy cały zakres jest posortowany) — przydatne przy diagnozowaniu "gdzie dokładnie" dane przestają być posortowane.
Przeszukiwania binarne
Cztery poniższe algorytmy wymagają, by zakres był już posortowany (a precyzyjniej:
partycjonowany względem szukanej wartości — w praktyce niemal zawsze oznacza to posortowany) — nieposortowany zakres to niezdefiniowane zachowanie, nie błąd czasu kompilacji ani wyjątek.
Należy upewnić się, że zakres jest posortowany tym samym sposobem co użyty algorytm wyszukiwania binarnego. Domyślnie jest używany
std::less
ale może być posortowane np. kolejnością malejącą, wtedy należy pamiętać aby przekazać właściwy algorytm.
std::lower_bound /
std::upper_boundZłożoność: O(log N) porównań (ściśle: Θ(log N) — liczba kroków szukania binarnego zależy tylko od N)
template < class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T & value );
template < class ForwardIt, class T, class Compare >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T & value, Compare comp );
template < class ForwardIt, class T >
ForwardIt upper_bound( ForwardIt first, ForwardIt last, const T & value );
template < class ForwardIt, class T, class Compare >
ForwardIt upper_bound( ForwardIt first, ForwardIt last, const T & value, Compare comp );
lower_bound
zwraca iterator na pierwszy element NIE mniejszy od
value
(czyli
>= value
);
upper_bound
— na pierwszy element ściśle większy od
value
. Różnica tych dwóch iteratorów to liczba elementów równych
value
(zero, gdy value nie występuje). To budulec do wstawiania z zachowaniem sortowania:
v.insert( std::lower_bound( v.begin(), v.end(), x ), x )
.
Złożoność
O(log N) porównań dotyczy samego algorytmu, ale liczba przesunięć iteratora zależy od jego kategorii: dla
RandomAccessIterator
(np.
vector
) całość jest rzeczywiście
O(log N). Dla zwykłego
ForwardIterator
(np. ręcznie pisana lista jednokierunkowa) przesunięcie iteratora do połowy zakresu samo w sobie kosztuje
O(N) — więc realny koszt całkowity to
O(N), mimo że porównań jest tylko
O(log N). To ta sama pułapka, co przy
std::list
:
std::list
nie ma
RandomAccessIterator
, więc
lower_bound
na nim traci sens praktyczny — lepiej użyć posortowanego kontenera asocjacyjnego.
std::equal_range /
std::binary_search (binary_search też w dokumentacji:
binary_search)
Złożoność:
O(log N) porównań (ściśle:
Θ(log N), z tego samego powodu co
lower_bound
/
upper_bound
)
template < class ForwardIt, class T >
std::pair < ForwardIt, ForwardIt > equal_range( ForwardIt first, ForwardIt last, const T & value );
template < class ForwardIt, class T, class Compare >
std::pair < ForwardIt, ForwardIt > equal_range( ForwardIt first, ForwardIt last, const T & value, Compare comp );
template < class ForwardIt, class T >
bool binary_search( ForwardIt first, ForwardIt last, const T & value );
template < class ForwardIt, class T, class Compare >
bool binary_search( ForwardIt first, ForwardIt last, const T & value, Compare comp );
equal_range
to po prostu para
{ lower_bound( ... ), upper_bound( ... ) }
(choć zwykle liczona wydajniej niż dwa osobne wywołania).
binary_search
zwraca tylko
bool
— jeśli potrzebujesz też pozycji, użyj
lower_bound
i porównaj wynik z
value
(tak jest wydajniej niż wołać oba).
Operacje na zbiorach
Wszystkie poniższe wymagają posortowanych zakresów wejściowych (tym samym komparatorem, którym potem sortowany jest — i będzie — wynik) i traktują je jak zbiory/multizbiory. Zakres wyjściowy nie może pokrywać się z żadnym z wejściowych.
Mimo nazwy sugerującej "zbiory", żaden z tych algorytmów nie wymaga
std::set
— działają na dowolnym posortowanym zakresie, także na
std::vector
. Odwrotnie: chcąc użyć ich na dwóch
std::set
, wystarczy podać ich iteratory —
std::set
jest z definicji posortowany.
std::includesZłożoność: O(N1+N2) w najgorszym przypadku (ściśle: Θ(N1+N2) — algorytm typu "merge", zawsze przegląda oba zakresy liniowo)
template < class InputIt1, class InputIt2 >
bool includes( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2 );
template < class InputIt1, class InputIt2, class Compare >
bool includes( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, Compare comp );
Sprawdza, czy każdy element zakresu 2 występuje w zakresie 1 z co najmniej taką samą krotnością (czyli zakres 2 jako multizbiór zawiera się w zakresie 1). Oba zakresy muszą być posortowane tym samym porządkiem.
std::set_union /
std::set_intersectionZłożoność: O(N1+N2) (ściśle: Θ(N1+N2) — jak wyżej, stały koszt niezależny od zawartości)
template < class InputIt1, class InputIt2, class OutputIt >
OutputIt set_union( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first );
template < class InputIt1, class InputIt2, class OutputIt, class Compare >
OutputIt set_union( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first, Compare comp );
template < class InputIt1, class InputIt2, class OutputIt >
OutputIt set_intersection( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first );
template < class InputIt1, class InputIt2, class OutputIt, class Compare >
OutputIt set_intersection( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first, Compare comp );
Dla elementu występującego
m
razy w zakresie 1 i
n
razy w zakresie 2:
set_union
zapisze
max( m, n )
kopii,
set_intersection
—
min( m, n )
kopii. Wynik jest posortowany.
std::set_difference /
std::set_symmetric_differenceZłożoność: O(N1+N2) (ściśle: Θ(N1+N2), jak wyżej)
template < class InputIt1, class InputIt2, class OutputIt >
OutputIt set_difference( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first );
template < class InputIt1, class InputIt2, class OutputIt, class Compare >
OutputIt set_difference( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first, Compare comp );
template < class InputIt1, class InputIt2, class OutputIt >
OutputIt set_symmetric_difference( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first );
template < class InputIt1, class InputIt2, class OutputIt, class Compare >
OutputIt set_symmetric_difference( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first, Compare comp );
set_difference
:
max( m - n, 0 )
kopii elementu (to, co zostaje z zakresu 1 po "odjęciu" zakresu 2) —
niesymetryczne, kolejność argumentów ma znaczenie.
set_symmetric_difference
:
| m - n |
kopii — elementy, które nie są częścią wspólną (to, co dałoby XOR na zbiorach).
Operacje na kopcu (stercie, ang. heap)
Kopiec binarny (nie mylić ze stosem/LIFO!) to struktura umożliwiająca
O(log N) wstawianie i
O(log N) pobranie największego (lub najmniejszego, wg komparatora) elementu — to właśnie na tych pięciu/sześciu algorytmach oparty jest
std::priority_queue. Reprezentacja to zwykły zakres o dostępie swobodnym (np.
vector
), interpretowany jako drzewo binarne wg konwencji "rodzic na indeksie i, dzieci na 2i+1 i 2i+2".
std::make_heapZłożoność: O(N) (ściśle: Θ(N) — algorytm Floyda ma z góry ustaloną liczbę kroków zależną tylko od N)
template < class RandomIt > void make_heap( RandomIt first, RandomIt last );
template < class RandomIt, class Compare > void make_heap( RandomIt first, RandomIt last, Compare comp );
Przekształca dowolny zakres w kopiec max (lub min, z odpowiednim
comp
) w miejscu. Mimo że budowanie kopca "od zera" wygląda na
O(N log N) (N wstawień razy
O(log N)), sprytny algorytm Floyda robi to w
O(N) — to klasyczna, trochę nieoczywista sztuczka algorytmiczna warta zapamiętania.
std::push_heap /
std::pop_heapZłożoność: O(log N) w najgorszym przypadku (Θ(log N) — element może przesiąknąć aż do korzenia/liścia), Θ(1) w najlepszym (element trafia na miejsce od razu)
template < class RandomIt > void push_heap( RandomIt first, RandomIt last );
template < class RandomIt, class Compare > void push_heap( RandomIt first, RandomIt last, Compare comp );
template < class RandomIt > void pop_heap( RandomIt first, RandomIt last );
template < class RandomIt, class Compare > void pop_heap( RandomIt first, RandomIt last, Compare comp );
Typowy wzorzec użycia z
vector
: żeby dodać element, najpierw
v.push_back( x )
(element trafia na koniec, jeszcze poza kopcem), potem
push_heap( v.begin(), v.end() )
(włącza go do kopca). Żeby zdjąć maksimum:
pop_heap( v.begin(), v.end() )
(przenosi maksimum na koniec zakresu i przywraca własność kopca dla reszty), potem dopiero
v.pop_back()
(fizycznie usuwa z kontenera). Same
push_heap
/
pop_heap
NIE zmieniają rozmiaru kontenera — to typowy błąd dla początkujących, którzy zapominają o drugim kroku (
push_back
/
pop_back
).
std::sort_heap /
std::is_heap /
std::is_heap_until (C++11)
Złożoność:
sort_heap
:
O(N log N) (ściśle:
Θ(N log N)) /
is_heap
,
is_heap_until
:
O(N) w najgorszym przypadku (
Θ(N)),
Θ(1) w najlepszym
template < class RandomIt > void sort_heap( RandomIt first, RandomIt last );
template < class RandomIt, class Compare > void sort_heap( RandomIt first, RandomIt last, Compare comp );
template < class RandomIt > bool is_heap( RandomIt first, RandomIt last );
template < class RandomIt, class Compare > bool is_heap( RandomIt first, RandomIt last, Compare comp );
template < class RandomIt > RandomIt is_heap_until( RandomIt first, RandomIt last );
template < class RandomIt, class Compare > RandomIt is_heap_until( RandomIt first, RandomIt last, Compare comp );
sort_heap
to w istocie heapsort — powtarza
pop_heap
aż do wyczerpania zakresu; wynikowy zakres jest posortowany, ale
przestaje być kopcem.
is_heap
/
is_heap_until
działają analogicznie do
is_sorted
/
is_sorted_until
.
Min / max
std::min /
std::max /
std::minmax (min/max też w dokumentacji:
min /
max)
Złożoność: O(1) dla dwóch wartości (ściśle: Θ(1))
template < class T > constexpr const T & min( const T & a, const T & b );
template < class T > constexpr const T & max( const T & a, const T & b );
template < class T > constexpr std::pair < const T &, const T & > minmax( const T & a, const T & b );
Działają na WARTOŚCIACH, nie na zakresach (stąd brak par iteratorów) — łatwo je pomylić z
min_element
/
max_element
poniżej.
minmax( a, b )
jest wydajniejszy niż osobne wywołanie
min
i
max
— potrzebuje najwyżej jednego porównania zamiast dwóch.
Przeciążenie z
std::initializer_list < T >
pozwala pisać
std::max( { a, b, c, d } )
zamiast zagnieżdżonych
std::max( std::max( a, b ), std::max( c, d ) )
. Zwracają
const T &
— referencję do jednego z argumentów, co przy tymczasowych obiektach bywa źródłem subtelnego dangling reference (np.
const auto & r = std::max( compute1(), compute2() );
jest bezpieczne tylko do końca pełnego wyrażenia — życiowo trwałe przedłużenie referencji do tymczasowego obiektu nie zadziała tu tak, jak można by się spodziewać, bo referencja zwrócona przez
max
nie jest tym samym obiektem, do którego przypisujesz).
std::min_element /
std::max_element /
std::minmax_element (min_element/max_element też w dokumentacji:
min_element /
max_element)
Złożoność:
min_element
/
max_element
:
O(N) (ściśle:
Θ(N), dokładnie N−1 porównań — trzeba zobaczyć każdy element, by być pewnym) /
minmax_element
: co najwyżej 3N/2 porównań (ściśle:
Θ(N) — to znany, dowiedziony optymalny wynik dla jednoczesnego szukania min i max)
template < class ForwardIt > ForwardIt min_element( ForwardIt first, ForwardIt last );
template < class ForwardIt, class Compare > ForwardIt min_element( ForwardIt first, ForwardIt last, Compare comp );
template < class ForwardIt > ForwardIt max_element( ForwardIt first, ForwardIt last );
template < class ForwardIt, class Compare > ForwardIt max_element( ForwardIt first, ForwardIt last, Compare comp );
template < class ForwardIt > std::pair < ForwardIt, ForwardIt > minmax_element( ForwardIt first, ForwardIt last );
template < class ForwardIt, class Compare > std::pair < ForwardIt, ForwardIt > minmax_element( ForwardIt first, ForwardIt last, Compare comp );
Tym razem działają na ZAKRESACH i zwracają iterator (nie wartość) — dzięki temu można np. dowiedzieć się, na której pozycji jest minimum, albo je potem zmodyfikować. Przy duplikatach
min_element
zwraca
pierwsze wystąpienie minimum.
Subtelna niespójność, łatwa do przeoczenia:
minmax_element
zwraca iterator na
pierwsze minimum, ale na
ostatnie maksimum (gdy są duplikaty) — inaczej niż wywołanie
max_element
osobno, które zwróciłoby pierwsze maksimum! To celowy wybór standardu, umożliwiający implementację w maks. 3N/2 porównaniach (zamiast 2N przy dwóch osobnych przebiegach) — ale jeśli Twój kod zakłada "pierwsze wystąpienie" dla obu, to pułapka.
std::clamp (
C++17)
Złożoność: O(1) (ściśle: Θ(1))
template < class T > constexpr const T & clamp( const T & v, const T & lo, const T & hi );
template < class T, class Compare > constexpr const T & clamp( const T & v, const T & lo, const T & hi, Compare comp );
Zwraca
lo
gdy
v < lo
,
hi
gdy
hi < v
, w przeciwnym razie
v
. Warunek wstępny:
lo <= hi
(w przeciwnym razie UB) — najczęstszy błąd to odwrócenie kolejności argumentów.
Kopiowanie i przenoszenie
std::copy /
std::copy_if /
std::copy_n (copy też w dokumentacji:
copy)
Złożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class OutputIt >
OutputIt copy( InputIt first, InputIt last, OutputIt d_first );
template < class InputIt, class OutputIt, class UnaryPred > OutputIt copy_if( InputIt first, InputIt last, OutputIt d_first, UnaryPred pred );
template < class InputIt, class Size, class OutputIt > OutputIt copy_n( InputIt first, Size count, OutputIt result );
Zwracają iterator na koniec zapisanego fragmentu w
dst
.
copy_n
przy
count <= 0
nic nie robi i zwraca
result
bez zmian — nie sprawdza, czy w źródle rzeczywiście jest
count
elementów (to odpowiedzialność wołającego).
std::copy_backward (też w dokumentacji:
copy_backward)
Złożoność: O(N) (ściśle: Θ(N))
template < class BidirIt1, class BidirIt2 >
BidirIt2 copy_backward( BidirIt1 first, BidirIt1 last, BidirIt2 d_last );
Kopiuje elementy zaczynając od końca zakresu (
*( last - 1 )
trafia na
*( d_last - 1 )
, itd.) — dzięki temu, w odróżnieniu od zwykłego
copy
, jest bezpieczne, gdy zakres docelowy NAKŁADA się na źródłowy, o ile
d_last
nie wypada między
first
a
last
(włącznie z
first
). To dokładnie ta sztuczka, której implementacje kontenerów używają np. przy
insert()
w środku wektora — trzeba "przesunąć" ogon w prawo, żeby zrobić miejsce, a robienie tego od przodu nadpisałoby dane, zanim zdążysz je przeczytać.
std::move (algorytm) /
std::move_backward (C++11)
Złożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class OutputIt >
OutputIt move( InputIt first, InputIt last, OutputIt d_first );
template < class BidirIt1, class BidirIt2 >
BidirIt2 move_backward( BidirIt1 first, BidirIt1 last, BidirIt2 d_last );
Odpowiedniki
copy
/
copy_backward
, które zamiast kopiować — przenoszą (każdy element jest efektywnie "wynoszony" przez move-assignment, po czym w źródle pozostaje w poprawnym, ale nieokreślonym stanie).
Częste źródło zamieszania dla uczących się: to NIE jest ten sam
std::move
, co
std::move
z
< utility >
(rzutowanie na referencję r-value,
static_cast < T && >
). To dwie zupełnie różne funkcje o tej samej nazwie w tej samej przestrzeni nazw — przeciążone wg liczby/typu argumentów (ta z
< algorithm >
bierze trzy argumenty i operuje na zakresie, ta z
< utility >
— jeden argument i tylko zmienia "kategorię wartości" wyrażenia, nic nie przenosząc same z siebie). W praktyce kompilator zawsze wybierze właściwe przeciążenie po liczbie argumentów, ale przy czytaniu cudzego kodu warto pamiętać, że to dwa różne byty.
Podmiana elementów (swap)
std::swap /
std::iter_swap /
std::swap_ranges (swap/iter_swap też w dokumentacji:
swap /
iter_swap)
Złożoność:
swap
/
iter_swap
:
O(1) (ściśle:
Θ(1)) /
swap_ranges
:
O(N) (ściśle:
Θ(N))
template < class T > void swap( T & a, T & b ); template < class ForwardIt1, class ForwardIt2 >
void iter_swap( ForwardIt1 a, ForwardIt2 b );
template < class ForwardIt1, class ForwardIt2 >
ForwardIt2 swap_ranges( ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2 );
iter_swap
przyjmuje iteratory (niekoniecznie tego samego typu — mogą to być iteratory dwóch różnych kontenerów, o ile przechowują ten sam typ wartości) i pod spodem woła
swap
na wskazywanych wartościach.
swap_ranges
to po prostu
iter_swap
powtórzony dla całego zakresu.
Choć historycznie
std::swap
bywał wymieniany razem z algorytmami z
< algorithm >
, od C++11 formalnie mieszka w
< utility >
. Warto też pamiętać o idiomie "swap via ADL" (argument-dependent lookup) — pisząc generyczny kod wołaj
using std::swap; swap( a, b );
zamiast wprost
std::swap( a, b )
, żeby pozwolić kompilatorowi wybrać bardziej wyspecjalizowaną, zwykle wydajniejszą wersję
swap
danej klasy, jeśli taka istnieje (tzw. swap idiom, kluczowy np. przy implementacji silnej gwarancji wyjątkowej).
Generowanie i wypełnianie
std::fill /
std::fill_n (też w dokumentacji:
fill /
fill_n)
Złożoność: O(N) (ściśle: Θ(N))
template < class ForwardIt, class T > void fill( ForwardIt first, ForwardIt last, const T & value );
template < class OutputIt, class Size, class T > OutputIt fill_n( OutputIt first, Size count, const T & value );
Proste przypisanie tej samej wartości do każdego elementu zakresu (lub pierwszych
count
elementów).
std::generate /
std::generate_nZłożoność: O(N) (ściśle: Θ(N))
template < class ForwardIt, class Generator > void generate( ForwardIt first, ForwardIt last, Generator g );
template < class OutputIt, class Size, class Generator > OutputIt generate_n( OutputIt first, Size count, Generator g );
g
jest wołane BEZ argumentów dla każdej pozycji (w odróżnieniu od
transform
, gdzie funkcja dostaje elementy wejściowe) — typowe zastosowanie to generator liczb losowych albo licznik ze stanem w lambdzie (np.
int n = 0; generate( v.begin(), v.end(),[ & ] { return n++; } );
, choć do tego konkretnego przypadku lepiej pasuje
std::iota
z
< numeric >
, patrz niżej).
Od C++26 dochodzi wyspecjalizowany
std::ranges::generate_random( zakres, generator )
(nagłówek
< random >
) — wypełnia zakres liczbami losowymi bez ręcznego pisania lambdy wołającej generator; implementacja może to zoptymalizować (np. wektoryzacja/generowanie "hurtowe"), czego nie gwarantuje ręczne
generate
z lambdą.
Transformacja i zastępowanie
std::transformZłożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class OutputIt, class UnaryOp >
OutputIt transform( InputIt first1, InputIt last1, OutputIt d_first, UnaryOp unary_op );
template < class InputIt1, class InputIt2, class OutputIt, class BinaryOp >
OutputIt transform( InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOp binary_op );
Wersja jednoargumentowa przekształca każdy element zakresu funkcją
unary_op
; wersja dwuargumentowa bierze po jednym elemencie z dwóch zakresów naraz (np. do dodawania dwóch wektorów element po elemencie).
d_first
może być równe
first1
(transformacja w miejscu) — to bezpieczne i częste.
To najbardziej ogólny z algorytmów modyfikujących —
replace
,
fill
czy nawet
copy
dałoby się (mniej czytelnie) zapisać przez
transform
z odpowiednią lambdą. Warto jednak używać najbardziej wyspecjalizowanego algorytmu pasującego do zadania — to lepiej oddaje intencję (patrz argumenty na początku artykułu) i bywa, że wyspecjalizowana wersja ma dodatkowe optymalizacje (np.
fill
dla typów trywialnych może zejść do
memset
).
std::replace /
std::replace_if (też w dokumentacji:
replace /
replace_if)
Złożoność: O(N) (ściśle: Θ(N))
template < class ForwardIt, class T >
void replace( ForwardIt first, ForwardIt last, const T & old_value, const T & new_value );
template < class ForwardIt, class UnaryPred, class T >
void replace_if( ForwardIt first, ForwardIt last, UnaryPred p, const T & new_value );
Modyfikują zakres w miejscu — każde wystąpienie
old_value
(lub każdy element spełniający
p
) zostaje nadpisane przez
new_value
.
std::replace_copy /
std::replace_copy_ifZłożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class OutputIt, class T >
OutputIt replace_copy( InputIt first, InputIt last, OutputIt d_first, const T & old_value, const T & new_value );
template < class InputIt, class OutputIt, class UnaryPred, class T >
OutputIt replace_copy_if( InputIt first, InputIt last, OutputIt d_first, UnaryPred p, const T & new_value );
Nieniszczące odpowiedniki powyższych — źródło zostaje bez zmian, wynik (z podmienionymi wartościami) trafia do
d_first
. To ten sam wzorzec "wersja w miejscu / wersja _copy", który zobaczysz jeszcze przy
remove
i
unique
poniżej.
Usuwanie i unikalność
std::remove /
std::remove_ifZłożoność: O(N) (ściśle: Θ(N))
template < class ForwardIt, class T >
ForwardIt remove( ForwardIt first, ForwardIt last, const T & value );
template < class ForwardIt, class UnaryPred >
ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPred p );
To prawdopodobnie najbardziej mylący algorytm w całej bibliotece dla początkujących:
NIE usuwa niczego z kontenera i nie zmienia jego rozmiaru!
remove
przesuwa elementy różne od
value
na początek zakresu (zachowując ich względną kolejność) i zwraca iterator na nowy "koniec" — reszta zakresu (między nowym końcem a starym
last
) zawiera elementy w nieokreślonym stanie (część to "usunięte" wartości, część to duplikaty przesuniętych). Dlatego niemal zawsze widuje się to w parze z metodą
erase
kontenera — tzw. idiom erase-remove:
v.erase( std::remove( v.begin(), v.end(), 42 ), v.end() );
Dopiero
erase
faktycznie zmniejsza kontener. Sam
remove
bez następującego
erase
to niemal zawsze błąd logiczny w kodzie (kompiluje się, ale nie robi tego, czego autor prawdopodobnie chciał).
std::remove_copy /
std::remove_copy_ifZłożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class OutputIt, class T >
OutputIt remove_copy( InputIt first, InputIt last, OutputIt d_first, const T & value );
template < class InputIt, class OutputIt, class UnaryPred >
OutputIt remove_copy_if( InputIt first, InputIt last, OutputIt d_first, UnaryPred p );
Nieniszczący wariant — źródło zostaje bez zmian, do
d_first
trafiają tylko elementy, które PRZESZŁYBY przez
remove
(czyli różne od
value
/ niespełniające
p
). Tu nie ma pułapki z poprzedniego punktu —
d_first
to zwykły iterator wyjściowy, więc "usuwanie" jest tu dosłowne (po prostu nie kopiujemy niechcianych elementów).
std::unique /
std::unique_copyZłożoność: O(N) (ściśle: Θ(N) — dokładnie N−1 porównań)
template < class ForwardIt > ForwardIt unique( ForwardIt first, ForwardIt last );
template < class ForwardIt, class BinaryPred > ForwardIt unique( ForwardIt first, ForwardIt last, BinaryPred p );
template < class InputIt, class OutputIt > OutputIt unique_copy( InputIt first, InputIt last, OutputIt d_first );
template < class InputIt, class OutputIt, class BinaryPred > OutputIt unique_copy( InputIt first, InputIt last, OutputIt d_first, BinaryPred p );
Usuwa (a precyzyjniej — tak jak
remove
, tylko PRZESUWA i zwraca nowy koniec, więc znów potrzeba
erase
) kolejne, SĄSIADUJĄCE ze sobą duplikaty, zostawiając pierwszy element z każdej serii.
unique
usuwa tylko sąsiadujące duplikaty —
{ 1, 2, 1, 1, 2 }
da w wyniku
{ 1, 2, 1, 2 }
(jedynka i dwójka z początku i końca NIE są sąsiadujące, więc zostają obie). Jeśli zamiarem jest "zostaw tylko unikalne wartości w całym zakresie", zakres trzeba NAJPIERW posortować (
std::sort
) — dopiero wtedy wszystkie duplikaty danej wartości sąsiadują ze sobą. Stąd klasyczny idiom "usuń duplikaty z wektora":
std::sort( v.begin(), v.end() );
v.erase( std::unique( v.begin(), v.end() ), v.end() );
Wersja oparta na zakresach ma tu dodatkową pułapkę odwrotną:
std::ranges::unique
zwraca nie iterator, a
subrange
(patrz też ograniczenia ranges w dalszej części artykułu), więc idiom trzeba zapisać jako
v.erase( std::ranges::unique( v ).begin(), v.end() );
albo przez nazwaną zmienną:
auto to_erase = std::ranges::unique( v ); v.erase( to_erase.begin(), v.end() );
Zmiana kolejności
std::reverse /
std::reverse_copy (też w dokumentacji:
reverse /
reverse_copy)
Złożoność: O(N) (ściśle: Θ(N) — dokładnie N/2 zamian)
template < class BidirIt > void reverse( BidirIt first, BidirIt last );
template < class BidirIt, class OutputIt > OutputIt reverse_copy( BidirIt first, BidirIt last, OutputIt d_first );
reverse
odwraca kolejność elementów w miejscu — zamienia
* first
z
*( last - 1 )
, potem
*( first + 1 )
z
*( last - 2 )
, i tak dalej, aż pozycje się miną lub zrównają w środku zakresu (stąd wymóg
BidirectionalIterator
— trzeba umieć iść też "do tyłu").
reverse_copy
nie rusza źródła — zapisuje odwróconą kolejność elementów pod
d_first
i, jak większość algorytmów z przyrostkiem
_copy
, zwraca iterator na koniec zapisanego (odwróconego) zakresu w miejscu docelowym, czyli
d_first +( last - first )
.
std::rotate /
std::rotate_copyZłożoność: O(N) (ściśle: Θ(N) — od C++11 gwarantowane dokładnie N zamian)
template < class ForwardIt > ForwardIt rotate( ForwardIt first, ForwardIt n_first, ForwardIt last );
template < class ForwardIt, class OutputIt >
OutputIt rotate_copy( ForwardIt first, ForwardIt n_first, ForwardIt last, OutputIt d_first );
Element wskazywany przez
n_first
staje się nowym pierwszym elementem zakresu, dotychczasowy
first .. n_first - 1
"zawija się" na koniec. Zwraca iterator odpowiadający pozycji, na którą trafił dawny
first
(czyli
first +( last - n_first )
). Działa już na zwykłym
ForwardIterator
(nie potrzeba nawet iteratora dwukierunkowego) — od C++11 gwarantowane jest dokładnie N zamian.
std::shift_left /
std::shift_right (C++20)
Złożoność: O(N) (ściśle: Θ(N))
template < class ForwardIt >
ForwardIt shift_left( ForwardIt first, ForwardIt last,
typename std::iterator_traits < ForwardIt >::difference_type n );
template < class ForwardIt >
ForwardIt shift_right( ForwardIt first, ForwardIt last,
typename std::iterator_traits < ForwardIt >::difference_type n );
Przesuwają elementy o
n
pozycji (odpowiednio w lewo/w prawo) za pomocą move-przypisań — miejsce zwolnione po przesunięciu zawiera elementy w nieokreślonym (ale poprawnym) stanie. Gdy
n <= 0
lub
n >= last - first
, zakres pozostaje bez zmian; w pozostałych przypadkach
shift_left
zwraca
first + n
(nowy koniec "przesuniętej" treści), a
shift_right
—
first + n
jako nowy początek.
To w istocie "rotate bez zawijania" — o ile
rotate
przenosi wypchnięte elementy na drugi koniec,
shift_left
/
shift_right
po prostu je porzuca (zostawiając nieokreśloną wartość na zwolnionym miejscu). Dodane w C++20 głównie z myślą o algorytmach operujących na "oknach" danych oraz o przenoszalnych, ale niekopiowalnych typach, gdzie
rotate
bywał niepotrzebnie kosztowny.
std::shuffle (C++11)
Złożoność:
O(N) (ściśle:
Θ(N) —
Fisher-Yates zawsze wykonuje dokładnie N−1 zamian)
template < class RandomIt, class URBG >
void shuffle( RandomIt first, RandomIt last, URBG && g );
Losowo tasuje zakres, korzystając z przekazanego generatora liczb losowych spełniającego wymogi
UniformRandomBitGenerator
(np.
std::mt19937
). Zastąpił usunięty w
C++17 std::random_shuffle
(który albo korzystał z globalnego, słabej jakości generatora, albo z domyślnie skonstruowanego — trudno było o powtarzalność i dobrą jakość losowości). Wymaga
RandomAccessIterator
(algorytm Fishera-Yatesa potrzebuje swobodnego dostępu).
Próbkowanie
std::sample (
C++17)
Złożoność: O(N)
template < class PopulationIt, class SampleIt, class Distance, class URBG >
SampleIt sample( PopulationIt first, PopulationIt last, SampleIt out, Distance n, URBG && g );
Losuje
n
elementów bez powtórzeń z zakresu od
first
do
last
(a dokładniej:
min( n, last - first )
, gdy zakres jest krótszy niż
n
) i zapisuje je do
out
, zachowując ich względną kolejność z zakresu źródłowego. Różni się od
shuffle
tym, że nie tasuje całego zakresu, tylko wybiera podzbiór — klasyczne zastosowanie: reprezentatywna próbka z dużego zbioru danych bez konieczności jego kopiowania/tasowania w całości. Dla iteratorów wejściowych innych niż
RandomAccessIterator
używany jest algorytm zbiornikowy (reservoir sampling) — dzięki temu działa nawet, gdy nie znamy z góry rozmiaru populacji (np. strumień danych).
PopulationIt
wymaga jedynie
InputIterator
— to najluźniejsza z kategorii, celowo, żeby
sample
dało się użyć nawet na jednorazowym, jednokierunkowym strumieniu danych. Ta swoboda ma jednak cenę: gdy
PopulationIt
NIE jest przynajmniej
ForwardIterator
(czyli zakres źródłowy da się przejść tylko raz), standard wymaga dodatkowo, by
SampleIt
(iterator wyjściowy) był
RandomAccessIterator
— dopiero to pozwala algorytmowi zbiornikowemu nadpisywać już wybrane próbki pod losowymi indeksami w miejscu docelowym. Gdy
PopulationIt
jest przynajmniej
ForwardIterator
(zakres można przejść wielokrotnie),
SampleIt
wystarczy zwykły
OutputIterator
— implementacja może wtedy przejść zakres dwukrotnie (raz, by poznać jego rozmiar, drugi raz, by wybrać elementy) i zapisywać wynik sekwencyjnie. Nazwy
PopulationIt
/
SampleIt
są zgodne z nazewnictwem samego standardu (tam pełne
PopulationIterator
/
SampleIterator
) — w odróżnieniu od
ForwardIt
/
InputIt
gdzie nazwa od razu mówi o minimalnej kategorii, tutaj po prostu trzeba to wiedzieć (stąd ten akapit zamiast zmiany nazwy).
Permutacje
Permutacja zakresu to dowolne inne ustawienie (przestawienie) tych samych elementów w innej kolejności — zakres N-elementowy ma dokładnie N! permutacji. Permutacje można uporządkować liniowo
leksykograficznie, czyli tak, jak porządkuje się słowa w słowniku: porównując kolejne elementy, aż do pierwszej różnicy (dokładnie to samo porównanie, którego formalnie dokonuje
lexicographical_compare
, opisany w dalszej części artykułu). Poniższe trzy algorytmy operują właśnie na tym liniowym porządku.
std::next_permutation /
std::prev_permutationZłożoność: O(N) w najgorszym przypadku (Θ(N) — np. przejście z ostatniej permutacji na pierwszą wymaga odwrócenia całego zakresu), Θ(1) w najlepszym (wystarczy zamienić dwa ostatnie elementy)
template < class BidirIt > bool next_permutation( BidirIt first, BidirIt last );
template < class BidirIt, class Compare > bool next_permutation( BidirIt first, BidirIt last, Compare comp );
template < class BidirIt > bool prev_permutation( BidirIt first, BidirIt last );
template < class BidirIt, class Compare > bool prev_permutation( BidirIt first, BidirIt last, Compare comp );
Przekształcają zakres w miejsce w kolejną (lub poprzednią) permutację w porządku leksykograficznym i zwracają
true
— a gdy podany zakres jest już permutacją ostatnią (odpowiednio pierwszą), "zawijają" go do permutacji pierwszej (ostatniej) i zwracają
false
. Dzięki temu klasyczna pętla "wypisz wszystkie permutacje" wygląda tak:
std::sort( v.begin(), v.end() ); do {
} while( std::next_permutation( v.begin(), v.end() ) );
Żeby przejść przez WSZYSTKIE permutacje, zakres musi zaczynać się od najmniejszej leksykograficznie (czyli posortowanej rosnąco) — w przeciwnym razie pętla przejdzie tylko przez "resztę" cyklu, pomijając permutacje leksykograficznie mniejsze od startowej. To częsty błąd przy pisaniu takiego kodu bez wcześniejszego
sort
.
std::is_permutation (C++11)
Złożoność: O(N²) w ogólności, O(N) gdy elementy są haszowalne (zależnie od implementacji i typu — nie precyzuję tu Θ)
template < class ForwardIt1, class ForwardIt2 >
bool is_permutation( ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2 );
template < class ForwardIt1, class ForwardIt2, class BinaryPred >
bool is_permutation( ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, BinaryPred p );
Sprawdza, czy oba zakresy zawierają te same elementy z tymi samymi krotnościami, niekoniecznie w tej samej kolejności (czyli: czy jeden jest permutacją drugiego). W ogólnym przypadku wymaga to porównań rzędu
O(N²) (dla dowolnego
ForwardIterator
i dowolnego predykatu równości), ale implementacje mogą przyspieszyć dla typów spełniających dodatkowe wymagania.
Porównywanie leksykograficzne
std::lexicographical_compareZłożoność: O(min(N1,N2)) w najgorszym przypadku (Θ(min(N1,N2))), Θ(1) w najlepszym
template < class InputIt1, class InputIt2 >
bool lexicographical_compare( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2 );
template < class InputIt1, class InputIt2, class Compare >
bool lexicographical_compare( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, Compare comp );
Porównuje dwa zakresy tak, jak porównuje się słowa w słowniku: element po elemencie, aż do pierwszej różnicy (a jeśli jeden zakres jest prefiksem drugiego — krótszy jest "mniejszy"). Zwraca
true
, gdy pierwszy zakres jest leksykograficznie mniejszy od drugiego. To właśnie ten algorytm stoi pod spodem
operator <
dla
std::vector
,
std::string
i innych kontenerów sekwencyjnych. Wersja z
comp
pozwala podać własne kryterium porównania pojedynczych elementów (np. porównanie ignorujące wielkość liter) zamiast domyślnego
operator <
.
std::lexicographical_compare_three_way (C++20)
Złożoność: O(min(N1,N2)) w najgorszym przypadku (Θ(min(N1,N2))), Θ(1) w najlepszym
template < class InputIt1, class InputIt2, class Cmp >
auto lexicographical_compare_three_way( InputIt1 first1, InputIt1 last1,
InputIt2 first2, InputIt2 last2, Cmp comp );
template < class InputIt1, class InputIt2 >
auto lexicographical_compare_three_way( InputIt1 first1, InputIt1 last1,
InputIt2 first2, InputIt2 last2 );
Odpowiednik powyższego dla operatora trójstronnego porównania
<=>
(spaceship operator) — zamiast
bool
zwraca kategorię porządkowania (np.
std::strong_ordering
), jednym przebiegiem rozstrzygając "mniejszy / równy / większy", zamiast — jak dawniej bywało — wołać
lexicographical_compare
dwukrotnie (raz dla
<
, raz dla
>
) żeby dowiedzieć się wszystkich trzech możliwości.
Algorytmy numeryczne (nagłówek <numeric>)
Osobny nagłówek, ale koncepcyjnie to dalej algorytmy uogólnione — operują na zakresach przez iteratory tak samo, jak wszystko powyżej.
std::iota (C++11)
Złożoność: O(N) (ściśle: Θ(N))
template < class ForwardIt, class T > void iota( ForwardIt first, ForwardIt last, T value );
Wypełnia zakres kolejnymi wartościami zaczynając od
value
, zwiększając je przez
++value
po każdym zapisie. Najczęstsze zastosowanie: szybkie wygenerowanie wektora indeksów
{ 0, 1, 2, ..., N - 1 }
.
std::accumulate /
std::reduce (
C++17)
Złożoność: O(N) (ściśle: Θ(N) — zawsze N kombinacji, bez skrótów)
template < class InputIt, class T > T accumulate( InputIt first, InputIt last, T init );
template < class InputIt, class T, class BinaryOp > T accumulate( InputIt first, InputIt last, T init, BinaryOp op );
template < class InputIt, class T > T reduce( InputIt first, InputIt last, T init );
template < class InputIt, class T, class BinaryOp > T reduce( InputIt first, InputIt last, T init, BinaryOp op );
accumulate
to klasyczny fold: domyślnie sumuje (
init = init + * it
dla kolejnych
it
), z
op
pozwala zastąpić dodawanie dowolną operacją — GWARANTUJE kolejność lewo-do-prawa, więc bezpiecznie użyjesz go np. do konkatenacji stringów albo mnożenia macierzy (gdzie kolejność ma znaczenie).
reduce
robi to samo co
accumulate
, ale świadomie REZYGNUJE z gwarancji kolejności — elementy mogą być łączone w dowolnej kolejności i dowolnym grupowaniu. To brzmi jak wada, ale jest zamierzoną cechą: dzięki temu
reduce
może być bezpiecznie zrównoleglony (i ma przeciążenia z execution policy), o ile Twoja operacja
op
jest łączna i przemienna (dla zwykłego dodawania liczb całkowitych — tak, dla dodawania
float
/
double
— technicznie nie do końca, ze względu na błędy zaokrągleń, choć w praktyce różnice bywają pomijalne). Zasada: gdy kolejność ma znaczenie —
accumulate
; gdy zależy Ci na wydajności przy dużych zakresach i operacja jest łączna —
reduce
.
std::inner_product /
std::transform_reduce (
C++17)
Złożoność: O(N) (ściśle: Θ(N))
template < class InputIt1, class InputIt2, class T >
T inner_product( InputIt1 first1, InputIt1 last1, InputIt2 first2, T init );
template < class InputIt1, class InputIt2, class T >
T transform_reduce( InputIt1 first1, InputIt1 last1, InputIt2 first2, T init );
inner_product
domyślnie liczy iloczyn skalarny:
init
+ Σ
( first1[ k ] * first2[ k ] )
, z gwarantowaną kolejnością lewo-do-prawa (jak
accumulate
).
transform_reduce
to jego "nieuporządkowany", równoległo-przyjazny odpowiednik (jak
reduce
jest dla
accumulate
) — dodatkowo ma wersję jednozakresową, łączącą "zmapuj funkcją, potem zredukuj" w jednym wywołaniu, bez tworzenia tymczasowego zakresu pośredniego.
std::partial_sum /
std::adjacent_differenceZłożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class OutputIt > OutputIt partial_sum( InputIt first, InputIt last, OutputIt d_first );
template < class InputIt, class OutputIt > OutputIt adjacent_difference( InputIt first, InputIt last, OutputIt d_first );
partial_sum
:
k
-ty element wyniku to suma
first[ 0 ] + ... + first[ k ]
(suma częściowa/prefiksowa; z
BinaryOp
— dowolna inna operacja zamiast dodawania).
adjacent_difference
robi w pewnym sensie operację odwrotną:
d_first[ 0 ] = first[ 0 ]
, a dla
k > 0
:
d_first[ k ] = first[ k ] - first[ k - 1 ]
. Zastosowanie
adjacent_difference
po
partial_sum
(z tymi samymi operacjami odwrotnymi) odtwarza oryginalny zakres.
std::inclusive_scan /
std::exclusive_scan (
C++17)
Złożoność: O(N) (ściśle: Θ(N))
template < class InputIt, class OutputIt > OutputIt inclusive_scan( InputIt first, InputIt last, OutputIt d_first );
template < class InputIt, class OutputIt, class T >
OutputIt exclusive_scan( InputIt first, InputIt last, OutputIt d_first, T init );
Nieuporządkowane (równoległo-przyjazne) odpowiedniki
partial_sum
— wymagają operacji łącznej z tego samego powodu, co
reduce
.
Różnica między nimi to dokładnie jeden element przesunięcia, stąd nazwy:
inclusive_scan
—
k
-ty wynik WŁĄCZA
k
-ty element wejścia (jak
partial_sum
:
d[ k ] = x[ 0 ] + ... + x[ k ]
);
exclusive_scan
—
k
-ty wynik WYŁĄCZA
k
-ty element (liczy tylko
x[ 0 ] + ... + x[ k - 1 ]
, startując od jawnie podanego
init
). Dlatego
exclusive_scan
WYMAGA parametru
init
(musi mieć od czego zacząć sumę pustą), a
inclusive_scan
ma
init
opcjonalny (bez niego pierwszy element wyniku to po prostu pierwszy element wejścia). Ten sam podział "inclusive/exclusive" znajdziesz w wielu innych bibliotekach programowania równoległego (np. CUDA Thrust, Intel TBB).
std::transform_inclusive_scan /
std::transform_exclusive_scan (
C++17)
Złożoność: O(N) (ściśle: Θ(N))
Łączą
transform_reduce
(zmapuj, potem zredukuj) z ideą skanu prefiksowego: najpierw stosują podaną transformację jednoargumentową do każdego elementu, potem liczą
inclusive_scan
/
exclusive_scan
na wynikach — bez materializowania zakresu pośredniego. Sygnatury są rozbudowane (kilka przeciążeń różniących się kolejnością/obecnością
init
i operacji), więc po dokładne parametry warto zajrzeć wprost do dokumentacji.
W
< numeric >
mieszkają też cztery funkcje, które formalnie NIE są algorytmami uogólnionymi w powyższym sensie — działają na pojedynczych wartościach, nie na zakresach:
std::gcd( a, b )
/
std::lcm( a, b )
(największy wspólny dzielnik / najmniejsza wspólna wielokrotność, obie
C++17) oraz
std::midpoint( a, b )
/
std::lerp( a, b, t )
(bezpieczny arytmetycznie punkt środkowy i interpolacja liniowa, obie
C++20, nagłówek
< numeric >
dla
midpoint
i
< cmath >
dla
lerp
). Warto o nich wiedzieć, bo często pojawiają się w tym samym kontekście, mimo że nie pasują do żadnej z kategorii z tego artykułu.
Funkcje z języka C
std::qsort /
std::bsearchZłożoność: zwykle O(N log N) / O(log N), ale standard tego NIE gwarantuje — stąd żadnego Θ tu nie podaję
void qsort( void * ptr, std::size_t count, std::size_t size,
int( * comp )( const void *, const void * ) );
void * bsearch( const void * key, const void * ptr, std::size_t count, std::size_t size,
int( * comp )( const void *, const void * ) );
Odziedziczone wprost z C (nagłówek
< cstdlib >
) — działają na surowej pamięci (
void *
), z ręcznie podanym rozmiarem pojedynczego elementu i wskaźnikiem na funkcję porównującą (zwracającą
int
: ujemny/zero/dodatni, analogicznie do C-owego
strcmp
).
W kodzie C++ praktycznie nie ma powodu po nie sięgać:
std::sort
/
std::binary_search
są w pełni typowo bezpieczne (żadnego rzutowania na
void *
), zwykle szybsze (komparator w postaci lambdy/funktora łatwo się inline'uje, wywołanie przez wskaźnik na funkcję zwykle nie), a dodatkowo standard C++ w ogóle nie gwarantuje żadnej konkretnej złożoności dla
qsort
/
bsearch
(w przeciwieństwie do
std::sort
, gdzie
O(N log N) w najgorszym przypadku to formalny wymóg od C++11) — tylko poprawność wyniku przy odpowiednio posortowanych/nieposortowanych danych. Te dwie funkcje mają sens głównie przy interfejsowaniu z istniejącym kodem C.
Scalanie (merge)
std::merge /
std::inplace_merge (inplace_merge też w dokumentacji:
inplace_merge)
Złożoność:
merge
:
O(N1+N2) (ściśle:
Θ(N1+N2) — trzeba przepisać wszystkie elementy) /
inplace_merge
:
O(N log N),
O(N) przy wystarczającej dodatkowej pamięci (ściśle:
Θ(N) w tym drugim przypadku)
template < class InputIt1, class InputIt2, class OutputIt >
OutputIt merge( InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt d_first );
template < class BidirIt >
void inplace_merge( BidirIt first, BidirIt middle, BidirIt last );
merge
scala dwa JUŻ posortowane zakresy źródłowe do trzeciego, docelowego miejsca — jest stabilny (przy remisie pierwszeństwo ma element z pierwszego zakresu).
inplace_merge
robi dokładnie to samo, ale dla dwóch sąsiadujących, posortowanych podzakresów TEGO SAMEGO zakresu (od
first
do
middle
oraz od
middle
do
last
), scalając je w miejscu w jeden posortowany zakres.
To dosłownie krok "merge" algorytmu merge sort — mając funkcję sortującą małe zakresy (albo rekurencyjnie dzieląc zakres na połowy aż do pojedynczych elementów, z definicji posortowanych),
inplace_merge
pozwala złożyć z tego pełny merge sort bez samodzielnego pisania logiki scalania:
void merge_sort( auto first, auto last ) {
if( last - first <= 1 ) return;
auto middle = first +( last - first ) / 2;
merge_sort( first, middle );
merge_sort( middle, last );
std::inplace_merge( first, middle, last );
}
Operacje na pamięci nieinicjalizowanej
W odróżnieniu od scalania (wyżej) — po te algorytmy w typowym kodzie aplikacyjnym raczej nie sięgniesz. To narzędzia dla osób piszących WŁASNE kontenery. Są potrzebne, bo zwykłe
copy
/
fill
zakładają, że pod docelowym adresem już ISTNIEJE żywy obiekt (bo wykonują przypisanie,
operator =
) — użycie ich na surowej, niezainicjalizowanej pamięci (np. świeżo zaalokowanej przez
std::allocator
, zanim jakikolwiek konstruktor tam zadziałał) to niezdefiniowane zachowanie. Algorytmy z tej grupy zamiast przypisania używają placement-new (konstruują obiekt "od zera" pod wskazanym adresem).
Specjalizowane algorytmy z < memory >
Złożoność:
O(N) (ściśle:
Θ(N) — poza
construct_at
:
O(1), ściśle
Θ(1))
template < class InputIt, class NoThrowForwardIt >
NoThrowForwardIt uninitialized_copy( InputIt first, InputIt last, NoThrowForwardIt d_first );
template < class ForwardIt > void uninitialized_default_construct( ForwardIt first, ForwardIt last ); template < class ForwardIt > void uninitialized_value_construct( ForwardIt first, ForwardIt last ); template < class ForwardIt > void destroy( ForwardIt first, ForwardIt last ); template < class T, class ... Args > constexpr T * construct_at( T * p, Args && ... args );
Grupę dopełniają warianty
_n
oraz przenoszące
uninitialized_move
/
uninitialized_move_n
(C++17).
destroy *
/
construct_at
pozwalają explicite wywołać destruktor/konstruktor bez zwalniania czy alokowania pamięci — to właśnie tymi cegiełkami zbudowane są od środka
std::vector
i podobne kontenery.
uninitialized_default_construct
a
uninitialized_value_construct
różnią się dokładnie tak, jak różni się
T x;
od
T x { };
dla typów skalarnych: pierwsza zostawia niezainicjalizowaną (nieokreśloną) wartość, druga — zeruje. Dla typów klasowych z jawnym konstruktorem różnicy zwykle nie ma (oba wołają ten sam konstruktor domyślny), ale dla
int
,
float
czy wskaźników różnica jest realna i łatwa do przeoczenia.
construct_at
(C++20) zyskał na znaczeniu, bo w przeciwieństwie do zwykłego placement-new (który przed C++20 nie mógł być użyty w kontekście
constexpr
) jest przyjazny dla stałowyrażeniowego kodu — to jeden z elementów, które w ogóle umożliwiły powstanie
constexpr
kontenerów w nowszych standardach. Od C++26 cała ta grupa (łącznie z
uninitialized_copy
/
fill
i pochodnymi) zyskuje pełne wsparcie
constexpr
.
Co nowego dla algorytmów: C++20, C++23, C++26
Powyżej, przy okazji poszczególnych algorytmów, zaznaczałem standard, w którym każdy z nich się pojawił. Poniżej krótkie, celowo zwięzłe zestawienie nowości z ostatnich trzech standardów, które NIE mają swojego klasycznego odpowiednika opisanego wyżej (bo są dostępne tylko jako
std::ranges::
/
std::views::
) albo dotyczą wszystkich algorytmów naraz. Pełny, dogłębny opis mechanizmu zakresów (w tym wszystkich nowych widoków/views) zostawiam
oddzielnemu artykułowi — tu tylko tyle, ile potrzeba, by zorientować się "co nowego" pod kątem samych algorytmów.
C++20
Największa zmiana to oczywiście całe
std::ranges::
/
std::views::
(zakresowe wersje niemal wszystkich algorytmów z tego artykułu, wraz z projekcjami — możliwością posortowania np. po polu struktury bez pisania lambdy) — opisane w artykule o zakresach. Z rzeczy klasycznych (nie-zakresowych), o których już była mowa wyżej:
shift_left
/
shift_right
,
lexicographical_compare_three_way
oraz polityka wykonania
std::execution::unseq
.
C++23
C++26
Wszystkie powyższe C++26 to na dzień pisania tego akapitu (2026 r.) świeżutkie nowości — żaden mainstreamowy kompilator nie ma jeszcze pełnego wsparcia dla wszystkich z nich naraz. Zanim zaczniesz z nich korzystać produkcyjnie, sprawdź
tabelę wsparcia kompilatorów na cppreference.
Dodatek: usunięte algorytmy
Biblioteka standardowa bardzo dba o kompatybilność wsteczną — usunięcia zdarzają się rzadko. Z całego
< algorithm >
/
< numeric >
do tej pory usunięto tak naprawdę jeden algorytm:
std::random_shuffle
— oznaczony jako przestarzały (deprecated) w
C++14, usunięty w
C++17. Powód usunięcia: jego wersja bez jawnie podanego generatora opierała się zwykle na
std::rand
i globalnym stanie losowości słabej jakości — trudno było o powtarzalność i dobrą jakość losowania. Zastępuje go opisany wyżej
std::shuffle
, wymagający jawnego podania generatora spełniającego
UniformRandomBitGenerator
.
W tej samej fali porządków (deprecation w C++11, usunięcie w C++17) zniknęły też pokrewne, choć formalnie nie algorytmiczne, pomocnicze obiekty funkcyjne z
< functional >
, którymi dawniej owijało się funkcje przekazywane do algorytmów:
std::ptr_fun
,
std::mem_fun
/
std::mem_fun_ref
,
std::bind1st
/
std::bind2nd
oraz bazowe klasy
std::unary_function
/
std::binary_function
. Lambdy (a od C++11 też
std::bind
) całkowicie je zastąpiły — patrz uwaga o
ptr_fun
przy
for_each
na początku artykułu.
Podsumowanie
Po przeczytaniu tego artykułu masz w ręku kompletną ściągę po algorytmach standardowych C++ — z tabelkami cheatsheet, do których możesz wracać bez czytania całości od nowa. Razem to
105 algorytmów (nie licząc wariantów
std::ranges::
, które doczekały się osobnego artykułu) — od
for_each
z C++98 po
generate_random
z C++26.
Ważniejsza od samej listy jest jednak umiejętność, którą przy okazji nabyłeś: wiesz już, jak czytać taką dokumentację i jak się uczyć kolejnych algorytmów samodzielnie, na co zwracać uwagę. Znasz kategorie (niemodyfikujące, podział danych, sortowanie, operacje na zbiorach...), wiesz, co mówi sygnatura, złożoność i wymagana kategoria iteratora, i wiesz, gdzie sprawdzić resztę —
cppreference.com aktualizuje się wraz z każdym nowym standardem, więc gdy pojawi się C++29 (czy jak go tam ostatecznie nazwą), ten sam sposób czytania dokumentacji zadziała bez żadnych zmian.
Warto też pamiętać, że biblioteka standardowa to nie koniec świata algorytmów w C++.
Boost.Algorithm to swego rodzaju "poczekalnia" do standardu — sporo algorytmów, które nigdy (albo jeszcze) nie trafiły do
< algorithm >
, a bywają przydatne; podobnie jak wiele innych bibliotek Boost w przeszłości, część z nich może z czasem stać się podstawą kolejnych propozycji do standardu. Osobną, wartą uwagi kategorią są algorytmy tekstowe — o pracy z tekstem przy użyciu dostępnych narzędzi w C++ napisałem
osobny artykuł.
Autor artykułu
Artykuł powstał we IX 2026, jest aktualny ze standardem
C++26.