Interestingly, vectors of strings don't seem to give students problems; however vectors of vectors do, and the two are really not fundamentally different. This lecture gives you some practice with vectors of vectors.

Let's suppose that I want to represent a matrix of integers in C++. The best way to do that, is to use a vector of integer vectors.

First, let me introduce you to **typedef**. It is a way in C/C++ to rename a
data type with your own name. For example, take a look at
**fred.cpp**:

#include <iostream> using namespace std; typedef int Fred; int main() { Fred i; i = 5; cout << "The Fred named i is equal to " << i << endl; } |

The **typedef** statement says that you can declare a variable to be
of type "Fred," and that when you do it, it will be an **int**. Thus,
when we declare **"Fred i"**, that is the same as saying **"int i"**:

UNIX>WhilefredThe Fred named i is equal to 5 UNIX>

#include <vector> #include <iostream> #include <sstream> #include <cstdlib> #include <cstdio> using namespace std; typedef vector <int> IVec; int main(int argc, char **argv) { int r, c, p, i, j, base, val; istringstream ss; vector <IVec> vdm; if (argc != 4) { cerr << "usage: vdm rows cols prime\n"; exit(1); } ss.clear(); ss.str(argv[1]); if (!(ss >> r)) { cerr << "Bad rows\n"; exit(1); } ss.clear(); ss.str(argv[2]); if (!(ss >> c)) { cerr << "Bad cols\n"; exit(1); } ss.clear(); ss.str(argv[3]); if (!(ss >> p)) { cerr << "Bad prime\n"; exit(1); } vdm.resize(r); for (i = 0; i < vdm.size(); i++) { base = i+1; val = 1; for (j = 0; j < c; j++) { vdm[i].push_back(val); val = (val * base) % p; } } for (i = 0; i < vdm.size(); i++) { for (j = 0; j < vdm[i].size(); j++) printf(" %4d", vdm[i][j]); cout << endl; } } |

This program takes three command line arguments: *r*, *c* and *p*.
It then creates a **r * c** "Vandermonde" matrix over the field defined by the
prime number *p*? What's a "field?" In this case, it is the numbers 0 through *p-1*,
where addition, subtraction and multiplication are all modulo *p*. Division
is defined to be the inverse of multiplication, but don't worry about it, since it doesn't
really matter in this lecture.

A *Vandermonde* matrix is one that has the value *(i+1) ^{j} mod p* in row

Take a look at how **vdm** is defined: **vector vdm**. What's an

UNIX>You should be able to verify to yourselves that all of the above matrices are Vandermonde matrices in their given fields.vdm 1 1 1011 UNIX>vdm 3 3 1011 1 1 1 2 4 1 3 9 UNIX>vdm 3 5 1011 1 1 1 1 1 2 4 8 16 1 3 9 27 81 UNIX>vdm 3 5 71 1 1 1 1 1 2 4 1 2 1 3 2 6 4 UNIX>

The numbers are arranged in rows, where row *i* has *i+1* elements (as always, our
lives are zero-indexed). The first and last element in each row is equal to one.
Each other element is the sum of the two elements above it. Suppose we want to write
a program to generate Pascal's triangle in a data structure. One easy way to do
this is to generate it as a vector of integer vectors, where element *i* of the
vector is a vector containing the elements of row *i*. We can visualize it below:

Scanning for a pattern, let's consider the *j*-th element in row *i*.
If it is the first or last element in the row, it will equal one. Otherwise, you can
see from the picture that it is equal to the sum of elements *j-1* and *j* in row *i-1*.
That gives us a nice way to construct the triangle. The code is in
**pascal.cpp**:

#include <vector> #include <iostream> #include <sstream> #include <cstdlib> #include <cstdio> using namespace std; typedef vector <int> IVec; int main(int argc, char **argv) { int r, i, j; istringstream ss; vector <IVec> pascal; if (argc != 2) { cerr << "usage: pascal rows\n"; exit(1); } ss.clear(); ss.str(argv[1]); if (!(ss >> r)) { cerr << "Bad rows\n"; exit(1); } pascal.resize(r); for (i = 0; i < pascal.size(); i++) { for (j = 0; j <= i; j++) { if (j == 0 || j == i) { pascal[i].push_back(1); } else { pascal[i].push_back(pascal[i-1][j-1] + pascal[i-1][j]); } } } for (i = 0; i < pascal.size(); i++) { for (j = 0; j < pascal[i].size(); j++) printf(" %4d", pascal[i][j]); cout << endl; } } |

When we run it, it's pictured a little differently than above, but you should see that it is clearly the same triangle:

UNIX>pascal 101 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 1 6 15 20 15 6 1 1 7 21 35 35 21 7 1 1 8 28 56 70 56 28 8 1 1 9 36 84 126 126 84 36 9 1 UNIX>

#include <cstdio> #include <cstdlib> #include <vector> #include <iostream> using namespace std; typedef vector <double> DVec; int main() { vector <DVec> v1, v2; DVec dv; int i, j; srand48(100); for (i = 0; i < 4; i++) { dv.clear(); for (j = 0; j < 5; j++) dv.push_back(drand48()*10); v1.push_back(dv); } v2 = v1; for (i = 0; i < v2.size(); i++) { for (j = 0; j < v2[i].size(); j += 2) v2[i][j] = 0; } printf("V1:\n\n"); for (i = 0; i < v1.size(); i++) { for (j = 0; j < v1[i].size(); j++) printf("%8.3lf", v1[i][j]); printf("\n"); } printf("\nV2:\n\n"); for (i = 0; i < v2.size(); i++) { for (j = 0; j < v2[i].size(); j++) printf("%8.3lf", v2[i][j]); printf("\n"); } } |

The first few lines create a vector of vectors of doubles. We can think of
it as a (4 * 5) matrix of doubles. The next statement: **"v2 = v1"**
is one that is not legal in C. It is in C++ though -- it makes a copy of
**v1** and puts it into **v2**. You can see that in the subsequent
code that sets all the even indexed columns of **v2** to zero, and then
prints out both matrices:

UNIX>SincevcopyV1: 2.511 2.089 9.409 4.225 3.959 3.826 2.317 5.355 5.332 8.629 9.624 3.154 7.360 6.547 0.503 4.538 8.065 6.558 8.119 8.299 V2: 0.000 2.089 0.000 4.225 0.000 0.000 2.317 0.000 5.332 0.000 0.000 3.154 0.000 6.547 0.000 0.000 8.065 0.000 8.119 0.000 UNIX>

There is another large copy operation in
**vcopy.cpp**:

for (i = 0; i < 4; i++) { dv.clear(); for (j = 0; j < 5; j++) dv.push_back(drand48()*10); v1.push_back(dv); } |

That **v1.push_back(dv)** call makes a copy of **dv**. If it didn't make a
copy, all of the rows of **v1** would be the same.

A faster way to write the above code and eliminate the expensive copy of the vector dv is to resize v1 to its final size before the for loop and then copy the randomly generated values directly into v1's vectors:

v1.resize(4); for (i = 0; i < 4; i++) { for (j = 0; j < 5; j++) v1[i].push_back(drand48()*10); } |

You won't see a difference in performance when the vector is a 4x5 vector. However, when I changed the vector to a 10000x10000 vector and ran the first for loop that used the copy operation to copy dv to v1, the for loop took roughly 3.9 seconds. When I ran the seond for loop that resized v1 to its final size so that it did not use the copy operation, the runtime decreased to roughly 3.3 seconds. That's roughly a 15% reduction in execution time that's achieved simply by avoiding an expensive copy operation.

Finally, you can achieve even faster performance by resizing each of v1's individual vectors to their final size rather than constantly using push_back to increase the size of these vectors by 1. The reason is that when you use push_back, the vector that you are pushing onto occasionally has to allocate a larger block of memory to hold the vector, copy the existing vector to the new vector, and then de-allocate the storage associated with the older vector. In contrast, when you resize the vector to its final size, this allocation of memory only has to be done once. Here is the code:

v1.resize(4); for (i = 0; i < 4; i++) { v1[i].resize(5); for (j = 0; j < 5; j++) v1[i][j] = (drand48()*10); } |

When I test this last block of code with a 10000x10000 vector, it runs in roughly 2.5 seconds. This is roughly a 24% savings over the block of code that uses push_back and a 36% savings over the original block of code that copied the dv vector into v1.