Hard 2: Structures working together

Each problem here combines two of the structures you know, a stack with another stack, a dictionary with a queue, a heap with a counter, to do something neither could do alone. As before, the method is described in words; your job is to build it. Any correct solution passes, including a plain-loop one; the described method is what the problem is there to teach.

A queue made of two stacks

A queue serves the oldest item first; a stack gives the newest. You can build a queue out of two stacks. Call them inbox and outbox. To add an item, push it onto inbox. To take the oldest item: if outbox is empty, pop every item from inbox and push each onto outbox, which reverses their order so the oldest is now on top; then pop from outbox. If outbox already has items, just pop from it.

Write drain(operations) that starts with two empty stacks (plain lists), applies a list of operations, and returns the list of items taken out, in order. Each operation is ("add", item) or ("take",). A take on an empty queue is ignored. Do not use pop(0) or a deque: the point is the two stacks.

drain([("add", "a"), ("add", "b"), ("take",), ("add", "c"), ("take",), ("take",)])  ->  ["a", "b", "c"]
def drain(operations):
    ...

A stack that knows its minimum

A normal stack can tell you its top instantly, but not its smallest item. Keep a second stack alongside, holding the minimum so far: whenever you push a value, also push the smaller of that value and the current minimum; whenever you pop, pop both. The top of the second stack is always the minimum of what is left.

Write min_after(operations) that applies a list of operations to an empty stack and returns a list with the current minimum after each operation, or None when the stack is empty. Operations are ("push", value) and ("pop",). A pop on an empty stack is ignored.

min_after([("push", 5), ("push", 3), ("push", 7), ("pop",), ("pop",)])  ->  [5, 3, 3, 3, 5]
def min_after(operations):
    ...

Evaluate a postfix expression

In postfix notation the operator comes after its two operands: 3 4 + means 3 + 4, and 3 4 + 2 * means (3 + 4) * 2. It can be evaluated with a stack in one pass: read the tokens left to right; push every number; when you meet an operator, pop two numbers, apply the operator with the first-popped as the right-hand operand, and push the result. At the end the stack holds exactly one number, the answer.

Write evaluate(expression) for a space-separated expression using integers and the operators +, -, *. The expression is always valid.

evaluate("3 4 +")       ->  7
evaluate("3 4 + 2 *")   ->  14
evaluate("10 2 3 * -")  ->  4
def evaluate(expression):
    ...

Days until a warmer day

Given daily temperatures, find for each day how many days you have to wait until a warmer one, or 0 if none comes. One way: walk the days left to right keeping a stack of days that are still waiting for a warmer day. When a new day arrives, pop every waiting day that is colder than it; for each one popped, the answer is the distance between them. Then push the new day to wait its turn. Days left on the stack at the end get 0.

Write days_until_warmer(temps) that returns the list of waits. Store positions on the stack, not temperatures, since you need the distances.

days_until_warmer([73, 74, 75, 71, 69, 72, 76, 73])  ->  [1, 1, 4, 2, 1, 1, 0, 0]
def days_until_warmer(temps):
    ...

Largest in every window

Given a list and a window size k, find the largest value in each window of k consecutive items. A direct way is to take max of every slice. Another way uses a deque of positions: before adding a new position, drop positions from the back whose values are smaller than the new value, since they can never be the maximum while the new one is in the window; then add the new position at the back. Drop the position at the front if it has fallen out of the window. The front is always the window's maximum.

Write window_max(nums, k) that returns the list of maximums. k is at least 1 and at most the length of the list. Either method is acceptable.

window_max([1, 3, -1, -3, 5, 3, 6, 7], 3)  ->  [3, 3, 5, 5, 6, 7]
from collections import deque

def window_max(nums, k):
    ...

Top k most frequent

Find the k values that appear most often in a list. Count each value with a dictionary, then rank the (value, count) pairs: highest count first, and when counts tie, the smaller value first. sorted or heapq.nsmallest can do the ranking if you give them a key that returns (-count, value); the minus sign makes bigger counts come first.

Write most_frequent(items, k) that returns the k most frequent values as a list, most frequent first. Ties are broken by the value itself in ascending order. k never exceeds the number of distinct values.

most_frequent(["a", "b", "a", "c", "b", "a"], 2)  ->  ["a", "b"]
most_frequent([3, 1, 3, 1, 2], 2)                 ->  [1, 3]
import heapq

def most_frequent(items, k):
    ...

Merge several sorted lists

Merging two sorted lists uses two positions. For many lists, a heap keeps track of which list currently has the smallest front item. Push one entry per list: (first value, list number, 0). Repeatedly pop the smallest, add its value to the result, and if that list has a next item, push (next value, list number, position + 1). Stop when the heap is empty.

Write merge_all(lists) that returns one sorted list containing every item from every list. Lists may be empty. Do not use sort or sorted.

merge_all([[1, 4, 9], [2, 3], [], [5]])  ->  [1, 2, 3, 4, 5, 9]
import heapq

def merge_all(lists):
    ...

Merge overlapping intervals

Each delivery slot is an interval [start, end]. Overlapping or touching slots should be merged into one. Sort the slots by start. Walk through them keeping the current merged slot: if the next slot starts at or before the current one ends, extend the current end to the larger of the two ends; otherwise the current slot is finished, so record it and start a new one from the next slot.

Write merge_slots(slots) that returns the merged slots as a list of [start, end] lists, sorted by start. Do not change the input.

merge_slots([[1, 3], [2, 6], [8, 10], [15, 18]])  ->  [[1, 6], [8, 10], [15, 18]]
merge_slots([[1, 4], [4, 5]])                     ->  [[1, 5]]
def merge_slots(slots):
    ...

Moving average

A sensor sends readings one at a time, and you want the average of the last k readings after each one arrives (or of all readings so far, while there are fewer than k). A deque with maxlen=k keeps exactly the last k, dropping the oldest by itself.

Write moving_averages(readings, k) that returns a list with the average after each reading, rounded to two decimal places.

moving_averages([10, 20, 30, 40], 2)  ->  [10.0, 15.0, 25.0, 35.0]
moving_averages([3, 6, 9], 5)         ->  [3.0, 4.5, 6.0]
from collections import deque

def moving_averages(readings, k):
    ...

First non-repeating character in a stream

Characters arrive one at a time. After each arrival, report the first character seen so far that has appeared exactly once, or "#" if there is none. Keep a count of each character in a dictionary, and a queue of characters in arrival order. After each arrival, drop characters from the front of the queue while their count is more than one; the front, if any, is the answer.

Write first_unique_stream(text) that returns a string with one answer character per arrival.

first_unique_stream("aabc")  ->  "a#bb"
first_unique_stream("abab")  ->  "aab#"
from collections import deque

def first_unique_stream(text):
    ...

Next: Hard 3: Arrays and pointers.