EXPLORATION 43

image

Useful Algorithms

The standard library includes a suite of functions, which the library calls algorithms, to simplify many programming tasks that involve repeated application of operations over data ranges. The data can be a container of objects, a portion of a container, values read from an input stream, or any other sequence of objects that you can express with iterators. I’ve introduced a few algorithms when appropriate. This Exploration takes a closer look at a number of the most useful algorithms.

Searching

The standard algorithms include many flavors of searching, divided into two broad categories: linear and binary. The linear searches examine every element in a range, starting from the first and proceeding to subsequent elements until reaching the end (or the search ends because it is successful). The binary searches require the elements be sorted in ascending order, using the < operator, or according to a custom predicate, that is, a function, functor, or lambda that returns a Boolean result.

Linear Search Algorithms

The most basic linear search is the find function. It searches a range of read iterators for a value. It returns an iterator that refers to the first matching element in the range. If find cannot find a match, it returns a copy of the end iterator. Listing 43-1 shows an example of its use. The program reads integers into a vector, searches for the value 42, and if found, changes that element to 0.

Listing 43-1.  Searching for an Integer

#include <algorithm>
#include <iostream>
 
#include "data.hpp"
 
int main()
{
  intvector data{};
  read_data(data);
  write_data(data);
  auto iter(std::find(data.begin(), data.end(), 42));
  if (iter == data.end())
    std::cout << "Value 42 not found ";
  else
  {
    *iter = 0;
    std::cout << "Value 42 changed to 0: ";
    write_data(data);
  }
}

Listing 43-2 shows the data.hpp file, which provides a few utilities for working with vectors of integers. Most of the examples in this Exploration will #include this file.

Listing 43-2.  The data.hpp File to Support Integer Data

#ifndef DATA_HPP_
#define DATA_HPP_
 
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
 
/// Convenient shorthand for a vector of integers.
typedef std::vector<int> intvector;
 
/// Convenient shorthand for an intvector's iterator.
typedef intvector::iterator intvec_iterator;
 
/// Read a series of integers from the standard input into @p data,
/// overwriting @p data in the process.
/// @param[in,out] data a vector of integers
inline void read_data(intvector& data)
{
  data.clear();
  data.insert(data.begin(), std::istream_iterator<int>(std::cin),
                            std::istream_iterator<int>());
}
 
/// Write a vector of integers to the standard output. Write all values on one
/// line, separated by single space characters, and surrounded by curly braces,
/// e.g., { 1 2 3 }.
/// @param data a vector of integers
inline void write_data(intvector const& data)
{
  std::cout << "{ ";
  std::copy(data.begin(), data.end(),
            std::ostream_iterator<int>(std::cout, " "));
  std::cout << "} ";
}
 
#endif

A companion to the find algorithm is find_if. Instead of searching for a matching value, find_if takes a predicate function or function object (from now on, I will write functor to indicate a free function, a function object, or a lambda). It calls the functor for every element in the range, until the functor returns true (or any value that can be converted automatically to true, such as a nonzero numeric value). If the functor never returns true, find_if returns the end iterator.

Every search algorithm comes in two forms. The first compares items using an operator (== for linear searches and < for binary searches). The second form uses a caller-supplied functor instead of the operator. For most algorithms, the functor is an additional argument to the algorithm, so the compiler can distinguish the two forms. In a few cases, both forms take the same number of arguments, and the library uses distinct names, because the compiler could not otherwise distinguish between the two forms. In these cases, the functor form has _if added to the name, such as find and find_if.

Suppose you want to search a vector of integers, not for a single value, but for any value that falls within a certain range. You can write a custom predicate to test a hard-coded range, but a more useful solution is to write a general-purpose functor that compares an integer against any range. Use this functor by supplying the range limits as argument to the constructor. Is this best implemented as a free function, function object, or lambda? ________________________ Because it must store state, I recommend writing a functor.

A lambda is good when you need to search for specific values, but a functor is easier if you want to write a generic comparator that can store the limits. Write the intrange functor. The constructor takes two int arguments. The function call operator takes a single int argument. It returns true, if the argument falls within the inclusive range specified in the constructor, or false, if the argument lies outside the range.

Listing 43-3 shows my implementation of intrange. As a bonus, I decided to allow the caller to specify the range limits in either order. That way, I neatly avoid the issue of error-checking and error-handling, if the caller tries to use a meaningless range, such as [10, 0].

Listing 43-3.  Functor intrange to Generate Integers in a Certain Range

#ifndef INTRANGE_HPP_
#define INTRANGE_HPP_
 
#include <algorithm>
 
/// Check whether an integer lies within an inclusive range.
class intrange
{
public:
  inline intrange(int low, int high);
  inline bool operator()(int test) const;
private:
  int const low_;
  int const high_;
};
 
/// Construct an integer range.
/// If the parameters are in the wrong order,
/// swap them to the right order.
/// @param low the lower bound of the inclusive range
/// @param high the upper bound of the inclusive range
inline intrange::intrange(int low, int high)
: low_{std::min(low, high)}, high_{std::max(low, high)}
{}
 
/// Check whether a value lies within the inclusive range.
/// @param test the value to test
inline bool intrange::operator()(int test)
const
{
  return test >= low_ and test <= high_;
}
 
#endif

The < operator form of the std::min function takes two arguments and returns the smaller. The std::max function also takes two arguments and returns the larger. Both functions compare their arguments with the < operator. As with other algorithms, you can call a functor form of both functions, passing a comparison functor to use instead of the < operator. The types of the first two arguments must be the same, and the return type matches that of the arguments. The mixmax function returns a std::pair (introduced in Exploration 15) of the minimum and maximum.

Write a test program that reads integers from the standard input and then uses find_if and intrange to find the first value that lies within the range [10, 20]. Compare your solution with mine in Listing 43-4.

Listing 43-4.  Using find_if and intrange to Find an Integer That Lies Within a Range

#include <algorithm>
#include <iostream>
 
#include "data.hpp"
#include "intrange.hpp"
 
int main()
{
  intvector data{};
  read_data(data);
  write_data(data);
  auto iter(std::find_if(data.begin(), data.end(), intrange{10, 20}));
  if (iter == data.end())
    std::cout << "No values in [10,20] found ";
  else
    std::cout << "Value " << *iter << " in range [10,20]. ";
}

A few of the following examples generate random data and apply algorithms to the data. The standard library has a rich, complicated library for generating pseudo-random numbers. The details of this library are beyond the scope of this book. Only the mathematically adventuresome should crack open the details of the <random> header. For your convenience, Listing 43-5 presents the randomint.hpp header, which defines the randomint class, which generates random integers in a caller-supplied range.

Listing 43-5.  Generating Random Integers

#ifndef RANDOMINT_HPP_
#define RANDOMINT_HPP_
 
#include <algorithm>
#include <random>
 
/// Generate uniformly distributed random integers in a range.
class randomint
{
public:
  typedef std::default_random_engine::result_type result_type;
 
  /// Construct a random-number generator to produce numbers in the range [<tt>low</tt>, <tt>high</tt>].
  /// If @p low > @p high the values are reversed.
  randomint(result_type low, result_type high)
     // std::random_device uses a system-dependent generation of randomness
      // to seed the pseudo-random-number generator.
  : prng_{std::random_device{}()},
    distribution_{std::min(low, high), std::max(low, high)}
  {}
 
  /// Generate the next random number generator.
  result_type operator()()
  {
     return distribution_(prng_);
  }
 
private:
  // implementation-defined pseudo-random-number generator
  std::default_random_engine prng_;
  // Map random numbers to a uniform distribution.
  std::uniform_int_distribution<result_type> distribution_;
};
#endif

The search function is similar to find, except it searches for a matching sub-range. That is, you supply an iterator range to search and an iterator range to match. The search algorithm looks for the first occurrence of a sequence of elements that equals the entire match range. Listing 43-6 shows a silly program that generates a large vector of random integers in the range 0 to 9 and then searches for a sub-range that matches the first four digits of π.

Listing 43-6.  Finding a Sub-range That Matches the First Four Digits of π

#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
 
#include "data.hpp"
#include "randomint.hpp"
 
int main()
{
  intvector pi{ 3, 1, 4, 1 };
  intvector data(10000);
  // The randomint functor generates random numbers in the range [0, 9].
 
  std::generate(data.begin(), data.end(), randomint{0, 9});
 
  auto iter(std::search(data.begin(), data.end(), pi.begin(), pi.end()));
  if (iter == data.end())
    std::cout << "The integer range does not contain the digits of pi. ";
  else
  {
    std::cout << "Easy as pi: ";
    std::copy(iter, iter+pi.size(), std::ostream_iterator<int>(std::cout, " "));
    std::cout << ' ';
  }
}

Binary Search Algorithms

The map container stores its elements in sorted order, so you can use any of the binary search algorithms, but map also has member functions that can take advantage of access to the internal structure of a map and, so, offer improved performance. Thus, the binary search algorithms are typically used on sequential containers, such as vector, when you know that they contain sorted data. If the input range is not properly sorted, the results are undefined: you might get the wrong answer; the program might crash; or something even worse might happen.

The binary_search function simply tests whether a sorted range contains a particular value. By default, values are compared using only the < operator. Another form of binary_search takes a comparison functor as an additional argument to perform the comparison.

WHAT’S IN A NAME?

The find function performs a linear search for a single item. The search function performs a linear search for a matching series of items. So why isn’t binary_search called binary_find? On the other hand, find_end searches for the match that is furthest right in a range of values, so why isn’t it called search_end? The equal function is completely different from equal_range, in spite of the similarity in their names.

The C++ standards committee did its best to apply uniform rules for algorithm names, such as appending _if to functions that take a functor argument but cannot be overloaded, but it faced some historical constraints with a number of names. What this means for you is that you have to keep a reference close at hand. Don’t judge a function by its name, but read the description of what the function does and how it does it, before you decide whether it’s the right function to use.

The lower_bound function is similar to binary_search, except it returns an iterator. The iterator points to the first occurrence of the value, or it points to a position where the value belongs if you want to insert the value into the range and keep the values in sorted order. The upper_bound function is similar to lower_bound, except it returns an iterator that points to the last position where you can insert the value and keep it in sorted order. If the value is found, that means upper_bound points to one position past the last occurrence of the value in the range. To put it another way, the range [lower_bound, upper_bound]) is the sub-range of every occurrence of the value in the sorted range. As with any range, if lower_bound == upper_bound, the result range is empty, which means the value is not in the search range.

Listing 43-7 shows a variation on Listing 43-1, sorting the integer vector and searching for a value using lower_bound to perform a binary search.

Listing 43-7.  Searching for an Integer Using Binary Search

#include <algorithm>
#include <iostream>
 
#include "data.hpp"
 
int main()
{
  intvector data{};
  read_data(data);
  std::sort(data.begin(), data.end());
  write_data(data);
  intvec_iterator iter{std::lower_bound(data.begin(), data.end(), 42)};
  if (iter == data.end())
    std::cout << "Value 42 not found ";
  else
  {
    *iter = 0;
    std::cout << "Value 42 changed to 0: ";
    write_data(data);
  }
}

Only two lines changed: adding one extra line to sort the vector and changing find to lower_bound. To better understand how lower_bound and upper_bound really work, it helps to write a test program. The program reads some integers from the user into a vector, sorts the vector, and clears the I/O state bits on the standard input (std::cin.clear()), so you can enter some test values. The program then repeatedly asks for integers from the user and searches for each value using lower_bound and upper_bound. To help you understand exactly what these functions return, call the distance function to determine an iterator’s position in a vector, as follows:

intvec_iterator iter{std::lower_bound(data.begin(), data.end(), 42)};
std::cout << "Index of 42 is " << std::distance(data.begin(), iter) << ' ';

The distance function (declared in <iterator>) takes an iterator range and returns the number of elements in the range. The return type is the iterator’s difference_type, which is just an integer type, although the exact type (e.g., int or long int) depends on the implementation.

Write the test program. Then run the program with the following sample input:

9 4 2 1 5 4 3 6 2 7 4

What should the program print as the sorted vector?

_____________________________________________________________

Fill in Table 43-1 with the expected values for the lower and upper bounds of each value. Then run the program to check your answers.

Table 43-1. Results of Testing Binary Search Functions

Tab43-1.jpg

Compare your test program with mine in Listing 43-8.

Listing 43-8.  Exploring the lower_bound and upper_bound Functions

#include <algorithm>
#include <iostream>
#include <vector>
 
#include "data.hpp"
 
int main()
{
  intvector data{};
  read_data(data);
  std::sort(data.begin(), data.end());
  write_data(data);
 
  std::cin.clear();
  int test{};
  while (std::cin >> test)
  {
    intvec_iterator lb{std::lower_bound(data.begin(), data.end(), test)};
    intvec_iterator ub{std::upper_bound(data.begin(), data.end(), test)};
    std::cout << "lower bound = " << std::distance(data.begin(), lb) << ' ' <<
                 "upper bound = " << std::distance(data.begin(), ub) << ' ';
  }
}

When you want the lower bound and upper bound of the same range, call equal_range, which returns a pair of iterators. The first member of the pair is the lower bound, and the second is the upper bound.

Other useful linear functions include count, which takes an iterator range and value and returns the number of occurrences of the value in the range. Its counterpart count_if takes a predicate instead of a value and returns the number of times the predicate returns true.

Three more algorithms have a common pattern. They apply a predicate to every element in a range and return a bool.

  • all_of(first, last, predicate) returns true if predicate(element) returns true for every element in the range [first, last].
  • any_of(first, last, predicate) returns true if predicate(element) returns true for at least one element in the range [first, last].
  • none_of(first, last, predicate) returns true if predicate(element) returns false for every element in the range [first, last].

You have already seen how min returns the minimum of two values. It has a partner, min_element, which takes an iterator range and returns the iterator for the minimum value in the range. Ditto for max_element. You can guess what minmax_element returns: a pair of iterators for the minimum and maximum values in the range. All three come in the usual overloaded forms: one uses the < operator, and the other takes an additional argument for a comparison predicate.

Unlike all the other algorithms, min, max, and minmax also have a form that takes a curly brace–enclosed list of values and returns the minimum, maximum, or both of those values. These are the only algorithms that take a single curly brace–enclosed list as a single argument. The only constraint is that all the values must have the same type. For example, the following

int a{1}, b{2}, c{3};
int smallest{ std::min({ a, b, c }) };

is equivalent to the traditional style of using iterators

int a{1}, b{2}, c{3};
std::vector<int> tmp{ a, b, c};
int smallest{ *std::min_element(tmp.begin(), tmp.end()) };

Comparing

To check whether two ranges are equal, that is, that they contain the same values, call the equal algorithm. This algorithm takes a start and one-past-the-end iterator for one range and the start of the second range. You must ensure that the two ranges have the same size. If every element of the two ranges is equal, it returns true. If any element doesn’t match, it returns false. The function has two forms: pass only the iterators to equal, and it compares elements with the == operator; pass a comparison functor as the last argument, and equal compares elements by calling the functor. The first argument to the functor is the element from the first range, and the second argument is the element from the second range.

The mismatch function is the opposite. It compares two ranges and returns a std::pair of iterators that refer to the first elements that do not match. The first member in the pair is an iterator that refers to an element in the first range, and the second member refers to the second range. If the two ranges are equal, the return value is a pair of end iterators.

The lexicographical_compare algorithm sets the record for the longest algorithm name. It compares two ranges and determines whether the first range is “less than” the second. It does this by comparing the ranges one element at a time. If the ranges are equal, the function returns false. If the ranges are equal up to the end of one range, and the other range is longer, the shorter range is less than the longer range. If an element mismatch is found, whichever range contains the smaller element is the smaller range. All elements are compared using the < operator (or a caller-supplied predicate) and checked for equivalence, not equality. Recall that elements a and b are equivalent if the following is true:

not (a < b) and not (b < a)

If you apply lexicographical_compare to two strings, you get the expected less-than relationship, which explains the name. In other words, if you call this algorithm with the strings "hello" and "help", it returns true; if you call it with "help" and "hello", it returns false; and if you call it with "hel" and "hello", it returns true.

Write a test program that reads two sequences of integers into separate vectors. (Remember to clear the state after reading the first vector’s data.) Then test the equal, mismatch, and lexicographical_compare functions on the two ranges. Remember that equal and mismatch require their input ranges to have the same size. You can ensure that you compare only the number of elements in the shorter vector by computing the end iterator instead of calling the end() member function.

std::equal(data1.begin(),
           data1.begin() + std::min(data1.size(), data2.size()),
           data2.begin())

Not all iterators allow addition, but a vector’s iterators do allow it. Adding an integer n to begin() offsets the iterator as though you had advanced it n times with the ++ operator. (Discover more about iterators in the next Exploration.)

Table 43-2 lists some suggested input data sets.

Table 43-2. Suggested Data Sets for Testing Comparison Algorithms

Data Set 1

Data Set 2

1 2 3 4 5

1 2 3

1 2 3

1 2 3 4 5

1 2 3 4 5

1 2 4 5

1 2 3

1 2 3

Compare your test program with mine in Listing 43-9.

Listing 43-9.  Testing Various Comparison Algorithms

#include <algorithm>
#include <iostream>
#include <vector>
 
#include "data.hpp"
 
int main()
{
  intvector data1{};
  intvector data2{};
 
  read_data(data1);
  std::cin.clear();
  read_data(data2);
 
  std::cout << "data1: ";
  write_data(data1);
  std::cout << "data2: ";
  write_data(data2);
 
  auto data1_end(data1.begin() + std::min(data1.size(), data2.size()));
 
  std::cout << std::boolalpha;
  std::cout << "equal(data1, data2) = " <<
    equal(data1.begin(), data1_end, data2.begin()) << ' ';
 
  auto result(mismatch(data1.begin(), data1_end, data2.begin()));
 
  std::cout << "mismatch(data1, data2) = index " <<
   std::distance(data1.begin(), result.first) << ' ';
 
  std::cout << "lex_comp(data1, data2) = " <<
    std::lexicographical_compare(data1.begin(), data1.end(),
                                 data2.begin(), data2.end()) << ' ';
}

Rearranging Data

You’ve already seen the sort algorithm many times. Other algorithms are also adept at rearranging values in a range. The merge algorithm merges two sorted input ranges into a single output range. As always, you must ensure the output range has sufficient room to accept the entire merged result from both input ranges. The two input ranges can be different sizes, so merge takes five or six arguments: two for the first input range, two for the second input range, one for the start of the output range, and an optional argument for a functor to use instead of the < operator.

The replace algorithm scans an input range and replaces every occurrence of an old value with a new value. The replacement occurs in place, so you specify the range with the usual pair of iterators, but no write iterator. The replace_if function is similar but takes a predicate instead of an old value. Write a program that reads a vector of integers and replaces all occurrences of values in the range [10, 20] with 0. Reuse the intrange functor class or write a lambda. Compare your program with mine in Listing 43-10.

Listing 43-10.  Using replace_if and intrange to Replace All Integers in [10, 20] with 0

#include <algorithm>
 
#include "data.hpp"
#include "intrange.hpp"
 
int main()
{
  intvector data{};
  read_data(data);
  write_data(data);
  std::replace_if(data.begin(), data.end(), intrange{10, 20}, 0);
  write_data(data);
}

Listing 43-11 shows the same program using a lambda.

Listing 43-11.  Using replace_if and intrange to Replace All Integers in [10, 20] with 0

#include <algorithm>
 
#include "data.hpp"
#include "intrange.hpp"
 
int main()
{
  intvector data{};
  read_data(data);
  write_data(data);
  std::replace_if(data.begin(), data.end(),
    [](int x)
    {
      return x >= 10 and x <= 20;
    },
    0);
  write_data(data);
}

A fun algorithm is random_shuffle, which shuffles elements in place into random order. This function takes two arguments, specifying the range to shuffle. Another form of the function takes three arguments. The final argument is a functor that returns a random number in the range [0, n], where n is the size of the input range, or it can be a uniform random number generator from the standard library (similar to what randomint uses in Listing 43-5).

Use the sequence.hpp file (from Listing 42-6) and generate a vector of 100 sequential integers. Then shuffle it into random order and print it. Compare your solution with mine in Listing 43-12.

Listing 43-12.  Shuffling Integers into Random Order

#include <algorithm>
 
#include "data.hpp"
#include "sequence.hpp"
 
int main()
{
  intvector data(100);
  std::generate(data.begin(), data.end(), sequence{1, 1});
  write_data(data);
  std::random_shuffle(data.begin(), data.end());
  write_data(data);
}

The generate algorithm repeatedly calls a functor with no arguments and copies the return value into an output range. It calls the functor once per element in the range, overwriting every element. The generate_n function takes an iterator for the start of a range and an integer for the size of the range. It then calls a functor (the third argument) once for each element of the range, copying the return value into the range. It is your responsibility to ensure that the range actually has that many elements in it. To use generate_n instead of generate in Listing 43-10, you could write

std::generate_n(data.begin(), data.size(), sequence(1, 1));

If you don’t have to call a functor for every item of a range but instead want to fill a range with copies of the same value, call fill, passing a pair of iterators that specify a range, and a value. The value is copied into every element in the range. The fill_n function takes a starting iterator and an integer size to specify the target range.

The transform algorithm modifies items by calling a functor for each item in an input range. It writes the transformed results to an output range, which can be the same as the input range, resulting in modifying the range in place. You’ve seen this algorithm at work already, so I won’t add much to what you already know. The function has two forms: unary and binary. The unary form takes one input range, the start of an output range, and a functor. It calls the functor for each element of the input range, copying the result to the output range. The output range can be the same as the input range, or it can be a separate range. As with all algorithms, you must ensure that the output range is large enough to store the results.

The binary form of transform takes an input range, the start of a second input range (it assumes the size is the same as the size of the first input range), the start of an output range, and a binary functor. The functor is called for each element in the input ranges; the first argument comes from the first input range, and the second argument comes from the second input range. As with the unary form, the function copies the result to the output range, which can be the same as either input range. Note that the types of the two input ranges do not have to be the same.

Copying Data

Some algorithms operate in place, and others copy their results to an output range. For example, reverse reverses items in place, and reverse_copy leaves the input range intact and copies the reversed items to an output range. If a copying form of an algorithm exists, its name has _copy appended. (Unless it is also a predicate form of a function, in which case it has _if appended after _copy, as in replace_copy_if.)

In addition to just plain copy, which you’ve seen many times already, the standard library offers copy_backward, which makes a copy but starts at the end and works toward the beginning, preserving the original order; copy_n, which takes the start of a range, a count, and a write iterator; and copy_if, which is like copy but takes a predicate and copies an element only if the predicate returns true. Distinguish copy_backward from reverse_copy. The latter starts at the beginning and works toward the end of the input range but copies the values into reverse order.

If you have to move elements instead of copy them, call std::move or std::move_backward. This std::move is different from the one you encountered in Exploration 39. This one is declared in <algorithm>. Like copy, the move algorithm takes two read iterators and a write iterator. It calls the other form of std::move for each element of the input range, moving the element into the output range.

As with all algorithms that write output, it is your responsibility to ensure the output range is large enough to handle everything you write to it. Some implementations of the standard library offer debugging modes to help detect violations of this rule. If your library offers such a feature, by all means, take full advantage of it.

Deleting Elements

The trickiest algorithms to use are those that “remove” elements. As you learned in Exploration 22, algorithms such as remove don’t actually delete anything. Instead, they rearrange the elements in the range, so that all the elements slated for removal are packed at the end of the range. You can then decide either to use the sub-range of elements you want to keep or erase the “removed” elements by calling the erase member function.

The remove function takes an iterator range and a value, and it removes all elements equal to that value. You can also use a predicate with remove_if to remove all elements for which a predicate returns true. These two functions have copying counterparts that don’t rearrange anything but merely copy the elements that are not being removed: remove_copy copies all the elements that are not equal to a certain value and remove_copy_if copies all elements for which a predicate returns false.

Another algorithm that removes elements is unique (and unique_copy). It takes an input range and removes all adjacent duplicates, thereby ensuring that every item in the range is unique. (If the range is sorted, then all duplicates are adjacent.) Both functions can take a comparison functor instead of using the default == operator.

Write a program that reads integers into a vector, erases all elements equal to zero, copies only those elements that lie in the range [24, 42] to another vector, sorts the other vector, and removes duplicates. Print the resulting vector. My solution is in Listing 43-13.

Listing 43-13.  Erasing Elements from a Vector

#include <algorithm>
#include "data.hpp"
#include "intrange.hpp"
 
int main()
{
  intvector data{};
  read_data(data);
  data.erase(std::remove(data.begin(), data.end(), 0), data.end());
  intvector copy{};
  std::remove_copy_if(data.begin(), data.end(), std::back_inserter(copy),
                      [](int x) { return  x< 24 or x > 42; });
  std::sort(copy.begin(), copy.end());
  copy.erase(std::unique(copy.begin(), copy.end()), copy.end());
  write_data(copy);
}

Iterators

Algorithms and iterators are closely related. All the algorithms (except min, max, and minmax) take two or more iterators as arguments. To use algorithms effectively, you must understand iterators. Therefore, the next Exploration will help you master iterators, all five flavors. That’s right. Iterators come in five different varieties. Keep reading to learn more.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset