Rust на LeetCode: Переход от Python к системному мышлению
Если вы зашли на PythonLib, то, скорее всего, уже знакомы с Python. Вы умеете писать лаконичные решения в одну строку и используете динамическую типизацию как данность. Но мир не стоит на месте. Всё чаще на собеседованиях в топовые компании (и не только) просят написать код на Rust. Почему? Потому что Rust — это язык, который сочетает скорость C с безопасностью памяти, а LeetCode — идеальный полигон для оттачивания этого мастерства.
В этой статье мы не просто пробежимся по синтаксису. Мы разберем, как мыслить в парадигме Rust при решении алгоритмических задач, какие структуры данных использовать, и как не утонуть в borrow checker. Поехали!
Почему LeetCode на Rust — это вызов и преимущество?
Большинство решений на LeetCode написано на Python или Java. Rust же заставляет вас думать о памяти, владении и времени жизни. Это кажется сложным, но именно это и делает вас сильнее как инженера. Решая задачи на Rust, вы автоматически изучаете:
- Систему владения (Ownership) — главную фишку языка.
- Заимствование (Borrowing) и изменяемые/неизменяемые ссылки.
- Эффективные итераторы без лишних аллокаций.
- Работу с
OptionиResultвместо исключений.
Если вы пришли из Python, первые 10 задач покажутся вам пыткой. Но когда вы поймете, как компилятор защищает вас от null pointer и гонок данных, вы влюбитесь в этот язык.
Что нужно изучить перед стартом?
Прежде чем кидаться в пул задач, давайте разберем инструментарий. Без него вы будете писать код как на C++ с макросами — неэффективно и больно.
1. Итераторы и замыкания (Closures)
В Rust итераторы — это не просто цикл for. Это мощный функциональный инструмент. Вместо того чтобы писать for i in 0..vec.len(), учитесь использовать .iter(), .map(), .filter(), .fold().
// Python: [x*2 for x in list if x > 0]
// Rust:
let result: Vec<i32> = list.iter()
.filter(|&x| *x > 0)
.map(|x| x * 2)
.collect();
Этот код не только короче, но и работает быстрее, так как итераторы ленивы и компилятор оптимизирует их в линейный цикл.
2. HashMap и BTreeMap
Для задач на подсчёт частот (например, "Two Sum" или "Group Anagrams") HashMap — ваш лучший друг. Но помните: в Rust вставка в HashMap требует владения ключом. Если вы используете &str, вам придётся клонировать строку или использовать String.
use std::collections::HashMap;
fn two_sum(nums: Vec<i32>, target: i32) -> Vec<i32> {
let mut map = HashMap::new();
for (i, &num) in nums.iter().enumerate() {
let complement = target - num;
if let Some(&j) = map.get(&complement) {
return vec![j as i32, i as i32];
}
map.insert(num, i);
}
vec![]
}
Обратите внимание: enumerate() возвращает кортеж (usize, &i32). Мы используем &num, чтобы разыменовать ссылку.
3. Vec и слайсы
Vec — это динамический массив. Но в задачах LeetCode часто нужно работать с подмассивами. Для этого используйте слайсы &[T]. Они не требуют копирования и позволяют передавать часть массива в функцию.
fn max_subarray_sum(arr: &[i32]) -> i32 {
let mut max_ending = arr[0];
let mut max_so_far = arr[0];
for &x in arr[1..].iter() {
max_ending = std::cmp::max(x, max_ending + x);
max_so_far = std::cmp::max(max_so_far, max_ending);
}
max_so_far
}
Топ-5 типичных ошибок новичков в Rust на LeetCode
Даже опытные Python-разработчики совершают одни и те же промахи. Давайте разберем их, чтобы вы не наступали на те же грабли.
Ошибка 1: Игнорирование владения при возврате из функции
Вы написали функцию, которая принимает &str и возвращает &str. Компилятор ругается на время жизни. Решение: либо возвращайте String, либо используйте аннотации времени жизни 'a.
// Неправильно (если строки разные):
fn longest(x: &str, y: &str) -> &str { // Ошибка!
if x.len() > y.len() { x } else { y }
}
// Правильно:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Ошибка 2: Паника при доступе к Option
В Python вы привыкли к dict.get(key), который возвращает None. В Rust HashMap::get() возвращает Option<&V>. Не используйте .unwrap() бездумно — это приведёт к панике на пустом значении. Используйте if let или match.
if let Some(value) = map.get(&key) {
// работаем с value
} else {
// ключа нет
}
Ошибка 3: Изменяемое и неизменяемое заимствование одновременно
Классика: вы проходите по вектору в цикле и пытаетесь его модифицировать. Компилятор скажет: "Нельзя". Используйте iter_mut() или индексы.
let mut vec = vec![1, 2, 3];
for i in 0..vec.len() {
vec[i] += 1; // Так можно
}
// for x in &vec { vec.push(1); } // Так нельзя!
Ошибка 4: Сложности с матрицами (Vec<Vec<i32>>)
Двумерные массивы в Rust — это боль. Обращение по индексу matrix[i][j] работает, но если вы хотите изменить строку, вам нужно получить &mut Vec<i32>.
fn rotate(matrix: &mut Vec<Vec<i32>>) {
let n = matrix.len();
for i in 0..n {
for j in i..n {
let temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix