Implement stack and interval algorithms with tests
Company: Rippling
Role: Machine Learning Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Onsite
Quick Answer: This question evaluates implementation-level competencies with fundamental data structures and algorithms—stack-based bracket validation, interval merging with open/closed endpoint semantics, and arithmetic expression evaluation using stacks—while also measuring the ability to design comprehensive unit tests; category: Coding & Algorithms for a Machine Learning Engineer role. It is commonly asked to gauge practical coding ability, correctness under tricky edge cases and input semantics, and test-coverage awareness, representing a practical application-level assessment that also requires conceptual reasoning about invariants and boundary behavior.
Part 1: Validate Bracket String with a Stack
Constraints
- 0 <= len(s) <= 100000
- s contains only '(', ')', '[', ']', '{', '}'
- Target complexity: O(n) time and O(n) auxiliary space
- Do not modify the input string
Examples
Input: "()[]{}"
Expected Output: True
Explanation: All brackets are matched and appear in valid order.
Input: "([{}])"
Expected Output: True
Explanation: This is a properly nested bracket string.
Input: "([)]"
Expected Output: False
Explanation: The bracket types cross, so nesting is invalid.
Input: "]{"
Expected Output: False
Explanation: The string starts with a closing bracket, so it cannot be valid.
Input: ""
Expected Output: True
Explanation: An empty string has no mismatched brackets.
Solution
def solution(s):
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for ch in s:
if ch in '([{':
stack.append(ch)
else:
if not stack or stack[-1] != pairs[ch]:
return False
stack.pop()
return not stackTime complexity: O(n). Space complexity: O(n).
Hints
- Use a stack to store opening brackets as you scan from left to right.
- When you see a closing bracket, the top of the stack must be the matching opening bracket.
Part 2: Merge Intervals with Open/Closed Endpoints
Constraints
- 0 <= number of intervals <= 200000
- For every interval, start <= end
- If start == end and not both endpoints are closed, the interval is empty
- Target complexity: O(n log n) time due to sorting and O(n) extra space
Examples
Input: [(1, 3, True, True), (3, 5, True, False)]
Expected Output: [(1, 5, True, False)]
Explanation: [1,3] and [3,5) touch at 3, and at least one touching endpoint is closed, so they merge.
Input: [(1, 3, True, False), (3, 5, False, True)]
Expected Output: [(1, 3, True, False), (3, 5, False, True)]
Explanation: [1,3) and (3,5] do not overlap and do not merge because neither interval includes 3.
Input: [(1, 4, True, False), (2, 4, False, True)]
Expected Output: [(1, 4, True, True)]
Explanation: The intervals overlap, and the merged interval keeps the shared end 4 as closed because one interval includes it.
Input: []
Expected Output: []
Explanation: An empty input list produces an empty union.
Input: [(3, 3, False, False), (3, 3, True, True), (3, 3, True, True), (5, 7, False, True), (1, 2, True, False), (2, 5, True, False)]
Expected Output: [(1, 5, True, False), (5, 7, False, True)]
Explanation: The empty interval (3,3) is discarded, duplicates are absorbed, [1,2) merges with [2,5), and (5,7] remains separate because 5 is excluded from both sides.
Solution
def solution(intervals):
cleaned = []
for start, end, left_closed, right_closed in intervals:
if start == end and not (left_closed and right_closed):
continue
cleaned.append((start, end, left_closed, right_closed))
if not cleaned:
return []
cleaned.sort(key=lambda x: (x[0], 0 if x[2] else 1, x[1], 0 if x[3] else 1))
result = []
cur_start, cur_end, cur_left, cur_right = cleaned[0]
for start, end, left_closed, right_closed in cleaned[1:]:
if cur_end > start or (cur_end == start and (cur_right or left_closed)):
if end > cur_end:
cur_end = end
cur_right = right_closed
elif end == cur_end:
cur_right = cur_right or right_closed
else:
result.append((cur_start, cur_end, cur_left, cur_right))
cur_start, cur_end, cur_left, cur_right = start, end, left_closed, right_closed
result.append((cur_start, cur_end, cur_left, cur_right))
return resultTime complexity: O(n log n). Space complexity: O(n).
Hints
- Sort intervals by starting position, with closed left endpoints before open ones when starts are equal.
- When two merged intervals end at the same value, the merged right endpoint is closed if either interval includes that endpoint.
Part 3: Evaluate Arithmetic Expression Using Stacks
Constraints
- 1 <= len(expr) <= 100000
- expr contains digits, spaces, '+', '-', '*', '/', and parentheses
- The expression is valid
- Intermediate and final results fit in 32-bit signed integer range
- Target complexity: O(n) time and O(n) space
Examples
Input: "1 + 2 * 3"
Expected Output: 7
Explanation: Multiplication happens before addition.
Input: "((42))"
Expected Output: 42
Explanation: Nested parentheses around a single number should still evaluate correctly.
Input: "-3+5"
Expected Output: 2
Explanation: The leading minus is unary, so the expression is (-3) + 5.
Input: "1+-2"
Expected Output: -1
Explanation: The second minus is unary, so the expression is 1 + (-2).
Input: "2*(-3 + 4) + 7 / 2"
Expected Output: 5
Explanation: (-3 + 4) = 1, so 2*1 = 2, and 7/2 truncates toward zero to 3, giving 2 + 3 = 5.
Solution
def solution(expr):
nums = []
ops = []
precedence = {'+': 1, '-': 1, '*': 2, '/': 2, 'u-': 3}
right_associative = {'u-'}
def trunc_div(a, b):
sign = -1 if (a < 0) ^ (b < 0) else 1
return sign * (abs(a) // abs(b))
def apply_op():
op = ops.pop()
if op == 'u-':
nums.append(-nums.pop())
return
b = nums.pop()
a = nums.pop()
if op == '+':
nums.append(a + b)
elif op == '-':
nums.append(a - b)
elif op == '*':
nums.append(a * b)
else:
nums.append(trunc_div(a, b))
i = 0
n = len(expr)
prev = 'start' # one of: start, num, op, (, )
while i < n:
ch = expr[i]
if ch == ' ':
i += 1
continue
if ch.isdigit():
value = 0
while i < n and expr[i].isdigit():
value = value * 10 + (ord(expr[i]) - ord('0'))
i += 1
nums.append(value)
while ops and ops[-1] == 'u-':
apply_op()
prev = 'num'
continue
if ch == '(':
ops.append(ch)
prev = '('
elif ch == ')':
while ops and ops[-1] != '(':
apply_op()
ops.pop()
while ops and ops[-1] == 'u-':
apply_op()
prev = ')'
else:
if ch == '-' and prev in ('start', 'op', '('):
ops.append('u-')
prev = 'op'
else:
while ops and ops[-1] != '(' and (
precedence[ops[-1]] > precedence[ch] or
(precedence[ops[-1]] == precedence[ch] and ch not in right_associative)
):
apply_op()
ops.append(ch)
prev = 'op'
i += 1
while ops:
apply_op()
return nums[-1]Time complexity: O(n). Space complexity: O(n).
Hints
- Use one stack for numbers and one for operators, applying operators by precedence.
- Treat unary minus as a separate operator with higher precedence than binary + and -.