Unit 7
Unit 7
UNIT 7
STANDARD LIBRARY CONTAINERS AND ITERATORS
• Even if the array size increases, this function always runs in the
same amount of time.
Linear Time – O(n)
• The runtime increases proportionally with the input size.
• Binary search reduces the search space in half each step, leading to
O(log n) complexity.
Quadratic Time – O(n²)
• The runtime grows proportionally to the square of the input size.
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
• Load Factor:
• The load factor of a hash table is defined as the number of elements divided by the number of buckets. It determines how
"full" the hash table is.
• A high load factor may lead to performance degradation (more collisions), and the hash table may need reallocation (resize) to
maintain optimal performance.
How Hash Tables Work
• Insertion:
• A key-value pair is inserted by computing the hash of the key and placing it in the
corresponding bucket.
• If a collision occurs, the item is inserted into the appropriate location in the bucket.
• Search:
• To search for a value, the hash table computes the hash of the key and looks for the value
in the corresponding bucket.
• If there are multiple items in the same bucket (due to a collision), a secondary search is
performed within the bucket.
• Deletion:
• To delete a key-value pair, the hash table computes the hash of the key and removes the
corresponding item from the bucket.
• If there is more than one item in the bucket, the appropriate entry is found and removed.
Advantages of Hash Tables
• Efficient Lookup, Insertion, and Deletion:
Hash tables provide O(1) time complexity for lookup, insertion, and deletion
on average, making them very efficient for large datasets.
• Fast Access:
Direct access to an element using its key is possible due to the hash function
that computes the location of the element.
Output:
Introduction to Containers in C++
• Containers in C++ are data structures provided by the Standard
Template Library (STL) that store collections of objects. They
simplify memory management and improve efficiency in handling
data.
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec1; // Empty vector
std::vector<int> vec2(5); // Vector of size 5 (default values)
std::vector<int> vec3(5, 100); // Vector of size 5 with all values = 100
std::vector<int> vec4 = {1, 2, 3, 4, 5}; // Initialization with values
return 0;
}
Adding and Removing Elements
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
return 0;
}
Accessing Elements
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << "First Element: " << vec.front() << std::endl; Output:
std::cout << "Last Element: " << vec.back() << std::endl;
std::cout << "Element at Index 2: " << vec[2] << std::endl;
return 0;
}
Vector Capacity & Size Methods
Vector Element Access Methods
Modifiers (Insertion, Deletion, and
Modification)
Iterators (Traversal & Access)
Relational Operators (Comparisons)
Example
#include <iostream> cout << "Front: " << v.front() << ", Back: " << v.back() << endl;
#include <vector>
#include <algorithm> // for sort, find // at: Safe element access
using namespace std; cout << "Element at index 1: " << v.at(1) << endl;
#include <iostream>
#include <deque>
int main() {
std::deque<int> dq1; // Empty deque
std::deque<int> dq2(5); // Deque of size 5 (default values)
std::deque<int> dq3(5, 100); // Deque of size 5 with all values = 100
std::deque<int> dq4 = {1, 2, 3, 4, 5}; // Initialization with values
return 0;
}
Adding and Removing Elements
#include <iostream>
#include <deque>
int main() {
std::deque<int> dq;
return 0;
}
Accessing Elements
#include <iostream>
#include <deque>
int main() {
std::deque<int> dq = {1, 2, 3, 4, 5};
std::cout << "First Element: " << dq.front() << std::endl; Output:
std::cout << "Last Element: " << dq.back() << std::endl;
std::cout << "Element at Index 2: " << dq[2] << std::endl;
return 0;
}
Methods of std::deque
Methods of std::deque
Example
#include <iostream>
#include <deque> // erase: remove element at position 3
#include <algorithm> // for sort, find dq.erase(dq.begin() + 3);
using namespace std;
// size and empty
int main() { cout << "Size: " << dq.size() << endl;
// Create a deque of integers cout << "Is deque empty? " << (dq.empty() ? "Yes" : "No") << endl;
deque<int> dq;
// sort (only works with random access iterators)
// push_back and push_front sort(dq.begin(), dq.end());
dq.push_back(10);
dq.push_back(20); // find
dq.push_front(5); auto it = find(dq.begin(), dq.end(), 99);
dq.push_front(1); if (it != dq.end()) {
cout << "Found 99 at index: " << distance(dq.begin(), it) << endl;
// emplace_back and emplace_front }
dq.emplace_back(25);
dq.emplace_front(0); // Display deque Output:
cout << "Deque elements: ";
// at, front, back for (int x : dq) {
cout << "Element at index 2: " << dq.at(2) << endl; cout << x << " ";
cout << "Front: " << dq.front() << ", Back: " << dq.back() << endl; }
cout << endl;
// pop_back and pop_front
dq.pop_back(); return 0;
dq.pop_front(); }
#include <iostream>
#include <list>
int main() {
std::list<int> lst1; // Empty list
std::list<int> lst2(5); // List of size 5 (default values)
std::list<int> lst3(5, 100); // List of size 5 with all values
= 100
std::list<int> lst4 = {1, 2, 3, 4, 5}; // Initialization with
values
return 0;
}
Adding and Removing Elements
#include <iostream>
#include <list>
int main() {
std::list<int> lst;
return 0;
}
Accessing Elements
• std::list does not support direct indexing ([]) or .at(). Use iterators
instead:
#include <iostream>
#include <list>
return 0;
}
Methods of std::list
Methods of std::list
Example
#include <iostream> advance(it, 3); // move to 4th element
#include <list> lst.erase(it);
#include <algorithm> // for find, sort
using namespace std; // size and empty
cout << "Size: " << lst.size() << endl;
int main() { cout << "Is list empty? " << (lst.empty() ? "Yes" : "No") << endl;
// Create a list of integers
list<int> lst; // find
auto findIt = find(lst.begin(), lst.end(), 99);
// push_back and push_front if (findIt != lst.end()) {
lst.push_back(10); cout << "Found 99 in list." << endl;
lst.push_back(20); }
lst.push_front(5);
lst.push_front(1); // sort (only works on list itself, not from <algorithm>)
lst.sort();
// emplace_back and emplace_front
lst.emplace_back(30); // reverse
lst.emplace_front(0); lst.reverse();
#include <iostream>
#include <forward_list>
int main() {
std::forward_list<int> flist1; // Empty list
std::forward_list<int> flist2 = {1, 2, 3, 4, 5}; // Initialization with values
std::forward_list<int> flist3(5, 100); // List of size 5 with all values = 100
return 0;
}
Adding and Removing Elements
#include <iostream>
#include <forward_list>
int main() {
std::forward_list<int> flist = {10, 20, 30};
return 0;
}
Methods of std::forward_list
Methods of std::forward_list
Example
#include <iostream> flist.remove(30); // Remove all occurrences of 30
#include <forward_list>
#include <algorithm> // for find // sort
using namespace std; flist.sort();
// emplace_after
flist.emplace_after(it, 12); // Insert 12 after the 3rd element
#include <iostream>
#include <set>
int main() {
std::set<int> s1; // Empty set
std::set<int> s2 = {10, 30, 20, 10, 40}; // Initialization with values
std::set<int> s3(s2); // Copy constructor
return 0;
}
Inserting and Erasing Elements
#include <iostream>
#include <set>
int main() {
std::set<int> s;
s.insert(10); // Insert 10
s.insert(20); Output:
s.insert(30);
s.insert(10); // Duplicate, won't be added
s.erase(20); // Removes 20
int main() {
std::map<int, std::string> myMap; // Empty map
return 0;
}
Inserting Elements
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap;
// insert or update using [] operator // iterate through map (keys are auto-sorted)
studentMarks["Charlie"] = 78; cout << "All student marks:" << endl;
studentMarks["David"] = 92; for (const auto& pair : studentMarks) {
cout << pair.first << ": " << pair.second << endl;
// emplace: more efficient insertion } Output:
studentMarks.emplace("Eve", 88);
// clear
// Access elements // studentMarks.clear();
cout << "Alice's Marks: " << studentMarks["Alice"] << endl;
return 0;
// find }
auto it = studentMarks.find("Bob");
if (it != studentMarks.end()) {
std::multiset
• A std::multiset is an ordered associative container similar to
std::set, but with duplicate elements allowed.
• Stores elements in sorted order (ascending by default).
• Allows duplicate elements.
• Implemented as a self-balancing binary search tree (Red-Black Tree).
• Operations (insert, erase, search) take O(log n) time.
Declaring and Initializing a std::multiset
#include <iostream>
#include <set>
int main() {
std::multiset<int> ms1; // Empty multiset
std::multiset<int> ms2 = {10, 30, 20, 10, 40}; //
Initialization with duplicates
return 0;
}
Inserting Elements
#include <iostream>
#include <set>
int main() {
std::multiset<int> ms;
ms.insert(10);
Output:
ms.insert(20);
ms.insert(30);
ms.insert(10); // Duplicate allowed
int main() {
std::multimap<int, std::string> mm; // Empty multimap
return 0;
}
Inserting Elements
#include <iostream>
#include <map>
int main() {
std::multimap<int, std::string> mm;
// emplace (construct in-place) // Display all key-value pairs (keys are sorted, duplicates allowed)
marks.emplace("Bob", 93); cout << "All student marks:" << endl;
for (const auto& p : marks) { Output:
// size and empty cout << p.first << " => " << p.second << endl;
cout << "Size: " << marks.size() << endl; }
cout << "Is multimap empty? " << (marks.empty() ? "Yes" : "No") << endl;
return 0;
// find: returns iterator to the *first* element with given key }
auto it = marks.find("Bob");
if (it != marks.end()) {
cout << "Found Bob: " << it->second << endl;
}
std::unordered_set
std::unordered_multiset
• C++ provides unordered associative containers that use hash
tables for fast operations.
• std::unordered_set → Stores unique elements (no duplicates allowed).
• std::unordered_multiset → Stores duplicate elements (allows multiple
occurrences of the same value).
Example of std::unordered_set
#include <iostream>
#include <unordered_set>
int main() {
std::unordered_set<int> us = {10, 20, 30, 10}; // Duplicates ignored
Output:
for (int num : us) {
std::cout << num << " "; // Order not guaranteed
}
}
Example of std::unordered_multiset
#include <iostream>
#include <unordered_multiset>
int main() {
std::unordered_multiset<int> ums = {10, 20, 30, 10, 20};
Output:
for (int num : ums) {
std::cout << num << " ";
}
}
std::unordered_map
std::unordered_multimap
• C++ provides unordered associative containers that store key-
value pairs in hash tables for fast operations.
• std::unordered_map → Stores unique keys, each key maps to a single
value.
• std::unordered_multimap → Stores duplicate keys, each key can have
multiple values.
Example of std::unordered_map
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<int, std::string> um = {{1, "Apple"}, {2, "Banana"}, {3, "Cherry"}};
Output:
Example of std::unordered_multimap
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_multimap<int, std::string> umm = {
{1, "Apple"}, {2, "Banana"}, {1, "Apricot"}, {3, "Cherry"}
}; Output:
1.Dereferencing the Iterator (*it): Accessing the element the iterator is pointing to.
2.Incrementing the Iterator (++it): Moving to the next element.
3.Decrementing the Iterator (--it): Moving to the previous element (available for bidirectional and random access iterators).
4.Comparison (it == end()): Checking if the iterator has reached the end of the container.
5.Accessing an element directly (it[n]): (Only for random access iterators like std::vector).
Example of using Iterator with vector
#include <iostream> // Using range-based for loop (simplified)
#include <vector> cout << "Using range-based for loop:" << endl;
using namespace std; for (auto &val : vec) {
int main() { cout << val << " ";
vector<int> vec = {1, 2, 3, 4, 5}; }
cout << endl;
// Using iterator to traverse the vector
cout << "Using iterators with vector:" << endl; return 0;
for (vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) { }
cout << *it << " ";
}
cout << endl;
Output:
Example of Iterators with list(Bidirectional
iterator)
#include <iostream> // Using reverse iterator to traverse in reverse order
#include <list> cout << "Using reverse iterator with list:" << endl;
using namespace std; for (list<int>::reverse_iterator rit = myList.rbegin(); rit != myList.rend(); ++rit) {
cout << *rit << " ";
int main() { }
list<int> myList = {10, 20, 30, 40}; std::cout << std::endl;
Output:
Example of Iterators with std::map
(Bidirectional & Random Access Iterator)
#include <iostream>
#include <map>
using namespace std;
int main() {
map<int, std::string> myMap = {{1, "One"}, {2, "Two"}, {3, "Three"}};
return 0;
} Output:
Container Adaptors in C++
• C++ provides container adaptors, which are wrapper containers
that modify the interface or behavior of other containers.
• They do not implement their own data structures but instead
adapt other containers (like std::queue, std::stack, etc.) to meet
specific needs.
• There are three main container adaptors in C++:
• std::stack - Adapts containers to work as LIFO (Last In, First Out) stacks.
• std::queue - Adapts containers to work as FIFO (First In, First Out)
queues.
• std::priority_queue - Adapts containers to work as priority queues
(based on element priority).
std::stack (LIFO - Last In, First Out)
• std::stack is an adaptor container that allows access to elements in
a LIFO (Last In, First Out) order.
• It is typically used for operations that follow the push-pop
behavior, such as undo/redo operations, depth-first search, and
more.
• Internal Container:
• By default, std::stack uses std::deque as the underlying container.
However, you can customize it to use other containers like std::vector or
std::list.
Key Operations:
Example of Stack
#include <iostream>
#include <stack>
int main() {
std::stack<int> s;
s.push(10);
s.push(20);
s.push(30); Output:
return 0;
}
std::queue (FIFO - First In, First Out)
• std::queue is an adaptor container that implements a FIFO (First
In, First Out) data structure, which is commonly used for tasks like
task scheduling, buffering, and breadth-first search.
• Internal Container:
• std::queue uses std::deque by default but can also work with other
containers like std::list.
Key Operations:
Example of Queue
#include <iostream>
#include <queue>
int main() {
std::queue<int> q;
q.push(10);
q.push(20); Output:
q.push(30);
return 0;
}
std::priority_queue (Priority Queue)
• std::priority_queue is an adaptor container that implements a
priority queue, where elements are ordered based on their
priority.
• By default, the highest priority element is the first to be removed.
It's commonly used in algorithms like Dijkstra's algorithm and in
task scheduling systems.
• Internal Container:
• By default, std::priority_queue uses std::vector as the underlying
container. However, it can be adapted to use other containers like
std::deque or std::list.
Key Operations:
By default, std::priority_queue uses the greater-than operator (>) for comparisons, meaning
larger elements have higher priority. You can customize the priority by passing a custom
comparison function.
Example of Priority queue
#include <iostream>
#include <queue>
#include <vector>
int main() {
// Max-heap (default)
std::priority_queue<int> pq;
pq.push(10);
pq.push(30);
pq.push(20);
Output:
std::cout << "Top: " << pq.top() << std::endl; // Output: 30
pq.pop();
std::cout << "Top after pop: " << pq.top() << std::endl; // Output: 20
return 0;
}
Lambda Expression in C++
• A lambda expression in C++ is an anonymous function that can
be defined inline and used without the need to define a separate
function.
• It allows you to write small, concise, and efficient functions for
short-term use, making your code more readable and functional.
Syntax of Lambda Expression
Syntax:
• Capture Clause ([]): Specifies which variables from the surrounding scope are available for use
inside the lambda. It can capture by value or by reference.
• Parameters (()): List of parameters (similar to function arguments).
• Return Type (-> return_type): Optional. Specifies the return type of the lambda.
• Function Body ({ }): The code that defines the functionality of the lambda expression.
Example
#include <iostream>
int main() {
auto add = [](int a, int b) { return a + b; };
std::cout << "Sum: " << add(3, 4) << std::endl; // Output: Sum: 7
return 0;
}
Output:
Example
#include <iostream>
int main() {
int x = 10;
auto printX = [x]() { std::cout << "Captured value: " << x << std::endl; }; Output:
printX(); // Output: Captured value: 10
return 0;
}
Example
#include <iostream>
int main() {
int x = 10;
auto modifyX = [&x]() { x = 20; };
modifyX();
std::cout << "Modified x: " << x << std::endl; // Output: Modified x: 20 Output:
return 0;
}
Example
#include <iostream>
int main() {
auto multiply = [](int a, int b) -> int { return a * b; };
std::cout << "Product: " << multiply(3, 4) << std::endl; // Output: Product: 12 Output:
return 0;
}