We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 11 of 21
Set algebra with BTreeSet
Compare two tag lists four ways, and find the first repeat in a third.
pub fn compare_tags(a: Vec<String>, b: Vec<String>)
-> (Vec<String>, Vec<String>, Vec<String>, Vec<String>)
pub fn first_duplicate(items: Vec<String>) -> Option<String>
compare_tags returns (both, only_a, only_b, either) — intersection,
a-minus-b, b-minus-a, union — each sorted ascending and free of
duplicates. Duplicates inside an input list are just duplicates; ["m","m"]
is the same set as ["m"].
first_duplicate returns the first item that has appeared before, or None.
Set operations return iterators, not sets
This is the design decision worth internalising, and it is why the starter does not compile:
error[E0277]: a value of type `Vec<String>` cannot be built from an iterator
over elements of type `&String`
sa.intersection(&sb) does not build a set. It returns a lazy iterator
that borrows both sets and yields &String as you consume it. Nothing is
allocated until you ask for something concrete.
Two consequences follow:
-
sa.intersection(&sb).count()never allocates at all. Neither does.any(|t| t.starts_with("x")), or.take(3). You pay only for what you take. If the operation returned aBTreeSet, every one of those would build and throw away a whole collection. -
Because the items are borrowed from sets you do not own, you cannot
collect()them intoVec<String>..cloned()(or.copied()forCopytypes) inserts the allocation exactly where it belongs — visible, at the point where you decided you wanted ownership.
That second point is the same borrowed-versus-owned distinction from the
ownership track, and it will keep arriving as E0277 for the rest of your
Rust life. The message even tells you the shape of the fix: it has an
iterator of &String and needs an iterator of String.
The four operations, and the one that is not symmetric
sa.intersection(&sb) // in both
sa.union(&sb) // in either
sa.difference(&sb) // in a, not in b
sa.symmetric_difference(&sb) // in exactly one
difference is directional. sa.difference(&sb) and
sb.difference(&sa) are different answers, and one of the test cases has
both non-empty specifically so that swapping them is caught. intersection
and union are symmetric; symmetric_difference is the union of the two
differences, which is what its name is telling you.
There are also three predicates that answer a question without building
anything: is_subset, is_superset, is_disjoint. a.is_disjoint(&b) is
clearer and faster than a.intersection(&b).next().is_none(), because it
can stop at the first hit and it says what you meant.
insert returns a bool, and it is the cleanest dedup idiom in the language
pub fn insert(&mut self, value: T) -> bool // true if the value was NEW
So “have I seen this before?” and “remember that I have seen it” are the same
operation, one lookup, no contains beforehand:
let mut seen = BTreeSet::new();
items.into_iter().find(|item| !seen.insert(item.clone()))
Read it as: keep inserting until an insert reports “not new”, and that item
is the first duplicate. The contains-then-insert version does two lookups
and has a race-shaped gap in it if the set is ever shared. HashMap::insert
has the same idea with more information — it returns Option<V>, the value
that was displaced.
::: question Why does first_duplicate need item.clone()?
Because find‘s closure receives &String — it is only looking at the
item, since it might have to hand it back to you as the return value — while
insert needs an owned String to store. Exactly the same asymmetry as
HashMap::get versus HashMap::entry, for exactly the same reason: a
collection can only store what it owns.
You could avoid the clone by storing &str keys borrowed from a slice you
keep alive — first_duplicate(items: &[String]) -> Option<&str> needs no
allocation at all. The signature is the thing that decided this, not the
algorithm. That is a theme worth watching for.
:::
Why BTreeSet and not HashSet here
Both would answer the question. BTreeSet is specified here because the
output must be sorted, and a BTreeSet is already in order — iterating
it gives you the sorted answer with no sort step. With a HashSet you get
the right elements in a random order (see the HashMap-order problem in this
track) and have to sort anyway.
The rule of thumb: HashSet when you only ask “is it in there?”, BTreeSet
when order, ranges, or reproducible output matter. And for very small
collections, do not be too proud to use a Vec and contains — it beats
both for a handful of elements, because there is no hashing and no pointer
chasing. Just know that it goes quadratic when the collection grows, which is
a later item in this track with a measurement attached.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.