Big-O • Arrays & Hashing • Linked Lists & Stacks • Trees & Graphs • Dynamic Programming • 2026

Data Structures and Algorithms Interview Questions

32 questions What each one tests, an answer frame, a spoken answer 40 min read

This page is for anyone facing a data structures and algorithms round, from a first job to a senior engineering role. Most rounds check whether you can state a brute force, spot the repeated work and reach a faster solution, then give its time and space cost. The questions run from Big-O and arrays through hashing, linked lists, stacks, trees, heaps and graphs to recursion and dynamic programming, and finish with real-work stories and judgement calls under time pressure. Every coding answer shows the brute force first, then a short Python solution with its cost. Say the answers out loud, then solve each problem yourself without looking.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Big-O Analysis 3 questions

Easy Technical round Fresher Practice question

1. How do you work out the Big-O of a piece of code? Take a loop inside a loop, and a loop that halves n each time.

What the interviewer is really testing:
Whether you can derive complexity from code rather than recite it, because every solution you write in a round needs its time and space cost stated.
Answer frame:

Count the work: how often the innermost step runs as n grows; drop constants and smaller terms.

Nested loops: two loops over n that each run fully give n times n, so O(n^2).

Halving: a loop that halves n each pass runs about log n times, so O(log n).

Space too: count the extra memory, including the recursion stack.

Sample spoken answer:

"I ask how many times the innermost line runs as the input grows, then drop constants and lower-order terms, because Big-O is about the growth rate, not exact counts. If I have a loop over n items and inside it another loop over n items, the inner line runs n times n, so it's O(n squared). If the inner loop only goes from i to n, it runs about half as often, but that's still O(n squared). A loop where n is halved each pass, like binary search, runs about log n times before it reaches one, so that's O(log n). Two loops one after the other are O(n) plus O(n), which is just O(n). I also state space separately: what extra memory I allocate and, for recursion, how deep the call stack gets."

Code:
def pairs(a):              # O(n^2) time, O(1) extra space
    count = 0
    for i in range(len(a)):
        for j in range(i + 1, len(a)):
            count += 1
    return count

def halvings(n):           # O(log n) time
    steps = 0
    while n > 1:
        n //= 2
        steps += 1
    return steps
Red flag to avoid:

Counting loops instead of work, for example calling any code with two loops O(n^2) even when they run one after the other.

They may ask next:
  • What is the Big-O of an outer loop where i doubles up to n, with an inner loop that runs i times?
  • Why do we usually quote the worst case rather than the average case?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

2. Appending to a dynamic array sometimes copies every element. Why is append still called O(1)?

What the interviewer is really testing:
Whether you understand amortized cost, which sits behind every growable list, resizing hash table and array-backed stack you use.
Answer frame:

Growth: when full, the array allocates a bigger block, a constant factor larger, and copies everything.

Averaging: with doubling, the total copy work over n appends is under 2n, so each append is O(1) on average.

Caveat: one append can still take O(n); amortized is not a per-call guarantee.

Sample spoken answer:

"A dynamic array, like a Python list, keeps spare capacity. Most appends just write into the next free slot, which is constant time. When it's full, it allocates a bigger block and copies everything across, which is O(n) for that one call. The key is that it grows by a multiple, not by a fixed amount. If it doubles, then over n appends the copies add up to roughly n plus n over two plus n over four and so on, which is less than 2n. Spread over n appends, that's a constant per append, so we call it amortized O(1). If it grew by a fixed ten slots each time instead, the copying would add up to O(n squared) overall. I'd also say that amortized isn't a guarantee for each call, so in latency-sensitive code one slow append can still matter."

Red flag to avoid:

Saying append is always O(1) with no mention of resizing, or calling it O(n) because of the occasional copy.

They may ask next:
  • Should the array shrink when you remove elements, and how would you avoid growing and shrinking over and over?
  • Where else does amortized analysis show up, for example in a hash table?
Say it in 60 seconds
Medium Situational round Fresher, Mid-level, Senior Practice question

3. The input can have a million items and your first idea is O(n^2). How do you decide what complexity you need before coding?

What the interviewer is really testing:
Whether you use the input limits to set a target complexity and reject a plan early, instead of coding something that can never finish in time.
Answer frame:

Estimate: a million squared is a trillion steps, far too slow; n log n is about twenty million.

Target: n near a million needs O(n) or O(n log n); a few thousand allows O(n^2).

Map to tools: n log n suggests sorting, a heap or binary search; O(n) suggests a hash map, two pointers or one sweep.

Say it: state the brute force and its cost, then the target, then the idea that reaches it.

Sample spoken answer:

"I start from the limits, because they tell me the target. A million squared is a trillion basic steps, which would take far too long in any language. A million times log of a million is about twenty million, which is comfortable. Simple operations run somewhere in the tens to hundreds of millions per second, slower in Python, so with n around a million I need O(n) or O(n log n). If n were only a few thousand, O(n squared) would be fine and simpler to write. So out loud I'd say: the brute force is O(n squared), it won't handle a million items, and I'm aiming for n log n, which points to sorting, a heap or binary search, or O(n), which points to a hash map, two pointers or a single sweep. That stops me spending twenty minutes on a solution that was never going to pass."

Red flag to avoid:

Coding the O(n^2) solution first and only then asking how big the input can be.

They may ask next:
  • If n is at most 20, what kinds of solutions become possible that wouldn't be otherwise?
  • How would a tight memory limit change your choice, for example a hash map over a huge number of keys?
Say it in 60 seconds

Arrays & Hashing 4 questions

Easy Coding round Fresher Practice question

4. Given a list of numbers and a target, return the indexes of two numbers that add up to the target. Start with brute force.

What the interviewer is really testing:
Whether you can start with a working brute force, spot the repeated work, and trade memory for time with a hash map.
Answer frame:

Brute force: check every pair, O(n^2) time, O(1) space.

Insight: for each number the partner you need is target minus it, and a dict answers that in O(1).

One pass: check the dict first, then store the value and its index; O(n) time, O(n) space.

Edge cases: duplicates, using the same element twice, no answer.

Sample spoken answer:

"The brute force is two loops checking every pair, which is O(n squared) time and constant space. The wasted work is that for each number I scan the whole list for one specific partner, target minus that number. A hash map answers that in constant time on average. So I walk the list once, and for each number I compute the complement and check whether I've already seen it. If yes, I return the stored index and the current one. If not, I store the current number with its index and move on. Checking before storing means I never pair an element with itself, and duplicates like three plus three work because the first three is already in the map when I reach the second. That's O(n) time and O(n) space."

Code:
def two_sum_brute(nums, target):      # O(n^2) time, O(1) space
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return [i, j]
    return []

def two_sum(nums, target):            # O(n) time, O(n) space
    seen = {}                         # value -> index
    for i, x in enumerate(nums):
        if target - x in seen:
            return [seen[target - x], i]
        seen[x] = i
    return []
Red flag to avoid:

Jumping to the hash map without stating the brute force, or storing before checking and pairing an element with itself.

They may ask next:
  • If the list were already sorted, how would you solve it with O(1) extra space?
  • How would you return every pair that works, without repeating a pair?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

5. How does a hash table give average O(1) lookups, and what can make it slow down to O(n)?

What the interviewer is really testing:
Whether you know what sits behind a dict or set well enough to reason about its cost, rather than treating O(1) as a promise.
Answer frame:

Hashing: a hash function turns the key into an index into an array of slots.

Collisions: handled by chaining (a list per slot) or open addressing (probe for another slot).

Load factor: when the table gets too full it grows and rehashes, so each slot stays short.

Worst case: a poor hash or keys chosen to collide pile into one place, so lookups become O(n).

Sample spoken answer:

"A hash table is an array of slots. To store a key, I run it through a hash function and map the result to a slot index. A lookup does the same calculation, so it jumps straight to the right slot instead of searching. Two keys can land on the same slot, which is a collision. One fix is chaining, where each slot holds a small list. Another is open addressing, where you probe for another free slot, which is what Python's dict does. The table tracks how full it is, and past a threshold it grows and rehashes everything, so on average only a few keys compete for any slot. That's why lookups are average O(1). The worst case is O(n): with a poor hash function, or keys someone picked to collide, everything piles up in one place and a lookup becomes a linear scan."

Red flag to avoid:

Saying lookups are always O(1), or not knowing what happens when two keys land on the same slot.

They may ask next:
  • Why must a key never change its hash while it's stored in the table?
  • When would you pick a balanced search tree over a hash table?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

6. Find the length of the longest substring with no repeated characters. How do you get it below O(n^2)?

What the interviewer is really testing:
Whether you recognise the sliding window pattern and can keep the window valid without restarting the scan.
Answer frame:

Brute force: from every start, extend with a set until a repeat; O(n^2).

Window: keep a left and a right edge; the right edge moves forward every step.

Jump left: remember each character's last index; on a repeat inside the window, move left just past it.

Cost: O(n) time, O(k) space for k distinct characters.

Sample spoken answer:

"Brute force, I'd start at every index and extend to the right with a set until I hit a repeat, which is O(n squared). The waste is that after a repeat I restart from the next index and re-scan characters I already know are fine. So I use a sliding window. I keep a left edge and move the right edge forward one character at a time, and I store the last index where I saw each character. If the current character was last seen inside my window, I move the left edge to just past that position, because any window holding both copies is invalid. Now the window has no repeats, and I track the biggest length. Each character is handled once, so it's O(n) time, and space is O(k), where k is the number of distinct characters."

Code:
def longest_unique(s):                # O(n) time, O(k) space
    last = {}                         # char -> last index seen
    left = best = 0
    for right, ch in enumerate(s):
        if ch in last and last[ch] >= left:
            left = last[ch] + 1
        last[ch] = right
        best = max(best, right - left + 1)
    return best
Red flag to avoid:

Moving the left edge backwards because of an old repeat that's already outside the window, which silently gives wrong lengths.

They may ask next:
  • How would you change it to allow at most two distinct characters in the window?
  • Why does the check need last[ch] >= left rather than just ch in last?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

7. Find all unique triplets in an array that sum to zero. Why does sorting the array first help?

What the interviewer is really testing:
Whether you can apply two pointers on a sorted array and handle duplicates cleanly, which is where most attempts go wrong.
Answer frame:

Brute force: three nested loops plus a set to remove repeats, O(n^3).

Sort, then fix one: for each i, find a pair in the rest that sums to minus nums[i].

Pointers move by sum: too small, move left up; too big, move right down.

Skip duplicates: at i and after each match, so each triplet appears once; O(n^2) time.

Sample spoken answer:

"Brute force is three loops, O(n cubed), plus a set of sorted tuples to remove repeated triplets. Sorting first changes things. I fix the first number at index i, and now I need two numbers after it that sum to minus that value. Because the array is sorted, I can use two pointers, one just after i and one at the end. If the sum is too small, I move the left pointer right to make it bigger; if it's too big, I move the right pointer left. That pair search is O(n), and I do it for each i, so O(n squared) overall, which outweighs the n log n sort. Duplicates are the tricky part: I skip i when it equals the previous value, and after a match I move the left pointer past equal values. Extra space is small apart from the output and the sort."

Code:
def three_sum(nums):                  # O(n^2) time
    nums.sort()
    out = []
    for i in range(len(nums) - 2):
        if i > 0 and nums[i] == nums[i - 1]:
            continue
        lo, hi = i + 1, len(nums) - 1
        while lo < hi:
            s = nums[i] + nums[lo] + nums[hi]
            if s < 0:
                lo += 1
            elif s > 0:
                hi -= 1
            else:
                out.append([nums[i], nums[lo], nums[hi]])
                lo += 1
                hi -= 1
                while lo < hi and nums[lo] == nums[lo - 1]:
                    lo += 1
    return out
Red flag to avoid:

Finding the O(n^2) idea but returning duplicate triplets, or hiding the problem with a set of tuples you can't explain.

They may ask next:
  • How would you change it to find the triplet whose sum is closest to a given target?
  • Could you use a hash set instead of two pointers, and what would you lose?
Say it in 60 seconds

Lists & Stacks 6 questions

Easy Technical round Fresher Practice question

8. When would you choose a linked list over an array? Compare the cost of access, insert and delete.

What the interviewer is really testing:
Whether you know the real trade-off, including that a cheap linked-list insert hides the cost of finding the position, and how memory layout affects speed.
Answer frame:

Access: array O(1) by index; linked list O(n), walking from the head.

Insert/delete: array O(n) in the middle because items shift; linked list O(1) once you hold the node.

Memory: arrays are contiguous and cache-friendly; nodes carry pointers and scatter in memory.

Pick: arrays by default; linked lists when you splice a lot at nodes you already hold.

Sample spoken answer:

"An array stores items side by side, so reading index i is O(1). Inserting or deleting in the middle is O(n) because everything after it has to shift, while appending to a dynamic array is amortized O(1). A linked list stores nodes that point to the next one. Reaching the i-th item is O(n) because I walk from the head, but once I'm holding a node, inserting or removing next to it is O(1), just a few pointer changes. The catch people miss is that finding the node is usually O(n) anyway, and arrays are much friendlier to the CPU cache because the data is contiguous. So in practice I default to an array. I reach for a linked list when I keep inserting and removing at nodes I already have a reference to, like the recency order in an LRU cache."

Red flag to avoid:

Saying linked lists are faster for insertion without mentioning that you first have to find the position.

They may ask next:
  • What does a doubly linked list give you that a singly linked list doesn't?
  • How would you remove an arbitrary item from a linked list in O(1)?
Say it in 60 seconds
Easy Coding round Fresher Practice question

9. Reverse a singly linked list. Do it iteratively first, then tell me how the recursive version differs.

What the interviewer is really testing:
Whether you can move pointers without losing the rest of the list, and know the hidden stack cost of recursion.
Answer frame:

Brute force: copy values to an array and write them back reversed; O(n) extra space.

Three pointers: prev, current and a saved next; point current back at prev, then advance.

Cost: iterative O(n) time, O(1) space; recursive O(n) space for the call stack.

Sample spoken answer:

"A simple brute force copies the values into an array and writes them back in reverse, which is O(n) time but O(n) extra space. I can do it in place with three pointers. I keep prev, starting at None, and current, starting at the head. At each step I first save current's next, because I'm about to overwrite it. Then I point current's next back at prev, move prev up to current, and move current to the saved next. When current is None, prev is the new head. That's O(n) time and O(1) extra space. The recursive version reverses the rest of the list first, then makes the next node point back at the current one. It's neat, but every node adds a stack frame, so it's O(n) space, and on a very long list Python would hit its recursion limit."

Code:
class Node:
    def __init__(self, val, next=None):
        self.val, self.next = val, next

def reverse(head):                  # O(n) time, O(1) space
    prev, cur = None, head
    while cur:
        nxt = cur.next
        cur.next = prev
        prev, cur = cur, nxt
    return prev
Red flag to avoid:

Overwriting next before saving it, which cuts off the rest of the list.

They may ask next:
  • How would you reverse only the nodes between positions m and n?
  • How would you check whether a linked list is a palindrome using O(1) extra space?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

10. How do you detect a cycle in a linked list, and how do you find the node where the cycle begins?

What the interviewer is really testing:
Whether you know the fast and slow pointer technique and can explain why it works, not just recite it.
Answer frame:

Brute force: a set of visited nodes; O(n) time, O(n) space.

Floyd: slow moves one step, fast moves two; they meet only if there's a cycle.

Find the start: reset one pointer to the head, move both one step at a time; they meet at the cycle start.

Sample spoken answer:

"The easy version keeps a set of nodes I've visited. If I reach a node twice, there's a cycle, and that node is where it starts. That's O(n) time and O(n) space. To get O(1) space I use two pointers: slow moves one step, fast moves two. If there's no cycle, fast runs off the end. If there is one, fast eventually catches slow inside the loop, because the gap between them closes by one each step, so they must meet. To find where the cycle starts, I put one pointer back at the head and leave the other at the meeting point, then move both one step at a time. They meet exactly at the start of the cycle, because the distance from the head to the start matches the distance from the meeting point forward to the start, give or take whole laps of the cycle."

Code:
def cycle_start(head):              # O(n) time, O(1) space
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:
            slow = head
            while slow is not fast:
                slow, fast = slow.next, fast.next
            return slow
    return None                     # no cycle
Red flag to avoid:

Comparing node values instead of node identity, which breaks as soon as two nodes hold the same value.

They may ask next:
  • How would you find the length of the cycle once you know there is one?
  • Where else do fast and slow pointers help, for example finding the middle of a list?
Say it in 60 seconds
Easy Coding round Fresher Practice question

11. Check whether a string of brackets like '([]{})' is balanced. Which data structure fits, and why?

What the interviewer is really testing:
Whether you see that the most recently opened bracket must close first, which is exactly the last in, first out order a stack gives.
Answer frame:

Why a stack: the last bracket opened must be the first one closed.

Scan: push openers; on a closer, the top must be its matching opener, or fail.

End check: the stack must be empty; O(n) time, O(n) space.

Sample spoken answer:

"A tempting brute force is to keep deleting adjacent pairs like '()' until nothing changes, which works but can be O(n squared). The better fit is a stack, because brackets nest: the most recently opened one has to close first, which is last in, first out. I scan left to right. When I see an opener, I push it. When I see a closer, the stack must not be empty and its top must be the matching opener; if so I pop, and if not the string is unbalanced. At the end the stack must be empty, otherwise something was opened and never closed. A simple counter isn't enough once there are different bracket types, because '([)]' has the right counts in the wrong order. It's O(n) time and O(n) space in the worst case, like a string of only openers."

Code:
def balanced(s):                    # O(n) time, O(n) space
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in s:
        if ch in '([{':
            stack.append(ch)
        elif ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
    return not stack
Red flag to avoid:

Only counting openers and closers, which wrongly accepts '([)]'.

They may ask next:
  • If there were only one kind of bracket, how would you do it in O(1) space?
  • How would you return the position of the first bracket that breaks the balance?
Say it in 60 seconds
Easy Coding round Fresher, Mid-level Practice question

12. Build a first-in, first-out queue using only two stacks. What does each operation cost?

What the interviewer is really testing:
Whether you understand how stack and queue order differ, and can explain why the occasional expensive move still averages out to constant time.
Answer frame:

Brute force: on every dequeue, pour everything into the second stack, pop, and pour it back; O(n) per dequeue.

Two roles: an in stack takes every push; an out stack serves every pop.

Refill only when empty: move everything from in to out just when out runs dry, which reverses the order once.

Cost: each item is moved at most once, so push and pop are amortized O(1); O(n) space.

Sample spoken answer:

"A stack gives back the newest item and a queue the oldest, so I need to flip the order. The brute force pours the whole stack into the second one on every dequeue, pops the bottom item, and pours everything back, which is O(n) per dequeue. The better version gives the stacks fixed roles. Every enqueue pushes onto the in stack. Every dequeue pops from the out stack, and only when the out stack is empty do I move everything across from the in stack. That single move reverses the order, so the oldest item ends up on top. Each item is pushed and popped at most twice in its life, so over any run of operations the cost averages to O(1) each, even though one dequeue can take O(n). In real Python code I'd just use collections.deque, but this shows I understand both structures."

Code:
class TwoStackQueue:                 # amortized O(1) per operation
    def __init__(self):
        self.inbox, self.outbox = [], []

    def push(self, x):
        self.inbox.append(x)

    def pop(self):
        if not self.outbox:
            while self.inbox:        # reverse the order once
                self.outbox.append(self.inbox.pop())
        return self.outbox.pop()     # raises IndexError if empty
Red flag to avoid:

Moving every item back and forth on each dequeue and calling it O(1), or not being able to explain why the cost averages out.

They may ask next:
  • How would you build a stack using only queues, and what would that cost?
  • How would you add a peek operation that doesn't remove the item?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

13. Design an LRU cache where get and put both run in O(1). Which data structures do you combine?

What the interviewer is really testing:
Whether you can combine two structures so each covers the other's weakness, and keep them in sync on every operation.
Answer frame:

Hash map: key to node, for O(1) lookup.

Doubly linked list: order by recency; move a node to the recent end in O(1), evict from the old end in O(1).

Keep in sync: get moves the node; put inserts or updates, then evicts from both past capacity.

In Python: OrderedDict already pairs a dict with a linked list.

Sample spoken answer:

"A naive version keeps a plain list ordered by use, but finding an item and moving it is O(n). For O(1) I combine two structures. A hash map from key to node gives constant-time lookup. A doubly linked list keeps items in order of use, most recent at one end and least recent at the other. Because it's doubly linked and the map hands me the node, I can unlink it and move it to the recent end in O(1), and evicting is just removing the node at the old end. On get, I look up the node, move it to the recent end and return the value. On put, I insert or update at the recent end, and if I'm over capacity I remove the oldest node and delete its key from the map. In Python, OrderedDict is exactly that pairing, so I'd show it and offer to write the list by hand."

Code:
from collections import OrderedDict

class LRUCache:                       # get/put O(1), space O(capacity)
    def __init__(self, capacity):
        self.cap = capacity
        self.data = OrderedDict()

    def get(self, key):
        if key not in self.data:
            return -1
        self.data.move_to_end(key)    # most recent at the end
        return self.data[key]

    def put(self, key, value):
        self.data[key] = value
        self.data.move_to_end(key)
        if len(self.data) > self.cap:
            self.data.popitem(last=False)    # evict least recent
Red flag to avoid:

Storing a timestamp per key and scanning for the oldest one on every eviction, which is O(n).

They may ask next:
  • Why does the list need to be doubly linked rather than singly linked?
  • How would you make this cache safe to use from several threads at once?
Say it in 60 seconds

Trees & Heaps 5 questions

Medium Coding round Fresher, Mid-level Practice question

14. Return a binary tree's values level by level, one list per level. What do you use, and what does it cost?

What the interviewer is really testing:
Whether you know that breadth-first order needs a queue, and can separate the levels cleanly.
Answer frame:

Brute force: for each depth, walk the whole tree collecting that depth; O(n times h).

Queue: BFS with a deque; pop from the left, push children on the right.

Level size: read the queue length at the start of a level and process exactly that many.

Cost: O(n) time; O(w) space for the widest level.

Sample spoken answer:

"A brute force would find the height, then for each depth run a walk that collects the nodes at that depth, which is O(n times h), and O(n squared) on a skewed tree. The natural fit is breadth-first search with a queue. I put the root in a deque. While the queue isn't empty, I read how many nodes are in it right now, and that's exactly one level. I pop that many from the left, record their values and push their children on the right. When that count is done, I've finished one level and add its list to the result. Each node enters and leaves the queue once, so it's O(n) time. Space is the widest level, which in a full tree is about half the nodes. I use a deque rather than a plain list, because popping from the front of a list is O(n)."

Code:
from collections import deque

def level_order(root):              # O(n) time, O(w) space
    if not root:
        return []
    out, q = [], deque([root])
    while q:
        level = []
        for _ in range(len(q)):
            node = q.popleft()
            level.append(node.val)
            if node.left:
                q.append(node.left)
            if node.right:
                q.append(node.right)
        out.append(level)
    return out
Red flag to avoid:

Using a stack or plain recursion and getting depth-first order, or popping from the front of a Python list in a loop.

They may ask next:
  • How would you return the levels in zigzag order, left to right and then right to left?
  • How would you return just the rightmost value on each level?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

15. Check whether a binary tree is a valid binary search tree. Why isn't comparing each node with its children enough?

What the interviewer is really testing:
Whether you understand that the BST rule covers whole subtrees, not just parent and child, and can carry that limit down the tree.
Answer frame:

The trap: a node can be fine next to its parent but break a limit set by an ancestor higher up.

Bounds: pass down a low and a high limit; each node must sit strictly between them.

Alternative: an in-order walk of a valid BST is strictly increasing.

Cost: O(n) time, O(h) space for the recursion.

Sample spoken answer:

"The rule is that everything in the left subtree is smaller than the node and everything in the right subtree is larger, not just the direct children. For example, root 10, left child 5, and 5's right child 12. Twelve is bigger than 5, so a local check passes, but it sits in 10's left subtree, so the tree is invalid. A brute force checks, for every node, the largest value on its left and the smallest on its right, which is O(n squared) on a skewed tree. Instead I pass bounds down. The root can be anything. Going left, the node's value becomes the upper bound; going right, it becomes the lower bound. Every node must sit strictly inside its bounds. That's O(n) time and O(h) space, where h is the height. Another clean way is an in-order walk, checking that each value beats the previous one."

Code:
def is_bst(node, lo=float('-inf'), hi=float('inf')):   # O(n) time, O(h) space
    if node is None:
        return True
    if not (lo < node.val < hi):
        return False
    return (is_bst(node.left, lo, node.val) and
            is_bst(node.right, node.val, hi))
Red flag to avoid:

Checking only that the left child is smaller and the right child is bigger at each node.

They may ask next:
  • How would your answer change if the tree may hold duplicate values?
  • How would you find the k-th smallest value in a BST?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

16. Find the lowest common ancestor of two nodes in a binary tree. How would your answer change if the tree were a binary search tree?

What the interviewer is really testing:
Whether you can let a recursion report what it found in each subtree and combine the results, and use the BST ordering when it is there.
Answer frame:

Brute force: record the path from the root to each node, then take the last node the two paths share; O(n) time, O(n) extra space.

One recursion: each call returns a target it found below, or None; the first node where both sides return something is the answer.

BST shortcut: from the root, go left while both values are smaller, right while both are bigger; the split point is the answer, O(h).

Cost: general tree O(n) time and O(h) stack; BST O(h) time.

Sample spoken answer:

"The straightforward way is to find the path from the root down to each node, then walk both paths together and take the last node they share. That's O(n) time but it stores two paths. A single recursion does it in one pass. Each call returns the target it found in its subtree, or None. If the current node is one of the targets, it returns itself. Otherwise it asks both children. If both sides come back with something, the two nodes are split across this node, so it's the lowest common ancestor. If only one side found something, it passes that up. That's O(n) time and O(h) space for the stack. In a binary search tree I can use the ordering instead: starting at the root, if both values are smaller I go left, if both are bigger I go right, and the first node where they split, or that equals one of them, is the answer. That's O(h), with no recursion needed."

Code:
def lca(node, p, q):                 # binary tree: O(n) time, O(h) space
    if node is None or node is p or node is q:
        return node
    left = lca(node.left, p, q)
    right = lca(node.right, p, q)
    if left and right:
        return node                  # p and q split here
    return left or right

def lca_bst(node, p, q):             # BST: O(h) time, O(1) space
    while node:
        if p.val < node.val and q.val < node.val:
            node = node.left
        elif p.val > node.val and q.val > node.val:
            node = node.right
        else:
            return node
Red flag to avoid:

Using the BST shortcut on a plain binary tree, or returning the first node where one target is found without checking the other side.

They may ask next:
  • What goes wrong if one of the two nodes might not be in the tree at all, and how would you fix it?
  • If every node had a pointer to its parent, how would you solve it without starting from the root?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

17. Find the k-th largest number in a large list. Compare sorting the list with using a heap.

What the interviewer is really testing:
Whether you know how a heap works and can choose the heap size and direction that keeps the cost at n log k.
Answer frame:

Sort: O(n log n); fine for small inputs and easy to read.

Min-heap of size k: push each number, pop the smallest once the size passes k; the top is the answer.

Why a min-heap: the smallest of the k biggest is the one to evict; O(n log k) time, O(k) space.

Heap basics: a complete tree stored in an array; push and pop O(log size), peek O(1).

Sample spoken answer:

"The simplest answer is to sort in descending order and take position k minus one. That's O(n log n) and perfectly fine for small lists. When n is large and k is small, I use a min-heap of size k. A heap is a complete binary tree stored in an array where every parent is no bigger than its children, so the minimum is always at the top, and push and pop cost O(log) of the heap size. I push each number, and whenever the heap grows past k, I pop the smallest. At the end the heap holds the k largest numbers, and its top is the k-th largest. That's O(n log k) time and O(k) space, and it also works on a stream I can't hold in memory. A min-heap for the largest sounds backwards, but its minimum is exactly the item I want to throw out."

Code:
import heapq

def kth_largest_sort(nums, k):        # O(n log n)
    return sorted(nums, reverse=True)[k - 1]

def kth_largest(nums, k):             # O(n log k) time, O(k) space
    heap = []
    for x in nums:
        heapq.heappush(heap, x)
        if len(heap) > k:
            heapq.heappop(heap)
    return heap[0]
Red flag to avoid:

Using a max-heap of k items so the top is the wrong one to evict, or not knowing that Python's heapq is a min-heap.

They may ask next:
  • What is quickselect, and what are its average and worst-case costs?
  • Why does building a heap from an existing array take O(n) rather than O(n log n)?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

18. You have k sorted lists with n items in total. Merge them into one sorted list efficiently.

What the interviewer is really testing:
Whether you can use a heap to pick the next smallest item across many sources, and state the cost in terms of both n and k.
Answer frame:

Brute force: concatenate and sort, O(n log n); or merge one list at a time, up to O(n times k).

Heap of heads: hold the current front item of each list; pop the smallest, push that list's next item.

Cost: each item passes through a heap of size k once, so O(n log k) time and O(k) extra space.

Ties: store (value, list index, position) so ties never compare the items themselves.

Sample spoken answer:

"The quick answer is to put everything in one list and sort it, which is O(n log n) and ignores that the inputs are already sorted. Merging the lists one at a time into a growing result is worse, up to O(n times k), because early items get copied again and again. The better way is a min-heap holding the front item of each list. I pop the smallest, add it to the output, and push the next item from the same list. The heap never holds more than k items, so each push and pop is O(log k), and each of the n items goes through once, giving O(n log k) time and O(k) extra space besides the output. I store tuples of value, list index and position, so ties break on the index, which matters when the items are linked-list nodes that Python can't compare."

Code:
import heapq

def merge_k(lists):                   # O(n log k) time, O(k) heap
    heap = [(lst[0], i, 0) for i, lst in enumerate(lists) if lst]
    heapq.heapify(heap)
    out = []
    while heap:
        val, i, j = heapq.heappop(heap)
        out.append(val)
        if j + 1 < len(lists[i]):
            heapq.heappush(heap, (lists[i][j + 1], i, j + 1))
    return out
Red flag to avoid:

Merging the lists one at a time into a growing result and calling it O(n).

They may ask next:
  • If the lists are too big for memory and live in files on disk, what changes?
  • How does merging the lists in pairs, like the merge step of merge sort, compare with the heap?
Say it in 60 seconds

Graphs 4 questions

Easy Technical round Fresher, Mid-level Practice question

19. When would you use breadth-first search over depth-first search on a graph, and what does each one cost?

What the interviewer is really testing:
Whether you pick a traversal for a reason, above all that BFS gives the shortest path when every edge costs the same.
Answer frame:

BFS: a queue, explores in rings by distance; gives the fewest-edges path in an unweighted graph.

DFS: a stack or recursion, goes deep first; suits cycle checks, topological order, components, backtracking.

Cost: both O(V + E) time with an adjacency list; mark nodes visited to avoid loops.

Memory: BFS holds the whole frontier; DFS holds the current path, and deep recursion can overflow.

Sample spoken answer:

"Both visit every reachable node and edge once, so with an adjacency list they're O(V plus E) time, plus O(V) space for the visited set. The difference is the order. BFS uses a queue and explores in rings: everything one edge away, then two edges away, and so on. That makes it the right choice for the shortest path when every edge costs the same, like the fewest moves on a grid or the fewest hops between two people in a network. DFS uses a stack or recursion and goes as deep as it can before backing up. I use it for cycle detection, topological sorting, counting connected components and backtracking. On memory, BFS can hold a very wide frontier, while DFS holds the current path, but recursive DFS on a deep graph can hit Python's recursion limit, so there I'd switch to an explicit stack."

Red flag to avoid:

Saying DFS finds the shortest path, or forgetting the visited set and looping forever on a cycle.

They may ask next:
  • How would you find the shortest path if the edges had different weights?
  • How do you detect a cycle in a directed graph with DFS?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

20. Given a grid of '1' for land and '0' for water, count the islands. Walk me through your approach.

What the interviewer is really testing:
Whether you can see a grid as a graph and count connected components with a flood fill, marking cells so none is counted twice.
Answer frame:

Model: each land cell is a node; up, down, left and right are its edges.

Scan: for each unvisited land cell, count one island and flood-fill all land reachable from it.

Mark visited: in place or in a set, at the moment you queue a cell.

Cost: O(rows times cols) time, and the same in the worst case for the queue.

Sample spoken answer:

"A naive version that searches from every land cell without remembering what it has seen repeats a lot of work. So I treat the grid as a graph where each land cell connects to its land neighbours up, down, left and right. The number of islands is then the number of connected components. I scan every cell. When I find land I haven't visited, I add one to the count and flood-fill from it with BFS, marking every connected land cell so I never count it again. Here I mark by flipping it to '0' in place, though I'd first ask whether I'm allowed to change the input; if not, I keep a visited set. Every cell is handled a constant number of times, so it's O(rows times columns) time, and the queue can grow that large in the worst case. I use an iterative BFS because recursion can hit Python's limit on a big all-land grid."

Code:
from collections import deque

def num_islands(grid):              # O(rows * cols) time and space
    rows, cols = len(grid), len(grid[0]) if grid else 0
    count = 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] != '1':
                continue
            count += 1
            grid[r][c] = '0'
            q = deque([(r, c)])
            while q:
                y, x = q.popleft()
                for ny, nx in ((y + 1, x), (y - 1, x), (y, x + 1), (y, x - 1)):
                    if 0 <= ny < rows and 0 <= nx < cols and grid[ny][nx] == '1':
                        grid[ny][nx] = '0'
                        q.append((ny, nx))
    return count
Red flag to avoid:

Marking a cell visited only when it's popped, so the same cell gets queued many times.

They may ask next:
  • How would you return the size of the largest island instead of the count?
  • How would you solve it with union-find, and when would that be the better choice?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

21. You have courses with prerequisites. Return an order to take them all, or say it's impossible. How do you approach it?

What the interviewer is really testing:
Whether you recognise a dependency problem as topological sorting on a directed graph, and that 'impossible' means there's a cycle.
Answer frame:

Model: a directed edge from each prerequisite to the course that needs it.

Brute force: keep scanning for a course whose prerequisites are all done; O(V times (V + E)).

Kahn's algorithm: count incoming edges; queue the zero-count courses; taking one lowers its dependants' counts.

Cycle check: if not every course comes out, there's a cycle; O(V + E) time and space.

Sample spoken answer:

"I model it as a directed graph with an edge from each prerequisite to the course that depends on it. An order exists exactly when the graph has no cycle, and that order is a topological sort. The slow way is to keep scanning all the courses for one whose prerequisites are all done, which means up to V passes over the whole graph. Kahn's algorithm does it in one pass. I count the incoming edges for each course. Every course with zero goes in a queue, since nothing blocks it. I pop a course, add it to the order, and lower the count of every course that depends on it; any that reach zero join the queue. If the order ends up holding every course, that's my answer. If some are left over, they're waiting on each other in a cycle, so I return an empty list. It's O(V plus E) time and space."

Code:
from collections import deque

def course_order(n, prereqs):         # O(V + E) time and space
    after = [[] for _ in range(n)]
    indeg = [0] * n
    for course, pre in prereqs:       # pre must come before course
        after[pre].append(course)
        indeg[course] += 1
    q = deque(i for i in range(n) if indeg[i] == 0)
    order = []
    while q:
        c = q.popleft()
        order.append(c)
        for nxt in after[c]:
            indeg[nxt] -= 1
            if indeg[nxt] == 0:
                q.append(nxt)
    return order if len(order) == n else []
Red flag to avoid:

Using an undirected-style visited check for cycles, which wrongly flags a harmless diamond of dependencies as a cycle.

They may ask next:
  • How would you do the same with DFS, and how does DFS spot the cycle?
  • If several courses are ready at once, how would you return the order that's smallest alphabetically?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

22. How does Dijkstra's algorithm find shortest paths, and why does it fail when edges have negative weights?

What the interviewer is really testing:
Whether you understand the greedy step Dijkstra relies on, so you know when it's safe and what to use instead.
Answer frame:

Greedy: always settle the unsettled node with the smallest known distance, using a min-heap.

Relax: for each edge out of it, if going through it is shorter, update the neighbour and push it.

Cost: O((V + E) log V) with a binary heap and an adjacency list.

Negative edges: break the rule that a settled distance is final; use Bellman-Ford instead.

Sample spoken answer:

"Dijkstra works on graphs whose edge weights are zero or more. I keep a best-known distance for each node, with the source at zero, and a min-heap of candidates. I pop the node with the smallest distance. Because no edge is negative, no other route can come back to it cheaper later, so that distance is final. Then I relax its edges: if the distance to this node plus the edge weight beats a neighbour's current best, I update it and push it onto the heap. Old heap entries that are out of date I simply skip when they're popped. With a binary heap that's O((V plus E) log V). With negative edges, the 'settled means final' guarantee breaks, because a path through a node I haven't finished could come back cheaper. Then I'd use Bellman-Ford, which is O(V times E) and also detects negative cycles. If all edges cost the same, plain BFS is enough."

Code:
import heapq

def dijkstra(graph, src):             # graph: {u: [(v, w), ...]}, w >= 0
    dist = {src: 0}
    heap = [(0, src)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue                  # stale entry
        for v, w in graph.get(u, []):
            nd = d + w
            if nd < dist.get(v, float('inf')):
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist
Red flag to avoid:

Claiming Dijkstra copes with negative weights if you keep relaxing, or not being able to say why the greedy choice is safe.

They may ask next:
  • How would you return the actual path, not just the distance?
  • If every edge weighs either 0 or 1, what faster approach could you use?
Say it in 60 seconds

Recursion & DP 3 questions

Medium Coding round Fresher, Mid-level Practice question

23. How do you tell that a problem needs dynamic programming? Show me using the number of ways to climb n stairs, one or two steps at a time.

What the interviewer is really testing:
Whether you can spot overlapping subproblems and move from a slow recursion to memoization and then to a table, rather than memorising answers.
Answer frame:

Signals: a count, a min or max, or a yes or no over a series of choices, where the same subproblem repeats.

Recurrence first: ways(n) = ways(n - 1) + ways(n - 2); plain recursion is exponential.

Memoize, then tabulate: cache for O(n); build bottom-up; keep only the last two values for O(1) space.

Sample spoken answer:

"I look for two things. First, the question asks for a count, a minimum, a maximum or whether something is possible, over a series of choices. Second, when I write the brute-force recursion, the same subproblem comes up again and again. For the stairs, my last move was either one step or two, so ways of n equals ways of n minus one plus ways of n minus two. As plain recursion that's exponential, because it recomputes the same values over and over. Adding a cache so each n is computed once makes it O(n) time and O(n) space. Then I flip it bottom-up: start from the base cases and build upwards. Each value only needs the previous two, so I keep two variables, which gives O(n) time and O(1) space. That path from recursion to memo to table is how I approach any DP question."

Code:
from functools import lru_cache

def ways_slow(n):                     # exponential time
    return 1 if n <= 1 else ways_slow(n - 1) + ways_slow(n - 2)

@lru_cache(maxsize=None)
def ways_memo(n):                     # O(n) time, O(n) space
    return 1 if n <= 1 else ways_memo(n - 1) + ways_memo(n - 2)

def ways(n):                          # O(n) time, O(1) space
    a, b = 1, 1                       # ways(0), ways(1)
    for _ in range(n - 1):
        a, b = b, a + b
    return b
Red flag to avoid:

Calling any cached recursion 'DP' without being able to state the recurrence and the base cases.

They may ask next:
  • What's the difference between memoization and tabulation, and when would you pick each?
  • How does the answer change if you can also take three steps at a time?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

24. Given coin values and an amount, find the fewest coins that make the amount. Why doesn't always taking the biggest coin work?

What the interviewer is really testing:
Whether you can break a greedy idea with a counterexample, then build a correct bottom-up table with a clear state.
Answer frame:

Greedy fails: coins 1, 3, 4 and amount 6; greedy gives 4 + 1 + 1, the best is 3 + 3.

Brute force: try every coin at every step; exponential, and it re-solves the same amounts.

State: best[a] = fewest coins for amount a = 1 + the min of best[a - c] over coins c that fit.

Cost: O(amount times coins) time, O(amount) space.

Sample spoken answer:

"Greedy works for some coin systems but not all. With coins 1, 3 and 4 and an amount of 6, greedy takes 4, then 1, then 1, which is three coins, but 3 plus 3 is only two. The brute force tries every coin at every step, which is exponential, and it keeps solving the same smaller amounts. So I define best of a as the fewest coins that make amount a. Best of zero is zero. For any a, the last coin used was some coin c, so best of a is one plus the smallest best of a minus c over all coins that fit. I fill the table from 1 up to the amount. If an amount can't be made, it stays at infinity, and at the end I return minus one for that case. That's O(amount times the number of coins) time and O(amount) space."

Code:
def coin_change(coins, amount):       # O(amount * len(coins)) time
    INF = float('inf')
    best = [0] + [INF] * amount
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a and best[a - c] + 1 < best[a]:
                best[a] = best[a - c] + 1
    return best[amount] if best[amount] != INF else -1
Red flag to avoid:

Defending the greedy answer without trying a single counterexample.

They may ask next:
  • How would you count the number of different ways to make the amount instead?
  • How would you return which coins were used, not just how many?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

25. Generate every subset of a list of distinct numbers. How does backtracking work here, and what does it cost?

What the interviewer is really testing:
Whether you can write a clean choose, explore, un-choose recursion and state its cost honestly when the output itself is exponential.
Answer frame:

Size: each item is in or out, so there are 2^n subsets; nothing can beat the output size.

Backtrack: add an item, recurse from the next index, then remove it to restore the state.

Copy on record: save a copy of the path, not the shared list itself.

Cost: O(n times 2^n) time and output; O(n) recursion depth.

Sample spoken answer:

"Each number is either in a subset or not, so there are 2 to the n subsets, and no approach can beat that, because the output itself is that big. One simple way is to count from zero to 2 to the n minus one and use each number's bits to pick items. Backtracking builds them with one shared path instead. Each call first records a copy of the current path. Then for each index from start onward, I add that number, recurse with start moved past it, and pop it off afterwards. That pop is the backtrack: it undoes the choice so the path is clean for the next option. Moving start forward means I never produce the same set in a different order. The cost is O(n times 2 to the n), since each subset takes up to n to copy. The classic bug is saving the path itself instead of a copy."

Code:
def subsets(nums):                    # O(n * 2^n) time and output
    out, path = [], []

    def backtrack(start):
        out.append(path[:])           # record a copy
        for i in range(start, len(nums)):
            path.append(nums[i])      # choose
            backtrack(i + 1)          # explore
            path.pop()                # un-choose

    backtrack(0)
    return out
Red flag to avoid:

Saving the shared path instead of a copy, so every recorded subset ends up empty.

They may ask next:
  • How would you change it if the list can contain duplicates and you need only unique subsets?
  • How would you generate every permutation instead, and how many are there?
Say it in 60 seconds

Sorting & Searching 2 questions

Easy Coding round Fresher Practice question

26. Write binary search on a sorted array. How do you avoid the usual off-by-one and infinite-loop bugs?

What the interviewer is really testing:
Whether you can write binary search correctly under pressure by holding a clear loop invariant, since many harder problems are built on it.
Answer frame:

Brute force: a linear scan, O(n); binary search needs sorted input and gives O(log n).

Invariant: if the target exists, it is between lo and hi, both inclusive.

Loop: while lo <= hi; move lo to mid + 1 or hi to mid - 1, never to mid itself.

Test: empty array, one element, target at each end, target missing.

Sample spoken answer:

"A linear scan is O(n) and works on anything. If the array is sorted, binary search halves the range each step, so it's O(log n) time and O(1) space. The bugs come from being vague about the range, so I fix an invariant: if the target is in the array, it's between lo and hi, both inclusive. That means the loop runs while lo is less than or equal to hi. I take the middle, and if it's the target I return it. If the middle value is too small, the target must be to the right, so lo becomes mid plus one. If it's too big, hi becomes mid minus one. I always move past mid and never set lo or hi to mid itself, because that's what causes infinite loops when two elements are left. Then I check the edge cases out loud: empty array, one element, target at either end, and target missing."

Code:
def binary_search(a, target):         # O(log n) time, O(1) space
    lo, hi = 0, len(a) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if a[mid] == target:
            return mid
        if a[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1
Red flag to avoid:

Mixing an inclusive hi with a lo < hi loop, or setting lo = mid, then patching the bugs by trial and error.

They may ask next:
  • How would you change it to return the first position of the target when there are duplicates?
  • How would you search a sorted array that has been rotated at an unknown point?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

27. Compare merge sort and quicksort. When would you choose each one?

What the interviewer is really testing:
Whether you know the worst cases, space costs and stability of the two main comparison sorts, not just that both are n log n.
Answer frame:

Merge sort: split, sort each half, merge; O(n log n) always, O(n) extra space for arrays, stable.

Quicksort: partition around a pivot; O(n log n) on average, O(n^2) worst; in place, not stable.

Worst case: bad pivots, like always the first element on sorted data; random pivots make it unlikely.

Choose: merge sort for stability, linked lists or data on disk; quicksort for arrays in memory.

Sample spoken answer:

"Both are divide and conquer. Merge sort splits the array in half, sorts each half and merges them. It's O(n log n) in every case and it's stable, meaning equal items keep their original order, but merging arrays needs O(n) extra space. Quicksort picks a pivot, moves smaller items to one side and larger to the other, then sorts each side. On average it's O(n log n), it sorts in place apart from the recursion stack, and it's usually fast in practice because it's cache-friendly. But with bad pivots, like always taking the first element of already sorted data, it drops to O(n squared); random or median-of-three pivots make that unlikely. It also isn't stable. So I'd pick merge sort when I need stability or a guaranteed time, or I'm sorting a linked list or data too big for memory, and quicksort for arrays in memory where extra space matters."

Red flag to avoid:

Saying quicksort is always O(n log n), or not knowing what a stable sort means.

They may ask next:
  • Why can't any comparison-based sort beat O(n log n) in the worst case?
  • When would a non-comparison sort like counting sort be the better choice?
  • Why do many standard library sorts use a hybrid of several algorithms?
Say it in 60 seconds

Real Work 3 questions

Medium Behavioral round Mid-level, Senior Practice question

28. Tell me about a time you picked the wrong data structure in real code. How did you notice, and what did you change?

What the interviewer is really testing:
Whether data structures are more than interview practice for you: can you link a real slowdown to a complexity mistake and fix it with evidence.
Answer frame:

Situation: what the code did and how the problem showed up.

Diagnosis: how you measured it and found the costly operation.

Fix: the structure you switched to and how the complexity changed.

Result and lesson: the measured improvement and what you do differently now.

Sample spoken answer:

"At my last company we had a nightly job that checked incoming orders against a list of blocked customer IDs. It was fine in testing, but as the blocked list grew, the job slowed down until it started missing its window. I profiled it and saw almost all the time was on one line: checking whether an ID was in a list. That's a linear scan, and it ran once per order, so the whole job cost orders times blocked IDs. I changed the list to a set, built once at the start, which made each check constant time on average. The job went from most of an hour to a couple of minutes, and I added a test with a large list so it couldn't quietly slow down again. Since then, I ask how big each input can realistically get before I choose a structure, not just whether it works on the sample data."

Red flag to avoid:

A story with no measurement, where the fix was a guess and the result is 'it felt faster'.

They may ask next:
  • How did you confirm the fix was the real cause and not just a lucky run?
  • What would you have done if there wasn't enough memory to hold every ID in a set?
Say it in 60 seconds
Medium Behavioral round Fresher, Mid-level Practice question

29. Tell me about a hard algorithm problem you got stuck on, in practice or at work, and how you got unstuck.

What the interviewer is really testing:
Whether you have a method for being stuck, such as small examples, brute force and pattern spotting, rather than freezing or guessing.
Answer frame:

The problem: what it was, in a sentence or two.

Method: worked a small example by hand, wrote the brute force, looked for repeated work.

Breakthrough: the pattern you spotted and why it fit.

Habit: what you now do first when you're stuck.

Sample spoken answer:

"In my final-year project I needed to find, for each reading in a sensor log, the next reading that was higher. My first version checked every later reading for each one, which was O(n squared), and it crawled on the full data. I was stuck on how to do better, so I stopped coding and worked a tiny example by hand, eight numbers on paper. I noticed that the readings still waiting for a higher value were always in decreasing order, and each new reading settled them starting from the most recent one. That's a stack. So I kept a stack of indexes still waiting, and each new reading popped every smaller one and gave it its answer. Each index is pushed and popped once, so it became O(n). Now, when I'm stuck, I write the brute force, do a small case by hand, and ask which work I'm repeating."

Red flag to avoid:

A story where you simply looked up the answer, with nothing about how you'd reason through a similar problem yourself.

They may ask next:
  • How do you decide when to stop thinking and start coding in a timed round?
  • What do you say to the interviewer while you're still stuck?
Say it in 60 seconds
Hard Behavioral round Mid-level, Senior Practice question

30. Describe a time you made code much faster by changing the algorithm rather than adding hardware or caching.

What the interviewer is really testing:
Whether you can find the real bottleneck, reason about how cost grows on real data, and weigh a smarter algorithm against simpler fixes.
Answer frame:

Baseline: what was slow, how slow, and on what input size.

Root cause: the operation whose cost grew too fast.

Change: the new approach, its complexity, and any trade-offs you accepted.

Proof: before and after timings, and how you showed the output was unchanged.

Sample spoken answer:

"At my last company we had a report that found overlapping bookings. The original code compared every booking with every other one, which was O(n squared), and in a busy month with tens of thousands of bookings it ran for over an hour. The first suggestion was a bigger server. I measured it and saw the pair comparison was nearly all the time, so hardware would only buy a constant factor. Instead I sorted the bookings by start time, which is O(n log n), and swept through once, keeping a min-heap of end times for bookings still open. For each new booking I dropped the ones that had already ended, and anything left in the heap overlapped it. The report came down to under a minute. Before shipping, I ran the old and new versions on three months of real data and checked they flagged exactly the same bookings."

Red flag to avoid:

Claiming a big speedup with no before and after numbers, or no check that the results stayed the same.

They may ask next:
  • What would you have done if sorting wasn't possible, for example on a live stream of bookings?
  • How did you convince reviewers who were comfortable with the old, simpler code?
Say it in 60 seconds

Interview Judgement 2 questions

Medium Situational round Fresher, Mid-level Practice question

31. You have twenty minutes left and only a rough idea of the optimal solution. Do you code the brute force or go for the optimal one?

What the interviewer is really testing:
Whether you manage time and risk in a round, knowing a working, well-explained solution usually beats an unfinished clever one.
Answer frame:

Say it out loud: name both options and their costs, and ask what the interviewer prefers.

Default: if the better idea isn't clear yet, code the brute force cleanly and correctly.

Then improve: point to the repeated work and sketch the faster approach in the time left.

If the optimal is clear: go straight to it, keeping the brute force as a fallback.

Sample spoken answer:

"I'd say the situation out loud rather than decide silently. Something like: I can code the O(n squared) brute force in about ten minutes and I'm confident it's right, or I can try the O(n) idea, which I'm less sure about. Would you rather see the working version first? Often the interviewer says go ahead with the brute force, and even without an answer, that's my default when the better idea isn't clear yet. A correct, tested solution with a clear explanation of how to improve it is worth more than half an optimal solution that doesn't run. Once it works, I use the remaining time to point at the wasted work, for example that it re-scans the array for every item, and sketch how a hash map would remove that. If the optimal idea is already clear to me, I skip the brute force and write it directly."

Red flag to avoid:

Spending the whole time on an optimal approach that never runs, without telling the interviewer what's happening.

They may ask next:
  • What if the interviewer says the brute force alone won't be enough?
  • What do you do if you realise halfway through the optimal version that it's wrong?
Say it in 60 seconds
Easy Situational round Fresher Practice question

32. You've finished coding and the interviewer says there's a bug but won't say where. How do you find it?

What the interviewer is really testing:
Whether you debug with a method, tracing small inputs and edge cases by hand, instead of changing lines at random.
Answer frame:

Re-read against the plan: does each line do what you said it would?

Dry run: trace a small, normal input out loud, tracking every variable.

Edge cases: empty input, one element, duplicates, negatives, answers at either end.

Fix and re-test: change one thing, explain why, and re-run the case that failed.

Sample spoken answer:

"I'd stay calm and treat it like any debugging job. First I re-read the code against the plan I described, line by line, because the bug is often where the code quietly differs from what I said, like a less-than that should be less-than-or-equal. Then I dry-run a small example out loud, writing down the variables after each step, so the interviewer can see exactly where my trace and the expected result split. If the normal case passes, I go through edge cases: an empty input, a single element, duplicates, negative numbers, and answers right at the start or end. Loop bounds and off-by-one errors are the usual suspects. When I find it, I explain why it was wrong, change just that one thing, and re-run the case that failed. What I'd avoid is changing several lines at random to see what sticks."

Red flag to avoid:

Changing lines at random until the output looks right, with no explanation of what the bug actually was.

They may ask next:
  • Which edge cases would you try first for a function that works on a linked list?
  • How would you test your code if you could run it but weren't given any test cases?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card