Vectors
Steven J. Zeil
A vector is similar to an array, except that it can “grow” to accommodate as many items as you actually try to put into it:
-
A very useful structure.
-
We’ll use it to implement many other ADTs.
-
-
Your text introduces a somewhat simplified version of the
std
interface (calledVector
to avoid clashing with thestd
class.)We’ll use that simplified form when we look at how to implement vectors, but will start by studying the
std::vector
interface so that you know what you can do when programming applications of it.
First, however, a slight digression into C++-isms. I stated earlier that “A vector is similar to an array”. The similarity comes from the fact that vectors keep their elements in numeric order and allow you to access them via indexing:
int a[];
vector<int> v;
⋮
a[i] = v[i+1];
string b[];
vector<string> w;
⋮
w[23] = "Hello";
Folderol c[];
vector<Folderol> x;
⋮
x[1] = c[1];
Because vectors are templates, we can have vectors containing almost any other type.
Why “almost any type”? You might remember that not every type can be used in an array. A type can only be used as array elements if that type provides a default constructor, because that constructor is used to initialize the elements of the array.
Similarly, types that are to be used as vector elements have a special requirement as well. This requirement is different from, but no more stringent than the requirement for arrays. Types to be used in a vector must supply a copy constructor.
1 The Vector Interface
Let’s take a quick look at how to work with vectors.
1.1 Constructors
v1 is a vector of integers. Initially, it has 0 elements.
`vector<string> v2(10, "abc");`v2 is a vector of strings initialized to hold 10 copies of "abc"
.
vector<string> v3 (containerOfStrings.begin(),
containerOfStrings.end());
v3
is initialized to hold a copy of the contents of some other container. That container does not have to be another vector.
vector<int> v4 = {2, 3, 5, 999};
v4
is initialized using an initializer list.
1.2 Copying
These operations replace the entire contents of a vector by something new:
Two ways to copy an entire vector from another vector:
vector<double> v, w;
⋮
v = w;
vector<double> z(v);
Other options:
v.assign (container.begin(), container.end());
You can assign all or part of another container to a vector.
v.assign(24, "abc");
You can also assign some number of copies of a single value to a vector.
1.3 Size & Capacity
vector<int>::size_type n;
n = v1.size();
How many elements currently in v1
?
v1.resize(n, x);
If v1
has more than n
elements, remove the last v1.size()-n
elements. If it has fewer than n
elements, add n-v1.size()
copies of x
to the end. (x
may be omitted, in which case the added elements are formed using their data type’s default constructor.)
vector<int>::size_type n;
n = v1.capacity();
How many elements total could v1
hold without requesting more memory? (We’ll see later that this affects performance.)
v1.reserve(n);
Add memory, if necessary, to make sure v1
will have room to hold at least n elements. Note that this does not actually change the size()
of the vector. It just helps make sure there is enough space for future growth.
1.4 Access to Elements
v1[0] = v1[1] + 1;
You can index into a vector just like with arrays. Note that you can only access v[i]
if the size()
of the vector is already at least i+1
.
You can also use the at
functions:
v1.at(0) = v1.at(1) + 1;
The difference between at
and the square bracket indexing is that access via at
is checked to be sure that the index is legal, between 0
and size()-1
, inclusive. Normally, this is not checked for square bracket style indexing (though the g++
compiler allows you to request such checking through the use of appropriate compiler flags).
x = v1.front();
v1.front() = v1.front()+1;
Provides access to the first element in the vector.
x = v1.back();
v1.back() = v1.back()+1;
Provides access to the last element in the vector.
1.5 Inserting Elements
v2.push_back("def");
Adds a new element, "def"
to the end of the vector v2
. v2.size()
increases by 1. This the “normal”, most common way to add elements to a vector.
Although not as efficient, the std::vector
also allows:
vector<std::string>::iterator pos;
⋮
v2.insert("def", pos);
Inserts a new element, "def"
, at the indicated position within the vector v2
. Any elements already in v2
at that position or higher are moved back one position to make room. v2.size()
increases by 1.
1.6 Removing Elements
v2.pop_back();
Removes the element at the end of the vector, decreasing the size()
by 1.
Although not as efficient, the std::vector
also allows:
vector<std::string>::iterator pos;
⋮
v2.erase(pos);
Removes the element at the indicated position within the vector v2
. Any elements already in v2
at higher positions are moved forward one position to “take up the slack”. v2.size()
decreases by 1.
vector<std::string>::iterator start, stop;
⋮
v2.erase(start, stop);
Removes the elements at the indicated positions from start
up to, but not including stop
, within the vector v2
. Any elements already in v2
at higher positions are moved forward to “take up the slack”. v2.size()
decreases by the number of elements removed.
You can remove all elements from a vector like this:
vector<std::string>::iterator pos;
⋮
v2.clear();
1.7 Comparing Vectors
Vectors can be compared using ==
and <
operators. (These operators are not shown in the simplified vector class from your textbook, because they were added to the C++ standard after the book was published.)
Two vectors are equal if they are the same length and if each corresponding pair of elements are equal. For example, if v1
is declared as
vector<int> v1 {1, 2, 3};
then, for varying values of v2
, we would have:
v2 |
v1 == v2 ? |
---|---|
vector<int> v2 {1, 2}; |
false |
vector<int> v2 {1, 2, 3}; |
true |
vector<int> v2 {1, 2, 3, 4}; |
false |
vector<int> v2 {2, 1, 3}; |
false |
The <
operator compares vectors according to the idea of lexicographic comparison:
-
The two vectors are scanned to find two corresponding elements that are not equal.
Those two non-equal elements are then comapred to see which is less than the other.
-
If the two vectors have equal contents up to the point where one vector or the other ends, the shorter vector is considered to be less than the other. (If neither vector is shorter than the other, then the two vectors are actually equal, and neither is condidered less than the other.)
This is, in essence, the same set of rules that we conventionally use when comparing strings to put them into “aplhabetic order”.
For example, if v1
is declared as
vector<int> v1 {1, 2, 3};
then, for varying values of v2
, we would have:
v2 |
v1 < v2 ? |
Why? |
---|---|---|
vector<int> v2 {1, 2}; |
false | v2 is shorter |
vector<int> v2 {1, 2, 3}; |
false | v1 == v2 |
vector<int> v2 {1, 2, 3, 4}; |
true | v1 is shorter |
vector<int> v2 {2, 1, 3}; |
true | v1[0] < v2[0] |
vector<int> v2 {1, 1, 3}; |
false | v1[1] > v2[1] |
vector<int> v2 {1, 2, 4}; |
true | v1[2] < v2[2] |
2 Using Vectors
2.1 Keeping Authors in a Book
Earlier, we developed a Book class that used std::array
to hold all of the authors in a book.
#ifndef BOOK_H
#define BOOK_H
#include <array>
#include <string>
#include "author.h"
class Publisher;
class Book {
private:
std::string title;
std::string isbn;
std::string publisher;
static const int MaxAuthors = 10;
std::array<std::string, MaxAuthors> authors;
int numAuthors;
public:
typedef std::array<std::string, MaxAuthors>::iterator iterator;
typedef std::array<std::string, MaxAuthors>::const_iterator const_iterator;
Book();
Book(const std::string& title, const std::string& isbn, const Publisher& publisher,
Author* authors = nullptr, int numAuthors = 0);
std::string getTitle() const {return title;}
void setTitle(std::string theTitle) {title = theTitle;}
void addAuthor (const Author&);
void removeAuthor (const Author&);
std::string getPublisher() const {return publisher;}
void setPublisher(const Publisher& publ);
std::string getISBN() const {return isbn;}
void setISBN(std::string id) {isbn = id;}
int numberOfAuthors() const;
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
bool operator== (const Book& right) const;
bool operator< (const Book& right) const;
};
std::ostream& operator<< (std::ostream& out, const Book& book);
#endif
/*
* book.cpp
*
* Created on: May 23, 2018
* Author: zeil
*/
#include "book.h"
#include "publisher.h"
#include <algorithm>
#include <cassert>
using namespace std;
Book::Book()
: title(), isbn(),
publisher(),
numAuthors(0)
{
}
Book::Book(const std::string& theTitle, const std::string& theISBN, const Publisher& thePublisher,
Author* theAuthors, int theNumAuthors)
: title(theTitle), isbn(theISBN), publisher(thePublisher.getName()), numAuthors(0)
{
for (int i = 0; i < theNumAuthors; ++i)
{
addAuthor(theAuthors[i]);
}
}
int Book::numberOfAuthors() const
{
return numAuthors;
}
void Book::addAuthor (const Author& au)
{
auto pos = find(authors.begin(), authors.end(), au.getName());
if (pos == authors.end())
{
assert (numAuthors < MaxAuthors);
authors[numAuthors] = au.getName();
++numAuthors;
}
}
void Book::removeAuthor (const Author& au)
{
auto pos = find(authors.begin(), authors.end(), au.getName());
if (pos != authors.end())
{
auto next = pos;
++next;
while (next != authors.end())
{
*pos = *next;
pos = next;
++next;
}
--numAuthors;
}
}
Book::iterator Book::begin()
{
return authors.begin();
}
Book::const_iterator Book::begin() const
{
return authors.begin();
}
Book::iterator Book::end()
{
return authors.begin()+numAuthors;
}
Book::const_iterator Book::end() const
{
return authors.begin()+numAuthors;
}
void Book::setPublisher (const Publisher& publ)
{
publisher = publ.getName();
}
bool Book::operator== (const Book& right) const
{
return getISBN() == right.getISBN();
}
bool Book::operator< (const Book& right) const
{
return getISBN() < right.getISBN();
}
std::ostream& operator<< (std::ostream& out, const Book& book)
{
out << book.getTitle() << ", by ";
bool firstTime = true;
for (const string& author: book)
{
if (!firstTime)
out << ", ";
firstTime = false;
out << author;
}
out << "; " << book.getPublisher() << ", " << book.getISBN();
return out;
}
Because standard arrays must be constructed with a fixed size, we have to have a separate data member numAuthors
to track how many of those array elements are actually in use.
We may be able to change that if we switch to a vector
-based implementation.
2.2 Vectorizing the book.h file
Changing the data structure to vector
is surprisingly simple:
Here is what we had:
class Book {
private:
std::string title;
std::string isbn;
std::string publisher;
static const int MaxAuthors = 10;
std::array<std::string, MaxAuthors> authors;
int numAuthors;
public:
typedef std::array<std::string, MaxAuthors>::iterator iterator;
typedef std::array<std::string, MaxAuthors>::const_iterator const_iterator;
and here is what we change it to:
class Book {
private:
std::string title;
std::string isbn;
std::string publisher;
std::vector<std::string> authors;
public:
typedef std::vector<std::string>::iterator iterator;
typedef std::vector<std::string>::const_iterator const_iterator;
- Obviously, the
std::array
uses will be replaced bystd::vector
. MaxAuthors
disappears. Because vectors can grow, we don’t need a pre-set maximum.numAuthors
disappears. We don’t need to know how many elements out of the vector actually contain author data. All of them will.
The last point highlights the difference in style between using arrays and using vectors. Most applications of arrays divide the array into used portions and portions available for future expansion. When we work with vectors, however, we only add as many :slots" to the vectors as we have data elements to fill them. (As we will see, the underlying implementation of vector may still use arrays, and those arrays will be divided into used and unused portions. But when we are using vectors that division is almost invisible.)
2.3 Vectorizing the book.cpp file
Probably nothing highlights the difference in styles between these two approaches than the following three functions:
With arrays:
int Book::numberOfAuthors() const
{
return numAuthors;
}
void Book::addAuthor (const Author& au)
{
auto pos = find(authors.begin(), authors.end(), au.getName());
if (pos == authors.end())
{
assert (numAuthors < MaxAuthors);
authors[numAuthors] = au.getName();
++numAuthors;
}
}
void Book::removeAuthor (const Author& au)
{
auto pos = find(authors.begin(), authors.end(), au.getName());
if (pos != authors.end())
{
auto next = pos;
++next;
while (next != authors.end())
{
*pos = *next;
pos = next;
++next;
}
--numAuthors;
}
}
With vectors:
int Book::numberOfAuthors() const
{
return authors.size();
}
void Book::addAuthor (const Author& au)
{
auto pos = find(authors.begin(), authors.end(), au.getName());
if (pos == authors.end())
{
authors.push_back(au.getName());
}
}
void Book::removeAuthor (const Author& au)
{
auto pos = find(authors.begin(), authors.end(), au.getName());
if (pos != authors.end())
{
authors.erase(pos);
}
}
We use push_back
operations to add new authors to the end of the vector-based collection, and the collection tracks its own size.
Constructors are also simplified slightly because we no longer have to initialize the numAuthors
data member, because it no longer exists.
2.4 Vectors copy nicely
We don’t provide our own implementation of the Big 3 in either of these versions, because both std::array
and std::vector
can be copied & assigned freely, and vectors clean up after themselves with an appropriate destructor.
2.5 Completing the Book operations
The only other difference of note is the provision of the iterators, particularly, the end
function.
For arrays the “end” position is the first unused slot in the array:
Book::iterator Book::begin()
{
return authors.begin();
}
Book::const_iterator Book::begin() const
{
return authors.begin();
}
Book::iterator Book::end()
{
return authors.begin() + numAuthors;
}
Book::const_iterator Book::end() const
{
return authors.begin() + numAuthors;
}
For vectors, all slots are used, so the end position is the position after the last slot:
Book::iterator Book::begin()
{
return authors.begin();
}
Book::const_iterator Book::begin() const
{
return authors.begin();
}
Book::iterator Book::end()
{
return authors.end();
}
Book::const_iterator Book::end() const
{
return authors.end();
}
The full implementation of the vector-based Book
is below:
#ifndef BOOK_H
#define BOOK_H
#include <vector>
#include <string>
#include "author.h"
class Publisher;
class Book {
private:
std::string title;
std::string isbn;
std::string publisher;
std::vector<std::string> authors;
public:
typedef std::vector<std::string>::iterator iterator;
typedef std::vector<std::string>::const_iterator const_iterator;
Book();
Book(const std::string& title, const std::string& isbn, const Publisher& publisher,
Author* authors = nullptr, int numAuthors = 0);
std::string getTitle() const {return title;}
void setTitle(std::string theTitle) {title = theTitle;}
void addAuthor (const Author&);
void removeAuthor (const Author&);
std::string getPublisher() const {return publisher;}
void setPublisher(const Publisher& publ);
std::string getISBN() const {return isbn;}
void setISBN(std::string id) {isbn = id;}
int numberOfAuthors() const;
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
bool operator== (const Book& right) const;
bool operator< (const Book& right) const;
};
std::ostream& operator<< (std::ostream& out, const Book& book);
#endif
/*
* book.cpp
*
* Created on: May 23, 2018
* Author: zeil
*/
#include "book.h"
#include "publisher.h"
#include <algorithm>
#include <cassert>
using namespace std;
Book::Book()
: title(), isbn(),
publisher()
{
}
Book::Book(const std::string& theTitle, const std::string& theISBN, const Publisher& thePublisher,
Author* theAuthors, int theNumAuthors)
: title(theTitle), isbn(theISBN), publisher(thePublisher.getName())
{
for (int i = 0; i < theNumAuthors; ++i)
{
addAuthor(theAuthors[i]);
}
}
int Book::numberOfAuthors() const
{
return authors.size();
}
void Book::addAuthor (const Author& au)
{
auto pos = find(authors.begin(), authors.end(), au.getName());
if (pos == authors.end())
{
authors.push_back(au.getName());
}
}
void Book::removeAuthor (const Author& au)
{
auto pos = find(authors.begin(), authors.end(), au.getName());
if (pos != authors.end())
{
authors.erase(pos);
}
}
Book::iterator Book::begin()
{
return authors.begin();
}
Book::const_iterator Book::begin() const
{
return authors.begin();
}
Book::iterator Book::end()
{
return authors.end();
}
Book::const_iterator Book::end() const
{
return authors.end();
}
void Book::setPublisher (const Publisher& publ)
{
publisher = publ.getName();
}
bool Book::operator== (const Book& right) const
{
return getISBN() == right.getISBN();
}
bool Book::operator< (const Book& right) const
{
return getISBN() < right.getISBN();
}
std::ostream& operator<< (std::ostream& out, const Book& book)
{
out << book.getTitle() << ", by ";
bool firstTime = true;
for (const string& author: book)
{
if (!firstTime)
out << ", ";
firstTime = false;
out << author;
}
out << "; " << book.getPublisher() << ", " << book.getISBN();
return out;
}
3 Required Performance
The C++ standard specifies that a legal (i.e., standard-conforming) implementation of vector
must satisfy the following performance requirements:
Operation | Speed |
---|---|
vector() |
O(1) |
vector(n, x) |
O(n) |
size() |
O(1) |
v[ i ] |
O(1) |
push_back(x) |
O(1) |
pop_back |
O(1) |
insert |
O(size()) |
erase |
O(size()) |
front, back |
O(1) |
Next, we will look at how vector actually manages to achieve this performance.