Introduction
Welcome to Rust 101 - a comprehensive collection of 101 exercises designed to take you from Rust beginner to confident Rustacean.
What Makes This Different?
Unlike micro-exercises like Rustlings, these exercises are:
- Substantial: Each exercise builds something real and satisfying
- Progressive: Concepts build naturally on each other
- Comprehensive: Forces you to use every Rust feature in context
- Practical: Skills transfer directly to real-world Rust programming
Structure
The 101 exercises are organized into 10 phases:
- Foundations (1-15): Basic syntax, ownership, simple collections
- Intermediate Collections (16-30): Advanced collection usage, patterns
- Error Handling & I/O (31-40): Result, Option, file operations
- Ownership Deep Dive (41-50): Lifetimes, borrowing, Cow
- Smart Pointers (51-65): Box, Rc, RefCell, Arc, Weak
- Trait System (66-75): Traits, generics, trait objects
- Advanced Patterns (76-85): Custom iterators, builder pattern, typestate
- Concurrency (86-92): Threads, channels, sync primitives
- Real-World Algorithms (93-100): Graph algorithms, DP, search
- Capstone (101): Complete compiler + VM with event loop
How to Use This Book
- Read the exercise - Understand what you're building
- Try it yourself - Write code without looking at hints
- Run the tests - Use
cargo test
to validate your solution - Peek at hints - If stuck, hints are in a separate section
- Compare solutions - After solving, see alternative approaches
- Do follow-ups - Extend the exercise with additional challenges
Prerequisites
- Basic programming experience (any language)
- Rust installed (
rustup
recommended) - Familiarity with command line
- A code editor (VS Code, RustRover, etc.)
Philosophy
These exercises teach Rust by:
- Forcing natural usage: You'll reach for
Rc<RefCell<T>>
because you need it, not because you memorized when to use it - Building intuition: Patterns emerge from solving real problems
- Embracing difficulty: Ownership errors teach better than explanations
- Celebrating progress: Each solved exercise is a real achievement
Ready to start? Head to Exercise 1: RPN Calculator!
Exercise 1: RPN Calculator
Difficulty: Beginner Concepts: String vs &str, Vec as stack, Result, pattern matching, parsing
Problem Statement
Implement a simple Reverse Polish Notation (RPN) calculator that evaluates arithmetic expressions.
In RPN, operators come after operands:
3 4 +
means 3 + 4 = 75 1 2 + 4 * + 3 -
means (5 + ((1 + 2) * 4)) - 3 = 14
Requirements:
- Support operators:
+
,-
,*
,/
- Integer arithmetic only
- Division rounds toward zero (truncates)
- All tokens must be separated by whitespace
- Return
Result<i32, String>
for success or error - Handle errors: invalid tokens, stack underflow, leftover values
Optional (no tests provided, but try it!):
- Support negative numbers (e.g.,
-5 3 +
= -2)
Learning Goals
- Understand
String
vs&str
- Use
Vec
as a stack (push/pop) - Return
Result
for error handling - Parse strings to numbers
- Pattern matching with
match
- Split strings and iterate over tokens
Examples
#![allow(unused)] fn main() { assert_eq!(rpn_calc("3 4 +"), Ok(7)); assert_eq!(rpn_calc("5 1 2 + 4 * + 3 -"), Ok(14)); assert_eq!(rpn_calc("10 3 /"), Ok(3)); // integer division assert_eq!(rpn_calc(""), Err("Empty expression".to_string())); assert!(rpn_calc("3 0 /").is_err()); // division by zero assert!(rpn_calc("1 2 3 +").is_err()); // too many operands }
Starter Code
#![allow(unused)] fn main() { pub fn rpn_calc(expr: &str) -> Result<i32, String> { // TODO: implement this function todo!() } #[cfg(test)] mod tests { use super::*; #[test] fn test_addition() { assert_eq!(rpn_calc("3 4 +"), Ok(7)); } #[test] fn test_subtraction() { assert_eq!(rpn_calc("10 3 -"), Ok(7)); } #[test] fn test_multiplication() { assert_eq!(rpn_calc("3 4 *"), Ok(12)); } #[test] fn test_division() { assert_eq!(rpn_calc("10 3 /"), Ok(3)); assert_eq!(rpn_calc("7 2 /"), Ok(3)); } #[test] fn test_complex() { // 5 + ((1 + 2) * 4) - 3 = 14 assert_eq!(rpn_calc("5 1 2 + 4 * + 3 -"), Ok(14)); } #[test] fn test_single_number() { assert_eq!(rpn_calc("42"), Ok(42)); } #[test] fn test_empty() { assert!(rpn_calc("").is_err()); } #[test] fn test_division_by_zero() { assert!(rpn_calc("3 0 /").is_err()); } #[test] fn test_too_many_operands() { assert!(rpn_calc("1 2 3 +").is_err()); } #[test] fn test_insufficient_operands() { assert!(rpn_calc("1 +").is_err()); } #[test] fn test_invalid_token() { assert!(rpn_calc("1 2 xyz").is_err()); } } }
Follow-up Challenges
- Add support for negative numbers (e.g., "-5 3 +" = -2)
- Add more operators:
%
(modulo),**
(power) - Add stack operations:
dup
(duplicate top),swap
(swap top two) - Convert infix expressions to RPN (requires parsing precedence)
- Make it generic over any numeric type using traits
Key Takeaways
&str
accepts both string literals and&String
referencesVec
works perfectly as a stack withpush()
andpop()
pop()
returnsOption<T>
- useok_or()
to convert toResult
Result<T, E>
is Rust's way of handling errors?
operator propagates errors earlyparse::<T>()
converts strings to numbersmatch
is powerful for pattern matching on stringssplit_whitespace()
handles any amount of whitespace- Order matters when popping from stack for non-commutative operators
Need help? View Hints | View Solution
Exercise 2: Sum of Squares
Difficulty: Beginner Concepts: Vec basics, iteration, fold, closures
Problem Statement
Write a function sum_of_squares
that takes a slice of integers and returns the sum of their squares.
For example:
[1, 2, 3]
→ 1² + 2² + 3² = 1 + 4 + 9 = 14[2, 4]
→ 2² + 4² = 4 + 16 = 20
Learning Goals
- Work with slices (
&[T]
) - Iterate over collections
- Use
fold
for accumulation - Practice with closures
- Understand iterator methods
Examples
#![allow(unused)] fn main() { assert_eq!(sum_of_squares(&[1, 2, 3]), 14); assert_eq!(sum_of_squares(&[2, 4]), 20); assert_eq!(sum_of_squares(&[]), 0); }
Starter Code
#![allow(unused)] fn main() { pub fn sum_of_squares(nums: &[i32]) -> i32 { // TODO: implement this function todo!() } #[cfg(test)] mod tests { use super::*; #[test] fn test_basic() { assert_eq!(sum_of_squares(&[1, 2, 3]), 14); } #[test] fn test_two_elements() { assert_eq!(sum_of_squares(&[2, 4]), 20); } #[test] fn test_empty() { assert_eq!(sum_of_squares(&[]), 0); } #[test] fn test_single() { assert_eq!(sum_of_squares(&[5]), 25); } #[test] fn test_negative() { assert_eq!(sum_of_squares(&[-3, 4]), 25); } #[test] fn test_zeros() { assert_eq!(sum_of_squares(&[0, 0, 0]), 0); } } }
Follow-up Challenges
- Make it generic over any numeric type:
sum_of_squares<T>(nums: &[T]) -> T
- Implement
sum_of_cubes
using the same pattern - Create a generic
sum_of_powers(nums: &[i32], power: u32) -> i32
- Use
par_iter()
from rayon for parallel computation (research exercise)
Key Takeaways
&[T]
is a slice - a view into a sequence of elementsiter()
creates an iterator over references to elementsmap()
transforms each elementsum()
is a convenient method for numeric iteratorsfold()
is the general accumulation pattern- Closures
|x| ...
are anonymous functions - Pattern
|&x|
destructures references in closure parameters
Need help? View Hints | View Solution
Exercise 3: Palindrome Checker
Difficulty: Beginner Concepts: String iteration, chars vs bytes, reversal, borrowing
Problem Statement
Write a function is_palindrome
that checks if a string reads the same forwards and backwards.
Ignore case and non-alphanumeric characters. For example:
"racecar"
→ true"A man, a plan, a canal: Panama"
→ true"hello"
→ false
Learning Goals
- Iterate over characters with
.chars()
- Understand the difference between chars and bytes
- Filter and transform characters
- Compare strings/iterators
- Handle Unicode properly
Examples
#![allow(unused)] fn main() { assert_eq!(is_palindrome("racecar"), true); assert_eq!(is_palindrome("hello"), false); assert_eq!(is_palindrome("A man, a plan, a canal: Panama"), true); assert_eq!(is_palindrome(""), true); }
Starter Code
#![allow(unused)] fn main() { pub fn is_palindrome(s: &str) -> bool { // TODO: implement this function todo!() } #[cfg(test)] mod tests { use super::*; #[test] fn test_simple_palindrome() { assert_eq!(is_palindrome("racecar"), true); } #[test] fn test_not_palindrome() { assert_eq!(is_palindrome("hello"), false); } #[test] fn test_with_spaces_and_punctuation() { assert_eq!(is_palindrome("A man, a plan, a canal: Panama"), true); } #[test] fn test_empty() { assert_eq!(is_palindrome(""), true); } #[test] fn test_single_char() { assert_eq!(is_palindrome("a"), true); } #[test] fn test_mixed_case() { assert_eq!(is_palindrome("RaceCar"), true); } #[test] fn test_numbers() { assert_eq!(is_palindrome("12321"), true); assert_eq!(is_palindrome("12345"), false); } } }
Follow-up Challenges
- Implement without allocating any String or Vec
- Make a case-sensitive version
- Create a function that returns the longest palindromic substring
- Handle grapheme clusters (combined Unicode characters) correctly using the
unicode-segmentation
crate
Key Takeaways
chars()
iterates over Unicode scalar values (characters)bytes()
iterates over raw bytes (not Unicode-aware)filter()
keeps elements matching a predicateflat_map()
is useful when a function returns an iterator (liketo_lowercase()
)collect()
gathers iterator elements into a collectionrev()
reverses an iterator- Always consider Unicode when working with text
Need help? View Hints | View Solution
Exercise 4: Find Min/Max
Difficulty: Beginner Concepts: Option, pattern matching, slice iteration, multiple return values
Exercise Statement
Write two functions that find the minimum and maximum values in a slice of integers.
Key learning points:
- Return
Option<i32>
to handle empty slices - Use pattern matching to handle the
Option
type - Understand when to use
Option
vs panicking - Practice with slice iteration
Learning Goals
- Understand
Option<T>
and when to use it - Pattern match on
Option
withSome
andNone
- Handle edge cases (empty slices)
- Use
min()
andmax()
methods on iterators - Return multiple values using tuples
Examples
#![allow(unused)] fn main() { assert_eq!(find_min(&[3, 1, 4, 1, 5]), Some(1)); assert_eq!(find_max(&[3, 1, 4, 1, 5]), Some(5)); assert_eq!(find_min(&[]), None); assert_eq!(find_max(&[]), None); // Bonus: find both at once assert_eq!(find_min_max(&[3, 1, 4, 1, 5]), Some((1, 5))); assert_eq!(find_min_max(&[]), None); }
Starter Code
#![allow(unused)] fn main() { pub fn find_min(nums: &[i32]) -> Option<i32> { // TODO: implement this function todo!() } pub fn find_max(nums: &[i32]) -> Option<i32> { // TODO: implement this function todo!() } // Bonus: return both min and max in a single pass pub fn find_min_max(nums: &[i32]) -> Option<(i32, i32)> { // TODO: implement this function todo!() } #[cfg(test)] mod tests { use super::*; #[test] fn test_find_min_basic() { assert_eq!(find_min(&[3, 1, 4, 1, 5]), Some(1)); } #[test] fn test_find_min_single() { assert_eq!(find_min(&[42]), Some(42)); } #[test] fn test_find_min_empty() { assert_eq!(find_min(&[]), None); } #[test] fn test_find_min_negative() { assert_eq!(find_min(&[-5, -1, -10, -3]), Some(-10)); } #[test] fn test_find_max_basic() { assert_eq!(find_max(&[3, 1, 4, 1, 5]), Some(5)); } #[test] fn test_find_max_single() { assert_eq!(find_max(&[42]), Some(42)); } #[test] fn test_find_max_empty() { assert_eq!(find_max(&[]), None); } #[test] fn test_find_max_negative() { assert_eq!(find_max(&[-5, -1, -10, -3]), Some(-1)); } #[test] fn test_find_min_max_basic() { assert_eq!(find_min_max(&[3, 1, 4, 1, 5]), Some((1, 5))); } #[test] fn test_find_min_max_single() { assert_eq!(find_min_max(&[42]), Some((42, 42))); } #[test] fn test_find_min_max_empty() { assert_eq!(find_min_max(&[]), None); } #[test] fn test_find_min_max_all_same() { assert_eq!(find_min_max(&[7, 7, 7, 7]), Some((7, 7))); } } }
Follow-up Challenges
- Make the functions generic over any type that implements
Ord
- Implement using only a
for
loop (no iterator methods) - Find the index of the min/max value instead of the value itself
- Return both the value and its index:
Option<(i32, usize)>
- Handle
f64
slices (think aboutNaN
andInfinity
)
Key Takeaways
Option<T>
represents a value that might not existSome(value)
contains a value,None
represents absence- Use
Option
instead of panicking or returning sentinel values (-1, null, etc.) - Iterator methods like
min()
andmax()
returnOption
- Tuples
(T, U)
are useful for returning multiple values - Pattern matching is the idiomatic way to unwrap
Option
values - Empty slices are a natural use case for
Option::None
Need help? View Hints | View Solution
Exercise 5: Word Counter
Exercise 6: Unique Elements
Exercise 7: Fizzbuzz
Exercise 8: Reverse String
Exercise 9: Caesar Cipher
Exercise 10: Vector Rotate
Exercise 11: Leap Year
Exercise 12: GCD
Exercise 13: Prime Checker
Exercise 14: Fibonacci Iterator
Exercise 15: Anagram Groups
Problem 16-30: Coming Soon
Problem 31-40: Coming Soon
Problem 41-50: Coming Soon
Problem 51-65: Coming Soon
Problem 66-75: Coming Soon
Problem 76-85: Coming Soon
Problem 86-92: Coming Soon
Problem 93-100: Coming Soon
Problem 101: Compiler + VM
Exercise 1: RPN Calculator - Hints
Hint 1: Algorithm overview
RPN uses a stack:
- Split input by whitespace
- For each token:
- If it's a number, push it onto the stack
- If it's an operator, pop two operands, apply operator, push result
- At the end, stack should have exactly one value
#![allow(unused)] fn main() { let mut stack: Vec<i32> = Vec::new(); }
Hint 2: Splitting the input
Use split_whitespace()
to tokenize:
#![allow(unused)] fn main() { for token in expr.split_whitespace() { // process token } }
Hint 3: Parsing numbers
Use parse()
with error handling:
#![allow(unused)] fn main() { if let Ok(num) = token.parse::<i32>() { stack.push(num); } }
Hint 4: Handling operators
Match on the operator and pop two values:
#![allow(unused)] fn main() { match token { "+" => { let b = stack.pop().ok_or("Stack underflow")?; let a = stack.pop().ok_or("Stack underflow")?; stack.push(a + b); } // ... other operators } }
Note: Order matters! For a - b
, you pop b
first, then a
.
Hint 5: Final validation
After processing all tokens, check the stack:
#![allow(unused)] fn main() { if stack.len() == 1 { Ok(stack[0]) } else if stack.is_empty() { Err("Empty expression".to_string()) } else { Err("Too many operands".to_string()) } }
Exercise 2: Sum of Squares - Hints
Hint 1: Using a for loop
The simplest approach uses a for loop:
#![allow(unused)] fn main() { let mut sum = 0; for &num in nums { sum += num * num; } sum }
Hint 2: Using iterator methods
You can use map
to square each element, then sum
to add them:
#![allow(unused)] fn main() { nums.iter().map(|&x| x * x).sum() }
Hint 3: Using fold
fold
is a general accumulation method:
#![allow(unused)] fn main() { nums.iter().fold(0, |acc, &x| acc + x * x) }
The first argument is the initial value, the closure takes (accumulator, element).
Exercise 3: Palindrome Checker - Hints
Hint 1: Cleaning the string
First, filter to keep only alphanumeric characters and convert to lowercase:
#![allow(unused)] fn main() { let cleaned: String = s.chars() .filter(|c| c.is_alphanumeric()) .flat_map(|c| c.to_lowercase()) .collect(); }
Hint 2: Comparing forward and backward
Once cleaned, compare the string with its reverse:
#![allow(unused)] fn main() { let reversed: String = cleaned.chars().rev().collect(); cleaned == reversed }
Hint 3: More efficient approach
For a solution with single allocation:
#![allow(unused)] fn main() { let chars: Vec<char> = s.chars() .filter(|c| c.is_alphanumeric()) .flat_map(|c| c.to_lowercase()) .collect(); chars.iter().eq(chars.iter().rev()) }
Hint 4: Why chars() not bytes()?
Use chars()
instead of bytes()
to handle Unicode correctly:
"café".bytes()
→ 5 bytes (é is multi-byte)"café".chars()
→ 4 characters
Exercise 4: Find Min/Max - Hints
Hint 1: Using iterator methods
The simplest approach uses iterator methods:
#![allow(unused)] fn main() { pub fn find_min(nums: &[i32]) -> Option<i32> { nums.iter().min().copied() } }
The min()
method returns Option<&i32>
, so use .copied()
to convert to Option<i32>
.
Hint 2: Handling empty slices
Iterator methods like min()
and max()
automatically return None
for empty iterators:
#![allow(unused)] fn main() { let empty: &[i32] = &[]; assert_eq!(empty.iter().min(), None); }
Hint 3: Manual implementation with fold
You can use fold
to find min/max manually:
#![allow(unused)] fn main() { pub fn find_min(nums: &[i32]) -> Option<i32> { if nums.is_empty() { return None; } Some(nums.iter().fold(nums[0], |min, &x| { if x < min { x } else { min } })) } }
Hint 4: Finding both min and max
For find_min_max
, you need to track both values in a single pass:
#![allow(unused)] fn main() { pub fn find_min_max(nums: &[i32]) -> Option<(i32, i32)> { if nums.is_empty() { return None; } let first = nums[0]; let (min, max) = nums.iter().fold((first, first), |(min, max), &x| { (min.min(x), max.max(x)) }); Some((min, max)) } }
Hint 5: Pattern matching on Option
When using the result, pattern match to handle both cases:
#![allow(unused)] fn main() { match find_min(&numbers) { Some(min) => println!("Minimum: {}", min), None => println!("Empty slice"), } }
Exercise 1: RPN Calculator - Solution
Main Solution
#![allow(unused)] fn main() { pub fn rpn_calc(expr: &str) -> Result<i32, String> { let mut stack: Vec<i32> = Vec::new(); for token in expr.split_whitespace() { match token { "+" => { let b = stack.pop().ok_or("Stack underflow")?; let a = stack.pop().ok_or("Stack underflow")?; stack.push(a + b); } "-" => { let b = stack.pop().ok_or("Stack underflow")?; let a = stack.pop().ok_or("Stack underflow")?; stack.push(a - b); } "*" => { let b = stack.pop().ok_or("Stack underflow")?; let a = stack.pop().ok_or("Stack underflow")?; stack.push(a * b); } "/" => { let b = stack.pop().ok_or("Stack underflow")?; let a = stack.pop().ok_or("Stack underflow")?; if b == 0 { return Err("Division by zero".to_string()); } stack.push(a / b); } _ => { // Try to parse as number let num = token .parse::<i32>() .map_err(|_| format!("Invalid token: {}", token))?; stack.push(num); } } } // Should have exactly one value left if stack.len() == 1 { Ok(stack[0]) } else if stack.is_empty() { Err("Empty expression".to_string()) } else { Err(format!("Too many operands: {} left on stack", stack.len())) } } }
Alternative Solution: With Helper Function
#![allow(unused)] fn main() { pub fn rpn_calc(expr: &str) -> Result<i32, String> { let mut stack: Vec<i32> = Vec::new(); fn apply_op(stack: &mut Vec<i32>, op: fn(i32, i32) -> Result<i32, String>) -> Result<(), String> { let b = stack.pop().ok_or("Stack underflow")?; let a = stack.pop().ok_or("Stack underflow")?; stack.push(op(a, b)?); Ok(()) } for token in expr.split_whitespace() { match token { "+" => apply_op(&mut stack, |a, b| Ok(a + b))?, "-" => apply_op(&mut stack, |a, b| Ok(a - b))?, "*" => apply_op(&mut stack, |a, b| Ok(a * b))?, "/" => apply_op(&mut stack, |a, b| { if b == 0 { Err("Division by zero".to_string()) } else { Ok(a / b) } })?, _ => stack.push(token.parse().map_err(|_| format!("Invalid token: {}", token))?), } } match stack.len() { 0 => Err("Empty expression".to_string()), 1 => Ok(stack[0]), _ => Err(format!("Too many operands: {} left on stack", stack.len())), } } }
Explanation
Key points:
- Vec as stack: We use
Vec::push()
andVec::pop()
for stack operations - Result propagation: The
?
operator propagates errors early - Option to Result:
ok_or()
convertsOption<T>
frompop()
toResult<T, E>
- Parse with error mapping:
parse()
returnsResult
, we usemap_err()
to customize the error message - Order matters: When popping for operators like
-
and/
, we popb
first, thena
- Final validation: We ensure exactly one value remains on the stack
Common mistakes to avoid:
- Forgetting to check for division by zero
- Wrong order when popping operands (leads to reversed subtraction/division)
- Not validating final stack size
- Not handling empty input
Exercise 2: Sum of Squares - Solution
Idiomatic Solution (using map + sum)
#![allow(unused)] fn main() { pub fn sum_of_squares(nums: &[i32]) -> i32 { nums.iter().map(|&x| x * x).sum() } }
Alternative: Using fold
#![allow(unused)] fn main() { pub fn sum_of_squares(nums: &[i32]) -> i32 { nums.iter().fold(0, |acc, &x| acc + x * x) } }
Alternative: Using for loop (more explicit)
#![allow(unused)] fn main() { pub fn sum_of_squares(nums: &[i32]) -> i32 { let mut sum = 0; for &num in nums { sum += num * num; } sum } }
Explanation
Key points:
- Slices:
&[i32]
is a borrowed view into a sequence of integers - Iterator methods:
iter()
,map()
, andsum()
chain together for clean, functional code - Closures:
|&x| x * x
is a closure that takes a reference and squares it - Pattern destructuring:
|&x|
destructures the reference, giving us the value directly - Type inference:
sum()
knows to returni32
based on the context
Why the idiomatic solution is preferred:
- Concise and readable
- No mutable state
- Composable with other iterator methods
- Compiler can optimize well
- Handles empty slices automatically (returns 0)
Exercise 3: Palindrome Checker - Solution
Simple Solution (with allocation)
#![allow(unused)] fn main() { pub fn is_palindrome(s: &str) -> bool { let cleaned: String = s.chars() .filter(|c| c.is_alphanumeric()) .flat_map(|c| c.to_lowercase()) .collect(); let reversed: String = cleaned.chars().rev().collect(); cleaned == reversed } }
Optimized (single allocation)
#![allow(unused)] fn main() { pub fn is_palindrome(s: &str) -> bool { let chars: Vec<char> = s.chars() .filter(|c| c.is_alphanumeric()) .flat_map(|c| c.to_lowercase()) .collect(); chars.iter().eq(chars.iter().rev()) } }
Alternative: Two-pointer approach
#![allow(unused)] fn main() { pub fn is_palindrome(s: &str) -> bool { let chars: Vec<char> = s.chars() .filter(|c| c.is_alphanumeric()) .flat_map(|c| c.to_lowercase()) .collect(); let (mut left, mut right) = (0, chars.len().saturating_sub(1)); while left < right { if chars[left] != chars[right] { return false; } left += 1; right -= 1; } true } }
Explanation
Key points:
- chars() vs bytes(): Use
chars()
for Unicode-aware iteration - filter(): Keeps only alphanumeric characters
- flat_map():
to_lowercase()
returns an iterator,flat_map()
flattens it - rev(): Reverses an iterator
- eq(): Compares two iterators element by element
Why flat_map() instead of map()?
to_lowercase()
returns an iterator because some characters expand when lowercased (e.g., German ß → ss)flat_map()
flattens the nested iterators into a single stream
Performance considerations:
- First solution: 2 allocations (cleaned + reversed)
- Second solution: 1 allocation (chars Vec)
- Third solution: 1 allocation but manual iteration
The second solution is usually the best balance of clarity and performance.
Exercise 4: Find Min/Max - Solution
Idiomatic Solution (using iterator methods)
#![allow(unused)] fn main() { pub fn find_min(nums: &[i32]) -> Option<i32> { nums.iter().min().copied() } pub fn find_max(nums: &[i32]) -> Option<i32> { nums.iter().max().copied() } pub fn find_min_max(nums: &[i32]) -> Option<(i32, i32)> { if nums.is_empty() { return None; } let min = nums.iter().min().copied().unwrap(); let max = nums.iter().max().copied().unwrap(); Some((min, max)) } }
Alternative: Single-pass min_max
#![allow(unused)] fn main() { pub fn find_min_max(nums: &[i32]) -> Option<(i32, i32)> { if nums.is_empty() { return None; } let first = nums[0]; let (min, max) = nums.iter().skip(1).fold((first, first), |(min, max), &x| { (min.min(x), max.max(x)) }); Some((min, max)) } }
Alternative: Using fold
#![allow(unused)] fn main() { pub fn find_min(nums: &[i32]) -> Option<i32> { nums.first().map(|&first| { nums.iter().skip(1).fold(first, |min, &x| min.min(x)) }) } pub fn find_max(nums: &[i32]) -> Option<i32> { nums.first().map(|&first| { nums.iter().skip(1).fold(first, |max, &x| max.max(x)) }) } }
Alternative: Manual loop
#![allow(unused)] fn main() { pub fn find_min(nums: &[i32]) -> Option<i32> { if nums.is_empty() { return None; } let mut min = nums[0]; for &num in &nums[1..] { if num < min { min = num; } } Some(min) } pub fn find_max(nums: &[i32]) -> Option<i32> { if nums.is_empty() { return None; } let mut max = nums[0]; for &num in &nums[1..] { if num > max { max = num; } } Some(max) } pub fn find_min_max(nums: &[i32]) -> Option<(i32, i32)> { if nums.is_empty() { return None; } let mut min = nums[0]; let mut max = nums[0]; for &num in &nums[1..] { if num < min { min = num; } if num > max { max = num; } } Some((min, max)) } }
Explanation
Key points:
-
Iterator methods:
min()
andmax()
returnOption<&i32>
- Use
.copied()
or.cloned()
to convert toOption<i32>
- Use
-
Why Option?: Empty slices have no min/max value
- Better than panicking or returning sentinel values like
-1
- Forces the caller to handle the empty case
- Better than panicking or returning sentinel values like
-
Single-pass algorithm: For
find_min_max
, we can find both in one iteration- Start with
first
as both min and max - Update both as we iterate
- More efficient than calling
min()
andmax()
separately
- Start with
-
Using .first(): Returns
Option<&i32>
for the first element- Safe way to check if slice is non-empty
- Can chain with
.map()
for elegant Option handling
-
Pattern matching: Not shown in solutions, but usage would be:
#![allow(unused)] fn main() { if let Some(min) = find_min(&nums) { println!("Minimum: {}", min); } }
Performance note:
The idiomatic find_min_max
solution makes two passes (one for min, one for max). The single-pass version with fold
is more efficient but slightly less clear.