A map is an associative container that stores data as key-value pairs, with each key uniquely associated with a value. It provides efficient access to values based on their corresponding keys.
- The implementation is based on an AVL tree to maintain balanced operations.
- It supports insertion, searching, deletion, updating, and traversal.
Example
A map stores each element as a key-value pair:
Key Value
10 50
20 80
30 90Here, 10, 20, and 30 are keys, while 50, 80, and 90 are their corresponding values.
Approach
The custom map is implemented using an AVL tree, which keeps the tree balanced to provide efficient operations.
- Create a Map node containing a key, value, child pointers, parent pointer, and height.
- Store the nodes according to their keys, similar to a Binary Search Tree.
- Balance the tree using AVL rotations after insertion and deletion.
- Store the complete Map implementation in a header file.
- Include the header file wherever the custom map is required.
Structure of the Custom Map
Each node of the AVL tree stores the key-value pair and the information required to maintain the tree.
- first stores the key.
- second stores the value associated with the key.
- left and right point to the child nodes.
- par points to the parent node.
- depth stores the height of the subtree.
- root stores the root of the AVL tree.
- cnt stores the number of elements in the map.
The basic structure is:
class Map {static Map* root;static int cnt;Map *left, *right, *par;int first, second, depth;};
Operations Supported by the Custom Map
The custom Map class implements the following operations:
1. insert()
This method is used to insert a key-value pair in the map. There are two insert() methods used here.
Syntax:
map.insert(first, second);
One is made public and it takes two parameters:
- first - It is the key
- second - It is the respective value of the key
void insert(int first, int second)
{
Map* temp = iterator(first);
// If element doesnot exist already
if (temp == nullptr)
insert(first)->second = second;
// If element exists already update it
else
temp->second = second;
}
The other one is made private. This method is called inside the operator overloading function of [].
- It takes only one parameter: first (It is the key).
- Creates the new node with "first" as the key and returns the instance of the node.
Syntax:
map[first] = second;
Map* insert(int first)
{
// Increase the number of elements
cnt++;
Map* newnode = create(first);
// If empty tree simply create the root
if (root == nullptr) {
root = newnode;
return root;
}
Map *temp = root, *prev = nullptr;
while (temp != nullptr) {
prev = temp;
if (first < temp->first)
temp = temp->left;
else if (first > temp->first)
temp = temp->right;
else {
free(newnode);
// If element already exists
// decrease the count
cnt--;
// If the key is found then it is
// returned by reference so that it is
// updatable
return temp;
}
}
if (first < prev->first)
prev->left = newnode;
else
prev->right = newnode;
newnode->par = prev;
// Once inserted Check and balance the tree
// at every node in path from "newnode" to "root"
balance(newnode);
// New object is inserted and returned to
// initialize in the main during assignment
return newnode;
}
int& operator[](int key) {
return insert(key)->second;
}
2. find()
It is used to find an element. This is a public method. It takes one parameter: first (which is the key) and returns the reference associated with the key. Internally it calls the private method iterator() to find the key
Syntax:
map.find(first);
Map* iterator(int first)
{
Map* temp = root;
while (temp != nullptr && temp->first != first) {
if (first < temp->first) {
temp = temp->left;
}
else {
temp = temp->right;
}
}
return temp;
}
Map* find(int first) {
return iterator(first);
}
Time Complexity: O(logN) where N is the size of the map.
3. update()
This value is used to update the value associated with a key. This is a public method. It takes two parameters:
- first: It is the key to be found.
- second: It is the new value of the key.
The function calls the iterator() function to get the instance of the key and updates the value associated to that key. If no such key exists then no operation is performed.
Syntax:
map.update(first, second);
void update(int first, int second)
{
Map* temp = iterator(first);
if (temp != nullptr) {
temp->second = second;
}
}
4. Accessing any key
Any value can be accessed using the subscript operator[]. The concept of method overloading is used to implement the functionality. It is a public method and the search() function is called inside the overloading function. It takes one parameter: first (it is the value of the key)
Syntax:
map[first];
const Map* iterator(int first) const
{
Map* temp = root;
while (temp != nullptr
&& temp->first != first) {
if (first < temp->first)
temp = temp->left;
else
temp = temp->right;
}
return temp;
}
const int search(int first) const
{
const Map* temp = iterator(first);
// If element exists with the given key
// return its value
if (temp != nullptr)
return temp->second;
// If element doesn't exist
// return default value of 0
return 0;
}
const int operator[](int key) const
{
// Search method is also qualified with const
return search(key);
}
5. erase()
Deletes the node with the given key, replaces it with its in-order predecessor or successor, and calls balance() on the affected node’s parent to maintain the AVL tree balance.
Syntax:
map.erase(first);
void erase(int first, Map* temp = root)
{
Map* prev = 0;
cnt--;
while (temp != 0 && temp->first != first) {
prev = temp;
if (first < temp->first) {
temp = temp->left;
}
else if (first > temp->first) {
temp = temp->right;
}
}
if (temp == nullptr) {
cnt++;
return;
}
if (cnt == 0 && temp == root) {
free(temp);
root = nullptr;
return;
}
Map* l = inorderPredecessor(temp->left);
Map* r = inorderSuccessor(temp->right);
if (l == 0 && r == 0) {
if (prev == 0) {
root = 0;
}
else {
if (prev->left == temp) {
prev->left = 0;
}
else {
prev->right = 0;
}
free(temp);
balance(prev);
}
return;
}
Map* start;
if (l != 0) {
if (l == temp->left) {
l->right = temp->right;
if (l->right != 0) {
l->right->par = l;
}
start = l;
}
else {
if (l->left != 0) {
l->left->par = l->par;
}
start = l->par;
l->par->right = l->left;
l->right = temp->right;
l->par = 0;
if (l->right != 0) {
l->right->par = l;
}
l->left = temp->left;
temp->left->par = l;
}
if (prev == 0) {
root = l;
}
else {
if (prev->left == temp) {
prev->left = l;
l->par = prev;
}
else {
prev->right = l;
l->par = prev;
}
free(temp);
}
balance(start);
return;
}
else {
if (r == temp->right) {
r->left = temp->left;
if (r->left != 0) {
r->left->par = r;
}
start = r;
}
else {
if (r->right != 0) {
r->right->par = r->par;
}
start = r->par;
r->par->left = r->right;
r->left = temp->left;
r->par = 0;
if (r->left != 0) {
r->left->par = r;
}
r->right = temp->right;
temp->right->par = r;
}
if (prev == 0) {
root = r;
}
else {
if (prev->right == temp) {
prev->right = r;
r->par = prev;
}
else {
prev->left = r;
r->par = prev;
}
free(temp);
}
balance(start);
return;
}
}
6. count()
This method returns the count of a key in the map. This is a public method. It takes one parameter: first(which is the key of value whose count should be found). This method calls the iterator() method internally and if no node is found then count is 0. Otherwise, count returns 1.
Syntax:
map.count(first);
int count(int first)
{
Map* temp = iterator(first);
// If key is found
if (temp != nullptr)
return 1;
// If key is not found
return 0;
}
7. size()
This method returns the size of the map. This is a public method. This method does not take any parameter.
Syntax:
map.size();
int size(void) {
return cnt;
}
8. empty()
This method checks if the map is empty or not. This is a public method. It returns true if the map is empty, else false. This method does not take any parameter.
Syntax:
map.empty();
bool empty(void)
{
if (root == 0)
return true;
return false;
}
9. clear()
This method is used to delete the whole map in. This is a public method. It does not take any parameter. It takes the erase() method internally.
Syntax:
map.clear();
void clear(void)
{
while (root != nullptr) {
erase(root->first);
}
}
10. iterate()
This method is used to traverse the whole map. This is a public method. It also does not take any parameter. The nodes are printed in the sorted manner of key.
Syntax:
map.iterate();
void iterate(Map* head = root)
{
if (root == 0)
return;
if (head->left != 0) {
iterate(head->left);
}
cout << head->first << ' ';
if (head->right != 0) {
iterate(head->right);
}
}
Creating the Custom Map Header File
Create a header file named map.h and place the complete Map class implementation in it.
#include <iostream>
using namespace std;
// Custom Map Class
class Map {
private:
Map* iterator(int first)
{
// A temporary variable created
// so that we do not
// lose the "root" of the tree
Map* temp = root;
// Stop only when either the key is found
// or we have gone further the leaf node
while (temp != nullptr &&
temp->first != first) {
// Go to left if key is less than
// the key of the traversed node
if (first < temp->first) {
temp = temp->left;
}
// Go to right otherwise
else {
temp = temp->right;
}
}
// If there doesn't exist any element
// with first as key, nullptr is returned
return temp;
}
// Returns the pointer to element
// whose key matches first.
// Specially created for search method
// (because search() is const qualified).
const Map* iterator(int first) const
{
Map* temp = root;
while (temp != nullptr
&& temp->first != first) {
if (first < temp->first) {
temp = temp->left;
}
else {
temp = temp->right;
}
}
return temp;
}
// The const property is used to keep the
// method compatible with the method "const
// int&[]operator(int) const"
// Since we are not allowed to change
// the class attributes in the method
// "const int&[]operator(int) const"
// we have to assure the compiler that
// method called(i.e "search") inside it
// doesn't change the attributes of class
const int search(int first) const
{
const Map* temp = iterator(first);
if (temp != nullptr) {
return temp->second;
}
return 0;
}
// Utility function to return the Map* object
// with its members initialized
// to default values except the key
Map* create(int first)
{
Map* newnode = (Map*)malloc(sizeof(Map));
newnode->first = first;
newnode->second = 0;
newnode->left = nullptr;
newnode->right = nullptr;
newnode->par = nullptr;
// Depth of a newnode shall be 1
// and not zero to differentiate
// between no child (which returns
// nullptr) and having child(returns 1)
newnode->depth = 1;
return newnode;
}
// All the rotation operation are performed
// about the node itself
// Performs all the linking done when there is
// clockwise rotation performed at node "x"
void right_rotation(Map* x)
{
Map* y = x->left;
x->left = y->right;
if (y->right != nullptr) {
y->right->par = x;
}
if (x->par != nullptr && x->par->right == x) {
x->par->right = y;
}
else if (x->par != nullptr && x->par->left == x) {
x->par->left = y;
}
y->par = x->par;
y->right = x;
x->par = y;
}
// Performs all the linking done when there is
// anti-clockwise rotation performed at node "x"
void left_rotation(Map* x)
{
Map* y = x->right;
x->right = y->left;
if (y->left != nullptr) {
y->left->par = x;
}
if (x->par != nullptr && x->par->left == x) {
x->par->left = y;
}
else if (x->par != nullptr && x->par->right == x) {
x->par->right = y;
}
y->par = x->par;
y->left = x;
x->par = y;
}
// Draw the initial and final graph of each
// case(take case where every node has two child)
// and update the nodes depth before any rotation
void helper(Map* node)
{
// If left skewed
if (depthf(node->left)
- depthf(node->right) > 1) {
// If "depth" of left subtree of
// left child of "node" is
// greater than right
// subtree of left child of "node"
if (depthf(node->left->left)
> depthf(node->left->right)) {
node->depth
= max(depthf(node->right) + 1,
depthf(node->left->right) + 1);
node->left->depth
= max(depthf(node->left->left) + 1,
depthf(node) + 1);
right_rotation(node);
}
// If "depth" of right subtree
// of left child of "node" is
// greater than
// left subtree of left child
else {
node->left->depth = max(
depthf(node->left->left) + 1,
depthf(node->left->right->left)
+ 1);
node->depth
= max(depthf(node->right) + 1,
depthf(node->left->right->right) + 1);
node->left->right->depth
= max(depthf(node) + 1,
depthf(node->left) + 1);
left_rotation(node->left);
right_rotation(node);
}
}
// If right skewed
else if (depthf(node->left)
- depthf(node->right) < -1) {
// If "depth" of right subtree of right
// child of "node" is greater than
// left subtree of right child
if (depthf(node->right->right)
> depthf(node->right->left)) {
node->depth
= max(depthf(node->left) + 1,
depthf(node->right->left) + 1);
node->right->depth
= max(depthf(node->right->right) + 1,
depthf(node) + 1);
left_rotation(node);
}
// If "depth" of left subtree
// of right child of "node" is
// greater than that of right
// subtree of right child of "node"
else {
node->right->depth = max(
depthf(node->right->right) + 1,
depthf(node->right->left->right) + 1);
node->depth = max(
depthf(node->left) + 1,
depthf(node->right->left->left) + 1);
node->right->left->depth
= max(depthf(node) + 1,
depthf(node->right) + 1);
right_rotation(node->right);
left_rotation(node);
}
}
}
// Balancing the tree about the "node"
void balance(Map* node)
{
while (node != root) {
int d = node->depth;
node = node->par;
if (node->depth < d + 1) {
node->depth = d + 1;
}
if (node == root
&& depthf(node->left)
- depthf(node->right) > 1) {
if (depthf(node->left->left)
> depthf(node->left->right)) {
root = node->left;
}
else {
root = node->left->right;
}
helper(node);
break;
}
else if (node == root
&& depthf(node->left)
- depthf(node->right)
< -1) {
if (depthf(node->right->right)
> depthf(node->right->left)) {
root = node->right;
}
else {
root = node->right->left;
}
helper(node);
break;
}
helper(node);
}
}
// Utility method to return the
// "depth" of the subtree at the "node"
int depthf(Map* node)
{
if (node == nullptr)
// If it is null node
return 0;
return node->depth;
}
// Function to insert a value in map
Map* insert(int first)
{
cnt++;
Map* newnode = create(first);
if (root == nullptr) {
root = newnode;
return root;
}
Map *temp = root, *prev = nullptr;
while (temp != nullptr) {
prev = temp;
if (first < temp->first) {
temp = temp->left;
}
else if (first > temp->first) {
temp = temp->right;
}
else {
free(newnode);
cnt--;
return temp;
}
}
if (first < prev->first) {
prev->left = newnode;
}
else {
prev->right = newnode;
}
newnode->par = prev;
balance(newnode);
return newnode;
}
// Returns the previous node in
// inorder traversal of the AVL Tree.
Map* inorderPredecessor(Map* head)
{
if (head == nullptr)
return head;
while (head->right != nullptr) {
head = head->right;
}
return head;
}
// Returns the next node in
// inorder traversal of the AVL Tree.
Map* inorderSuccessor(Map* head)
{
if (head == nullptr)
return head;
while (head->left != nullptr) {
head = head->left;
}
return head;
}
public:
// Root" is kept static because it's a class
// property and not an instance property
static class Map* root;
static int cnt;
// "first" is key and "second" is value
Map *left, *right, *par;
int first, second, depth;
// overloaded [] operator for assignment or
// inserting a key-value pairs in the map
// since it might change the members of
// the class therefore this is
// invoked when any assignment is done
int& operator[](int key) {
return insert(key)->second;
}
// Since we have two methods with
// the same name "[]operator(int)" and
// methods/functions cannot be
// distinguished by their return types
// it is mandatory to include a const
// qualifier at the end of any of the methods
// This method will be called from a const
// reference to the object of Map class
// It will not be called for assignment
// because it doesn't allow to change
// member variables
// We cannot make it return by reference
// because the variable "temp" returned
// by the "search" method is
// statically allocated and therefore
// it's been destroyed when it is called out
const int operator[](int key) const
{
return search(key);
}
// Count returns whether an element
// exists in the Map or not
int count(int first)
{
Map* temp = iterator(first);
if (temp != nullptr) {
return 1;
}
return 0;
}
// Returns number of elements in the map
int size(void) {
return cnt;
}
// Removes an element given its key
void erase(int first, Map* temp = root)
{
Map* prev = nullptr;
cnt--;
while (temp != nullptr &&
temp->first != first) {
prev = temp;
if (first < temp->first) {
temp = temp->left;
}
else if (first > temp->first) {
temp = temp->right;
}
}
if (temp == nullptr) {
cnt++;
return;
}
if (cnt == 0 && temp == root) {
free(temp);
root = nullptr;
return;
}
Map* l
= inorderPredecessor(temp->left);
Map* r
= inorderSuccessor(temp->right);
if (l == nullptr && r == nullptr) {
if (prev == nullptr) {
root = nullptr;
}
else {
if (prev->left == temp) {
prev->left = nullptr;
}
else {
prev->right = nullptr;
}
free(temp);
balance(prev);
}
return;
}
Map* start;
if (l != nullptr) {
if (l == temp->left) {
l->right = temp->right;
if (l->right != nullptr) {
l->right->par = l;
}
start = l;
}
else {
if (l->left != nullptr) {
l->left->par = l->par;
}
start = l->par;
l->par->right = l->left;
l->right = temp->right;
l->par = nullptr;
if (l->right != nullptr) {
l->right->par = l;
}
l->left = temp->left;
temp->left->par = l;
}
if (prev == nullptr) {
root = l;
}
else {
if (prev->left == temp) {
prev->left = l;
l->par = prev;
}
else {
prev->right = l;
l->par = prev;
}
free(temp);
}
balance(start);
return;
}
else {
if (r == temp->right) {
r->left = temp->left;
if (r->left != nullptr) {
r->left->par = r;
}
start = r;
}
else {
if (r->right != nullptr) {
r->right->par = r->par;
}
start = r->par;
r->par->left = r->right;
r->left = temp->left;
r->par = nullptr;
if (r->left != nullptr) {
r->left->par = r;
}
r->right = temp->right;
temp->right->par = r;
}
if (prev == nullptr) {
root = r;
}
else {
if (prev->right == temp) {
prev->right = r;
r->par = prev;
}
else {
prev->left = r;
r->par = prev;
}
free(temp);
}
balance(start);
return;
}
}
// Returns if the map is empty or not
bool empty(void)
{
if (root == nullptr)
return true;
return false;
}
// Given the key of an element it updates
// the value of the key
void update(int first, int second)
{
Map* temp = iterator(first);
if (temp != nullptr) {
temp->second = second;
}
}
// Deleting the root of
// the tree each time until the map
// is not empty
void clear(void)
{
while (root != nullptr) {
erase(root->first);
}
}
// Inorder traversal of the AVL tree
void iterate(Map* head = root)
{
if (root == nullptr)
return;
if (head->left != nullptr) {
iterate(head->left);
}
cout << head->first << ' ';
if (head->right != nullptr) {
iterate(head->right);
}
}
// Returns a pointer/iterator to the element
// whose key is first
Map* find(int first) {
return iterator(first);
}
// Overloaded insert method,
// takes two parameters - key and value
void insert(int first, int second)
{
Map* temp = iterator(first);
if (temp == nullptr) {
insert(first)->second = second;
}
else {
temp->second = second;
}
}
};
Map* Map::root = nullptr;
int Map::cnt = 0;
Now save it as a header file say map.h to include it in other codes and implement the functionalities.
Executing the Custom Map
Follow these steps to use the custom map:
- Create the header file as map.h.
- Store map.h in the same directory as the C++ source file.
- Include the header using #include "map.h".
- Create a Map object and use its supported operations.
- Compile and run the program.
Examples
The following examples demonstrate the operations supported by the custom Map class.
Example 1: Programs to demonstrate the use of insert(), accessing any key and update() methods
#include "map.h"
#include <iostream>
using namespace std;
int main()
{
Map map;
// 1st way of insertion
map[132] = 3;
map[34] = 5;
map[42] = -97;
map[22] = 10;
map[12] = 42;
// 2nd way of insertion
map.insert(-2,44);
map.insert(0,90);
// accessing elements
cout<<"Value at key 42 before updating = "
<<map[42]<<endl;
cout<<"Value at key -2 before updating = "
<<map[-2]<<endl;
cout<<"Value at key 12 before updating = "
<<map[12]<<endl;
// Updating value at key 42
map[42] = -32;
// Updating value at key -2
map.insert(-2,8);
// Updating value at key 12
map.update(12,444);
// accessing elements
cout<<"Value at key 42 after updating = "
<<map[42]<<endl;
cout<<"Value at key -2 after updating = "
<<map[-2]<<endl;
cout<<"Value at key 12 after updating = "
<<map[12]<<endl;
cout<<"Value at key 0 = "<<map[0]<<endl;
return 0;
}
Output

Example 2: Programme to demonstrate the use of erase(), clear() and iterate() methods
#include "map.h"
#include <iostream>
using namespace std;
int main()
{
Map map;
map[132] = 3;
map[34] = 5;
map[42] = -97;
map[22] = 10;
map[12] = 42;
// Iterating the Map elements before erasing 22
map.iterate();
map.erase(22);
// Iterating the Map elements after erasing 22
map.iterate();
// Deleting the whole map
map.clear();
// Now since there are zero elements
// in the Map the output is blank
cout<<"\nElements in Map after clear operation: ";
map.iterate();
return 0;
}
Output:

Example 3: Programme to demonstrate the use of find(), count(), empty() and size() methods
#include "map.h"
#include <iostream>
using namespace std;
int main()
{
Map map;
map[132] = 3;
map[34] = 5;
map[42] = -97;
cout<<"Value at 132 before updating = "
<<map[132]<<endl;
// Find method returns pointer to element
// whose key matches given key
Map *it = map.find(132);
// Updating the value at key 132
it->second = 98;
cout<<"Value at 132 after updating = "
<<map[132]<<endl;
// Count of an element which is not present
// in the map is 0
cout<<"Count of 77 = "<<map.count(77)<<endl;
// Count of an element which is present
// in the map is 1
cout<<"Count of 34 = "<<map.count(34)<<endl;
// Size of map/number of elements in map
cout<<"Map size = "<<map.size()<<endl;
// Map is not empty therefore returned 0
cout<<"Is map empty: "<<map.empty()<<endl;
// Clearing the map
map.clear();
// Map is empty therefore return 1
cout<<"Is map empty: "<<map.empty()<<endl;
return 0;
}
Output
