0% found this document useful (0 votes)
36 views

Objects, and Algorithms

The Standard Template Library (STL) is a set of C++ template classes and functions that provide common programming structures like containers, iterators, and algorithms. It allows programmers to write generic and reusable code. Key components of STL include containers like vectors and lists that store elements, iterators that navigate container elements, and algorithms that perform operations on elements. Vectors provide fast random access but slow insertion/removal. Lists provide fast insertion/removal but slow random access. Using iterators and algorithms, code can work with different container types.

Uploaded by

George Nădejde
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
36 views

Objects, and Algorithms

The Standard Template Library (STL) is a set of C++ template classes and functions that provide common programming structures like containers, iterators, and algorithms. It allows programmers to write generic and reusable code. Key components of STL include containers like vectors and lists that store elements, iterators that navigate container elements, and algorithms that perform operations on elements. Vectors provide fast random access but slow insertion/removal. Lists provide fast insertion/removal but slow random access. Using iterators and algorithms, code can work with different container types.

Uploaded by

George Nădejde
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 27

Standard Template Library

(STL)
The Standard Template Library (STL) is the heart of the C++ standard
library. There is no official definition of the STL, however, generally
accepted definition may be this:
The STL is the parts of C++ Standard Library what work with iterators.

So, it includes the containers, part of the iostream libraries, function


objects, and algorithms.

The STL is an example of generic programming. While the object-


oriented programming concentrates on the data aspect of
programming, generic programming concentrates on algorithms.

A goal of generic programming is to write code that is independent of


data types. Templates are the C++ tools for creating generic program.
Templates let us define a function or class in terms of a generic type. The
STL goes further by providing a generic representation of algorithms.

STL Component
STL has three key components - containers (popular templatized data
structures), iterators and algorithms.

1. Containers 
Containers manage collection of element. To meet different needs, the
STL provides different kinds of containers. 

1. Sequential containers 
These are ordered collections in which every element has a certain
position. This position depends on the time and place of the insertion, but
it is independent of the value of the element. These
include vector, deque, and list.
2. Associative containers 
These are sorted collections in which the actual position of an element
depends on its value due to a sorting criterion. These
include set, multiset, map, and multimap.

2. Iterators 
Iterators are objects that can navigate over elements. An iterator
represents a certain position in a container. Iterators are divided into five
groups, based on the operations they support.

1. Input iterators 
These are read-only iterators where each iterated location may be read
only once. The most common manifestation of input iterator
is istream_iterators.
2. Output iterators 
These are write-only iterators where each iterated location may be read
only once. The most common manifestation of output iterator
is ostream_iterator.
3. Forward iterators 
These have the capabilities of both input and output iterators, but they
can read or write a single location repeatedly. They don't
support operator--, so they can move only forward.
4. Bidirectional iterators 
These are just like forward iterators, except they can go backward as well
as forward. The standard associative containers all offer bidirectional
iterators.
5. Random access iterators 
These do everything bidirectional iterators do, but they also offer iterator
arithmetic, i.e., the ability to jump forward or backward in a single
step. vector, string, and deque each provide random access iterators.

3. Algorithms 
The STL provides several standard algorithms for the processing of
elements of collections. They can search, sort, modify, or simply use the
element for different purpose. Algorithms use iterators. So, an algorithm
has to be written only once to work with arbitrary containers because the
iterator interface for iterators is common for all container types.
Vectors
A vector manages its elements in a dynamic array. It enables random
access, which means we can access each element directly with index.

Appending and removing elements at the end of the array is very fast. But
inserting an element in the middle or at the beginning of the array takes
time because all the following elements should be moved to make room
for it while maintaining the order.

Deques
Double-ended-queue (deque) is a dynamic array that is implemented so
that it can grow in both directions. So, inserting element at the end and at
the beginning is fast. Inserting and elements in the middle, however, takes
time because element must be moved.

Lists
A list is implemented as a doubly linked list of element. In other words,
each element in a list has its own segment of memory and refers to its
predecessor and its successor. Lists do notprovide random access.
General access to an arbitrary element takes linear time and this is a lot
worse than vectors and deques.

The advantage of a list is that the insertion or removal of an element is


fast at any position. Only the links must be changed. This implies that
moving an element in the middle of a list is very fast compared with
moving an element in a vector or a deque.
Vector
Initializing Vectors
#include <vector>

int main ()
{
std::vector<int> vec1; // empty
vector of ints
std::vector<int> vec2 (3); // 3 ints
std::vector<int> vec2 (3,10); // 3 ints
with value 10
std::vector<int> vec3 (vec2.begin(),vec2.end()); //
iterating via vec2
std::vector<int> vec4 (vec3); // a copy
of vec3
int myInt[] = {1,2,3}; //
construct from arrays:
std::vector<int> vec5 (myInt, myInt + sizeof(myInt) / sizeof(int)
);

return 0;
}
Using the
push_back()/pop_back()/size()/clear()/
empty() member function
#include <vector>
#include <iostream>
#include <string>

int main()
{
std::vector<std::string> Scientist;

Scientist.push_back("James Maxwell");
Scientist.push_back("Edwin Hubble");
Scientist.push_back("Charles Augustin de Coulomb");
Scientist.push_back("Louis Pasteur");

std::cout << "Now, we have " << Scientist.size() << "


scientists.\n";
Scientist.pop_back();
std::cout << "Now, we have " << Scientist.size() << "
scientists.\n";

std::vector<std::string>::iterator iter;
for (iter = Scientist.begin(); iter != Scientist.end(); +
+iter)
std::cout << *iter << std::endl;

Scientist.clear();
if(Scientist.empty())
std::cout << "Nothing in the list\n";
else
std::cout << "You have something in the list\n";

return 0;
}
Output is:

Now, we have 4 scientists.


Now, we have 3 scientists.
James Maxwell
Edwin Hubble
Charles Augustin de Coulomb
Nothing in the list

The push_back() adds a new element to the end of a vector. As a result,


Scientist[0] is now equal to "James Maxwell", and Scientist[3] is now equal
to "Louis Pasteur".
The pop_back() removes the last element of a vector and reduces the
vector size by one. In the example, the size of "Scientist" is reduced from 4
to 3.
The clear() member function removes all of the elements of a vector and
sets its size to 0.
The empty() returns "true" if the vector is empty, otherwise, it returns
"false."

Iterators
We can declare iterator as following: container type::iterator
new_iterator

vector<string>::iterator iter;

Iterators are values that identify an element in a container. We can


access/change the value of the element using iterator.
We can also declare another iterator:

vector<string>::const_iterator iter;

This constant iterator is just like a regular iterator except that we can


think of a constant iterator as providing read-only access. The iterator
itself, however, can change. In other words, we can move iter all around
the vector. But we can't change the value of any of the elements
through iter.

begin() and end()


In the above example, we assign the return value
of Scientist.begin() to iter. The begin()returns an iterator that refers to a
container's first element. So, the statement assigns an iterator that refers
to the first element of Scientist to iter. Then, while looping through the
elements, we test the return value of Scientist.end() against iter to make
sure the two are equal.

The end() returns an iterator one past the last element in a container. This


means the loop will continue until iter has looped through all of the
elements in Scientist. In the action statement in the loop, ++iter,
increments iter, which traverse it to the next element in the vector.
Depending upon the iterator, we can perform other mathematical
operations on iterators to traverse them around a container.

In the body of the loop, we send *iter to cout. By placing the dereference


operator(*) in front of iter, we display the value of the element to which
the iterator refers.

insert() and erase()


We can insert an item, for example, an the beginning:

Scientist.insert(Scientist.begin(),"Leonardo da Vinci");

not or we can remove an item from the list, an element not at the end but
from the middle:

Scientist.erase(Scientist.begin() + 2);
Vector - Performance
Vectors grow dynamically, and every vector has a specific size. When we
add a new element to a vector, the computer reallocates memory and
may even copy all of the vector elements into this new memory, and this
can cause a performance hit.

capacity()
The capacity() returns the capacity of a vector (the number of elements
that a vector can hold before a program must allocate more memory for
it). So, a vector's capacity is not the same thing as its size which is the
number of elements a vector currently holds. In short, capacity()is the
size of the container and the size() is the currently filled level.
The capacity() is always equal to or larger than the size. The difference
between them is the number of elements that we can add to the vector
before the array under the hood needs to be reallocated.

reserve()
Before we look into the reserve() we need to know what's happening
whenever a vector needs more space. It's doing similar
to realloc operation. New memory allocation, copy from the old to the
new, destruct old objects, deallocate old memory, invalidation of iterators.
It's expensive!

The reserve() increases the capacity of a vector to the number supplied as


an argument. The reserve() gives us control over when a reallocation of
additional memory occurs.

Scientist.reserve(20); // reserve memory for 20 additional


elements
By using reserve() to keep a vector's capacity large enough for our
purposes, we can delay memory reallocation.
According to Scott Meyers, the following code requires 2-18 reallocations:

vector<int> v;
for(int i = 0; i < 1000; ++i) v.push_back(i);

So, he suggested we should use resever() to reduce the costs:

vector<int> v;
v.reserve(1000);
for(int i = 0; i < 1000; ++i) v.push_back(i);

insert() and erase()


Adding or removing an element from the end of a vector
using push_back() or pop_back() is extremely efficient. Adding or
removing an element at any other element in a vector, however, can
require more work because we may have to move multiple elements to
accommodate the insertion or deletion. With large vectors this can cause
a performance hit.

Vector - Matrix
Vector - Matrix Initialization
Here is another example of vector of vector, 3x2, matrix initialization:

#include <iostream>
#include <vector>

using namespace std;

#define ROW 3
#define COL 2

int main()
{
// vector with ROW rows, each row has COL columns with
initial value of 99
vector<vector<int> > v2D(ROW, vector<int>(COL,99));

for(int i = 0; i < ROW; ++i) {


for(int j = 0; j < COL; ++j) {
cout << v2D[i][j] << " ";
}
cout << endl;
}
return 0;
}

Output is:

99 99
99 99
99 99
Actually, similar initialization is used for a C++ Starter Package of Google
AI Ants 2011:

/*
struct for representing a square in the grid.
*/
struct Square
{
bool isVisible, isWater, isHill, isFood;
int ant, hillPlayer;
...

Square()
{
isVisible = isWater = isHill = isFood = 0;
ant = hillPlayer = -1;
};
...
};
...
std::vector<std::vector<Square> > grid;
...
grid = <vector<vector<Square> >(rows, vector<Square>(cols,
Square()));
...
std::vector<std::vector<bool> > visited(rows,
std::vector<bool>(cols, 0));

Matrix
We can make a matrix with vector:
#include <iostream>
#include <vector>

int main()
{
// 3 x 2 matrix

std::vector<std::vector<int> > myVecA(3,std::vector<int>(2) )


;
myVecA[0][0] = 0;
myVecA[0][1] = 1;
myVecA[1][0] = 10;
myVecA[1][1] = 11;
myVecA[2][0] = 20;
myVecA[2][1] = 21;

for(int i = 0;i < 3 ; i++) {


for(int j = 0; j < 2; j++) {
std::cout << myVecA[i][j] << " " ;
}
std::cout << std::endl;
}
std::cout << std::endl;

std::vector<std::vector<int> > myVecB;


for (int i = 0; i < 3; i++) {
std::vector<int> row; // Create an empty row
for(int j = 0; j < 2; j++) {
row.push_back(i+j); // Add an element (column)
to the row
}
myVecB.push_back(row); // Add the row to the main
vector
}
for(int i = 0;i < 3 ; i++) {
for(int j = 0; j < 2; j++) {
std::cout << myVecB[i][j] << " " ;
}
std::cout << std::endl;
}
std::cout << std::endl;

std::vector<std::vector<int> > myVecC;


for (int i = 0; i < 3; i++) {
myVecC.push_back(std::vector<int>()); // Add an empty
row
}
int dummy = 0;
for (int i = 0; i < 3; i++) {
for (std::vector<std::vector<int> >::iterator it =
myVecC.begin(); it != myVecC.end(); ++it) {
it->push_back(i*100 + dummy++); // Add column
to all rows
}
}
for(int i = 0;i < 3 ; i++) {
for(int j = 0; j < 2; j++) {
std::cout << myVecC[i][j] << " " ;
}
std::cout << std::endl;
}
std::cout << std::endl;

return 0;
}

Output from the run is:

0 1
10 11
20 21

0 1
1 2
2 3

0 103
1 104
2 105

Matrix multiplication
Here is an example of multi-dimensional vectors used to calculate matrix
multiplication:

#include <vector>
using namespace std;

void mat_mul(const vector<vector<int> >&a;, int row_a,


const vector<vector<int> >&b;, int row_b,
vector<vector<int> >&c;)
{
for(int i = 0; i < row_a; i++)
for(int j = 0; j < 4; j++)
for(int k = 0; k < row_b; k++)
c[i][j] += a[i][k]*b[k][j];
}

int main()
{
vector<vector<int> > a(2, vector<int>(3));
vector<vector<int> > b(3, vector<int>(4));
vector<vector<int> > c(2, vector<int>(4));

a[0][0] = 6; a[0][1] = 5; a[0][2] = -2;


a[1][0] = -1; a[1][1] = -4; a[1][2] = 0;
b[0][0] = 6; b[0][1] = 1; b[0][2] = 7; b[0][3] = 3;
b[1][0] = 2; b[1][1] = 4; b[1][2] = -1; b[1][3] = 5;
b[2][0] = 8; b[2][1] = 9; b[2][2] = 10; b[2][3] = 11;

int row_a = 2;
int row_b = 3;

mat_mul(a,row_a,b,row_b, c);

/*
c = {
30 8 17 21
-14 -17 -3 -23
}
*/

return 0;
}

My vector::reserve(), capacity(),
resize(), and push_back()
The vector class in this section is not the stl's vector, but implementation
wise, it should be similar, and we get some insight how it works.

When we change sizes, the most basic operation is vector::reserve(). This


operation adds space for new elements, and its code looks like this;

class vector
{
int sz; // current number of elements
double *elem; // address of first element
int space; // current number of elements + free_space
public:
void reserve(int);
void resize(int);
int capacity() const;
void push_back(double);
};

void vector::reserve(int amount)


{
if(amount <= space) return; // we have
enough space
double *pd = new double[amount]; // allocate new
space
for (int i = 0; i < sz; ++i) pd[i] = elem[i]; // copy old
element
delete[] elem; // deallocate
old space
elem = pd;
space = amount;
}

We are not initializing the elements of the reserved space,


and push_back() will do it. Here, we are just reserving space. So, we can
get the information about the amount of available space by using:

int vector::capacity() const {return space;}

If v is a vector, then the space for our new vector element when we use
with out push_back()will be:

v.capacity()-v.size()
That's the currently available size for new elements without reallocation.

If we utilize the reserve(), our resize() fairly straightforward: make the


vector have the requested size of elements and initialize each new
element with 0.0.

void vector::resize(int requested_size)


{
reserve(requested_size);
for(int i = sz; i < requested_size; ++i) elem[i] = 0;
sz = requested_size;
}

So, the push_back() looks like this:

void vector::push_back(double d)
{
if(space == 0) reserve(10);
else if (sz == space) reserve(2*space); // we need more
space, so double the size
elem[sz] = d; // put d at the end
++sz; // increase the
number of element, sz
}

vector::operator=()
vector& vector::operator=(const vector& v)
{
if(this == &v;) return;
if(v.sz <= space) {
for(int i = 0; i < sz; ++i) elem[i] = v.elem[i];
sz = v.sz;
return *this;
}
double *pd = new double[v.sz];
for(int i = 0; i < sz; ++i) pd[i] = v.elem[i];
delete[] elem;
elem = pd;
space = sz = v.sz;
return *this;
}

The operator=() implemented above may be the simplest:

1. Allocate memory for a copy.


2. Copy the elements/
3. Free the old memory allocation.
4. Update sz, elem, space.

Queue with Vector


The code below implements queue using vector. It's a contrived one. The
vector contains objects which have only one integer member.

#include <iostream>
#include <vector>
using namespace std;

typedef struct object


{
int data;
} Object;

class queue
{
public:
explicit queue(int);
~queue();
void pop();
void push(int);
int peek(){return qv.front()->data;}
private:
vector<Object*> qv;

};

queue::queue(int n)
{
Object *ptr = new Object;
ptr->data = n;
qv.push_back(ptr);
}

void queue::push(int n)
{
Object *ptr = new Object;
ptr->data = n;
qv.push_back(ptr);

}
void queue::pop()
{
vector<Object*>::iterator st = qv.begin();
delete *st;
qv.erase(st);
}
queue::~queue()
{
vector<Object*>::iterator it;
for(it = qv.begin(); it != qv.end(); ++it) {
delete *it;
}
qv.erase(qv.begin(), qv.end());
}
int main()
{
queue *q = new queue(10);
q->push(20);
q->push(30);
q->push(40);
q->push(50);
q->pop();
q->pop();
cout << q->peek();
delete q;
return 0;}

Vector vs List
1. vector

1. Contiguous memory.
2. Pre-allocates space for future elements, so extra space may be
required.
3. Unlike a list where additional space for a pointer is needed,
each element only requires the space for the element type itself.
4. Can re-allocate memory for the entire vector at any time that
we add an element.
5. Insertions at the end are constant, but insertions elsewhere
are a costly O(n).
6. Erasing an element at the end of the vector is constant time,
but for the other locations it's O(n).
7. We can randomly access its elements.
8. Iterators, pointers, and references are invalidated if we add or
remove elements to or from the vector.

9. vector::iterator it = v.begin();
10. for(it = v.begin(); it != v.end(); ++it) {
11. if(*it == 5) v.erase(it);
12. }

So, after the *it == 5, this may crash.

13. We can easily get at the underlying array if we need an array of


the elements:

14. vector<int> v;
15. for(int i = 0; i < 10; ++i) v.push_back(i);
16. int *a = &v;[0];

vector is very similar to the array, so the following line is true:

&v;[i] == &v;[0] + i;

2. list

1. Non-contiguous memory.
2. No pre-allocated memory. The memory overhead for the list
itself is constant.
3. Each element requires extra space for the node which holds
the element, including pointers to the next and previous elements in the
list.
4. Never has to re-allocate memory for the whole list just
because we add an element.
5. Insertions and erasures are cheap no matter where in the list
they occur.
6. It's cheap to combine lists with splicing.
7. We cannot randomly access elements, so getting at a
particular element in the list can be expensive.
8. Iterators remain valid even when we add or remove elements
from the list.
9. If we need an array of the elements, we'll have to create a new
one and add them all to it, since there is no underlying array.

Vectors with Iterator and


Algorithms(STL)
The following code examples shows how the vector works with iterator
and algorithms such as find() and for_each(). It also demonstrates the
characteristics of string class as part of STL.

Coding itself is almost self explanatory:

// Palindrome: "A man a plan a canal Panama"


// Using vector, this code shows the given input is a palindrome
// It starts with an incomplete input,
// to demonstrate some other operations of vector.

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <iterator>

using namespace std;

// print out all element separated by spaces


template <class T>
void showAll(T& v) {
copy(v.begin(),v.end(),
ostream_iterator<string>(cout," "));
cout << endl;
}

// find the s1 and put s2 before s1


template <class T>
void insertMyWay(T& v, string s1, string s2) {
T::iterator pos;
for(pos = v.begin(); pos != v.end(); ++pos) {
if(*pos == s1) {
v.insert(pos,s2);
break;
}
}
}

// reverse characters of a string input


void reverseCharacters(string& s) {
reverse(s.begin(),s.end());
}

int main()
{
const int SIZE = 7;

// create an empty vector


vector<string> word;

// reserve memory for 7 elements


word.reserve(SIZE);

// append
word.push_back("a");
word.push_back("man");
word.push_back("plan");
word.push_back("canal");
word.push_back("Panama");

// display all elements


showAll(word);

// insert a string - we iterate the vector by ourselves


insertMyWay(word,"plan","a");
showAll(word);

// insert a string - using STL algorithm find()


word.insert(find(word.begin(),word.end(),"canal"),"a");
showAll(word);

// reverse the element - using STL algorithm reverse()


reverse(word.begin(),word.end());
showAll(word);

// pass each element(string) of vector


// then, reverseCharacters() will reverse the characters
for_each(word.begin(),word.end(),reverseCharacters);

showAll(word);
return 0;
}

The output is:

a man plan canal Panama


a man a plan canal Panama
a man a plan a canal Panama
Panama canal a plan a man a
amanaP lanac a nalp a nam a

We could have done more to the output such as put spaces between
appropriate words and convert characters to lower/upper case. Yet, it
demonstrates enough use of several features that STL provides.

In the following Chapters, we'll look at Associative Containers, iterators,


and algorithms in detail.

List
Erasing elements in a loop
It's a little bit tricky to erase elements from the list within a loop. Here is a
sample:
#include <iostream>
#include <list>
using namespace std;
int main()
{
list myList;
int i;

for (i = 0;i < 10;++i) {


myList.push_back(i);
}

for each(int m in myList) {


cout << m <<",";
}
cout << "\n";

list::iterator it = myList.begin();
while(it != myList.end()) {
if(*it % 2 == 0)
it = myList.erase(it);
++it; // needed for VC
}
for(it = myList.begin(); it != myList.end(); ++it) {
cout << *it <<",";
}
return 0;}

The iterator returned from the line:

it = myList.erase(it);

should points to the next element, Visual C++ seems it an additional step
to increment the iterator:

++it;
But gcc can do the job without the additional line.

Types defined for STL


containers
All STL containers define the types. In this list, we use X to represent a
container type, such as vector<double>, and T for the type stored in the
container, such as double.

1. X::value_type - T, the element type. 


T is the type we get when the iterator is dereferenced.
For sets and multisets, it is constant. 
For maps and multimaps, it is pair<const key-type, mapped-type>.
2. X::reference - T&.
3. X::const_reference - const T&.
4. X::iterator - iterator type pointing to T, behaves like T*.
5. X::const_iterator - iterator type pointing to const T, behaves
like const T*.
6. X::difference_type - signed integral type used to represent the
distance from one iterator to another.
7. X::size_type - unsigned integral type size_type can represent size of
data objects, such as number of elements, and subscripts.

You might also like