October 31, 2025•6 min

Aplatir un Vec<Vec<T>> en Vec<T> avec des itérateurs

m
mayo

Aplatissement avec les itérateurs

La manière la plus idiomatique est d'utiliser .flatten() ou .flat_map() :

Vec<Vec<T>> → flatten() → Vec<T> [1, 2] [3] [4, 5, 6] .flatten().collect() [1, 2, 3, 4, 5, 6]
let nested = vec![vec![1, 2], vec![3], vec![4, 5, 6]];

// Méthode 1 : flatten() (pour Vec<Iterables>)
let flat: Vec<_> = nested.iter().flatten().copied().collect();

// Méthode 2 : flat_map() (pour des transformations personnalisées)
let flat: Vec<_> = nested.into_iter().flat_map(|v| v).collect();

Résultat : [1, 2, 3, 4, 5, 6]

Concaténation manuelle

Pour comparaison, voici comment tu pourrais le faire manuellement :

let mut flat = Vec::new();
for subvec in nested {
    flat.extend(subvec);  // ou append() si subvec n'est plus nécessaire
}

Comparaison des performances

Méthode Complexité temporelle Complexité spatiale Allocations Optimisations
Itérateur (flatten) O(n) O(1) itérateur 1 (résultat) Peut fusionner les itérateurs
Manuel (extend) O(n) O(1) espace temporaire 1 (résultat) Pré-allocation possible

Principales observations

Avantage de la pré-allocation (Manuel)

Tu peux pré-allouer le Vec cible si la taille totale est connue :

let total_len: usize = nested.iter().map(|v| v.len()).sum();
let mut flat = Vec::with_capacity(total_len);  // Critique pour les grands jeux de données
flat.extend(nested.into_iter().flatten());

Évaluation paresseuse des itérateurs

  • .flatten() est paresseux, mais .collect() doit quand même allouer le résultat.
  • Les itérateurs chaînés (ex: .filter().flatten()) peuvent être mieux optimisés que les boucles manuelles.

Cette paresse signifie que flatten() ne garde vivants que deux objets — l'itérateur externe et l'itérateur interne qu'il est en train de vider — et fait tout son travail dans un seul appel à next() :

Ce que fait un seul .next() sur .flatten() collect() demande un élément itérateur interne courant un élément → on le cède interne épuisé outer.next() None → flatten terminé Some(vec_interne) devient le courant Aucun Vec intermédiaire n'est créé pour la couche imbriquée — seul collect() alloue, une fois, pour le résultat.

Exemple de benchmark

let nested: Vec<Vec<i32>> = (0..1_000).map(|i| vec![i; 100]).collect();

// Approche par itérateur
let start = std::time::Instant::now();
let flat = nested.iter().flatten().copied().collect::<Vec<_>>();
println!("flatten: {:?}", start.elapsed());

// Approche manuelle avec pré-allocation
let start = std::time::Instant::now();
let total_len = nested.iter().map(|v| v.len()).sum();
let mut flat = Vec::with_capacity(total_len);
flat.extend(nested.into_iter().flatten());
println!("manual: {:?}", start.elapsed());

Résultat typique :

  • La méthode manuelle avec pré-allocation est ~10–20% plus rapide pour les grands Vec.
  • La version avec itérateur est plus concise et aussi rapide pour les petites données.

Les approches comparées

Approche Convient le mieux pour Pièges
Itérateur Lisibilité, chaînage d'opérations Légèrement plus lent sans pré-allocation
Manuel Performances maximales, grandes données Verbeux ; nécessite le calcul de la longueur

Avancé : Aplatissement sans copie

Si tu as Vec<&[T]> au lieu de Vec<Vec<T>>, utilise .flatten().copied() pour éviter le clonage :

let slices: Vec<&[i32]> = vec![&[1, 2], &[3, 4]];
let flat: Vec<i32> = slices.iter().flatten().copied().collect();

Utilise .flatten() pour :

  • Un code propre et idiomatique.
  • Le chaînage avec d'autres adaptateurs d'itérateurs (ex: .filter()).

Utilise extend manuel pour :

  • Les grands jeux de données où la pré-allocation est importante.
  • Les cas où tu connais déjà la longueur totale.

Toujours pré-allouer pour la concaténation manuelle de grandes collections !

Pour aplatir et dédupliquer en une passe, collecte dans un HashSet plutôt que dans un Vec — .flatten().collect::<HashSet<_>>() — en acceptant de perdre l'ordre.

Retour au blog
Partager ::