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

STL Tutorial - Sequence Containers

This document provides an overview of sequence containers in the Standard Template Library (STL) for C++, focusing on vectors, deques, and lists. It explains their characteristics, use cases, and performance implications, emphasizing the differences in access times and insertion/deletion efficiencies. Additionally, it includes examples of constructing and utilizing these containers, along with their member functions.

Uploaded by

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

STL Tutorial - Sequence Containers

This document provides an overview of sequence containers in the Standard Template Library (STL) for C++, focusing on vectors, deques, and lists. It explains their characteristics, use cases, and performance implications, emphasizing the differences in access times and insertion/deletion efficiencies. Additionally, it includes examples of constructing and utilizing these containers, along with their member functions.

Uploaded by

deepak jain
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
You are on page 1/ 16

STL Tutorial - Lesson 2: Sequence Containers

Sequence Containers

The Standard Template Library, or STL, is a collection of container classes, generic


algorithms and related components that can greatly simplify many programming tasks in
C++. This lesson covers sequence containers. Sequence containers hold elements of a
single type as a linear sequence. That is, they are ordered in the same way as they are
added to the container. The STL provides three sequence containers: vector, deque and
list. In addition, arrays, which are a built-in part of the language and the string class,
which is part of the standard library, may be used as sequence containers. The generic
algorithms will work with arrays and strings. Other types of data structures such as
stacks, queues and priority queues are provided in the STL by the use of container
adapters on the sequence containers. This will be covered in a later lesson. In later
lessons, we'll also cover associative containers that store their elements in a sorted
manner.

The first question that needs to be addressed is why is there more than one sequence
container. As we will see, they are optimized for different uses. The vector class is
optimized for insertions and deletions at its end, has compatibility with C-style arrays
and provided random access iterators. Random access iterators allow constant time
retrieval of any element and are required by certain generic algorithms. The deque
(pronounced "deck") class is optimized for insertions and deletions at either end and also
provides random access iterators. Lists allow constants time insertions and deletions
anywhere. They provide only bidirectional iterators, and not random access, so they
can't be used with certain generic algorithms. Just about all you need to know about
iterators was covered in the overview lesson. Any other details will be discussed as
needed.

Vectors

We'll begin by examining each of the container classes and outlining their similarities
and differences. In particular, note the usage guidance provided at the end of the bullets
for each class. The vector class is a sequence container that is part of the Standard
Template Library, or STL. Vectors provide fast constant time access to their elements
and fast constant time insertions and deletions at their end. Additionally, since they
provide random access iterators, they may be used with any of the generic algorithms.
Here are the main characteristics of the vector class. We'll examine each point in the
examples that follow.

 The vector class provides random access iterators. Random access iterators support the full
range of operations possible on iterators and on pointers. That is they support pointer style
arithmetic and comparisons. They support *, ==, !=, ++, --, +, +=, -, -=, <, <=, > and >=
operations. Random access iterators support fast retrieval from any location. Any element can
be quickly retrieved without traversing the entire container. Certain of the generic algorithms
require random access iterators as arguments. This is true of many of the sorting algorithms.
 Random access iterators, which are provided by the vector container class, allow access in
constant time. This means that the time to access an element is independent of its position in
the vector. The time to access the ith element is the same as the time to access the first or
last.
1
 Constant time insertions and deletions at the end. (except when the vector is grown). A vector
is grown when it reaches its capacity and additional elements are inserted. We'll see how to
control a vectors capacity when it is constructed and by using the reserve() method of the
vector class.
 Linear time insertions at the beginning of the vector. To insert at the beginning of a vector, all
subsequent elements must be moved. The cost of insertions at the start of a vector is
proportional to the number of elements already in the vector. For a vector containing N
elements the cost is O(N).
 Insertions or deletions in the middle are expensive operations. All subsequent elements must
be moved.
 The vector class has compatibility with built-in arrays. The data in vectors is stored in a
contiguous block, just like a built-in array. A pointer to the start of a vector can be passed to
function expecting a built-in array. This is a useful feature for using some API's that expect an
array argument. A simple example is an array passed to an interface card.
 The vector class provides random access iterators, so any of the generic algorithms can be
used. We will see that the list container class, which provides bidirectional iterators cannot be
used with all of the generic algorithms.
 With vectors, as long as reallocation does not occur, iterators and references are not
invalidated by insertions. With a deque all iterators and references are invalidated by insertions
in the middle of the sequence.
 With vectors, only iterators and references for elements past the point of erasure are
invalidated. With a deque all iterators and references are invalidated by erasures within the
middle of the sequence.
 Two containers are considered equal if they are of the same size and elements in the same
position are equal as defined by the “==” operator of the type in the container.
 Use when fast random access if required and insertions/deletions will be occurring only
at the end.
 Use when compatibility with built-in arrays is required.

Deques

The deque class is a sequence container that is part of the Standard Template Library, or
STL. Deques, like vectors, provide random access iterators that allow constant time (fast)
access to any of their elements and can be used with any of the generic algorithms. They
also are optimized for insertions and deletions at their beginnings and ends. Due to
internal structure of deques, they tend to be slightly less efficient than vectors. That is,
manipulations using deques will operate slightly slower than the same operations on
vectors. Deques are the container of choice if you will be doing insertions and deletions
at both ends. As with vectors, if insertions and deletions will be done in the interior of the
sequence, lists may be a better choice. Here are the main characteristics of the deque
class. We'll examine each point in the examples that follow.

 The deque class provides random access iterators. Random access iterators support the full
range of operations possible on iterators and on pointers. That is they support pointer style
arithmetic and comparisons. They support *, ==, !=, ++, --, +, +=, -, -=, <, <=, > and >=
operations. Random access iterators support fast retrieval from any location. Any element can
be quickly retrieved without traversing the entire container. Certain of the generic algorithms
require random access iterators as arguments. This is true of many of the sorting algorithms.
 Random access iterators, which are provided by the deque container class, allow access in
constant time. This means that the time to access an element is independent of its position in

2
the deque. The time to access the ith element is the same as the time to access the first or
last.
 Constant time insertions and deletions at either end.
 Insertions or deletions in the middle are expensive operations. Elements must be moved.
 No compatibility with built-in arrays.
 The deque class provides random access iterators, so any of the generic algorithms can be
used. We will see that the list container class, which provides bidirectional iterators cannot be
used with all of the generic algorithms.
 All iterators and references are invalidated by insertions in the middle of the sequence.
 All iterators and references are invalidated by erasures within the middle of the sequence.
 Two containers are considered equal if they are of the same size and elements in the same
position are equal as defined by the “==” operator of the type in the container.
 Random access of elements is usually slower than vector. But, use if insertions and deletions
are required at both ends. Inserts and erasures at the beginning of the sequence are constant
time; vectors require linear time (time proportional to the size of the vector) to do this. All other
operations are slower.
 Use when fast random access if required and insertions/deletions will be occurring at
both ends.

Lists

The list class is a sequence container that is part of the Standard Template Library, or
STL. Lists are usually implemented using a doubly-linked list as the internal structure.
They provide bidirectional iterators, rather than random access iterators like vectors and
deques. Because of this elements are access in linear time rather than constant time. To
access an element the list must be traversed. Additionally, some of the generic
algorithms, mostly those that are sorting-related, require random access iterators. So,
lists can't be used with them. Lists are optimized for insertions and deletions anywhere,
including in the interior of the sequence. Consider using lists when insertions and
deletions are to be made within the interior of the sequence. Here are the main
characteristics of the list class. We'll examine each point in the examples that follow.

 The list class provides random bidirectional iterators. They support a subset of pointer style
operations: *, ==, !=, ++, --.
 Lists provide linear time access to elements. The time to access an element increases as the
size of the list grows.
 Constant time insertions and deletions anywhere.
 Elements stored (at least conceptually) as a doubly-linked list.
 The iterators are bidirectional rather than random access. This is necessary to allow constant
time insertions and deletions in the interior of a sequence. But, this does limit the use of certain
of the generic algorithms. In particular, sorting related algorithms cannot be used. The list class
provides its own sorting member functions to compensate this deficiency.
 Insertions never invalidate iterators and references.
 Deletions only invalidate iterators and references to the deleted elements.
 Two containers are considered equal if they are of the same size and elements in the same
position are equal as defined by the “==” operator of the type in the container.
 Use if insertions/deletions are needed within the sequence.

3
Constructing Sequence Containers

There are multiple constructors provided by each container class to create container objects. The different
constructors provide ways to specify the size of the container and initial values for its elements. Let's take a look
at a simple example showing the various constructors.

#include <vector> // Needed to use vectors


#include <deque> // Needed to use deques
#include <list> // Needed to use lists
#include <string>
using namespace std;

int main() {

// 1. Creating an empty container

vector<int> v1;
list<string> l1;
deque<float> d1;

// 2. Creating a container of some size using the default value for


// built-in types or the default constructor for user-defined
// types such as classes

vector<int> v2(100);
// Creates a vector of 100 ints with initial values of 0
deque<int> d2(5);
// Creates a deque of 5 ints with initial values of 0

// 3. Creates a vector of some size with specified initial values


vector<int> v3(100, -5);
// Creates a vector of 100 integers with initial values of -5.
vector<string> v4(4, "dog");
// Creates a vector containing 4 strings initialized to "dog".
list<string> l2(5, "cat");
// Creates a list containing 5 stings initialized to "cat". This allows
// for efficient deletion and insertion of cats in the interior of
// the sequence.

// 4. Creates a vector by providing start and end iterators to another container.


string s1("Hello World");
vector<char> v5(s1.begin(), s1.end());
// Creates a vector containing "Hello World"

list<int> l3(v3.begin(), v3.end());


// Creates a list containing the elements of v3
deque<int> d3(v3.begin(), v3.end());

4
// Creates a deque containing the elements of v3

int numbers[5] = {9, 3, 2, 5, 6};


vector<int> v6(numbers, numbers + 3);
// Creates a vector containing the first three elements of
// the array numbers.

// 5. Creates a container initialized from another container.


vector<int> v7(v6);
list<int> l4(l3);
deque<int> d4(d2);

return 0;
}

Vectors - Member Functions

Before we start with examples of how to use the STL sequence container classes, let's look at the standard way
to traverse the elements of a container. Generally, two iterators are required for a loop. One is to the start of the
sequence. The second is one past the last element to be accessed. Many of the generic algorithms also require an
iterator to the start of the sequence and an iterator to one past the end of the sequence as arguments. The
element “one past” the end of the sequence is not processed; it serves as a sentinel, or stopping point. Let's
review a list of the vector member functions and operators and then look at some examples.

assign Clears a vector and assigns the specified elements to it.


at Returns a reference to the element at the specified position
back Returns a reference to the element at the end of the vector
begin Returns an iterator to the start of the vector
capacity Returns the number of elements that the vector can hold
clear Removes all elements from the vector
empty Tests whether the vector is empty
end Returns an iterator to the end of the vector
erase Removes element(s) at specified position(s)
front Returns a reference to the first element in the vector
insert Inserts element(s) at specified position(s)
max_size Returns the maximum size of the vector
pop_back Removes the element at the end of the vector
push_back Adds an element to the end of the vector
reserve Reserves a contiguous block of memory for the vector
size Returns the number of elements in the vector
swap Exchanges the contents of two vectors
operator[] Returns a reference to the element at the specified position

5
operator!= Test whether two vectors are unequal
operator< Test whether one vector is less than another
operator<= Test whether one vector is less than or equal to another
operator== Test whether two vectors are equal
operator> Test whether one vector is greater than another
operator>= Test whether one vector is greater than or equal to another

Vector Methods Example

Below is an example showing how to use some of methods of the vector class. In the pages that follow, we'll
see how to combine the container classes with the generic algorithms to do some really neat things. Promise. :-)

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

int main() {

// Create a vector, initialized by an array


int myArray[5] = {4,3,7,8,9};
vector<int> vec1(myArray, myArray + 5);

// Iterator
vector<int>::iterator iter;

// This is how to loop through the vector


// begin() returns an iterator to the start of vec1
// end() returns one past the end of vec1.
for (iter = vec1.begin(); iter != vec1.end(); iter++) {
cout << *iter << " ";
}
cout << endl;

cout << "The vector has " << vec1.size() << " elements" << endl;
cout << "The second element is " << vec1[1] << endl;
cout << "The third element is " << vec1.at(2) << endl;
cout << "The first element is " << vec1.front() << endl;
cout << "The last element is " << vec1.back() << endl;

cout << endl;


vec1.pop_back(); // Removes last element
cout << "The last element is now " << vec1.back() << endl;
vec1.pop_back(); // Removes last element
cout << "The last element is now " << vec1.back() << endl;
vec1.push_back(2); // Puts 2 at the end of vec1
cout << "The last element is now " << vec1.back() << endl;
cout << endl;

6
// Create a second vector
vector<int> vec2;

// Insert values from vec1 to vec2


vec2.insert(vec2.begin(),vec1.begin(),vec1.end());

for (iter = vec2.begin(); iter != vec2.end(); iter++) {


cout << *iter << " ";
}
cout << endl;

// Insert more values (Three fives) at the end of vec2


vec2.insert(vec2.end(),3,5);

for (iter = vec2.begin(); iter != vec2.end(); iter++) {


cout << *iter << " ";
}
cout << endl;

return 0;
}

OUTPUT:
43789
The vector has 5 elements
The second element is 3
The third element is 7
The first element is 4
The last element is 9

The last element is now 8


The last element is now 7
The last element is now 2

4372
4372555

Vector Examples

The following example shows how to combine the use of the generic algorithms with the vector container class.
Notice how simple it is to perform common operations such as reversing the sequence or sorting. All of the
generic algorithms will be covered in a later lesson.

#include <vector> // Needed to use the vector class


#include <algorithm> //Needed to use the generic algorithms
#include <iostream> // You know this one
using namespace std;

int main() {

7
// Create a vector, initialized by an array
int myArray[10] = {99,4,3,7,8,67,9,33,12,67};
vector<int> vec1(myArray, myArray + 10);

// Iterator
vector<int>::iterator iter;

cout << "Original sequence: ";


for (iter = vec1.begin(); iter != vec1.end(); iter++) {
cout << *iter << " ";
}
cout << endl;

// REVERSE
reverse(vec1.begin(), vec1.end());

cout << "Reversed: ";


for (iter = vec1.begin(); iter != vec1.end(); iter++) {
cout << *iter << " ";
}
cout << endl;

// SORT
cout << "Sorted: ";
sort(vec1.begin(), vec1.end());

for (iter = vec1.begin(); iter != vec1.end(); iter++) {


cout << *iter << " ";
}
cout << endl;

// REMOVE ANY DUPLICATE ELEMENTS


vector<int>::iterator newEnd;
newEnd = unique(vec1.begin(), vec1.end());

cout << "Duplicates removed: ";


for (iter = vec1.begin(); iter != newEnd; iter++) {
cout << *iter << " ";
}
cout << endl;

return 0;
}

OUTPUT:
Original Sequence: 99 4 3 7 8 67 9 33 12 67
Reversed: 67 12 33 9 67 8 7 3 4 99
Sorted: 3 4 7 8 9 12 33 67 67 99
Duplicates removed: 3 4 7 8 9 12 33 67 99

8
Deques - Member Functions

Here are the deque (pronounced deck) methods. Notice that they are the same as the vector member functions
with two exceptions. First, two member functions have been add to insert and delete from the front: pop_front
and push_front. Remember that deques are optimized to allow insertions and deletions at both their beginnings
and ends. Second, the capacity and reserve methods, present in the vector class, are not part of deque. This is
because the issues with the size of the allocated block of memory are pertinent only to vector. Vector requires a
contiguous block of memory. Deque has a different internal structure. Later in this lesson, we'll examine using
the capacity and reserve methods of vector.

assign Clears a deque and assigns the specified elements to it.


at Returns a reference to the element at the specified position
back Returns a reference to the element at the end of the deque
begin Returns an iterator to the start of the deque
clear Removes all elements from the deque
empty Tests whether the deque is empty
end Returns an iterator to the end of the deque
erase Removes element(s) at specified position(s)
front Returns a reference to the first element in the deque
insert Inserts element(s) at specified position(s)
max_size Returns the maximum size of the deque
pop_back Removes the element at the end of the deque
pop_front Removes the element at the front of the deque
push_back Adds an element to the end of the deque
push_front Adds an element to the front of the deque
size Returns the number of elements in the deque
swap Exchanges the contents of two deques
operator[] Returns a reference to the element at the specified position
operator!= Test whether two deques are unequal
operator< Test whether one deque is less than another
operator<= Test whether one deque is less than or equal to another
operator== Test whether two deques are equal
operator> Test whether one deque is greater than another
operator>= Test whether one deque is greater than or equal to another

Deque Methods Example

Most of the methods of deque are the same as vector. The examples shown for vector will
also work for deque. Below is an example showing how to use some of methods of the deque class that were not
illustrated in the vector example. Remember that both vector and deque allow fast random access to their
9
elements, with vector being slightly faster. Vectors are optimized for insertions and deletions at their ends.
Deques are optimized for insertions and deletions at both ends.

#include <deque> // Needed to use the deque class


#include <iostream>
using namespace std;

int main() {

// Create an empty deque


deque<float> deq1;

// Create an iterator variable


deque<float>::iterator iter = deq1.begin();

// Load from the back


// Notice the ordering of the elements in the output
deq1.push_back(2.5);
deq1.push_back(3.5);

// iter was initialized above


for (; iter != deq1.end(); iter++) {
cout << *iter << " ";
}
cout << endl;

// Load from the front


// Notice the ordering of the elements in the output
deq1.push_front(67.8);
deq1.push_front(33.3);

// iter was initialized above


for (iter = deq1.begin(); iter != deq1.end(); iter++) {
cout << *iter << " ";
}
cout << endl;

cout << "Deque contains " << deq1.size() << " elements" << endl;

// Clear the deck


deq1.clear();

// Check if deque is emtpy


if (deq1.empty()) {
cout << "Deque is empty" << endl;
}
else {
cout << "Deque is not empty" << endl;
}

10
cout << "Deque contains " << deq1.size() << " elements" << endl;

return 0;

OUTPUT:
2.5 3.5
33.3 67.8 2.5 3.5
Deque contains 4 elements
Deque is empty
Deque contains 0 elements

Deque Examples

The following example shows how to combine the use of the generic algorithms with the deque container class.
Notice how easy it is to search for a value and to merge two sequences. The combination of the container
classes and the generic algorithms leads to simple programming solutions. All of the generic algorithms will be
covered in a later lesson.

#include <deque> // Needed to use the deque class


#include <algorithm> // Needed for generic algorithms
#include <iostream>
using namespace std;

int main() {

// Create two deques containing number sequences


int arr1[5] = {4,8,6,7,5};
deque<int> deq1(arr1, arr1 + 5);

int arr2[5] = {4,10,8,6,12};


deque<int> deq2(arr2, arr2 + 5);

// Create an iterator variable


deque<int>::iterator iter;

// Search for a value within deq1


iter = find(deq1.begin(), deq1.end(), 4);

if (iter == deq1.end()) {
cout << "deq1 does not contain 4" << endl;
}
else {
cout << "deq1 contains " << *iter << endl;
}

// Merge two sorted sequences into a single deque


// Both deq1 and deq2 must be sorted before using merge
// The mergedResults deque must be large enough to hold the results
11
deque<int> mergedResults(deq1.size() + deq2.size());

sort(deq1.begin(), deq1.end());
sort(deq2.begin(), deq2.end());

merge(deq1.begin(), deq1.end(), deq2.begin(), deq2.end(),


mergedResults.begin());

for (iter = deq1.begin(); iter != deq1.end(); iter++) {


cout << *iter << " ";
}
cout << endl;

for (iter = deq2.begin(); iter != deq2.end(); iter++) {


cout << *iter << " ";
}
cout << endl;

for (iter = mergedResults.begin(); iter != mergedResults.end(); iter++) {


cout << *iter << " ";
}
cout << endl;

return 0;

OUTPUT:
deq1 contains 4
45678
4 6 8 10 12
4 4 5 6 6 7 8 8 10 12

Lists - Member Functions

Here are the list methods. Notice that most of the methods are the same as in vector and deque. This is expected
since the container classes are designed to be able to be used in place of one another. There are several new
methods. These are sort, merge, remove, replace and unique. These are optimized for use with lists and should
be used in place of the generic algorithms of the same names. The "at" method and the operator[] are not
present. This is because lists do not support random access of elements. These operators. if present, would need
to traverse the list to work and would be very inefficient.

assign Clears a list and assigns the specified elements to it.


back Returns a reference to the element at the end of the list
begin Returns an iterator to the start of the list
clear Removes all elements from the list
empty Tests whether the list is empty

12
end Returns an iterator to the end of the list
erase Removes element(s) at specified position(s)
front Returns a reference to the first element in the list
insert Inserts element(s) at specified position(s)
max_size Returns the maximum size of the list
merge Merges the argument list into the target list
pop_back Removes the element at the end of the list
pop_front Removes the element at the front of the list
push_back Adds an element to the end of the list
push_front Adds an element to the front of the list
remove Removes elements from a list equal to some value
reverse Reverses the elements in a list
size Returns the number of elements in the list
sort Sorts the elements of a list
splice Adds a range of elements from the argument list to the target list
swap Exchanges the contents of two lists
unique Removes adjacent duplicate elements from a list
operator!= Test whether two lists are unequal
operator< Test whether one list is less than another
operator<= Test whether one list is less than or equal to another
operator== Test whether two lists are equal
operator> Test whether one list is greater than another
operator>= Test whether one list is greater than or equal to another

List Methods Example

Below is an example demonstrating the use of some list methods, emphasizing those unique to list

#include <list> // Needed to use the list class


#include <string> // Needed for string class
#include <iostream>
using namespace std;

int main() {

list<string> phrase;
list<string>::iterator iter;

phrase.push_back("Welcome");
phrase.push_back("To");

13
phrase.push_back("My");
phrase.push_back("World");

for (iter = phrase.begin(); iter != phrase.end(); iter++) {


cout << *iter << " ";
}
cout << endl;

phrase.reverse();

for (iter = phrase.begin(); iter != phrase.end(); iter++) {


cout << *iter << " ";
}
cout << endl;

phrase.reverse();

phrase.remove("My");
for (iter = phrase.begin(); iter != phrase.end(); iter++) {
cout << *iter << " ";
}
cout << endl;

return 0;

OUTPUT:
Welcome To My World
World My To Welcome
Welcome To World

List Examples

The following example shows how to combine the use of the generic algorithms with the list container class.
All of the generic algorithms will be covered in a later lesson.

#include <list> // Needed to use the deque class


#include <string> // Needed for string
#include <algorithm> // Needed for find and fill
#include <iostream>
using namespace std;

int main() {

string s1 = "Hello World";


list<char> phrase(s1.begin(), s1.end());

list<char>::iterator iter;

14
for (iter = phrase.begin(); iter != phrase.end(); iter++) {
cout << *iter;
}
cout << endl;

iter = find(phrase.begin(), phrase.end(), 'W');

// So sleepy - see output


fill(iter, phrase.end(), 'Z');

for (iter = phrase.begin(); iter != phrase.end(); iter++) {


cout << *iter;
}
cout << endl;

return 0;

OUTPUT:
Hello World
Hello ZZZZZ

Capacity and Reserve

The final example in this lesson explores how vectors grow in size and how to use reserve to size the vector
correctly prior to use. There are two reasons to avoid reallocations (vector resizing). First, each time a resizing
occurs, a new contiguous memory buffer is allocated, the vector is copied into this new buffer and the original
(old) buffer is freed. This is an expensive operation. A second reason to avoid growing vectors is that all
iterators and references are invalidated after the insertion and growth. This may complicate coding.

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

int main() {

int N = 1024;

vector<int> v1;
vector<int> v2;

vector<int>::size_type c;

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


c = v1.capacity();
v1.push_back(i);
15
if (c != v1.capacity()) {
cout << "v1 grown from " << c << " to " << v1.capacity();
cout << endl;
}
}

// Reserve enough contiguous memory for v2


v2.reserve(N);

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


c = v2.capacity();
v2.push_back(i);
if (c != v2.capacity()) {
cout << "v2 grown from " << c << " to " << v2.capacity();
cout << endl;
}
}

return 0;

OUTPUT:
v1 grown from 0 to 1
v1 grown from 1 to 2
v1 grown from 2 to 3
v1 grown from 3 to 4
v1 grown from 4 to 6
v1 grown from 6 to 9
v1 grown from 9 to 13
v1 grown from 13 to 19
v1 grown from 19 to 28
v1 grown from 28 to 42
v1 grown from 42 to 63
v1 grown from 63 to 94
v1 grown from 94 to 141
v1 grown from 141 to 211
v1 grown from 211 to 316
v1 grown from 316 to 474
v1 grown from 474 to 711
v1 grown from 474 to 711
v1 grown from 711 to 1066

16

You might also like