460 Python Developer Interview Questions & Answers

154 top • 31 Amazon • 49 Google • 44 Netflix • 48 Meta • 41 NVIDIA • 47 Apple • 46 Microsoft

Python Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

91. Reverse Each Word and Then Reverse the Word OrderCodingEasy

Question Details

Given the string "How are you", produce two outputs. First, reverse the characters inside each word to get "woH era uoy". Second, reverse the order of the words to get "you are How". Explain the transformations and complexity.

Short Interview Answer (30-60 seconds)

I first split the sentence into a list of words. For the first output, I reverse the characters inside each word while keeping the words in their original positions. For the second output, I reverse the order of the original words without changing their characters. For "How are you", the results are "woH era uoy" and "you are How". The algorithm takes O(n) time and O(n) auxiliary space because it creates word data and new output strings.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to apply two different transformations to the same input sentence. One transformation reverses the characters inside each word. The other reverses the positions of the words. Splitting the sentence into words is a good fit because it lets us transform each word or change the list order without mixing the two operations.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
How to Explain It in an Interview
1. Understand the input and required outputs

The input is the string "How are you".

We need to produce two separate results.

For the first result, each word stays in its original position, but the characters inside the word are reversed.

"How" becomes "woH".

"are" becomes "era".

"you" becomes "uoy".

The first output is "woH era uoy".

For the second result, each word keeps its original characters, but the order of the words is reversed.

["How", "are", "you"] becomes ["you", "are", "How"].

The second output is "you are How".

2. Split the sentence into words

We call split on the input string.

Before this step, the state is the string "How are you".

After this step, the state is the list ["How", "are", "you"].

This original word list is used to build both outputs.

3. Reverse the characters inside each word

We process the words from left to right.

The current word is "How". Reversing its characters gives "woH".

The current word is "are". Reversing its characters gives "era".

The current word is "you". Reversing its characters gives "uoy".

The transformed words are ["woH", "era", "uoy"].

We join them with one space between neighboring words. The first result is "woH era uoy".

4. Reverse the order of the original words

We use the original list ["How", "are", "you"]. We do not use the character-reversed words from the first transformation.

Reversing the list order gives ["you", "are", "How"].

We join these words with spaces. The second result is "you are How".

5. Explain why the results are correct

For the first transformation, the algorithm applies character reversal independently to every word. It never changes the position of a word. Therefore, each output word is the reverse of the corresponding input word.

For the second transformation, the algorithm reads the original word list from the last position to the first position. It does not change the characters inside a word. Therefore, the output contains the original words in exactly reversed order.

6. Explain the Python implementation

The function calls text.split() to create the original word list. A generator expression applies word[::-1] to each word, and join builds the first output. The expression words[::-1] creates the words in reversed order, and another join builds the second output. The function returns both strings as a tuple.

7. Explain complexity and edge cases

Let n be the total number of characters in the input. Splitting the string, reversing all word characters, reversing the word list, and building the output strings together take O(n) time.

The auxiliary space complexity is O(n). The word list, reversed list slice, temporary reversed word strings, and returned strings grow with the input size.

For an empty string, both outputs are empty strings. For a one-word string, the first output reverses that word, while the second output is unchanged. Character case is preserved. Because the implementation uses split and joins with one space, repeated spaces or leading and trailing spaces are normalized in the outputs.

Key Insight / Why This Solution Works

The key insight is that the two required results change different levels of the sentence. The first result changes characters inside each word. The second result changes the positions of whole words. We first create the original word list and then build both results independently from it. The first invariant is that every processed word remains at its original word position and has its characters reversed. The second invariant is that every output position receives the corresponding original word from the opposite end of the list.

Code
def reverse_each_word_and_word_order(text: str) -> tuple[str, str]:
    words = text.split()

    reversed_each_word = " ".join(word[::-1] for word in words)
    reversed_word_order = " ".join(words[::-1])

    return reversed_each_word, reversed_word_order


if __name__ == "__main__":
    input_text = "How are you"
    first_output, second_output = reverse_each_word_and_word_order(input_text)

    print(first_output)
    print(second_output)
Time & Space Complexity

Let n be the total number of characters in the input string. Splitting the sentence takes O(n) time. Reversing the characters across all words takes O(n) time because the total number of word characters is at most n. Reversing the word list and joining both outputs also take O(n) time. Therefore, the total time complexity is O(n). The auxiliary space complexity is O(n) because the code creates a word list, a reversed list slice, reversed word strings, and new result strings.

Where it is used

This pattern is useful in text-processing tools that split a sentence into tokens, transform individual tokens, reorder tokens, and rebuild the final text. It also tests common Python string operations such as split, slicing, generator expressions, and join.

Why Interviewers Ask This

The interviewer is checking whether the candidate can distinguish between reversing characters and reversing word positions. The problem also tests correct use of Python string slicing, split, join, generators, and list slicing. A strong candidate keeps both transformations independent, uses the original words for the second output, explains the exact example correctly, and gives an accurate O(n) time and O(n) auxiliary space analysis.

Common interview mistakes

A common mistake is reversing the complete string, which produces "uoy era woH" and combines both transformations incorrectly. Another mistake is using the reversed-character words to build the second output instead of using the original word list. A candidate may also reverse the word order for the first output or reverse the characters for the second output. Another mistake is claiming O(1) auxiliary space even though Python creates new lists, slices, reversed strings, and result strings. It is also easy to forget that split normalizes repeated whitespace.

Interview tip

Show the original word list once, and then draw two separate branches from it. One branch reverses characters inside each word. The other branch reverses only the list order. This makes the difference between the two outputs clear.

Interviewer may ask next
How would you preserve the original spacing exactly?

The current split and join approach normalizes whitespace. To preserve spacing, I would tokenize the input into alternating word and whitespace sections. I would transform only the word sections and keep the whitespace sections unchanged. Reversing characters inside words would still take O(n) time and O(n) space. For reversed word order, the expected placement of the original whitespace must be clearly defined because moving words can make spacing ownership ambiguous.

Can this solution use less auxiliary space?

The required output strings already need O(n) space because Python strings are immutable and new strings must be returned. Some temporary data can be reduced by using generators, as the first transformation does, but text.split() and words[::-1] still create collections whose size grows with the input. The overall auxiliary space therefore remains O(n), and the time complexity remains O(n).

92. Valid ParenthesesCodingEasy

Question Details

Given a string containing parentheses, brackets, and braces, determine whether every opening symbol is closed by the correct type in the correct order. Explain the edge cases and complexity.

Short Interview Answer (30-60 seconds)

I use a stack to store opening symbols that have not been matched yet. I process each character from left to right. When I see an opening symbol, I push it onto the stack. For a closing symbol, I check that the stack is not empty and that its top is the required opening type. Then I pop the match. The string is valid only when the stack is empty at the end. The time complexity is O(n), and the auxiliary space complexity is O(n).

Detailed Explanation

See the Code while reading this explanation.

The input is a string containing parentheses, square brackets, and braces. The function must return True only when every opening symbol is closed by the correct type in the correct order. A stack is a good fit because the most recent unmatched opening symbol must be closed first.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Valid Parentheses diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives one string named s.

The possible opening symbols are (, [, and {.

The possible closing symbols are ), ], and }.

The function returns True when all opening symbols are matched by both type and order. It returns False when a closer has no matching opener, has the wrong opener, appears in the wrong order, or when an opener remains unmatched at the end.

2. Choose the stack and mapping

The stack stores opening symbols that have not been matched yet. Each stack entry is one unmatched opening symbol. The top of the stack is the most recent opener.

The pairs dictionary maps each closing symbol to the opening symbol it requires:

) maps to ( ] maps to [ } maps to {

The main invariant is that after every processed character, the stack contains exactly the unmatched opening symbols from the processed part of the string. The most recent unmatched opener is always on top.

3. Initialize the state

The stack starts empty:

stack = []

The closing-to-opening mapping is:

pairs = {')': '(', ']': '[', '}': '{'}

Traversal begins at index 0. The empty stack is correct because no characters have been processed yet.

4. Walk through the verified example

The example input is "{[()]}".

Its length is 6.

The exact indices and characters are:

Index 0 contains { Index 1 contains [ Index 2 contains ( Index 3 contains ) Index 4 contains ] Index 5 contains }

Step 1 processes index 0. The current character is {. The stack before the step is []. The character is an opener, so it is pushed. The stack becomes ['{']. Processing continues.

Step 2 processes index 1. The current character is [. The stack before the step is ['{']. The character is an opener, so it is pushed. The stack becomes ['{', '[']. Processing continues.

Step 3 processes index 2. The current character is (. The stack before the step is ['{', '[']. The character is an opener, so it is pushed. The stack becomes ['{', '[', '(']. Processing continues.

Step 4 processes index 3. The current character is ). The stack before the step is ['{', '[', '(']. The pairs dictionary says that ) requires (. The stack is not empty, and its top is (. The types match, so the code pops (. The stack becomes ['{', '[']. Processing continues.

Step 5 processes index 4. The current character is ]. The stack before the step is ['{', '[']. The pairs dictionary says that ] requires [. The top is [, so the types match. The code pops [. The stack becomes ['{']. Processing continues.

Step 6 processes index 5. The current character is }. The stack before the step is ['{']. The pairs dictionary says that } requires {. The top is {, so the types match. The code pops {. The stack becomes [].

Traversal is complete after index 5. The final stack is empty, so the function returns True.

5. Explain why the result is correct

Every opening symbol is pushed onto the stack. A closing symbol is accepted only when it matches the most recent unmatched opening symbol at the top.

This checks the symbol type. It also checks the nesting order.

For example, "([)]" is invalid. When the code reaches ), the stack top is [. The required opener is (, so the function returns False.

The invariant remains true after every push and every valid pop. If the stack is empty at the end, every opening symbol has been matched. If the stack is not empty, at least one opener was never closed.

6. Explain the Python implementation

The function first creates the pairs dictionary and an empty stack.

It then reads each character from left to right.

If the character is in pairs, it is a closing symbol. The code first checks whether the stack is empty. It also checks whether the stack top is different from the required opener. If either condition is true, the function returns False immediately.

If the closer matches the top opener, the code pops that opener.

If the character is not in pairs, the code treats it as an opening symbol and pushes it onto the stack.

After the loop, return not stack returns True only when the stack is empty.

7. Explain complexity and edge cases

Let n be the number of characters in the string.

The time complexity is O(n). Each character is processed once. Every opening symbol is pushed once and popped at most once. The dictionary contains only three fixed mappings.

The auxiliary space complexity is O(n) in the worst case. The stack can contain many unmatched opening symbols.

The important edge cases shown in the diagram are:

An empty string returns True.

A string that starts with a closing symbol, such as "]", returns False.

A wrong type, such as "(]", returns False.

A wrong order, such as "([)]", returns False.

Unclosed opening symbols, such as "((", return False.

Key Insight / Why This Solution Works

The key insight is that bracket matching follows last in, first out order. This means the most recent unmatched opening symbol must be closed before any earlier opening symbol. A stack provides this order directly. Opening symbols are pushed. For each closing symbol, the algorithm uses the pairs dictionary to find the required opener and compares it with the stack top. The invariant is that the stack always contains exactly the unmatched opening symbols from the processed prefix, with the most recent opener on top.

Code
def is_valid(s: str) -> bool:
    pairs = {")": "(", "]": "[", "}": "{"}
    stack: list[str] = []

    for char in s:
        if char in pairs:
            if not stack or stack[-1] != pairs[char]:
                return False
            stack.pop()
        else:
            stack.append(char)

    return not stack


if __name__ == "__main__":
    example = "{[()]}"
    result = is_valid(example)
    print(result)
Time & Space Complexity

Let n be the length of the string. The time complexity is O(n) because each character is processed once. Every opening symbol is pushed once and popped at most once. Python dictionary membership and lookup are O(1) on average, and this dictionary has only three fixed entries. The auxiliary space complexity is O(n) because the stack may hold all opening symbols in the worst case. Auxiliary space means extra memory used by the algorithm.

Where it is used

This stack pattern is useful when software must validate nested structures. Examples include checking brackets in source code, parsing mathematical expressions, reading nested configuration data, and validating markup. The pattern applies whenever the most recently opened item must be closed first.

Why Interviewers Ask This

This question tests whether the candidate recognizes a last in, first out pattern and chooses a stack. It also checks careful handling of symbol type, nesting order, empty-stack conditions, and leftover opening symbols. The interviewer can evaluate whether the candidate maintains a clear invariant, writes safe Python conditions, supports early failure, handles important edge cases, and explains the O(n) time and O(n) auxiliary space correctly.

Common interview mistakes

A common mistake is popping before checking the opening-symbol type. The code must compare stack[-1] first and pop only after a match. Another mistake is trying to pop when the stack is empty. Some candidates check only whether the counts are equal, but equal counts do not prove correct order. Another mistake is forgetting to verify that the stack is empty after the loop. Candidates may also claim O(1) auxiliary space even though the stack can grow with the input.

Interview tip

Explain the invariant before writing the loop: the stack contains only unmatched opening symbols, and its top is the next opener that a closing symbol must match.

Interviewer may ask next
How would the solution work if the characters arrived as a stream?

The same stack method can process each character as it arrives. Push every opening symbol. For a closing symbol, check the stack and compare its top with the required opener. Return False immediately on a mismatch. When the stream ends, return True only if the stack is empty. The time complexity is O(n), and the auxiliary space complexity is O(n) in the worst case. The main tradeoff is that a valid final result cannot be confirmed until the stream ends.

Can the auxiliary space be reduced below O(n)?

Not for the general version of this problem. The input may contain many opening symbols before their closing symbols appear. The algorithm must remember both their types and their order. That information can require O(n) stack space. Removing the stack would lose information needed to validate later closers. The time complexity remains O(n), and the worst-case auxiliary space remains O(n).

93. Merge Two Sorted ListsCodingEasy

Question Details

Given the heads of two sorted linked lists, merge them into one sorted linked list and return its head. Explain whether your solution is iterative or recursive and analyze its complexity.

Short Interview Answer (30-60 seconds)

I would solve this iteratively with two pointers and a dummy node. One pointer starts at the head of each sorted list. I compare the two current node values and attach the node with the smaller value. When the values are equal, I attach the node from List 1. I then move that list’s pointer forward. When one list ends, I attach the remaining chain. The time complexity is O(n + m), and the auxiliary space complexity is O(1).

Detailed Explanation

See the Code while reading this explanation.

This problem asks us to merge two already sorted linked lists into one sorted linked list. The best fit here is an iterative two-pointer method. We reuse the existing nodes, compare only the current heads, and build the result from left to right. A dummy node gives us a fixed starting point and avoids special handling for the first merged node.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Merge Two Sorted Lists diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is the head of List 1 and the head of List 2. Both lists are sorted in non-decreasing order. We must return the head of one merged sorted linked list.

For the example:

List 1: 1 → 2 → 4

List 2: 1 → 3 → 4

The returned list is:

1 → 1 → 2 → 3 → 4 → 4

We return node references, not a new array of values.

2. Choose the iterative two-pointer method

We use pointer p1 for List 1 and pointer p2 for List 2. At each step, we compare p1.val and p2.val. We attach the node with the smaller value. When the values are equal, the code attaches the node from List 1 because it uses the condition p1.val <= p2.val.

We also use a dummy node and a tail pointer. dummy is a fixed starting node. tail always points to the last node in the merged part built so far.

The central invariant is this: the nodes after dummy are always sorted, contain exactly the nodes already selected from both lists, and tail points to the last selected node.

3. Initialize the state

Create a dummy node. Set tail to dummy.

Set p1 to the head of List 1 and p2 to the head of List 2.

At the start:

p1 points to value 1 in List 1.

p2 points to value 1 in List 2.

The merged list after dummy is empty.

4. Walk through the example

Step 1: p1 is 1 and p2 is 1. Since 1 <= 1, attach the node from List 1. Move p1 to 2. Move tail to the attached node. The merged values are now 1.

Step 2: p1 is 2 and p2 is

  1. Since 2 > 1, attach the node from List
  2. Move p2 to
  3. Move tail forward. The merged values are now 1 → 1.

Step 3: p1 is 2 and p2 is 3. Since 2 <= 3, attach 2 from List 1. Move p1 to 4. The merged values are now 1 → 1 → 2.

Step 4: p1 is 4 and p2 is 3. Since 4 > 3, attach 3 from List 2. Move p2 to 4. The merged values are now 1 → 1 → 2 → 3.

Step 5: p1 is 4 and p2 is 4. Since 4 <= 4, attach the node from List 1. Move p1 to None. The merged values are now 1 → 1 → 2 → 3 → 4.

Step 6: List 1 is exhausted. Attach the remaining List 2 chain starting at node 4. We do not move p2 or tail because the whole remaining chain is linked in one operation. The final merged list is 1 → 1 → 2 → 3 → 4 → 4.

5. Explain why the result is correct

At every step, p1 and p2 point to the smallest remaining nodes in their own lists. Choosing the node with the smaller value is safe because no later node in either sorted list can be smaller. When the values are equal, choosing the List 1 node first still keeps the merged list sorted.

This keeps the merged prefix sorted. We never skip a node. When one list ends, every remaining node in the other list is already sorted and is greater than or equal to the last selected node. Therefore, attaching the remaining chain keeps the final list sorted.

6. Explain the Python implementation

The loop runs while both pointers are not None. It compares p1.val and p2.val. If p1.val <= p2.val, it attaches p1 and advances p1. Otherwise, it attaches p2 and advances p2. After either choice, tail moves to the node that was just attached.

After the loop, at least one pointer is None. The line tail.next = p1 if p1 is not None else p2 attaches the entire remaining chain in one operation.

Finally, dummy.next is returned because it points to the real head of the merged list.

7. Explain complexity and edge cases

Let n be the number of nodes in List 1 and m be the number of nodes in List 2. Each node chosen during comparison is processed once, and any remaining chain is attached directly. In the worst case, the algorithm examines all nodes, so the time complexity is O(n + m).

The algorithm uses only dummy, tail, p1, and p2. It does not create a new node for every value. Therefore, the auxiliary space complexity is O(1).

Relevant edge cases are one empty list, both lists empty, duplicate values, and one list containing only values smaller than the other list.

Key Insight / Why This Solution Works

The key idea is to use the fact that both linked lists are already sorted. We only need to compare the two current head nodes. We attach the node with the smaller value. When the values are equal, the code chooses the node from List 1 because it uses p1.val <= p2.val. We then advance only the pointer from the selected list. A dummy node gives us a stable starting point, while tail marks the end of the merged prefix. The invariant is that dummy.next through tail is always sorted and contains exactly the nodes already selected from the two lists. Once one list is exhausted, the remaining chain from the other list can be attached directly because it is already sorted.

Code
from __future__ import annotations
from typing import Optional


class ListNode:
    def __init__(self, val: int = 0, next: Optional[ListNode] = None):
        self.val = val
        self.next = next


def mergeTwoLists(head1: Optional[ListNode], head2: Optional[ListNode]) -> Optional[ListNode]:
    dummy = ListNode()
    tail = dummy

    p1 = head1
    p2 = head2

    while p1 is not None and p2 is not None:
        if p1.val <= p2.val:
            tail.next = p1
            p1 = p1.next
        else:
            tail.next = p2
            p2 = p2.next

        tail = tail.next

    # Attach the remaining chain from the non-empty list.
    tail.next = p1 if p1 is not None else p2

    return dummy.next


def build_list(values: list[int]) -> Optional[ListNode]:
    dummy = ListNode()
    tail = dummy

    for value in values:
        tail.next = ListNode(value)
        tail = tail.next

    return dummy.next


def list_to_string(head: Optional[ListNode]) -> str:
    values: list[str] = []
    current = head

    while current is not None:
        values.append(str(current.val))
        current = current.next

    return " -> ".join(values)


if __name__ == "__main__":
    list1 = build_list([1, 2, 4])
    list2 = build_list([1, 3, 4])

    merged_head = mergeTwoLists(list1, list2)
    print(list_to_string(merged_head))
    # Output: 1 -> 1 -> 2 -> 3 -> 4 -> 4
Time & Space Complexity

Let n be the number of nodes in the first list and m be the number of nodes in the second list. Each node selected during comparison is processed once, and any remaining chain is attached directly. In the worst case, the algorithm examines all nodes, so the time complexity is O(n + m). It uses only a fixed number of extra pointers: dummy, tail, p1, and p2. It reuses the existing list nodes instead of copying them into another structure. Therefore, the auxiliary space complexity is O(1). The dummy node is only one extra node, so the extra memory does not grow with the input size.

Where it is used

This pattern is useful when software needs to combine two already ordered streams or sequences. Examples include merging sorted database results, combining timestamp-ordered event feeds, joining ordered task queues, and merging sorted linked-list partitions. The same two-pointer idea also appears in merge sort.

Why Interviewers Ask This

Interviewers use this problem to test whether a candidate can work safely with linked-list references. They want to see correct pointer movement, careful node rewiring, and use of a dummy node to simplify head handling. The problem also checks whether the candidate recognizes the two-pointer pattern, explains the equal-value rule, preserves sorted order, handles duplicate values and empty lists, and gives the correct O(n + m) time and O(1) auxiliary space analysis.

Common interview mistakes

A common mistake is moving the wrong pointer after attaching a node. The pointer from the selected list must move forward. Another mistake is forgetting the exact equal-value rule. With the condition p1.val <= p2.val, the node from List 1 is selected when both values are equal. Candidates may also forget to move tail after each attachment or forget to attach the remaining chain after the main loop. Returning dummy instead of dummy.next adds the placeholder node to the result. It is also incorrect to claim that this iterative solution uses O(n + m) auxiliary space because it reuses the existing nodes and uses O(1) extra space.

Interview tip

State the invariant before coding: the list after dummy is always sorted, and tail points to the last node in that merged prefix. Also mention that <= chooses List 1 when the current values are equal.

Interviewer may ask next
Can this problem also be solved recursively?

Yes. Compare the two current heads. Choose the node with the smaller value. When the values are equal, the same tie rule can choose the List 1 node. Set the selected node’s next pointer to the result of recursively merging the remaining lists. The base case returns the other list when one head is None. The time complexity remains O(n + m). The auxiliary space becomes O(n + m) in the worst case because each recursive call uses stack space. The iterative solution avoids that recursion stack.

What happens if one or both input lists are empty?

If both lists are empty, the function returns None. If only one list is empty, the main loop does not run. The remaining non-empty list is attached directly to dummy.next and returned. The result stays sorted because no comparisons are needed. Attaching the existing chain takes O(1) work, and the auxiliary space remains O(1).

94. Best Time to Buy and Sell StockCodingEasy

Question Details

Given daily stock prices, choose one day to buy and a later day to sell so that profit is maximized. Return zero when no profitable transaction exists and explain the linear-time approach.

Short Interview Answer (30-60 seconds)

I keep track of the lowest stock price seen so far and the best profit found so far. I process prices from left to right. If the current price is lower than the saved minimum, I update the minimum. Otherwise, I calculate the profit from selling today and keep the larger profit. This works because every selling day is compared with the cheapest earlier buying price. The solution runs in O(n) time and uses O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is a list of daily stock prices. We must buy on one day and sell on a later day. The goal is to return the largest possible profit. If no profitable transaction exists, we return 0. The solution uses a linear scan with two variables: min_price and max_profit.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Best Time to Buy and Sell Stock diagram
How to Explain It in an Interview
1. Understand the input and output

The input is a list called prices. Each value is the stock price for one day.

The output is one integer. It is the largest profit from one buy followed by one later sell. We return 0 when every possible transaction would lose money or make no profit.

For the example prices = [7, 1, 5, 3, 6, 4], the answer is 5. We buy at index 1 for price 1. We sell later at index 4 for price 6. The profit is 6 - 1 = 5.

2. Choose the linear-time approach

I scan the prices from left to right.

I keep min_price as the lowest price seen so far. I keep max_profit as the largest valid profit found so far.

The main invariant is simple. Whenever the current price is considered as a selling price, min_price is the lowest price found at an earlier index. This makes every calculated profit a valid buy-before-sell transaction.

3. Initialize the state

I set min_price to positive infinity. Any real stock price will be smaller than this starting value.

I set max_profit to 0. This also handles cases where no profitable transaction exists.

4. Walk through the example

At index 0, the price is 7. Since 7 is lower than infinity, min_price becomes 7. We do not calculate a profit on this step.

At index 1, the price is 1. Since 1 is lower than 7, min_price becomes 1. We do not calculate a profit on this step.

At index 2, the price is 5. It is not lower than min_price. The current profit is 5 - 1 = 4. max_profit becomes 4.

At index 3, the price is 3. The current profit is 3 - 1 = 2. This is smaller than 4, so max_profit stays 4.

At index 4, the price is 6. The current profit is 6 - 1 = 5. This is larger than 4, so max_profit becomes 5.

At index 5, the price is 4. The current profit is 4 - 1 = 3. This is smaller than 5, so max_profit stays 5.

After all prices are processed, the result is 5.

5. Explain why the result is correct

Whenever the current price is considered as a selling price, min_price is the lowest price from an earlier day. Subtracting min_price therefore gives the best profit possible for selling on that day.

The algorithm keeps the largest of these profits. Therefore, max_profit is the best profit across all valid buy-before-sell transactions.

6. Explain the Python implementation

The for loop processes each price from left to right.

The if branch updates min_price when a cheaper buying price is found. The else branch calculates the profit only when the current price is not a new minimum.

The max function keeps the larger value between the previous best profit and the current profit. After the loop, the function returns max_profit.

7. Explain complexity and edge cases

The time complexity is O(n) because each price is processed once. The auxiliary space complexity is O(1) because only a constant number of variables are stored.

If prices always decrease, max_profit stays 0. If all prices are equal, the answer is 0. An empty list or a one-element list also returns 0. Two prices produce either their positive difference or 0.

Key Insight / Why This Solution Works

The key insight is that a selling price only needs to be compared with the lowest buying price seen before it. The algorithm keeps min_price as the lowest processed price and max_profit as the largest profit found. When a new lower price appears, it becomes the new possible buying price. Otherwise, the algorithm calculates price - min_price and updates max_profit. The invariant is that min_price is the cheapest valid earlier buying price whenever a sale is evaluated, while max_profit stores the best valid profit found so far.

Code
from typing import List


def max_profit(prices: List[int]) -> int:
    min_price = float("inf")
    max_profit = 0

    for price in prices:
        if price < min_price:
            min_price = price
        else:
            profit = price - min_price
            max_profit = max(max_profit, profit)

    return max_profit


if __name__ == "__main__":
    example_prices = [7, 1, 5, 3, 6, 4]
    result = max_profit(example_prices)

    print("Prices:", example_prices)
    print("Maximum profit:", result)
Time & Space Complexity

The time complexity is O(n), where n is the number of prices. We make one pass through the list, and each price is processed one time. The auxiliary space complexity is O(1). Auxiliary space means extra memory used by the algorithm. The amount of extra memory does not grow with the input because we only keep a constant number of variables.

Where it is used

This pattern is useful when data arrives in order and we need the best difference between an earlier small value and a later large value. Similar logic can be used for tracking price increases, measuring growth over time, finding the best gain in a sequence, and processing streaming values without storing the full history.

Why Interviewers Ask This

Interviewers use this problem to check whether a candidate can replace a slow nested-loop solution with a single-pass algorithm. They also evaluate whether the candidate understands processing order, because the buy must happen before the sell. The problem tests state tracking, invariant reasoning, edge-case handling, clean Python code, and accurate complexity analysis.

Common interview mistakes

A common mistake is selling before buying. Process the list from left to right so the stored buying price always comes first. Another mistake is using two nested loops, which takes O(n²) time. Some candidates return the buy and sell prices even though this question asks for the profit. It is also incorrect to return a negative result when prices decrease. The required result is 0.

Interview tip

State the invariant before writing the loop: min_price is the lowest price seen so far, and max_profit is the best valid profit seen so far. This makes the code and correctness explanation much easier to follow.

Interviewer may ask next
How would you return the buy and sell indices instead of only the profit?

Store the index whenever min_price is updated. When a new max_profit is found, save the stored buy index and the current sell index. Return those saved indices at the end. The time complexity remains O(n), and the auxiliary space remains O(1).

What changes if multiple buy and sell transactions are allowed?

The goal changes because we can collect profit from every upward price movement. Add prices[i] - prices[i - 1] whenever the current price is higher than the previous price. This preserves correctness by capturing every profitable increase. The time complexity is O(n), and the auxiliary space is O(1). The tradeoff is that this solves a different contract with unlimited transactions.

95. Daily TemperaturesCodingMedium

Question Details

Given a list of daily temperatures, return a list where each position contains the number of days until a warmer temperature occurs. Use zero when no warmer future day exists. Explain the monotonic-stack approach and analyze time and space complexity.

Short Interview Answer (30-60 seconds)

I would use a monotonic stack that stores indices of days that are still waiting for a warmer temperature. I scan the temperatures from left to right. While the current temperature is warmer than the temperature at the index on top of the stack, I pop that index and calculate the day difference. Then I push the current index. Each index is pushed and popped at most once, so the time complexity is O(n). The auxiliary space complexity is O(n).

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to return, for each day, how many days we must wait for a warmer temperature. If no warmer day exists, the answer for that position stays zero. A monotonic stack fits this problem because it keeps the indices of unresolved days and lets a warmer day resolve several earlier days efficiently.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Daily Temperatures diagram
How to Explain It in an Interview
1. Understand the input and output

The input is a list of temperatures.

For the example:

Input: [73, 74, 75, 71, 69, 72, 76, 73]

The output must contain one number for each input position:

Output: [1, 1, 4, 2, 1, 1, 0, 0]

Each output value tells us how many days we must wait for a strictly warmer temperature. Equal temperatures do not count as warmer.

2. Use a monotonic stack of indices

The stack stores indices, not temperatures.

Each stored index represents a day that has not found a warmer future day yet.

The temperatures at those indices are non-increasing from the bottom of the stack to the top. This means the top is the most recent unresolved day and is the first one we compare with the current temperature.

3. Initialize the state

Create a result list filled with eight zeros:

[0, 0, 0, 0, 0, 0, 0, 0]

Create an empty stack:

[]

A zero remains in the result when no warmer future day is found.

4. Walk through the example

At index 0, the temperature is 73. The stack is empty, so push index 0.

Stack: [0]

At index 1, the temperature is 74. It is warmer than 73 at index 0. Pop index 0 and set result[0] = 1 - 0 = 1. Then push index 1.

Stack: [1]

Result: [1, 0, 0, 0, 0, 0, 0, 0]

At index 2, the temperature is 75. It is warmer than 74 at index 1. Pop index 1 and set result[1] = 2 - 1 = 1. Then push index 2.

Stack: [2]

Result: [1, 1, 0, 0, 0, 0, 0, 0]

At index 3, the temperature is 71. It is not warmer than 75 at index 2, so push index 3.

Stack: [2, 3]

At index 4, the temperature is 69. It is not warmer than 71 at index 3, so push index 4.

Stack: [2, 3, 4]

At index 5, the temperature is 72. It is warmer than 69 at index 4. Pop index 4 and set result[4] = 5 - 4 = 1.

It is also warmer than 71 at index 3. Pop index 3 and set result[3] = 5 - 3 = 2.

It is not warmer than 75 at index 2, so stop popping and push index 5.

Stack: [2, 5]

Result: [1, 1, 0, 2, 1, 0, 0, 0]

At index 6, the temperature is 76. It is warmer than 72 at index 5. Pop index 5 and set result[5] = 6 - 5 = 1.

It is also warmer than 75 at index 2. Pop index 2 and set result[2] = 6 - 2 = 4.

Then push index 6.

Stack: [6]

Result: [1, 1, 4, 2, 1, 1, 0, 0]

At index 7, the temperature is 73. It is not warmer than 76 at index 6, so push index 7.

Final stack: [6, 7]

The remaining indices have no warmer future day, so their result values remain zero.

5. Explain why the solution is correct

The stack contains only unresolved days.

When the current temperature is warmer than the temperature at the top index, the current day is the first warmer day for that stored index. No earlier day could have resolved it, because that index would already have been removed from the stack.

The distance is current_index - previous_index. After an index is popped, its answer is complete and never changes.

6. Explain the Python implementation

The code loops through each temperature with its index. The while loop resolves every previous day that is cooler than the current day. For each popped index, the code stores the distance to the current index. The current index is then pushed onto the stack. After the loop, unresolved indices keep their initial value of zero.

7. Explain complexity and edge cases

Each index is pushed onto the stack once and popped at most once. Therefore, the total time complexity is O(n).

The monotonic stack can hold up to n indices, so the auxiliary space complexity is O(n). The returned result list also uses O(n) output space.

Important edge cases include an empty list, one temperature, strictly decreasing temperatures, strictly increasing temperatures, and equal temperatures.

Key Insight / Why This Solution Works

The key insight is to delay the answer for a day until a warmer temperature appears. The stack stores indices of unresolved days. Their temperatures are non-increasing from bottom to top. When a warmer temperature arrives, it can resolve one or more indices from the top of the stack. The first warmer day found for a popped index is the correct answer because all earlier processed temperatures failed to resolve it. This avoids checking every future day for every position.

Code
from typing import List


def dailyTemperatures(temperatures: List[int]) -> List[int]:
    result = [0] * len(temperatures)
    stack: List[int] = []

    for current_index, current_temperature in enumerate(temperatures):
        while stack and temperatures[stack[-1]] < current_temperature:
            previous_index = stack.pop()
            result[previous_index] = current_index - previous_index

        stack.append(current_index)

    return result


if __name__ == "__main__":
    example = [73, 74, 75, 71, 69, 72, 76, 73]
    print(dailyTemperatures(example))
    # Output: [1, 1, 4, 2, 1, 1, 0, 0]
Time & Space Complexity

Let n be the number of temperatures. The time complexity is O(n). Although there is a while loop inside the for loop, each index enters the stack once and leaves the stack at most once. The auxiliary space complexity is O(n) because the stack may store up to n indices. The returned result list also uses O(n) output space.

Where it is used

This monotonic-stack pattern is useful when each item needs the next greater or next smaller item. Common examples include stock-span calculations, next-greater-element problems, waiting-time analysis, histogram problems, and finding when a later measurement crosses a previous value.

Why Interviewers Ask This

This problem tests whether the candidate can recognize the next-greater-element pattern and choose a monotonic stack. It also checks whether the candidate understands why indices must be stored, can maintain a stack invariant, handles equal values correctly, and can explain why nested loops still produce O(n) total time. The interviewer also wants to see clear Python code and accurate reasoning about auxiliary space.

Common interview mistakes

A common mistake is storing temperatures instead of indices. The index is needed to calculate the number of days. Another mistake is popping when temperatures are equal. The question requires a strictly warmer day, so the comparison must use less than, not less than or equal to. Candidates may also pop only one item instead of continuing while several earlier days are cooler. Another mistake is claiming O(n²) time because of the nested loops. Each index is popped at most once, so the total time is O(n). Finally, do not overwrite the remaining zeros because they correctly represent days with no warmer future temperature.

Interview tip

State the stack invariant before coding: the stack stores unresolved indices whose temperatures are non-increasing from bottom to top. Then explain that each warmer day repeatedly resolves cooler indices from the top.

Interviewer may ask next
What changes if we need the next day with a temperature greater than or equal to the current temperature?

Change the while-loop comparison from temperatures[stack[-1]] < current_temperature to temperatures[stack[-1]] <= current_temperature. Equal temperatures would then resolve earlier days. The stack would contain unresolved indices whose temperatures are strictly decreasing from bottom to top. Time remains O(n), and auxiliary space remains O(n).

Can the auxiliary space be reduced to O(1)?

Not with the same one-pass monotonic-stack method for arbitrary input. The algorithm may need to remember many unresolved indices, such as in a strictly decreasing list. That requires O(n) auxiliary space in the worst case. A brute-force method can use O(1) auxiliary space when the required output array is excluded from the count, but its time complexity becomes O(n²).

96. Minimum Path SumCodingMedium

Question Details

Given a grid of nonnegative numbers, find the minimum sum of values along a path from the top-left cell to the bottom-right cell when movement is allowed only to the right or downward. Explain the dynamic-programming state, boundary handling, and time and space complexity.

Short Interview Answer (30-60 seconds)

I would use dynamic programming. I create a table where dp[i][j] stores the minimum sum needed to reach cell (i, j). I initialize the top-left cell, then fill the first row from left to right and the first column from top to bottom. For every other cell, I add its value to the smaller total from above or from the left. The answer is dp[m - 1][n - 1]. Time and auxiliary space are both O(m × n).

Detailed Explanation

See the Code while reading this explanation.

The problem asks for the minimum sum along a path from the top-left cell to the bottom-right cell. We may move only right or down. Dynamic programming works well because the best result for each cell depends only on two results that were calculated earlier: the cell above and the cell to the left.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Minimum Path Sum diagram
How to Explain It in an Interview
1. Understand the input and output

The input is a grid of nonnegative numbers.

The output is one number. It is the minimum sum of all values on a valid path from the top-left cell to the bottom-right cell.

A valid move goes only right or down. We do not need to return the path itself.

The example grid is:

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

One minimum path is:

1 → 3 → 1 → 1 → 1

Its total is 7.

2. Define the dynamic-programming state

Let dp[i][j] mean the minimum accumulated path sum needed to reach cell (i, j).

This is the main invariant. After dp[i][j] is calculated, it contains the correct minimum total for reaching that cell using only right and down moves.

We create a dynamic-programming table with the same dimensions as the input grid.

3. Initialize the starting cell and boundaries

The top-left cell is the starting point, so:

dp[0][0] = grid[0][0] = 1

Cells in the first row can only be reached from the left. We calculate them with:

dp[0][j] = dp[0][j - 1] + grid[0][j]

For the example, the first row becomes:

[1, 4, 5]

Cells in the first column can only be reached from above. We calculate them with:

dp[i][0] = dp[i - 1][0] + grid[i][0]

For the example, the first column becomes:

[1, 2, 6]

4. Fill the remaining cells

An interior cell can be reached from either the cell above it or the cell to its left.

We choose the smaller accumulated total and add the current grid value:

dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1])

At cell (1, 1), the grid value is 5. The total above is 4, and the total on the left is 2:

dp[1][1] = 5 + min(4, 2) = 7

The table state is:

[[1, 4, 5], [2, 7, ∞], [6, ∞, ∞]]

At cell (1, 2), the grid value is 1:

dp[1][2] = 1 + min(5, 7) = 6

The table state is:

[[1, 4, 5], [2, 7, 6], [6, ∞, ∞]]

At cell (2, 1), the grid value is 2:

dp[2][1] = 2 + min(7, 6) = 8

The table state is:

[[1, 4, 5], [2, 7, 6], [6, 8, ∞]]

At cell (2, 2), the grid value is 1:

dp[2][2] = 1 + min(6, 8) = 7

The completed table is:

[[1, 4, 5], [2, 7, 6], [6, 8, 7]]

5. Explain why the result is correct

The first row is correct because each cell in that row has only one possible incoming direction: from the left.

The first column is correct because each cell in that column has only one possible incoming direction: from above.

For every interior cell, any valid path must enter from either above or the left. The dynamic-programming table already contains the minimum total for both of those positions. Choosing the smaller total and adding the current value therefore gives the minimum total for the current cell.

The table is filled from top to bottom and left to right. This guarantees that both required dependencies are ready before each cell is calculated.

6. Explain the Python implementation

The code reads the number of rows and columns. It creates an m by n dynamic-programming table.

It initializes the starting cell. It then fills the first row and first column using their only possible incoming directions.

Two nested loops process the remaining cells. Each cell uses the same recurrence shown in the walkthrough.

The answer is read from the bottom-right cell:

dp[m - 1][n - 1]

For the example, this value is 7.

7. Explain complexity and edge cases

The algorithm calculates each of the m × n cells once. Its time complexity is O(m × n).

The dynamic-programming table stores m × n values. Its auxiliary space complexity is O(m × n).

Important edge cases include a one-cell grid, a grid with one row, a grid with one column, and a grid containing only zeros.

Key Insight / Why This Solution Works

The key insight is that the minimum sum for reaching a cell depends only on the minimum sums for reaching the cell above it and the cell to its left. The dynamic-programming state is dp[i][j], which stores the minimum accumulated sum needed to reach cell (i, j). Boundary cells have only one incoming direction. Every interior cell uses grid[i][j] + min(dp[i - 1][j], dp[i][j - 1]). The invariant is that every completed dp cell contains the correct minimum sum for reaching that position.

Code
from typing import List


def minPathSum(grid: List[List[int]]) -> int:
    m, n = len(grid), len(grid[0])

    # dp[i][j] is the minimum path sum needed to reach cell (i, j).
    dp = [[0] * n for _ in range(m)]

    # Starting cell.
    dp[0][0] = grid[0][0]

    # First row can only be reached from the left.
    for j in range(1, n):
        dp[0][j] = dp[0][j - 1] + grid[0][j]

    # First column can only be reached from above.
    for i in range(1, m):
        dp[i][0] = dp[i - 1][0] + grid[i][0]

    # Fill the remaining cells.
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1])

    # The bottom-right cell contains the final answer.
    return dp[m - 1][n - 1]


if __name__ == "__main__":
    grid = [[1, 3, 1], [1, 5, 1], [4, 2, 1]]

    print(minPathSum(grid))  # Output: 7
Time & Space Complexity

Let m be the number of rows and n be the number of columns. The algorithm calculates every cell once, so the time complexity is O(m × n). It creates a table with one stored result for every grid cell, so the auxiliary space complexity is O(m × n). Auxiliary space means extra memory used by the algorithm. The table can be reduced to one row, which would lower the auxiliary space to O(n), but the illustrated solution uses the full table.

Where it is used

This dynamic-programming pattern is useful when a larger answer can be built from smaller nearby answers. It can be used for finding low-cost routes through a grid, calculating minimum processing costs across stages, and solving other grid problems where movement is limited to specific directions.

Why Interviewers Ask This

This problem tests whether a candidate can recognize a dynamic-programming pattern and define a clear state. It also tests careful boundary handling because the first row and first column follow different rules. The interviewer checks whether the candidate uses the correct dependency order, writes a valid recurrence, keeps the example consistent, produces working Python code, and explains time and auxiliary space complexity accurately.

Common interview mistakes

A common mistake is using the raw grid values above and to the left instead of the accumulated values stored in dp. Another mistake is applying the interior-cell recurrence to the first row or first column, where one dependency does not exist. Candidates may also fill the table in an order that uses a dependency before it has been calculated. Some return the smallest grid value instead of dp[m - 1][n - 1]. Another mistake is claiming O(1) auxiliary space while storing the full m by n table.

Interview tip

State the meaning of dp[i][j] before writing the recurrence. Then explain the starting cell, the first row, the first column, and one interior-cell calculation.

Interviewer may ask next
Can the auxiliary space be reduced?

Yes. We can use one array of length n. Before updating dp[j], it stores the minimum sum from the cell above. dp[j - 1] stores the minimum sum from the current row's cell on the left. We update with dp[j] = grid[i][j] + min(dp[j], dp[j - 1]). The time complexity remains O(m × n), and the auxiliary space becomes O(n). The tradeoff is that the complete two-dimensional table is no longer stored.

How would you return one minimum path as well as the minimum sum?

Keep the full dynamic-programming table. Start at the bottom-right cell and move backward. At each step, move to the valid top or left neighbor with the smaller dp value. Continue until reaching the top-left cell, then reverse the collected cells. Building the table still takes O(m × n) time and O(m × n) auxiliary space. Reconstructing and storing the path takes O(m + n) additional time and output space.

97. Validate an IPv4 Address and Restore One from DigitsCodingMedium

Question Details

First, determine whether a string is a valid IPv4 address made of four dot-separated octets, where each octet is between 0 and 255. Then, given a digits-only string such as "127001", determine whether dots can be inserted without changing digit order to form at least one valid IPv4 address.

Short Interview Answer (30-60 seconds)

I handle the two input forms separately. If the string contains dots, I split it into exactly four octets and validate each one. If it contains only digits, I use DFS with backtracking. I try segment lengths from three down to one, reject invalid octets, and stop when four valid octets consume every digit. Dotted validation takes O(n) time and O(n) temporary space. Restoration checks at most 3^4 choices and uses constant auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem has two connected parts. First, we validate a string that already contains dots. Second, we check whether a digits-only string can be split into four valid IPv4 octets without changing the digit order. Both parts use the same octet validation rule. The restoration part uses depth-first search, or DFS, with backtracking because each octet may contain one, two, or three digits.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Validate an IPv4 Address and Restore One from Digits diagram
How to Explain It in an Interview
1. Understand the input and output

The input is one string, and the output is a Boolean value.

If the string contains dots, it must already be a valid IPv4 address. A valid address contains exactly four dot-separated octets.

Each octet must meet these rules:

  • It contains one to three ASCII digits.
  • Its value is between 0 and 255.
  • A multi-digit octet cannot begin with zero.
  • The single octet "0" is valid.

If the input contains only digits, we must decide whether dots can be inserted to create at least one valid IPv4 address. We cannot remove, reorder, or replace digits.

For the example "127001", one valid result is "127.0.0.1", so the function returns True.

2. Use one shared octet validator

A helper function validates one possible octet.

It first checks that the part is not empty and that every character is an ASCII digit from "0" to "9". It then rejects a multi-digit part that starts with zero. Finally, it converts the part to an integer and checks that the value is at most 255.

The same helper is used when validating an already dotted address and when testing DFS candidates.

3. Validate an already dotted IPv4 address

If the input contains a dot, split it using ".".

The split must produce exactly four parts. If it produces fewer or more than four, return False.

Validate all four parts with the shared octet helper. Return True only if every part is valid.

For example, "255.255.11.135" is valid. The string "256.1.1.1" is invalid because 256 is above 255. The string "01.2.3.4" is invalid because "01" has a leading zero.

4. Initialize DFS for a digits-only string

A restorable IPv4 address needs at least four digits and at most twelve digits. There are four octets, and each octet contains between one and three digits.

If the digits-only input has fewer than four characters or more than twelve characters, return False immediately.

The DFS state contains:

  • index: the position where the unused part of the string begins
  • path: the valid octets selected so far

The central invariant is that every octet in path is valid, the octets use a continuous prefix of the input, and their order matches the original digit order.

5. Walk through the example "127001"

Start with index 0 and path [].

The search tries candidate lengths from three down to one.

At index 0, the first candidate is "127". It is valid because it contains only digits, has no leading zero, and its value is at most 255. Append it to the path.

The state becomes index 3 and path ["127"].

At index 3, the candidate "001" is rejected because it is a multi-digit octet beginning with zero.

The candidate "00" is rejected for the same reason.

The candidate "0" is valid. Append it.

The state becomes index 4 and path ["127", "0"].

At index 4, the candidate "01" is rejected because it begins with zero and has more than one digit.

The candidate "0" is valid. Append it.

The state becomes index 5 and path ["127", "0", "0"].

At index 5, the candidate "1" is valid. Append it.

The state becomes index 6 and path ["127", "0", "0", "1"].

Now the path contains exactly four octets, and index 6 is the end of the string. The DFS returns True. Processing stops immediately after this valid restoration is found.

6. Explain backtracking and the base case

For each DFS state, the code tries candidate lengths three, two, and one.

If a candidate is valid, it is appended to path. The function then recursively processes the remaining digits.

If the recursive call returns True, the answer has been found, so the function returns True immediately.

If that candidate does not lead to a complete address, path.pop() removes it. This restores the previous state before trying the next candidate. This restoration step is called backtracking.

The base case is reached when path contains four octets. It succeeds only when index is also equal to the input length. This guarantees that exactly four valid octets use every digit.

7. Explain correctness, complexity, and edge cases

The algorithm is correct because invalid octets are never added to path. Every accepted candidate is the next continuous substring, so digit order is preserved. A successful result requires exactly four valid octets and complete use of the string.

For dotted validation, the code processes the input and creates split parts. The time complexity is O(n), and the temporary space is O(n).

For restoration, there are at most three candidate lengths for each of four octets. Therefore, the search examines at most 3^4, or 81, structural choices. Because IPv4 always contains exactly four octets, the recursion depth and path size are both bounded by four, so the auxiliary space is O(1).

Relevant edge cases include "0" as a valid octet, leading-zero candidates such as "01", values above 255, inputs shorter than four digits, inputs longer than twelve digits, and strings that cannot be divided into exactly four valid octets.

Key Insight / Why This Solution Works

Use one helper to validate an octet. For dotted input, split the string and require exactly four valid parts. For digits-only input, use DFS with backtracking. At each state, try the next substring with length three, two, or one. Add it only when it is a valid octet. The invariant is that path always contains valid octets that form a continuous prefix of the original string. The search succeeds only when four octets consume every digit. Failed choices are removed before trying another candidate, and the function stops after finding the first valid restoration.

Code
from typing import List


def valid_ipv4_or_restore(ip: str) -> bool:
    def valid_octet(part: str) -> bool:
        if not part or not all("0" <= char <= "9" for char in part):
            return False
        if len(part) > 1 and part[0] == "0":
            return False
        return int(part) <= 255

    # Case 1: Validate an IPv4 address that already contains dots.
    if "." in ip:
        parts = ip.split(".")
        if len(parts) != 4:
            return False
        return all(valid_octet(part) for part in parts)

    # Case 2: Restore an IPv4 address from digits using DFS.
    n = len(ip)
    if n < 4 or n > 12:
        return False

    def dfs(index: int, path: List[str]) -> bool:
        if len(path) == 4:
            return index == n

        # Not enough or too many digits remain for the missing octets.
        octets_left = 4 - len(path)
        digits_left = n - index
        if digits_left < octets_left or digits_left > octets_left * 3:
            return False

        for length in range(3, 0, -1):
            end = index + length
            if end > n:
                continue

            part = ip[index:end]
            if not valid_octet(part):
                continue

            path.append(part)
            if dfs(end, path):
                return True
            path.pop()

        return False

    return dfs(0, [])


if __name__ == "__main__":
    example = "127001"
    print(valid_ipv4_or_restore(example))  # True
Time & Space Complexity

For an address that already contains dots, let n be the number of characters. Splitting and validating the parts takes O(n) time. Python creates the split parts and substrings, so it uses O(n) temporary space.

For a digits-only string, each of the four octets can try at most three lengths. The search therefore checks at most 3^4, or 81, structural choices. This is a fixed upper bound for IPv4 restoration. The recursion depth is at most 4, and path stores at most 4 octets, so the auxiliary space is O(1).

Where it is used

This logic is useful in network configuration tools, form validation, imported server lists, firewall-rule editors, and systems that clean or verify IP address data. The DFS pattern is also useful when a string must be divided into a fixed number of continuous parts and every part must satisfy strict rules.

Why Interviewers Ask This

This question tests whether a candidate can separate validation from search and reuse one clear helper rule. It checks careful handling of leading zeros, numeric limits, continuous substrings, recursion state, backtracking, and early return. It also shows whether the candidate can define a correct base case, keep the code consistent with a walkthrough, and explain why the IPv4 search has a small fixed bound.

Common interview mistakes

Candidates often accept multi-digit octets such as "01" or "001" even though only the single octet "0" may begin with zero. Another mistake is forgetting to reject values above 255. In DFS, each candidate must be a continuous substring, and the original digit order must stay unchanged. A failed candidate must be removed with path.pop() before another choice is tried. The base case must require both exactly four octets and complete use of the input. The complexity should not be described as exponential in an unbounded n because IPv4 has exactly four octets and at most twelve digits.

Interview tip

Before writing the recursion, state the invariant clearly: path contains only valid octets and represents a continuous prefix of the original string. Then write the success condition as four octets plus complete input consumption.

Interviewer may ask next
How would you return one restored IPv4 address instead of only True or False?

Keep the same DFS and path. When four octets consume all digits, return ".".join(path). Each recursive call should return either a restored string or None. Return the first successful string immediately. Failed candidates still require path.pop(). The search still checks at most 81 structural choices, and the auxiliary recursion space remains O(1).

How would you return every valid restored IPv4 address?

Create a results list and continue searching after a valid address is found. When the base case succeeds, append ".".join(path) to results instead of returning immediately. Backtracking still removes each candidate after its recursive call. The structural search remains bounded by 3^4 choices, but extra output space grows with the number and total size of returned addresses.

98. Max Consecutive Ones IIICodingMedium

Question Details

Given a binary array and an integer k, return the maximum number of consecutive 1s obtainable by changing at most k zeros to ones. Explain the sliding-window state, when the left boundary moves, and the time and space complexity.

Short Interview Answer (30-60 seconds)

I would use a sliding window. The right pointer expands the window one element at a time, while zero_count tracks how many zeros are inside it. If zero_count becomes greater than k, I repeatedly move the left pointer until the window is valid again. I then update the longest valid window. This works because each valid window contains at most k zeros. Both pointers move only forward, so the time complexity is O(n), and the auxiliary space complexity is O(1).

Detailed Explanation

See the Code while reading this explanation.

The problem asks for the longest contiguous part of a binary array that can become all ones after changing at most k zeros. A sliding window is a good fit because it lets us expand a candidate subarray, count its zeros, and shrink it only when it requires more than k changes.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Max Consecutive Ones III diagram
How to Explain It in an Interview
1. Understand the input and required output

The input contains a binary array named nums and an integer k. Every array element is either 0 or 1.

We may change at most k zeros into ones. We must return the maximum length of a contiguous subarray that can become all ones.

The example is:

nums = [1, 1, 0, 0, 1, 1, 1, 0, 1] k = 2

The expected result is 7.

2. Choose the sliding-window approach

The window is the contiguous subarray from left to right, including both boundaries.

The right pointer expands the window. The variable zero_count stores the number of zeros currently inside it.

The main invariant is that after the shrinking loop finishes, the window contains at most k zeros. Therefore, every window used to update the answer can be changed into all ones with at most k changes.

3. Initialize the sliding-window state

Set left to 0 because the first window begins at the first array position.

Set zero_count to 0 because no values have entered the window yet.

Set max_ones to 0 because no valid window length has been recorded.

Then move right from index 0 through index 8.

4. Walk through the exact example

At right = 0, nums[right] is 1. zero_count stays 0. The valid window length is 1, so max_ones becomes 1.

At right = 1, nums[right] is 1. zero_count stays 0. The window length is 2, so max_ones becomes 2.

At right = 2, nums[right] is

  1. zero_count becomes
  2. This is allowed because k is
  3. The window length is 3, so max_ones becomes 3.

At right = 3, nums[right] is 0. zero_count becomes 2. The window is still valid. Its length is 4, so max_ones becomes 4.

At right = 4, the value is

  1. zero_count remains
  2. The window length becomes 5, so max_ones becomes 5.

At right = 5, the value is 1. The window length becomes 6, so max_ones becomes 6.

At right = 6, the value is 1. The window from index 0 through index 6 is [1, 1, 0, 0, 1, 1, 1]. It contains two zeros, so it is valid. Its length is 7, and max_ones becomes 7.

At right = 7, nums[right] is 0. zero_count increases from 2 to 3. Now zero_count is greater than k, so the window must shrink repeatedly.

First, remove index 0. Its value is 1, so zero_count remains 3. left becomes 1.

Next, remove index 1. Its value is also 1, so zero_count remains 3. left becomes 2.

Next, remove index 2. Its value is 0, so zero_count decreases from 3 to 2. left becomes 3.

The valid window is now from index 3 through index 7. Its values are [0, 1, 1, 1, 0]. Its length is 5, so max_ones remains 7.

At right = 8, nums[right] is

  1. zero_count stays
  2. The valid window from index 3 through index 8 is [0, 1, 1, 1, 0, 1]. Its length is 6, so max_ones remains 7.

The final answer is 7. By changing the zeros at indices 2 and 3, the first seven values become [1, 1, 1, 1, 1, 1, 1].

5. Explain why the result is correct

The right pointer considers each new possible window ending position. If the window contains more than k zeros, the left pointer moves until the window contains at most k zeros again.

After shrinking, the current window is always valid. The algorithm records the largest length among these valid windows. Therefore, max_ones is the maximum number of consecutive ones obtainable after changing at most k zeros.

6. Explain the Python implementation

The for loop moves right through the array. When nums[right] is 0, the code increments zero_count.

The while loop runs when zero_count is greater than k. Before moving left, it checks whether nums[left] is 0. If it is, that zero is leaving the window, so zero_count is decremented. The code then increments left.

After the while loop, the window is valid. Its length is right - left + 1. The code compares this length with max_ones and keeps the larger value.

7. Explain complexity and edge cases

The time complexity is O(n). The right pointer visits each array element once. The left pointer also moves only forward and can move at most n times in total.

The auxiliary space complexity is O(1). The algorithm uses only a few integer variables and does not create storage that grows with the input.

If k is 0, the answer is the longest existing run of ones. If every value is 1, the answer is the full array length. If every value is 0, the answer is min(k, n). If k is at least the number of zeros, the answer is n. An empty array returns 0.

Key Insight / Why This Solution Works

Use a variable-size sliding window. Expand the right boundary to include each new array value. Count how many zeros are inside the current window. A window is valid when zero_count is at most k because all of its zeros can be changed into ones. If zero_count becomes greater than k, move the left boundary repeatedly until the window is valid again. The central invariant is that every window used to update max_ones contains at most k zeros. This avoids examining every possible subarray separately.

Code
from typing import List


def longestOnes(nums: List[int], k: int) -> int:
    left = 0
    zero_count = 0
    max_ones = 0

    for right, val in enumerate(nums):
        if val == 0:
            zero_count += 1

        while zero_count > k:
            if nums[left] == 0:
                zero_count -= 1
            left += 1

        max_ones = max(max_ones, right - left + 1)

    return max_ones


if __name__ == "__main__":
    nums = [1, 1, 0, 0, 1, 1, 1, 0, 1]
    k = 2
    print(longestOnes(nums, k))  # 7
Time & Space Complexity

The time complexity is O(n), where n is the length of nums. The right pointer moves across the array once. The left pointer also moves only forward and can move at most n times in total. The inner while loop does not make the total time O(n squared) because an element can leave the window only once. The auxiliary space complexity is O(1) because the algorithm stores only a fixed number of integer variables.

Where it is used

This sliding-window pattern is useful when software must find the longest or shortest contiguous range that stays within a limit. Examples include finding the longest period containing at most a certain number of failures, the largest event range with limited missing records, or the longest text segment containing at most a fixed number of special characters.

Why Interviewers Ask This

This question tests whether a candidate can recognize a variable-size sliding window. It checks whether the candidate can maintain a count while two boundaries move at different times. The interviewer also evaluates repeated shrinking, inclusive window-length calculation, and correct pointer order. Another important part is explaining why a for loop containing a while loop still takes O(n) total time when both pointers move only forward.

Common interview mistakes

One mistake is using an if statement instead of a while loop when zero_count is greater than k. The window may need to remove several values before it becomes valid. Another mistake is forgetting to decrement zero_count when a zero leaves the left side. A candidate may also increment left before checking nums[left], which checks the wrong element. Another error is updating max_ones while the window is still invalid. Finally, this problem requires a contiguous subarray, not a subsequence.

Interview tip

State the invariant before writing the code: after the while loop finishes, the window from left through right contains at most k zeros. Then explain how each update preserves that invariant.

Interviewer may ask next
How would you return the boundaries of one longest valid subarray instead of only its length?

Store best_left and best_right whenever the current valid window is longer than max_ones. Save the current left and right values before updating max_ones. Return [best_left, best_right] at the end. The sliding-window logic does not change. The time complexity remains O(n), and the auxiliary space complexity remains O(1).

How would the solution work if the binary values arrived as a stream?

Store the positions of zeros in a queue. When a new zero arrives, add its position. If the queue contains more than k zero positions, remove the oldest zero position and move left to one position after it. This preserves the rule that the current window contains at most k zeros. The time complexity is O(n), and the auxiliary space complexity is O(k). The tradeoff is the extra queue needed because earlier stream values may no longer be available.

99. Top K Frequent ElementsCodingMedium

Question Details

Given an integer array and an integer k, return the k most frequent values. Produce a solution faster than sorting the full array and explain your use of buckets, a heap, or another suitable structure.

Short Interview Answer (30-60 seconds)

I would use a frequency map and bucket sort. First, I count how many times each value appears. Then I create n + 1 buckets, where bucket i stores values that appear exactly i times. I scan the buckets from the highest frequency down and collect values until I have k results. This avoids sorting the full array or all unique values. The solution uses O(n) expected time and O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to return the k values that appear most often in an integer array. The solution uses a frequency map and frequency buckets. This method fits the problem because a value cannot appear more than n times, where n is the array length. We can use each frequency as a bucket index and avoid sorting the full array or all unique values.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Top K Frequent Elements diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives an integer array named nums and an integer k.

For the example:

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

k = 2

The function must return two values with the highest frequencies.

The frequencies are:

1 appears 3 times.

2 appears 3 times.

3 appears 1 time.

One valid result is [1, 2]. Because 1 and 2 have the same frequency, their order in the returned list is not important.

2. Build the frequency map

I count how many times each distinct value appears.

The frequency map becomes:

1 -> 3

2 -> 3

3 -> 1

Each key is a value from nums. Its mapped value is the number of times that value appears.

3. Create and fill the frequency buckets

The input has n = 7 elements, so I create n + 1 buckets. Their indices range from 0 through 7.

Bucket i stores every value that appears exactly i times.

After placing the values into their correct buckets:

bucket[1] = [3]

bucket[2] = []

bucket[3] = [1, 2]

The remaining buckets are empty.

The important invariant is that every value is stored in the bucket matching its exact frequency.

4. Scan the buckets from high frequency to low frequency

I scan the bucket array from index 7 down to index 1.

Buckets 7 through 4 are empty.

At bucket 3, I find the values [1, 2]. I append them to the result.

The result becomes [1, 2]. Its length is now equal to k, so the function returns immediately.

The lower-frequency value 3 does not need to be collected.

5. Explain why the result is correct

Every value is placed in the bucket that represents its exact frequency. Scanning the bucket indices from high to low therefore processes values with greater frequencies before values with smaller frequencies.

The function stops only after collecting k values. Those values must therefore be among the k most frequent values in the input.

6. Explain the Python implementation

Counter builds the frequency map. The code creates len(nums) + 1 empty lists for the buckets. It places each distinct value into the bucket matching its frequency.

The outer loop scans the buckets from the highest index down to 1. The inner loop processes every value stored in the current bucket. Each value is appended to result. As soon as result contains k values, the function returns it.

7. Explain complexity and edge cases

The expected time complexity is O(n). Counting values uses Python hash-table operations, which take O(1) time on average. Creating the bucket array, filling it, and scanning it each take O(n) time.

The auxiliary space complexity is O(n). The frequency map and bucket array can both grow with the input size.

Relevant cases include k = 1, negative values, values with equal frequencies, and k being equal to the number of unique values. The stated input should keep k between 1 and the number of unique values.

Key Insight / Why This Solution Works

The key idea is to use each frequency as a direct bucket index. A frequency map first stores each distinct input value and the number of times it appears. Bucket i then stores all values that appear exactly i times. The central invariant is that every value remains in the bucket matching its exact frequency. Scanning the buckets from the highest index down processes the most frequent values first. This avoids the O(m log m) cost of sorting m unique values by frequency.

Code
from collections import Counter
from typing import List


def top_k_frequent(nums: List[int], k: int) -> List[int]:
    frequency = Counter(nums)

    # Bucket i stores values that appear exactly i times.
    buckets: List[List[int]] = [[] for _ in range(len(nums) + 1)]

    for value, count in frequency.items():
        buckets[count].append(value)

    result: List[int] = []

    # Process higher frequencies before lower frequencies.
    for count in range(len(buckets) - 1, 0, -1):
        for value in buckets[count]:
            result.append(value)

            if len(result) == k:
                return result

    # Defensive fallback; valid input should return inside the loop.
    return result


if __name__ == "__main__":
    nums = [1, 1, 1, 2, 2, 2, 3]
    k = 2
    print(top_k_frequent(nums, k))
Time & Space Complexity

The expected time complexity is O(n). Counter uses a Python hash table, so counting each value takes O(1) time on average. Creating n + 1 buckets takes O(n). Placing all distinct values into buckets takes at most O(n). Scanning the bucket array also takes O(n). The auxiliary space complexity is O(n) because the frequency map and bucket array can both grow with the number of input elements.

Where it is used

This pattern is useful when software needs to find the most common items, such as popular search terms, frequent error codes, common product IDs, repeated words, or heavily used application features. Frequency buckets work especially well when each count is bounded by the total number of input items.

Why Interviewers Ask This

Interviewers use this problem to test whether a candidate can improve on ordinary sorting. They evaluate frequency counting, correct bucket design, descending traversal, handling of tied frequencies, and the ability to stop after collecting k values. They also check whether the candidate can explain why the bucket indices preserve frequency order and whether the stated time and space complexity matches Python's hash-based implementation.

Common interview mistakes

A common mistake is sorting the full array instead of counting frequencies. Another mistake is storing a frequency inside a bucket instead of storing the original value. Some candidates scan the buckets from low frequency to high frequency, which returns the least frequent values first. Others forget to stop after collecting exactly k values. It is also incorrect to place 2 in bucket 2 for this example because 2 appears three times. Finally, [1, 2] should be described as one valid ordering, not the only possible ordering.

Interview tip

Before writing the code, define the bucket meaning clearly: bucket i stores every value that appears exactly i times. Then trace the example frequencies before scanning the buckets.

Interviewer may ask next
How would the solution change if the values arrived as a continuous stream?

I would maintain a frequency map as values arrive. If top-k results are requested only occasionally, I could build a min heap of size k from the current frequency map when a query arrives. For m unique values, the query would take O(m log k) time and O(k) heap space, in addition to O(m) space for the frequency map. The tradeoff is that the result can be produced without allocating an n-sized bucket array.

Can we use less bucket-array space?

Yes. After building the frequency map, I can maintain a min heap containing at most k value-frequency pairs. If the heap grows beyond k, I remove the pair with the smallest frequency. This takes O(n + m log k) expected time, where m is the number of unique values. It uses O(m + k) auxiliary space including the frequency map. The tradeoff is slower processing than the bucket approach.

100. Course ScheduleCodingMedium

Question Details

Given a number of courses and prerequisite pairs, determine whether all courses can be completed. Model the problem as a directed graph and explain how cycle detection or topological sorting solves it.

Short Interview Answer (30-60 seconds)

I model the courses as a directed graph and use Kahn’s topological sort. For each prerequisite pair [a, b], I add an edge from b to a because course b must be completed first. I count each course’s indegree and place every course with indegree zero into a queue. I process those courses and reduce the indegrees of their dependent courses. If I process all courses, I return True. Otherwise, a cycle exists. The time and auxiliary space complexities are both O(V + E).

Detailed Explanation

See the Code while reading this explanation.

The problem asks whether all courses can be completed when some courses depend on other courses. We represent these dependencies as a directed graph. Then we use Kahn’s topological sort, which is a breadth-first process. It repeatedly takes courses that have no remaining prerequisites. If every course can be processed, the graph has no cycle and all courses can be completed.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Course Schedule diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives numCourses and a list called prerequisites.

Each pair [a, b] means course b must be completed before course a. Therefore, the directed edge goes from b to a.

The function returns True if all courses can be completed. It returns False if a cycle prevents one or more courses from becoming available.

The diagram uses this example:

numCourses = 4 prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]]

The expected result is True.

2. Build the directed graph and indegree array

We use an adjacency list called graph. graph[x] contains every course that becomes closer to available after course x is completed.

We also use an indegree array. indegree[x] is the number of prerequisites that course x still needs.

For the example, the directed edges are:

0 → 1 0 → 2 1 → 3 2 → 3

The adjacency list is:

0: [1, 2] 1: [3] 2: [3] 3: []

The initial indegree array is [0, 1, 1, 2]. Course 0 needs no prerequisite. Courses 1 and 2 each need course 0. Course 3 needs both courses 1 and 2.

3. Initialize the queue and processed count

We place every course with indegree zero into a queue. These courses can be taken immediately because they have no remaining prerequisites.

The initial queue is [0].

We also create a variable named taken. It counts how many courses have been processed. It starts at 0.

The central invariant is that every course placed in the queue has indegree zero. Therefore, all of its prerequisites have already been completed.

4. Walk through the exact example

Initial state:

Queue: [0] Indegree: [0, 1, 1, 2] Taken: 0

Step 1:

Remove course 0 from the queue. Increase taken from 0 to 1.

Course 0 points to courses 1 and 2. Reduce both indegrees by one.

Indegree changes from [0, 1, 1, 2] to [0, 0, 0, 2].

Courses 1 and 2 now have indegree zero, so add them to the queue.

Queue after this step: [1, 2]

Step 2:

Remove course 1. Increase taken from 1 to 2.

Course 1 points to course 3. Reduce course 3’s indegree from 2 to 1.

Indegree becomes [0, 0, 0, 1].

Course 3 is not added to the queue because it still has one remaining prerequisite.

Queue after this step: [2]

Step 3:

Remove course 2. Increase taken from 2 to 3.

Course 2 also points to course 3. Reduce course 3’s indegree from 1 to 0.

Indegree becomes [0, 0, 0, 0].

Course 3 now has no remaining prerequisites, so add it to the queue.

Queue after this step: [3]

Step 4:

Remove course 3. Increase taken from 3 to 4.

Course 3 has no dependent courses, so no indegree changes are needed.

Queue after this step: [].

The queue is now empty. We processed 4 courses, which equals numCourses. Therefore, the function returns True. One valid topological order produced by this processing order is 0 → 1 → 2 → 3.

5. Explain why the algorithm is correct

A course enters the queue only when its indegree becomes zero. This means all of that course’s prerequisites have already been processed.

When we process a course, we remove its effect as a prerequisite by reducing the indegree of each dependent course.

If the graph has no cycle, this process eventually makes every course available. If a cycle exists, every course inside that cycle keeps at least one incoming edge from another course in the same cycle. Those courses never reach indegree zero and never enter the queue.

Therefore, taken equals numCourses exactly when all courses can be completed.

6. Explain the Python implementation

The code first creates one adjacency-list entry for every course and initializes all indegrees to zero.

For every pair [course, prerequisite], it adds course to graph[prerequisite]. It also increases indegree[course].

Next, it creates a deque containing every course whose indegree is zero.

The while loop removes one available course from the front of the queue. It increases taken and visits every dependent course. Each dependent course loses one remaining prerequisite, so its indegree is reduced by one. When that indegree becomes zero, the dependent course is added to the queue.

After the queue becomes empty, the code returns whether taken equals numCourses.

7. Explain complexity and edge cases

Let V be the number of courses and E be the number of prerequisite pairs.

The time complexity is O(V + E). We initialize structures for V courses, examine each of the E prerequisite pairs once, process each course at most once, and process each directed edge once.

The auxiliary space complexity is O(V + E). The adjacency list stores the directed edges. The indegree array stores one number per course. The queue can hold up to V courses.

Important edge cases include no prerequisites, a valid linear chain, a cycle, and disconnected groups of courses. Disconnected groups are handled because every course with indegree zero is placed into the initial queue.

Key Insight / Why This Solution Works

The key insight is that course prerequisites form a directed dependency graph. For each pair [a, b], the edge is b → a because b must be completed before a. Kahn’s topological sort tracks the number of remaining prerequisites for every course. The queue contains only courses with indegree zero. This is the central invariant. Processing a course reduces the indegrees of its dependent courses. If all courses are processed, a valid topological ordering exists. If some courses remain, those courses are blocked by a directed cycle.

Code
from collections import deque


def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
    graph = [[] for _ in range(numCourses)]
    indegree = [0] * numCourses

    for course, prerequisite in prerequisites:
        graph[prerequisite].append(course)
        indegree[course] += 1

    queue = deque(course for course in range(numCourses) if indegree[course] == 0)

    taken = 0

    while queue:
        course = queue.popleft()
        taken += 1

        for dependent_course in graph[course]:
            indegree[dependent_course] -= 1

            if indegree[dependent_course] == 0:
                queue.append(dependent_course)

    return taken == numCourses


if __name__ == "__main__":
    numCourses = 4
    prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]]

    print(canFinish(numCourses, prerequisites))  # True
Time & Space Complexity

Let V be the number of courses and E be the number of prerequisite pairs. The time complexity is O(V + E). We create data for every course, read every prerequisite pair once, process each course at most once, and examine every directed edge once. The auxiliary space complexity is O(V + E). The adjacency list uses O(V + E) space, while the indegree array and queue each use up to O(V) space.

Where it is used

This graph pattern is useful when work has dependencies. Examples include course planning, package installation, build systems, job scheduling, deployment pipelines, and task orchestration. Topological sorting can verify that the dependencies contain no cycle and can also produce one valid execution order.

Why Interviewers Ask This

Interviewers use this problem to test whether you recognize a dependency graph and connect cycle detection with topological sorting. They also evaluate whether you can define edge direction correctly, maintain indegree values, use a queue properly, and explain why unprocessed courses indicate a cycle. The problem tests clean Python implementation, handling of disconnected graph components, maintenance of a clear invariant, and accurate O(V + E) time and space analysis.

Common interview mistakes

A common mistake is reversing the edge. For [a, b], the correct direction is b → a because b must be completed first. Another mistake is increasing the indegree of the prerequisite instead of the dependent course. Candidates may also add a course to the queue before its indegree reaches zero, reduce the wrong neighbor’s indegree, or return True only because the queue became empty. The correct final check is taken == numCourses. Using list.pop(0) instead of deque.popleft() is another avoidable mistake because removing from the front of a list is slower.

Interview tip

Define the edge direction before writing any code. Say, “For [a, b], I add b → a and increase indegree[a].” This prevents the most common error in this problem.

Interviewer may ask next
How would you return one valid course order instead of only True or False?

I would create an order list and append each course when it is removed from the queue. After processing, I would return order if len(order) equals numCourses. Otherwise, I would return an empty list because a cycle exists. The invariant stays the same because a course enters the queue only after all its prerequisites are processed. The time complexity remains O(V + E), and the auxiliary space complexity remains O(V + E).

How does the algorithm handle disconnected groups of courses?

It already handles them. Every course with indegree zero is placed into the initial queue, even when it belongs to a separate graph component. Each component is processed independently. If every component is acyclic, all courses are counted and the function returns True. If any component contains a cycle, some courses remain unprocessed and the function returns False. The time complexity is O(V + E), and the auxiliary space complexity is O(V + E).

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.