International_file
NIGCOM_FILESfamous
plain_text
2 years ago
24 kB
7
Indexable
Time to dive into some more super useful functionalities that the standard C++ library offers - data structures that can be used for solving lots of different computer science problems! Note that when we're talking about data structures, we're talking about stuff like vectors, queues, etc. And NOT structs like so: struct not_this_please { int nope; double definitely no; } not_this_please_instance_1; We're talking algorithm related data structures! It might be a bit confusing, so we might perhaps use the term that C++ uses, and just say outright that we're going to go through a cursory, non-exhaustive tour of the C++ STL Container library. (STL stands for Standard Template Library.) 2. C++ Container Reference This will not be an exhaustive list. 2.1 Container Concept go to top Standard containers in the C++ STL and standard library wraps different abstract and non-abstract data structures in a nice neat interface. They: Are well documented Are guaranteed to be implemented correctly Are fast and usually more efficient Share common interfaces Although one gotcha needs to be taken care of if working with them in a threaded application. Threaded reads are fine, but any operation that modifies the container (in a way that invalidates iterators) are not thread safe generally. 2.2 Arrays go to top Array Image Source Contiguous and continuous collection of homogeneous elements that can be accessed by index. The index is the displacement from the start of the array. It can't be resized. C++ container: Array Example:array<int, 5> array{{1, 2, 3, 4, 5}}; Notice that these are different from the standard int i[5] type arrays! The underlying implementation is about the same, but we're talking containers here. #include <array> // Array 1 uses aggregate initialisation array<int, 5> array{{1, 2, 3, 4, 5}}; array<int, 5> array_2 = {1, 2, 3, 4, 5} array.size(); // Get size array.max_size(); // Get max size array.empty(); // Check if empty // Get iterator to beginning and end array.begin(); array.end(); // Get reverse iterators array.rbegin(); // Last element backwards array.rend(); // First element - 1 index // Get values at beginning and end array.front(); array.end(); // Get values at index array.at(INDEX); // Just directly access the entire array array.data(); 2.3 Dynamic Arrays go to top Just like normal arrays, except that they can be resized as they are dynamically allocated. It's not so efficient to add or remove elements from dynamic arrays since all the elements are contiguous and need to be shifted. If you need that, it's probably better to use a linked list. On the other hand, contiguous memory containers are great for accessing data though! C++ container: Vector Example: std::vector<int> i{1, 2, 3}; #include <vector> std::vector<int> vector{1, 2, 3}; vector.size(); // Get size vector.max_size(); // Get max size vector.empty(); // Check if empty vector.resize(); // Resize vector vector.reserve(); // Request change in capacity vector.shrink_to_fit(); // Shrink to fit vector.capacity(); // Return size of allocated storage capacity // Get iterator to beginning and end vector.begin(); vector.end(); // Get reverse iterators vector.rbegin(); // Last element backwards vector.rend(); // First element - 1 index // Get values at beginning and end vector.front(); vector.end(); // Get values at index vector.at(INDEX); // Just directly access the entire underlying array vector.data(); vector.assign(); // Assign vector content vector.push_back(); // Add element at end vector.pop_back(); // Delete last element // Insertions // https://www.geeksforgeeks.org/vector-insert-function-in-c-stl/ vector.insert(); // Deletions // https://www.geeksforgeeks.org/vector-erase-and-clear-in-cpp/ vector.erase(); vector.clear(); // Swap values vector.swap(); // Emplace (returns a modified copy of the vector) vector.emplace(); vector.emplace_back(); 2.4 Linked Lists go to top img(Singly Linked List) Image Source 1562761542554 (Doubly Linked List) Image Source Linked lists contain data that aren't contiguous. Each element in the list is linked using pointers, so in order to access the next element, you need to first access the preceding element. This allows for very quick insertions and deletions, but not very efficient indexing. They come in two flavours too! Singly linked lists only support indexing in one direction, while doubly linked lists do it both ways. The C++ list is actually a doubly linked list. C++ container: List and Forward List Example: std::list<int> i; Example: std::forward_list<int> i; List is doubly linked. Forward list is singly linked. From here on out it'll get too unwieldy to write everything. Most containers have a standard interface with .begin, .end, and most of the insertion and deletion methods. So just check out the reference! List // Adapted From Source: https://www.geeksforgeeks.org/list-cpp-stl/ #include <list> int main() { // Declaring forward list std::list<int> list1{1, 2, 3}; // Declaring another forward list std::list<int> list2{4, 5, 6}; // Assigning values using assign() list1.assign({1, 2, 3}); // Assigning repeating values using assign() // 5 elements with value 10 list2.assign(5, 10); } Forward List // Source: https://www.geeksforgeeks.org/forward-list-c-set-1-introduction-important-functions/ #include <forward_list> int main() { // Declaring forward list std::forward_list<int> flist1{1, 2, 3}; // Declaring another forward list std::forward_list<int> flist2{4, 5, 6}; // Assigning values using assign() flist1.assign({1, 2, 3}); // Assigning repeating values using assign() // 5 elements with value 10 flist2.assign(5, 10); } 2.5 Pairs go to top They're pairs. Of objects. In twos. Yeah. They don't have to be the same type though. That's cool... Right? 👀 It's not really a data structure per se, if anything it's a primitive. But it's nice to include here. C++ container: Pair Example: ``std::pair<int, float> pair;` #include <utility> std::pair<int, char> pair; std::pair<int, char> pair_2(1, 'o'); pair.first = 100; pair.second = 'w'; 2.6 Tuples go to top They're like arrays, but can be heterogeneous. Pairs are technically tuples that are limited to size 2. C++ container: Tuple Example: std::tuple<int,char> tuple(10,'x'); Example: auto wow = std::make_tuple("wow!", 1, 2, 'a'); #include <tuple> int main() { std::tuple <char, int, float> wow; wow = std::make_tuple("a", 15, 12); } // Unpack tuple values into variables using tie() std::tie(a, b, c) = wow; std::tie(a, ignore, c) = wow; // Use ignore to ignore values // Concatenate tuples std::tuple_cat(tuple_1, tuple_2); // Swap tuple elements tuple_1.swap(tuple_2); // Swaps tuple_1 values with tuple_2's // Index into tuples tuple_1.get(1); 2.7 Queues go to top queue Image Source Abstract data structure that contains elements. Adheres to FIFO (First In, First Out.) C++ container: Queue Example: std::queue<int> numbers; You can also have a double ended queue called a deque! C++ container: Deque Example: std::deque<int> numbers; The specific C++ implementation uses a vector of vectors to implement deques. And queues are just limited versions of deques. Specifically it's a queue of chunks of fixed size, which are vectors. And the queue of chunks is a vector. schematic of the memory layout of a deque Image Source Queue Note that the standard implementation does not support random access (at) indexing! #include <queue> // Function to print queue template<typename T> void print_queue(T& q) { while(!q.empty()) { std::cout << q.top() << " "; q.pop(); } std::cout << '\n'; } // Construct Queue std::queue queue; // Insert and pop queue.push(1); queue.pop(); // Get front queue.front(); // Get size queue.size(); Deque #include <deque> // Construct Queue std::deque deque; // Insert and pop deque.push_back(1); deque.push_front(2); deque.pop_back(); deque.pop_front(); // Get front and back deque.front(); deque.back(); // Get size deque.size(); deque.max_size(); // Index deque.at(1); 2.8 Priority Queues go to top priority_queue Image Source Just like queues, except FIFO isn't adhered to. Only the 'top' element will be shoved to the front of the queue to be popped next. What 'top' means depends on the specific implementation. Top could mean things like highest or lowest value depending on some value determining function. C++ container: Priority Queue Example: std::priority_queue<int> q; // Source: https://en.cppreference.com/w/cpp/container/priority_queue // Function to print queue template<typename T> void print_queue(T& q) { while(!q.empty()) { std::cout << q.top() << " "; q.pop(); } std::cout << '\n'; } #include <queue> std::priority_queue<int> q; for(int n : {1,8,5,6,3,4,0,9,7,2}) { q.push(n); } q.pop(); 2.9 Stacks go to top Image result for stack Image Source Abstract data structure that contains elements. Adheres to LIFO (Last In, First Out.) C++ container: Stack Example: std::stack<int> s; #include <stack> // Function to print a stack template<typename T> void showstack(T& s) { while (!s.empty()) { cout << '\t' << s.top(); s.pop(); } cout << '\n'; } std::stack<int> s; // Push to stack s.push(1); showstack(s); // Get top value s.top(); // Pop s.pop(); 2.10 Trees go to top img Image Source Oh man. There are way too many trees. And we can't cover them all. The image above should give you a good intuition of what trees are, but each special type of trees have their own special features. They can sometimes be implemented in array or vector form as well, but we'll not talk about that. There are no dedicated C++ generalised tree containers. There's too many to generalise properly! There should be some specialised libraries for use with trees, but none specifically for trees in the STL. (Though some STL classes like sets and maps do make use of red-black trees in their implementations.) 2.11 Sets go to top A set is an abstract data type that stores sorted, unique values. You can insert or delete elements from them but those elements can't be modified. They're collections of unique, sorted keys. In C++ these are implemented using red-black trees. C++ container: Set Example: std::set<int> s; There's a variant called a multiset that allows duplicate non-unique keys to be stored! C++ container: Multiset Example: std::multiset<int> s; #include <set> std::set<int> s; std::multiset<int> ms; // All the below apply as well // Insert and erase s.insert(50); s.insert(50); // This second one does nothing since sets store UNIQUE values ms.insert(50); ms.insert(50); // The multiset now has two elements of 50 s.erase(50); // Get iterators to beginning and end s.begin(); s.end(); // Get reverse iterators s.rbegin(); // Last element backwards s.rend(); // First element - 1 index // Get iterators s.lower_bound(5); // Iterator >= 5 s.upper_bound(50); // Iterator <=50 s.equal_range(); // Gets you upper and lower bound as a pair // Get iterator indexed at key s.find(5) // Get sizes s.size(); s.max_size(); // Check if empty s.empty(); 2.12 Maps go to top A map is an abstract data type that stores sorted key-value pairs. You can insert or delete keys, but you can't modify the keys. You can modify the values though! They're collections of unique, sorted key-value pairs. In C++ these are implemented using red-black trees. C++ container: Map std::map<key_type, value_type> Example: std::map<int, int> map = {{1, 1}, {2, 2}}; There's a variant called a multimap that allows duplicate non-unique keys to be stored! C++ container: Multimap Example: std::multimap<int> s; #include <map> std::map<char, int> map; std::multimap<char, int> multimap; // All the below apply as well // Insert and erase map.insert(std::pair<char, int>('a', 50)); map.insert(std::pair<char, int>('a', 50)); // This second one does nothing since sets store UNIQUE values multimap.insert(std::pair<char, int>('a', 50)); multimap.insert(std::pair<char, int>('a', 50)); // The multiset now has two elements of 50 map.erase(50); // Get iterators to beginning and end map.begin(); map.end(); // Get reverse iterators map.rbegin(); // Last element backwards map.rend(); // First element - 1 index // Get iterators map.lower_bound(5); // Iterator >= 5 map.upper_bound(50); // Iterator <=50 map.equal_range(); // Gets you upper and lower bound as a pair // Index into map map.at(5) // Get sizes map.size(); map.max_size(); // Check if empty map.empty(); 2.13 Unordered Sets/HashSets go to top They're like sets. But they're unordered. Because they're implemented using hashes! These use more memory but with the benefit of faster accesses if the sets are large. C++ container: Unordered Set Example: std::unordered_set<int> unordered_set; And of course there's the multiset variant C++ container: Unordered Multiset Example: std::unordered_multiset<int> unordered_multiset; #include <unordered_set> std::unordered_set<int> s; std::unordered_multiset<int> ms; // All the below apply as well // Insert and erase s.insert(50); s.insert(50); // This second one does nothing since sets store UNIQUE values ms.insert(50); ms.insert(50); // The multiset now has two elements of 50 s.erase(50); // Get iterators to beginning and end s.begin(); s.end(); // No reverse or iterators that require sorting // Get iterators s.equal_range(); // Gets you upper and lower bound as a pair // Get iterator indexed at key s.find(5) // Get sizes s.size(); s.max_size(); // Check if empty s.empty(); // Hash policy methods s.load_factor(); s.max_load_factor(); s.rehash(); // Set number of buckets s.reserve(); // Change capacity s.hash_function(); // Get hash function // Bucket methods s.bucket_count(); s.max_bucket_count(); s.bucket_size(); s.bucket(); // Locate element's bucket 2.14 Unordered Maps/HashMaps go to top Image result for hash c++ Image Source They're like maps. But they're unordered. Because they're implemented using hashes! These use more memory but with the benefit of faster accesses if the maps are large. Keys are passed through a hash function that spits out a bucket ID. The buckets are there to prevent collisions (when the bucket IDs clash for different keys.) C++ container: Unordered Map std::unordered_map<key_type, value_type> Example: std::unordered_map<int, int> unordered_map = {{1, 1}, {2, 2}}; And the multimap variant C++ container: Unordered Multimap Example: std::unordered_multimap<int, int> unordered_multimap; #include <unsorted_map> std::map<char, int> map; std::multimap<char, int> multimap; // All the below apply as well // Insert and erase map.insert(std::pair<char, int>('a', 50)); map.insert(std::pair<char, int>('a', 50)); // This second one does nothing since sets store UNIQUE values multimap.insert(std::pair<char, int>('a', 50)); multimap.insert(std::pair<char, int>('a', 50)); // The multiset now has two elements of 50 map.erase(50); // Get iterators to beginning and end map.begin(); map.end(); // No reverse or iterators that require sorting // Get iterators map.equal_range(); // Gets you upper and lower bound as a pair // Index into map map.at(5) // Get sizes map.size(); map.max_size(); // Check if empty map.empty(); // Hash policy methods map.load_factor(); map.max_load_factor(); map.rehash(); // Set number of buckets map.reserve(); // Change capacity map.hash_function(); // Get hash function // Bucket methods map.bucket_count(); map.max_bucket_count(); map.bucket_size(); map.bucket(); // Locate element's bucket 3. C++ Iterator Reference 3.1 Iterator Concept go to top Reference An iterator is an object that points to an element inside a container. So you can use them to move through the contents of a container! They're not pointers per se, they're more of an abstract object, but act a lot like pointers. There are five kinds of iterators provisioned for in the STL, each with increasingly more functionality. So you'll usually see this kind of image floating around in the net: img Image Source And you'd think that it indicates some sort of class hierarchy, but actually no. This diagram is just a statement of which iterator has more functionality than the other. Iterator Type Functionality Input Read only, forward moving Output Can write only once, forward moving Forward Read and write, forward moving Bidirectional Read and write, forward and backward moving Random Access Read and write, with random access So as you can see, random access iterators have the most functionality. 1562814980420 Image Source 3.2 Iterator-Container Compatibility go to top Of course, since we just went through containers above, and iterators are used to traverse containers, it's best to talk about which containers support which iterators. We'll mention the highest level of available iterator to use for each container. Container Iterator Vector Random Access List Bidirectional Deque Random Access Map Bidirectional Multimap Bidirectional Set Bidirectional Multiset Bidirectional Stack - Queue - Priority-Queue - 3.3 Basic Iterator Usage go to top Iterators help to simplify your code and increase its re-usability, since they provide a common interface for traversing standard library and STL containers. Container Methods // Here we define an iterator for a vector vector<int> v = {1, 2, 3}; vector<int>::iterator i; // Accessing the elements using iterators for (i = v.begin(); i != v.end(); ++i) { cout << *i << " "; } // Adding and Deleting Elements v.push_back(4); v.erase(4); // Get beginning and end positions of container v.begin(); v.end(); Iterator Operations // Get current position of the iterator *i; // Increment the iterator (now we operate on the iterator!) std::advance(i, 3); // Advance the iterator 3 positions // Return new iterator at new position // Of course, prev can't be used for forward only iterators auto it_1 = std::next(i, 3); // New iterator that is 3 positions ahead of i auto it_2 = std::prev(i, 3); // New iterator that is 3 positions behind i Inserters Pass in two iterators that define a range of values to insert from a container. Then an inserter function that takes in the container to be operated on, and an iterator that points where to operate. std::list<int> foo{1, 2, 3, 7, 8}; std::list<int> bar{4, 5, 6}; std::list<int>::iterator it = foo.begin(); advance (it, 3); std::copy(bar.begin(),bar.end(),std::inserter(foo,it)); // Foo now contains {1, 2, 3, 4, 5, 6, 7, 8} // You can also use front_inserter and back_inserter, which are essentially // Equivalent calls to inserter(foo, foo.begin()) and inserter(foo, foo.end()) respectively std::copy (bar.begin(),bar.end(),front_inserter(foo)); // Inserts at front of container std::copy (bar.begin(),bar.end(),back_inserter(foo)); // Inserts at back of container Notice that the inserter inserts the values before the current position 3.4 Iterator Overview Preface go to top So again, remember that iterators are abstract objects that help you remember what they can do. Creating an iterator for a particular object creates an iterator with method implementations that are provisioned for by the type of iterator that object supports. Eg. Creating a vector iterator automatically creates a random access iterator, which supports all the methods that a random access iterator supports. So when going through these, I'll just add the functionalities incrementally. To avoid repeating stuff. For some of these functions, you'll have to include the <algorithm> library. 3.5 Input Iterators go to top Useful for single pass algorithms. You can't decrement them, and you can't assign with them. You also can't do more complex relational operations with them like >, <, <=, >=. // Suppose we have two input iterators // input_it_1 and input_it_2 // Comparisons input_it_1 == input_it_2; // Checks if the iterators are pointing to the same position input_it_1 != input_it_2; // Checks if the iterators are not pointing to the same position // Dereferencing auto a = *input_it_1; // Get the value pointed at auto a = input_it_1 -> m; // Get member m of the element pointed at // Incrementing (Moves the position forward) input_it_1++; ++input_it_1; Supported Methods Note: equal_range() requires forward iterators and is not supported. // You can swap the values pointed at by two input iterators, even if you can't insert directly! std::swap(input_it_1, input_it_2); // Find elements std::find(input_it_1, input_it_2, val); // Compare elements within a range with another range std::equal(input_it_1, input_it_2, start_of_sequence_to_compare); // Copy a range into another range std::copy(input_it_1, input_it_2, some_output_iterator); 3.6 Output Iterators go to top Useful for single pass algorithms. You can't decrement them, and you can't access with them. You also can't do comparisons with them! // Suppose we have two output iterators // output_it_1 and output_it_2 // Dereferencing *output_it_1 = a; // Get the value pointed at output_it_1 -> m = a; // Get member m of the element pointed at // Incrementing (Moves the position forward) output_it_1++; ++output_it_1; Supported Methods // You can swap the values pointed at by two input iterators, even if you can't insert directly! std::swap(output_it_1, output_it_2); // Copy a range into another range std::copy(input_it_1, input_it_2, some_output_iterator); // Move a range into another range std::move(input_it_1, input_it_2, some_output_iterator); 3.7 Forward Iterators go to top Combine the powers of input and output iterators and you get the forward iterator! Note that you still can't decrement them! Or do more arithmetic than simple incrementing. I'm not going to repeat the functionalities for input and output ones. But the cool thing is that forward iterators become more than the sum of their parts! So we get some new functions that we can use. Supported Methods // Return the bounds of the subrange the includes all elements equal to some value std::equal_range(forward_it_first, forward_it_last, val); // Replace all values equal to a certain value std::replace(forward_it_first, forward_it_last, old_val, new_val); 3.8 Bidirectional Iterators go to top Just like forward iterators, except you can decrement them! Which again, unlocks more cool functions. Still can't randomly access them though. Sadly. You can use them for multi-pass algorithms though! // Finally you can decrement! bidirectional_it--; --bidirectional_it; Supported Methods // Create a new container, but with elements reversed std::reverse_copy(bidirectional_it_first, bidirectional_it_last, output_it_result); 3.9 Random Access Iterators go to top And finally we get to the all encompassing iterator. The random access iterator that allows for offset access and stronger comparisons like <, >, >=, <=. // You can do nicer comparsions now! random_access_it_1 <= random_access_it_1; // Checks if it_1 is before or at it_2 // And you can jump positions! random_access_it_1 + 2; // And do offset dereferencing! random_access_it[3]; Supported Methods // Random shuffle! std::random_shuffle(random_access_first, random_access_last, random_number_generator); . . . |\-^-/| . /| } O.=.O { |\
Editor is loading...