STL (Standard Template Library)
Table of Contents
Containers
Containers are data structures that are already implemented in STL.
Container is further divided into the following data structure types:
- Sequential
- vectors
- stack
- queue
- Ordered
- maps
- multimap
- set
- multiset
- Unordered
- unordered map
- unordered set
Above list does not include all the containers but most relevant ones. See https://cplusplus.com/reference/stl/ for more information on STL containers.
Nested Containers
Cotainers can be nested. Some examples are given below.
vector<vector<int>>
map<int, vector<int>>
set<pair<int, string>>
Pairs
pair is defined in the utility
header file. Pairs are copied when passed as
arguments or assigned to some other variable. Use cpp references or pointers to
refer to a pair if needed.
#include <utility> #include <string> #include <iostream> int main() { std::pair<int, std::string> p = {1, "string"}; std::cout << p.first << ":" << p.second << std::endl; // pairs are copied std::pair<int,std::string> p1 = p; p1.first = 43; std::cout << p.first << ":" << p.second << std::endl; // cpp references use with pairs std::pair<int,std::string> &p2 = p; p2.first = 69; std::cout << p.first << ":" << p.second << std::endl; // with pointers std::pair <int, std::string> *p3 = &p; p3->first = 990; std::cout << p.first << ":" << p.second << std::endl; }
Vectors
Dynmaically sized contiguous memory blocks. Defined in vector
header file.
Vector has same size limits as arrays in cpp. Local vector can't be more than
105 in size and global vector can't be more than 107 in size. This limit is on
the contiguous memory allocation that's why it's same for both arrays and
vectors.
#include <vector> #include <iostream> int main() { std::vector<int> v = {1,2,3}; // vector of initial size 10 with all values set to 3 // std::vector<int> x(10) will set all 10 values to 0 (data type's zero value) std::vector<int> x(10,3); for (int i : x) { std::cout << i << " "; } std::cout << std::endl; for (int i : v) { std::cout << i << std::endl; } v.push_back(69); // O(1) std::cout << v[3] << std::endl; std::cout << v.size() << std::endl; v.pop_back(); // O(1) std::cout << v.size() << std::endl; }
Vectors can be copied unlike arrays. Use cpp references or pointers to get a reference to a vector.
#include <vector> #include <iostream> int main() { std::vector<int> v = {1,2,3}; std::vector<int> v1 = v; v1.push_back(10); for (int i : v) { std::cout << i << " "; } std::cout << std::endl; std::vector<int> &v2 = v; v2[0] = 99; std::cout << v[0] << std::endl; std::vector<int> *v3 = &v; (*v3)[0] = 32; std::cout << v[0] << std::endl; }
Vectors can be nested to created multi-dimensional vectors (2d, 3d, …). Vector of pairs is also common in use.
#include <utility> #include <vector> #include <iostream> int main() { std::vector<std::pair<int, int>> v = {{1,2}, {2,9}}; for (std::pair<int, int> p : v) { std::cout << p.first << " : " << p.second << std::endl; } }
Maps
A data structure to store collection of key:value
pairs. Pairs are stored in
sorted order in a normal Map, use unordered map for storing unordered pairs.
Insertion and access in map is O(log(n)). Keys in a map are unique. It'll replace the old value in case of an overwrite operation.
Find operation (O(log(n))) returns an iterator that points to the found pair or
the .end()
if the pair does not exist.
Erase (map.erase()
) (O(log(n))), takes in either the key or the iterator to
the pair we want to erase from the map. You can't pass m.end()
to erase as
it's points to the element which is after the last element.
Use map.clear()
to clear the map.
#include<iostream> #include<map> int main() { std::map<int, std::string> m; m[1] = "abc"; m.insert({2, "dewf"}); m[9] = "jsf"; m[5] = "jsklfjlks"; m[99]; // insertion will still happen even if we didn't assign anything for (auto i : m) { std::cout << i.first << " : " << i.second << std::endl; } auto el = m.find(5); if (el != m.end()) { std::cout << el->second << std::endl; } m.erase(m.find(1)); // O(log(n)) + O(log(n)) for find and then erase m.erase(9); // O(log(n)) for (auto i : m) { std::cout << i.first << " : " << i.second << std::endl; } }
Unordered Maps
Difference from normal normal maps:
- Inbuilt implementation: uses hash tables. Each key's hash values is calculated for comparison
- time complexity: Insertion/Access time is O(1) (average time complexity).
.erase()
happens in O(1) when given an iterator. - valid key data types:
vector<int>
orpair<int, int>
can't be used as key of a pair in unorderedmaps as their hash is not predefined by default. All standard data types (including std::string) can be used.
#include<iostream> #include<unordered_map> int main() { std::unordered_map<int, std::string> m; m[1] = "abc"; m.insert({2, "dewf"}); m[9] = "jsf"; m[5] = "jsklfjlks"; m[99]; // insertion will still happen even if we didn't assign anything for (auto i : m) { std::cout << i.first << " : " << i.second << std::endl; } }
Multimaps
Pairs with same keys can exist in a multimap
.
Sets
Collection of unique elements.
- Normal Sets: Stores elements in sorted order. Insertion/Access takes
O(log(n)) time. Find is also O(log(n)).
=set.erase()=
is same as it was in maps. - Unordered Sets: Stores elements in random order. Insert take O(1) time. Standard data types (including) strings can be used by default (uses hash for uniq check).
- Multisets: allows for duplicate elements. If passed a value to
set.erase()
, it'll delete all the occurence of it. With an iterator, it'll only delete the element that's pointed by the iterator.
#include <iostream> #include <set> using namespace std; int main() { set<string> s = {"gh", "asdf"}; s.insert("abc"); s.insert("abc"); // stores unique element auto res = s.find("asdf"); // return s.end() if not found cout << *res << endl; for (auto val : s) { cout << val << endl; } }
#include <iostream> #include <unordered_set> using namespace std; int main() { unordered_set<string> s = {"sidd"}; s.insert("sidd"); // stores unique element cout << s.size() << endl; for (auto &val : s) { cout << val << endl; } }
Stack
LIFO
Operations:
- push (element on top of the stack)
- pop (element from top of the stack)
- top (get the top element of the stack)
#include <iostream> #include <stack> using namespace std; int main() { stack<int> s; s.push(2); s.push(9); s.push(8); s.push(3); s.push(99); s.push(10); while(!s.empty()) { cout << s.top() << endl; s.pop(); } }
LC question (valid parens)
- Solution 1
class Solution1 { public: bool isValid(string s) { stack<char> st; bool res = true; for (char &c : s) { if (c == '(' || c == '[' || c == '{') { st.push(c); } else { if (st.empty()) { res = false; } else { auto top = st.top(); st.pop(); if (c == ')' && top != '(') { res = false; } else if (c == '}' && top != '{') { res = false; } else if (c == ']' && top != '[') { res = false; } } } } if (!st.empty()) { res = false; } return res; } };
class Solution2 { public: unordered_map<char, int> mp = {{'(', -1}, {'{', -2}, {'[', -3}, {')', 1}, {'}', 2}, {']', 3}}; bool isValid(string s) { stack<char> st; for (char &c : s) { if (mp[c] < 0) { st.push(c); } else { if (st.empty()) { return false; } auto top = st.top(); st.pop(); if (mp[top] + mp[c] != 0) { return false; } } } return st.empty(); } };
Driver Code with both the solutions
#include <iostream> #include <stack> #include <unordered_map> using namespace std; class Solution1 { public: bool isValid(string s) { stack<char> st; bool res = true; for (char &c : s) { if (c == '(' || c == '[' || c == '{') { st.push(c); } else { if (st.empty()) { res = false; } else { auto top = st.top(); st.pop(); if (c == ')' && top != '(') { res = false; } else if (c == '}' && top != '{') { res = false; } else if (c == ']' && top != '[') { res = false; } } } } if (!st.empty()) { res = false; } return res; } }; class Solution2 { public: unordered_map<char, int> mp = {{'(', -1}, {'{', -2}, {'[', -3}, {')', 1}, {'}', 2}, {']', 3}}; bool isValid(string s) { stack<char> st; for (char &c : s) { if (mp[c] < 0) { st.push(c); } else { if (st.empty()) { return false; } auto top = st.top(); st.pop(); if (mp[top] + mp[c] != 0) { return false; } } } return st.empty(); } }; int main() { auto sol1 = new Solution1(); auto sol2 = new Solution2(); cout << sol1->isValid("((()))") << endl; cout << sol1->isValid("({[]})") << endl; cout << sol1->isValid("(]") << endl; cout << sol1->isValid("[](()") << endl; cout << sol2->isValid("((()))") << endl; cout << sol2->isValid("({[]})") << endl; cout << sol2->isValid("(]") << endl; cout << sol2->isValid("[](()") << endl; }
Queues
FIFO
Operations:
- push (element to the back of the queue)
- pop (remove the first element)
- front (get the first element of the queue)
#include <iostream> #include <queue> using namespace std; int main() { queue<string> q; q.push("abc"); q.push("def"); q.push("ghi"); q.push("jkl"); q.push("mno"); while(!q.empty()) { cout << q.front() << endl; q.pop(); } }
Iterators
Pointer like structures. .begin()
points at the first element of a data
structure. .end()
points at the element after the last element.
Increment operator (++) works for any iterator but adding 1 (it + 1
) only
works for vectors.
(+1) moves to the next memory location which, in case of vectors is the location
of the next element. Maps and other data structure do no store elements in
contiguous memory block so this does not work.
#include <iostream> #include <vector> int main() { std::vector<int> v = {1,2,3,4}; std::vector<int>::iterator it = v.begin(); std::cout << (*it) << std::endl; std::cout << (*(it + 1)) << std::endl; // i++/++i -> next iterator for (auto i = v.begin(); i != v.end(); ++i) { std::cout << (*i) << std::endl; } }
C++11 introduced range based loops and the auto
keyword that shortens the
syntax required for iterator usage.
#include <iostream> #include <vector> int main() { std::vector<int> v = {1,2,3,4}; // uses iterators under the hood // the value inside identifier `i` is copied // use cpp references (int &i) when copying is not desired for (int i : v) { std::cout << i << std::endl; } std::vector<std::pair<int,int>> p = {{1,2}, {3,4}}; // pi :: pair<int,int> // pari<int,int> -> auto! for (auto pi : p) { std::cout << pi.first << std::endl; } }
Algorithms
- {next,prev}permutations
Sort (O(nlog(n)))
std::sort
takes in the first iterator (pointer to the first element) and
pointer to the next element after the last element.
#include <iostream> #include <algorithm> using namespace std; int main() { int a[5] = {3,34,2,61,3}; sort(a, a+5); for (int i : a) { cout << i << endl; } }
Usage with vectors:
#include <iostream> #include <algorithm> using namespace std; int main() { vector<int> a = {1,99,3,4,10, 8}; sort(a.begin(), a.end()); for (int i : a) { cout << i << endl; } }
- Comparator Function in Sort
The default logic for sort is to rearrange container elements in ascending order. We can change this behaviour
Return
false
from the comparator function if you want to swap, else returntrue
. We typically pass a comparison expression that when evaluated tofalse
, triggers the swap operation.#include <iostream> #include <algorithm> using namespace std; bool cmp(pair<int, int> a, pair<int, int> b) { if (a.first != b.first) { return a.first < b.first; } return a.second > b.second; } int main() { vector<pair<int, int>> a = {{4,3}, {5,5}, {5,3}, {25, 6}, {7, 9}, {8, 5}}; sort(a.begin(), a.end(), cmp); for (auto i : a) { cout << i.first << " " << i.second << endl; } }
Lower/Upper Bound (O(log(n)))
The input to lower/upper bound should be sorted to get O(log(n)) perf. If the input isn't sorted, it'll take O(n) time.
Use set.lower_bound
or set.upper_bound
and map.lower_bound
or
map.upper_bound
to get O(log(n)) with sets and maps.
Lower bound returns pointer (or iterator) to the given element or next element
bigger than the given element. If both (given element and any element bigger
than the given element) is not found, it'll return .end()
.
lower_bound
searches for element x in a container such that i <= x, where i is
the search argument passed.
#include <iostream> #include <algorithm> using namespace std; int main() { int a[5] = {3,34,2,61,3}; sort(a, a+5); cout << *lower_bound(a, a+5, 3) << endl; cout << *lower_bound(a, a+5, 34) << endl; cout << *lower_bound(a, a+5, 61) << endl; // neither 69 nor any value greater than 69 exist so we get .end() if (lower_bound(a, a+5, 69) == (a+5)) { cout << "yes" << endl; } // we get 34 as 5 doesn't exist in `a`` and 34 is the next bigger element cout << *lower_bound(a, a+5, 5) << endl; }
Upper bound returns pointer (or iterator) to the element which is greater than the given element.
upper_bound
searches for element x in a container such that i < x, where i is
the search argument passed.
#include <iostream> #include <algorithm> using namespace std; int main() { int a[5] = {3,34,2,61,3}; sort(a, a+5); cout << *upper_bound(a, a+5, 3) << endl; cout << *upper_bound(a, a+5, 34) << endl; if (upper_bound(a, a+5, 61) == (a+5)) { cout << "no element bigger than " << 61 << " found" << endl; } // neither 69 nor any value greater than 69 exist so we get .end() if (upper_bound(a, a+5, 69) == (a+5)) { cout << "no eleme bigger than 69 found" << endl; } // we get 34 as 5 doesn't exist in `a`` and 34 is the next bigger element cout << *upper_bound(a, a+5, 5) << endl; }
minelement, maxelement, accumulate, count, find and, reverse
#include <iostream> #include <vector> #include <algorithm> #include <numeric> using namespace std; int main() { vector<int> v = {1,3,932,342,4,1}; cout << *min_element(v.begin(), v.end()) << endl; cout << *max_element(v.begin(), v.end()) << endl; // accumulate gives sum of all elements in a container // defined in the `numeric` header file cout << accumulate(v.begin(), v.end(), 0) << endl; // count returns the number of occurrence of some element in a container cout << count(v.begin(), v.end(), 1) << endl; // find auto it = find(v.begin(), v.end(), 69); if (it != v.end()) cout << *it << endl; else cout << "not found" << endl; // reverse reverse(v.begin(), v.end()); for (int &i:v) { cout << i << " "; } cout << endl; string s = "abcdef"; reverse(s.begin(), s.end()); cout << s << endl; }
cpp lambdas
Lambdas in cpp can be used to create functions that are created for one time use. Lambda funcs can be assigned too.
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> v = {1,3,932,342,4,1}; for (int i : v) { cout << i << " "; } cout << endl; // sort in descending order using cpp lambdas sort(v.begin(), v.end(), [](int a, int b){ return a > b; }); for (int i : v) { cout << i << " "; } cout << endl; cout << [](int x){ return x*2; }(10) << endl; }
all_of
, any_of
and, none_of
all_of
: returns true if the provided function returns true for all the
elements.
any_of
: returns true if the provided function returns true for at least one
element.
none_of
: returns true if the provided function returns false for all the
elements.
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> v = {1,3,932,342,4,1}; // checks if all the elements are greater than 10 (they're not) cout << all_of(v.begin(), v.end(), [](auto a) { return a > 10; }) << endl; cout << any_of(v.begin(), v.end(), [](auto a) { return a > 10; }) << endl; // since none of the elments are negative, it prints 1 (true) cout << none_of(v.begin(), v.end(), [](auto a) { return a < 0; }) << endl; }
Functors
TODO look more into this topic (not important for LC) Classes that act as functions?