// -*- mode: c++ -*- // (set-default-font (format "-xos4-terminus-bold-*-*-*-24-*-*-*-*-*-*-*")) /* STL Continued ============= 2007.12.06. http://ortros/~salvi/stl/ Overview -------- 1) Iterator 2) List 3) Priority Queue 4) Associative Containers 5) Choosing the Correct Container 6) Algorithms 7) Hands-on Exercises 8) Bibliography */ ------------------------------------------------------------------------------- // 1. Iterator // In C, we used arrays with pointers. // In C++, we use containers with iterators. // Most member functions and algorithms rely on iterators, // they form the core of STL. // Iterators are classified into five categories: // Input < Forward < Bidirectional < Random Access, and Output // So a Forward iterator is an Input iterator, // and a Bidirectional iterator is a Forward iterator, and so on. // Output iterators are like the space between two elements. // With iterators, you can: // - read/write the current element (*iterator), if it is Input // - go to the next element (iterator++), if it is Input // - go to the previous element (iterator--), if it is Bidirectional // - go to any element (iterator + n), if it is Random Access // - insert elements, if it is Output // Its type is Container::iterator or Container::const_iterator: std::vector<double> v; v.push_back(1); v.push_back(2); v.push_back(3); std::vector<double>::iterator it; std::vector<double>::const_iterator const_it; // Every container has two member functions that give us the place (iterator) // of first element and the place after the last element: begin() and end(). // A loop through the elements of a vector can be written like for(std::vector<double>::const_iterator i = v.begin(); i != v.end(); ++i) { std::cout << *i << " "; // Prints "1 2 3 " } ------------------------------------------------------------------------------- // Forward iterators can be moved n times using advance: std::advance(iterator, 3); // = ++iterator; ++iterator; ++iterator; // Random access iterators can also be subtracted, thus we can // calculate the distance between two elements. v.end() - v.begin(); // = v.size() = 3 // Some examples: typedef std::vector<double> DVec; typedef DVec::iterator DVecIt; DVecIt i = v.begin(); std::cout << *i; // prints 1 ++i; std::cout << *i; // prints 2 std::cout << v.end() - i; // prints 2 std::cout << i - v.begin(); // prints 1 (we are now at v[1]) i += 2; std::cout << *i; // undefined (we are at v.end()) // There are also other iterator types, e.g.: // reverse_iterator (goes backwards) // istream_iterator (input for streams) // ostream_iterator (output for streams) // Output iterators are important, because many algorithms use them. // For a container, they can be created using std::inserter & co: std::inserter(v, iterator); // iterator points to an element in v, uses insert std::front_inserter(v); // = std::inserter(v, v.begin()), uses push_front std::back_inserter(v); // = std::inserter(v, v.end()), uses push_back // For example, the loop that prints all elements can be written like this: std::copy(v.begin(), v.end(), ostream_iterator<double>(std::cout, " ")); // (don't worry if you do not understand this yet :) ------------------------------------------------------------------------------- // 2. List // List is a doubly linked list, very similar to deque in use, but it has // no random access operator (operator[] or at()). #include <list> std::list<int> l; // Standard container functions: l.front(); l.back(); // the first and last element l.empty(); // is the list empty? l.clear(); // empty the list l.size(); // the size of the list l.push_front(1); l.push_back(2);// put 1 at the start and 2 at the end l.pop_front(); l.pop_back(); // remove the first / last element l.swap(another_container); // swap the contents with another container // Special member functions: l.reverse(); // reverses the order of the elements l.sort(); // sorts the list in ascending order l.sort(&sorting); // sorts the list by the function sorting() l.splice(position, other_list); // inserts another list at a given position l.merge(other_list); // merges with another list in ascending order l.merge(other_list, &sorting); // merges with another list by sorting() l.unique(); // leaves only one of consecutive same elements // For example, if l = [1, 2, 3, 3, 2, 4, 3], unique gives [1, 2, 3, 2, 4, 3]. ------------------------------------------------------------------------------- // 3. Priority Queue // STL has a few more containers, called "container adapters", // like stack, queue, ... and priority_queue. // These are "container adapters", because they use other containers. // We can even specify (with a template parameter) what container to use. // Priority queues have their elements sorted by a sorting function-object // (which is a template parameter as well, defaults to std::less). #include <queue> std::priority_queue<int> pq; std::priority_queue<int, std::vector<int>, std::greater<int> > pq2; pq.empty(); // is pq empty? => true pq.push(4); pq2.push(4); // put 4 in the queues pq.push(2); pq2.push(2); // put 2 in the queues pq.push(3); pq2.push(3); // put 3 in the queues pq.top(); pq2.top(); // the highest priority elements => 4 and 2 pq.pop(); pq2.pop(); // take out one element from both queues pq.size(); // the size of pq => 2 pq.top(); pq2.top(); // the highest priority elements => 3 and 3 // For more interesting examples, we must create a struct for comparing. // Why do we need a struct? Because template arguments cannot be functions. // They can be values of "simple types" (like 3 or 5.6 or true) and types. // As a workaround, we can create a type with operator() and it will behave // as a function, but it still can be passed as a template argument. ------------------------------------------------------------------------------- // Let's create a simple type and a comparing struct for it: struct MyType { int priority; std::string data; MyType(int p, std::string d) : priority(p), data(d) { } }; /* Constructor(arg1, arg2, ...) : var1(arg1), var2(arg2), ... { ... } is just another way of saying Constructor(arg1, arg2, ...) { var1 = arg1; var2 = arg2; ... } */ struct MyTypeLess { bool operator() (MyType const &a, MyType const &b) const { return a.priority < b.priority; } }; std::priority_queue<MyType, std::vector<MyType>, MyTypeLess> pq3; pq3.push(MyType(4, "four")); pq3.push(MyType(2, "two")); pq3.push(MyType(3, "three")); std::cout << pq3.top().data << std::endl; // Prints "four" ------------------------------------------------------------------------------- // 4. Associative Containers // Associative containers can be sets or maps, they can be hashed and can have // multiple values. So we have 8 types (all combinations): // set/map, multiset/multimap, hash_set/hash_map, hash_multiset/hash_multimap // (hashing is a way to make element lookup faster, we won't discuss it today) // Associative containers are always sorted, like priority queues. The sorting // is based on a key. // Sets are the simplest associative containers, they only contain a key. // Sets can only contain one element once. Multisets group the equal values. // The sorting "function" is a template parameter. #include <set> std::set<MyType, MyTypeLess> s; std::multiset<MyType, MyTypeLess> ms; s.insert(MyType(1, "one")); ms.insert(MyType(1, "one")); s.insert(MyType(1, "ichi")); ms.insert(MyType(1, "ichi")); s.insert(MyType(2, "two")); ms.insert(MyType(2, "two")); s.insert(MyType(1, "one")); ms.insert(MyType(1, "one")); // s = [(1, one), (2, two)] // ms = [(1, {one, ichi, one}), (2, {two})] // Maps and multimaps store key/value pairs. // Maps have a shorthand notation for searching and insertion: operator[] // std::pair<Type1, Type2> contains two public variables: first and second. // New pairs can be created with std::make_pair(value1, value2). #include <map> std::map<std::string, std::string> m; m["ichi"] = "one"; // m.insert(std::make_pair("ichi", "one")); m["ichi"] = "ein"; // m.find("ichi")->second = "ein"; ------------------------------------------------------------------------------- // Associative containers have a different set of "standard operations": a.empty(); // is the container empty? a.clear(); // empty the container a.insert(element); // insert an element a.erase(position); // erases an element a.size(); // the number of elements a.count(key); // the number of elements with the given key a.swap(other_container); // swap the elements with another container a.find(key); // an iterator to the element (or end()) a.lower_bound(key); // an iterator to the first element that is >= a.upper_bound(key); // an iterator to the last element that is <= a.equal_range(key); // a pair of iterators: to the first element // with the given key, and to the next element // after the last with the given key typedef std::multiset<MyType, MyTypeLess> MyMSet; MyMSet ms; ms.insert(MyType(0, "zero")); ms.insert(MyType(2, "two1")); ms.insert(MyType(2, "two2")); ms.insert(MyType(2, "two3")); ms.insert(MyType(4, "four")); // After this, ms = [(0, {"zero"}), (2, {"two2"/"two1"/"two3"}), (4, {"four"})] // Note that the order of two1, two2 and two3 is implementation-dependent. ms.upper_bound(MyType(0, "")); // an iterator to the first "two" ("two2" now) ms.lower_bound(MyType(2, "")); // an iterator to the first "two" ("two2" now) ms.lower_bound(MyType(3, "")); // an iterator to "four" ms.upper_bound(MyType(3, "")); // an iterator to "four" MyType element(2, ""); std::pair<MyMSet::iterator, MyMSet::iterator> p = ms.equal_range(element); p.first; // an iterator to the first "two" ("two2" now) p.second; // four ------------------------------------------------------------------------------- // 5. Choosing the Correct Container /* First let's do a short review on vectors. Vector ====== ____________________________________|___________________________________ | | | | | | | | | | | | | | 1 | 2 | 3 | 4 | 5 | 6 | X | X | X | X | X | X | |_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____| | Let this be the contents of an std::vector v. Then v.size() => 6 and v.capacity() => 12 (the length of the reserved memory). If we insert an element at the back, the next X is replaced with the new element, and the size grows with 1. New memory is allocated when necessary. We can change the size/capacity with two functions: v.reserve(n) => Ensures that v.capacity() >= n. Doesn't change the size. v.resize(n) => Ensures that v.size() = n (and v.capacity() >= n, of course). Every change in the capacity requires memory allocation, and may require the copying of all elements in the vector. Operation speed --------------- 1) v.push_back() : fast (constant), except when v.size() == v.capacity() 2) v[i] : fast (constant) 3) v.insert(...) : very slow (linear copying), except at the end 4) v.erase(...) : very slow (linear copying), except at the end 5) searching : slow (linear) Note that deque has the same properties, but it is fast on both ends. */ ------------------------------------------------------------------------------- /* Let's look at the operations of lists now. List _____ ==== .->/ \___ / _| 7 | \ | / \_____/<. | | | | | _____ _____ _|_V_ _|_V_ _____ _____ / \ --> / \ --> / \ --> / \ --> / \ --> / \ | 1 | | 2 | | 3 | | 4 | | 5 | | 6 | \_____/ <-- \_____/ <-- \_____/ <-- \_____/ <-- \_____/ <-- \_____/ Operation speed --------------- 1) l.push_back() / l.push_front() : fast (constant) 2) l[i] : N/A 3) l.insert(...) : slow (linear) 4) l.erase(...) : slow (linear) 5) searching : slow (linear) Associative Containers ====================== Operation speed --------------- 1) a.push_back() : N/A 2) a[i] : N/A 3) a.insert(...) : quite fast 4) a.erase(...) : quite fast 5) searching : quite fast (only for keys in maps) */ ------------------------------------------------------------------------------- /* Conclusion ========== |-------------+-----------------------------------------------------------| | TYPE | BEST USAGE | |-------------+-----------------------------------------------------------| | Vector | Random access and inserting mainly at the end | |-------------+-----------------------------------------------------------| | Deque | Random access and inserting mainly at both ends | |-------------+-----------------------------------------------------------| | List | Linear handling (e.g. from start to end), many insertions | |-------------+-----------------------------------------------------------| | Associative | Searching for values often | |-------------+-----------------------------------------------------------| */ ------------------------------------------------------------------------------- // 6. Algorithms // STL has a lot of algorithms, implemented very efficiently. // Here I only list the names of the most important ones, details // can be found in any STL reference (see Bibliography). // Most functions can have more (optional) parameters as well. // Ranges are given in the form [first, last) // Call function for all x in a range: std::for_each(first, last, &function); // function is a UnaryFunction // Find a value in the specified range: std::find(first, last, value); // Count the number of elements equal to value: std::count(first, last, value); // Compare two ranges if they are equal, in an element-to-element sense: std::equal(first1, last1, first2); // Copy a range to a specified place: std::copy(first, last, result); // result is an output iterator // Swap two values: std::swap(a, b); // Remove value from a range: std::remove(first, last, value); // Replace old_value with new_value in a range: std::replace(first, last, old_value, new_value); // Fill a range with values generated by invoking a function: std::generate(first, last, &function); // function is a Generator // Minimum/maximum of two values: std::min(a, b); std::max(a, b); // Minimal/maximal element in a range: std::min_element(first, last); std::max_element(first, last); // ... and a lot more, e.g.: // reverse, rotate, sort, lower_bound, upper_bound, equal_range, merge, etc. ------------------------------------------------------------------------------- // 7. Hands-on Exercises /* Do these exercises now (you can ask me if something is not clear) If there is not enough time, the rest is homework. A testing C++ file is available at: http://ortros/~salvi/stl/exercises.html 1) Write a function "add_subtract" that takes a list of int/double pairs. The first values are all different, they form the order of application. The function should add/subtract their second values interchangebly. Example: [(1, 2.3) (3, 4.1) (5, 5.0) (2, 1.0), (4, 2.9), (9, 7.0)] Result : 2.3 - 1.0 + 4.1 - 2.9 + 5.0 - 7.0 = 0.5 Hint: You can use a priority queue or std::sort. (do both for more practice :) 2) Write a function "minimum_value" that takes a multiset of int/std::string pairs. The function should return a map that maps every int to its minimal pair. Example: [(1, {"dog", "cat", "dragon"}) (25, {"tiger", "mouse"})] Result : [(1 -> "cat") (25 -> "mouse")] Hint: std::equal_range is your friend. Also, upper_bound(*iterator) gives the iterator with the next key. ************************* * That's all for today! * ************************* */ ------------------------------------------------------------------------------- // 8. Bibliography /* For an extensive function reference, see - http://www.sgi.com/tech/stl/ - http://cppreference.com/ - http://www.cplusplus.com/reference/ - ... and of course, the include files. The following books contain very good learning materials: - N. M. Josuttis: The C++ Standard Library: A Tutorial and Reference, Addison-Wesley, 1999. - D. R. Musser, G. J. Derge, A. Saini: The STL Tutorial and Reference Guide: C++ Programming with the Standard Template Library, Addison-Wesley, 2nd Edition, 2001 - S. Meyers: Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library, Addison-Wesley Professional, 2001. - B. Karlsson: Beyond the C++ Standard Library: An Introduction to Boost, Addison-Wesley Professional, 2005. */