Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Foundations (1-15): Basic syntax, ownership, simple collections
  2. Intermediate Collections (16-30): Advanced collection usage, patterns
  3. Error Handling & I/O (31-40): Result, Option, file operations
  4. Ownership Deep Dive (41-50): Lifetimes, borrowing, Cow
  5. Smart Pointers (51-65): Box, Rc, RefCell, Arc, Weak
  6. Trait System (66-75): Traits, generics, trait objects
  7. Advanced Patterns (76-85): Custom iterators, builder pattern, typestate
  8. Concurrency (86-92): Threads, channels, sync primitives
  9. Real-World Algorithms (93-100): Graph algorithms, DP, search
  10. Capstone (101): Complete compiler + VM with event loop

How to Use This Book

  1. Read the exercise - Understand what you're building
  2. Try it yourself - Write code without looking at hints
  3. Run the tests - Use cargo test to validate your solution
  4. Peek at hints - If stuck, hints are in a separate section
  5. Compare solutions - After solving, see alternative approaches
  6. 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 = 7
  • 5 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

  1. Add support for negative numbers (e.g., "-5 3 +" = -2)
  2. Add more operators: % (modulo), ** (power)
  3. Add stack operations: dup (duplicate top), swap (swap top two)
  4. Convert infix expressions to RPN (requires parsing precedence)
  5. Make it generic over any numeric type using traits

Key Takeaways

  • &str accepts both string literals and &String references
  • Vec works perfectly as a stack with push() and pop()
  • pop() returns Option<T> - use ok_or() to convert to Result
  • Result<T, E> is Rust's way of handling errors
  • ? operator propagates errors early
  • parse::<T>() converts strings to numbers
  • match is powerful for pattern matching on strings
  • split_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

  1. Make it generic over any numeric type: sum_of_squares<T>(nums: &[T]) -> T
  2. Implement sum_of_cubes using the same pattern
  3. Create a generic sum_of_powers(nums: &[i32], power: u32) -> i32
  4. Use par_iter() from rayon for parallel computation (research exercise)

Key Takeaways

  • &[T] is a slice - a view into a sequence of elements
  • iter() creates an iterator over references to elements
  • map() transforms each element
  • sum() is a convenient method for numeric iterators
  • fold() 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

  1. Implement without allocating any String or Vec
  2. Make a case-sensitive version
  3. Create a function that returns the longest palindromic substring
  4. 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 predicate
  • flat_map() is useful when a function returns an iterator (like to_lowercase())
  • collect() gathers iterator elements into a collection
  • rev() 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 with Some and None
  • Handle edge cases (empty slices)
  • Use min() and max() 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

  1. Make the functions generic over any type that implements Ord
  2. Implement using only a for loop (no iterator methods)
  3. Find the index of the min/max value instead of the value itself
  4. Return both the value and its index: Option<(i32, usize)>
  5. Handle f64 slices (think about NaN and Infinity)

Key Takeaways

  • Option<T> represents a value that might not exist
  • Some(value) contains a value, None represents absence
  • Use Option instead of panicking or returning sentinel values (-1, null, etc.)
  • Iterator methods like min() and max() return Option
  • 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:

  1. Split input by whitespace
  2. 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
  3. 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:

  1. Vec as stack: We use Vec::push() and Vec::pop() for stack operations
  2. Result propagation: The ? operator propagates errors early
  3. Option to Result: ok_or() converts Option<T> from pop() to Result<T, E>
  4. Parse with error mapping: parse() returns Result, we use map_err() to customize the error message
  5. Order matters: When popping for operators like - and /, we pop b first, then a
  6. 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:

  1. Slices: &[i32] is a borrowed view into a sequence of integers
  2. Iterator methods: iter(), map(), and sum() chain together for clean, functional code
  3. Closures: |&x| x * x is a closure that takes a reference and squares it
  4. Pattern destructuring: |&x| destructures the reference, giving us the value directly
  5. Type inference: sum() knows to return i32 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:

  1. chars() vs bytes(): Use chars() for Unicode-aware iteration
  2. filter(): Keeps only alphanumeric characters
  3. flat_map(): to_lowercase() returns an iterator, flat_map() flattens it
  4. rev(): Reverses an iterator
  5. 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:

  1. Iterator methods: min() and max() return Option<&i32>

    • Use .copied() or .cloned() to convert to Option<i32>
  2. 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
  3. 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() and max() separately
  4. 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
  5. 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.