A1 FA25 Final Exam Review
Table of Contents:
- Preliminaries
- 1. HashMap and the Entry API
- 2. HashSet and Set Operations
- 3. BTreeMap and Ordered Collections
- 4. VecDeque and Circular Buffers
- 5. Iterators and Iterator Chains
- 6. Algorithm Complexity
- 7. Option and Result Types
- Final Tips for the Exam
Suggested way to use this review material
- The material is organized by major topics.
- For each topic, there are:
- high level overview
- examples
- true/false questions
- find the bug questions
- predict the output questions
- coding challenges
- Try to answer the questions without peeking at the solutions.
- This material focuses on the topics covered in the final third of the course, building on what you learned for midterms 1 and 2.
Exam Format:
The exam will be in four parts:
- Part 1 (10 pts): 5 questions, 2 points each -- select all that are true
- Part 2 (16 pts): 4 questions, 4 points each -- find the bug in the code and fix it
- Part 3 (12 pts): 4 questions, 3 points each -- Predict the output and explain why
- Part 4 (12 pts): 2 questions, 6 points each -- hand-coding problems
Total Points: 50
Suggested time budget for each part:
- Part 1: (~10 min)
- Part 2: (~16 min)
- Part 3: (~12 min)
- Part 4: (~22 min)
for a total of 60 minutes and then another 60 minutes to check your work (if needed).
Preliminaries
The final exam is cumulative but emphasizes the material from the final third of the course. You should be comfortable with:
- Basic Rust syntax (functions, variables, types) (see midterm 1 review)
- Structs, enums, and pattern matching
- Ownership, borrowing, and references
- Generics and traits
- Iterators and closures
See a1 midterm 2 review for more details.
This review focuses on new material: collections (HashMap, HashSet, BTreeMap, VecDeque) and algorithm complexity.
References and Dereferencing
When References Are Created
References are created with &:
#![allow(unused)] fn main() { let x = 5; let r = &x; // r is &i32 let s = "hello"; // s is already &str (string slice) let v = vec![1, 2, 3]; let slice = &v[..]; // slice is &[i32] }
And are common patterns in Rust code. For example, to sum a slice of integers:
#![allow(unused)] fn main() { fn process_ints(ints: &[i32]) -> i32 { let mut sum = 0; for int in ints { sum += *int; } sum } let ints = [1, 2, 3]; println!("sum: {}", process_ints(&ints)); }
When Double References (&&) Occur
Double references commonly appear when:
Iterating over a slice of references:
#![allow(unused)] fn main() { fn process(words: &[&str]) { for word in words { // word is &&str // Rust auto-dereferences `word: &&str` to `word: &str` print!("word: {}, len: {} ", word, word.len()); } println!(); } let words = vec!["art", "bees"]; process(&words); }
Automatic Dereferencing
Rust automatically dereferences in several situations:
1. Method calls (auto-deref):
#![allow(unused)] fn main() { let s = String::from("hello"); let r = &s; let rr = &&s; // All of these work - Rust auto-derefs to call len() s.len(); // String::len(&s) r.len(); // auto-derefs &String to String rr.len(); // auto-derefs &&String through &String to String }
2. Deref coercion in function arguments:
#![allow(unused)] fn main() { fn print_len(s: &str) { println!("{}", s.len()); } let owned = String::from("hello"); print_len(&owned); // &String coerces to &str automatically print_len("hello"); // Already a &str }
3. Comparison operators:
#![allow(unused)] fn main() { let x = 5; let r = &x; // assert!(r == 5); // ERROR! r is a reference, not a value assert!(r == &5); // Compares values, not addresses, but types must match assert!(*r == 5); // Explicit deref to i32 also works }
When Explicit Dereferencing (*) Is Required
1. Assigning to or modifying the underlying value:
#![allow(unused)] fn main() { let mut x = 5; let r = &mut x; *r += 1; // Must deref to modify x println!("x: {}", x); }
2. When types don't match and coercion doesn't apply:
#![allow(unused)] fn main() { let words: &[&str] = &["a", "b"]; for word in words { // word is &&str, but HashMap wants &str as key let key: &str = *word; // Explicit deref needed } }
3. Using entry() or insert() with reference keys:
#![allow(unused)] fn main() { use std::collections::HashMap; fn count<'a>(words: &[&'a str]) -> HashMap<&'a str, i32> { let mut map = HashMap::new(); for word in words { // word: &&str *map.entry(*word) // *word dereferences &&str to &str .or_insert(0) += 1; } map } let words = vec!["a", "b"]; println!("{:?}", count(&words)); println!("{:?}", words); }
Quick Reference Table
| Situation | Type | Need explicit *? |
|---|---|---|
| Method calls | &T, &&T, etc. | No (auto-deref) |
Deref coercion (&String → &str) | Function args | No |
Modifying through &mut T | *r = value | Yes |
HashMap key from &&str | entry(*word) | Yes |
| Pattern matching | &x pattern | Alternative to * |
1. HashMap and the Entry API
Module(s)
Quick Review
HashMap<K, V> is a hash table that maps keys to values:
- Keys must implement
HashandEqtraits - O(1) average lookup, insertion, and deletion
- Does NOT maintain insertion order
f64cannot be used directly as a key (doesn't implementHashdue to NaN)
Key Methods:
insert(key, value)- inserts or overwritesget(&key)- returnsOption<&V>get_mut(&key)- returnsOption<&mut V>contains_key(&key)- returnsboolremove(&key)- removes and returnsOption<V>
The Entry API is the idiomatic way to insert-or-update:
#![allow(unused)] fn main() { *map.entry(key).or_insert(default) += 1; }
.entry(key)returns anEntryenum, which can be eitherOccupiedorVacant- Entry API methods:
or_insert(default)inserts the default value if the key is not presentor_insert_with(f)inserts the value returned by the function if the key is not presentor_default()inserts the default value for the type if the key is not presentand_modify(f)modifies the value if the key is present
Examples
#![allow(unused)] fn main() { use std::collections::HashMap; // Basic HashMap usage let mut scores = HashMap::new(); // .insert returns None if the key was not in the map let mut result = scores.insert("Alice", 85); println!("result: {:?}", result); // None, because "Alice" was not in the map // .insert() returns Some(&value), where value is the old value if the key was // already in the map result = scores.insert("Alice", 87); println!("result: {:?}", result); // Some(&85) scores.insert("Bob", 90); // get() returns Option println!("scores.get(\"Alice\"): {:?}", scores.get("Alice")); // Some(&85) println!("scores.get(\"Carol\"): {:?}", scores.get("Carol")); // None // unwrap_or provides a default println!("scores.get(\"Carol\").unwrap_or(&0): {:?}", scores.get("Carol").unwrap_or(&0)); // &0 }
#![allow(unused)] fn main() { use std::collections::HashMap; // Entry API for counting let mut word_counts = HashMap::new(); for word in ["apple", "banana", "apple"] { *word_counts.entry(word).or_insert(0) += 1; } // word_counts: {"apple": 2, "banana": 1} println!("word_counts: {:?}", word_counts); }
#![allow(unused)] fn main() { use std::collections::HashMap; // Entry API - or_insert only inserts if key is missing let mut map = HashMap::new(); *map.entry("a").or_insert(0) += 1; // a = 1 *map.entry("a").or_insert(10) += 1; // a = 2 (10 is NOT used, key exists) println!("map: {:?}", map); }
True/False Questions
-
T/F: Keys in a HashMap must implement the
HashandEqtraits. -
T/F: HashMap maintains insertion order of elements.
-
T/F:
f64can be used directly as a HashMap key. -
T/F: The
entry()API allows efficient insert-or-update operations. -
T/F:
map.get(&key)returnsVdirectly. -
T/F: Looking up a value by key in a HashMap is O(1) on average.
Answers
- True - HashMap requires Hash and Eq traits for keys
- False - HashMap does not maintain insertion order (use IndexMap for that)
- False - f64 doesn't implement Hash due to NaN issues; use OrderedFloat
- True - The entry() API is designed for efficient insert-or-update patterns
- False - get() returns Option<&V>, not V directly
- True - HashMap lookup is O(1) average case
Find the Bug
Question 1:
#![allow(unused)] fn main() { use std::collections::HashMap; fn count_words<'a>(words: &[&'a str]) -> HashMap<&'a str, i32> { let mut counts = HashMap::new(); for word in words { counts.entry(word).or_insert(0) += 1; } counts } }
Answer
Bug: We need to dereference the key word to get the &str, not the &&str
and then dereference counts... so we can modify the value.
Fix:
#![allow(unused)] fn main() { use std::collections::HashMap; fn count_words<'a>(words: &[&'a str]) -> HashMap<&'a str, i32> { let mut counts = HashMap::new(); for word in words { *counts.entry(*word).or_insert(0) += 1; } counts } }
Question 2:
#![allow(unused)] fn main() { use std::collections::HashMap; fn merge_maps(map1: HashMap<String, i32>, map2: HashMap<String, i32>) -> HashMap<String, i32> { let mut result = map1; for (key, value) in map2 { result.insert(key, result.get(&key).unwrap() + value); } result } }
Answer
Bug: Using get(&key).unwrap() on a key that might not exist in result (map1). If a key from map2 is not in map1, this panics.
Fix:
#![allow(unused)] fn main() { use std::collections::HashMap; fn merge_maps(map1: HashMap<String, i32>, map2: HashMap<String, i32>) -> HashMap<String, i32> { let mut result = map1; for (key, value) in map2 { *result.entry(key).or_insert(0) += value; } result } }
Predict the Output
Question 1:
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("Alice", 85); scores.insert("Bob", 90); scores.insert("Alice", 95); let alice_score = scores.get("Alice").unwrap_or(&0); let carol_score = scores.get("Carol").unwrap_or(&0); println!("{} {}", alice_score, carol_score); }
Answer
Output: 95 0
Reasoning:
- "Alice" is inserted twice. The second insert (95) overwrites the first (85).
get("Alice")returns Some(&95), unwrap_or gives 95get("Carol")returns None, unwrap_or provides default &0
Question 2:
use std::collections::HashMap; fn main() { let mut map: HashMap<&str, i32> = HashMap::new(); *map.entry("a").or_insert(0) += 1; *map.entry("b").or_insert(5) += 1; *map.entry("a").or_insert(10) += 1; let a = map.get("a").unwrap(); let b = map.get("b").unwrap(); println!("{} {}", a, b); }
Answer
Output: 2 6
Reasoning:
- First entry("a"): key doesn't exist, inserts 0, then += 1 → a = 1
- entry("b"): key doesn't exist, inserts 5, then += 1 → b = 6
- Second entry("a"): key exists (value 1), or_insert(10) does NOT insert, just returns &mut to existing value, then += 1 → a = 2
Coding Challenge
Challenge: Implement most_frequent
Write a function that takes a slice of integers and returns the value that appears most frequently. Return None if the slice is empty.
use std::collections::HashMap; fn most_frequent(numbers: &[i32]) -> Option<i32> { // Your code here } fn main() { let nums = vec![1, 2, 2, 3, 3, 3, 4]; println!("{:?}", most_frequent(&nums)); // Should print Some(3) println!("{:?}", most_frequent(&[])); // Should print None }
Solution
use std::collections::HashMap; fn most_frequent(numbers: &[i32]) -> Option<i32> { if numbers.is_empty() { return None; } let mut counts = HashMap::new(); for &num in numbers { *counts.entry(num).or_insert(0) += 1; } counts.into_iter() .max_by_key(|&(_, count)| count) .map(|(num, _)| num) } fn main() { let nums = vec![1, 2, 2, 3, 3, 3, 4]; println!("{:?}", most_frequent(&nums)); // Should print Some(3) println!("{:?}", most_frequent(&[])); // Should print None }
2. HashSet and Set Operations
Quick Review
HashSet
- Elements must implement
HashandEqtraits - O(1) average lookup, insertion, deletion
- Automatically removes duplicates
- Does NOT maintain insertion order
Key Methods:
insert(value)- returnsbool(true if new)contains(&value)- returnsbool(true if value is in the set)remove(&value)- returnsbool(true if value was in the set)
Set Operations:
intersection(&other)- returns a set with elements in both setsunion(&other)- returns a set with elements in either setdifference(&other)- returns a set with elements in self but not othersymmetric_difference(&other)- returns a set with elements in one but not both
Examples
#![allow(unused)] fn main() { use std::collections::HashSet; // Creating HashSets let set1: HashSet<i32> = vec![1, 2, 3, 4].into_iter().collect(); let set2: HashSet<i32> = vec![3, 4, 5, 6].into_iter().collect(); // Set operations let inter: HashSet<_> = set1.intersection(&set2).copied().collect(); println!("inter: {:?}", inter); // inter = {3, 4} let uni: HashSet<_> = set1.union(&set2).copied().collect(); println!("uni: {:?}", uni); // uni = {1, 2, 3, 4, 5, 6} let diff: HashSet<_> = set1.difference(&set2).copied().collect(); println!("diff: {:?}", diff); // diff = {1, 2} (in set1 but not set2) let sym_diff: HashSet<_> = set1.symmetric_difference(&set2).copied().collect(); println!("sym_diff: {:?}", sym_diff); // sym_diff = {1, 2, 5, 6} // Checking membership let has_three = set1.contains(&3); // true println!("has_three: {}", has_three); // HashSet for uniqueness let words = vec!["apple", "banana", "apple", "cherry"]; let unique: HashSet<_> = words.into_iter().collect(); println!("unique: {:?}", unique); // unique = {"apple", "banana", "cherry"} }
True/False Questions
-
T/F: HashSet automatically removes duplicate values.
-
T/F: Elements in a HashSet must implement
HashandEqtraits. -
T/F: HashSet maintains elements in sorted order.
-
T/F: The
intersection()method returns elements common to two sets. -
T/F: Checking if an element exists in a HashSet is O(n).
Answers
- True - HashSet stores only unique values
- True - HashSet requires Hash and Eq traits for elements
- False - HashSet doesn't maintain any particular order (use BTreeSet for sorted)
- True - intersection() returns elements present in both sets
- False - HashSet lookup is O(1) average, not O(n)
Find the Bug
Question:
#![allow(unused)] fn main() { use std::collections::HashSet; fn find_common<T: PartialEq>(set1: &HashSet<T>, set2: &HashSet<T>) -> HashSet<T> { set1.intersection(set2).cloned().collect() } }
Answer
Bug: HashSet requires Hash + Eq trait bounds, not just PartialEq. The function also needs Clone for .cloned().
Fix:
#![allow(unused)] fn main() { use std::collections::HashSet; use std::hash::Hash; fn find_common<T: Hash + Eq + Clone>(set1: &HashSet<T>, set2: &HashSet<T>) -> HashSet<T> { set1.intersection(set2).cloned().collect() } }
Predict the Output
Question:
use std::collections::HashSet; fn main() { let set1: HashSet<&str> = vec!["apple", "banana", "cherry"].into_iter().collect(); let set2: HashSet<&str> = vec!["cherry", "date", "elderberry"].into_iter().collect(); let inter: HashSet<_> = set1.intersection(&set2).copied().collect(); println!("inter: {:?}", inter); let diff: HashSet<_> = set1.difference(&set2).copied().collect(); println!("diff: {:?}", diff); let sym_diff: HashSet<_> = set1.symmetric_difference(&set2).copied().collect(); println!("sym_diff: {:?}", sym_diff); println!("{} {} {}", inter.len(), diff.len(), sym_diff.len()); }
Answer
Output:
inter: {"cherry"}
diff: {"banana", "apple"}
sym_diff: {"elderberry", "apple", "date", "banana"}
1 2 4
Reasoning:
- set1: {"apple", "banana", "cherry"}
- set2: {"cherry", "date", "elderberry"}
- intersection: {"cherry"} → length = 1
- difference (in set1 but not set2): {"apple", "banana"} → length = 2
- symmetric difference: {"apple", "banana", "date", "elderberry"} → length = 4
Coding Challenge
Challenge: Find duplicates
Write a function that takes a slice of integers and returns a Vec containing only the values that appear more than once. The result should not contain duplicates itself.
use std::collections::{HashMap, HashSet}; fn find_duplicates(numbers: &[i32]) -> Vec<i32> { // Your code here } fn main() { let nums = vec![1, 2, 2, 3, 3, 3, 4, 5, 5]; println!("{:?}", find_duplicates(&nums)); // [2, 3, 5] (order may vary) }
Solution
use std::collections::HashSet; fn find_duplicates(numbers: &[i32]) -> Vec<i32> { let mut seen = HashSet::new(); let mut duplicates = HashSet::new(); for &num in numbers { if !seen.insert(num) { // insert returns false if value already existed duplicates.insert(num); } } duplicates.into_iter().collect() } fn main() { let nums = vec![1, 2, 2, 3, 3, 3, 4, 5, 5]; println!("{:?}", find_duplicates(&nums)); // [2, 3, 5] (order may vary) }
3. BTreeMap and Ordered Collections
Quick Review
BTreeMap<K, V> is a sorted map based on B-trees:
- Keys are always in sorted order
- Keys must implement
Ordtrait (not Hash) - O(log n) lookup, insertion, deletion
- Efficient for range queries
- Iteration yields key-value pairs in sorted key order
When to use BTreeMap vs HashMap:
- HashMap: faster single-key operations (O(1) vs O(log n))
- BTreeMap: need sorted order, range queries, or keys don't implement Hash
Examples
#![allow(unused)] fn main() { use std::collections::BTreeMap; let mut map = BTreeMap::new(); map.insert(3, "three"); map.insert(1, "one"); map.insert(4, "four"); // Iteration is in sorted key order for (k, v) in map.iter() { println!("{}: {}", k, v); } // Output: // 1: one // 3: three // 4: four // First and last keys let first = map.keys().next(); // Some(&1) let last = map.keys().last(); // Some(&4) // Range queries for (k, v) in map.range(2..=4) { println!("{}: {}", k, v); } // Output: 3: three, 4: four }
True/False Questions
-
T/F: BTreeMap stores keys in sorted order.
-
T/F: Insertion into a BTreeMap is O(1).
-
T/F: BTreeMap requires keys to implement the
Hashtrait. -
T/F: Iterating over a BTreeMap yields key-value pairs in sorted key order.
-
T/F: BTreeMap is faster than HashMap for all operations.
Answers
- True - BTreeMap keys are always in sorted order
- False - BTreeMap insertion is O(log n), not O(1)
- False - BTreeMap requires Ord trait, not Hash
- True - Iteration yields pairs in sorted key order
- False - HashMap is faster (O(1) vs O(log n)) for single lookups; BTreeMap is better for range queries and ordered iteration
Predict the Output
Question:
use std::collections::BTreeMap; fn main() { let mut scores = BTreeMap::new(); scores.insert("Charlie", 85); scores.insert("Alice", 95); scores.insert("Bob", 90); let first_key = scores.keys().next().unwrap(); let last_key = scores.keys().last().unwrap(); println!("{} {}", first_key, last_key); }
Answer
Output: Alice Charlie
Reasoning:
- BTreeMap stores keys in sorted (alphabetical) order
- Sorted order: Alice, Bob, Charlie
- first key (next()): "Alice"
- last key: "Charlie"
4. VecDeque and Circular Buffers
Quick Review
VecDeque
- O(1) push/pop from both ends
- Implemented as a circular/ring buffer
- Can be used as a stack OR a queue
- Grows dynamically like Vec
Key Methods:
push_front(value)- add to frontpush_back(value)- add to backpop_front()- remove from front, returnsOption<T>pop_back()- remove from back, returnsOption<T>front()/back()- peek without removing
Use Cases:
- Queue (FIFO): push_back + pop_front
- Stack (LIFO): push_back + pop_back
- Rolling windows / circular buffers
Examples
#![allow(unused)] fn main() { use std::collections::VecDeque; let mut deque: VecDeque<i32> = VecDeque::new(); // Building a deque deque.push_back(1); // [1] deque.push_back(2); // [1, 2] deque.push_front(3); // [3, 1, 2] deque.push_back(4); // [3, 1, 2, 4] // Removing elements let front = deque.pop_front(); // Some(3), deque is [1, 2, 4] let back = deque.pop_back(); // Some(4), deque is [1, 2] // Using as a queue (FIFO) let mut queue = VecDeque::new(); queue.push_back("first"); queue.push_back("second"); let next = queue.pop_front(); // Some("first") // Iteration for val in deque.iter() { println!("{}", val); } }
True/False Questions
-
T/F: VecDeque allows efficient O(1) insertion and removal at both ends.
-
T/F: VecDeque is implemented as a circular buffer.
-
T/F: VecDeque can only store elements that implement
Copy. -
T/F:
push_front()andpush_back()are the primary insertion methods. -
T/F: VecDeque maintains elements in sorted order.
-
T/F:
VecDeque::push_front()is O(n).
Answers
- True - VecDeque provides O(1) operations at both ends
- True - VecDeque is implemented as a growable ring/circular buffer
- False - VecDeque can store any type
- True - push_front() and push_back() are the main insertion methods
- False - VecDeque maintains insertion order, not sorted order
- False - VecDeque::push_front() is O(1), that's its main advantage over Vec
Predict the Output
Question 1:
use std::collections::VecDeque; fn main() { let mut buffer: VecDeque<i32> = VecDeque::new(); buffer.push_back(1); buffer.push_back(2); buffer.push_front(3); buffer.push_back(4); buffer.pop_front(); let sum: i32 = buffer.iter().sum(); println!("{}", sum); }
Answer
Output: 7
Reasoning:
- push_back(1): [1]
- push_back(2): [1, 2]
- push_front(3): [3, 1, 2]
- push_back(4): [3, 1, 2, 4]
- pop_front() removes 3: [1, 2, 4]
- sum = 1 + 2 + 4 = 7
Question 2:
use std::collections::VecDeque; fn main() { let mut q: VecDeque<i32> = VecDeque::new(); q.push_back(10); q.push_back(20); q.push_back(30); let first = q.pop_front().unwrap(); q.push_back(first + 5); for val in q.iter() { print!("{} ", val); } println!(); }
Answer
Output: 20 30 15
Reasoning:
- Initial pushes: [10, 20, 30]
- pop_front() removes 10, first = 10
- push_back(10 + 5) adds 15: [20, 30, 15]
- Iteration prints: 20 30 15
Coding Challenge
Challenge: Implement a Rolling Average
Write a function that calculates the running (cumulative) average at each position. The running average at position i is the mean of all elements from index 0 to i.
fn running_average(values: &[f64]) -> Vec<f64> { // Your code here } fn main() { let data = vec![2.0, 4.0, 6.0, 8.0]; let result = running_average(&data); println!("{:?}", result); // Should print [2.0, 3.0, 4.0, 5.0] }
Solution
fn running_average(values: &[f64]) -> Vec<f64> { let mut result = Vec::new(); let mut sum = 0.0; for (i, &value) in values.iter().enumerate() { sum += value; let avg = sum / (i + 1) as f64; result.push(avg); } result } fn main() { let data = vec![2.0, 4.0, 6.0, 8.0]; let result = running_average(&data); println!("{:?}", result); // Should print [2.0, 3.0, 4.0, 5.0] }
5. Iterators and Iterator Chains
Quick Review
Iterator Creation:
iter()- yields&T(immutable references)iter_mut()- yields&mut T(mutable references)into_iter()- consumes collection, yields ownedT
Key Iterator Methods:
map(|x| ...)- transform each elementfilter(|x| ...)- keep elements matching predicatefold(init, |acc, x| ...)- accumulate into single valuecollect()- consume iterator into collectionsum()- sum all elementscount()- count elementstake(n)- take first n elementsskip(n)- skip first n elementsenumerate()- yields(index, value)pairs
Important: Iterator adaptors (map, filter, etc.) are lazy - they don't execute until consumed by a method like collect(), sum(), or for loop.
Examples
#![allow(unused)] fn main() { let numbers = vec![1, 2, 3, 4, 5, 6]; // Filter and map let result: Vec<i32> = numbers.iter() .filter(|&&x| x % 2 == 0) // keep even: [2, 4, 6] .map(|x| x * 2) // double: [4, 8, 12] .collect(); // Sum let sum: i32 = numbers.iter().sum(); // 21 // Filter, map, take let result: Vec<i32> = numbers.iter() .filter(|&&x| x % 2 == 1) // keep odd: [1, 3, 5] .map(|x| x * x) // square: [1, 9, 25] .take(2) // first 2: [1, 9] .collect(); // Enumerate for (i, val) in numbers.iter().enumerate() { println!("Index {}: {}", i, val); } // Fold for custom accumulation let product: i32 = numbers.iter() .fold(1, |acc, x| acc * x); // 720 }
True/False Questions
-
T/F: Iterator methods like
map()andfilter()are lazily evaluated. -
T/F: The
collect()method transforms an iterator into a collection. -
T/F: Calling
.iter()on a Vec transfers ownership of the elements. -
T/F: The
fold()method requires an initial accumulator value. -
T/F: Iterator chains are evaluated from right to left.
Answers
- True - Iterator adaptors are lazy; they don't execute until consumed
- True - collect() consumes the iterator and builds a collection
- False - iter() borrows elements (&T); into_iter() takes ownership
- True - fold() requires an initial value as its first argument
- False - Iterator chains are evaluated left to right (and lazily)
Find the Bug
Question 1:
#![allow(unused)] fn main() { fn double_evens(numbers: &[i32]) -> Vec<i32> { numbers.iter() .filter(|&x| x % 2 == 0) .map(|x| x * 2) } }
Answer
Bug: Missing .collect() at the end of the iterator chain. Iterator adaptors are lazy and return an iterator, not a Vec.
Fix:
#![allow(unused)] fn main() { fn double_evens(numbers: &[i32]) -> Vec<i32> { numbers.iter() .filter(|&x| x % 2 == 0) .map(|x| x * 2) .collect() } }
Question 2:
#![allow(unused)] fn main() { fn sum_positive(numbers: &[i32]) -> i32 { numbers.iter() .filter(|x| x > 0) .sum() } }
Answer
Bug: The filter closure receives &&i32 (reference to reference), but comparing x > 0 tries to compare a reference with an integer.
Fix:
#![allow(unused)] fn main() { fn sum_positive(numbers: &[i32]) -> i32 { numbers.iter() .filter(|&&x| x > 0) // or .filter(|x| **x > 0) .sum() } }
Predict the Output
Question 1:
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6]; let result: Vec<i32> = numbers.iter() .filter(|&x| x % 2 == 1) .map(|x| x * x) .take(2) .collect(); println!("{:?}", result); }
Answer
Output: [1, 9]
Reasoning:
- filter keeps odd numbers: [1, 3, 5]
- map squares them: [1, 9, 25]
- take(2) keeps first 2: [1, 9]
Question 2:
fn main() { let data = vec![10, 20, 30, 40, 50]; let result: i32 = data.iter() .skip(1) .take(3) .filter(|&&x| x > 25) .sum(); println!("{}", result); }
Answer
Output: 70
Reasoning:
- Original: [10, 20, 30, 40, 50]
- skip(1): [20, 30, 40, 50]
- take(3): [20, 30, 40]
- filter(x > 25): [30, 40]
- sum: 30 + 40 = 70
Question 3:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let sum: i32 = numbers.iter() .enumerate() .filter(|(i, _)| i % 2 == 0) .map(|(_, v)| v) .sum(); println!("{}", sum); }
Answer
Output: 9
Reasoning:
- enumerate gives: [(0,1), (1,2), (2,3), (3,4), (4,5)]
- filter where index % 2 == 0: [(0,1), (2,3), (4,5)]
- map extracts values: [1, 3, 5]
- sum: 1 + 3 + 5 = 9
Coding Challenge
Challenge: Count elements in range
Write a function that counts how many elements in a slice fall within a given range [low, high] (inclusive).
fn count_in_range(numbers: &[i32], low: i32, high: i32) -> usize { // Your code here - use iterator methods } fn main() { let nums = vec![1, 5, 10, 15, 20, 25]; println!("{}", count_in_range(&nums, 5, 20)); // Should print 4 }
Solution
fn count_in_range(numbers: &[i32], low: i32, high: i32) -> usize { numbers.iter() .filter(|&&x| x >= low && x <= high) .count() } fn main() { let nums = vec![1, 5, 10, 15, 20, 25]; println!("{}", count_in_range(&nums, 5, 20)); // Should print 4 }
6. Algorithm Complexity
Quick Review
Big O Notation describes how runtime grows with input size:
| Complexity | Name | Example |
|---|---|---|
| O(1) | Constant | HashMap lookup, Vec::push (amortized) |
| O(log n) | Logarithmic | BTreeMap operations, binary search |
| O(n) | Linear | Linear search, single loop |
| O(n log n) | Linearithmic | Sorting (merge sort, quicksort) |
| O(n²) | Quadratic | Nested loops, bubble sort |
Common Operations:
| Data Structure | Insert | Lookup | Delete |
|---|---|---|---|
| Vec (end) | O(1)* | O(1) | O(1) |
| Vec (middle) | O(n) | O(1) | O(n) |
| HashMap | O(1) | O(1) | O(1) |
| BTreeMap | O(log n) | O(log n) | O(log n) |
| VecDeque (ends) | O(1) | O(1) | O(1) |
*amortized
Graph Algorithms:
- BFS (Breadth-First Search): uses a queue (FIFO)
- DFS (Depth-First Search): uses a stack (LIFO)
True/False Questions
-
T/F: A
Vec::push()operation is O(1) amortized. -
T/F: Searching for a key in a HashMap is O(n) in the average case.
-
T/F: Sorting a vector with
.sort()is O(n log n). -
T/F: Graph BFS traversal uses a queue data structure.
-
T/F: Inserting into a BTreeMap is O(1).
Answers
- True - Vec::push() is amortized O(1) due to capacity doubling
- False - HashMap lookup is O(1) average, not O(n)
- True - Rust's sort uses a modified merge sort, which is O(n log n)
- True - BFS uses a queue (FIFO); DFS uses a stack (LIFO)
- False - BTreeMap insertion is O(log n), not O(1)
7. Option and Result Types
Quick Review
Option
Some(value)- contains a valueNone- no value
Result<T, E> - for operations that might fail:
Ok(value)- success with valueErr(error)- failure with error
Common Methods:
unwrap()- get value or panicunwrap_or(default)- get value or defaultunwrap_or_else(|| ...)- get value or compute default?operator - propagate errors (Result) or None (Option)is_some()/is_ok()- check variantmap(|x| ...)- transform if Some/Ok
Examples
#![allow(unused)] fn main() { // Option let maybe_value: Option<i32> = Some(5); let no_value: Option<i32> = None; let x = maybe_value.unwrap_or(0); // 5 let y = no_value.unwrap_or(0); // 0 // Result fn divide(a: i32, b: i32) -> Result<i32, String> { if b == 0 { Err(String::from("division by zero")) } else { Ok(a / b) } } let result = divide(10, 2); // Ok(5) let error = divide(10, 0); // Err("division by zero") // Using ? to propagate fn calculate(a: i32, b: i32) -> Result<i32, String> { let quotient = divide(a, b)?; // Returns Err early if divide fails Ok(quotient * 2) } }
True/False Questions
-
T/F:
Option::unwrap()will panic if the value isNone. -
T/F: The
?operator can be used to propagate errors fromResult. -
T/F:
Some(5)andNoneare both variants ofOption<i32>. -
T/F:
Result<T, E>is used for operations that might fail with an error. -
T/F:
unwrap_or(default)returns the contained value or a provided default.
Answers
- True - unwrap() panics on None
- True - The ? operator propagates Result errors
- True - Some and None are Option variants
- True - Result is for fallible operations
- True - unwrap_or() provides a default value
Final Tips for the Exam
-
HashMap vs BTreeMap: Use HashMap for fast O(1) lookups. Use BTreeMap when you need sorted keys or range queries.
-
Entry API: Always use
entry().or_insert()for counting patterns instead ofget().unwrap(). -
HashSet trait bounds: Remember that HashSet requires
Hash + Eq, not justPartialEq. -
Iterator laziness: Remember to call
.collect()or another consumer - map/filter alone don't execute! -
Reference patterns in closures:
iter()yields&Tfilter(|x| ...)receives&&Twhen used with iter()- Use
|&x|or|&&x|to destructure
-
VecDeque for both ends: Use VecDeque when you need efficient push/pop from both front and back.
-
Complexity matters: Know that HashMap is O(1), BTreeMap is O(log n), and sorting is O(n log n).
-
Understand references and when to dereference: Remember that iterators yield references, not values.
-
Review the preliminaries as well!
Good luck on your final exam! 🦀