Data Structures Road Map Part 2
Data Structures Road Map Part 2
Data structures and algorithms (Egypt Japan University Of Science & Technology)
2-https://www.youtube.com/watch?v=tEVTakjxSQ&list=PL1DUmTEdeA6JlommmGP5wicYLxX5PVCQt&index=14
after watching the videos go on and read the following from Geeks for Geeks or you can read from here:
https://www.geeksforgeeks.org/introduction-to-tree-data-structure-and-algorithm-tutorials/
A tree data structure is a hierarchical structure that is used to represent and organize data in a way that is easy to navigate and
search. It is a collection of nodes that are connected by edges and has a hierarchical relationship between the nodes. The topmost
node of the tree is called the root, and the nodes below it are called the child nodes. Each node can have multiple child nodes, and
these child nodes can also have their own child nodes, forming a recursive structure.
Recursive Definition:
A tree consists of a root, and zero or more subtrees T1, T2, … , Tk such that there is an edge from the
root of the tree to the root of each subtree.
Child Node: The node which is the immediate successor of a node is called the child node of that node. Examples: {D, E} are
the child nodes of {B}.
Root Node: The topmost node of a tree or the node which does not have any parent node is called the root node. {A} is the root
node of the tree. A non-empty tree must contain exactly one root node and exactly one path from the root to all other nodes of
the tree.
Leaf Node or External Node: The nodes which do not have any child nodes are called leaf nodes. {K, L, M, N, O, P} are the
leaf nodes of the tree.
Ancestor of a Node: Any predecessor nodes on the path of the root to that node are called Ancestors of that node. {A,B} are
the ancestor nodes of the node {E}
Descendant: Any successor node on the path from the leaf node to that node. {E,I} are the descendants of the node {B}.
Sibling: Children of the same parent node are called siblings. {D,E} are called siblings.
Level of a node: The count of edges on the path from the root node to that node. The root node has level 0.
Internal node: A node with at least one child is called Internal Node.
Neighbour of a Node: Parent or child nodes of that node are called neighbors of that node.
Properties of a Tree:
Number of edges: An edge can be defined as the connection between two nodes. If a tree has N nodes then it will have (N-1)
edges. There is only one path from each node to any other node of the tree.
Depth of a node: The depth of a node is defined as the length of the path from the root to that node. Each edge adds 1 unit of
length to the path. So, it can also be defined as the number of edges in the path from the root of the tree to the node.
Height of a node: The height of a node can be defined as the length of the longest path from the node to a leaf node of the
tree.
Height of the Tree: The height of a tree is the length of the longest path from the root of the tree to a leaf node of the tree.
Degree of a Node: The total count of subtrees attached to that node is called the degree of the node. The degree of a leaf node
must be 0. The degree of a tree is the maximum degree of a node among all the nodes in the tree.
Traversing in a tree is done by depth first search and breadth first search algorithm.
It has no self-loop
1. General tree (source main book , Dr. Samir explains from it)
A general tree data structure has no restriction on the number of nodes. It means that a parent node can have any number of child
nodes.
Ordered Trees
A tree is ordered if there is a linear ordering defined for the children of each node;
that is, we can identify children of a node as being the first, second, third, and so
on. Such an ordering is determined by how the tree is to be used, and is usually indicated by drawing the tree with siblings arranged
from left to right, corresponding
to their linear relationship. Ordered trees typically indicate the linear order relationship existing between siblings by listing them in a
sequence or iterator in the
correct order.
Tree Functions
p.parent( ): Return the parent of p; an error occurs if p is the root.
p.children( ): Return a position list containing the children of node p.
p.isRoot( ): Return true if p is the root and false otherwise.
p.isExternal( ): Return true if p is external and false otherwise.
isRoot O(1)
isExternal O(1)
parent O(1)
children(p) O(cp)
size O(1)
empty O(1)
root O(1)
positions O(n)
Note:
Let p be a node of a tree T. The depth of p is the number of ancestors of p, excluding p itself. For example, in the tree of Figure 7.2,
the node storing International has depth 2. Note that this definition implies that the depth of the root of T is 0.
The depth of p’s node can also be recursively defined as follows:
• If p is the root, then the depth of p is 0
• Otherwise, the depth of p is one plus the depth of the parent of p
Based on the above definition, the recursive algorithm depth(T, p) shown in Code Fragment 7.3, computes the depth of a node
referenced by position p of T by calling itself recursively on the parent of p, and adding 1 to the value returned.
2. Binary tree
struct Node //or by using oop we can use class Node and proceed into our implementations
{
int data;
struct Node *left_child;
struct Node *right_child;
};
A node of a binary tree can have a maximum of two child nodes. In the given tree diagram, node B, D, and F are left children, while
E, C, and G are the right children.
#include<iostream>
using namespace std;
class Node {
public:
int data; // data value stored in the node
Node* right, * left; // pointers to the right and left child nodes
Node(int value) {
// constructor to initialize the node with the given value and set left and right child nodes to NULL.
data = value;
right = left = NULL;
}
};
class BST {
public:
Node* root; // pointer to the root node of the binary search tree
BST() {
root = NULL; // constructor to initialize the root pointer to NULL
}
void Preorder(Node* r) { // a recursive function to traverse the binary search tree in preorder (root left right)
if (r == NULL) return; // if r is NULL, return
cout << r->data << "\t"; // print the data value of the root node
Preorder(r->left); // traverse the left subtree in preorder
Preorder(r->right); // traverse the right subtree in preorder
}
void Inorder(Node* r) { // a recursive function to traverse the binary search tree in inorder (left root right)
if (r == NULL) return; // if r is NULL, return
Inorder(r->left); // traverse the left subtree in inorder
cout << r->data << "\t"; // print the data value of the root node
Inorder(r->right); // traverse the right subtree in inorder
}
void Postorder(Node* r) { // a recursive function to traverse the binary search tree in postorder (left right root)
if (r == NULL) return; // if r is NULL, return
Postorder(r->left); // traverse the left subtree in postorder
Postorder(r->right); // traverse the right subtree in postorder
cout << r->data << "\t"; // print the data value of the root node
}
Node* Search(Node* r, int key) {
if (r == NULL) return NULL;
else if (r->data == key)return r;
else if (key < r->data) return Search(r->left, key);
else return Search(r->right, key);
}
Node* Findmin(Node* r) {
if (r == NULL)
return NULL;
else if (r->left == NULL)
return r;
else
return Findmin(r->left);
}
Node* Findmax(Node* r) {
if (r == NULL)
return NULL;
else if (r->right == NULL)
return r;
else
return Findmax(r->right);
}
// This method takes in a root node 'r' and an integer value 'key' that needs to be deleted from the BST.
Node* Delete(Node* r, int key) {
// If the value of 'key' is less than the value of the current node's data, call 'Delete' function recursively on the left child.
if (key < r->data)
r->left = Delete(r->left, key);
// If the value of 'key' is greater than the value of the current node's data, call 'Delete' function recursively on the right child.
else if (key > r->data)
r->right = Delete(r->right, key);
// If the value of 'key' matches the value of the current node's data.
else {
// If the current node is a leaf node, delete it by setting it to null.
if (r->left == NULL && r->right == NULL) {
r = NULL;
}
// If the current node has only one child (at the left), replace it with the child node and delete the parent node.
else if (r->left != NULL && r->right == NULL) {
r->data = r->left->data;
delete r->left;
r->left = NULL;
}
// If the current node has only one child (at the right), replace it with the child node and delete the parent node.
else if (r->left == NULL && r->right != NULL) {
r->data = r->right->data;
delete r->right;
r->right = NULL;
}
// If the current node has two children, replace it with the maximum node in its left subtree and recursively delete the maximum node
else {
Node* max = Findmax(r->left);
r->data = max->data;
r->left = Delete(r->left, max->data);
}
}
// Return the root node of the modified BST.
return r;
}
};
int main() {
BST btree; // create an object of the BST class
int arr[9] = { 45,15,79,90,10,55,12,20,50 }; // an array of integers to be inserted in the binary search tree
for (int i = 0; i < 9; i++) {
btree.Insert(arr[i]); // insert each integer in the binary search tree
}
btree.Inorder(btree.root);
cout << "\n--------------------------------------------------------\n";
btree.Preorder(btree.root);
cout << "\n--------------------------------------------------------\n";
btree.Postorder(btree.root);
cout << "\n--------------------------------------------------------\n";
// Declare an integer variable 'key' to store the value that the user wants to search for.
int key;
// Call the 'Search' function of a binary search tree object 'btree' to check if 'key' is in the tree.
// If it is found, print "Item Found"; otherwise, print "Sorry, item not found".
if (btree.Search(key)) {
cout << "Item Found" << endl;
}
else {
// Find the minimum element of the binary search tree by calling the 'Findmin' function of 'btree'.
// If the tree is empty (i.e., the root is null), print "No elements in the Tree";
// otherwise, print the minimum element's value.
cout << "Find minimum\n";
Node* min = btree.Findmin(btree.root);
if (min == 0) {
cout << "No elements in the Tree" << endl;
}
else {
cout << min->data << endl;
}
// Find the maximum element of the binary search tree by calling the 'Findmax' function of 'btree'.
// If the tree is empty (i.e., the root is null), print "No elements in the Tree";
// otherwise, print the maximum element's value.
cout << "Find maximum\n";
Node* max = btree.Findmax(btree.root);
if (max == 0) {
cout << "No elements in the Tree" << endl;
}
else {
cout << max->data << endl;
}
// Delete two items (12 and 15) from the binary search tree by calling the 'Delete' function of 'btree'.
// The 'Delete' function returns a pointer to the root of the modified tree.
Node* result = btree.Delete(btree.root, 12);
cout << "\nPreorder after delete 12\n";
// Print the elements of the modified tree in preorder traversal by calling the 'Preorder' function of 'btree'.
btree.Preorder(result);
3. Balanced tree
If the height of the left sub-tree and the right sub-tree is equal or differs at most by 1, the tree is known as a balanced tree.
Priority queues
first we will summarize the ADT methods and terminologies
Applications require comparing and ranking objects based on parameters called "keys"
Keys are assigned to elements as specific attributes and can identify, rank, or weigh them
Keys may represent properties that an element did not originally possess
The key assigned to an element may not be unique and can be changed by the application
Keys can sometimes be complex properties that cannot be quantified with a single number
Keys can be extracted from the object itself or externally generated by the application.
A priority queue needs a comparison rule that defines a total order relation
The total order relation must satisfy the reflexive, antisymmetric, and transitive properties
A priority queue is a container of elements, each associated with a key that determines the priority
The fundamental functions of a priority queue are insert, min, and removeMin
The removeMin function removes the same element returned by min, which is the element with the smallest associated key
value
The example given is a priority queue of standby passengers for a fully booked flight, where the priority of each passenger is
determined by fare paid, frequent-flyer status, and time of insertion
Comparators
The issue of how to compare elements in a priority queue is important and has different solutions.
Implementing a different priority queue for each element type and comparison method is a direct but not very general solution.
A better solution is to design the priority queue as a templated class, where the element type is specified as an abstract
template argument (E) and each concrete class provides a means for comparing two objects of type E.
This could be done through a "comp" function or overloading the C++ comparison operator "<".
A concrete example is provided with a class Point2D that defines a two-dimensional point and a lexicographical less-than
operator based on the relative values of x and y coordinates.
This approach of overloading the relational operators is general enough for many situations, but it relies on the assumption that
objects of the same type are always compared in the same way. There are situations, however, where it is desirable to apply
different comparisons to objects of the same type. Consider the following examples.
Example 8.2: There are at least two ways of comparing the C++ character strings, "4" and "12". In the lexicographic ordering, which
is an extension of the alphabetic ordering to character strings, we have "4" > "12". But if we interpret these strings as integers, then
"4" < "12".
1. The composition method involves defining each entry of a priority queue as a pair (e,k), where e is the element and k is the key
that defines the priority ordering. Each key object defines its own comparison function. This approach is very general and is
studied in Chapter 9.
2. The approach used is based on defining a special object called a comparator, whose job is to provide a definition of the
comparison function between any two elements. A comparator for element type E can be implemented as a class that defines a
single function whose job is to compare two objects of type E. It is possible to derive all the other relational operators by
combining less-than comparisons with other boolean operators.
The code fragment 8.2 shows a generic function printSmaller that takes two arguments and prints the smaller of the two. The
function is templated by the element type E and the comparator type C, where C implements a less-than function for two objects of
type E. The function takes three arguments, the two elements to be compared, p and q, and an instance of a comparator, isLess ,
for these elements. It then uses the comparator to determine which element is smaller and prints that value.
The comparator approach is a bit less general than the composition method, because the comparator bases its decisions on the
contents of the elements themselves. In the composition method, the key may contain information that is not part of the element
object. The comparator approach has the advantage of being simpler, since we can insert elements directly into our priority queue
without creating element-key pairs. Furthermore, in Exercise R-8.4 we show that there is no real loss of generality in using
comparators
Having described the priority queue abstract data type at an intuitive level, we now describe it in more detail. As an ADT, a priority
queue P supports the following functions:
size(): Return the number of elements in P. empty(): Return true if P is empty and false otherwise. insert(e): Insert a new element
e into P.
min(): Return a reference to an element of P with the smallest associated key value (but do not remove it); an error condition
occurs if the priority queue is empty.
removeMin(): Remove from P the element referenced by min(); an error condition occurs if the priority queue is empty.
As mentioned above, the primary functions of the priority queue ADT are the insert, min, and removeMin operations. The other
functions, size and empty, are generic collection operations. Note that we allow a priority queue to have multiple entries with the
same key.
insert(5) – {5}
insert(9) – {5,9}
insert(2) – {2,5,9}
insert(7) – {2,5,7,9}
removeMin() – {5,7,9}
size() 3 {5,7,9}
removeMin() – {7,9}
removeMin() – {9}
removeMin() – {}
empty() true {}
removeMin() “error” {}
Before discussing specific implementations of the priority queue, we first define an informal C++ interface for a priority queue in
Code Fragment 8.4. It is not a complete C++ class, just a declaration of the public functions.
Although the comparator type C is included as a template argument, it does not appear in the public interface. Of course, its value is
relevant to any concrete implementation
The function min returns a constant reference to the element in the queue, meaning that its value can be read but not modified. The
member functions size , empty , and min are all declared as constant. An error is thrown if the functions min or removeMin are called
on an empty priority queue, which is signaled by an exception of type QueueEmpty .
1. In the first phase, we put the elements of L into an initially empty priority queue P through a series of n insert operations, one
for each element.
2. In the second phase, we extract the elements from P in nondecreasing order by means of a series of n combinations of min
and removeMin operations, putting them back into L in order.
Pseudo-code for this algorithm is given in Code Fragment 8.5. It assumes that L is given as an STL list, but the code can be
adapted to other containers.
Algorithm PriorityQueueSort(L,P):
Input: An STL list L of n elements and a priority queue, P,
that compareselements using a total order relation
Output: The sorted list L
while !L.empty() do
e ← L.front
L.pop front() {remove an element e from the list}
P.insert(e) {. . . and it to the priority queue}
while !P.empty() do
e ← P.min()P.removeMin() {remove the smallest element e from the queue}
L.push back(e) {. . . and append it to the back of L}
The algorithm works for any priority queue P regardless of its implementation.
The running time of the algorithm depends on the running times of operations insert, min, and removeMin, which depend on
how P is implemented.
PriorityQueueSort is considered more a sorting "scheme" than a sorting "algorithm" because it doesn't specify how P is
implemented.
The PriorityQueueSort scheme is the paradigm of several popular sorting algorithms, including selection-sort, insertion-sort, and
heap-sort, which are discussed in the chapter.
The C++ Standard Template Library (STL) provides an implementation of a priority queue called priority_queue.
The priority_queue is a container in the STL, and its definition file is called "queue."
The priority_queue class is templated with three parameters: the base type of the elements, the underlying STL container, and
the comparator object.
The second parameter (the underlying container) defaults to the STL vector, and the third parameter (the comparator) defaults
to using the standard C++ less-than operator.
The STL priority queue uses comparators in the same way as defined in Section 8.1.2, where a comparator is a class that
overrides the "()" operator to define a boolean function that implements the less-than operator.
Two STL priority queues are defined in the code fragment, one storing integers and the other storing two-dimensional points
under the left-to-right ordering.
#include <queue>
using namespace std; // make std accessible
priority_queue<int> p1; // a priority queue of integers
// a priority queue of
// points with left-to-right order
priority_queue<Point2D, vector<Point2D>, LeftRight> p2;
#include<iostream>
#include<queue>
pq.push(100);
while(!pq.empty()){
cout << pq.top() << endl;
pq.pop();
}
}
empty(): Return true if the priority queue is empty and false otherwise.
top(): Return a constant reference to the largest element of the priority queue.
Other than the differences in function names, the most significant difference between our interface and the STL priority queue is that
the functions top and pop access the largest item in the queue according to priority order, rather than the smallest. An example of
the usage of the STL priority queue is shown in Code Fragment 8.6.
Insertion of an element e into P can be done in O(1) time by adding e to the end of L using L.push back(e).
Since the list is unsorted, both min and removeMin operations require searching all elements of the list, taking O(n) time each,
where n is the number of elements in P.
Size and empty functions can be implemented by returning the corresponding functions of list L.
Using an unsorted list for P results in constant-time insertion but linear-time search and removal.
Function min is implemented by accessing the first element of the list, and removeMin is implemented by removing the first
element of the list.
Insert function requires scanning through the list to find the appropriate position, making it take O(n) time.
If the list is implemented as a doubly linked list, then min and removeMin operations take O(1) time.
In summary, a sorted list implementation allows for fast queries and deletions, but slow insertions.
We assume that the list is implemented by a doubly linked list. The space requirement is O(n)
Selection-Sort
The priority queue implementation using an unsorted list has a time complexity of O(n) for the first phase of PriorityQueueSort
since each element can be inserted in constant time.
In the second phase, the time complexity of each min and removeMin operation is proportional to the number of elements
currently in the unsorted list.
The bottleneck in this implementation is the repeated selection of the minimum element from an unsorted list in the second
phase.
The algorithm implemented using an unsorted list for the priority queue is known as selection-sort.
As noted above, the bottleneck is the second phase, where we repeatedly remove an element with smallest key from the priority
queue P. The size of P starts at n and decreases to 0 with each removeMin. Thus, the first removeMin operation takes time O(n),
the second one takes time O(n-1), and so on. Therefore, the total time needed for the second phase is
Insertion-sort
Implementing priority queue P using a sorted list improves the second phase's running time to O(n) because min and
removeMin operations take O(1) time.
However, the first phase now becomes the bottleneck because each insert operation takes time proportional to the size of P in
the worst case.
This sorting algorithm is called insertion-sort because the bottleneck involves the repeated "insertion" of a new element at the
appropriate position in a sorted list.
We can improve the performance of insertion-sort by changing our definition to insert elements from the end of the priority
queue sequence in the first phase.
This would make the worst-case running time of the first phase O(n^2), but the best-case running time O(n) for a pre-sorted
input list.
In this modified insertion-sort, the running time is O(n+I), where I is the number of inversions in the input list.
Inversions refer to pairs of elements in the input list that start out in the wrong relative order.
Thus, insertion-sort performs well on nearly sorted or partially sorted lists with few inversions, but poorly on highly unsorted lists
with many inversions.
Heaps
There are two implementations of the PriorityQueueSort scheme: selection-sort and insertion-sort.
Selection-sort has a fast first phase but slow second phase, while insertion-sort has a slow first phase but a fast second phase.
Balancing the running times of the two phases could speed up the overall running time for sorting.
An efficient realization of a priority queue uses a heap data structure, which allows both insertions and removals in logarithmic
time.
Heaps store elements and keys in a binary tree instead of a list, which is what improves the running time.
The relational property of T, defined in terms of the way keys are stored, is the following:
Heap-Order Property: In a heap T, for every node v other than the root, the key associated with v is greater than or equal to the key
associated with v’s parent.
The heap data structure stores elements and keys in a binary tree.
The heap-order property states that the keys encountered on a path from the root to an external node of the binary tree are in
nondecreasing order.
The minimum key is always stored at the root of the binary tree.
The name "heap" comes from the fact that the most important key is informally said to be "at the top of the heap."
The heap data structure defined here is not related to the memory heap used in programming languages like C++.
The smallest key is conventionally stored at the top of a heap, but it is arbitrary.
If the comparator were defined to indicate the opposite of the standard total order relation between keys, the root of the heap
would store the largest key.
Therefore, without loss of generality, we always assume that we are interested in the minimum key, which is always at the root
of the heap.
Given nodes v and w on the same level of T, we say that v is to the left of w if v is encountered before w in an inorder traversal
of T
Complete Binary Tree Property: A heap T with height h is a complete binary tree, that is, levels 0,1,2,... ,h-1 of T have the maximum
number of nodes possible (namely, level i has 2i nodes, for 0 ≤ i ≤ h - 1) and the nodes at level h fill this level from left to right
heap: A complete binary tree T whose nodes store the elements of the queue and whose keys satisfy the heap-order property. We
assume the binary tree T is implemented using a vector, as described in Section 8.3.2. For each node v of T, we denote the
associated key by k(v).
comp: A comparator that defines the total order relation among the keys.
insert
1. Add a new node z to T using the operation add and store the new element e in this node, making it the last node of T.
2. Compare the key k(z) with the key k(u) stored at the parent u of z. If k(z) ≥ k(u), the heap-order property is satisfied and the
algorithm terminates.
3. If k(z) < k(u), swap the entries stored at z and u. This causes the new entry (k, e) to move up one level.
5. The upward movement of the newly inserted entry by means of swaps is conventionally called up-heap bubbling.
6. In the worst case, up-heap bubbling causes the new entry to move all the way up to the root of heap T. Thus, in the worst
case, the number of swaps performed in the execution of function insert is equal to the height of T, which is ⌊logn⌋ by
Proposition 8.5.
The upward movement of the newly inserted entry by means of swaps is conventionally called up-heap bubbling. A swap either
resolves the violation of the heap-order property or propagates it one level up in the heap. In the worst case, upheap bubbling
causes the new entry to move all the way up to the root of heap T. (See Figure 8.7.) Thus, in the worst case, the number of swaps
performed in the execution of function insert is equal to the height of T, that is, it is log n by Proposition 8.5.
Removal
1. If the heap T is empty, then there is no minimum element to remove and the algorithm terminates.
3. If the root node r is the only node in the heap T , delete it and the algorithm terminates.
4. Otherwise, move the last node w of the heap to the root position r and delete the node w . (See Figure 8.8(a) and (b).)
5. Compare the key k(u) stored at the new root u with the keys k(v) stored at its children v . If k(u) is larger than both k(v1)
and k(v2) , the heap-order property is satisfied and the algorithm terminates.
6. Otherwise, swap the entry stored at node u with the entry stored at the child node v with the smallest key. (See Figure 8.8(c)
and (d).)
7. Continue swapping and comparing up the heap T until the heap-order property is restored. (See Figure 8.8(e) and (f).)
Down-heap bubbling after a removal from a priority queue implemented with a heap:
1. Check if the heap T has only one node, the root. If yes, the heap-order property is trivially satisfied, and the algorithm
terminates.
2. If the root r has no right child, let s be the left child of r. Otherwise (if r has both children), let s be a child of r with the smaller key.
3. Check if k(r) ≤ k(s). If yes, the heap-order property is satisfied, and the algorithm terminates.
4. If k(r) > k(s), swap the entries stored at r and s to locally restore the heap-order property for node r and its children (but it may
still be violated at s).
5. Continue swapping down the heap T until no violation of the heap-order property occurs, propagating the violation one level
down the heap. This process is called down-heap bubbling.
6. In the worst case, an entry moves all the way down to the bottom level, and the number of swaps performed in the execution of
function removeMin is equal to the height of heap T, which is ⌊logn⌋.
https://www.youtube.com/watch?v=REOsj0nYWKE
Figure 9.1: A conceptual illustration of the map ADT. Keys (labels) are assigned to values (folders) by a user. The resulting entries
(labeled folders) are inserted into the map (file cabinet). The keys can be used later to retrieve or remove values.
A map stores key-value pairs (k,v), where k is the key and v is its corresponding value.
The map ADT requires each key to be unique, creating a mapping of keys to values.
The key is used as a unique identifier assigned by an application or user to an associated value object.
Maps are appropriate in situations where each key serves as a unique index address for its value.
The composition pattern is a general object-oriented design pattern that defines a single object composed of other objects.
An entry in a map is an example of the composition pattern, combining a key and value into a single object.
To implement this, we define a class that stores two objects and provides functions to access and update them.
Code Fragment 9.1 presents an implementation of an Entry class that is templated based on the key and value types.
The Entry class provides member functions to access and update the key and value members.
Map ADTs
A map is a collection of key-value entries, with each value associated with a distinct key.
A special pointer object, called a position, allows us to reference entries of the map.
We define a more general object called an iterator, which can reference entries and navigate around the map.
Given a map iterator p, the associated entry may be accessed by dereferencing the iterator as *p.
The individual key and value can be accessed using p->key() and p->value(), respectively.
We can enumerate all the entries of a map by initializing an iterator to M.begin() and then repeatedly incrementing it until it is
equal to M.end().
A special sentinel iterator called end indicates that an object is not present in the map. By convention, it refers to an imaginary
element beyond the last element of the map.
Function Description
If M contains an entry e = (k,v), with key equal to k, then return an iterator p referring to this
find(k)
entry, and otherwise return the special iterator end.
If M does not have an entry with key equal to k, then add entry (k,v) to M, and otherwise, replace
put(k,v)
the value field of this entry with v; return an iterator to the inserted/modified entry.
erase(k) Remove from M the entry with key equal to k; an error condition occurs if M has no such entry.
Remove from M the entry referenced by iterator p; an error condition occurs if p points to the end
erase(p)
sentinel.
Two means of removing entries from a map: given a key or given an iterator.
Key-based operation should only be used when it is known that the key is present in the map, otherwise, check for the key first
using "M.find(k)" and apply "M.erase(p)" if found.
Iterator-based removal operation is more efficient since it doesn't need to repeat the search for the key.
The "put" operation can either insert an entry or modify an existing one. It is designed this way to ensure that keys are unique.
Example 9.1 shows the effect of a series of operations on an initially empty map storing entries with integer keys and single-
character values. The entries of the map are not listed in any particular order.
Entry and Iterator provide types for entry and iterator objects, respectively.
The iterator object is similar to the STL iterator and supports * operator, unary increment and decrement operators, and
comparison using ==.
The interface includes the functions described earlier for map operations.
If the erase(k) function is called with a key that is not in the map, an exception of type NonexistentElement is thrown.
To declare an object of type map , you need to include the definition file called "map", and it is part of the std namespace.
map is templated with two arguments, the key type and the value type. An iterator type is provided for referencing individual
The map iterator type is map<K,E>::iterator , and the (k,v) entries are stored in a composite object called pair.
Each map object defines two special iterators through the functions begin and end . A map iterator is bidirectional and can be
moved forwards and backwards using the increment and decrement operators.
The principal member functions of the STL map include insert , erase , find , clear , empty , and size .
Function Description
find(k) Find the entry with key k and return an iterator to it; if no such key exists return end.
operator[k] Produce a reference to the value of key k; if no such key exists, create a new entry for key k.
The map ADT is similar to the functions provided by the STL map.
The STL map's insert function takes a composite object of type pair, with its first and second elements being the key and value.
The subscript operator ("[]") in the STL map is used for convenient search, insert, and modify operations. It inserts the pair (k,v)
if k is not present or modifies the value if it is.
The value of M[k] can be read by performing find(k) and accessing the value part of the resulting iterator.
As with the other STL containers we have seen, the STL does not check for errors. It is up to the programmer to be sure that no
illegal operations are performed.
The find(k), put(k,v), and erase(k) functions involve a scan down the list L looking for the entry with key k.
[L.begin(), L.end()) denotes all positions of the list L from L.begin() to, but not including, L.end().
This list-based map implementation is simple, but it is only efficient for very small maps. Every one of the fundamental functions
takes O(n) time on a map with n entries, because each function involves searching through the entire list in the worst case. Thus,
we would like something much faster.
Hash Tables
A map's keys are like "addresses" for its values.
Applications of maps with "addresses" include compilers' symbol tables and registries of environment variables.
The most efficient way to implement a map with "addresses" is to use a hash table.
A hash table consists of two main components: a bucket array and a hash function.
While worst-case running time of map operations in a hash table is O(n), the expected time is O(1).