Module 09 Tutorial
Prerequisites: Review Module 09 Concepts first.
This module focuses on practical STL applications. You’ll use different containers strategically: a map for date lookups, a stack for RPN evaluation, and two containers for sorting comparison.
Container allocation strategy:
| Exercise | Container | Why |
|---|---|---|
| ex00 | std::map | Sorted key-value pairs, efficient date lookup |
| ex01 | std::stack | LIFO operations for RPN evaluation |
| ex02 | std::vector + std::deque | Performance comparison for sorting |
Exercise 00: Bitcoin Exchange
Section titled “Exercise 00: Bitcoin Exchange”Subject Analysis
Section titled “Subject Analysis”Create a program that:
- Reads a database file (CSV:
date,exchange_rate) - Reads an input file (CSV:
date | value) - For each input line, calculates
value * exchange_rate - Uses the closest earlier date if exact match not found
Key constraints:
- Date format:
YYYY-MM-DD - Input value: positive number between 0 and 1000
- Must handle invalid input gracefully
- Database file provided:
data.csv
Approach Strategy
Section titled “Approach Strategy”Think before coding:
-
Which container for the database?
std::map<std::string, float>- perfect for key-value with sorted keys- String dates sort correctly lexicographically (“2020-01-15” < “2020-02-01”)
-
How to find closest earlier date?
map::lower_bound(key)returns iterator to first element >= key- If no exact match, go back one position
-
What validations are needed?
- Date format and validity (leap years!)
- Value is positive and <= 1000
- Line format (contains
|)
Progressive Code Building
Section titled “Progressive Code Building”Stage 1: Class structure
#ifndef BITCOINEXCHANGE_HPP#define BITCOINEXCHANGE_HPP
#include <map>#include <string>
class BitcoinExchange {private: std::map<std::string, float> _database;
bool isValidDate(const std::string& date) const; bool isValidValue(const std::string& value, float& result) const; std::string trim(const std::string& str) const;
public: BitcoinExchange(); BitcoinExchange(const BitcoinExchange& other); BitcoinExchange& operator=(const BitcoinExchange& other); ~BitcoinExchange();
void loadDatabase(const std::string& filename); void processInput(const std::string& filename);};
#endifStage 2: Load database
#include "BitcoinExchange.hpp"#include <fstream>#include <sstream>#include <iostream>
void BitcoinExchange::loadDatabase(const std::string& filename) { std::ifstream file(filename.c_str()); if (!file.is_open()) { throw std::runtime_error("Error: could not open database file."); }
std::string line; std::getline(file, line); // Skip header
while (std::getline(file, line)) { size_t comma = line.find(','); if (comma == std::string::npos) continue;
std::string date = line.substr(0, comma); std::string rateStr = line.substr(comma + 1);
float rate; std::istringstream iss(rateStr); if (iss >> rate) { _database[date] = rate; } }}Stage 3: Date validation
bool BitcoinExchange::isValidDate(const std::string& date) const { // Check format: YYYY-MM-DD (10 chars) if (date.length() != 10) return false; if (date[4] != '-' || date[7] != '-') return false;
// Extract parts int year, month, day; if (sscanf(date.c_str(), "%d-%d-%d", &year, &month, &day) != 3) { return false; }
// Validate ranges if (year < 1 || month < 1 || month > 12 || day < 1) return false;
// Days per month int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// Leap year check bool isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); if (isLeap) daysInMonth[2] = 29;
return day <= daysInMonth[month];}
bool BitcoinExchange::isValidValue(const std::string& value, float& result) const { std::istringstream iss(value); if (!(iss >> result)) return false; if (result < 0) return false; return true;}Stage 4: Find closest date and process
void BitcoinExchange::processInput(const std::string& filename) { std::ifstream file(filename.c_str()); if (!file.is_open()) { std::cerr << "Error: could not open file." << std::endl; return; }
std::string line; std::getline(file, line); // Skip header
while (std::getline(file, line)) { size_t pipe = line.find(" | "); if (pipe == std::string::npos) { std::cerr << "Error: bad input => " << line << std::endl; continue; }
std::string date = trim(line.substr(0, pipe)); std::string valueStr = trim(line.substr(pipe + 3));
// Validate date if (!isValidDate(date)) { std::cerr << "Error: bad input => " << date << std::endl; continue; }
// Validate value float value; if (!isValidValue(valueStr, value)) { std::cerr << "Error: bad input => " << valueStr << std::endl; continue; } if (value < 0) { std::cerr << "Error: not a positive number." << std::endl; continue; } if (value > 1000) { std::cerr << "Error: too large a number." << std::endl; continue; }
// Find closest date std::map<std::string, float>::iterator it = _database.lower_bound(date); if (it == _database.end() || it->first != date) { if (it == _database.begin()) { std::cerr << "Error: date too early => " << date << std::endl; continue; } --it; // Go to previous (closest earlier) date }
float rate = it->second; std::cout << date << " => " << value << " = " << (value * rate) << std::endl; }}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”Understanding lower_bound:
| Scenario | lower_bound returns |
|---|---|
| Exact match exists | Iterator to that element |
| No exact match | Iterator to first element > key |
| All elements < key | end() iterator |
std::map<std::string, float> db;db["2020-01-01"] = 100;db["2020-02-01"] = 200;db["2020-03-01"] = 300;
// Case 1: Exact matchdb.lower_bound("2020-02-01"); // Points to 2020-02-01
// Case 2: Between two datesdb.lower_bound("2020-02-15"); // Points to 2020-03-01// We want 2020-02-01, so --it
// Case 3: Before all datesdb.lower_bound("2019-01-01"); // Points to 2020-01-01// No earlier date exists - error!Leap year calculation:
bool isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);// 2000: leap (divisible by 400)// 1900: not leap (divisible by 100 but not 400)// 2020: leap (divisible by 4, not by 100)// 2021: not leapCommon Pitfalls
Section titled “Common Pitfalls”1. Not handling “earlier than database” case:
// WRONG: Assumes there's always an earlier datestd::map<std::string, float>::iterator it = _database.lower_bound(date);--it; // Undefined behavior if it == begin()!
// RIGHT: Check for begin()if (it == _database.begin()) { std::cerr << "Error: date too early" << std::endl; continue;}--it;2. Forgetting leap years:
// WRONG: February always 28 daysint daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// RIGHT: Check leap yearif (isLeap) daysInMonth[2] = 29;3. Wrong pipe format:
// WRONG: Subject uses " | " (with spaces)size_t pipe = line.find("|");
// RIGHT: Look for " | "size_t pipe = line.find(" | ");Testing Tips
Section titled “Testing Tips”Create test input file:
date | value2011-01-03 | 32011-01-03 | 22011-01-03 | 12011-01-03 | 1.22011-01-09 | 12012-01-11 | -12001-42-422012-01-11 | 12012-01-11 | 2147483648Test script:
./btc input.txt
# Expected output:# 2011-01-03 => 3 = 0.9# 2011-01-03 => 2 = 0.6# 2011-01-03 => 1 = 0.3# 2011-01-03 => 1.2 = 0.36# 2011-01-09 => 1 = 0.32# Error: not a positive number.# Error: bad input => 2001-42-42# 2012-01-11 => 1 = 7.1# Error: too large a number.Final Code
Section titled “Final Code”#ifndef BITCOINEXCHANGE_HPP#define BITCOINEXCHANGE_HPP
#include <map>#include <string>
class BitcoinExchange {private: std::map<std::string, float> _database;
bool isValidDate(const std::string& date) const; std::string trim(const std::string& str) const;
public: BitcoinExchange(); BitcoinExchange(const BitcoinExchange& other); BitcoinExchange& operator=(const BitcoinExchange& other); ~BitcoinExchange();
void loadDatabase(const std::string& filename); void processInput(const std::string& filename);};
#endifExercise 01: RPN (Reverse Polish Notation)
Section titled “Exercise 01: RPN (Reverse Polish Notation)”Subject Analysis
Section titled “Subject Analysis”Create a program that:
- Takes an RPN expression as argument
- Evaluates it and prints the result
- Numbers are always single digits (< 10)
- Operators:
+,-,*,/
RPN explained:
- Normal:
(3 + 4) * 2 - RPN:
3 4 + 2 * - Read left to right, no parentheses needed
Approach Strategy
Section titled “Approach Strategy”Think before coding:
-
Why use a stack?
- RPN naturally uses LIFO order
- Push numbers, pop for operations, push result
-
Algorithm:
For each token:If number: push to stackIf operator: pop two, calculate, push resultFinal answer: only item on stack -
Order matters for subtraction/division:
5 3 -means5 - 3 = 2, not3 - 5- Pop b first, then a:
result = a OP b
Progressive Code Building
Section titled “Progressive Code Building”Stage 1: Class structure
#ifndef RPN_HPP#define RPN_HPP
#include <stack>#include <string>
class RPN {private: std::stack<int> _stack;
bool isOperator(char c) const;
public: RPN(); RPN(const RPN& other); RPN& operator=(const RPN& other); ~RPN();
int evaluate(const std::string& expression);};
#endifStage 2: Core evaluation
#include "RPN.hpp"#include <cctype>#include <stdexcept>
bool RPN::isOperator(char c) const { return c == '+' || c == '-' || c == '*' || c == '/';}
int RPN::evaluate(const std::string& expression) { // Clear stack from any previous run while (!_stack.empty()) { _stack.pop(); }
for (size_t i = 0; i < expression.length(); i++) { char c = expression[i];
// Skip spaces if (c == ' ') { continue; }
// Number: push to stack if (std::isdigit(c)) { _stack.push(c - '0'); } // Operator: pop two, calculate, push result else if (isOperator(c)) { if (_stack.size() < 2) { throw std::runtime_error("Error"); }
int b = _stack.top(); _stack.pop(); int a = _stack.top(); _stack.pop();
int result; switch (c) { case '+': result = a + b; break; case '-': result = a - b; break; case '*': result = a * b; break; case '/': if (b == 0) { throw std::runtime_error("Error"); } result = a / b; break; default: throw std::runtime_error("Error"); } _stack.push(result); } // Invalid character else { throw std::runtime_error("Error"); } }
// Must have exactly one result if (_stack.size() != 1) { throw std::runtime_error("Error"); }
return _stack.top();}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”RPN evaluation trace:
Expression: "8 9 * 9 - 9 - 9 - 4 - 1 +"
Step | Token | Stack (bottom->top) | Action-----|-------|---------------------|--------1 | 8 | [8] | Push 82 | 9 | [8, 9] | Push 93 | * | [72] | Pop 9,8; push 8*9=724 | 9 | [72, 9] | Push 95 | - | [63] | Pop 9,72; push 72-9=636 | 9 | [63, 9] | Push 97 | - | [54] | Pop 9,63; push 63-9=548 | 9 | [54, 9] | Push 99 | - | [45] | Pop 9,54; push 54-9=4510 | 4 | [45, 4] | Push 411 | - | [41] | Pop 4,45; push 45-4=4112 | 1 | [41, 1] | Push 113 | + | [42] | Pop 1,41; push 41+1=42
Result: 42Why pop b before a?
// For "5 3 -":// Stack after pushes: [5, 3] (3 on top)int b = _stack.top(); _stack.pop(); // b = 3int a = _stack.top(); _stack.pop(); // a = 5result = a - b; // 5 - 3 = 2 (correct!)
// If we did a - b wrong:// result = b - a; // 3 - 5 = -2 (wrong!)Common Pitfalls
Section titled “Common Pitfalls”1. Wrong operand order:
// WRONG: Reversed operandsint a = _stack.top(); _stack.pop();int b = _stack.top(); _stack.pop();result = a - b; // Wrong order!
// RIGHT: b is popped first (was on top)int b = _stack.top(); _stack.pop();int a = _stack.top(); _stack.pop();result = a - b; // Correct!2. Not handling division by zero:
// WRONG: Crash on divide by zerocase '/': result = a / b; break;
// RIGHT: Check firstcase '/': if (b == 0) throw std::runtime_error("Error"); result = a / b; break;3. Accepting multi-digit numbers:
// WRONG: Subject says single digits onlyif (std::isdigit(c)) { // Parse full number...}
// RIGHT: Single digit onlyif (std::isdigit(c)) { _stack.push(c - '0'); // Only 0-9}4. Not validating final stack:
// WRONG: Accept any stack statereturn _stack.top();
// RIGHT: Must have exactly one resultif (_stack.size() != 1) { throw std::runtime_error("Error");}return _stack.top();Testing Tips
Section titled “Testing Tips”#include <iostream>#include "RPN.hpp"
int main(int argc, char** argv) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " <expression>" << std::endl; return 1; }
RPN calculator; try { int result = calculator.evaluate(argv[1]); std::cout << result << std::endl; } catch (std::exception& e) { std::cerr << e.what() << std::endl; return 1; }
return 0;}Test cases:
./RPN "8 9 * 9 - 9 - 9 - 4 - 1 +" # 42./RPN "7 7 * 7 -" # 42./RPN "1 2 * 2 / 2 * 2 4 - +" # 0./RPN "(1 + 1)" # Error (parentheses not allowed)./RPN "1 2 + +" # Error (not enough operands)./RPN "1 2" # Error (no operator)./RPN "5 0 /" # Error (division by zero)Final Code
Section titled “Final Code”#ifndef RPN_HPP#define RPN_HPP
#include <stack>#include <string>
class RPN {private: std::stack<int> _stack; bool isOperator(char c) const;
public: RPN(); RPN(const RPN& other); RPN& operator=(const RPN& other); ~RPN();
int evaluate(const std::string& expression);};
#endif#include "RPN.hpp"#include <cctype>#include <stdexcept>
RPN::RPN() {}RPN::RPN(const RPN& other) : _stack(other._stack) {}RPN& RPN::operator=(const RPN& other) { if (this != &other) _stack = other._stack; return *this;}RPN::~RPN() {}
bool RPN::isOperator(char c) const { return c == '+' || c == '-' || c == '*' || c == '/';}
int RPN::evaluate(const std::string& expression) { while (!_stack.empty()) _stack.pop();
for (size_t i = 0; i < expression.length(); i++) { char c = expression[i];
if (c == ' ') continue;
if (std::isdigit(c)) { _stack.push(c - '0'); } else if (isOperator(c)) { if (_stack.size() < 2) throw std::runtime_error("Error");
int b = _stack.top(); _stack.pop(); int a = _stack.top(); _stack.pop();
switch (c) { case '+': _stack.push(a + b); break; case '-': _stack.push(a - b); break; case '*': _stack.push(a * b); break; case '/': if (b == 0) throw std::runtime_error("Error"); _stack.push(a / b); break; } } else { throw std::runtime_error("Error"); } }
if (_stack.size() != 1) throw std::runtime_error("Error");
return _stack.top();}Exercise 02: PmergeMe
Section titled “Exercise 02: PmergeMe”Subject Analysis
Section titled “Subject Analysis”Implement the Ford-Johnson merge-insertion sort algorithm:
- Take positive integers as arguments
- Sort using two different containers
- Display: before, after, time for each container
- Handle at least 3000 numbers
Output format:
Before: 3 5 9 7 4After: 3 4 5 7 9Time to process a range of 5 elements with std::vector : 0.00031 usTime to process a range of 5 elements with std::deque : 0.00014 usApproach Strategy
Section titled “Approach Strategy”Think before coding:
-
What is Ford-Johnson?
- Also called “merge-insertion sort”
- Minimizes comparisons through clever pairing
- Uses binary insertion with Jacobsthal sequence
-
Simplified algorithm:
1. Pair adjacent elements2. Put larger in "main chain", smaller in "pend"3. Recursively sort main chain4. Insert pend elements using binary search -
Why two containers?
- Subject requires performance comparison
vector: contiguous memory, cache-friendlydeque: fast insertion at both ends
Progressive Code Building
Section titled “Progressive Code Building”Stage 1: Class structure
#ifndef PMERGEME_HPP#define PMERGEME_HPP
#include <vector>#include <deque>#include <string>
class PmergeMe {private: std::vector<int> _vec; std::deque<int> _deq;
void sortVector(); void sortDeque();
public: PmergeMe(); PmergeMe(const PmergeMe& other); PmergeMe& operator=(const PmergeMe& other); ~PmergeMe();
void parseInput(int argc, char** argv); void sort(); void displayBefore() const; void displayAfter() const;};
#endifStage 2: Parse and validate input
#include "PmergeMe.hpp"#include <cstdlib>#include <stdexcept>#include <iostream>
void PmergeMe::parseInput(int argc, char** argv) { for (int i = 1; i < argc; i++) { char* endptr; long num = std::strtol(argv[i], &endptr, 10);
if (*endptr != '\0') { throw std::runtime_error("Error: invalid input."); } if (num <= 0) { throw std::runtime_error("Error: only positive integers."); } if (num > 2147483647) { throw std::runtime_error("Error: integer overflow."); }
_vec.push_back(static_cast<int>(num)); _deq.push_back(static_cast<int>(num)); }}Stage 3: Ford-Johnson for vector
void PmergeMe::sortVector() { if (_vec.size() <= 1) return;
std::vector<int> larger, smaller; bool hasOdd = (_vec.size() % 2 != 0); int oddElement = hasOdd ? _vec.back() : 0;
// Step 1: Pair elements size_t limit = hasOdd ? _vec.size() - 1 : _vec.size(); for (size_t i = 0; i < limit; i += 2) { if (_vec[i] > _vec[i + 1]) { larger.push_back(_vec[i]); smaller.push_back(_vec[i + 1]); } else { larger.push_back(_vec[i + 1]); smaller.push_back(_vec[i]); } }
// Step 2: Recursively sort larger elements _vec = larger; sortVector(); larger = _vec;
// Step 3: Insert smaller elements using binary search for (size_t i = 0; i < smaller.size(); i++) { std::vector<int>::iterator pos = std::lower_bound(larger.begin(), larger.end(), smaller[i]); larger.insert(pos, smaller[i]); }
// Step 4: Insert odd element if exists if (hasOdd) { std::vector<int>::iterator pos = std::lower_bound(larger.begin(), larger.end(), oddElement); larger.insert(pos, oddElement); }
_vec = larger;}Stage 4: Timing
#include <ctime>
void PmergeMe::sort() { // Time vector sort clock_t startVec = clock(); sortVector(); clock_t endVec = clock(); double timeVec = (double)(endVec - startVec) / CLOCKS_PER_SEC * 1000000;
// Time deque sort clock_t startDeq = clock(); sortDeque(); clock_t endDeq = clock(); double timeDeq = (double)(endDeq - startDeq) / CLOCKS_PER_SEC * 1000000;
std::cout << "Time to process a range of " << _vec.size() << " elements with std::vector : " << timeVec << " us" << std::endl; std::cout << "Time to process a range of " << _deq.size() << " elements with std::deque : " << timeDeq << " us" << std::endl;}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”Ford-Johnson algorithm trace:
Input: [3, 5, 9, 7, 4, 1]
Step 1: Pair and separatePairs: (3,5) (9,7) (4,1)Larger: [5, 9, 4]Smaller: [3, 7, 1]
Step 2: Recursively sort larger[5, 9, 4] -> [4, 5, 9] (after recursion)
Step 3: Insert smaller using binary searchInsert 3: [3, 4, 5, 9]Insert 7: [3, 4, 5, 7, 9]Insert 1: [1, 3, 4, 5, 7, 9]
Result: [1, 3, 4, 5, 7, 9]Why lower_bound for insertion?
// lower_bound returns iterator to first element >= valuestd::vector<int> v = {1, 3, 5, 7};
// Insert 4:std::vector<int>::iterator pos = std::lower_bound(v.begin(), v.end(), 4);// pos points to 5 (first element >= 4)v.insert(pos, 4);// v = {1, 3, 4, 5, 7}Common Pitfalls
Section titled “Common Pitfalls”1. Not implementing actual Ford-Johnson:
// WRONG: Just using std::sortvoid sortVector() { std::sort(_vec.begin(), _vec.end());}
// RIGHT: Implement the pairing and recursive merge-insertion2. Forgetting odd element:
// WRONG: Ignores odd elementfor (size_t i = 0; i < _vec.size(); i += 2) { // Pairs only}
// RIGHT: Handle odd element separatelybool hasOdd = (_vec.size() % 2 != 0);int oddElement = hasOdd ? _vec.back() : 0;size_t limit = hasOdd ? _vec.size() - 1 : _vec.size();3. Timing the wrong thing:
// WRONG: Include parsing in timingclock_t start = clock();parseInput(argc, argv); // Wrong!sortVector();clock_t end = clock();
// RIGHT: Only time the sortparseInput(argc, argv);clock_t start = clock();sortVector();clock_t end = clock();4. Using same container:
// WRONG: Subject requires TWO different containersstd::vector<int> _data1;std::vector<int> _data2; // Both vectors!
// RIGHT: Different container typesstd::vector<int> _vec;std::deque<int> _deq;Testing Tips
Section titled “Testing Tips”# Generate test numbers./PmergeMe 3 5 9 7 4
# Test with random numbers./PmergeMe `shuf -i 1-100000 -n 3000 | tr "\n" " "`
# Verify sorting./PmergeMe 5 4 3 2 1# Should show: Before: 5 4 3 2 1# After: 1 2 3 4 5Verification script:
#!/bin/bash# Generate random numbers, sort, and verifyNUMS=$(shuf -i 1-10000 -n 100 | tr "\n" " ")./PmergeMe $NUMS 2>&1 | head -2
# Compare with sort commandecho "Expected: $(echo $NUMS | tr " " "\n" | sort -n | tr "\n" " ")"Final Code
Section titled “Final Code”#ifndef PMERGEME_HPP#define PMERGEME_HPP
#include <vector>#include <deque>
class PmergeMe {private: std::vector<int> _vec; std::deque<int> _deq;
void sortVector(); void sortDeque();
public: PmergeMe(); PmergeMe(const PmergeMe& other); PmergeMe& operator=(const PmergeMe& other); ~PmergeMe();
void parseInput(int argc, char** argv); void sort(); void displayBefore() const; void displayAfter() const;};
#endif#include <iostream>#include "PmergeMe.hpp"
int main(int argc, char** argv) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " <numbers...>" << std::endl; return 1; }
try { PmergeMe sorter; sorter.parseInput(argc, argv); sorter.displayBefore(); sorter.sort(); sorter.displayAfter(); } catch (std::exception& e) { std::cerr << e.what() << std::endl; return 1; }
return 0;}Module 09 Summary
Section titled “Module 09 Summary”| Exercise | Container | Algorithm |
|---|---|---|
| ex00 | std::map | lower_bound for date lookup |
| ex01 | std::stack | RPN evaluation (LIFO) |
| ex02 | vector + deque | Ford-Johnson merge-insertion |
Key takeaways:
- Choose containers wisely - each has strengths
- STL algorithms -
lower_bound,sort,findsave time - Performance matters - measure with
clock() - Validate input - robust error handling is essential