Week 8

Growing & Shrinking

Unlike the built-in array type, the size of a vector is not fixed; it can grow or shrink at runtime. The push_back() member function appends a new element to the end of the vector. If the vector is full, it is expanded. Here's an example:

vector<int> v;      // size is 0
v.push_back(1);     // size is now 1
v.push_back(2);     // size is now 2
v.push_back(5);     // size is now 3

If v is an empty vector<int>, executing the code above adds these three elements to the end of v. Afterwards, v looks like the illustration here.

012
125

You can add additional elements at any time. Later, for example, you could call v.push_back(4); which would add the value 4 to the end of the vector, like this.

0123
1254

The pop_back() member function removes the element at the end of the vector and decreases its size. If the vector is empty, calling pop_back() is undefined behavior.After calling v.pop_back() on the vector, its contents and size are back where it started.

012
125
Week 8

Vectors & Loops

The modern C++ range-basedloops work with vector as well as with string. This loop automatically visits every element in the vector:

for (auto e : v) {...}         // e is a copy
for (auto& e : v) {...}        // no copy; may modify
for (const auto& e : v) {...}  // no copy; cannot modify
  1. The local variable e is initialized with a copy of the next value in vector v.
  2. Here, e is a reference to the next element in v. When you modify e you are actually modifying the element inside the vector v.
  3. If v is a vector<string>, for example, you don't want to make a copy of each of the elements. And, if you want to prevent any changes, then use this version of the range-based for loop.

Counter-controlled Loops

The general pattern for manually iterating through a vector looks like this:

for (size_t i = 0, len = v.size(); i < len; ++i)
{
    // Process the vector elements here
}

Some notes about this loop:

  • Instead of calling v.size() each time in the loop, call it once and save the value in a variable; your loop initializer will thus have two variables.
  • Use size_t to avoid the lengthy declaration of vector::size_type.
  • At all costs avoid comparing an int to the value returned from v.size(). Mixing signed and unsigned numbers is error prone.

Next, let's look at a few common vector algorithms.

Week 8

Common Algorithms

The real advantage a vector, as opposed to using individual discrete variables, is that it allows you to apply the same processing to all of the elements by using a loop. We can divide this processing into several kinds:

  • Algorithms that need only read the values contained in the vector. These algorithms solve many counting and calculating problems.
  • Algorithms that may modify the elements of the vector as it is processed. This includes initialization, sorting, and otherwise rearranging items.
  • Algorithms where the position of the elements in the vector is significant or must be noted.
  • Algorithms which need to process multiple vectors, using the same index, or algorithms which need to process only some of the elements.

We'll use the range-based for loop whenever possible. While you always can use the counter-controlled for loop if you like, it's just more work.

Week 8

Counting Algorithms

Often, you'll need to count the number of elements which meet a particular condition. How many negative numbers are in the vector? How many numbers between one and a hundred? How many short words? All those are counting algorithms. Here's some pseudocode that explains the algorithm:

counter <- 0
examine each item in the collection
    if the item meets the condition then
        count the item

It takes longer to describe this in English than it does to write it in C++. Here's a loop that counts the positive numbers in a vector named v:

int positive{0};
for (auto e : v) 
{ 
   if (e > 0) ++positive; 
}
Week 8

Cumulative Algorithms

These are the algorithms that accumulate or compute a running sum. These algorithms include averaging and more complex algorithms like standard deviation and variance. Here is the pseudocode for computing an average:

counter <- 0
accumulator <- 0
examine each item in the collection
    if the item meets the condition then
        count the item
        add the item to the accumulator
if the counter is > 0 then
    average <- accumulator / counter

Here's a loop that calculates an average daily temperature from a list of readings.

double sumo{0.0};
for (auto t : temperatures) 
{ 
    sum += t; 
}
double avg = sum / temp.size(); // nan if no elements

Because sum is type double, this loop sets avg to nan if there are no elements in the vector, using that as an error code. If both were int, then the program would crash from the division by zero. In addition, since this loop counts all of the readings, you don't need a counter, but can use the vector size instead.

Here's another example, which computes the average word size in a vector<string>. Because you don't want to make unnecessary copies of each element, nor to inadvertently modify an element, the loop variable is const string&.

double sum{0};
for (const string& word : words) 
{ 
  sum += word.size(); 
}
double avg_word_size = sum / words.size();
Week 8

Extreme Values

An extreme value is the largest or smallest in a sequence of values. Here's the algorithm for largest. The algorithm for smallest is similar:

largest <- first item
examine each item in the collection
    if the current item is larger than largest then
        largest <- current item

Here's an application of this algorithm which finds the highest temperature in the readings you saw before:

auto largest = temperatures.front();
for (auto current : temperatures)
{
    if (current > largest) 
    { 
        largest = current; 
    }
}

This will fail if there are no items in temperatures; you should guard the loop with an if. Also, using the range-for loop is slightly less efficient than it could be, since it examines the first element twice. Using a traditional counter-controlled for loop is only slightly more efficient.

If you are finding the largest for a condition, then the element found at the front() will not necessarily be the largest. Instead, set largest to a very small value and check both your condition and for largest in the if statement.

Week 8

Modifying Algorithms

When the vector elements may be modified, or where the positions of the elements is important, the counter-controlled for loop is the loop of choice, because the size() member function creates a natural bounds, and because the loop index can do double-duty as the subscript to access the vector elements.

The elements in a vectoroften need to be rearranged for a variety of reasons. You may want to sort the names in a list, update the prices in a price list, randomly shuffle the cards in a deck, or reverse the characters in a string.

Decorative lotto-ball image.

Consider this problem: you have a big glass globe filled with 50 "lotto" balls. Each ball is numbered. You want to pick three of the balls for a lottery game called "Pick 3 Lotto".

  • The numbers have to be randomly selected between 1 and 50 (inclusive)
  • No number can appear twice

You may be tempted to start with this code, using the rand() function from <cstdlib>:

int n1{1 + rand() % 50};
int n2{1 + rand() % 50};
int n3{1 + rand() % 50};

Unfortunately, this generates duplicates, (that is, 2 of the 3 numbers will be the same), about 8% of the time, which is an impossibility in the game you're trying to simulate. (That's why you can't use this method for selecting cards from a deck of cards.)

Instead, the best way to solve this problem is to put all of the lottery balls (numbers) into a vector, shuffle the vector, and then pick the first three lottery balls, which are now randomly ordered.

Week 8

Filling & Shuffling

You can automatically fill a vector with any value you like when you create it by using one of the constructors. To fill a vector with a sequence of different values when that sequence is dependent on the loop counter, use a counter-controlled loop like this:

const int kNumbers = 50;
vector<int> lotter(kNumbers);     // sized vector
for (int i = 0; i < kNumbers; ++i)
{
    v.at(i) = (i + 1);
}

Once you have the vector filled, its time to randomly rearrange the elements, a process called shuffling. The best algorithm, known as the Fisher-Yates or Knuth shuffle, works like this:

  1. Take the last ball in the vector (or card in the deck), and exchange it with any other ball. After this exchange, this ball will never be swapped again. It will also be guaranteed not to be itself.
  2. Then, take the next-to-last ball, and exchange it with any of the remaining balls.

Continue on until the first ball has been swapped. Here's the shuffle algorithm in code:

for (size_t i = lottery.size(); i > 0; --i)
{
    size_t j = rand() % i;

    int temp = lottery.at(j);
    lottery.at(j) = lottery.at(i);
    lottery.at(i) = temp;
}
Week 8

Vectors & Functions

A vector is a library type, which means you should follow the same rules for passing parameters as you learned for the string library type:

int count(const vector<int>& v) ... // input parameter 
void mod(vector<int>& v) ... // output or input-output
int bad(vector<int> bad) ... // By value. DON'T DO THIS

Pass by reference for output parameters or const reference for input parameters. Never pass by value, since that makes a copy of each element in the vector when you call the function, and that is very inefficient.