Hard 3: Lists and positions
These problems move one or two positions through a list in a disciplined way: from both ends inward, a reader ahead of a writer, or a window that grows on the right and shrinks on the left. (Programmers often call such a position a pointer; here it just means an index you move.) Several ask you to change the list in place rather than build a new one, and the checks confirm that the list you were given ends up changed. Any correct solution passes; the described method is what each problem teaches.
Two sum on a sorted list
When the list is sorted, two positions can find a pair that sums to a target without checking every pair. Put one position at the start and one at the end. If the two values add up to the target, you are done. If the sum is too small, only moving the left position rightwards can make it bigger; if too large, only moving the right position leftwards can make it smaller. Repeat until the positions meet.
Write pair_sum(sorted_nums, target) that returns the two positions as a
tuple (left, right), or None if no pair works. Values are distinct.
pair_sum([1, 3, 4, 6, 8], 10) -> (2, 3)
pair_sum([1, 2], 10) -> None
def pair_sum(sorted_nums, target):
...
Remove duplicates in place
A sorted list has duplicates next to each other. Remove them without
making a new list, using two positions: a reader that visits every
item, and a writer that marks where the next kept item goes. Whenever
the reader sees a value different from the last kept one, copy it to the
writer's position and advance the writer. At the end, the first writer
items are the unique values.
Write dedupe(sorted_nums) that rearranges the list so its first part
holds the unique values in order, and returns how many there are. The
point is to practise the reader-and-writer method rather than build a new
list or use a set.
nums = [1, 1, 2, 3, 3, 3]
dedupe(nums) -> 3 and nums now starts [1, 2, 3, ...]
def dedupe(sorted_nums):
...
Move the zeros to the end
Rearrange a list so every zero is at the end and the other values keep their order, without making a new list. Use a writer position: walk the list with a reader, and every time the reader finds a non-zero value, put it at the writer's position and advance the writer. When the reader is done, fill from the writer to the end with zeros.
Write move_zeros(nums) that modifies the list in place and returns it.
move_zeros([0, 1, 0, 3, 12]) -> [1, 3, 12, 0, 0]
def move_zeros(nums):
...
Squares of a sorted list
A sorted list may contain negatives, so squaring the values does not keep
them sorted: [-3, 1, 2] squares to [9, 1, 4]. The largest square is
always at one of the two ends of the original. Use a position at each end,
compare their squares, write the larger into the last free slot of the
result, and move that position inward. Fill the result from the back.
Write sorted_squares(nums) that returns a new sorted list of the
squares. The point is to practise filling from both ends rather than call
sort or sorted.
sorted_squares([-4, -1, 0, 3, 10]) -> [0, 1, 9, 16, 100]
def sorted_squares(nums):
...
Merge into the first list
a is a sorted list that has extra zeros at the end: exactly enough room
for all of b, another sorted list. Merge b into a so that a ends
up sorted, without making a new list. Filling from the front would
overwrite values of a not yet placed, so fill from the back: compare the
last real value of a with the last value of b, put the larger in the
last slot, and move inward.
Write merge_into(a, m, b) where m is how many real values a has at
the start, so len(a) is always m + len(b). Modify a in place and
return it.
merge_into([1, 3, 5, 0, 0], 3, [2, 4]) -> [1, 2, 3, 4, 5]
def merge_into(a, m, b):
...
Rotate a list by k
Rotating [1, 2, 3, 4, 5] right by 2 gives [4, 5, 1, 2, 3]. It can be
done in place with three reversals: reverse the whole list, then reverse
the first k items, then reverse the rest. On the example, the list goes
[5, 4, 3, 2, 1], then [4, 5, 3, 2, 1], then [4, 5, 1, 2, 3].
Write rotate(nums, k) that rotates the list right by k in place and
returns it. k may be larger than the list; rotating by the length brings
it back to the start, so only k % len(nums) matters. The point is to
practise the three reversals rather than build a new list with slicing.
rotate([1, 2, 3, 4, 5], 2) -> [4, 5, 1, 2, 3]
rotate([1, 2, 3], 4) -> [3, 1, 2]
def rotate(nums, k):
...
Range sums with a prefix table
Many questions of the form "what is the total of items i to j" can be
answered from one precomputed table. Build prefix where prefix[0] is 0
and prefix[i] is the sum of the first i items. Then the sum of items
from position i up to and including j is prefix[j + 1] - prefix[i].
Write range_sums(nums, queries) where each query is a tuple (i, j) with
0 <= i <= j < len(nums). Return the list of answers. The point is to
build the prefix table once rather than re-add the items for every query.
range_sums([2, 4, 6, 8], [(0, 1), (1, 3), (2, 2)]) -> [6, 18, 6]
def range_sums(nums, queries):
...
Best run of days
Daily profits can be negative. Find the largest total over any run of
consecutive days (at least one day). One method: walk the days keeping a
running total of the current run. For each day, in this order: add the
day to the running total; if the running total is now the best seen so
far, remember it; then, if the running total is below zero, reset it to
zero, because a negative run can only hurt whatever comes next. Because a
run must have at least one day, start best at the first day's profit,
not at zero, so that an all-negative list still gives the least bad day.
Write best_run(profits) for a non-empty list.
best_run([-2, 1, -3, 4, -1, 2, 1, -5, 4]) -> 6 (4 - 1 + 2 + 1)
best_run([-3, -1, -2]) -> -1
def best_run(profits):
...
Longest stretch without repeats
Find the length of the longest stretch of a string with no repeated
characters. Keep a window [left, right] and a set of the characters in
it. Move right one step at a time; if the new character is already in
the set, move left rightwards, removing characters from the set, until
it is not. The window is then valid again; record its length.
Write longest_unique(text).
longest_unique("abcabcbb") -> 3
longest_unique("bbbbb") -> 1
longest_unique("") -> 0
def longest_unique(text):
...
Most water between two walls
heights lists the heights of vertical walls at positions 0, 1, 2, and so
on. Pick two walls; the water they could hold between them is the distance
between them times the shorter wall's height. Find the most water any pair
can hold. Start with the two outermost walls. Moving inward makes the pair
narrower, so a narrower pair can only hold more if its shorter wall is
taller than the current shorter wall. Keeping the current shorter wall can
never help, so move that wall's position inward and try again. Stop when
the positions meet.
Write most_water(heights) for a list with at least two walls.
most_water([1, 8, 6, 2, 5, 4, 8, 3, 7]) -> 49
most_water([1, 1]) -> 1
def most_water(heights):
...
That is the end of the course. If you solved all 120 without help, you can write Python. Go back to any you skipped, and then build something of your own.