Skip to content

Latest commit

 

History

History
372 lines (273 loc) · 10.7 KB

File metadata and controls

372 lines (273 loc) · 10.7 KB

Collections

Rust's standard library includes several useful data structures called collections. Unlike other data types that represent a single value, collections can store multiple values, and their data is stored on the heap. This allows the size of the collections to grow or shrink at runtime.

Vectors

Vectors allow you to store multiple values in a contiguous block of memory, where all the values are stored next to each other. The vector variable itself is stored on the stack, while the actual data it references is stored on the heap.

Example: Using Vectors in Rust

fn main() {
  let mut vec = Vec::new();  // Create a new empty vector
  vec.push(1);  // Add values to the vector
  vec.push(2);
  vec.push(3);
  println!("{:?}", vec);  // Print the contents of the vector
}

Explanation:

  1. We create a mutable empty vector vec using Vec::new().
  2. We use vec.push() to add values (1, 2, 3) to the vector.
  3. Finally, we print the vector using the println! macro with "{:?}" to format the output for debugging.

Q: Write a function that takes a vector as input and returns a vector containing only even values:

fn main() {
  let mut vec = Vec::new();  // Initialize a new vector
  vec.push(1);
  vec.push(2);
  vec.push(3);
  println!("{:?}", filter_even(vec));  // Call the filter_even function
}

fn filter_even(vec: Vec<i32>) -> Vec<i32> {  // Define the filter_even function
  let mut new_vec = Vec::new();
  for val in vec {
    if val % 2 == 0 {  // Check if the value is even
      new_vec.push(val);  // Add even values to new_vec
    }
  }
  return new_vec;  // Return the new vector containing only even values
}

Explanation:

  • The filter_even function takes a vector vec of integers as input.
  • We iterate over the vector, and for each element, we check if it's even using the modulus operator (val % 2 == 0).
  • If it is, we push it to a new vector new_vec, which is returned as the result.

HashMaps

HashMaps in Rust store key-value pairs, similar to objects in JavaScript, dictionaries in Python, or hashmaps in Java. They allow for efficient retrieval of values based on a key.

HashMap Methods

  1. insert: Adds a key-value pair to the HashMap.
  2. get: Retrieves a value associated with a key.
  3. remove: Removes a key-value pair.
  4. clear: Clears all the entries in the HashMap.

Example: Using HashMaps in Rust

use std::collections::HashMap;

fn main() {
  let mut users: HashMap<String, u32> = HashMap::new();  // Create a new HashMap
  users.insert(String::from("tushar"), 20);  // Insert key-value pairs
  users.insert(String::from("harkirat"), 21);

  let user1 = users.get("tushar");  // Get the value associated with the key "tushar"
  println!("{}", user1.unwrap());  // Print the value (unwrap to handle Option)
}

Explanation:

  1. We create a HashMap named users where the key is a String and the value is a u32.
  2. We insert two key-value pairs: "tushar" -> 20 and "harkirat" -> 21.
  3. We retrieve the value associated with the key "tushar" using the get method and print it.

Q: Write a function that takes a vector of tuples (each containing a key and a value) and returns a HashMap where the keys are unique, and the values are vectors of all the corresponding values for each key:

use std::collections::HashMap;

fn group_values_by_key(pairs: Vec<(String, i32)>) -> HashMap<String, Vec<i32>> {  // Update the return type
  let mut map: HashMap<String, Vec<i32>> = HashMap::new();  // Create a HashMap to store vectors
  for (key, value) in pairs {
    map.entry(key).or_insert(Vec::new()).push(value);  // Group values by key
  }
  return map;
}

fn main() {
  let pairs: Vec<(String, i32)> = vec![
    (String::from("tushar"), 20),
    (String::from("harkirat"), 21),
    (String::from("tushar"), 30),  // Example with duplicate key "tushar"
  ];
  let grouped_pairs = group_values_by_key(pairs);  // Call the function
  for (key, values) in grouped_pairs {
    println!("{}: {:?}", key, values);  // Print each key with its vector of values
  }
}

Explanation:

  • We define a function group_values_by_key that takes a vector of tuples.
  • It returns a HashMap where the keys are unique and the values are vectors containing all associated values for each key.
  • We use entry(key).or_insert() to insert a new vector if the key doesn’t exist and then push the value into the vector.

Iterators

The Iterator pattern allows you to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators, you don't have to reimplement that logic yourself.

In Rust, iterators are lazy, meaning they have no effect until you call methods that consume the iterator to use it up. For example, the code below creates an iterator over the items in the vector v by calling the iter method defined on Vec<T>. This code by itself doesn't do anything useful:

let v = vec![1, 2, 3];
let v_iter = v.iter();

The iterator is stored in the v_iter variable.


Types of Iterators:

  1. Iterating using for loops:

    fn main() {
      let nums = vec![1, 2, 3];
    
      for value in nums {
        println!("{}", value);
      }
    }

    Explanation:

    • This directly uses into_iter under the hood, which takes ownership of the vector.
    • Once the loop is done, nums can no longer be used.
  2. Iterating after creating an iterator:

    fn main() {
      let nums = vec![1, 2, 3];
      let iter = nums.iter(); // returns a struct of type Iter<_, i32>
    
      for value in iter {
        println!("{}", value);
      }
    }

    Explanation:

    • iter() creates an iterator that borrows each element immutably.
    • You can still use the original vector after this since it wasn’t consumed.
  3. IterMut

    fn main() {
      let mut v1 = vec![1, 2, 3];
      let v1_iter = v1.iter_mut(); // this will help you change the internal values of the vector
    
      for val in v1_iter {
        *val += 2;
      }
    
      println!("{:?}", v1);
    }

    Explanation:

    • iter_mut() allows mutable access to each element in the vector.
    • The values are modified in-place.
    • Since you're borrowing mutably, the original vector remains usable after the loop.
  4. Iterating using .next()

    fn main() {
      let nums = vec![1, 2, 3];
      let mut iter = nums.iter();
    
      while let Some(val) = iter.next() {
        print!("{}", val);
      }
    }

    Explanation:

    • next() manually retrieves each item from the iterator, one at a time.
    • It returns Some(value) until all items are consumed, then returns None.
    • You must make the iterator mut because next() changes its internal state.
  5. IntoIter

    fn main() {
      let nums = vec![1, 2, 3];
      let iter = nums.into_iter();
    
      for value in iter {
        println!("{}", value);
      }
    }

    Explanation:

    • into_iter() takes ownership of the collection.
    • The original vector nums is no longer usable after this.
    • Best used when you don't need the original collection again.

Useful when:

  1. You no longer need the original collection
  2. You want performance benefits by transferring ownership (avoiding references)

Which to choose:

  • iter: Use this if you want immutable references to the inner values and don’t want to transfer ownership
  • iter_mut: Use this if you want mutable references to the inner values and still want to keep the original collection
  • into_iter: Use this if you want to move the values and don’t need the original collection afterward

The two programs below are functionally the same (the for loop uses into_iter under the hood):

  1. fn main() {
      let v1 = vec![1, 2, 3];
      for val in v1 {
        println!("{}", val);
      }
    }
  2. fn main() {
      let v1 = vec![1, 2, 3];
      let iter = v1.into_iter();
    
      for val in iter {
        println!("{}", val);
      }
    }

Consuming Adapters:

Methods that call next() are called consuming adapters, because calling them uses up the iterator.

fn main() {
  let v1 = vec![1, 2, 3];
  let v1_iter = v1.iter();
  let total: i32 = v1_iter.sum(); // .sum() consumes v1_iter
  assert_eq!(total, 6);

  let sum2: i32 = v1_iter.sum(); // Error: v1_iter was already consumed
}

Explanation:

  • The sum() method consumes the iterator and returns the total.
  • Once consumed, the iterator can't be used again.
  • This ensures safe handling of ownership.

Iterator Adapters:

These are methods defined on the Iterator trait that don’t consume the iterator. Instead, they produce new iterators by modifying the original one.

  1. map

    fn main() {
      let v1: Vec<i32> = vec![1, 2, 3];
      let iter = v1.iter();
      let iter2 = iter.map(|x| x + 1);
    
      for x in iter2 {
        println!("{}", x);
      }
    }

    Explanation:

    • map() transforms each item using the closure.
    • It's lazy: nothing is executed until you use the iterator.
    • In this example, each element is incremented by 1.
  2. filter

    fn main() {
      let v1: Vec<i32> = vec![1, 2, 3];
      let iter = v1.iter();
      let iter2 = iter.filter(|x| *x % 2 == 0);
    
      for x in iter2 {
        println!("{}", x);
      }
    }

    Explanation:

    • filter() skips values that don’t satisfy the condition.
    • It's also lazy — values are only evaluated during iteration.
    • Here, only even numbers are printed.

Q: Write the logic to first filter out all odd values then double each value and create a new vector:

fn filter_and_map(v: Vec<i32>) -> Vec<i32> {
  // Create a lazy iterator:
  // 1. Filter: retain only odd values
  // 2. Map: double each retained value
  let new_iter = v
    .iter()
    .filter(|x| *x % 2 == 1) // Keep only odd numbers
    .map(|x| x * 2);         // Double each retained value

  // Collect the results from the iterator into a new vector
  let new_vec: Vec<i32> = new_iter.collect();

  return new_vec; // Return the final vector
}

fn main() {
  let v1: Vec<i32> = vec![1, 2, 3];
  let ans = filter_and_map(v1);
  println!("{:?}", ans); // Output: [2, 6]
}

Explanation:

  • The filter_and_map function takes a vector of integers as input.
  • We use an iterator to filter out all odd numbers (x % 2 == 1) and double each retained number using map.
  • Since iter() returns immutable references (&i32), operations in filter and map are done on references.
  • Finally, we collect the transformed values into a new vector using .collect() and return it.