I subscribe to a lovely newsletter that, among other things, presents readers with a coding challenge every week.
I had some fun with last week’s, so I thought I’d do this again, using Rust. So here’s this week’s question:
Given an array that was once sorted in ascending order is rotated at some pivot unknown to you beforehand (so [0,2,4,7,9] might become [7,9,0,2,4], for example). Find the minimum value in that array in O(n) or less.
The way my brain works, this is what jumped at me straight away:
fn find_value_of_drop(arr: &[usize]) -> usize {
// start loop at 1 so we can always look back one space
for i in 1..arr.len() {
// compare this element to the one before, looking for a "drop" in value
if arr[i] < arr[i - 1] {
// if we found a drop, we know this element is the minimum value
return arr[i];
}
}
// If we made it here and still haven't found the drop, we know the array is
// such that the first element is the minimum value, so we'll return that
arr[0]
}
(In fact, I first wrote is as a while
loop that ended with a i = i + 1
because even though I haven’t written C++ for 20 years, it’s how I learned loops.)
The trick is that we can find the pivot by finding the first element where the previous element is greater. And once we find the pivot, we have also found the minimum value of the array.
I’m pretty sure that code above works – you can test it yourself against a few tests I wrote.
Using enumerate
After going through this whole thing I realize now that we can use a little Rust to make this a little nicer without disturbing the underlying logic by using enumerate
, which I think is similar to Ruby’s “each with index”:
fn find_value_of_drop_more_elegant(arr: &[usize]) -> usize {
for (i, item) in arr.iter().enumerate() {
if let Some(n) = arr.get(i + 1) {
if item > n {
return *n;
}
}
}
arr[0]
}
I like this solution!
But, as I managed to do last week, I wanted to see if there were other, potentially more elegant/Rust-y way to look at this problem. First, I tried cycle
, thinking it would help make the problems of (a) running out of elements in the array and (b) checking the first element a bit easier. But I don’t think cycle
really helps here.
Peeking
So I looked to peekable
/ peek
, an iterator I think I had tried to use before without success.
I think the fundamental issue for me with these higher level iterators like peek – specifically the ones that return iterators – is that the examples the documentation gives all use next()
to move through the example array slice. Here’s the peekable/peek example:
let xs = [1, 2, 3];
let mut iter = xs.iter().peekable();
// peek() lets us see into the future
assert_eq!(iter.peek(), Some(&&1));
assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
While what I almost always am in need of is a sturdy example that makes use of a loop like for
or while
. (An exception to this is the nice and concise examples for iterators that return a bool, like all
and any
, and those that return something other than an iterator, like sum
.) How often do I only want to next
a couple of times? My only guess is that there’s some general way to loop these specialized iterators.
My instincts (most likely from Ruby, but also from how we used enumerate
above) is to want to write something like for (this_element, next_element) in arr.iter().peekable() {
and then have access to this_element
and an Option of next_element
in the block (since if you’re at the end of the array slice you won’t have a next_element). But, of course, this isn’t Ruby.
After some haphazard Googling, I found a forum post (I’ve since lost track of) and adapted to this beast, which passed my tests:
fn find_value_of_drop_more_elegant(arr: &[usize]) -> usize {
let mut peekable_arr = arr.iter().peekable();
while let Some(l) = peekable_arr.next() {
match peekable_arr.peek() {
Some(next_element) => {
if l > next_element {
return **next_element;
}
}
None => return arr[0],
};
}
arr[0]
}
But, phew, at first glance I did not like it, nor did I fully understand it (I still might not).
First of all it’s longer and denser than my first solution (I thought these where supposed to be higher level iterators!). And to me it doesn’t look very approachable or readable, especially at first look, particularly that second line, which uses both a while let
and next
, a pattern I wasn’t familiar with.
And despite the borrow checker and I reaching some sort of détente about a year ago, that **next_element
causes me to raise an eyebrow – usually a sign something is off. I think it’s partially due to peek
giving you a reference to the next element (hence the Some(&&1)
in the documentation example). And my best guess is that the other *
is needed because next()
gives references as well, so by that return
line it’s like, double referenced?
An attempt at generalizing?
In order to better understand this function that I basically copied from the internet, let’s start slow. Let’s learn about that second line, the one with while let
and the dreaded next()
.
First let’s open the documentation for next()
in a browser tab and let it simmer in our minds.
After thinking about next and while let for a bit, I figured out how to write a simplified example without peek
or any other high level iterator.
let a = [10, 22, 33, 44, 55, 66];
let mut my_iter = a.iter();
// A call to next() returns the next value as a reference wrapped in
// an Option...
// assert_eq!(Some(&10), my_iter.next());
// And crucially, despite its name, the first time you call it you
// get the FIRST element.
// We should also note that it "returns None when iteration is finished"
// Since we want to loop through all elements, and we're dealing
// with Options, `while let` actually does fit our use-case pretty well!
while let Some(element) = my_iter.next() {
println!("I'm on element {}", element);
}
For me, this shows that this “while-let-next” construction is useful at a pretty low-level, in that we can use it as a way to just iterate (whether we’re doing something fancy like peek
or not). Cool!
Next, I knew that while let
was a sort of syntatic sugar for avoiding a formal match
statement and its None
branch – just like if let
, which I’ve met before.
I figured “expanding” the while let
might help me understand what’s going on better. Looking at the “Rust by Example” explanation of while let
, I was able to expand my simplified example out:
let a = [10, 22, 33, 44, 55, 66];
let mut my_iter = a.iter();
loop {
match my_iter.next() {
// if there is indeed a next element... do something
Some(element) => println!("I'm on element {}", element),
// if there's not a next element... break
_ => break, // think this can be None instead of _
}
}
OK cool, cool. Next, the big task: using what I’ve learned with the simpler example and returning to the peekable code. This is where I learned the unfortunate coincidence of the name of the next
method, mixed with this coding challenge and peek
(which gives you the next next()
element, if you get what I mean).
Here’s how I expanded and annotated that while let
:
fn find_value_of_drop_using_peek_written_out(vec: &[usize]) -> usize {
// let's set up a super neat peekable iterator
let mut peekable_vec = vec.iter().peekable();
// start an ole fashion loop!
loop {
// this is a crucial line below. We're asking if there are any elements
// left in our peekable array slice. `next()` is a bit of confusing term
// here, because later we'll use peek to look at what you might think of
// as the NEXT element.
match peekable_vec.next() {
// if there is a "next" element...
// call it this_element and see if we can peek ahead to the NEXT element
Some(this_element) => match peekable_vec.peek() {
Some(next_element) => {
// Yes, there is a NEXT element, so we can do our comparison
// because here we have access to both this_element and
// next_element
if this_element > next_element {
return **next_element;
}
}
// No, no NEXT element, so just continue with the loop
// though we could probably break or return vec[0] here...
None => continue,
},
// This means we're really at the end of the vector
// which means the first element is the minimum value
// break so we get to the last line of the function and
// return vec[0]
_ => break,
}
}
vec[0]
}
I think I’m starting to get a handle on it!?
A slightly tighter example using peek and if let
One of the great things about Mastodon is if you toot out some Rust code, someone will refactor it further for you. In this case, my above solution can get significantly tighter (and arguably more readable) by replacing the match
statement with an if let
. Behold:
fn find_value_of_drop_more_elegant(arr: &[usize]) -> usize {
let mut peekable_arr = arr.iter().peekable();
while let Some(l) = peekable_arr.next() {
if let Some(next_element) = peekable_arr.peek() {
if l > next_element {
return **next_element;
}
}
}
// If we made it here and still haven't found the drop, we know the array is
// such that the first element is the minimum value, so we'll return that
arr[0]
}
Though I think it’s fair to say it’s still quite a bit more formidable than my very first solution (shout-out to high school comp sci!).
Windows
Next to explore is the Rust method windows
. Using some of what we learned above from enumerate
and peek
, I managed to write this out:
fn find_value_of_drop_more_elegant(arr: &[usize]) -> usize {
let mut windows = arr.windows(2);
while let Some(&[this_element, next_element]) = windows.next() {
if this_element > next_element {
return next_element;
}
}
arr[0]
}
Which I think I like better than using peek, right? We get rid of the inner if let
at least.
I’m guessing there’s some situation where you could use peek
but couldn’t use windows
? Maybe if you’re not dealing with a slice? Something to explore.
Thanks to all the Mastodon/Fediverse users who guided me on this far. I’ll leave this a bit open-ended for now: How else can I solve this and learn other Rust methods/iterators? I’m wondering if there’s a one- or two-line solution possible.