
LeetCode: Stack
Stack Intro
- Stack Application: Tracking Nested or Hierarchical Structures
- Stack Application: Backtracking by Tracking History or State
- Stack Application: Monotonic Property Maintenance
- Stack Application: Simulating Recursion or Call Stacks
- Stack Application: Expression Evaluation and Parsing
- Stack Application: Dynamic Programming State Compression
- Stack Application: Interval and Range Processing
739. Daily Temperatures ::3:: - Medium
- Intro
- Abstract
- Pseudocode
- Solution 1: [Monotonic] Monotonic Decreasing Stack of Cold Temps - Stack/Monotonic Property Maintenance
- Solution 2: [Monotonic] Reverse Iteration Monotonic Decreasing Stack of Warm Temps [SC Opt] - Stack/Monotonic Property Maintenance
- Solution 3: [Dynamic Programming] Reverse Iteration With Jump Traversal Using Dynamic Programming Building Future Warm Temperatures List [TC Opt] - Stack/Algorithm
84. Largest Rectangle in Histogram ::2:: - Hard
- Intro
- Abstract
- Pseudocode
- Solution 1: [Monotonic] Imaginary Boundaries Surrounding Rectangles Game By Monotonic Stack Covering Heights Rule Implication - Stack/Monotonic Property Maintenance
- Solution 2: [Monotonic] Reverse Iteration Index Math Imaginary Boundaries Surrounding Rectangles Game By Monotonic Stack Covering Heights Rule Implication - Stack/Monotonic Property Maintenance
Stack Intro
LeetCode problems with elegant solutions using stacks.
Stack Application: Tracking Nested or Hierarchical Structures
We can track structure while iterating over an object ensuring it maintains some criteria
Ex: Validate if a string containing brackets ()[] is properly balanced:
def balancedParentheses(s: str) -> bool:
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in pairs.values():
stack.append(char)
elif char in pairs:
if not stack or stack.pop() != pairs[char]:
return False
return not stackStack Application: Backtracking by Tracking History or State
We can use stacks in backtracking to store the state of exploration. When a branch reaches a dead end or a solution, we pop the state to return to the previous state and continue exploring other branches.
Ex: Subset Sum with Backtracking
def subset_sum(nums, target):
stack = [(0, [], 0)] # (index, current_subset, current_sum)
result = []
while stack:
index, current_subset, current_sum = stack.pop()
if current_sum > target: # Prune invalid paths
continue
if current_sum == target: # Valid solution
result.append(list(current_subset))
continue
# Push new states for further exploration
for i in range(index, len(nums)):
stack.append((i + 1, current_subset + [nums[i]], current_sum + nums[i]))
return result
# subset_sum([2, 3, 6, 7], 7) = [[7]]Stack Application: Monotonic Property Maintenance
A stack can maintain a monotonic property (increasing or decreasing) over a sequence while processing elements, ensuring efficient lookups or modifications.
Ex: Find the Next Greater Element
def nextGreaterElement(nums):
stack = [] # Stores indices of elements in decreasing order
result = [-1] * len(nums) # Initialize result with -1
for i in range(len(nums)):
while stack and nums[i] > nums[stack[-1]]:
idx = stack.pop()
result[idx] = nums[i] # Found the next greater element
stack.append(i)
return result
# Example: nextGreaterElement([2, 1, 2, 4, 3]) -> [4, 2, 4, -1, -1]Stack Application: Simulating Recursion or Call Stacks
We can use a stack to emulate recursion by explicitly managing the call stack.
Ex: Traverse a binary tree in preorder (root -> left -> right):
def preorderTraversal(root):
if not root:
return []
stack = [root] # Start with the root node
result = []
while stack:
node = stack.pop() # Simulate recursion by processing the top of the stack
if node:
result.append(node.val) # Visit the node
# Push right child first so the left child is processed next
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
# Example: For a tree with root → 1, left → 2, right → 3, preorderTraversal(root) -> [1, 2, 3]Stack Application: Expression Evaluation and Parsing
We can use a stack to evaluate or parse expressions by storing operands and incrementally applying operators. This approach is well-suited for postfix and prefix notations.
Ex: Post and Prefix
def evaluatePostfix(expression):
stack = [] # To hold operands during evaluation
for token in expression.split():
if token.isdigit(): # If it's an operand, push it to the stack
stack.append(int(token))
else: # If it's an operator, pop two operands and apply the operator
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(a - b)
elif token == '*':
stack.append(a * b)
elif token == '/': # Assuming integer division
stack.append(a // b)
return stack.pop() # Final result is the only item left in the stack
# Example:
# Input: "3 4 + 2 * 1 +"
# Output: 15 (Equivalent to (3 + 4) * 2 + 1)Stack Application: Dynamic Programming State Compression
We can use a stack to compress the necessary state while scanning through data, especially when enforcing a specific constraint or invariant like monotonicity. Instead of storing the entire history, we prune irrelevant elements from the stack to keep only the most useful summary of the past
Ex: Given an array, partition it into the minimum number of strictly increasing subsequences
def min_partitions(nums):
stacks = [] # Each element represents the last number in a subsequence
for num in nums:
placed = False
for i in range(len(stacks)):
# If we can append to subsequence i
if stacks[i] < num:
stacks[i] = num
placed = True
break
if not placed:
# Start a new subsequence (partition)
stacks.append(num)
return len(stacks)
# Example usage:
nums = [1, 3, 2, 4, 6, 5]
print(min_partitions(nums)) # Output: 2Stack Application: Interval and Range Processing
We can use stacks to efficiently process intervals or ranges, such as merging overlapping intervals, calculating spans, or finding next/previous smaller or larger elements within a range.
Ex: Largest Rectangle in Histogram
def largestRectangleArea(heights):
stack = [] # stores indices of bars
max_area = 0
for i, h in enumerate(heights + [0]): # Add sentinel to flush stack
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
left = stack[-1] if stack else -1
width = i - left - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
# Example:
# Input: [2, 1, 5, 6, 2, 3]
# Output: 10 (largest rectangle is formed by heights 5 and 6)20. Valid Parentheses ::2:: - Easy
Topics: String, Stack
Intro
Given a string s containing: ( ) [ ] { }, determine if the input string is valid. An input string is valid if: 1. Open brackets must be closed by the same type of brackets 2. Open brackets must be closed in the correct order. 3. Every close bracket has a corresponding open bracket of the same type.
| Input | Output |
|---|---|
| "()" | true |
| "()" | false |
| "(]" | true |
| "([])" | true |
Constraints:
1 ≤ s.length ≤ 104
s consists of parentheses only '()[]'
Abstract
Use a stack to track open parentheses. We we encounter a closed parenthesis make sure there is a matching open one.
Pseudocode
Sol 1: Manual Condition Stack Check
1. stack = []
2. mapping = {closeChar: openChar}
3. for each c in s:
a. if c is closing:
if stack empty: return False
topElem = stack.pop()
if mapping[c] != topElem: return False
b. if c is opening:
stack.append(c)
4. return stack is emptySolution 1: [Stack] Manual Condition Stack Check [TC Opt] - Stack/Tracking Nested or Hierarchical Structures
def isValid(self, s: str) -> bool:
# sc: O(n)
stack = []
# Open close paren matching map
# sc: O(1)
mapping = {
')' : '(',
']' : '[',
'}' : '{'
}
# tc: O(n)
for c in s:
# Found Closed:
# need to match with open
# tc: O(1)
if c in ')]}':
# Empty Check:
# there is no open paren to match the current closed paren, invalid stack
# tc: O(1)
if not stack:
return False
# Check:
# the closed type should match the paren currently at the top of stack
# tc: O(1)
topElem = stack.pop()
if mapping[c] != topElem:
return False
# Matched open/closed:
# we have found an open and closed paren,
# append the open for the next pair for future matching
# tc: O(1)
if c in '([{':
stack.append(c)
# Finished iterating over string,
# validate that stack is empty and all paren have been matched
isStackEmpty = not stack
# overall: tc O(n)
# overall: sc O(n)
return isStackEmpty150. Evaluate Reverse Polish Notation ::1:: - Medium
Topics: Array, Stack, Math, Design
Intro
You are given an array of strings tokens that represents an arithmetic expression in a Reverse Polish Notation. Evaluate the expression. Return an integer that represents the value of the expression. Note that: The valid operators are '+', '-', '*', and '/'. Each operand may be an integer or another expression. The division between two integers always truncates toward zero. There will not be any division by zero. The input represents a valid arithmetic expression in a reverse polish notation. The answer and all the intermediate calculations can be represented in a 32-bit integer.
| Input | Output |
|---|---|
| ["2","1","+","3","*"] | 9 |
| ["4","13","5","/","+"] | 6 |
| ["10","6","9","3","+","-11","","/","","17","+","5","+"] | 22 |
Constraints:
1 ≤ tokens.length ≤ 104
tokens[i] is either an operator: "+", "-", "*", or "/", or an integer in the range [-200, 200].
Abstract
We're designing abstract syntax tree that when execute, will execute the given operations in reverse polish notation.
Pseudocode
Sol 1: Stack Postfix Expression Evaluation Algorithm
1. stack = []
2. for each token in tokens:
a. if token is number:
stack.append(int(token))
b. else (operator):
b = stack.pop()
a = stack.pop()
stack.append(a OP b)
3. return stack.pop()Solution 1: [Stack] Stack Postfix Expression Evaluation Algorithm [TC Opt] - Stack/Expression Evaluation and Parsing
def evalRPN(self, tokens: List[str]) -> int:
# Reverse Polish Notation:
# We only push numbers to the stack,
# and when we hit an operator we pop b the a
# since the numbers are on the stack in reverse
# input: ["4","13","5","/","+"]
# expected: 4 + (13 / 5) = 6
# "4" -> [4] push() 4 to stack
# "13" -> [4, 13] push() 13 to stack
# "5" -> [4, 13, 5] push() 5 to stack
# "/" -> [4] hit operator, pop() b = 5, pop() a = 13, complete operation int(13 / 5) = 2
# "2" -> [4, 2] push() 2 to stack
# "+" -> [] hit operator, pop() b = 2, pop() a = 4, finish operator (4 + 2) = 6
# "6" -> [6] push() 6 to stack
# [6] stack holds answer
# sc: stack holds up to n/2 intermediate results O(n)
stack = []
# tc: O(n)
for token in tokens:
# Found Integer:
# Cast to int and push() to stack
if token not in "+-*/":
stack.append(int(token))
# Found Operation:
# Pop() 2 numbers from stack, b then a, and compute
else:
# tc: pop operation constant O(1)
b = stack.pop()
a = stack.pop()
# Complete Operation:
# Push() result to stack
match token:
case "+":
stack.append(a + b)
case "-":
stack.append(a - b)
case "*":
stack.append(a * b)
case "/":
# a / b
# 13 / 5
# Explicit truncation towards zero
# -7 / 3 # -2.333 division: Remainder x
# -7 // 3 # -3 floor division: Rounds down "towards infinity" x
# int(-7 / 3) # -2 int(division): Rounds up "towards 0", as required by RPN this one
stack.append(int(a / b))
# Top of stack holds result
res = stack[0]
# overall: tc O(n)
# overall: sc O(n)
return res232. Implement Queue using Stacks ::1:: - Easy
Topics: Stack, Design, Queue
Intro
Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (push, peek, pop, and empty). Implement the MyQueue class: void push(int x) Pushes element x to the back of the queue. int pop() Removes the element from the front of the queue and returns it. int peek() Returns the element at the front of the queue. boolean empty() Returns true if the queue is empty, false otherwise. Notes: You must use only standard operations of a stack, which means only push to top, peek/pop from top, size, and is empty operations are valid. Depending on your language, the stack may not be supported natively. You may simulate a stack using a list or deque (double-ended queue) as long as you use only a stack's standard operations. Follow-up: Can you implement the queue such that each operation is amortized O(1) time complexity? In other words, performing n operations will take overall O(n) time even if one of those operations may take longer.
| Input | Output |
|---|---|
| ["MyQueue", "push", "push", "peek", "pop", "empty"] [[], [1], [2], [], [], []] | [null, null, null, 1, 1, false] |
Constraints:
1 ≤ x ≤ 9
At most 100 calls will be made to push, pop, peek, and empty.
All the calls to pop and peek are valid.
Abstract
something!
Pseudocode
Sol 1: Lazy Transfer In Stack Out Stack
1. def __init__(self):
a. (self.inStack = [])
b. (self.outStack = [])
2. _transfer():
a. if not self.outStack:
while self.inStack:
self.outStack.append(self.inStack.pop())
3. push(x):
a. self.inStack.append(x)
4. pop():
a. self._transfer()
b. return self.outStack.pop()
5. peek():
a. self._transfer()
b. return self.outStack[-1]
6. empty():
a. return not self.inStack and not self.outStackSolution 1: [Stack] Lazy Transfer From In LIFO Stack To Out FIFO Stack [TC Opt] - Stack/Design Queue Using Stack
class MyQueue:
def __init__(self):
# Two Stacks (In LIFO / Out FIFO):
# - In stack holds elements in LIFO order, stored here until transferred
# - Out stack holds elements in FIFO order, which are passed by switching from LIFO to FIFO
# Lazy Transfer:
# - Only transfer when out FIFO stack is empty and we need some element
# - Transfer all elements current in LIFO In Stack to FIFO Out Stack
# - All elem moved In to Out at most once, transfer is O(1) amortized across calls
# In stack:
# - LIFO which wait to be transferred
# sc: O(n)
self.inStack = []
# Out stack:
# - FIFO order via reversing via grabbing from the top of the LIFO stack
# sc: O(n)
self.outStack = []
# Transfer:
# tc: O(n)
def _transfer(self) -> None:
# Ran out of FIFO elements
if not self.outStack:
# Transfer entire LIFO element to FIFO stack
while self.inStack:
# Grab and push
elem = self.inStack.pop()
self.outStack.append(elem)
# Push:
# tc: O(1)
def push(self, x: int) -> None:
# Push to LIFO stack, will eventually be transferred
self.inStack.append(x)
# Pop:
# tc: O(1) amortized,
# tc: occasional O(n) transfer across many future O(1) calls
def pop(self) -> int:
# Check if LIFO is empty, and transfer if necessary
self._transfer()
# LIFO guaranteed to have at least 1 element
return self.outStack.pop()
# Peek:
# tc: O(1) amortized
# tc: occasional O(n) transfer across many future O(1) calls
def peek(self) -> int:
# Check if LIFO is empty, and transfer if necessary
self._transfer()
# LIFO guaranteed to have at least 1 element
return self.outStack[-1]
# Empty:
# tc: O(1)
def empty(self) -> bool:
# Both LIFO and FIFO are empty
return not self.inStack and not self.outStack
# overall: tc O(1) amortized, O(n)
# overall: sc O(n)155. Min Stack ::2:: - Medium
Topics: Stack, Design
Intro
Design a stack that supports push, pop, top, and retrieving the minimum element in constant time. Implement the MinStack class: MinStack() initializes the stack object. void push(int val) pushes the element val onto the stack. void pop() removes the element on the top of the stack. int top() gets the top element of the stack. int getMin() retrieves the minimum element in the stack. You must implement a solution with O(1) time complexity for each function.
| Input | Output |
|---|---|
| ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]] | [null,null,null,null,-3,null,0,-2] |
Constraints:
-231 ≤ val ≤ 231 - 1
Methods pop, top and getMin operations will always be called on non-empty stacks.
At most 3 * 104 calls will be made to push, pop, top, and getMin.
Abstract
We need to design a stack that runs in O(1) time complexity for each main function.
Pseudocode
Sol 1: Tuple Stack Manual Size Pointer To Avoid Inbuilt Pop()
1. self.stack = [], self.size = 0
_overwriteCheck():
1. return self.size < len(self.stack)
push(val):
1. if self.size > 0:
currMin = min(val, self.stack[self.size-1][1])
2. else:
currMin = val
3. if self._overwriteCheck():
self.stack[self.size] = (val, currMin)
4. else:
self.stack.append((val, currMin))
5. self.size += 1
pop():
1. self.size -= 1
top():
1. return self.stack[self.size-1][0]
getMin():
1. return self.stack[self.size-1][1]
Sol 2: Stack Using Inbuilt Pop() and Push()
1. self.stack = []
push(val):
1. if self.stack:
a. currMin = min(self.stack[-1][1], val)
2. else:
a. currMin = val
3. self.stack.append((val, currMin))
pop():
1. self.stack.pop()
top():
1. return self.stack[-1][0]
getMin():
1. return self.stack[-1][1]Solution 1: [Stack] Tuple Stack Manual Size Pointer To Avoid Inbuilt Pop() - Stack/Dynamic Programming State Compression
class MinStack:
# MinStack:
# Track 2 things
# - Top Of Stack
# - Min val
# Push(), Pop(), Top(), GetMin():
# - all in O(1)
# - may need to update the above 2 details:
# Stack:
# Tuple Representing (Top Of Stack, Min At Level):
# (7, 1) 5
# (4, 1) 4
# (1, 1) 3
# (7, 5) 2
# (5, 5) 1
# Stack Level
def __init__(self):
# Tuple Stack: (top of stack, min up to this level):
# sc: O(n)
self.stack = []
self.size = 0
# Helper:
# If size pointer is behind actual length of stack, we overwrite a value when we push
# If size pointer is accurate to length of stack, we simply append to the end of the stack
def _overwriteCheck(self):
return self.size < len(self.stack)
# Push():
def push(self, val: int):
# If some min exists, compare
# else new value automatically update to new value
if self.size > 0:
currMin = min(val, self.stack[self.size-1][1])
else:
currMin = val
# Overwrite, pointer is not at actual top of stack
if self._overwriteCheck():
self.stack[self.size] = (val, currMin)
# No overwrite, append to end of stack
else:
self.stack.append((val, currMin))
# Adjust pointer to new top of stack
self.size += 1
# Pop():
def pop(self):
# Adjust pointer to new top of stack,
# allows us to pop without using the inbuilt pop(),
# however, this now requires us to use an overwrite in push()
# since actual stack memory size may differ from the pointer length
self.size -= 1
# Top():
def top(self):
# Grab val at top of stack
return self.stack[self.size - 1][0]
# GetMin():
def getMin(self):
# Grab min at current level
return self.stack[self.size - 1][1]
# overall: tc O(1)
# overall: sc O(n)Solution 2: [Stack] Stack Using Inbuilt Pop() and Push() - Stack/Dynamic Programming State Compression
class MinStack:
# MinStack:
# Track 2 things
# - Top Of Stack
# - Min val
# Push(), Pop(), Top(), GetMin():
# - all in O(1)
# - may need to update the above 2 details:
# Stack:
# Tuple Representing (Top Of Stack, Min At Level):
# (7, 1) 5
# (4, 1) 4
# (1, 1) 3
# (7, 5) 2
# (5, 5) 1
# Stack Level
def __init__(self):
# Tuple Stack: (top of stack, min up to this level):
# sc: O(n)
self.stack = []
# Push():
def push(self, val: int):
# Compare new val to curr stack min
if self.stack:
currMin = min(self.stack[-1][1], val)
else:
currMin = val
self.stack.append((val, currMin))
# Pop():
def pop(self):
# Remove item at top of stack,
# new min automatically updates to new top
self.stack.pop()
# Top():
def top(self):
# View top of stack val
return self.stack[-1][0]
# GetMin():
def getMin(self):
# View top of stack min
return self.stack[-1][1]735. Asteroid Collision ::1:: - Medium
Topics: Array, Stack, Simulation
Intro
We are given an array asteroids of integers representing asteroids in a row. The indices of the asteroid in the array represent their relative position in space. For each asteroid, the absolute value represents its size, and the sign represents its direction (positive meaning right, negative meaning left). Each asteroid moves at the same speed. Find out the state of the asteroids after all collisions. If two asteroids meet, the smaller one will explode. If both are the same size, both will explode. Two asteroids moving in the same direction will never meet.
| Input | Output |
|---|---|
| asteroids = [5,10,-5] | [2,-1,2] |
| nums = [1,2,3,4,3] | [2,3,4,-1,4] |
Constraints:
1 ≤ nums.length ≤ 10^4
10^9 ≤ nums[i] ≤ 10^9
Abstract
Smashing stones but with a directional component. Each stone caries a weight Positives move to the right, negative to the left. Every stone moves at the same speed, 1 tick per interval, So two right movers (+ +) will never meet. And two left movers (- -) will never meet. A right mover in front of a left mover (- +) will never meet. Only a front mover behind a left mover (+ -) will meet. One left mover can smash multiple right movers if its big enough. Use a stack to hold surviving right movers. When a left-mover shows up, keep colliding it against the top of the stack, smaller one explodes, equal sizes mutually explode, and bigger one survives and absorbs the rest.
Pseudocode
Sol 1: Collision Resolution With Alive Flag
1. self.stack = []
2. for curr in asteroids:
a. alive = True
b. while alive and curr < 0 and stack and 0 < stack[-1]:
if stack[-1] < -curr:
stack.pop()
continue
elif -curr < stack[-1]:
alive = False
else:
stack.pop()
alive = False
if alive:
c. stack.append(curr)
3. survivors = stack
3. return survivorsSolution 1: [Monotonic Stack] Collision Resolution With Alive Flag - Stack/Simulation
def asteroidCollision(self, asteroids: List[int]) -> List[int]:
# Collision Rule:
# - Two stones only hit if a right mover behind a left mover (+ -)
# - Same direction asteroids never meet (they move at same speed) (+ +) or (- -)
# - A left mover behind a right mover will never hit (- +).
# Tracking Survivors:
# - Everything else gets pushed to stack to track surviving asteroids
# - Only collide if a right mover hits a left mover (+ -)
# - so we only collide if we find a new left mover
# while the top of the stack holds a right mover
# Stack:
# - stores both left and right movers
# - uses tracking survivors + collision rules to determine what to pop/push
# Stack Progression Example: [-3, 10, -5]
# - (-3) survives (no collisions)
# - 10 survives (no collisions)
# - (-5) destroyed (collides with 10)
# - stack = [-3, 10]
# sc: O(n)
stack = []
# tc: O(n)
for curr in asteroids:
# Alive:
# - is curr alive
alive = True
# Right Movers:
# - pushed immediately to the stack
# - can never collide with anything already on the stack
# Left Movers:
# - if stack is empty, push to stack
# - if top of stack is also a left mover, push to stack (can't hit reach each other)
# - check if top stack is a right mover (collision will occur)
# - if beats top of stack, continue to compare to top of stack
# Collision If:
# - curr is alive left mover (alive & negative: curr < 0)
# - top of stack is right mover, (positive: 0 < stack[-1])
# - then collide, either left or right survives or none
# tc: O(n)
while alive and curr < 0 and stack and 0 < stack[-1]:
# Left mover is bigger than right:
# - pop the destroyed right
# - keep compare curr against new top
if stack[-1] < -curr:
stack.pop()
continue
# Right mover is bigger than left:
# - alive flag to false for destroyed left
# - iterate to next curr
elif -curr < stack[-1]:
alive = False
# Neither survives:
# - pop the destroyed right
# - alive flag to false for destroyed left
else:
stack.pop()
alive = False
# Check for survivor:
# - no collision, always alive: (- +), (+ +), or (- -)
# - left mover destroyed all right movers on top of stack
if alive:
stack.append(curr)
# Stack holds any left or right movers that survived
survivors = stack
# overall: tc O(n)
# overall: sc O(n)
return survivors1475. Final Prices With a Special Discount in a Shop ::1:: - Easy
Topics: Array, Stack, Monotonic Stack
Intro
You are given an integer array prices where prices[i] is the price of the ith item in a shop. There is a special discount for items in the shop. If you buy the ith item, then you will receive a discount equivalent to prices[j] where j is the minimum index such that j > i and prices[j] lte prices[i]. Otherwise, you will not receive any discount at all. Return an integer array answer where answer[i] is the final price you will pay for the ith item of the shop, considering the special discount.
| Input | Output |
|---|---|
| prices = [8,4,6,2,3] | [4,2,4,2,3] |
| prices = [1,2,3,4,5] | [1,2,3,4,5] |
| prices = [10,1,1,6] | [9,0,1,6] |
Constraints:
1 ≤ prices.length ≤ 5000
1 ≤ prices[i] ≤ 1000
Abstract
We need to compute the final price of every item after applying its discount.
Each item's discount is determined by the first item to its right whose price is less than or equal to the current item's price. If no such item exists, the item receives no discount.
The challenge is efficiently finding the first future price satisfying this condition for every item, then subtracting that value from the current price.
Pseudocode
Sol 1: Increasing Stack of Pending Indices
1. stack = []
a. for i in range(len(prices)):
while stack and prices[i] <= prices[stack[-1]]:
origPriceIndex = stack.pop()
discount = prices[i]
prices[origPriceIndex] -= discount
2. stack.append(i)
3. return pricesSolution 1: [Monotonic Stack] Increasing Stack of Pending Indices - Stack/Monotonic Stack Next Smaller Element
def finalPrices(self, prices: List[int]) -> List[int]:
# Discount Rule:
# - each prices has a change to get a discount 1 time
# - the first later price that is less than, subtract that from the price
# Monotonic Stack:
# - stores prices that are waiting for a discount
# - once a price gets its 1 time discount, remove from stack and update prices
# [0] ... [i]
# low high
# Stack Progression Example: [7, 4, 6, 2, 3]
# - 7 pending => [7]
# - 4 is 7's discount (7 - 4 = 3), 4 pending => [4]
# - 6 pending => [4, 6]
# - 2 resolves 6's discount (6-2=4), then resolves 4's discount (4-2=2), 2 pending (stack: [2])
# - 3 pending => [2, 3]
# - loop ends, 2 and 3 never found a discount, keep original prices
# - discountedPrices = [3, 2, 4, 2, 3]
# Pending Indices
# sc: O(n)
stack = []
# tc: O(n)
for i in range(len(prices)):
# Discount Found:
# - current price[i] is less than top of stack
# - then we found discount for top of stack
# - discount top of stack, pop price we just discounted,
# and compare with new top if discount also applies
while stack and prices[i] <= prices[stack[-1]]:
# Grab original price index
origPriceIndex = stack.pop()
# Get discount
discount = prices[i]
# Reduce price by discount
prices[origPriceIndex] -= discount
# Push curr price to pending for a discount
stack.append(i)
# overall: tc O(n)
# overall: sc O(n)
return prices496. Next Greater Element I ::1:: - Easy
Topics: Array, Hash Table, Stack, Monotonic Stack
Intro
The next greater element of some element x in an array is the first greater element that is to the right of x in the same array. You are given two distinct 0-indexed integer arrays nums1 and nums2, where nums1 is a subset of nums2. For each 0 lte i lt nums1.length, find the index j such that nums1[i] == nums2[j] and determine the next greater element of nums2[j] in nums2. If there is no next greater element, then the answer for this query is -1. Return an array ans of length nums1.length such that ans[i] is the next greater element as described above. Follow up: Could you find an O(nums1.length + nums2.length) solution?
| Input | Output |
|---|---|
| nums1 = [4,1,2], nums2 = [1,3,4,2] | [-1,3,-1] |
| nums1 = [2,4], nums2 = [1,2,3,4] | [3,-1] |
Constraints:
1 ≤ nums.length ≤ nums2.length ≤ 1000
0 ≤ nums1[i], nuns2[i] ≤ 10^4
All integers in nums1 and nums2 are unique.
All the integers of nums1 also appear in nums2.
Abstract
Given an array, for every num, determine what is the next greatest value.
Pseudocode
Sol 1: Decreasing Stack With Hash Map Lookup
1. (nextGreater = {})
2. (stack = [])
3. for curr in nums2:
a. while stack and stack[-1] < curr:
foundNextForIndex = stack.pop()
nextGreater[foundNextForIndex] = curr
b. stack.append(curr)
4. (res = [])
5. for query in nums1:
a. if query in nextGreater:
res.append(nextGreater[query])
else:
res.append(-1)
6. return resSolution 1: [Monotonic Stack] Decreasing Stack With Hash Map Lookup - Stack/Monotonic Stack Next Greater Element
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
# Monotonic Stack:
# - scan nums2 left to right
# - maintain a stack strictly decreasing from bottom to top.
# [0] ... [i]
# high low
# - if curr number is greater than top of stack,
# we have found the 'next greater element' for that top of stack
# - continue comparing curr to top of stack
# - anything left on top of stack does not have a next greater element
# Hash Map:
# -
# Since nums1 is a subset of nums2, we only need to solve
# "next greater element" once per number in nums2, then look up
# each answer for nums1 in O(1) instead of resolving it twice.
# Hashmap:
# - num2 value => next greatest element
# - constraints guarantee all values in nums2 are unique, so it's
# safe to key this hashmap by value directly
# - if duplicates were allowed, a value-keyed hashmap would break --
# a later occurrence of the same value could silently overwrite
# an earlier index's already-correct answer (this is exactly
# why 503 keys its stack/result by index instead of value)
# - and we don't want to do by index here,
# is because the queries from nums1 are queries by value, not by index
# sc: O(n)
nextGreater = {}
# sc: O(n)
stack = []
# Generate the next greatest element hashmap
# tc: O(n)
for curr in nums2:
# Found Next Greatest:
# - curr is greater than top of stack
# - pop top of stack and record curr as its next greatest
while stack and stack[-1] < curr:
# Next Greatest:
# record curr as the next greatest for top of stack
foundNextForIndex = stack.pop()
nextGreater[foundNextForIndex] = curr
# Pending:
# - push curr to stack, waiting for its next greatest element
stack.append(curr)
# Next greatest num index for query stream
# sc: O(n)
res = []
# Use next greatest element hashmap to respond to queries in nums1
# tc: O(m), m = len(nums1)
for query in nums1:
# Query Stream:
# - check we found a next greatest for num
# - if not, return -1
if query in nextGreater:
res.append(nextGreater[query])
else:
res.append(-1)
# overall: tc O(n + m)
# overall: sc O(n)
return res503. Next Greater Element II ::1:: - Medium
Topics: Array, Stack, Monotonic Stack
Intro
Given a circular integer array nums (i.e., the next element of nums[nums.length - 1] is nums[0]), return the next greater number for every element in nums. The next greater number of a number x is the first greater number to its traversing-order next in the array, which means you could search circularly to find its next greater number. If it doesn't exist, return -1 for this number.
| Input | Output |
|---|---|
| nums = [1,2,1] | [2,-1,2] |
| nums = [1,2,3,4,3] | [2,3,4,-1,4] |
Constraints:
1 ≤ nums.length ≤ 10^4
10^9 ≤ nums[i] ≤ 10^9
Abstract
Given an array, for every num, determine what is the next greatest value. The array functions as a circular array.
Pseudocode
Sol 1: Decreasing Stack of Indices Simulated Circular Scan
1. (n = len(nums))
2. (res = [-1] * n)
3. (stack = [])
4. for i in range(2 * n):
a. curr = nums[i % n]
while stack and nums[stack[-1]] < curr:
foundNextForIndex = stack.pop()
res[foundNextForIndex] = curr
b. if i < n:
stack.append(i)
5. return resSolution 1: [Monotonic Stack] Decreasing Stack of Indices Simulated Circular Scan - Stack/Monotonic Stack Next Greater Element
def nextGreaterElements(self, nums: List[int]) -> List[int]:
# Monotonic Stack (Circular):
# - same idea as Next Greater Element I, but array is circular,
# so element at end of list could have next greater near beginning of list
# - simulate wrapping around by scanning the array twice
# (indices 0..2n-1), using i % n to map back into the real
# array, without actually building a doubled array in memory
# [0] ... [i]
# high low
# - if curr number is greater than the number at the index on top of stack,
# we have found the 'next greater element' for that index
# - continue comparing curr to the new index on top of the stack
# - anything left on the stack after second passes does not have a next greater
# Indices Instead Of Values:
# - unlike Next Greater Element I, nums are not guaranteed unique values
# - we can't use a value -> answer hashmap due to potential duplicate overwrite
# - instead we store indexes and write next greatest into res array by index
n = len(nums)
# Next greatest num index
# sc: O(n)
res = [-1] * n
# sc: O(n)
stack = []
# Simulated circular scan:
# - pass around array twice to ensure last element
# can find its next greatest element if it exists via wrap around
# tc: O(2n) ~ O(n)
for i in range(2 * n):
# Wrap:
# - Turn doubled range back in bounds of real array
curr = nums[i % n]
# Found Next Greatest:
# - curr is greater than the number at the index on top of stack
# - pop that index and record curr as its next greatest
while stack and nums[stack[-1]] < curr:
# Next Greatest:
# record curr as the next greatest for top of stack
foundNextForIndex = stack.pop()
res[foundNextForIndex] = curr
# Booking Keeping:
# - only push one copy of elements
# - do not push anything during second iteration
if i < n:
stack.append(i)
# overall: tc O(n)
# overall: sc O(n)
return res739. Daily Temperatures ::3:: - Medium
Topics: Array, Stack, Monotonic Stack, Two Pointers, Dynamic Programming
Intro
Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the ith day to get a warmer temperature. If there is no future day for which this is possible, keep answer[i] == 0 instead.
| Input | Output |
|---|---|
| [30,40,50,60] | [1,1,1,0] |
| [30,60,90] | [1,1,0] |
| [73,74,75,71,69,72,76,73] | [1,1,4,2,1,1,0,0] |
Constraints:
1 ≤ temperatures.length ≤ 105
30 ≤ temperatures[i] ≤ 100
Abstract
Given an array, for every num, find the number of steps until a higher num.
Pseudocode
Sol 1: Monotonic Decreasing Stack of Cold Temps
1. (n = len(temperatures))
2. (res = [0] * n)
3. (stack = [])
4. for i in range(n):
a. while stack and temperatures[stack[-1]] < temperatures[i]:
hotDayIndex = i
coldDayIndex = stack.pop()
coldDayWaitTime = hotDayIndex - coldDayIndex
res[coldDayIndex] = coldDayWaitTime
b. stack.append(i)
5. return res
Sol 3: Reverse Iteration With Jump Traversal Using Dynamic Programming
1. (n = len(temperatures))
2. (dp = [0] * n)
3. (maxHottestDay = n-1)
4. for i in range(n-2, -1, -1):
a. if temperatures[maxHottestDay] <= temperatures[i]:
maxHottestDay = i
b. else:
tempCandidateIndex = i+1
while temperatures[tempCandidateIndex] <= temperatures[i]:
tempCandidateIndex = tempCandidateIndex + dp[tempCandidateIndex]
dp[i] = tempCandidateIndex - i
5. return dpSolution 1: [Monotonic] Monotonic Decreasing Stack of Cold Temps - Stack/Monotonic Property Maintenance
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
# Monotonic Stack:
# - maintains indexes for monotonic decreasing temperatures
# - When monotonic decreasing rule breaks, that means we have found a hotter temperature
# for at least 1 of the previous temperatures
# Stack:
# * * * * *
# * * * * * * * * * *
# * * * * * * * * * * * * * * *
# * * * * * * * * * * * * * * * * * *
# * * * * * * * * * * * * * * * * * *
# * * * * + * * * * * + * * * * + * * * + * * * *
# ------------ --- ==> ------------ --- ==> --------- --- ==> ------ --- ==> ---------
# 0 1 2 3 4 calc wait 0 1 2 3 4 0 1 2 4 0 1 4 join() 0 1 4
# older hot day days
# Day 3 waits 1 day Day 2 waits 2 days Day 4 is colder than Day 1
n = len(temperatures)
# Hotter day wait time for temperatures
# sc: O(n)
res = [0] * n
# Stores indexes for temperatures
# sc: O(n)
stack = []
# tc: O(n)
for i in range(n):
# Hotter Temperature:
# - if stack is non empty, verify monotonic decreasing
# - if monotonic decreasing broken, curr temperature is hotter for at least 1 of the previous temperatures
# - pop indexes for temperatures off the stack until monotonic decreasing is true again
# - for each of the indexes for temperatures popped, we can calculate a hot day wait time
while stack and temperatures[stack[-1]] < temperatures[i]:
# i: Hotter day index
hotDayIndex = i
# stack[-1]: Cold day index
coldDayIndex = stack.pop()
# Wait time:
# - Hot day is the closest hotter day for the cold day,
# - Day 5 to Day 3 has a way time of 2 days, so wait time = (5 - 3) = 2
coldDayWaitTime = hotDayIndex - coldDayIndex
# Set wait time for current cold day
res[coldDayIndex] = coldDayWaitTime
# Monotonic Decrease:
# - appending index for temperature will keep monotonic decreasing true
# - this is the new coldest day on the stack
# - this is now waiting for a new hotter day
stack.append(i)
# overall: tc O(n)
# overall: sc O(n)
return resSolution 2: [Monotonic] Reverse Iteration Monotonic Decreasing Stack of Warm Temps [SC Opt] - Stack/Monotonic Property Maintenance
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
# Aggressive Pruning:
# - Reverse tends to use less actual in memory
# - In reverse, we are store hot temperatures in monotonic decreasing order
# - This ensures we have a list of the most recent hot temperatures
# to ensure minimum wait time for cold days
# - So colder temperatures are immediately resolved for their wait time,
# so colder days are never stored waiting for hotter days
# which can use less memory depending on the data
# Temperatures:
# temp:[ 1, 2, 4, 5, 4, 3, 1]
# index: 0 1 2 3 4 5 6
# Iterating Backwards + Monotonic Decrease (what we want):
# - we represent increasing temperatures
# - by storing hotter temperatures with the more recent days being on top of the stack
# list of temperatures our stack
# * *
# * * * *
# * * * *
# * * * * * *
# * * * * represented * * * *
# --------------- by => ---------------
# 0 1 2 3 our stack 3 2 1 0
n = len(temperatures)
# Hotter day wait time for temperatures
# sc: O(n)
res = [0] * n
# Stores indexes for temperatures
# sc: O(n)
stack = []
# tc: O(n)
for i in range(n-1, -1, -1):
# Hotter Temperature:
# - if stack is non empty, verify monotonic decreasing
# - if monotonic decreasing broken, curr temperature can serve as a hotter more recent temperature
# - pop older hot temperature and replace with newer hotter temperature
# - ensures minimum wait time for cold days
while stack and temperatures[stack[-1]] <= temperatures[i]:
stack.pop()
# Monotonic Decreasing:
# - curr is colder than top of stack
# - ergo, everything on stack is hotter than curr
# - top of stack guaranteed to be the closest hottest day to curr
# - calculate wait days for current cold day
if stack:
# Closest hotter day to curr
futureHotDayIndex = stack[-1]
# Cold day curr
currentColdDayIndex = i
# Wait time calculation
waitDays = futureHotDayIndex - currentColdDayIndex
# Set wait time for current cold day
res[i] = waitDays
# Monotonic Decrease:
# - appending index for temperature will keep monotonic decreasing true
# - this is least hottest day on the stack
# - this is acting as the most recent hottest day
stack.append(i)
# overall: tc O(n)
# overall: sc O(n)
return resSolution 3: [Dynamic Programming] Reverse Iteration With Jump Traversal Using Dynamic Programming Building Future Warm Temperatures List [TC Opt] - Stack/Algorithm
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
# Dynamic Programming:
# - Use previously found hottest day to serve as jump skips for curr temperature
# Jump Traversal:
# - for curr i, try i+1 as the next hottest day
# - if i+1 is the next hottest day, this is easy 1 day wait
# - if its not, then the next hottest day for i+1, can also apply for i
# - skip to next hottest day for i+1, to serve as a candidate for i
n = len(temperatures)
# Hotter day wait time for temperatures
# sc: o(n)
dp = [0] * n
# Hottest day found so far:
maxHottestDay = n-1
# Reverse iteration to build next hottest day dp list
# tc: O(n)
for i in range(n-2, -1, -1):
# New Hottest Day
# - curr is hotter than max hottest day
# - no future possible days exist that are hotter
if temperatures[maxHottestDay] <= temperatures[i]:
maxHottestDay = i
# Next Hottest Possible:
# - curr has at least 1 hotter day in the future
# - calculate wait days for curr
else:
# Init candidate:
# - curr's first candidate is the next day
tempCandidateIndex = i+1
# Update Candidate:
# - if candidate is not a hotter day
# - jump to the candidates own next hottest
while temperatures[tempCandidateIndex] <= temperatures[i]:
# Jump to candidate's own next hottest day:
# candidate's next hottest day = candidate index + wait days time
tempCandidateIndex = tempCandidateIndex + dp[tempCandidateIndex]
# Found next hottest day:
# - Set wait time for curr
dp[i] = tempCandidateIndex - i
# overall: tc O(n)
# overall: sc O(n)
return dp901. Online Stock Span ::1:: - Medium
Topics: Stack, Design, Monotonic Stack, Data Stream
Intro
Design an algorithm that collects daily price quotes for some stock and returns the span of that stock's price for the current day. The span of the stock's price in one day is the maximum number of consecutive days (starting from that day and going backward) for which the stock price was less than or equal to the price of that day. For example, if the prices of the stock in the last four days is [7,2,1,2] and the price of the stock today is 2, then the span of today is 4 because starting from today, the price of the stock was less than or equal 2 for 4 consecutive days. Also, if the prices of the stock in the last four days is [7,34,1,2] and the price of the stock today is 8, then the span of today is 3 because starting from today, the price of the stock was less than or equal 8 for 3 consecutive days. Implement the StockSpanner class: StockSpanner() Initializes the object of the class. int next(int price) Returns the span of the stock's price given that today's price is price.
| Input | Output |
|---|---|
| ["StockSpanner", "next", "next", "next", "next", "next", "next", "next"] [[], [100], [80], [60], [70], [60], [75], [85]] | [null, 1, 1, 1, 2, 1, 4, 6] |
Constraints:
1 ≤ price ≤ 10^5
Almost 10^4 calls will be made to next
Abstract
Given a array of nums, for each num, determine how many the count of smaller or equal numbers in the past, excluding anything once we encounter a higher num. [20 100, 80, 60, 70, 85] Here, only 80, 60, 70 count, because we cut off at the 100 before we reach the 20.
Pseudocode
Sol 1: Decreasing Stack of (Price, Span) Pairs
1. def __init__(self):
a. (self.stack = [])
2. def next(self, price: int) -> int:
a. span = 1
b. while self.stack and self.stack[-1][0] <= price:
(prevPrice, prevSpan) = self.stack.pop()
span += prevSpan
c. self.stack.append((price, span))
d. return spanSolution 1: [Monotonic Stack] Decreasing Stack of (Price, Span) Pairs - Stack/Monotonic Stack Online Stock Span
class StockSpanner:
# Price Stream:
# - Given a stream of prices
# - For each index i, find find out how many consecutive days immediately preceding today
# had prices less than or equal to today's price
# - Consecutive days cannot be broken up by a more expensive day
# Monotonic Stack (Price, Span):
# - monotonic decreasing prices:
# [0] ... [i]
# high low
# - Tuples carry the span they 'own'
# representing their number of consecutive days (including itself)
# When today's price is greater than or equal to the price on top of the stack,
# that top entry can never again be the answer for any future day
# (today always dominates it looking backward),
# so we absorb its span into today's before pushing.
# Why this works:
# Absorbing spans instead of popping and forgetting means we
# never have to re-walk days we've already collapsed.
# Each day is pushed once and popped at most once over the life of the object,
# so the amortized cost per call stays O(1).
def __init__(self):
# Tracking: (price, span)
# sc: O(n)
self.stack = []
def next(self, price: int) -> int:
# Spanning:
# today always spans at least itself
span = 1
# Calculate Span:
# - stack is non empty
# - todays price is greater than the top of the stack
# - top of the stack should be included into todays span
# - we can remove it from the top of the stack, since in the future
# it will be included within todays span
while self.stack and self.stack[-1][0] <= price:
# Popped span is now included within todays span,
# no need to keep it on the stack anymore
(prevPrice, prevSpan) = self.stack.pop()
# Include span we popped into curr's span
span += prevSpan
# Monotonic Decreasing:
# - Curr becomes new max needed to surpass to have a valid span
self.stack.append((price, span))
# Each (price, span) pushed/popped at most once across all calls to next()
# tc: O(1) amortized
return span
# overall: tc O(n)
# overall: sc O(n)853. Car Fleet ::2:: - Medium
Topics: Array, Stack, Sorting, Monotonic Stack
Intro
There are n cars at given miles away from the starting mile 0, traveling to reach the mile target. You are given two integer array position and speed, both of length n, where position[i] is the starting mile of the ith car and speed[i] is the speed of the ith car in miles per hour. A car cannot pass another car, but it can catch up and then travel next to it at the speed of the slower car. A car fleet is a car or cars driving next to each other. The speed of the car fleet is the minimum speed of any car in the fleet. If a car catches up to a car fleet at the mile target, it will still be considered as part of the car fleet. Return the number of car fleets that will arrive at the destination.
| Input | Output |
|---|---|
| target = 10, position = [3], speed = [3] | 1 |
| target = 100, position = [0,2,4], speed = [4,2,1] | 1 |
| target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3] | 3 |
Constraints:
1 ≤ n ≤ 105
0 < target ≤ 1010
0 ≤ position[i] < target
All of values of position are unique
0 < speed[i] ≤ 106
Abstract
Given a list of speeds, determine how many cars will catch up to another and end up bumper to bumper (fleet) before arriving to the destination.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Monotonic] [Sorting] Increasing Stack of (Slower aka Higher) Fleet Times Tracking Monotonic Pattern [TC Opt] - Stack/Monotonic Property Maintenance
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
# Fleet Rule:
# - a car that catches up to a slower car ahead of it is forced to go at the slower
# cars speed, which creates a fleet
# - a car that never catches up to the car ahead of it forms its own fleet
# Sorting By Distance To Target:
# - sort cars by closest to target first and farthest last
# [0] ... [n-1]
# (closest) (farthest)
# higher val lower val
# Sort By Distance Implication:
# - if a farther car has a lower time to target than the car ahead of it,
# it will catch up to that car and join its fleet
# - that car ahead of it sets the time to target for the entire fleet
# Iterate from closest cars to farthest cars:
# - Check each car with the car/fleet ahead of it
# - If curr car has a lower time to arrival (faster) than the car/fleet ahead of it,
# it will join that fleetCompare arrival time with the most recently created fleet
# - If car has a higher time to arrival (slower) than the car/fleet ahead of it,
# it will be left behind and starts a new fleet
# Monotonic Increasing Stack:
# - top of stack holds the fleet with the slowest time to arrival
# - we only need to check if a farther car catches up with the slowest fleet
# or if it becomes its own new slowest fleet
# Tracks number of fleets from from low -> high time to arrival
# sc: O(n)
stack = []
# Prepare to sort by distance:
# O(n)
cars = list(zip(position, speed))
# Sort by descending (higher/closer to farther/lower):
# tc: timSort O(n log n)
cars.sort(reverse=True)
# Iterate from closer to farther
# tc: O(n)
for (pos, spd) in cars:
# Time To Target:
currTimeToTarget = (target - pos) / spd
# Monotonic Increasing Broken:
# - curr car has lower time to target than top of stack (slowest fleet)
# - curr car catches up to fleet, and now must follow that fleets time to target
if stack and currTimeToTarget < stack[-1]:
# Top of stack stays the same,
# current slowest fleet stays the same
continue
# Monotonic Increasing Kept:
# - stack is empty
# - curr time to target is greater/slower than top of stack
# - new curr becomes the new slowest fleet
else:
# Curr becomes new slowest fleet
stack.append(currTimeToTarget)
# Stack is storing all fleet time to targets,
# total count of time to targets representing count of fleets
numOfFleets = len(stack)
# overall: tc O(n log n)
# overall: sc O(n)
return numOfFleetsSolution 2: [Greedy] Track Current Fleet With Greedy Assumption [SC Opt] - Stack/Algorithm
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
# Greedy Monotonic Stack Compression:
# - previous stack is doing 2 things:
# tracks slowest fleet at top of stack
# tracks number of fleets by number of time to targets on stack
# - we can do both of these with variables
# Slowest Time To Target Variable:
# - give a new car, compare to the slowest time to target
# - if new car has a higher time to target, it is slower, and it becomes the new slowest
# - if new car has a lower time to target, it is faster, and catches up to the slowest
# with the slowest time to target staying the same
# Fleet Rule:
# - a car that catches up to a slower car ahead of it is forced to go at the slower
# cars speed, which creates a fleet
# - a car that never catches up to the car ahead of it forms its own fleet
# Sorting By Distance To Target:
# - sort cars by closest to target first and farthest last
# [0] ... [n-1]
# (closest) (farthest)
# higher val lower val
# Sort By Distance Implication:
# - if a farther car has a lower time to target than the car ahead of it,
# it will catch up to that car and join its fleet
# - that car ahead of it sets the time to target for the entire fleet
numFleets = 0
slowestFleetTimeToTarget = 0
# Prepare to sort by distance:
# O(n)
cars = list(zip(position, speed))
# Sort by descending (higher/closer to farther/lower):
# tc: timSort O(n log n)
cars.sort(reverse=True)
# Iterate from closer to farther
# tc: O(n)
for (pos, spd) in cars:
# Time To Target:
currCarTimeToTarget = (target - pos) / spd
# Curr Car Slower:
# - curr car is slower than the current slowest time to target,
# so it can't catch up and starts a new fleet
if slowestFleetTimeToTarget < currCarTimeToTarget:
# Update slowest time to target
slowestFleetTimeToTarget = currCarTimeToTarget
# Create new fleet
numFleets += 1
# Curr Car Faster:
# - curr car is faster than the current slowest time to target,
# so it catches up and joins that fleet
else:
# Slowest fleet stays the same,
# no new fleet created
continue
# overall: tc O(n log n)
# overall: sc O(1)
return numFleets84. Largest Rectangle in Histogram ::2:: - Hard
Topics: Array, Stack, Monotonic Stack
Intro
Given an array of integers heights representing the histogram's bar height where the width of each bar is 1, return the area of the largest rectangle in the histogram.
| Input | Output |
|---|---|
| [2,4] | 4 |
| [2,1,5,6,2,3] | 10 |
Constraints:
1 ≤ heights.length ≤ 105
0 ≤ heights[i] ≤ 104
Abstract
Given array of heights, find the area of the largest rectangle in the histogram.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Monotonic] Imaginary Boundaries Surrounding Rectangles Game By Monotonic Stack Covering Heights Rule Implication - Stack/Monotonic Property Maintenance
def largestRectangleArea(self, heights: List[int]) -> int:
# Monotonic Stack:
# - maintains monotonic increasing heights
# - when monotonic increasing stays true,
# that implies new height is taller than all older heights
# - when monotonic increasing is false,
# that implies new height is smaller than at least 1 of the older heights
# - if stack is non empty, that means we have multiple tall bars
# that will serve to generate an area by covering shorter walls
# - the width of the rectangle is distance between the index
# the tall bar on top of the stack and index of the new shorter wall
# Small Bar Popping Tall Bar Area Generation:
# - a small bar to that breaks the monotonic increasing rule triggers area generation
# - we know that the tallest bar on the stack, 'covers' all other bars on the stack
# - we can use the tallest bar as the right boundary while using each smaller bars height,
# which gives us a valid width (tallest bar index - some smaller bar index)
# as well as a valid height (smaller bar height) which is guaranteed to be covered by the tallest bar,
# allowing use to use it as the height for the rectangle
# * A * | *
# * * * A A A | * *
# * * * * * * A * * A A * | A A A A
# * * * * + * ==> * * * A * ==> * * A A * | * A A A A
# --------- ... --- --- --------- ... ------ --------- ... ------ | --------- ... ------
# older --> newer new 0 1 2 6 7 0 1 2 6 7 | 0 1 2 6 7
# |
# area from 6-6 area from 2-6 | We do not generate area from 1-4
# using height using height | as 7 cannot act as its own right boundary
# from 6 bar from the 2 bar |
# Round 1: Generating areas while popping
# - notice how we can't generate an area with a right boundary
# - areas are not generated until a smaller bar appears
# - tallest bar will serve as a right boundary for all areas generated per round
# Pop() 1
# * * A
# * * * * * A *
# * * * * * * * * * * A * * * *
# * * * * * ==> * * * * * ==> * * * A * ==> pop() ==> * * * *
# --------- ... ---- + --- --------- ... ------ --------- ... ------- tall --------- ... ---
# 0 1 2 6 new 7 0 1 2 6 7 0 1 2 6 7 bar 0 1 2 7
# Pop() 2
#
# * * A
# * * * * * * * A * * *
# * * * * ==> * * * * ==> * * A * ==> pop() ==> * * *
# --------- ... + --- --------- ... --- --------- ... --- tall ------- ... ---
# 0 1 2 new 7 0 1 2 7 0 1 2 7 bar 0 1 7
# Final Result
#
# * * * *
# * * * ==> * * *
# ------ ... + --- ------ ... ---
# 0 1 new 7 0 1 7
# Round 2: Generating the walls while popping
# Pop() 1
#
# * * * * * *
# * * * + * ==> pop() ==> * * * * ==> pop() ==> * * * A A *
# ------ ... --- new ___ tall ------ ... ------ ... ---
# 0 1 7 8 0 1 7 8 0 1 7
# Diagram 2: Correcting the distance
# If you recall, the distance between these two walls is actually:
#
# * * * * A A A A
# * * * + * ==> * * * * ==> * A A A A *
# --------------- new --- ----------------- -----------------
# Diagram 2: A Stack Of Indexes
# So to solve this problem, we keep the indexes on the stack not the heights,
# in order to save the distance between the walls for when the time comes
# to calculate the width of areas.
# Diagram 2: Indexes
# A A A A
# * A A A A *
# -----------------
# 0 1 2 3 4 5
# Diagram 2: Width Calculation
# For any bar x
# - Left boundary = the nearest bar to the left that is shorter
# - Right boundary = the nearest bar to the right that is shorter
# - Width = (right boundary index - left boundary index) - 1
# So for here: = (5 - 0) - 1 = 5 - 1 = 4 wide
# Summary Popping Rule:
# We keep popping as long as the stack has taller bars than the new smaller bar.
# A taller bar implies that previous bars on the stack are covered the new smaller bar is covered and can generate an area.
# Once an area can be generated, we just need to calculate the width or the distance between the shorter bar and taller bar
# Popping Rule Implies either:
#
# 1. The stack gets completely popped:
# Implies the new height candidate is the smallest height encountered so far
# and was covered by all other previous tall bars.
# This allows the area being generated to span from
# 0 -> new_candidate = 0 -> i = i - 0: so width = i
# and the height to be the height of the new height candidate, so height = new height candidate
#
# 2. The stack does not get completely popped:
# Implies the new height candidate is taller than at least 1 older bar
# and is not covered by all other previous tall bars.
# This creates a left bound for the area being generated to span from
# left bound -> new candidate, left bound -> i = i - left_bound: so width = i - left_bound
# The height
# After the area is generated, the new height candidate becomes the new top of the stack,
# and is compared against new walls, which may or may not be taller or shorter.
# Sentinel:
# With the rule that area is not calculated until a smaller bar appears,
# we need to add a 0 to flush the remaining bars on the stack,
# so that every bar is eventually processed regardless of height
sentinel = 0
heights.append(sentinel)
# sc: stores indexes for up to n heights O(n)
stack = []
max_area = 0
# tc: O(n)
for i in range(len(heights)):
# Monotonic increasing is broken:
# Check: if new bar breaks monotonic increasing order
# Implies: The new candidate is shorter than at least 1 bar on the stack
# Implies: We have found a right boundary
# Rule: The taller bar can generate an area, pop() height for taller bar,
# then check top of stack for the left boundary
while stack and heights[stack[-1]] > heights[i]:
# Grab taller bar index and height
tallWallIndex = stack.pop()
tallWallHeight = heights[tallWallIndex]
# Check: if stack is empty after pop()
# Implies: the new wall is shorter than all previous heights,
# Implies: the left boundary goes up until an imaginary index -1 before the start
if not stack:
# Actual rectangle is from 0 to i-1
# So the surrounding boundaries are at -1 and i:
# width = (right boundary) - (left boundary) - 1
# = i - (imaginary index -1 before the start) - 1
# = i - (-1) - 1
width = i - (-1) - 1
# Check: Stack is not empty after pop()
# Implies: top of stack can serve as left boundary
else:
# Grab left boundary
leftWallIndex = stack[-1]
# Actual rectangle is from (leftWallIndex + 1) to (i-1)
# So the surrounding boundaries are at (leftWallIndex) and i
# width = (right boundary) - (left boundary) - 1
# = i - (left wall on top of stack) - 1
# = i - stack[-1] - 1
width = i - leftWallIndex - 1
# Check: new max area
max_area = max(max_area, tallWallHeight * width)
# Monotonic increasing is maintained:
# Curr height is taller than all walls on the stack, there is no area to generate, append to stack and grab next new wall
stack.append(i)
# overall: tc O(n)
# overall: sc O(n)
return max_areaSolution 2: [Monotonic] Reverse Iteration Index Math Imaginary Boundaries Surrounding Rectangles Game By Monotonic Stack Covering Heights Rule Implication - Stack/Monotonic Property Maintenance
def largestRectangleArea(self, heights: List[int]) -> int:
# Monotonic Stack:
# A stack that maintains monotonic increasing heights
# Stack popping off taller bars:
# * *
# * * * * *
# * * * * * * * * * * * * * * *
# * * * * + * ==> * * * * * => * * * * => * * * ==> * * *
# ------------ new --- pop off ------------ --- --------- --- ------ --- ---------
# taller bars shorter bar taller walls final result
# Reverse Iteration Notice:
# The diagram is the same for left to right iteration and right to left iteration
# even though we are grabbing the bars on the right vs the left.
# That is because the stack itself cannot tell the difference and just does the same algorithm.
# Thus, all that changes is the width calculation with the indexes
# Index Math:
# Follow the same approach as forward iteration of finding the imaginary boundaries
n = len(heights)
max_area = 0
stack = []
# tc: O(n)
for i in range(n-1, -2, -1):
# Sentinel: append a sentinel index of -1, height of 0
curr_height = 0 if i == -1 else heights[i]
# Monotonic increasing is broken:
# Check: if new bar breaks monotonic increasing order
# Implies: The new candidate is shorter than at least 1 bar on the stack
# Implies: We have found a right boundary
# Rule: The taller bar can generate an area, pop() height for taller bar,
# then check top of stack for the left boundary
while stack and heights[stack[-1]] > curr_height:
# Grab taller bar index and height
index = stack.pop()
height = heights[index]
# Check: if stack is empty after pop()
# Implies: the new wall is shorter than all previous heights,
# Implies: the right boundary goes up until an imaginary index n after the end
if not stack:
# Actual rectangle is from (i+1) to (n-1)
# So the surrounding boundaries are at i and n:
# width = (right boundary) - (left boundary) - 1
# = (imaginary index) - i - 1
# = n - i - 1
width = n - i - 1
# Check: Stack is not empty after pop()
# Implies: top of stack can serve as right boundary
else:
# Grab right boundary
rightWallIndex = stack[-1]
# Actual rectangle is from (i+1) to (rightWallIndex - 1)
# So the surrounding boundaries are at i and rightWallIndex:
# width = (right boundary) - (left boundary) - 1
# = (right wall on top of stack) - i - 1
# = stack[-1] - i - 1
width = rightWallIndex - i - 1
# check new area
max_area = max(max_area, height * width)
# if non-sentinel index, append
if i >= 0:
stack.append(i)
# overall: tc O(n)
# overall: sc O(n)
return max_area402. Remove K Digits ::1:: - Medium
Topics: String, Stack, Greedy, Monotonic Stack
Intro
Given string num representing a non-negative integer num, and an integer k, return the smallest possible integer after removing k digits from num.
| Input | Output |
|---|---|
| num = "10", k = 2 | "0" |
| num = "10200", k = 1 | "200" |
| num = "1432219", k = 3 | "1219" |
Constraints:
1 ≤ k ≤ num.length ≤ 105
num consists of only digits
num does not have any leading zeros except for 0 itself
Abstract
Remove k digits in a way so that resulting integer is as large as possible.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Greedy] [Stack] Push Lower Digits To Left Of Stack Then Slice Via Monotonic Increasing Digits Stack - Stack/Monotonic Property Maintenance
def removeKdigits(self, num: str, k: int) -> str:
# Monotonic Stack:
# A Stack that maintains increasing digits
# To create the smallest number possible, we want our stack to keep smaller numbers
# on the left side and larger numbers on the right:
# Smaller number (left to right):
# stack = [ 1 2 3 4 5 ]
# Instead of larger number (right to left):
# stack = [ 5 4 3 2 1 ]
# A monotonic increasing stack allows us to create the smallest number possible
# A monotonic decreasing stack allows us to create the largest number possible
# So with a monotonic increasing stack, by ordering left to right,
# we can slice at the end and grab as many numbers as we need or remove
# more numbers (if we still have k left)
# Slicing Grabbing:
# "12345" [:3] -> "123"
# 3 grabs the leftmost digits, creates the smallest number possible
# Slicing Removal:
# "12345" [:-2] -> "123"
# -2 removes rightmost 2 digits, creates the smallest number possible
# Greedy Summary:
# The greedy removal is ensuring monotonic stack,
# as we want the smaller numbers to be to the left as much as possible,
# and we want to keep removing from the stack as long as we can (have k left)
# sc: stack holds increasing digits up to n digits O(n)
stack = []
# tc: O(n)
for digit in num:
# Check: if stack is non empty, we have a candidate for removal
# Check: if (k > 0) we still have digits to remove
# Implies: if monotonic increasing broken, there is a larger digit to remove
# Then: remove larger digit on stack to allow lower digit to be more to the left
while stack and k > 0 and stack[-1] > digit:
# Top of stack is larger than new digit, remove
stack.pop()
# Decrease remaining digits to remove
k -= 1
# Monotonic increasing valid:
# New digit is as far to the left as possible
stack.append(digit)
# Building stack complete:
# Digits are now ordered left to right with smallest being on the left
# Check: if (k > 0) we still have digits to remove
# Then: remove larger digits from the right side, to produce final smallest answer
if k > 0:
stack = stack[:-k]
# Join all digits in the stack into a single string
smallerNumber = ''.join(stack)
# Clean answer:
# Remove leading zeros "0000200" -> "200"
result = smallerNumber.lstrip('0')
# 0 Check:
# if smallerNumber was all zeros, we will get an empty string after removing zeros
# "0000" -> ""
# so check if empty and return "0"
if not result:
result = "0"
# overall: tc O(n)
# overall: sc O(n)
return result