Skip to content

← Collections, Text and First Iterators step 16 of 21

Medium Primitives

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

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.