Get a free weekly practice problem!

Keep that axe sharp.

× No thanks

You only have free questions left (including this one).

But it doesn't have to end here! Sign up for the 7-day coding interview crash course and you'll get a free Interview Cake problem every week.

You want to be able to access the largest element in a stack.

You've already implemented this Stack class:

class Stack # Initializes an empty stack. def initialize @items = [] end # Pushes a new item onto the stack. def push(item) @items << item end # Removes and returns the last item. # # If the stack is empty, returns nil. (It would also be # reasonable to throw an exception.) def pop if @items.empty? nil else @items.pop end end # Returns the last item without removing it. def peek if @items.empty? nil else @items[-1] end end end

Use your Stack class to implement a new class MaxStack with a method get_max that returns the largest element in the stack. get_max should not remove the item.

Your stacks will contain only integers.

What if we push several items in increasing numeric order (like 1, 2, 3, 4...), so that there is a new max after each push? What if we then pop each of these items off, so that there is a new max after each pop? Your algorithm shouldn't pay a steep cost in these edge cases.

You should be able to get a runtime of for push, pop, and get_max.

A just-in-time approach is to have get_max simply walk through the stack (popping all the elements off and then pushing them back on) to find the max element. This takes time for each call to get_max. But we can do better.

To get time for get_max, we could store the max integer as a member variable (call it max). But how would we keep it up to date?

For every push, we can check to see if the item being pushed is larger than the current max, assigning it as our new max if so. But what happens when we pop the current max? We could re-compute the current max by walking through our stack in time. So our worst-case runtime for pop would be . We can do better.

What if when we find a new current max (new_max), instead of overwriting the old one (old_max) we held onto it, so that once new_max was popped off our stack we would know that our max was back to old_max?

What data structure should we store our set of maxes in? We want something where the last item we put in is the first item we get out ("last in, first out").

We can store our maxes in another stack!

We define two new stacks within our MaxStack class@stack holds all of our integers, and @maxes_stack holds our "maxima." We use @maxes_stack to keep our max up to date in constant time as we push and pop:

  1. Whenever we push a new item, we check to see if it's greater than or equal to the current max, which is at the top of @maxes_stack. If it is, we also push it onto @maxes_stack.
  2. Whenever we pop, we also pop from the top of maxes_stack if the item equals the top item in @maxes_stack.
class MaxStack def initialize @stack = @maxes_stack = end # Adds a new item onto the top of our stack. # # If the item is greater than or equal to the last item # in maxes_stack, it's the new max! So we'll add it to # maxes_stack. def push(item) if !@maxes_stack.peek || item >= @maxes_stack.peek @maxes_stack.push(item) end @stack.push(item) end # Removes and returns the top item from our stack. # # If it equals the top item in maxes_stack, they must # have been pushed in together. So we'll pop it out of # maxes_stack too. def pop item = @stack.pop if item == @maxes_stack.peek @maxes_stack.pop end item end # Returns the max item of our stack. # # The last item in maxes_stack is the max item in our stack. def get_max @maxes_stack.peek end end

time for push, pop, and get_max. additional space, where m is the number of operations performed on the stack.

Our solution requires additional space for the second stack. But do we really need it?

Can you come up with a solution that requires additional space? (It's tricky!)

Notice how in the solution we're spending time on push and pop so we can save time on get_max. That's because we chose to optimize for the time cost of calls to get_max.

But we could've chosen to optimize for something else. For example, if we expected we'd be running push and pop frequently and running get_max rarely, we could have optimized for faster push and pop methods.

Sometimes the first step in algorithm design is deciding what we're optimizing for. Start by considering the expected characteristics of the input.

Reset editor

Powered by

. . .