Objects, and Algorithms
Objects, and Algorithms
(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.
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.
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::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:
Iterators
We can declare iterator as following: container type::iterator
new_iterator
vector<string>::iterator iter;
vector<string>::const_iterator iter;
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!
vector<int> v;
for(int i = 0; i < 1000; ++i) v.push_back(i);
vector<int> v;
v.reserve(1000);
for(int i = 0; i < 1000; ++i) v.push_back(i);
Vector - Matrix
Vector - Matrix Initialization
Here is another example of vector of vector, 3x2, matrix initialization:
#include <iostream>
#include <vector>
#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));
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
return 0;
}
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;
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));
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.
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);
};
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.
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;
}
#include <iostream>
#include <vector>
using namespace std;
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. }
14. vector<int> v;
15. for(int i = 0; i < 10; ++i) v.push_back(i);
16. int *a = &v;[0];
&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.
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <iterator>
int main()
{
const int SIZE = 7;
// append
word.push_back("a");
word.push_back("man");
word.push_back("plan");
word.push_back("canal");
word.push_back("Panama");
showAll(word);
return 0;
}
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.
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;
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;}
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.