Эта глава рассказывает, как работать со стандартными алгоритмами и как использовать их для стандартных контейнеров. Эти алгоритмы первоначально являлись частью того, что часто называется Standard Template Library (STL — стандартная библиотека шаблонов) и представляет собой набор алгоритмов, итераторов и контейнеров, которые теперь вошли в стандартную библиотеку (глава 6 содержит рецепты по работе со стандартными контейнерами). Я их буду называть просто стандартными алгоритмами, итераторами и контейнерами, но не забывайте, что это то же самое, что другие авторы называют частью STL. Одним из базовых элементов стандартной библиотеки являются итераторы, так что первый рецепт описывает, что они собой представляют и как их использовать. После этого идет несколько рецептов, которые объясняют, как использовать и расширять стандартные алгоритмы. Наконец, если вы не нашли ничего подходящего в стандартной библиотеке, то рецепт 7.10 расскажет, как написать собственный алгоритм.
Представленные здесь рецепты в основном предназначены для работы со стандартными контейнерами, и тому есть две причины. Во-первых, стандартные контейнеры очень распространены, и лучше изучить стандарт, чем изобретать колесо. Во-вторых, реализация алгоритмов из стандартной библиотеки предоставляет хороший пример для подражания в смысле взаимодействия и производительности. Если вы посмотрите, как профессионалы выполнили код стандартной библиотеки, вы, скорее всего, узнаете много нового и полезного для себя.
Все стандартные алгоритмы используют итераторы. Даже если вы знакомы с концепцией итераторов, которые рассматриваются в первом рецепте, посмотрите на табл. 7.1, которая содержит перечень соглашений, используемых в остальных рецептах главы при демонстрации объявлений функций стандартных алгоритмов.
Табл. 7.1. Сокращения категорий итераторов
Сокращение | Значение |
---|---|
In | Input iterator (Итератор ввода) |
Out | Output iterator (Итератор вывода) |
Fwd | Forward iterator (Однонаправленный итератор) |
Bid | Bidirectional iterator (Двунаправленный итератор) |
Rand | Random-access iterator (Итератор произвольного доступа) |
Стандартные алгоритмы также используют функциональные объекты, или функторы. Функциональный объект — это класс, который переопределяет
operator()
так, что его можно вызвать как функцию. Функтор, который возвращает bool
(и не поддерживает состояния, и, следовательно, называется чистым (pure), называется предикатом (predicate), и он является еще одной обычной функциональной особенностью стандартных алгоритмов. Обычно предикат принимает один или два аргумента: если он принимает один аргумент, то это унарный предикат, а если два — то бинарный предикат. Для краткости я при демонстрации объявлений функций использую сокращения, приведенные в табл. 7.2.
Табл. 7.2. Типы функторов
Имя типа | Описание |
---|---|
UnPred | Унарный предикат. Принимает один аргумент и возвращает bool |
BinPred | Бинарный предикат. Принимает два аргумента и возвращает bool |
UnFunc | Унарная функция. Принимает один аргумент и возвращает некое значение |
BinFunc | Бинарная функция. Принимает два аргумента и возвращает некое значение |
В большинстве случаев там, где требуется аргумент в виде функтора, может использоваться указатель на функцию. При использовании термина «функтор» я также подразумеваю указатель на функцию, если не указано иного.
Имеется диапазон итераторов — скорее всего, из стандартного контейнера — и стандартные алгоритмы не удовлетворяют вашим требованиям, так что вам требуется выполнить итерации самостоятельно.
Для доступа к элементам контейнера и перехода от одного элемента к другому используйте
iterator
или const_iterator
. В стандартной библиотеке алгоритмы и контейнеры взаимодействуют с помощью итераторов, и одной из базовых идей стандартных алгоритмов является то, что они избавляют вас от необходимости непосредственного использования итераторов, за исключением тех случаев, когда вы пишете собственный алгоритм. И даже в этом случае вы должны понимать различные типы итераторов с тем, чтобы эффективно использовать стандартные алгоритмы и контейнеры. Пример 7.1 представляет некоторые простые способы использования итераторов.
Пример 7.1. Использование итераторов с контейнерами
#include
#include
#include
#include
using namespace std;
static const int ARRAY_SIZE = 5;
template
FwdIter fixOutliersUBound(FwdIter p1,
FwdIter p2, const T& oldVal, const T& newVal) {
for ( ; p1 != p2; ++p1) {
if (greater(*p1, oldVal)) {
*p1 = newVal;
}
}
}
int main() {
list lstStr;
lstStr.push_back("Please");
lstStr.push_back("leave");
lstStr.push_back("a");
lstStr.push_back("message");
// Создать итератор для последовательного перебора элементов списка
for (list::iterator p = lstStr.begin();
p != lstStr.end(); ++p) {
cout << *p << endl;
}
// Или можно использовать reverse_iterator для перебора от конца
// к началу, rbegin возвращает reverse_iterator, указывающий
// на последний элемент, a rend возвращает reverse_iterator, указывающий
// на один-перед-первым.
for (list::reverse_iterator p = lstStr.rbegin();
p != lstStr.rend(); ++p) {
cout << *p << endl;
}
// Перебор диапазона элементов
string arrStr[ARRAY_SIZE] = {"My", "cup", "cup", "runneth", "over"};
for (string* p = &arrStr[0];
p != &arrStr[ARRAY_SIZE]; ++p) {
cout << *p << endl;
}
// Использование стандартных алгоритмов со стандартной последовательностью
list lstStrDest;
unique_copy(&arrStr[0], &arrStr[ARRAY_SIZE],
back_inserter(lstStrDest));
}
Итератор — это тип, который используется для ссылки на единственный объект в контейнере. Стандартные контейнеры используют итераторы как основной механизм для доступа к содержащимся в них элементам. Итератор ведет себя как указатель; для доступа к объекту, на который указывает итератор, вы его разыменовываете (с помощью операторов
*
или ->
), а для перевода итератора вперед или назад используется синтаксис, аналогичный арифметике указателей. Однако есть несколько причин, по которым итератор — это не то же самое, что указатель. Однако перед тем, как я покажу их, давайте рассмотрим основы использования итераторов.
Итератор объявляется с помощью типа, элементы которого с его помощью будут перебираться. Например, в примере 7.1 используется
list
, так что итератор объявляется вот так.
list::iterator p = lstStr.begin();
Если вы не работали со стандартными контейнерами, то часть этого объявления
::iterator
может выглядеть несколько необычно. Это вложенный в шаблон класса list typedef
, предназначенный именно для этой цели — чтобы пользователи контейнера могли создать итератор для данного конкретного экземпляра шаблона. Это стандартное соглашение, которому следуют все стандартные контейнеры. Например, можно объявить итератор для list
или для set
, как здесь.
list::iterator p1;
set::iterator p2;
Возвращаясь обратно к нашему примеру, итератор о инициализируется первым элементом последовательности, который возвращается методом
begin
. Чтобы перейти к следующему элементу, используется operator++
. Можно использовать как префиксный инкремент так и постфиксный инкремент (p++
), аналогично указателям на элементы массивов, но префиксный инкремент не создает временного значения, так что он более эффективен и является предпочтительным. Постфиксный инкремент (p++
) должен создавать временную переменную, так как он возвращает значение p
до его инкрементирования. Однако он не может инкрементировать значение после того, как вернет его, так что он вынужден делать копию текущего значения, инкрементировать текущее значение, а затем возвращать временное значение. Создание таких временных переменных с течением времени требует все больших и больших затрат, так что если вам не требуется именно постфиксное поведение, используйте префиксный инкремент.
Как только будет достигнут элемент
end
, переход на следующий элемент следует прекратить. Или, строго говоря, когда будет достигнут элемент, следующий за end
. В отношении стандартных контейнеров принято некое мистическое значение, которое представляет элемент, идущий сразу за последним элементом последовательности, и именно оно возвращается методом end
. Этот подход работает в цикле for
, как в этом примере:
for (list::iterator p = lstStr.begin();
p != lstStr.end(); ++p) {
cout << *p << endl;
}
Как только
p
станет равен end
, p
больше не может увеличиваться. Если контейнер пуст, то begin == end
равно true
, и тело цикла никогда не выполнится. (Однако для проверки пустоты контейнера следует использовать метод empty
, а не сравнивать begin
и end
или использовать выражение вида size == 0
.)
Это простое объяснение функциональности итераторов, но это не все. Во-первых, как только что было сказано, итератор работает как
rvalue
или lvalue
, что означает, что его разыменованное значение можно присваивать другим переменным, а можно присвоить новое значение ему. Для того чтобы заменить все элементы в списке строк, можно написать нечто подобное следующему
for (list::iterator p = lstStr.begin();
p != lstStr.end(); ++p) {
*p = "mustard";
}
Так как
*p
ссылается на объект типа string
, для присвоения элементу контейнера новой строки используется выражение string::operator=(const char*)
. Но что, если lstStr
— это объект типа const
? В этом случае iterator
не работает, так как его разыменовывание дает не-const объект. Здесь требуется использовать const_iterator
, который возвращает только rvalue
. Представьте, что вы решили написать простую функцию для печати содержимого контейнера. Естественно, что передавать контейнер следует как const
-ссылку.
template
void printElements(const T& cont) {
for(T::const_iterator p = cont.begin();
p ! = cont.end(); ++p) {
cout << *p << endl;
}
}
В этой ситуации следует использовать именно
const
, a const_iterator
позволит компилятору не дать вам изменить *p
.
Время от времени вам также может потребоваться перебирать элементы контейнера в обратном порядке. Это можно сделать с помощью обычного
iterator
, но также имеется reverse_iterator
, который предназначен специально для этой задачи. reverse_iterator
ведет себя точно так же, как и обычный iterator
, за исключением того, что его инкремент и декремент работают противоположно обычному iterator
и вместо использования методов begin
и end
контейнера с ним используются методы rbegin
и rend
, которые возвращают reverse_iterator
. reverse_iterator
позволяет просматривать последовательность в обратном порядке. Например, вместо инициализации reverse_iterator
с помощью begin
он инициализируется с помощью rbegin
, который возвращает reverse_iterator
, указывающий на последний элемент последовательности. operator++
перемещает его назад — по направлению к началу последовательности, rend
возвращает reverse_iterator
, который указывает на элемент, находящийся перед первым элементом. Вот как это выглядит.
for (list::reverse_iterator p = lstStr.rbegin();
p != lstStr.rend(); ++p) {
cout << *p << endl;
}
Но может возникнуть ситуация, когда использовать
reverse_iterator
невозможно. В этом случае используйте обычный iterator
, как здесь.
for (list::iterator p = --lstStr.end();
p != --lstStr.begin(); --p) {
cout << *p << endl;
}
Наконец, если вы знаете, на сколько элементов вперед или назад следует выполнить перебор, используйте вычисление значения, на которое следует перевести итератор. Например, чтобы перейти в середину списка, сделайте вот так.
size_t i = lstStr.size();
list::iterator p = begin();
p += i/2; // Переход к середине последовательности
Но помните: в зависимости от типа используемого контейнера эта операция может иметь как постоянную, так и линейную сложность. При использовании контейнеров, которые хранят элементы последовательно, таких как
vector
или deque
, iterator
может перейти на любое вычисленное значение за постоянное время. Но при использовании контейнера на основе узлов, такого как list
, такая операция произвольного доступа недоступна. Вместо этого приходится перебирать все элементы, пока не будет найден нужный. Это очень дорого. Именно поэтому выбор контейнера, используемого в каждой конкретной ситуации, определяется требованиями к перебору элементов контейнера и их поиска в нем. (За более подробной информацией о работе стандартных контейнеров обратитесь к главе 6.)
При использовании контейнеров, допускающих произвольный доступ, для доступа к элементам использования
operator[]
с индексной переменной следует предпочитать iterator
. Это особенно важно при написании обобщенного алгоритма в виде шаблона функции, так как не все контейнеры поддерживают iterator
с произвольным доступом.
С итератором можно делает еще много чего, но не с любым
iterator
. iterator
может принадлежать к одной из пяти категорий, обладающих разной степенью функциональности. Однако они не так просты, как иерархия классов, так что именно это я далее и опишу.
Итераторы, предоставляемые различными типами контейнеров, не обязательно все умеют делать одно и то же. Например,
vector::iterator
позволяет использовать для перехода на некоторое количество элементов вперед operator+=
, в то время как list::iterator
не позволяет. Разница между этими двумя типами итераторов определяется их категорией.
Категории итераторов — это, по сути, интерфейс (не технически; для реализации категорий итераторов абстрактные базовые классы не используются). Имеется пять категорий, и каждая предлагает увеличение возможностей. Вот как они выглядят — от наименее до наиболее функциональной.
Input iterator (Итератор ввода)
Итератор ввода поддерживает переход вперед с помощью
p++
или ++p
и разыменовывание с помощью *p
. При его разыменовывании возвращается rvalue
, iterator
ввода используется для таких вещей, как потоки, где разыменовывание итератора ввода означает извлечение очередного элемента из потока, что позволяет прочесть только один конкретный элемент.
Output iterator (Итератор вывода)
Итератор вывода поддерживает переход вперед с помощью
p++
или ++p
и разыменовывание с помощью *p
. От итератора ввода он отличается тем, что из него невозможно читать, а можно только записывать в него — по одному элементу за раз. Также, в отличие от итератора ввода, он возвращает не rvalue
, a lvalue,
так что в него можно записывать значение, а извлекать из него — нельзя.
Forward iterator (Однонаправленный итератор)
Однонаправленный итератор объединяет функциональность итераторов ввода и вывода: он поддерживает
++p
и p++
, а *p
может рассматриваться как rvalue
или lvalue
. Однонаправленный итератор можно использовать везде, где требуется итератор ввода или вывода, используя то преимущество, что читать из него и записывать в него после его разыменовывания можно без ограничений
Bidirectional iterator (Двунаправленный итератор)
Как следует из его названия, двунаправленный
iterator
может перемещаться как вперед, так и назад. Это однонаправленный iterator
, который может перемещаться назад с помощью --p
или p--
.
Random-access iterator (Итератор произвольного доступа)
Итератор произвольного доступа делает все, что делает двунаправленный
iterator
, но также поддерживает операции, аналогичные операциям с указателями.. Для доступа к элементу, расположенному в позиции n после p
последовательности, можно использовать p[n]
, можно складывать его значение или вычитать из него с помощью +
, +=
, -
или -=
, перемещая его вперед или назад на заданное количество элементов. Также с помощью <
, >
, <=
или >=
можно сравнивать два итератора p1
и p2
, определяя их относительный порядок (при условии, что они оба относятся к одной и той же последовательности).
Или можно представить все в виде диаграммы Венна. Она представлена на рис. 7.1.
Рис. 7.1. Категории итераторов
Большая часть стандартных контейнеров поддерживает как минимум двунаправленный
iterator
, некоторые (vector
и deque
) предоставляют iterator
произвольного доступа. Категория итератора, поддерживаемая контейнером, определяется стандартом.
В большинстве случае вы будете использовать
iterator
для простейших задач: поиск элемента и его удаление или что-либо подобное. Для этой цели требуется только однонаправленный iterator
, который доступен для всех контейнеров. Но когда потребуется написать нетривиальный алгоритм или использовать алгоритм из стандартной библиотеки, часто потребуется нечто большее, чем простой однонаправленный iterator
. Но как определить, что вам требуется? Здесь на сцену выходят категории итераторов.
Различные категории итераторов позволяют стандартным (и нестандартным) алгоритмам указать диапазон требуемой функциональности. Обычно стандартные алгоритмы работают с диапазонами, указываемыми с помощью итераторов, а не с целыми контейнерами. Объявление стандартного алгоритма говорит, какую категорию
iterator
он ожидает». Например, std::sort
требует итераторов произвольного доступа, так как ему требуется за постоянное время ссылаться на несмежные элементы. Таким образом, объявление sort
выглядит вот так.
template
void sort(RandomAccessIterator first, RandomAccessIterator last);
По имени типа итератора можно определить, что он ожидает итератор произвольного доступа. Если попробовать откомпилировать
sort
для категории итератора, отличной от произвольного доступа, то она завершится ошибкой, так как младшие категории iterator
не реализуют операций, аналогичных арифметике с указателями.
Категория итератора, предоставляемая определенным контейнером и требуемая определенным стандартным алгоритмом, — это то, что определяет, какой алгоритм с каким контейнером может работать. Многие из стандартных алгоритмов описаны далее в этой главе. Таблица 7.1 показывает сокращения, используемые в остальной части главы для указания типов итераторов, принимаемых алгоритмами в качестве аргументов.
Этот рецепт описывал итераторы, как они используются для контейнеров. Но шаблон итераторов используется не только для контейнеров, и, таким образом, имеются другие типы итераторов. Имеются потоковые итераторы, итераторы буферов потоков и итераторы хранения в необработанном виде, но они здесь не описываются.
Глава 6.
Требуется удалить объекты из контейнера.
Для удаления одного или диапазона элементов используйте метод контейнера erase или один из стандартных алгоритмов. Пример 7.2 показывает пару различных способов удаления элементов из последовательностей.
Пример 7.2. Удаление элементов из контейнера
#include
#include
#include
#include
#include
#include "utils.h" // Для printContainer(): см. 7.10
using namespace std;
int main() {
list lstStr;
lstStr.push_back("On");
lstStr.push_back("a");
lstStr.push_back("cloudy");
lstStr.push_back("cloudy");
lstStr.push_back("day");
list::iterator p;
// Найти то что требуется, с помощью find
p = find(lstStr.begin(), lstStr.end(), "day");
p = lstStr.erase(p); // Теперь p указывает на последний элемент
// Или для удаления всех вхождений чего-либо используйте remove
lstStr.erase(remove(lstStr.begin(), lstStr.end(), "cloudy"),
listStr.end());
printContainer(lstStr); // См. 7.10
}
Для удаления одного или нескольких элементов из контейнера используйте метод
erase
. Все контейнеры содержат два перегруженных erase
: один принимает единственный аргумент iterator
, который указывает на элемент, который требуется удалить, а другой принимает два аргумента, которые представляют диапазон удаляемых элементов. Чтобы удалить один элемент, получите iterator
, указывающий на этот элемент, и передайте этот iterator
в erase
, как в примере 7.2.
p = find(lstStr.begin(), lstStr.end(), "day");
p = lstStr.erase(p);
В результате объект, на который указывает
p
, будет удален, для чего будет вызван его деструктор, а после этого оставшиеся элементы будут реорганизованы. Реорганизация зависит от типа контейнера, и, следовательно, сложность этой операции от контейнера к контейнеру будет различаться. Сигнатура и поведение при использовании последовательного контейнера и ассоциативного контейнера также будут различаться.
В последовательностях
erase
возвращает iterator
, который ссылается на первый элемент, следующий непосредственно за последним удаленным элементом, что может оказаться end
, если был удален последний элемент последовательности. Сложность этой операции для каждого контейнера различна, так как последовательности реализованы по- разному. Например, из-за того, что все элементы vector
хранятся в непрерывном фрагменте памяти, удаление из него элемента, кроме первого и последнего, с целью заполнения образовавшегося промежутка требует сдвига всех последующих элементов в сторону начала. Это приводит к значительному снижению производительности (в линейном отношении), и именно по этой причине не следует использовать vector
, если требуется удалять (или вставлять, что в данном случае приводит к таким же последствиям) элементы где-либо, кроме концов. Более подробно этот вопрос обсуждается в рецепте 6.2.
В ассоциативных контейнерах
erase
возвращает void
. При удалении одного элемента сложность имеет вид амортизированной константы, а при удалении диапазона — логарифмической зависимости плюс количество удаляемых элементов. Причина этого заключается в том, что ассоциативные контейнеры часто реализуются как сбалансированные деревья (например, красно-черное дерево).
erase
удобен, но не интересен. Если требуется большая гибкость в выражении того, что требуется удалить, следует обратить внимание на стандартные алгоритмы (из
). Рассмотрим такую строку из примера 7.2.
lstStr.erase(std::remove(lstStr.begin(), lstStr.end(), "cloudy"),
lstStr.end());
Обратите внимание, что я использую
erase
, но на этот раз по какой-то причине мне требуется удалить из list
все вхождения слова «cloudy», remove
возвращает iterator
, который передается в erase
как начало удаляемого диапазона, a end
передается в erase
как конечная точка диапазона. В результате удаляются все объекты obj
(вызывая их метод delete
) из диапазона, для которого obj == "cloudy"
равно истине. Но поведение этой строки может оказаться не совсем таким, как ожидается. Здесь мне требуется пояснить некоторую терминологию.
remove
на самом деле ничего не удаляет. Он перемещает все, что не равно указанному значению, в начало последовательности и возвращает iterator
, который ссылается на первый элемент, следующий за этими перемещенными элементами. Затем вы должны вызвать erase
для контейнера, чтобы удалить объекты между [p, end)
, где p
— это iterator
, возвращенный remove
.
remove
также имеет несколько вариантов. Что, если требуется удалить элементы, которые удовлетворяют некоторому предикату, а не просто равны какому-то значению? Используйте remove_if
. Например, представьте, что есть класс с именем Conn
, который представляет какой-то тип соединений. Если это соединение простаивает больше определенного значения, его требуется удалить. Во-первых, создайте функтор, как здесь.
struct IdleConnFn :
public std::unary_function { // Включите эту строку,
bool operator() (const Conn& c) const { // чтобы он работал с
if (с.getIdleTime() > TIMEOUT) { // другими объектами из
return(true); //
} else return(false);
}
} idle;
Затем вызовите
remove_if
с erase и передайте в него новый функтор, как здесь.
vec.erase(std::remove_if(vec.begin(), vec.end(), idle), vec.end());
Есть причина, по которой такие функторы следует наследовать от
unary_function
, unary_function
определяет несколько typedef
, используемых другими функторами из
, и если они их не найдут, то другие функторы не скомпилируются. Например, если вы очень злы и хотите удалить все не задействованные в данный момент соединения, то в функторе проверки на простой можно использовать функтор not1
.
vec.erase(std::remove_if(vec.begin(), vec.end(); std::not1(idle)),
vec.end());
Наконец, вам может потребоваться сохранить первоначальную последовательность (может, с помощью
const
) и скопировать результаты, кроме некоторых элементов, в новую последовательность. Это можно сделать с помощью remove_copy
и remove_copy_if
, которые работают аналогично remove и remove_if
, за исключением того, что здесь также требуется передавать iterator
вывода, в который будут записываться результирующие данные. Например, чтобы скопировать из одного списка в другой строку, сделайте так.
std::remove_copy(lstStr.begin(), lstStr.end(), lstStr2, "cloudy");
При использовании
remove_copy
или любого стандартного алгоритма, записывающего в выходной диапазон, следует помнить, что выходной диапазон должен уже быть достаточно большим, чтобы в нем поместились элементы, которые туда будут записываться.
erase
и remove
(и связанные с ними алгоритмы) предлагают удобный способ удалять определенные элементы последовательностей. Они предоставляют простую альтернативу самостоятельному перебору и поиску нужных элементов с последующим их удалением по одному.
Рецепты 6.2 и 7.1.
Имеется последовательность данных и требуется перемешать их так, чтобы они были расположены в случайном порядке.
Используйте стандартный алгоритм
random_shuffle
, определенный в
. random_shuffle
принимает два итератора произвольного доступа и (необязательно) функтор генератора случайных чисел и реорганизует случайным образом элементы заданного диапазона. Пример 7.3 показывает, как это делается.
Пример 7.3. Случайное перемешивание последовательностей
#include
#include
#include
#include
#include "utils.h" // Для printContainer(): см. 7.10
using namespace std;
int main() {
vector v;
back_insert_iterator > p = back_inserter(v);
for (int i = 0; i < 10; ++i) *p = i;
printContainer(v, true);
random_shuffle(v.begin(), v.end());
printContainer(v, true);
}
Вывод должен выглядеть примерно так.
-----
0123456789
-----
8192057346
random_shuffle
очень прост в использовании. Дайте ему диапазон, и он перемешает этот диапазон случайным образом. Имеется две версии, и их прототипы выглядят так.
void random_shuffle(RndIter first, RndIter last);
void random_shuffle(RndIter first, RndIter last, RandFunc& rand);
В первой версии используется зависящая от реализации функция генерации случайных чисел, которой должно быть достаточно для большинства задач. Если ее недостаточно — например, требуется неоднородное распределение, такое, как гауссово — то можно написать собственную функцию, которую можно передать во вторую версию.
Этот генератор случайных чисел должен быть функтором с единственным аргументом, возвращающим единственное значение, и оба они должны преобразовываться в
iterator_traits::difference_type
. В большинстве случаев для этого подойдет целое число. Например, вот мой псевдогенератор случайных чисел.
struct RanNumGenFtor {
size_t operator()(size_t n) const {
return(rand() % n);
}
} rnd;
random_shuffle(v.begin(), vend(), rnd);
Приложения
random_shuffle
ограничены последовательностями, которые предоставляют итераторы случайного доступа (string
, vector
и deque
), массивами или собственными контейнерами, удовлетворяющими этому требованию. Перемешать случайным образом ассоциативный контейнер невозможно, так как его содержимое всегда хранится в упорядоченном виде. На самом деле для ассоциативных контейнеров не всегда можно использовать алгоритм, изменяющий его диапазон (и который часто называется видоизменяющим (mutating) алгоритмом).
Имеется два диапазона и требуется сравнить их на равенство или определить, какой из них меньше, чем другой, основываясь на каком-либо порядке сортировки элементов.
В зависимости от типа выполняемого сравнения используйте один из стандартных алгоритмов —
equal
, lexicographical_compare
или mismatch
, определенных в
. Пример 7.4 показывает некоторые из них в действии.
Пример 7.4. Различные типы сравнения
#include
#include
#include
#include
#include "utils.h"
using namespace std;
using namespace utils;
int main() {
vector vec1, vec2;
vec1.push_back("Charles");
vec1.push_back("in");
vec1.push_back("Charge");
vec2.push_back("Charles");
vec2.push_back("in");
vec2.push_back("charge"); // Обратите внимание на строчную "с"
if (equal(vec1.begin(), vec1.end(), vec2.begin())) {
cout << "Два диапазона равны!" << endl;
} else {
cout << "Два диапазона HE равны!" << endl;
}
string s1 = "abcde";
string s2 = "abcdf";
string s3 = "abc";
cout << boolalpha // Отображает логические значения как "true" или "false"
<< lexicographical_compare(s1.begin(), s1.end(),
s1.begin(), s1.end()) << endl;
cout << lexicographical_compare(s1.begin(), s1.end(),
s2.begin(), s2.end()) << endl;
cout << lexicographical_compare(s2.begin(), s2.end(),
s1.begin(), s1.end()) << endl;
cout << lexicographical_compare(s1.begin(), s1.end(),
s3.begin(), s3.end()) << endl;
cout << lexicographical_compare(s3.begin(), s3.end(),
s1.begin(), s1.end()) << endl;
pair iters =
mismatch(s1.begin(), s1.end(), s2.begin());
cout << "first mismatch = " << *(iters.first) << endl;
cout << "second mismatch = " << *(iters.second) << endl;
}
Вывод примера 7.4 выглядит так.
Два диапазона НЕ равны!
false
true
false
false
true
first mismatch = e
second mismatch = f
Для сравнения двух последовательностей на равенство используйте
equal
. Он принимает три или четыре аргумента, в зависимости от используемой версии. Вот как объявлен equal
.
bool equal(In1 first1, In1 last1, In2 first2);
bool equal(In1 first1, In1 last1, In2 first2, BinPred pred);
equal
с помощью operator==
сравнивает каждый элемент между first1
и last1
с элементами, начиная с first2
. Если указать pred
, то equal
для проверки будет использовать его. Перед вызовом equal
убедитесь, что каждая последовательность имеет одинаковую длину. Он предполагает, что второй диапазон не меньше первого, и если это не так, то его поведение не определено.
Если требуется узнать, где и как последовательности отличаются, используйте
lexicographical_compare
или mismatch
. lexicographical_compare
сравнивает две последовательности и возвращает истину, если первая лексикографически меньше второй, что означает, что каждая пара элементов в двух последовательностях сравнивается с помощью оператора <
. Объявление lexicographical_compare
выглядит вот так.
bool lexicographical_compare(In1 first1, In1 last1,
In2 first2, In2 last2);
bool lexicographical_compare(In1 first1, In1 last1,
In2 first2, In2 last2, Compare comp);
Если
operator<
возвращает истину или первая последовательность заканчивается раньше второй, то возвращается истина. В противном случае возвращается ложь. Рассмотрим последовательность символов из примера 7.4.
string s1 = "abcde";
string s2 = "abcdf";
string s3 = "abc";
lexicographical_compare(s1.begin(), s1.end(), // abcde < abcde
s1.begin(), s1.end()); // = false
lexicographical_compare(s1.begin(), s1.end(), // abcde < abcdf
s2.begin(s2.end()); // = true
lexicographical_compare(s2.begin(), s2.end(), // abcdf < abcde
s1.begin(), s1.end()); // = false
lexicographical_compare(s1.begin(), s1.end(), // abcde < abc
s3.begin(s3.end()); // = false
lexicographical_compare(s3.begin(), s3.end(), // abc < abcde
s1.begin(), s1.end()); // = true
Сложность
lexicographical_compare
линейна и выполняет число сравнений, равное длине меньшей из двух последовательностей, или до тех пор, пока один из элементов в одной из последовательностей не окажется меньше соответствующего элемента другой. Сравнения реализованы полностью на основе operator<
, так что если iter1
и iter2
— это итераторы двух последовательностей, то сравнение останавливается тогда, когда *iter1 < *iter2
или *iter2 < *iter1
.
mismatch
говорит, где две последовательности различаются. Однако его объявление несколько отличается от equal
и lexicographical_compare
, так как он возвращает не bool
, a pair<>
итераторов. Вот оно.
pair mismatch(In1 first1, In1 last1, In2 first2);
pair mismatch(In1 first1, In1 last1, In2 first2, BinPred);
Два возвращаемых итератора указывают на различные элементы каждой из последовательностей. Рассмотрим пример 7.4.
string s1 = "abcde";
string s2 = "abcdf";
pair iters =
mismatch(s1.begin(), s1.end(), s2.begin());
cout << "first mismatch = " << *(iters.first) << '\n'; // 'e'
cout << "second mismatch = " << *(iters.second) << '\n'; // 'f'
Вы должны убедиться, что длина второго диапазона не меньше первого. Если вторая последовательность короче первой,
mismatch
не сможет узнать этого и продолжит выполнение сравнения элементов за границей второй последовательности, что приведет к непредсказуемому поведению. Кроме того, если несовпадений нет, то первый итератор будет указывать на last1
, который может оказаться недействительным (например, если в качестве last1
передать end()
.
Вы, должно быть, заметили по объявлениям каждой из этих функций, что типы итераторов для каждой из этих последовательностей различны. Это означает, что две последовательности могут быть контейнерами разных типов, но при условии, что типы элементов, на которые указывают итераторы, имеют определенный для них
operator<
. Например, можно сравнивать string
и vector
.
string s = "Coke";
vector v;
v.push.back('c');
v.push_back('o');
v.push_back('k');
v.push_back('e');
std::cout << std::lexicographical_compare(s.begin(), s.end(),
v.begin(), v.end()) << '\n';
Здесь каждый символ двух последовательностей сравнивается вне зависимости от типа контейнера, в которых они хранятся.
Стандартная библиотека C++ предоставляет несколько различных способов сравнения последовательностей. Если ни один из них вам не подходит, посмотрите на их исходный код — он является хорошим примером того, как надо писать собственные эффективные обобщенные алгоритмы.
Рецепт 7.1.
Имеется две отсортированные последовательности и их требуется объединить.
Используйте либо шаблон функции
merge
, либо шаблон функции inplace_merge
. merge
объединяет две последовательности и помещает результат в третью, a inplace_merge
объединяет две последовательно расположенные последовательности. Пример 7.5 показывает, как это делается.
Пример 7.5. Объединение двух последовательностей
#include
#include
#include
#include
#include
#include
#include "utils.h" // Для printContainer(): см. 7.10
using namespace std;
int main() {
vector v1, v2, v3;
v1.push_back("a");
v1.push_back("c");
v1.push_back("e");
v2.push_back("b");
v2.push_back("d");
v2.push_back("f");
v3.reserve(v1.size() + v2.size() + 1);
// Используйте back_inserter от итератора, чтобы избежать необходимости
// помещать в контейнер набор объектов по умолчанию. Но это не означает,
// что не требуется использовать reserve!
merge(v1.begin(), v1.end(), v2.begin(), v2.end(),
back_inserter >(v3));
printContainer(v3);
// Теперь выполняем действия
random_shuffle(v3.begin(), v3.end());
sort(v3.begin(), v3.begin() + v3.size() / 2);
sort(v3.begin() + v3.size() / 2, v3.end());
printContainer(v3);
inplace_merge(v3.begin(), v3.begin() + 3, v3.end());
printContainer(v3);
// Однако если используется два списка, используйте list::merge.
// Как правило, ...
list lstStr1, lstStr2;
lstStr1.push_back("Frank");
lstStr1.push_back("Rizzo");
lstStr1.push_back("Bill");
lstStr1.push_back("Cheetoh");
lstStr2.push_back("Allie");
lstStr2.push_back("McBeal");
lstStr2.push_back("Slick");
lstStr2.push_back("Willie");
lstStr1.sort(); // Отсортировать, иначе объединение выдаст мусор!
lstStr2.sort();
lstStr1.merge(lstStr2); // Заметьте, что это работает только для другого
// списка того же типа
printContainer(lstStr1);
}
Вывод примера 7.5 выглядит так.
-----
a
b
с
d
e
f
-----
b
d
e
a
c
f
-----
a
b
с
d
e
f
Allie
Bill
Cheetoh
Frank
McBeal
Rizzo
Slick
Willie
merge
объединяет две последовательности и помещает результат в третью — опционально используя функтор сравнения, указанный пользователем и определяющий, когда один элемент меньше другого, а по умолчанию используя operator<
. Сложность линейна: число выполняемых merge
сравнений равно сумме длин двух последовательностей минус один. Типы элементов в обеих последовательностях должны быть сравниваемы с помощью operator<
(или указанного вами функтора сравнения) и должны быть преобразуемы к типу элементов выходной последовательности при помощи конструктора копирования или присвоения; или должен быть определен оператор преобразования, определенный так, чтобы тип элементов выходной последовательности имел для обоих типов операторы присвоения и конструкторы копирования.
Объявления
merge
выглядят вот так
void merge(In1 first1, In1 last1, In2 first2, In2 last2, Out result);
void merge(In1 first1, In1 last1, In2 first2, In2 last2, Out result,
BinPred comp)
Использование
merge
довольно просто. Обе последовательности должны быть отсортированы (иначе вывод будет представлять собой мусор), и ни одна из них при использовании merge
не изменяется. Итератор вывода, в который помещаются результаты, должен иметь достаточно места для помещения в него числа элементов, равного сумме длин входных последовательностей. Этого можно добиться, явно зарезервировав достаточно места либо, как это сделано в примере 7.5, использовав back_inserter
:
merge(v1.begin(), v1.end(), v2.begin(), v2.end(),
back_inserter(v3));
back_inserter
— это класс, определенный в
, который предоставляет удобный способ создания выходного итератора, который каждый раз, когда ему присваивается значение, вызывает для последовательности метод push_back
. Таким образом, вам не требуется явно изменять размер выходной последовательности. Следующий вызов создает back_inserter
для vector
с именем v3
.
back_inserter(v3);
Указывать аргументы шаблона не требуется, так как
back_inserter
— это шаблон функции, а не класса, так что тип аргументов, с которыми он вызван, определяется автоматически. Эквивалентный вызов с явным указанием аргументов шаблона выглядит вот так.
back_inserter >(v3);
Однако заметьте, что иногда вам потребуется явно указывать размер выходной последовательности, особенно при использовании в качестве такой последовательности
vector
, vector
при добавлении в него элементов с помощью push_back
может потребовать изменений своего размера, а это очень дорогостоящая операция. За подробностями обратитесь к рецепту 6.2.
Если в последовательностях есть два одинаковых элемента, то элемент из первой последовательности будет предшествовать элементу из второй. Следовательно, если дважды вызвать
merge
, поменяв для второго вызова последовательности местами, результирующие выходные последовательности будут различаться (предсказуемо и правильно, но различаться).
Объединение двух
list
— это хороший пример ситуации, где можно использовать метод последовательности или аналогичный стандартный алгоритм. Следует предпочесть метод стандартному алгоритму, делающему то же самое, но это не всегда работает, и вот пример, который показывает, почему.
Рассмотрим список строк из примера 7.5:
lstStr1.sort(); // Сортируем, или объединение даст мусор!
lstStr2.sort(),
lstStr1.merge(lstStr2); // Это list::merge
Есть две причины, по которым этот код отличается от вызова
std::merge
. Во-первых, оба списка list
должны иметь один и тот же тип элементов. Это требование следует из объявления list::merge
, которое имеет вид:
void merge(list& lst);
template
void merge(list& lst, Compare comp)
Где
T
— это такой же тип, как и в самом шаблоне класса списка. Так что, например, невозможно объединить список из символьных массивов с завершающим нулем со списком из строк типа string
.
Второе отличие состоит в том, что
list::merge
стирает входную последовательность, в то время как std::merge
оставляет две входные последовательности неизменными. Скорее всего list::merge
будет обладать лучшей производительностью, так как в большинстве случаев элементы списка не копируются, а перекомпонуются, но такая перекомпоновка не гарантируется, так что с целью выяснения реального поведения требуются эксперименты.
Также объединить две непрерывные последовательности можно с помощью
inplace_merge
. inplace_merge
отличается от merge
, так как он объединяет две последовательности «на месте». Другими словами, если есть две непрерывные последовательности (т.е. они являются частями одной и той же последовательности) и они отсортированы и требуется отсортировать общую последовательность, то вместо алгоритма сортировки можно использовать inplace_merge
. Преимущество inplace_merge
заключается в том, что при наличии достаточного объема памяти его работа занимает линейное количество времени. Если же памяти недостаточно, то он занимает n log n, что равно средней сложности сортировки.
Объявление
inplace_merge
несколько отличается от merge:
void inplace_merge(Bid first, Bid mid, Bid last);
void inplace_merge(Bid first, Bid mid, Bid last, BinPred comp)
inplace_merge
требует двунаправленных итераторов, так что он не является взаимозаменяемым с merge, но в большинстве случаев должен работать. Как и merge
, для определения относительного порядка элементов он по умолчанию использует operator<
, а при наличии — comp
.
Имеется диапазон элементов, которые требуется отсортировать.
Для сортировки диапазонов имеется целый набор алгоритмов. Можно выполнить обычную сортировку (в восходящем или нисходящем порядке) с помощью
sort
, определенного в
, а можно использовать одну из других функций сортировки, таких как partial_sort
. Посмотрите на пример 7.6, показывающий как это сделать
Пример 7.6. Сортировка
#include
#include
#include
#include
#include
#include
#include
#include "utils.h" // Для printContainer(): см. 7.10
using namespace std;
int main() {
cout << "Введите набор строк: ";
istream_iterator start(cin);
istream_iterator end; // Здесь создается "маркер"
vector v(start, end);
// Стандартный алгоритм sort сортирует элементы диапазона. Он
// требует итератор произвольного доступа, так что он работает для vector.
sort(v.begin(), v.end());
printContainer(v);
random_shuffle(v.begin(), v.end()); // См. 7.2
string* arr = new string[v.size()];
// Копируем элементы в массив
copy(v.begin(), v.end(), &arr[0]);
// Сортировка работает для любого типа диапазонов, но при условии, что
// ее аргументы ведут себя как итераторы произвольного доступа.
sort(&arr[0], &arr[v.size()]);
printRange(&arr[0], &arr[v.size()]);
// Создаем список с такими же элементами
list lst(v.begin(), v.end());
lst.sort(); // Самостоятельная версия sort работать не будет, здесь требуется
// использовать list::sort. Заметьте, что невозможно отсортировать
// только часть списка.
printContainer(lst);
}
Запуск примера 7.6 может выглядеть вот так.
Введите набор строк: a z b y c x d w
^Z
-----
a b c d w x y z
-----
w b y c a x z d
-----
a b c d w x y z
-----
a b c d w x y z
Сортировка — это очень часто выполняющаяся операция, и есть два способа отсортировать последовательность. Можно обеспечить хранение элементов в определенном порядке с помощью ассоциативного контейнера, но при этом длительность операции вставки будет иметь логарифмическую зависимость от размера последовательности. Либо можно сортировать элементы только по мере надобности с помощью
sort
, имеющей несколько опций.
Стандартный алгоритм
sort
делает именно то, что от него ожидается: он сортирует элементы диапазона в восходящем порядке с помощью operator<
. Он объявлен вот так.
void sort(Rnd first, Rnd last);
void sort(Rnd first, Rnd last, BinPred comp);
Как и в большинстве других алгоритмов, если
operator<
не удовлетворяет вашим требованиям, можно указать собственный оператор сравнения. В среднем случае сложность имеет зависимость n log n. В худшем случае она может быть квадратичной.
Если требуется, чтобы одинаковые элементы сохранили свой первоначальный порядок, используйте
stable_sort
. Он имеет такую же сигнатуру, но гарантирует, что порядок эквивалентных элементов изменен не будет. Его сложность при наличии достаточного объема памяти в худшем случае составляет n log n. Если памяти недостаточно, то сложность может оказаться равной n(log n)².
Однако
sort
работает не для всех контейнеров. Он требует итераторов произвольного доступа, так что при использовании контейнера, не предоставляющего таких итераторов, он неприменим. Итераторы произвольного доступа предоставляют стандартные последовательные контейнеры deque
, vector
и string
/wstring
(которые не являются контейнерами, но удовлетворяют большинству требований к ним), list
— это единственный, который такого итератора не предоставляет. Если требуется отсортировать список, используйте list::sort
. Например, в примере 7.6 вы, вероятно, заметили, что list::sort
не принимает никаких аргументов.
lst.sort();
Это отличает его от
std::sort
, с помощью которого можно отсортировать только часть последовательности. Если требуется отсортировать часть последовательности, то не следует использовать list
.
Концепция сортировки очень проста, но есть несколько вариаций на тему ее реализации в стандартной библиотеке. Следующий перечень описывает эти вариации.
partial_sort
Принимает три итератора произвольного доступа —
first
, middle
и last
— и (необязательно) функтор сравнения. Он имеет два постусловия: элементы диапазона (first
, middle
) будут меньше, чем элементы диапазона (middle
, last
), и диапазон (first
, middle
) будет отсортирован с помощью operator<
или указанного функтора сравнения. Другими словами, он сортирует только первые n элементов.
partial_sort_сору
Делает то же, что и
partial_sort
, но помещает результаты в выходной диапазон. Он берет первые n элементов из исходного диапазона и в соответствующем порядке копирует их в результирующий диапазон. Если результирующий диапазон (n) короче, чем исходный диапазон (m), то в результирующий диапазон копируется только n элементов.
nth_element
Принимает три итератора произвольного доступа —
first
, nth
и last
— и необязательный функтор сравнения. Он помешает элемент, на который ссылается nth
, в то место, где он находился бы, если бы весь диапазон был отсортирован. Следовательно, все элементы диапазона (first
, nth
) будут меньше, чем элемент в позиции nth
(те, что находятся в диапазоне (nth
, last
) не сортируются, но больше, чем те, что предшествуют nth
). Этот алгоритм следует использовать тогда, когда требуется отсортировать только один или несколько элементов диапазона и избежать затрат на сортировку всего диапазона.
Также можно разделить элементы диапазона в соответствии с каким-либо критерием (функтором), и это является предметом обсуждения рецепта 7.7.
Рецепт 7.7.
Имеется диапазон элементов, которые требуется каким-либо образом разделить на группы. Например, необходимо переместить в начало диапазона все элементы, которые меньше определенного значения.
Для перемещения элементов используйте стандартный алгоритм
partition
с предикатом-функтором. См. пример 7.7.
Пример 7.7. Разделение диапазона
#include
#include
#include
#include
#include
#include
#include
#include "utils.h" // Для printContainer(): см. рецепт 7.10
using namespace std;
int main() {
cout << "Введите набор строк: ";
istream_iterator start(cin);
istream_iterator end; // Здесь создается "маркер"
vector v(start, end);
// Реорганизуем элементы в v так, чтобы те, которые меньше,
// чем "foo", оказались перед остальными.
vector::iterator p =
partition(v.begin(), v.end(),
bind2nd(less(), "foo"));
printContainer(v);
cout << "*p = " << *p << endl;
}
Вывод примера 7.7 выглядит примерно так.
Введите набор строк: a d f j k l
^Z
-----
a d f j k l
*p = j
После работы
partition
итератор p
указывает на первый элемент, для которого less(*p, "foo")
не равно true
.
partition
принимает начало и конец диапазона и предикат и перемешает все элементы, для которых предикат равен true
, в начало диапазона. Он возвращает итератор, указывающий на первый элемент, для которого предикат не равен true
, или на конец диапазона, если все элементы удовлетворяют предикату. Он объявлен вот так.
Bi partition(Bi first, Bi last, Pred pred);
pred
— это функтор, который принимает один аргумент и возвращает true
или false
. Предиката по умолчанию не существует — вы должны указать такой предикат, который удовлетворяет требованию разделения диапазона. При этом можно написать свой предикат, а можно использовать один из предикатов стандартной библиотеки. Например, в примере 7.7 можно видеть, что я для создания функтора использовал less
и bind2nd
.
vector::iterator p =
partition(v.begin(), v.end(),
bind2nd(less(), "foo"));
Здесь все элементы, которые меньше
"foo"
, перемещаются в начало последовательности. bind2nd
здесь необязателен, но он удобен для автоматического создания функтора, который принимает один аргумент и возвращает результат вычисления less(*i, "foo")
для каждого i-го элемента последовательности. Если требуется, чтобы одинаковые элементы сохранили свой первоначальный порядок, то следует использовать stable_partition
.
partition
и другие алгоритмы, которые меняют порядок элементов диапазона, не работают со стандартными ассоциативными контейнерами set
, multiset
, map
и multimap
. Причиной этого является то, что ассоциативные контейнеры хранят свои элементы в упорядоченном виде и перемещать и удалять элементы разрешается только самим контейнерам. Использовать partition
можно с любым диапазоном, для которого можно получить, по крайней мере, двунаправленный итератор, и это выполняется для всех стандартных последовательных контейнеров, включая deque
, vector
и list
.
Рецепт 7.9.
Имеются последовательности, которые требуется реорганизовать с помощью операций над множествами, таких как объединение (union), различие (difference) или пересечение (intersection).
Для этой цели используйте специальные функции стандартной библиотеки.
set_union
, set_difference
и set_intersection
. Каждая из них выполняет соответствующую операцию над множеством и помещает результат в выходной диапазон. Их использование показано в примере 7.8.
Пример 7.8. Использование операций над множествами
#include
#include
#include
#include
#include
#include "utils.h" // Для printContainer(): см. 7.10
using namespace std;
int main() {
cout << "Введите несколько строк: ";
istream_iterator start(cin);
istream_iterator end;
set s1(start, end);
cin.clear();
cout << "Введите еще несколько строк: ";
set s2(++start, end);
set setUnion;
set setInter;
set setDiff;
set_union(s1.begin(), s1.end(), s2.begin(), s2.end(),
inserter(setUnion, setUnion.begin()));
set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(),
inserter(setDiff, setDiff.begin()));
set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
inserter(setInter,setInter.begin()));
cout << "Объединение:\n";
printContainer(setUnion);
cout << "Различие:\n";
printContainer(setDiff);
cout << "Пересечение:\n";
printContainer(setInter);
}
Вывод этой программы выглядит примерно так (
printContainer
просто печатает содержимое контейнера).
Введите несколько строк: a b c d
^Z
Введите еще несколько строк: d е f g
^Z
Объединение: a b с d e f g
Различие: a b c
Пересечение: d
Операции с множествами в стандартной библиотеке выглядят и работают сходным образом. Каждая принимает два диапазона, выполняет свою операцию с ними и помешает результаты в выходной итератор. Вы должны убедиться, что для выходной последовательности имеется достаточно места, или использовать
inserter
или back_inserter
(как использовать back_inserter
, рассказывается в рецепте 7.5).
Объявление
set_union
выглядит вот так.
Out set_union(In first1, In last1, In first2, In last2, Out result);
Объявления
set_difference
, set_intersection
и set_symmetric_difference
выглядят точно так же.
Чтобы использовать эти функции, сделайте так, как показано в примере 7.8. Например, чтобы найти пересечение двух множеств, вызовите
set_intersection
вот так.
set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
inserter(setInter, setInter.begin()));
Последний аргумент
set_intersection
требует некоторых пояснений, inserter
— это шаблон функции, определенный в
, который принимает контейнер и итератор и возвращает выходной итератор, который при записи в него значения вызывает для первого аргумента inserter
метод insert
. При его использовании для последовательного контейнера он вставляет значения перед iterator
, переданным в качестве второго аргумента. При его использовании для ассоциативного контейнера, как это делается в показанном выше фрагменте кода, этот итератор игнорируется, и элементы вставляются в соответствии с критерием сортировки контейнера.
set
— это удобный пример для наших целей, но операции над множествами работают для любых последовательностей, а не только для set
. Например, операции над множествами можно выполнить для list
:
list lst1, lst2, lst3;
// Заполняем их данными
lst1.sort(); // Элементы должны быть отсортированы
lst2.sort();
set_symmetric_difference(lst1 begin(), lst1.end(),
lst2.begin(), lst2.end(), back_inserter(lst3));
Однако так как
list
хранит данные в неотсортированном виде, его вначале требуется отсортировать иначе результаты операций над множествами будут неверными. Также обратите внимание, что в этом примере вместо inserter
используется back_inserter
. back_inserter
работает аналогично inserter
, за исключением того, что для добавления элементов в контейнер он использует push_back
. Вы не обязаны действовать точно так же. Например, можно изменить размер выходного контейнера так, чтобы он стал достаточно большим
lst3.resize(lst1.size() + lst2.size()),
set_symmetric_difference(lst1.begin(), lst1.end(),
lst2.begin(), lst2.end(), lst3.begin())
;
Если выходная последовательность будет достаточно большой, то можно просто передать итератор, указывающий на первый элемент последовательности, используя
begin
.
Если вы не знаете, что такое
set_symmetric_difference
, я вам расскажу. Это объединение разностей двух множеств, определенных в прямом и обратном порядке. Это значит, что если а и b — это множества, то симметричная разность — это а - b b - а. Другими словами, симметричная разность — это множество всех элементов, которые присутствуют в одном из множеств, но отсутствуют в другом.
Есть еще один момент, который следует знать при работе с операциями над множествами. Так как последовательности не обязаны быть уникальными, можно получить «множество» с повторяющимися значениями. Конечно, строго математически множество не может содержать повторяющиеся значения, так что этот момент может быть не очевиден, Рассмотрим, как будет выглядеть вывод примера 7.8, если вместо
set
использовать list
(при запуске примера 7.8 можно вводить повторяющиеся значения, но они не будут добавлены в set
, так как set::insert
не выполняется для элементов, которые уже присутствуют в set
).
Введите несколько строк: a a a b с с
^Z
Введите еще несколько строк: a a b b с
^Z
Объединение a a a b b с с
Различие: a c
Пересечение: a a b с
Здесь операции над множествами перебирают обе последовательности и сравнивают соответствующие значения, определяя, что следует поместить в выходную последовательность.
Наконец, операции над множествами в их оригинальном виде (использующие для сравнения элементов
operator<
) могут не работать так, как вам требуется, если последовательности содержат указатели. Чтобы обойти эту проблему, напишите функтор, который сравнивает объекты указателей, как в рецепте 7.4.
Рецепт 7.4.
Имеется последовательность элементов, и с каждым элементом требуется выполнить какие-либо действия — либо на месте, либо скопировав их в другую последовательность.
Используйте стандартные алгоритмы
transform
или for_each
. Они оба просты, но позволяют выполнить почти любые действия с элементами последовательностей. Пример 7.9 показывает, как это делается.
Пример 7.9. Преобразование данных
#include
#include
#include
#include
#include
#include
#include
#include "utils.h" // Для printContainer(): см. 7.10
using namespace std;
// Преобразуем строки к верхнему регистру
string strToUpper(const string& s) {
string tmp;
for (string::const_iterator p = s.begin(); p != s.end(); ++p)
tmp += toupper(*p);
return(tmp);
}
string strAppend(const string& s1, const string& s2) {
return(s1 + s2);
}
int main() {
cout << "Введите несколько строк: ";
istream_iterator start(cin);
istream iterator end;
list lst(start, end), out;
// Используем преобразование с помощью унарной функции...
transform(lst.begin(), lst.end(), back_inserter(out),
strToUpper);
printContainer(out);
cin.clear();
cout << Введите другой набор строк: ";
list lst2(++start, end);
out.clear();
// ...или бинарную функцию и другую входную последовательность
transform(lst.begin(), lst.end(), lst2.begin(),
back_inserter(out), StrAppend);
printContainer(out);
}
Очевидно, что для преобразования данных используется
transform
. Он имеет две формы. Первая форма принимает последовательность, итератор вывод и унарный функтор. Он применяет функтор к каждому элементу последовательности и присваивает возвращаемое значение следующему значению, на которое указывает итератор вывода. Итератор вывода может быть другой последовательностью или началом оригинальной последовательности. В этом отношении transfоrm
может выполнять преобразование как «на месте», так и копируя результат в другую последовательность.
Вот как выглядит объявление
transform
.
Out transform(In first, In last, Out result, UnFunc f);
Out transform(In first1, In last1, In first2, In last2,
Out result, BinFunc f);
Обе версии возвращают итератор, который указывает на один после конца результирующей последовательности.
Использование обеих версий очень просто. Чтобы скопировать строку из одной последовательности в другую, но с преобразованием ее к верхнему регистру, сделайте так, как в примере 7.9.
std::transform(lst.begin(), lst.end(),
std::back_inserter(out), strToUpper);
Если требуется изменить первоначальную последовательность, просто передайте в качестве результирующего итератора начало этой последовательности.
std::transform(lst.begin(), lst.end(),
lst.begin(), strToUpper);
Использование двух последовательностей и бинарной операции работает точно так же. и в качестве выходной последовательности можно использовать одну из входных.
Если требуется преобразовать элементы на месте, можно избежать накладных расходов на присвоение каждому элементу возвращаемого значения некоторой функции. Или, если функтор, который требуется использовать, изменяет свой объект-источник, то можно использовать
for_each
.
void strToUpperInPlace(string& s) {
for (string::iterator p = s.begin(); p != s.end(); ++p)
*p = std::toupper(*p);
}
// ...
std::for_each(lst.begin(), lst.end(), strToUpperInPlace);
Если же все, что требуется сделать, — это изменить саму последовательность, не изменяя каждый из ее элементов, то в рецепте 7.6 описывается множество стандартных алгоритмов для реорганизации элементов в последовательностях.
Рецепты 7.1 и 7.6.
Для диапазона требуется выполнить алгоритм, и ни один из стандартных алгоритмов не удовлетворяет требованиям.
Напишите алгоритм в виде шаблона функции и с помощью имен параметров шаблона укажите свои требования к итератору. В примере 7.10 показан измененный стандартный алгоритм
сору
.
Пример 7.10. Написание собственного алгоритма
#include
#include
#include
#include
#include
#include
#include
#include "utils.h" // Для printContainer(): см. 7.10
using namespace std;
template
Out copyIf(In first, In last, Out result, UnPred pred) {
for ( ; first != last; ++first)
if (pred(*first)) *results = *first;
return(result);
}
int main() {
cout << "Введите несколько строк: ";
istream_iterator start(cin);
istream_iterator end; // Здесь создается "маркер"
vector v(start, end);
list lst;
copyIf(v.begin(), v.end(), back_inserter >(lst),
bind2nd(less(), "cookie"));
printContainer(lst);
}
Запуск примера 7.10 будет выглядеть примерно так.
Введите несколько строк: apple banana danish eclaire
^Z
-----
apple banana
Вы видите, что он копирует в результирующий диапазон только те значения, которые меньше, чем «cookie».
Стандартная библиотека содержит шаблон функции
сору
, который копирует элементы из одного диапазона в другой, но нет стандартной версии, которая принимает предикат и выполняет условное копирование элементов (т.е. алгоритм copy_if
), так что пример 7.10 делает именно это. Его поведение довольно просто: при наличии диапазона-источника и начала диапазона-приемника производится копирование в целевой диапазон элементов, для которых унарный функтор-предикат возвращает true
.
Этот алгоритм прост, но в его реализации есть еще кое-что, что привлекает внимание. Посмотрев на объявление, вы увидите, что в нем присутствует три параметра шаблона.
template
Out copyIf(In first, In last, Out result UnPred pred) {
Первый параметр шаблона
In
— это тип входного итератора. Так как это входной диапазон, все, что должен иметь возможность сделать с ним copyIf
, — это извлечь разыменованное значение этого итератора и перевести итератор на следующий элемент. Это дает описание категории итератора ввода (категории итераторов описаны в рецепте 7.1), так что с помощью указания имени параметра шаблона In
мы объявляем именно этот тип итератора. Стандартного соглашения здесь нет (In
и Out
— это мои соглашения, которые я описал в первом рецепте этой главы), но вы легко можете придумать свои собственные соглашения об именах: InIter
, Input_T
или даже InputIterator.
Второй параметр шаблона Out
— это тип итератора, который указывает на диапазон, в который будут копироваться элементы, copyIf
должен иметь возможность записать разыменованное значение в выходной итератор и увеличить его значение, что дает нам описание оператора вывода. Объявив требования к итераторам с помощью имен параметров шаблона, вы делаете соглашения о вызовах алгоритма понятными без документации. Но зачем использовать две разные категории итераторов?
Имеется, по крайней мере, две причины использования в
copyIf
двух различных категорий итераторов. Во-первых, операции с каждым диапазоном несколько отличаются друг от друга, и так как мне никогда не потребуется возвращаться назад по входному диапазону или присваивать ему значения, все, что мне требуется, — это итератор ввода. Аналогично мне никогда не потребуется читать из выходного диапазона, так что все, что здесь требуется, — это итератор вывода. Имеются требования к каждому из итераторов, которые не применимы к другому итератору, так что нет никакого смысла использовать для обоих диапазонов, например, два двунаправленных итератора. Во-вторых, использование различных типов итераторов позволяет вызывающему коду читать из одного типа диапазона и записывать в другой. В примере 7.10 я читаю из vector
и записываю в list
.
vector v(start, end);
list lst;
copyIf(v.begin(), v.end(), back_inserter >(lst),
bind2nd(less(), "cookie"));
Если попробовать сделать то же самое, использовав в алгоритме один и тот же тип итераторов, то он просто не скомпилируется.
В примере 7.10 я в качестве начала выходного диапазона передаю
back_inserter
, а не, скажем, итератор, возвращаемый lst.begin
. Это делается потому, что lst не содержит элементов, и в этом алгоритме (как и в стандартном алгоритме копирования) целевой диапазон должен быть достаточно большим, чтобы вместить все элементы, которые будут в него скопированы. В противном случае увеличение итератора вывода в copyIf
приведет к неопределенному поведению. back_inserter
возвращает итератор вывода, который при его увеличении вызывает для контейнера метод push_back
. В результате этого при каждом увеличении выходного итератора размер lst
увеличивается на один. Более подробно шаблон класса back_inserter
я описываю в рецепте 7.5.
При написании собственного алгоритма для работы с диапазонами (т.е. со стандартными контейнерами) вы должны работать с аргументами-итераторами, а не с аргументами-контейнерами. У вас может возникнуть желание объявить
copyIf
так, чтобы он принимал два контейнера, а не итератор исходного и результирующего диапазонов, но это менее обобщенное решение, чем диапазоны. Во-первых, если передавать аргументы-контейнеры, то станет невозможно работать с подмножеством элементов контейнера. Далее, в теле copyIf
появится зависимость от методов контейнеров begin
и end
, которые дадут требуемый диапазон, и возвращаемый тип будет зависеть от типа контейнера, используемого в качестве выходного. Это означает, что использование в copyIf
нестандартных диапазонов, таких как встроенные массивы или собственные контейнеры, работать не будет. Именно по этим и некоторым другим причинам все стандартные алгоритмы оперируют с диапазонами.
Наконец, если вы пишете свой алгоритм, дважды убедитесь, что стандартные алгоритмы вас не устраивают. На первый взгляд они могут казаться очень простыми алгоритмами, но их кажущаяся простота проистекает из их обобщенности, и в девяти случаях из десяти их можно расширить так, что они подойдут для новых задач. Иногда следует стремиться к повторному использованию стандартных алгоритмов, так как это дает гарантию переносимости и эффективности.
Рецепт 7.5.
Имеется диапазон элементов, который требуется напечатать в поток, например, в
cout
с целью отладки.
Напишите шаблон функции, который принимает диапазон или контейнер, перебирает все его элементы и использует алгоритм
сору
и ostream_iterator
для записи. Если требуется дополнительное форматирование, напишите свой простой алгоритм, который перебирает диапазон и печатает каждый элемент в поток. (См. пример 7.11)
Пример 7.11. Печать диапазона в поток
#include
#include
#include
#include
#include
using namespace std;
int main() {
// Итератор ввода - это противоположность итератору вывода: он
// читает элементы из потока так. как будто это контейнер.
cout << "Введите несколько строк: ";
istream_iterator start(cin);
istream_iterator end;
vector v(start, end);
// Используем выходной поток как контейнер, используя
// output_iterator. Он создает итератор вывода, для которого запись
// в каждый элемент эквивалентна записи в поток.
copy(v.begin(), v.end(), ostreamIterator(cout, ", "));
}
Вывод примера 7.11 может выглядеть так.
Введите несколько строк: z x y a b с
^Z
z, x, y, a, b, с,
Потоковый итератор — это итератор, который основан на потоке, а не на диапазоне элементов контейнера, и позволяет рассматривать поток как итератор ввода (читать из разыменованного значения и увеличивать итератор) или итератор вывода (аналогично итератору ввода, но для записи в разыменованное значение вместо чтения из него). Это облегчает чтение значений (особенно строк) из потока, что делается в нескольких других примерах этой главы, и запись значений в поток, что делается в примере 7.11. Я знаю, что этот рецепт посвящен записи диапазона в поток, но позвольте мне немного отойти от этой задачи и, поскольку я использую потоковые итераторы во многих примерах этой главы, объяснить, что это такое.
В примере 7.11 показаны три ключевые части
istream_iterator
. Первая часть — это создание istream_iterator
, указывающего на начало потокового ввода. Это делается вот так.
istream_iterator start(cin);
В результате создается итератор с именем
start
, который указывает на первый элемент входной последовательности, точно так же, как vec.begin
(vec
— это vector
) возвращает итератор, который указывает на первый элемент в векторе. Аргумент шаблона string
говорит istream_iterator
, что элементы в этой последовательности имеют тип string
. Аргумент конструктора cin
— это входной поток, из которого производится чтение. Однако это абстракция, так как первого элемента не существует, поскольку из cin
еще ничего прочитано не было. Это произойдет несколько позже.
Вторая часть итератора входного потока — это маркер конца, который создается вот так.
istream_iterator end;
Стандартные контейнеры используют специальное значение «один после конца», указывающее на точку, где должно остановиться использование алгоритма. Так как итератор входного потока не имеет в памяти последнего элемента, он для создания логической конечной точки, представляющей точку остановки использования алгоритма, использует конструктор без аргументов.
Последней частью методики использования
istream_iterator
является его использование для извлечения значений. Удобным способом вытащить в контейнер все значения, введенные в поток, является использование конструктора диапазона контейнера. Например, если создать vector
с двумя итераторами, то его конструктор скопирует в контейнер все элементы диапазона, определяемого итераторами. Если передать только что созданные итераторы start
и end
, то это будет выглядеть так.
vector v(start, end);
Именно здесь происходит чтение значений из потока. При создании
v
он начинает со start
и перебирает все значения, пока не достигнет end
. Каждый раз, когда v
читает из *start
, происходит нечто эквивалентное такому вызову cin
.
cin >> v[i]; // v - это vector
Другими словами, следующее значение, извлекаемое из
cin
, преобразуется в string
и вставляется в vector
.
При использовании
cin
как входного потока маркер конца файла, который отмечает конец потока, определяется используемой платформой. В Windows для завершения входного потока требуется нажать на Enter, Ctrl-Z, Enter. Чтобы увидеть, что требуется сделать на вашей платформе, проведите эксперименты, но велика вероятность, что будут использоваться эти же клавиши.
Итераторы выходных потоков ведут себя аналогично итераторам потоков ввода. В примере 7.11 я копирую значения из своего
vector
в cout
, создав для этого ostream_iterator
, который указывает на cout
, следующим образом.
copy(v.begin(), v.end(), ostream_iterator(cout, ", "));
Аргумент шаблона
ostream_iterator
говорит, что записываемые элементы будут иметь тип string
. Первый аргумент конструктора ostream_iterator
— это поток, в который будет производиться запись (и который может быть любым потоком вывода, включая ofstream
и ostringstream
), а второй это используемый разделитель. Это дает удобный способ выводить диапазон значений на стандартный вывод, что я часто делаю при отладке.
Если требуется дополнительное управление внешним видом вывода, например вывод последовательности в квадратных или фигурных скобках или отсутствие последнего разделителя в конце последовательности, то это потребует всего нескольких дополнительных строк кода. Пример 7.12 показывает тело
printContainer
и printRange
, первая из которых используется в примерах этой главы.
Пример 7.12. Написание собственной функции печати
#include
#include
#include
#include
#include
using namespace std;
template
void printContainer(const C& c, char delim = ',', ostream& out = cout) {
printRange(c.begin(), c.end(), delim, out);
}
template
void printRange(Fwd first, Fwd last, char delim = ',', ostream& out = cout) {
out << "{";
while (first != last) {
out << *first;
if (++first != last)
out << delim << ' ';
}
out << "}" << endl;
}
int main() {
cout << "Введите набор строк: ";
istream_iterator start(cin);
istream_iterator end;
vector v(start, end);
printContainer(v);
printRange(v.begin(), v.end(), ';', cout);
}
Функция
printRange
представляет собой более общий подход, так как оперирует с диапазонами (более подробно это объясняется в рецепте 7.10), но printContainer
более удобна для печати целого контейнера. Имеется множество других способов сделать это. В голову также приходит определение версии operator<<
, которая бы работала с выходным потоком и контейнером, и использование стандартного алгоритма for_each
с собственным функтором для записи элементов в поток.