Framework Engineering - Whiteboard Coding Patterns

Here is solutions for practice whiteboard coding in AXON.

1. User Key-Value Timestamp

from collections import defaultdict

class Location:
    def __init__(self, latitude, longitude, timestamp):
        self.latitude = latitude
        self.longitude = longitude
        self.timestamp = timestamp

    def __str__(self):
        return f"({self.latitude}, {self.longitude}, {self.timestamp})"


class LocationTracker:
    def __init__(self):
        self.locations = defaultdict(list)

    def set(self, user_id: str, location: Location) -> None:
        if user_id not in self.locations:
            self.locations[user_id] = []

        self.locations[user_id].append(location)

    # Query time and binary search
    def get(self, user_id: str, query_time: int) -> Location:
        if user_id not in self.locations:
            return Location(0, 0, 0)

        userLocations = self.locations[user_id]
        left, right = 0, len(userLocations) - 1

        while left <= right:
            mid = left + (right - left) // 2

            if userLocations[mid].timestamp == query_time:
                return userLocations[mid]
            elif userLocations[mid].timestamp < query_time:
                left = mid + 1
            else:
                right = mid - 1

        return userLocations[left]


def main():
    locationTracker = LocationTracker()

    locationTracker.set(1, Location(2,5,1))
    locationTracker.set(1, Location(3,6,3))
    locationTracker.set(1, Location(7,4,6))

    query_location = locationTracker.get(1, 2)
    print(query_location)


main()


2. Encrypt, Descrypt, Hash Code

# Encrypt, Descrypt, Hash Code

class FuzzySearch:
    ALPHABET_SIZE = 26
    BASE_ORD = ord('a')

    def __init__(self, secret: str) -> None:
        self.secret = secret
        self.block_size = len(secret)

    def _idx2char(self, idx: int) -> chr:
        # 1 -> a, 2 -> b, 3 -> c,...
        char_idx = idx - 1 + self.BASE_ORD
        return chr(char_idx)

    def _char2idx(self, char: str) -> int:
        # a -> 1, b -> 2, c -> 3,...
        return ord(char) - self.BASE_ORD + 1

    def _addChar(self, char1: str, char2: str) -> str:
        v1 = self._char2idx(char1)
        v2 = self._char2idx(char2)
        return self._idx2char((v1 + v2) % self.ALPHABET_SIZE)

    def _subChar(self, char1: str, char2: str) -> str:
        v1 = self._char2idx(char1)
        v2 = self._char2idx(char2)
        return self._idx2char((v1 - v2 + self.ALPHABET_SIZE) % self.ALPHABET_SIZE)

    def _addStr(self, str1: str, str2: str) -> str:
        result = []

        for c1, c2 in zip(str1, str2):
            merged = self._addChar(c1, c2)
            result.append(merged)

        return ''.join(result)

    def encrypt(self, data: str) -> str:
        result = []

        for i, char in enumerate(data):
            secret_char = self.secret[i % self.block_size]
            merged = self._addChar(char, secret_char)
            result.append(merged)

        return ''.join(result)

    def decrypt(self, data: str) -> str:
        result = []

        for i, char in enumerate(data):
            secret_char = self.secret[i % block_size]
            merged = self._subChar(char, secret_char)
            result.append(merged)

        return ''.join(result)

    def hash(self, data: str) -> str:
        n = len(data)

        acc = self.secret
        for i in range(0, n - self.block_size, self.block_size):
            block_str = self.data[i, i + self.block_size]
            acc = self._addStr(acc, block_str)

        return acc

fuzzySearch = FuzzySearch('abc')
print(fuzzySearch.encrypt('abcdef'))

3. Insert/Delete/Random O(1)

# Insert/Delete/Random O(1)
from collections import defaultdict
import random

class InsertDeleteRandom:
    def __init__(self):
        self.arr = []
        self.pos = defaultdict(set)

    def insert(self, num):
        if num not in self.pos:
            self.pos[num] = set()

        # Add to array
        self.arr.append(num)

        # Add the index
        self.pos[num].add(len(self.arr) - 1)


    def remove(self, num):
        if num not in self.pos:
            return

        # Remove idx
        remove_idx = self.pos[num].pop()
        last_val = self.arr[-1]

        # Replace the remove_idx by last_val and pop last_val
        self.arr[remove_idx] = last_val

        # Pop last_val and remove index
        # self.pos[num].discard(remove_idx)
        self.pos[last_val].add(remove_idx)

        # Remove last val
        self.pos[last_val].discard(len(self.arr) - 1)
        self.arr.pop()


    def random(self):
        return random.choice(self.arr)

def main():
    solution = InsertDeleteRandom()

    solution.insert(1)
    solution.insert(3)
    solution.insert(3)
    solution.insert(5)

    solution.remove(3)

    print(solution.random())

main()

4. Police Parking Garage

from collections import deque
from typing import List

class PoliceParkingGarage:
    def __init__(self, carIds: List[str]):
        self.stack = deque(carIds)
        self.queue = deque()

    def check_out(self):
        if not self.stack:
            raise Exception("No car for check out")

        # Pop the car
        top = self.stack.pop()

        # Add to queue
        self.queue.append(top)

        return top


    def check_in(self):
        if not self.queue:
            raise Exception("Not car to check in")

        # Pop the left car
        leftCar = self.queue.popleft()

        # Add to stack
        self.stack.append(leftCar)

        return leftCar

    def getNextCars(self, n: int) -> List[int]:
        tempStack = self.stack
        tempQueue = self.queue

        # All the cars check-in
        while tempQueue:
            leftCar = tempQueue.popleft()
            tempStack.append(leftCar)

        result = []
        while tempStack:
            top = tempStack.pop()
            result.append(top)

        return result[:n]

def main():
    garage = PoliceParkingGarage([1,2,3,4,5])

    print(garage.check_out())

    print(garage.check_in())

    print(garage.getNextCars(3))

main()

5. Inorder Shuffle

class Solution:
    def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
        m, n = len(s1), len(s2)
        k = len(s3)

        if m + n != k:
            return False

        def dfs(i, j):
            if i == m and j == n:
                return True

            if i < m and s1[i] == s3[i + j]:
                if dfs(i + 1, j):
                    return True

            if j < n and s2[j] == s3[i + j]:
                if dfs(i, j + 1):
                    return True

            return False

        return dfs(0, 0)

def main():
    solution = Solution()

    s1 = "aaaa"
    s2 = "bbbb"
    s3 = "aabbbbaa"

    print(solution.isInterleave(s1, s2, s3))


main()



    # Top-down solution
    def isInterLeave(self, s1: str, s2: str, s3: str) -> bool:
        n = len(s1)
        m = len(s2)
        k = len(s3)

        if k != n + m:
            return False

        dp = {}

        def dfs(i, j):
            # Step 1: Base case
            if i == n and j == m:
                return True

            k = i + j

            if (i, j) in dp:
                return dp[(i, j)]

            # Step 2: Backtrack
            res = False

            if i < len(s1) and s1[i] == s3[k]:
                res = dfs(i + 1, j)

            if j < len(s2) and s2[j] == s3[k]:
                res = dfs(i, j + 1)

            dp[(i, j)] = res

            return res

        return dfs(0, 0)


    # Bottom-up solution
    def isInterLeave(self, s1: str, s2: str, s3: str) -> bool:
        m, n, k = len(s1), len(s2), len(s3)

        if m + n != k:
            return False

        # Step 1: Base case
        dp = [[False] * (n + 1) for _ in range(m + 1)]
        dp[0][0] = True

        for j in range(1, n + 1):
            dp[0][j] = dp[0][j - 1] and s2[j - 1] = s3[j - 1]

        for i in range(1, m + 1):
            dp[i][0] = dp[i - 1][0] and s1[i - 1] = s3[i - 1]

        # Step 2: Update
        for i in range(m + 1):
            for j in range(n + 1):
                dp[i][j] = (dp[i - 1][j] and s1[i - 1] == s3[i + j - 1])
                        or (dp[i][j - 1] and s2[j - 1] == s3[i + j - 1])

        return dp[m][n]

6. K Closest Points to Origin

import heapq

class Solution:
    def _distance(self, x, y):
        return x ** 2 + y ** 2

    def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
        maxHeap = []

        for x, y in points:
            # heap push
            dist = -self._distance(x, y)
            heapq.heappush(maxHeap, [dist, x, y])

            if len(maxHeap) > k:
                heapq.heappop(maxHeap)

        res = []
        while maxHeap:
            dist, x, y = heapq.heappop(maxHeap)
            res.append([x, y])

        return res


7. Number of Islands

class Solution:

    # DFS
    def numIslands(self, grid: List[List[str]]) -> int:
        directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]
        ROWS, COLS = len(grid), len(grid[0])

        def dfs(r, c):
            # Base case
            if r < 0 or r >= ROWS or c < 0 or c >= COLS or grid[r][c] == "0":
                return

            # Backtrack or add visited
            grid[r][c] = "0"
            for dr, dc in directions:
                dfs(r + dr, c + dc)

        count = 0
        for r in range(ROWS):
            for c in range(COLS):
                if grid[r][c] == "1":
                    dfs(r, c)
                    count += 1

        return count
from collections import deque

class Solution:

    # DFS
    def numIslands(self, grid: List[List[str]]) -> int:
        directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]
        ROWS, COLS = len(grid), len(grid[0])

        def bfs(r, c):
            q = deque()
            grid[r][c] = "0"
            q.append([r, c])

            while q:
                row, col = q.popleft()

                for dr, dc in directions:
                    nr, nc = row + dr, col + dc

                    # Base case
                    if nr < 0 or nr >= ROWS or nc < 0 or nc >= COLS or grid[nr][nc] == "0":
                        continue

                    # Backtrack
                    q.append([nr, nc])
                    grid[nr][nc] = "0"

        count = 0
        for r in range(ROWS):
            for c in range(COLS):
                if grid[r][c] == "1":
                    bfs(r, c)
                    count += 1

        return count


8. Valid Palindrome

class Solution:
    def alphaNum(self, c):
        return (ord('A') <= ord(c) <= ord('Z') or
                ord('a') <= ord(c) <= ord('z') or
                ord('0') <= ord(c) <= ord('9'))

    def isPalindrome(self, s: str) -> bool:
        left, right = 0, len(s) - 1
        while left <= right:
            while left < right and not self.alphaNum(s[left]):
                left += 1

            while left < right and not self.alphaNum(s[right]):
                right -= 1

            if s[left].lower() != s[right].lower():
                return False

            left += 1
            right -= 1

        return True

9. N-Queens

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        col = set()
        posDiag = set()
        negDiag = set()

        res = []
        board = [["."] * n for i in range(n)]

        def backtrack(r):
            # Base case
            if r == n:
                copy = ["".join(row) for row in board]
                res.append(copy)
                return

            for c in range(n):
                if c in col or (r + c) in posDiag or (r - c) in negDiag:
                    continue

                # Current
                col.add(c)
                posDiag.add(r + c)
                negDiag.add(r - c)
                board[r][c] = "Q"

                # Backtrack another row
                backtrack(r + 1)

                col.remove(c)
                posDiag.remove(r + c)
                negDiag.remove(r - c)
                board[r][c] = "."

        backtrack(0)
        return res

10. Roman to Integer

class Solution:
    def romanToInt(self, s: str) -> int:
        roman = {
            "I": 1,
            "V": 5,
            "X": 10,
            "L": 50,
            "C": 100,
            "D": 500,
            "M": 1000
        }

        n = len(s)
        res = 0
        for i in range(n):
            if i + 1 < len(s) and roman[s[i]] < roman[s[i + 1]]:
                res -= roman[s[i]]
            else:
                res += roman[s[i]]

        return res
class Solution:
    def intToRoman(self, num: int) -> str:
        roman = {
            "M": 1000,
            "CM": 900,
            "D": 500,
            "CD": 400,
            "C": 100,
            "XC": 90,
            "L": 50,
            "XL": 40,
            "X": 10,
            "IX": 9,
            "V": 5,
            "IV": 4,
            "I": 1
        }

        res = ""
        for key, value in roman.items():
            count = num // value

            if count:
                res += count * key
                num -= count * value

        return res

11. Edit Distance

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)

        def dfs(i, j):

            # Base case
            if i == m:
                return n - j

            if j == n:
                return m - i

            if word1[i] == word2[j]:
                return dfs(i + 1, j + 1)
            else:
                # Backtrack
                res = min(dfs(i + 1, j), dfs(i, j + 1), dfs(i + 1, j + 1))
                return res + 1

        return dfs(0, 0)
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        dp = {}

        def dfs(i, j):
            # Base case
            if i == m:
                return n - j

            if j == n:
                return m - i

            if (i, j) in dp:
                return dp[(i, j)]

            if word1[i] == word2[j]:
                dp[(i, j)] = dfs(i + 1, j + 1)
            else:
                # Backtrack
                res = min(dfs(i + 1, j), dfs(i, j + 1), dfs(i + 1, j + 1))
                dp[(i, j)] = res + 1

            return dp[(i, j)]

        return dfs(0, 0)

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        # Base case
        n, m = len(word1), len(word2)
        dp = [[0] * (m + 1) for _ in range(n + 1)]

        for i in range(n + 1):
            # Convert word[:i] to empty string
            dp[i][0] = i
        for j in range(m + 1):
            # Convert empty string to word[:j]
            dp[0][j] = j

        # Update
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
                else:
                    dp[i][j] = 1 + min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1])

        return dp[n][m]

12. Longest Substring Without Repeating Characters

  • Keep l = 0, increase right => If reach the condition, shrink the left, left += 1.
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        charSet = set()
        l = 0
        res = 0

        for r in range(len(s)):
            while s[r] in charSet:
                charSet.remove(s[l])
                l += 1

            charSet.add(s[r])
            res = max(res, r - l + 1)

        return res

13. Design Twitter

from collections import defaultdict
from collections import deque
import heapq

class Twitter:
    MAX_SIZE = 10

    def __init__(self):
        # Hashmap userId and [tweetID]
        self.tweets = defaultdict(deque)

        # Remove in hashset o(1)
        self.following = defaultdict(set)

        # Add current timestamp
        self.curr_time = 0


    # O(1)
    def postTweet(self, userId: int, tweetId: int) -> None:
        if userId not in self.tweets:
            self.tweets[userId] = deque()

        self.tweets[userId].append([self.curr_time, tweetId])

        if len(self.tweets[userId]) > self.MAX_SIZE:
            self.tweets[userId].popleft()

        self.curr_time += 1


    # Naive solution, optimize later
    def getNewsFeed(self, userId: int) -> List[int]:
        # Skip elements in small time
        minHeap = []

        # Total people he is following
        userList = list(self.following[userId]) + [userId]

        for user in userList:
            for post in self.tweets[user]:
                heapq.heappush(minHeap, post)
                if len(minHeap) > self.MAX_SIZE:
                    heapq.heappop(minHeap)

        # Get only top 10 recent heap
        result = []

        while minHeap:
            time, tweetID = heapq.heappop(minHeap)
            result.append(tweetID)

        return result[::-1]


    def follow(self, followerId: int, followeeId: int) -> None:
        if followerId == followeeId:
            return

        if followerId not in self.following:
            self.following[followerId] = set()

        self.following[followerId].add(followeeId)


    def unfollow(self, followerId: int, followeeId: int) -> None:
        if followerId == followeeId:
            return

        if followeeId not in self.following[followerId]:
            return

        self.following[followerId].discard(followeeId)

2. Template

2.1. DFS Tree

# DFS Tree

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right


def dfs(node, path, res):
    # Base case
    if node is None:
        return

    # Current
    path.append(node.val)

    # Backtrack
    if not node.left and not node.right:
        res.append(path[:])
    else:
        dfs(node.left, path, res)
        dfs(node.right, path, res)

    # Backtrack
    path.pop()


def main():
    # Build example tree
    #        1
    #       / \
    #      2   3
    #     /
    #    4

    root = TreeNode(1)
    root.left = TreeNode(2)
    root.right = TreeNode(3)
    root.left.left = TreeNode(4)

    res = []
    dfs(root, [], res)

    print("All root-to-leaf paths:")
    print(res)


if __name__ == "__main__":
    main()

2.2. BFS Tree

from collections import deque

class TreeNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

# Level traversal
def bfs(root):
    queue = deque()
    queue.append(root)

    result = []

    while queue:
        level = len(queue)

        curr = []
        for i in range(level):
            # Current
            node = queue.popleft()
            curr.append(node.val)

            # Backtrack
            if node.left:
                queue.append(node.left)

            if node.right:
                queue.append(node.right)

        result.append(curr)

    return result


def main():
    # Build example tree
    #        1
    #       / \
    #      2   3
    #     /
    #    4

    root = TreeNode(1)
    root.left = TreeNode(2)
    root.right = TreeNode(3)
    root.left.left = TreeNode(4)

    res = bfs(root)

    print("BFS Res:")
    print(res)


if __name__ == "__main__":
    main()

2.3. BFS Graph

from collections import deque

graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B'],
    'F': ['C']
}

def bfs(root):
    queue = deque()

    # Node
    queue.append(root)

    # Visited
    visited = set()

    while queue:
        node = queue.popleft()
        print(node + " ")
        visited.add(node)

        # Neighbor
        for neighbor in graph[node]:
            if neighbor not in visited:
                queue.append(neighbor)

bfs('B')

2.4. DFS Graph

from collections import deque

graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B'],
    'F': ['C']
}

visited = set()

def dfs(node):
    # Base case
    if not node:
        return

    # Node
    print(node)

    # Visited
    visited.add(node)

    # Neighbor
    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs(neighbor)

dfs('B')

2.5. BFS Matrix

from collections import deque

grid = [
    [1, 1, 0, 1, 1],
    [0, 1, 0, 1, 0],
    [1, 1, 1, 1, 1],
    [0, 0, 0, 0, 0],
    [1, 1, 1, 1, 1]
]

ROWS, COLS = len(grid), len(grid[0])

def bfs(r, c):
    # queue
    queue = deque()
    # visited
    visited = set()

    directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]

    queue.append((r, c))

    # current
    while queue:
        row, col = queue.popleft()
        visited.add((r, c))
        print(f"({row}, {col})")

        # neighbor
        for dr, dc in directions:
            nr, nc = r + dr, c + dc

            if nr < 0 or nr >= ROWS or nc < 0 or nc >= COLS:
                continue

            if (nr, nc) not in visited and grid[nr][nc] == 1:
                queue.append((nr, nc))
                visited.add((nr, nc))


for i in range(ROWS):
    for j in range(COLS):
        if grid[i][j] == 1:
            bfs(i, j)

2.6. DFS Matrix

from collections import deque

grid = [
    [1, 1, 0, 1, 1],
    [0, 1, 0, 1, 0],
    [1, 1, 1, 1, 1],
    [0, 0, 0, 0, 0],
    [1, 1, 1, 1, 1]
]

ROWS, COLS = len(grid), len(grid[0])
count = 0
visited = set()
directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]

def dfs(r, c):
    # Base case
    if r < 0 or r >= ROWS or c < 0 or c >= COLS or grid[r][c] == 0 or (r, c) in visited:
        return

    # Node
    # Visited
    visited.add((r, c))

    # Neighbor
    for dr, dc in directions:
        nr, nc = r + dr, c + dc
        dfs(nr, nc)


for i in range(ROWS):
    for j in range(COLS):
        if grid[i][j] == 1 and (i, j) not in visited:
            dfs(i, j)
            count += 1

print(count)


arr = [1, 2, 3, 4, 5]

def binary_search(arr, target):
    n = len(arr)
    left, right = 0, n - 1

    while left < right:
        mid = left + (right - left) // 2

        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return left


print(binary_search(arr, 7))

2.7. Binary Search Left Most

arr = [1, 2, 3, 3, 4, 4, 4, 5]

def binary_search(arr, target):
    n = len(arr)
    left, right = 0, n - 1

    while left <= right:
        mid = left + (right - left) // 2

        if arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return left


print(binary_search(arr, 4))

2.8. Binary Search Right Most

arr = [1, 2, 3, 3, 4, 4, 4, 5]

def binary_search(arr, target):
    n = len(arr)
    left, right = 0, n - 1

    while left <= right:
        mid = left + (right - left) // 2

        if arr[mid] <= target:
            left = mid + 1
        else:
            right = mid - 1

    return left - 1


print(binary_search(arr, 4))

2.9. Remove Duplicates

arr = [1,2,3,3,4,5]

def remove_duplicates(arr):
    left = 0
    n = len(arr)

    for right in range(n):
        if right > 0 and arr[right] == arr[right - 1]:
            continue

        arr[left] = arr[right]
        left += 1

    return arr[:left]


arr = remove_duplicates(arr)
print(arr)

2.10. Two Sum

arr = [1,2,3,3,4,5]
target = 5

def two_sum(arr, target):
    n = len(arr)
    left, right = 0, n - 1

    res = []
    while left <= right:
        if arr[left] + arr[right] == target:
            res.append([arr[left], arr[right]])
            left += 1
            right -= 1
        elif arr[left] + arr[right] < target:
            left += 1
        else:
            right -= 1

    return res


print(two_sum(arr, target))

2.11. Sliding Window (Fixed Size)

nums = [1,3,2,1,2,5]
k = 3

def max_sum_size_k(nums, k):
    left = 0
    n = len(nums)

    curr = 0
    maxSum = 0
    for right in range(n):
        curr += nums[right]

        if right - left + 1 == k:
            maxSum = max(maxSum, curr)
            curr -= nums[left]
            left += 1

    return maxSum

print(max_sum_size_k(nums, k))

2.12. Longest Substring Without Repeating Characters

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        left = 0
        n = len(s)
        charSet = set()
        maxLength = 0

        for right in range(n):
            while s[right] in charSet:
                charSet.discard(s[left])
                left += 1

            charSet.add(s[right])
            maxLength = max(maxLength, right - left + 1)

        return maxLength

2.13. Top K - Max Heap

import heapq

arr = [2,1,4,3,6,5,8,7]
k = 3

def maxHeapTopK(arr, k):
    # Get 3 smallest value
    maxHeap = []

    for i, num in enumerate(arr):
        heapq.heappush(maxHeap, [-num, i])
        if len(maxHeap) > k:
            heapq.heappop(maxHeap)

    result = []
    while maxHeap:
        val, i = heapq.heappop(maxHeap)
        result.append(-val)

    return result[::-1]


def minHeapTopK(arr, k):
    # Get 3 largest value
    return


print(maxHeapTopK(arr, k))

2.14. Top K - Min Heap

import heapq

arr = [2,1,4,3,6,5,8,7]
k = 3

def maxHeapTopK(arr, k):
    # Get 3 smallest value
    maxHeap = []

    for i, num in enumerate(arr):
        heapq.heappush(maxHeap, [-num, i])
        if len(maxHeap) > k:
            heapq.heappop(maxHeap)

    result = []
    while maxHeap:
        val, i = heapq.heappop(maxHeap)
        result.append(-val)

    return result[::-1]


def minHeapTopK(arr, k):
    # Get 3 largest value
    minHeap = []

    for i, num in enumerate(arr):
        heapq.heappush(minHeap, [num, i])
        if len(minHeap) > k:
            heapq.heappop(minHeap)

    result = []
    while minHeap:
        val, i = heapq.heappop(minHeap)
        result.append(val)

    return result[::-1]


print(minHeapTopK(arr, k))

2.15. Monotonic Stack

Note: While stack, pop it.

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        stack = []
        n = len(temperatures)

        result = [0] * n
        for i in range(n):
            while stack and temperatures[i] > temperatures[stack[-1]]:
                result[stack[-1]] = i - stack[-1]
                stack.pop()

            stack.append(i)

        return result

3. System Design - Physical Envidences

3.1. Context

Precinct
 └── LockerSet
      └── Locker
           └── Box
                └── Evidence

Case
 └── Box (1-to-many)
User
 └── Role / Permission

3.2. High Level Design

3.3. API Design

POST /cases
POST /cases/{caseId}/boxes
POST /evidence/checkin
POST /evidence/checkout
POST /permissions/grant
POST /permissions/revoke
GET  /cases/{caseId}/audit

3.4. Data Model

  • users:
user_id (PK)
role ENUM(DETECTIVE, CHIEF, ADMIN)
precinct_id

  • lockers:
locker_id (PK)
locker_set_id
size ENUM(S,M,L)
status ENUM(AVAILABLE, OCCUPIED, MAINTENANCE)
  • boxes:
box_id (PK)
case_id (FK)
locker_id (FK)
status ENUM(IN, OUT)
  • evidence (USB, documents)
evidence_id (PK)
box_id (FK)
description
December 16, 2025