Containers in STL : Associative Containers


Associative containers always store data in sorted fashion. Associative containers are very efficient in searching a value with O( log n ) runtime complexity because of its sorted nature which allows us to perform binary search.

=> How are associative containers implemented ?

Associative containers are implemented through self balancing binary search trees, more precisely red black trees.

When we insert an element in the container, it will be inserted in the tree in it proper location and if we remove an element from the container, the tree will be rearranged/balanced so that everything is still in order.

The default sorting criteria for associative containers is '<'(less than), though a custom criteria can also be provided.

There are 4 associative containers in C++ STL :
1. set
2. map
3. multiset
4. multimap


1. set and multiset


Set cannot have duplicate items so if you try to insert an already existing value, it will not be inserted.

set<int> st;             // create a set of integers

=> Inserting an element

st.insert(5);            // st = {5};           
st.insert(1);            // st= {1,5};
st.insert(3);            // st= {1,3,5};


After insertion of every element, the data in set is automatically sorted.

Time complexity : O(log n)    , irrespective of where the value is inserted (in beginning, in middle or somewhere else) because it takes logarithmic time to find its correct position in sorted order in a binary search tree.

This member function returns a pair of values :
pair<set<int>::iterator, bool>

the bool value indicates whether insertion is successful(1) or it failed(0) because of duplicate value.

If it succeeded, then the iterator points to this new value, else if it failed it points to the existing duplicate value corresponding to the value we tried to insert.

=> Searching an element

This is one of the most important feature of associative container. They provide logarithmic time searching as compared to sequence containers which provide linear time searching.

set<int>::iterator it = st.find(3);
Time complexity : O(log n)
Note: sequence containers don't even provide a find function.


=> Erasing an element

a. using iterator
st.erase(it);           
Time complexity : O(1)


b. erase by value
st.erase(3);           

Time complexity : O(log n)

This member function automatically finds the element in the set and erases it.

Note : sequence containers don't provide this kind of erase.
 

=> Traversing set

We use the universal iterator traversal to traverse the elements(which is in sorted order).

for(auto it=st.begin();it!=st.end();it++)
    cout<<*it;


Now. let's discuss multiset.

As you might be guessing, multiset is almost similar to set, it just has one difference that it allows duplicates.
It also stores the values in sorted order but may have multiple occurrences of the same value(unlike set).

multiset<int> ms;
ms.insert(3);                // ms={3};
ms.insert(5);                // ms = {3,5};
ms.insert(3);                // ms = {3,3,5};


All the functions provided by set are also provided by multiset.

Note : A very important point to remember is that both set and multiset are read-only, i.e values in the container can not be modified.

auto it=st.find(10);
*it = 10;                     // assignment not allowed.


Why ?
Think about this, if we modify any value inside a set/multiset, the container may get corrupted as the elements may not be sorted anymore .The container might lose its most important property due to which it offered its advantages.

Properties of set/multiset :


1. Fast searching of elements. Time complexity : O(log n)
2. No random access operator ( like vector/deque)
3. Slow traversal

 

2. map and multimap

Sometimes we don't want to sort the container by values of elements, but by some key provided for every value.
They have the same interface as set/multiset except that instead of directly storing values they store key-value pairs.
Keys are actually used to position the elements in map/multimap, the value just corresponds to the key.
map, just like set does not allow duplicate keys.

Create a map from int(key) to char(value)
map<int,char> mp;   

=> Inserting a value

A pair of values is inserted in map/multimap
Different forms :
1. mp.insert(pair<int,char>(10,'c'));
2. Use make_pair function
    mp.insert(make_pair(50,'b'));

3. pair<int,char> p = make_pair(25,'b');
    mp.insert(p);

4. Map also provides random access by key value using random access operator ('[ ]'). 

    mp[15]='c';    // inserts pair(15,'c') in map if not already present otherwise updates the value
 

Time Complexity : O(log n), similar to set

=> Searching a key

mp.find(20);

Searches for this key and gets the key-value pair.

Time Complexity : O(log n) 

 

 => Traversing map

Use universal iterator traversal to traverse in sorted order. We can access the key and value using the members of pair: first and second respectively.

for auto it=mp.begin();it!=mp.end();it++)
    cout<<(*it).first<<" "<<(*it).second;


(*it).first can also be written as it->first. Similarly,
(*it).second can also be written as it->second.
This form is preferred mostly as it's more easier and takes less time.

multimap is similar to map, just the difference is that it allows duplicate keys.

Note for multiset and multimap : find() member function returns an iterator to one of the value(multiset) or key(multimap) in case of duplicates, which is typically the first one.   

Note for map/multimap :  Keys cannot be modified inside the container but their corresponding values can be changed.

auto it = mp.find(10);
(*it).first = '20';                 // error
(*it).second = 'f';               // No error


=> Now you might be wondering, why are all these containers called as "associative" ?

The term "associative" actually refers to the key-value nature of map which associates a value with a key. Think of set/multiset as special kind of map where the key of any element is the same as its value.


=> How to provide custom sorting criteria in associative containers?

We need to use functor (a class/structure that overloads the () operator so it can be called like a function).

struct comp {
    bool operator() (int x, int y) {       // this is our custom comparator which overrides default comparator
        return x>y;                           // sort by decreasing order
    }
};


Then declare set using this functor
set<int, comp> st;                        // this will sort the set in decreasing order

Note : sort in decreasing order can also be written as set<int, greater<int> > st;

=> Using associative container for user defined data types :

If we wish to stored user defined data types in associative container, we must provide custom comparison function otherwise compiler shows error as there is no internal implementation available.

Consider a structure

struct Person{
    int age;
    string name;
};


Let's define a set for Person.

We define structure 'comp' as discussed before which provides custom comparison function. We sort the set by age.

struct comp {
    bool operator() (Person x, Person y) {       
        return x.age > y.age;
    }
};


Now declare set simply as:
set<Person, comp> st;

Other associative containers can also be implemented in similar way.
 

=> Finally before the end of the article, let's check out two important member functions provided by associative containers which are very commonly used for multiset and multimap :
lower_bound(val) and upper_bound(val).

lower_bound(val) : searches for the first position corresponding to the value(or key in multimap) 'val'.
upper_bound(val) : searches for the first position corresponding to the first greater value(or key in multimap) 'val'.

For example :

 

To get total number of elements with value 10, use :
int total_count = st.upper_bound(10) - st.lower_bound(10);

This completes our discussion of Associative containers. If you have any doubts. please mention in the comments section.

Also check out other types of C++ STL containers if you missed them out :
1. Sequence Containers
2. Unordered Containers



Thank you for your patience reading. If you enjoyed this post, I’d be very grateful if you’d help it spread by emailing it to a friend, or sharing it on Whatsapp or Facebook.

😇Happy Learning!!