We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 16 of 21
windows and chunks are slice methods, not iterator adapters
Implement two functions over a list of integers.
pub fn run_lengths(data: Vec<i64>) -> Vec<(i64, usize)>
pub fn max_window_sum(data: Vec<i64>, k: usize) -> Option<i64>
run_lengths performs run-length encoding. A run is a maximal block
of adjacent equal values. Return one (value, count) pair per run, in
the order the runs appear. For example, [1, 1, 2, 2, 1] becomes
[(1, 2), (2, 2), (1, 1)]: the final 1 is not next to the first two, so
it starts a new run. An empty input gives an empty list.
max_window_sum returns the largest sum of any k consecutive
elements (a window of length k). For [1, 2, 3, 4] and k = 2 the
windows are [1, 2], [2, 3] and [3, 4], so the answer is Some(7).
Return None when no window exists: when k == 0, or when
k > data.len() (which includes every k on an empty list). When
k == data.len() there is exactly one window, the whole list. If every
value is negative, the answer is the least negative window sum, not 0.
The tests keep values small enough that no window sum, and no partial sum
while adding up a window, overflows i64.
The test harness prints runs as [[value, count], ...] and max as a
number, or null for None.
The compiler error in the starter
run_lengths is already written in the starter. max_window_sum is
written as if windows were an iterator method:
data.iter().windows(k).map(|w| w.iter().sum::<i64>()).max()
Rust 1.98 rejects it with:
error[E0599]: no method named `windows` found for struct `std::slice::Iter<'a, T>` in the current scope
|
| data.iter().windows(k).map(|w| w.iter().sum::<i64>()).max()
| ---- ^^^^^^^ method not found in `std::slice::Iter<'_, i64>`
| |
| method `windows` is available on `&[i64]`
The type in the message is the clue. .iter() turned the vector into a
std::slice::Iter, which is an iterator: a value that hands out items
one at a time through its next method. windows is not defined on
iterators. It is a method of the slice type [T], a view of elements
stored next to each other in memory. The same is true of chunks,
chunks_exact, chunk_by, split_at, sort and binary_search; none of
them is available on Iterator in stable Rust.
The fix is to remove .iter(). A Vec<T> dereferences to [T], so
data.windows(k) calls the slice method directly. Its result is an
iterator, one whose items are sub-slices &[i64], so you can call .map()
and .max() on it afterwards. Call the slice method first, then use
iterator methods on what it returns.
Why these are slice methods
An iterator only promises next: each call hands out the following item,
and the iterator does not have to keep earlier items. windows(3) over
[a, b, c, d] must yield [a, b, c] and then [b, c, d], so it needs to
see b and c a second time. A general iterator cannot provide that
without storing items it has already returned. A slice already holds every
element in memory, so windows only has to compute index ranges, and each
item it yields is a borrowed sub-slice, not a copy. sort and
binary_search need the same kind of access: sort rearranges elements in
place and binary_search jumps to the middle of the data.
(Nightly Rust has an unstable Iterator::map_windows that buffers items to
provide fixed-size windows over any iterator. Using it on Rust 1.98 stable
is error E0658.)
If you want neighbouring pairs and can iterate the data twice, for example because it is in a slice or vector, you can zip one pass with a second pass that skips the first element:
let v = [1, 2, 3, 4, 5];
v.iter().zip(v.iter().skip(1)) // (1, 2), (2, 3), (3, 4), (4, 5) as references
This works lazily and stores nothing extra. A one-shot iterator, such as lines read from standard input, cannot be iterated twice; there you would remember the previous item in a variable as you loop.
windows(0) panics
Calling windows with size 0 does not return an empty iterator. It
panics with the message window size must be non-zero (chunks(0)
panics similarly with chunk size must be non-zero). The standard library
documents these under the Panics heading of each method. This exercise
wants None for k == 0, so the check must happen before the call, and a
test covers it. For k > data.len(), windows does not panic: it yields
no windows, .max() returns None, and a separate check is optional.
chunks, chunks_exact, windows and chunk_by
These methods split a slice in different ways:
let v = [1, 2, 3, 4, 5];
v.chunks(2) // [1, 2] [3, 4] [5] last chunk may be shorter
v.chunks_exact(2) // [1, 2] [3, 4] leftover [5] is in .remainder()
v.windows(2) // [1, 2] [2, 3] [3, 4] [4, 5] overlapping, all length 2
chunks splits the slice into non-overlapping pieces, and the last piece
is shorter when the length is not a multiple of the size. chunks_exact
yields only full-size pieces and exposes the leftover elements through
.remainder(). Because every chunk has the same length, the compiler can
sometimes generate faster code for the loop body, though that is not
guaranteed. windows yields overlapping pieces, and every piece has
exactly the requested length.
chunk_by splits by content instead of by a fixed size. It calls a
predicate on each pair of neighbouring elements and starts a new chunk
wherever the predicate returns false. With equality as the predicate,
each chunk is one run, which is how the starter's run_lengths works:
data.chunk_by(|a, b| a == b) // [1, 1, 2, 2, 1] -> [1, 1] [2, 2] [1]
Every chunk is non-empty, so run[0] is always valid. chunk_by became
stable in Rust 1.77. Before that it existed only on nightly under the name
group_by, so older articles may use that name. It only groups
neighbouring elements; collecting every equal value in the whole list
into one group needs a different approach, such as a map.
Check your understanding
The windows solution does O(n·k) additions. Can you do better, and is it worth it here?
Show answer
Hide answer
For each of the roughly n window positions, w.iter().sum() adds k
numbers. A sliding window keeps a running sum instead: when the window
moves one step, subtract the element that leaves and add the element that
enters. That is O(n) additions regardless of k. With the same
k == 0 and k > data.len() checks in front, it can look like this:
let mut sum: i64 = data[..k].iter().sum();
let mut best = sum;
for (enter, leave) in data[k..].iter().zip(&data) {
sum = sum - leave + enter;
best = best.max(sum);
}
Some(best)
Here data[k..] lists the entering elements and &data lists the leaving
ones; zip stops when the shorter of the two runs out.
For this exercise the test inputs are small, so either version is fine, and
the windows version states the intent in one line. The trade-off is
readability against doing less arithmetic for large k. The sliding
version also relies on the bounds checks: without them, data[..k] would
panic when k > data.len().
Grading compiles your code, runs the tests and runs cargo clippy with
warnings treated as errors.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.