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: September 3, 2026)

91. 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.

92. Longest Substring Without Repeating CharactersCodingMedium

Question Details

Given a string, return the length of its longest substring containing no repeated characters. Explain the sliding-window state, how the window moves, and the time and space complexity.

Short Interview Answer (30-60 seconds)

I would use a sliding window with two boundaries, left and right, plus a hash map that stores each character's most recent index. I move right through the string one character at a time. If the character already appears inside the current window, I move left to one position after its previous index. The window always contains unique characters. I track its largest length. This takes O(n) expected time and O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem asks for the length of the longest contiguous substring that contains no repeated characters. A sliding window fits well because it lets us maintain one valid substring while moving through the string. A hash map stores the most recent index of each character. This lets the left boundary jump forward when a repeated character appears.

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?
Longest Substring Without Repeating Characters diagram
How to Explain It in an Interview
1. Understand the input and output

The input is one string. The output is an integer.

The integer represents the maximum length of a substring with no repeated characters. A substring must use consecutive characters from the original string.

For the example abcabcbb, the answer is 3. Valid longest substrings include abc, bca, and cab.

2. Choose the sliding window and hash map

The sliding window is the section of the string between left and right, including both boundaries.

The hash map stores:

character -> most recent index

The main invariant is that the current window always contains unique characters.

The right boundary expands the window. When a repeated character is still inside the current window, the left boundary jumps to one position after that character's previous index.

3. Initialize the state

Set left = 0. This is the beginning of the current window.

Set max_len = 0. This stores the largest valid window length found so far.

Start with an empty hash map called char_index.

Then process each character from left to right using the right index.

4. Walk through the example

The string is abcabcbb.

At index 0, the character is a. It is not in the map. Store a -> 0. The current window is a, so its length is 1. Set max_len to 1.

At index 1, the character is b. It is not in the map. Store b -> 1. The window is ab, so its length is 2. Set max_len to 2.

At index 2, the character is c. It is not in the map. Store c -> 2. The window is abc, so its length is 3. Set max_len to 3.

At index 3, the character is a. Its previous index is 0, which is inside the current window because 0 >= left. Move left to 0 + 1, so left becomes 1. Update the map to a -> 3. The window is now bca, with length 3.

At index 4, the character is b. Its previous index is 1, which is inside the current window. Move left to 2. Update the map to b -> 4. The window is cab, with length 3.

At index 5, the character is c. Its previous index is 2, which is inside the current window. Move left to 3. Update the map to c -> 5. The window is abc, with length 3.

At index 6, the character is b. Its previous index is 4, which is inside the current window. Move left to 5. Update the map to b -> 6. The window is cb, with length 2.

At index 7, the character is b. Its previous index is 6, which is inside the current window. Move left to 7. Update the map to b -> 7. The window is b, with length 1.

The largest length found is still 3, so the function returns 3.

5. Explain why the result is correct

Before updating the answer, the algorithm makes sure the current window contains no repeated characters.

When a repeated character is inside the window, moving left past its previous index removes that duplicate. The left boundary never moves backward.

Because every valid window ending at each right index is considered, the largest recorded window length is the correct answer.

6. Explain the Python implementation

The loop uses enumerate to get both the current index and character.

The condition checks whether the character has appeared before and whether its previous index is still inside the current window.

After adjusting left, the code stores the character's newest index. It then calculates the current length as right - left + 1 and updates max_len.

7. Explain complexity and edge cases

The right boundary processes each character once. The left boundary only moves forward. Python dictionary lookup and insertion take O(1) time on average, so the expected time complexity is O(n).

The hash map may store an index for every distinct character, so the auxiliary space complexity is O(n) in the general case.

Important edge cases include an empty string, a string where every character is the same, a string where every character is unique, and strings containing spaces or symbols.

Key Insight / Why This Solution Works

The key idea is to keep a valid sliding window instead of checking every possible substring. The window is defined by left and right. Its invariant is that all characters between those boundaries are unique. The hash map stores the most recent index of each character. When the character at right was previously seen inside the window, left jumps to one position after that earlier index. Otherwise, left stays unchanged. After that, the algorithm updates the character's latest index and records the current window length. This avoids restarting the search after every duplicate.

Code
def lengthOfLongestSubstring(s: str) -> int:
    char_index: dict[str, int] = {}
    left = 0
    max_len = 0

    for right, char in enumerate(s):
        # Move left only when the previous occurrence
        # is still inside the current window.
        if char in char_index and char_index[char] >= left:
            left = char_index[char] + 1

        # Store the most recent index of this character.
        char_index[char] = right

        # The current window is inclusive of left and right.
        current_length = right - left + 1
        max_len = max(max_len, current_length)

    return max_len


if __name__ == "__main__":
    example = "abcabcbb"
    result = lengthOfLongestSubstring(example)
    print(result)  # 3
Time & Space Complexity

Let n be the number of characters in the string. The expected time complexity is O(n). The right pointer processes each character once, and the left boundary only moves forward. Dictionary lookup and insertion are O(1) on average in Python. The auxiliary space complexity is O(n) in the general case because the hash map may store the most recent index of every distinct character.

Where it is used

This sliding-window pattern is useful when software must analyze consecutive data while maintaining a rule. Examples include finding unique sections of text, checking recent event streams for duplicates, measuring valid ranges in logs, and processing continuous sequences without repeatedly scanning earlier elements.

Why Interviewers Ask This

Interviewers use this problem to test whether a candidate can recognize the sliding-window pattern and maintain a clear invariant. They also check whether the candidate can use a hash map correctly, handle repeated characters without moving the left boundary backward, distinguish a substring from a subsequence, calculate inclusive window length, and explain expected time complexity accurately for Python dictionary operations.

Common interview mistakes

A common mistake is moving left backward when a repeated character appears before the current window. The condition char_index[char] >= left prevents this. Another mistake is using a set but removing only one character when the window may require repeated shrinking. Candidates may also confuse a substring with a subsequence. A substring must be contiguous. Other mistakes include updating the maximum length before fixing a duplicate, using right - left instead of right - left + 1, and claiming O(1) space even though the dictionary can grow with the input.

Interview tip

State the window invariant before writing code: every character between left and right must be unique. Then explain that the hash map lets left jump directly past the previous duplicate instead of moving one step at a time.

Interviewer may ask next
How would you return the actual longest substring instead of only its length?

Keep best_start and best_length along with max_len. Whenever the current window becomes longer than the best window, store its starting index and length. At the end, return s[best_start:best_start + best_length]. The expected time remains O(n). The hash map still uses O(n) auxiliary space. Creating the returned substring requires space proportional to its length.

How would this work if characters arrived as a stream?

Process each new character as the next right position and keep the same left, hash map, and maximum length state between arrivals. Update the window exactly as in the original algorithm. This preserves O(1) average work per arriving character and O(k) space, where k is the number of distinct characters whose latest indices are stored. Returning the actual substring would require retaining the needed stream characters.

93. Group AnagramsCodingMedium

Question Details

Given a list of strings, group together strings that are anagrams of one another. Explain the grouping key you choose and analyze the complexity in terms of the number and length of the strings.

Short Interview Answer (30-60 seconds)

I would use a hash map to group the strings. For each string, I sort its characters to create a signature key. Anagrams contain the same characters, so they produce the same sorted key. I append the original string to the list stored under that key. After processing every string, I return the grouped lists. If n is the number of strings and k is the maximum string length, the expected time is O(n × k log k), and the auxiliary space is O(n × k).

Detailed Explanation

See the Code while reading this explanation.

The input is a list of strings. The output is a list of groups, where each group contains strings that are anagrams of one another. The main idea is to create a common signature for each anagram group. Sorting the characters of a string creates that signature. A hash map then stores all strings with the same signature together.

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?
Group Anagrams diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a list such as ["eat", "tea", "tan", "ate", "nat", "bat"]. We must group strings that contain the same characters with the same frequencies.

One valid output is [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]. The problem does not require one fixed order for the groups.

2. Choose the grouping key

For each string, I sort its characters. The sorted string becomes its signature key.

For example:

"eat" becomes "aet".

"tea" becomes "aet".

"ate" becomes "aet".

Because these strings have the same signature, they belong in the same group.

The strings "tan" and "nat" both produce "ant". The string "bat" produces "abt".

3. Initialize the data structure

I create a defaultdict that maps each signature to a list of original strings. A defaultdict automatically creates an empty list when a signature is used for the first time.

The central invariant is that every string stored under a key has exactly the same sorted characters as that key. Therefore, every list contains only anagrams.

4. Walk through the example

At index 0, the current string is "eat". Sorting it produces "aet". The map becomes {"aet": ["eat"]}.

At index 1, the current string is "tea". Its signature is also "aet". I append it to the existing list. The map becomes {"aet": ["eat", "tea"]}.

At index 2, the current string is "tan". Its signature is "ant". The map becomes {"aet": ["eat", "tea"], "ant": ["tan"]}.

At index 3, the current string is "ate". Its signature is "aet". The map becomes {"aet": ["eat", "tea", "ate"], "ant": ["tan"]}.

At index 4, the current string is "nat". Its signature is "ant". The map becomes {"aet": ["eat", "tea", "ate"], "ant": ["tan", "nat"]}.

At index 5, the current string is "bat". Its signature is "abt". The final map becomes {"aet": ["eat", "tea", "ate"], "ant": ["tan", "nat"], "abt": ["bat"]}.

After all six strings are processed, I return the map values. One valid result is [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]].

5. Explain why the result is correct

Two strings are anagrams when they contain the same characters with the same frequencies. Sorting both strings must therefore produce the same signature.

The algorithm places strings with the same signature under the same map key. Strings with different character collections produce different signatures and are stored in different groups. This means each returned group contains only anagrams, and every input string appears in exactly one group.

6. Explain the Python implementation

The code initializes groups as defaultdict(list). It then processes the strings from left to right.

For each string s, sorted(s) sorts its characters. The expression ''.join(sorted(s)) combines those characters into a signature string. The code appends the original string to groups[key].

After every string has been processed, list(groups.values()) returns all grouped lists.

7. Explain complexity and edge cases

Let n be the number of strings and k be the maximum string length. Sorting one string takes O(k log k). Across n strings, the expected total time is O(n × k log k). Python dictionary lookup and insertion take O(1) time on average.

The stored signature keys can require O(n × k) auxiliary space. Sorting one string temporarily uses O(k) working space. The returned groups contain O(n) references to the original strings.

Important edge cases include an empty input list, a single string, strings with repeated characters, all strings being anagrams, and empty strings. Multiple empty strings produce the same empty signature and are grouped together.

Key Insight / Why This Solution Works

The key insight is that anagrams become identical after their characters are sorted. The algorithm uses this sorted string as a hash map key. Each key maps to a list of original strings with that signature. The invariant is that every string stored under one key has the same sorted characters, so every list contains only anagrams. This is more efficient than comparing each string with every other string because each string can be placed directly into its correct group.

Code
from collections import defaultdict
from typing import List


def groupAnagrams(strs: List[str]) -> List[List[str]]:
    groups = defaultdict(list)

    for s in strs:
        key = "".join(sorted(s))
        groups[key].append(s)

    return list(groups.values())


if __name__ == "__main__":
    strings = ["eat", "tea", "tan", "ate", "nat", "bat"]
    result = groupAnagrams(strings)
    print(result)
Time & Space Complexity

Let n be the number of strings and k be the maximum string length. Sorting one string takes O(k log k). We create a sorted signature for each of the n strings, so the expected total time is O(n × k log k). Python dictionary lookup and insertion are O(1) on average. The stored signature keys can require O(n × k) auxiliary space. Sorting one string temporarily uses O(k) working space. The returned group lists contain O(n) references to the original strings.

Where it is used

This pattern is useful when records must be grouped by a normalized form. Examples include grouping words with the same letters, detecting equivalent text values after normalization, and organizing records that share the same set of attributes. The important idea is to create one stable key that represents every item belonging to the same group.

Why Interviewers Ask This

Interviewers use this problem to test whether a candidate can recognize a grouping pattern and design a reliable hash map key. They also check whether the candidate understands string normalization, repeated characters, empty strings, and valid output ordering. The problem tests clean Python coding with defaultdict and accurate complexity analysis. In particular, the candidate should include the O(k log k) sorting cost for each string and describe Python dictionary operations as average O(1), not guaranteed O(1).

Common interview mistakes

A common mistake is using the original string as the map key instead of creating a shared signature. Another mistake is sorting the input list rather than sorting the characters inside each string. Candidates may compare every pair of strings, which performs unnecessary work. They may also forget that repeated characters matter, so "abb" and "ab" are not anagrams. Another mistake is claiming O(n × k) time even though sorting each string adds a log k factor. It is also incorrect to claim that only one output ordering is valid.

Interview tip

Explain the grouping key before writing the code. Say that the sorted form of each string is the hash map key and the value is the list of original strings with that signature. Then show that "eat", "tea", and "ate" all become "aet". This makes both the algorithm and its correctness easy to understand.

Interviewer may ask next
Can the time complexity be improved if every string contains only lowercase English letters?

Yes. Instead of sorting each string, we can count how many times each of the 26 letters appears. We use the resulting 26-number tuple as the hash map key. Creating the key takes O(k) time per string, so the expected total time becomes O(n × k). The auxiliary space remains O(n × k) for the stored keys and groups. The tradeoff is that this approach depends on a fixed and known alphabet.

Does the current solution preserve the input order inside each group?

Yes. The algorithm processes the input from left to right and appends each string to its group when it is encountered. Therefore, strings inside each group remain in their original relative order. For the displayed input, the "aet" group is ["eat", "tea", "ate"]. The expected time remains O(n × k log k), and the auxiliary space remains O(n × k).

94. 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.

95. 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.

96. 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.

97. 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).

98. Stable In-Place Partition of Positive and Negative NumbersCodingHard

Question Details

Given an array containing positive and negative numbers, rearrange it so that all positive numbers appear before all negative numbers while preserving the original relative order within both groups. Perform the rearrangement in place using O(1) auxiliary space, and explain the algorithm, correctness, and time complexity.

Short Interview Answer (30-60 seconds)

I use a stable insertion-and-shift approach. I scan the array from left to right and remember the index of the first negative number. When I later find a positive number, I save it, shift the negative block one position to the right, and insert the positive number at the remembered index. This preserves the original order of both groups. The worst-case 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 us to move all positive numbers before all negative numbers. We must preserve the original order inside both groups. We must also modify the same array and use only constant extra memory. The diagram uses a stable insertion-and-shift method. It remembers the first misplaced negative number and inserts each later positive number before that negative block.

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?
Stable In-Place Partition of Positive and Negative Numbers diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an array of positive and negative integers.

For the example:

Input: [3, -1, 2, -2, 5, -3, 4, -4]

Output: [3, 2, 5, 4, -1, -2, -3, -4]

The positive numbers keep their original order: 3, 2, 5, 4.

The negative numbers also keep their original order: -1, -2, -3, -4.

The array must be changed in place. This means we modify the original list instead of building another list.

2. Choose the stable insertion-and-shift method

A normal swap is not enough. Swapping a positive number with an earlier negative number can change the order of the negative numbers.

Instead, we remember the position of the first negative number that must move right. When we find a later positive number, we save that positive number. We then shift the whole negative block one place to the right and insert the positive number at the beginning of that block.

This is similar to inserting an item into an earlier position in an array.

3. Initialize the state

We use one variable named first_negative.

It starts at -1.

A value of -1 means that we have not yet found a negative number that appears before a later positive number.

We then scan the array from left to right using index j.

4. Walk through the example

Start with:

array = [3, -1, 2, -2, 5, -3, 4, -4]

first_negative = -1

At j = 0, the value is 3. It is positive. There is no earlier negative block, so the array does not change.

At j = 1, the value is -1. This is the first negative number, so first_negative becomes 1.

At j = 2, the value is 2. A negative block already starts at index 1. We save 2, shift [-1] one place to the right, and insert 2 at index 1.

The array becomes:

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

Then first_negative becomes 2.

At j = 3, the value is -2. It belongs to the negative block, so nothing changes.

At j = 4, the value is 5. We save 5, shift [-1, -2] one place to the right, and insert 5 at index 2.

The array becomes:

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

Then first_negative becomes 3.

At j = 5, the value is -3. Nothing changes.

At j = 6, the value is 4. We save 4, shift [-1, -2, -3] one place to the right, and insert 4 at index 3.

The array becomes:

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

Then first_negative becomes 4.

At j = 7, the value is -4. Nothing changes.

The traversal is complete.

5. Explain why the result is correct

Before each iteration, every positive number before first_negative is already in the correct relative order.

The negative numbers starting at first_negative also remain in their original relative order.

When we find a later positive number, we do not swap it with only one negative number. We shift the complete negative block one place to the right. This keeps the negative values in the same order.

We then insert the positive value at first_negative. This also keeps the positive values in their original order.

Therefore, both groups remain stable.

6. Explain the Python implementation

The function starts with first_negative = -1.

The outer loop visits each index from left to right.

When nums[j] is negative and first_negative is still -1, the code records j as the beginning of the negative block.

When nums[j] is positive and a negative block already exists, the code saves nums[j] in positive.

The inner loop shifts every value from first_negative through j - 1 one position to the right.

The saved positive value is then written at first_negative.

Finally, first_negative moves one position to the right because the positive region has grown by one element.

7. Explain complexity and edge cases

The outer loop visits the array once. However, a positive number may require shifting many earlier negative numbers.

In the worst case, many shifts happen for many positive numbers. Therefore, the worst-case time complexity is O(n²).

The algorithm uses only indices and one temporary value. Therefore, the auxiliary space complexity is O(1).

If all values are positive, the array stays unchanged.

If all values are negative, the array stays unchanged.

If the array is already partitioned, no shifts are needed.

An empty array and a one-element array also remain unchanged.

Key Insight / Why This Solution Works

The key idea is to treat the first misplaced negative number as an insertion position. The variable first_negative marks the beginning of the negative block. When a later positive number is found, that value is saved, the negative block between first_negative and the current index is shifted one position to the right, and the positive value is inserted at first_negative. The central invariant is that positives before first_negative stay in their original order, and the encountered negatives from first_negative onward also stay in their original order.

Code
def stable_partition(nums: list[int]) -> list[int]:
    first_negative = -1

    for j in range(len(nums)):
        if nums[j] < 0:
            if first_negative == -1:
                first_negative = j
        elif nums[j] > 0 and first_negative != -1:
            positive = nums[j]

            for k in range(j, first_negative, -1):
                nums[k] = nums[k - 1]

            nums[first_negative] = positive
            first_negative += 1

    return nums


if __name__ == "__main__":
    numbers = [3, -1, 2, -2, 5, -3, 4, -4]
    result = stable_partition(numbers)
    print(result)
    # Output: [3, 2, 5, 4, -1, -2, -3, -4]
Time & Space Complexity

Let n be the number of elements. The outer loop visits n positions. A positive number may also shift several earlier negative numbers. In the worst case, the total number of shifts grows like 1 + 2 + 3 and so on. Therefore, the worst-case time complexity is O(n²). The algorithm uses only first_negative, loop indices, and one saved value. It does not create another array. Therefore, the auxiliary space complexity is O(1).

Where it is used

This pattern is useful when data must be grouped while keeping the original order inside each group. Examples include moving valid records before invalid records, placing active items before inactive items, or grouping events by a condition when stable order matters and extra memory is limited.

Why Interviewers Ask This

The interviewer is checking whether you understand the difference between ordinary partitioning and stable partitioning. They want to see whether you can preserve relative order without using another array. The problem also tests careful in-place updates, loop direction, invariant reasoning, and accurate complexity analysis. A strong answer explains why simple swapping fails, why shifting preserves order, and why the O(1) space requirement causes the worst-case time to become O(n²).

Common interview mistakes

A common mistake is swapping each positive value with the first negative value. That can change the relative order of the negative numbers. Another mistake is forgetting to save the positive value before shifting, which can overwrite it. Candidates may also shift in the wrong direction. The shift must move from right to left so unread values are not destroyed. Another mistake is incrementing first_negative before the insertion is complete. Finally, claiming O(n) time is incorrect because the inner shifting loop can run many times.

Interview tip

State the invariant before coding: first_negative marks the start of the stable negative block. Then explain that every later positive value is inserted at that position while the whole negative block shifts right.

Interviewer may ask next
How would the solution change if O(n) extra space were allowed?

I could create a new list, first append all positive numbers in their original order, and then append all negative numbers in their original order. I would copy the result back into the original array if in-place output is still required. This keeps the result stable. The time complexity becomes O(n), and the auxiliary space complexity becomes O(n). The tradeoff is faster execution but more memory.

Can this stable partition be done in O(n) time with O(1) auxiliary space?

Not with this simple insertion-and-shift method. Its worst-case time is O(n²) because the same elements may be shifted many times. A divide-and-conquer stable partition can reduce the time to O(n log n), but a straightforward recursive version uses O(log n) call-stack space. Achieving stronger bounds with strict O(1) auxiliary space requires much more advanced techniques and is not the approach shown here. The interview tradeoff is simplicity versus better asymptotic time.

99. Regular Expression MatchingCodingHard

Question Details

Given an input string and a pattern containing ordinary characters, '.' and '*', determine whether the pattern matches the entire input string. Explain how '.' matches one character, how '*' represents zero or more occurrences of the preceding element, the dynamic-programming or memoized state, and the time and space complexity.

Short Interview Answer (30-60 seconds)

I would use dynamic programming. I define dp[i][j] as whether the first i characters of the string fully match the first j characters of the pattern. A normal character or '.' uses the diagonal state. For '*', I either use zero copies of the preceding element or let it consume one more matching character. I fill the table from smaller prefixes to larger prefixes and return dp[m][n]. The time complexity is O(m × n), and the auxiliary space complexity is O(m × n).

Detailed Explanation

See the Code while reading this explanation.

The problem asks whether the pattern matches the entire input string. The pattern can contain ordinary characters, '.', and '*'. A dot matches exactly one character. A star means zero or more copies of the pattern element immediately before it. Dynamic programming works well because the answer for two prefixes can be built from answers for smaller prefixes.

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?
Regular Expression Matching diagram
How to Explain It in an Interview
1. Define the state

Let m be the length of the input string s. Let n be the length of the pattern p.

Create a Boolean table named dp with m + 1 rows and n + 1 columns.

dp[i][j] is True when the first i characters of s completely match the first j characters of p.

The row index represents a string-prefix length. The column index represents a pattern-prefix length.

Set dp[0][0] to True because an empty string matches an empty pattern.

The required answer is dp[m][n].

2. Initialize the empty-string row

An empty string can match a pattern such as "a*" because '*' may use zero copies of the preceding element.

For j from 2 through n, when p[j - 1] is '*', set:

dp[0][j] = dp[0][j - 2]

This ignores the preceding element and its '*'.

For p = "c*a*b", dp[0][2] is True because "c*" can match an empty string. dp[0][4] is also True because both "c*" and "a*" can use zero occurrences.

The complete first row is:

True, False, True, False, True, False

3. Apply the transition rules

Process i from 1 through m. For each row, process j from 1 through n.

If p[j - 1] is the same as s[i - 1], set:

dp[i][j] = dp[i - 1][j - 1]

The same rule is used when p[j - 1] is '.'. A dot matches exactly one character, so the remaining prefixes must also match.

If p[j - 1] is '*', first try zero occurrences of its preceding element:

dp[i][j] = dp[i][j - 2]

If p[j - 2] matches s[i - 1], or p[j - 2] is '.', the star may consume one more character:

dp[i][j] = dp[i][j] or dp[i - 1][j]

The state dp[i - 1][j] keeps the same starred pattern while shortening the string by one character.

4. Walk through the verified example

Use s = "aab" and p = "c*a*b".

The string length is 3. The pattern length is 5. Therefore, the DP table has 4 rows and 6 columns.

The rows represent these string prefixes:

Row 0: empty string Row 1: "a" Row 2: "aa" Row 3: "aab"

The columns represent these pattern prefixes:

Column 0: empty pattern Column 1: "c" Column 2: "c*" Column 3: "c*a" Column 4: "c*a*" Column 5: "c*a*b"

The completed table is:

Row 0: True, False, True, False, True, False Row 1: False, False, False, True, True, False Row 2: False, False, False, False, True, False Row 3: False, False, False, False, False, True

At dp[0][2], "c*" matches the empty string by using zero c characters.

At dp[0][4], "c*a*" also matches the empty string because both starred elements can use zero occurrences.

At dp[1][3], the first 'a' matches the pattern character 'a'. The remaining prefixes are represented by dp[0][2], which is True. Therefore, dp[1][3] becomes True.

At dp[1][4], "a*" can consume the first 'a', so the cell becomes True.

At dp[2][4], the zero-occurrence choice dp[2][2] is False. The one-or-more choice dp[1][4] is True, so "a*" consumes the second 'a'. Therefore, dp[2][4] becomes True.

At dp[3][5], the final 'b' matches the pattern character 'b'. The previous prefixes match at dp[2][4], so dp[3][5] becomes True.

The final result is dp[3][5] = True. Therefore, the pattern matches the entire string.

5. Explain why it is correct

The central invariant is that dp[i][j] correctly records whether s[0:i] completely matches p[0:j].

A normal character and '.' reduce the problem to dp[i - 1][j - 1]. This is correct because each consumes exactly one character from the string and one element from the pattern.

A '*' has exactly two useful choices. It can use zero occurrences through dp[i][j - 2]. It can use one or more occurrences through dp[i - 1][j] when the preceding element matches the current string character.

Every transition reads smaller prefix states that have already been computed. Therefore, dp[m][n] correctly answers whether the complete string matches the complete pattern.

6. Connect the explanation to the Python code

The code creates the table and sets dp[0][0] to True.

It initializes the first row for starred pattern pairs that can match an empty string.

It then fills the table row by row. A direct match or '.' copies the diagonal value. A '*' first uses the zero-occurrence value. When its preceding element matches, it also uses the value from the previous string row in the same pattern column.

The function returns dp[m][n].

7. Explain complexity and edge cases

The table has m + 1 rows and n + 1 columns. Each cell takes constant work, so the time complexity is O(m × n).

The complete table uses O(m × n) auxiliary space.

Important edge cases include an empty string and empty pattern, a pattern such as "a*" matching an empty string, a direct character mismatch without a useful '*', and invalid patterns such as a leading '*'. If valid patterns are guaranteed, separate validation is not needed.

Key Insight / Why This Solution Works

The key insight is to compare prefixes of the string and pattern. The invariant is that dp[i][j] tells whether s[0:i] fully matches p[0:j]. Ordinary characters and '.' consume one character from each side, so they use dp[i - 1][j - 1]. A '*' creates two cases. Zero occurrences use dp[i][j - 2]. One or more occurrences use dp[i - 1][j] when the preceding pattern element matches the current string character. This avoids trying every possible expansion of every star separately.

Code
def is_match(s: str, p: str) -> bool:
    m, n = len(s), len(p)

    dp = [[False] * (n + 1) for _ in range(m + 1)]
    dp[0][0] = True

    # Patterns such as a* or c*a* can match an empty string.
    for j in range(2, n + 1):
        if p[j - 1] == "*":
            dp[0][j] = dp[0][j - 2]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if p[j - 1] == "." or p[j - 1] == s[i - 1]:
                dp[i][j] = dp[i - 1][j - 1]

            elif p[j - 1] == "*" and j >= 2:
                # Use zero occurrences of the preceding element.
                dp[i][j] = dp[i][j - 2]

                # Use one or more occurrences when it matches.
                if p[j - 2] == "." or p[j - 2] == s[i - 1]:
                    dp[i][j] = dp[i][j] or dp[i - 1][j]

    return dp[m][n]


if __name__ == "__main__":
    input_string = "aab"
    pattern = "c*a*b"
    print(is_match(input_string, pattern))  # True
Time & Space Complexity

Let m be the number of characters in the input string and n be the number of characters in the pattern. The algorithm fills a table with about m × n cells. Each cell takes constant work, so the time complexity is O(m × n). The table also stores about m × n Boolean values, so the auxiliary space complexity is O(m × n). Auxiliary space means extra memory used by the algorithm.

Where it is used

This kind of matching is useful in text validation, search filters, routing rules, log processing, and simplified pattern engines. The same prefix-based dynamic programming idea is also useful when two sequences must be compared and one rule can represent several possible choices.

Why Interviewers Ask This

This problem tests whether the candidate can convert pattern rules into a precise dynamic programming state. The interviewer is looking for correct base cases, recurrence design, dependency order, and careful index handling. The question also tests whether the candidate understands the difference between full matching and partial matching, can explain the two meanings of '*', can produce valid Python code, and can justify O(m × n) time and space complexity.

Common interview mistakes

A common mistake is treating '*' as a wildcard that works by itself. It only applies to the pattern element immediately before it. Another mistake is forgetting the zero-occurrence case dp[i][j - 2]. Candidates also use the wrong dependency for repeated matches. The one-or-more case must use dp[i - 1][j], not dp[i - 1][j - 1]. It is also easy to forget first-row initialization for patterns such as "a*". Another mistake is checking whether the pattern matches only part of the string instead of the entire string.

Interview tip

State the meaning of dp[i][j] before writing code. Then explain '*' as two clear choices: remove the starred pair, or let the same starred pattern consume one more matching character.

Interviewer may ask next
Can the auxiliary space be reduced?

Yes. The computation can use two rows because each cell needs values from the current row and the previous row. The time complexity remains O(m × n), while the auxiliary space becomes O(n). The current row must still be filled from left to right because dp[i][j] may use dp[i][j - 2]. The tradeoff is that the optimized code is harder to read and debug.

How would you handle an invalid pattern such as a leading '*'?

Validate the pattern before building the DP table. A '*' must have an element before it, so a leading '*' should be rejected. If the pattern rules also forbid consecutive stars, those can be rejected during the same scan. Validation takes O(n) time and O(1) extra space. The matching algorithm then remains O(m × n) time and O(m × n) auxiliary space.

100. Build a Production Logging DecoratorCodingHard

Question Details

Write a Python decorator that logs the execution timestamp, function name, passed arguments, execution duration, return value, and any exception raised. It must support functions with different signatures and handle errors without changing the original function's behavior. Explain the decorator structure and error-handling approach.

Short Interview Answer (30-60 seconds)

I would build a decorator that accepts any callable signature through *args and **kwargs. Before the call, it records a UTC timestamp and logs the function name and arguments. It then starts time.perf_counter() and calls the original function exactly once. It logs either the return value or the raised exception with the duration. A bare raise preserves the original exception and traceback. functools.wraps preserves metadata. The wrapper adds O(L) logging work and O(L) auxiliary space, where L is the formatted log data size.

Detailed Explanation

See the Code while reading this explanation.

The problem asks for a reusable Python decorator that records when a function runs, what it receives, how long it takes, what it returns, and what exception it raises. The decorator must work with different signatures and must not change the wrapped function's successful result or failure behavior. The solution uses a closure, *args, **kwargs, a monotonic timer, and careful exception re-raising.

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. Define the behavior contract

The decorator must call the original function exactly once with the same positional and keyword arguments.

If the call succeeds, the wrapper must return the exact value produced by the function. If the call fails, the wrapper must re-raise the same exception. Logging is only an observation layer. It must not replace the result or hide the error.

2. Support functions with different signatures

The outer log_execution function receives the callable being decorated. The inner wrapper receives calls through *args and **kwargs.

*args collects positional arguments. **kwargs collects keyword arguments. The wrapper forwards both directly with func(*args, **kwargs).

ParamSpec and TypeVar preserve the callable's parameter and return types for type checkers. functools.wraps preserves metadata such as the original name, documentation, and __wrapped__ link. Inspection tools can use that link to recover the original signature.

3. Record the timestamp and start the timer

At the start of each call, the wrapper records datetime.now(timezone.utc). This gives an explicit UTC execution timestamp.

The wrapper safely logs the timestamp, qualified function name, positional arguments, and keyword arguments. It then reads time.perf_counter() immediately before calling the original function. This clock is designed for measuring elapsed duration and is not affected by normal wall-clock adjustments.

4. Handle a successful call

The wrapper calls the original function once and stores the returned value.

It reads time.perf_counter() again and subtracts the start value. This gives the execution duration in seconds.

The wrapper safely logs the timestamp, function name, duration, and return value. It then returns the same result object to the caller.

5. Handle a failed call

The wrapper catches BaseException only so it can record the failure and immediately re-raise it. This includes normal exceptions and control-flow exceptions such as KeyboardInterrupt and SystemExit.

It calculates the duration and safely logs the exception type, exception value, and traceback. It then uses a bare raise. A bare raise preserves the same exception object and its active traceback.

The wrapper does not return a fallback value. It does not wrap the error in a new exception. It does not call the function again.

6. Prevent logging from changing behavior

Argument or return-value formatting can fail if an object's repr method is broken. _safe_repr catches that formatting error and returns a placeholder.

A logging handler can also fail. _safe_log catches logging-system errors so they do not stop the wrapped function or replace its exception.

These safeguards keep the decorator's main contract intact. The original function's outcome remains the outcome seen by the caller.

7. Explain complexity and edge cases

Let L be the total number of characters created while representing arguments, a return value, or an exception for logging. The wrapper's total added work is O(L), and its added memory is O(L). The timer, timestamp, branching, and local references use O(1) work and space by themselves.

The decorator correctly handles positional arguments, keyword arguments, default arguments, methods, None returns, mutable return objects, and raised exceptions. It preserves the exact returned object. It also re-raises KeyboardInterrupt and SystemExit after attempting to log them. Sensitive values still require a separate redaction policy before production use.

Key Insight / Why This Solution Works

Use a closure-based decorator. The outer function stores the original callable. The wrapper accepts every call through *args and **kwargs, records a UTC timestamp, safely logs the call, starts a monotonic performance timer immediately before execution, and invokes the original function exactly once. On success, it logs the duration and returned object, then returns that same object. On failure, it logs the duration and exception, then uses a bare raise. The central invariant is that logging never changes the arguments sent to the function, the value returned on success, or the exception propagated on failure.

Code
from __future__ import annotations

import logging
import time
from datetime import datetime, timezone
from functools import wraps
from typing import Any, Callable, ParamSpec, TypeVar, cast

P = ParamSpec("P")
R = TypeVar("R")

logger = logging.getLogger(__name__)


def _safe_repr(value: Any, max_length: int = 500) -> str:
    """Create bounded log text without allowing repr() errors to escape."""
    try:
        text = repr(value)
    except BaseException:
        text = f"<unrepresentable {type(value).__name__}>"

    if len(text) > max_length:
        return text[: max_length - 3] + "..."
    return text


def _safe_log(level: int, message: str, *values: Any, exc_info: bool = False) -> None:
    """Prevent logging failures from changing wrapped-function behavior."""
    try:
        logger.log(level, message, *values, exc_info=exc_info)
    except BaseException:
        pass


def log_execution(func: Callable[P, R]) -> Callable[P, R]:
    """Log execution details while preserving the callable's behavior."""

    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        started_at = datetime.now(timezone.utc)

        _safe_log(
            logging.INFO,
            "function_started timestamp=%s function=%s args=%s kwargs=%s",
            started_at.isoformat(),
            func.__qualname__,
            _safe_repr(args),
            _safe_repr(kwargs),
        )

        start_counter = time.perf_counter()
        try:
            result = func(*args, **kwargs)
        except BaseException as error:
            duration_seconds = time.perf_counter() - start_counter
            _safe_log(
                logging.ERROR,
                "function_failed timestamp=%s function=%s duration_seconds=%.6f "
                "exception_type=%s exception=%s",
                started_at.isoformat(),
                func.__qualname__,
                duration_seconds,
                type(error).__name__,
                _safe_repr(error),
                exc_info=True,
            )
            raise

        duration_seconds = time.perf_counter() - start_counter
        _safe_log(
            logging.INFO,
            "function_succeeded timestamp=%s function=%s duration_seconds=%.6f return_value=%s",
            started_at.isoformat(),
            func.__qualname__,
            duration_seconds,
            _safe_repr(result),
        )
        return result

    return cast(Callable[P, R], wrapper)


@log_execution
def divide(total: float, count: float = 1) -> float:
    return total / count


if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s %(message)s",
    )

    print(divide(10, count=2))

    try:
        divide(10, count=0)
    except ZeroDivisionError:
        print("The original ZeroDivisionError reached the caller.")
Time & Space Complexity

Let L be the total size of the text created for the logged arguments, return value, and exception. Creating those representations and log messages takes O(L) added time and O(L) added memory. The timestamp, timer reads, subtraction, condition handling, and local references are O(1). The wrapped function's own time and memory are separate. The code truncates the final logged text, but Python may first build a larger repr, so the representation cost can still grow with the represented value.

Where it is used

This decorator pattern is useful around API handlers, service methods, background jobs, scheduled tasks, data-processing functions, and command handlers. It provides execution timing and failure details without adding the same logging code to every function. In production, teams should redact passwords, tokens, personal data, and other sensitive values before logging arguments or results.

Why Interviewers Ask This

This question tests whether the candidate understands decorators, closures, *args, **kwargs, metadata preservation, typing with ParamSpec, exception propagation, and reliable duration measurement. It also tests production judgment. The candidate should recognize that logging code can fail, arguments may contain sensitive data, and value formatting can be expensive. A strong solution keeps observability separate from business behavior while still producing useful success and failure records.

Common interview mistakes

Common mistakes are swallowing an exception and returning None, wrapping the error in a different exception, or using raise error instead of a bare raise, which changes traceback details. Calling the original function again inside an error path can duplicate side effects. Forgetting functools.wraps loses useful metadata. Using wall-clock time for duration is less reliable than time.perf_counter(). Logging helpers must also be defensive because broken repr methods or logging handlers can otherwise change behavior. Finally, logging secrets or large values without redaction and size controls is unsafe.

Interview tip

Write and say the contract first: forward the same arguments, call once, return the same object on success, and use a bare raise on failure. Then add timestamp, timing, and safe logging around that contract.

Interviewer may ask next
How would you redact passwords and tokens before logging arguments?

Use inspect.signature(func).bind(*args, **kwargs) to map values to parameter names. Replace configured sensitive fields with a fixed marker before formatting the bound arguments. Pass the original args and kwargs to the function unchanged. For p parameters and L formatted characters, the added work is O(p + L) and the added memory is O(p + L). The tradeoff is extra configuration and processing, but correctness is preserved because only the logged copy is changed.

How would you support asynchronous functions?

Use inspect.iscoroutinefunction(func) when creating the decorator. Return an async def wrapper for coroutine functions and call the original function with await func(*args, **kwargs). Keep a normal def wrapper for synchronous functions. Both paths use the same timestamp, timer, safe logging, and bare re-raise rules. Added logging cost remains O(L) time and O(L) space. The tradeoff is maintaining two wrapper implementations.

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.

Content Accuracy and Verification: To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.