460 Python Developer Interview Questions & Answers

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

Python Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

81. Palindrome CheckCodingEasy

Question Details

Given a string, write a Python program that determines whether it reads the same forward and backward. Explain how your solution handles an empty string, a single character, letter case, and the time and space complexity.

Short Interview Answer (30-60 seconds)

I would use two pointers. One starts at the beginning of the string, and the other starts at the end. First, I convert the string to lowercase so letter case does not affect the comparison. While left is before right, I compare the mirrored characters. If they differ, I return False immediately. Otherwise, I move both pointers inward. If the loop finishes, I return True. The time complexity is O(n). The complete Python implementation uses O(n) auxiliary space because lower() creates a new string.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to decide whether a string reads the same forward and backward. The solution uses two pointers. One pointer starts at the first character, and the other starts at the last character. Before comparing them, the code converts the string to lowercase. This makes the check case-insensitive, so "Level" is treated as "level".

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

The input is one string. The function returns True when the string is a palindrome. It returns False when at least one pair of mirrored characters is different.

The solution treats uppercase and lowercase versions of the same letter as equal. For example, the input "Level" becomes "level" before the comparisons begin.

2. Choose the two-pointer approach

We use two integer pointers named left and right.

The left pointer starts at index 0. The right pointer starts at the last index, which is len(text) - 1.

The central invariant is that every mirrored pair outside the current left and right positions has already matched. When the current pair also matches, both pointers can safely move toward the center.

3. Initialize the state

For the example input "Level", the normalized string is "level".

Its length is 5. The indices are 0, 1, 2, 3, and 4.

The characters are:

index 0 = "l" index 1 = "e" index 2 = "v" index 3 = "e" index 4 = "l"

We start with left = 0 and right = 4.

4. Walk through the example

At step 1, the state is left = 0 and right = 4.

We compare text[0], which is "l", with text[4], which is also "l". The characters are equal. We move both pointers inward. The new state is left = 1 and right = 3.

At step 2, we compare text[1], which is "e", with text[3], which is also "e". The characters are equal. We move both pointers inward again. The new state is left = 2 and right = 2.

The loop condition is left < right. At this point, 2 < 2 is false, so the loop stops. The middle character does not need to be compared with itself.

No mismatch was found, so the function returns True.

5. Explain why the result is correct

A palindrome must have equal characters at mirrored positions. The first character must match the last character. The second character must match the second-last character, and so on.

If one mirrored pair is different, the string cannot read the same in both directions. Returning False immediately is therefore correct.

If every required mirrored pair matches until the pointers meet or cross, the whole string is a palindrome. Returning True is therefore correct.

6. Explain the Python implementation

The function first calls s.lower() and stores the new lowercase string in text.

It initializes left to 0 and right to len(text) - 1.

The while loop runs while left < right. Inside the loop, the code compares text[left] and text[right]. If they are different, it returns False immediately.

If they match, left increases by 1 and right decreases by 1. When the loop finishes without a mismatch, the function returns True.

7. Explain complexity and edge cases

The time complexity is O(n), where n is the string length. In the worst case, the algorithm checks all mirrored pairs until the pointers reach the center. The lowercase conversion also takes O(n) time.

The pointer logic itself uses O(1) extra space. However, the complete Python implementation uses O(n) auxiliary space because s.lower() creates a new string.

An empty string returns True because left starts at 0 and right starts at -1, so the loop never runs.

A one-character string returns True because left and right both point to index 0, so the loop never runs.

Different letter cases are handled by converting the input to lowercase.

A string such as "abc" returns False at the first comparison because "a" and "c" do not match.

Key Insight / Why This Solution Works

The key insight is that a palindrome can be verified by comparing characters in mirrored positions. A two-pointer approach does this directly. The left pointer begins at the start, and the right pointer begins at the end. The invariant is that every mirrored pair outside the two pointers has already matched. If the current pair differs, the answer is False. If it matches, both pointers move inward. When the pointers meet or cross, every required pair has matched, so the answer is True. This avoids creating a reversed copy only for comparison, although the shown implementation still creates a lowercase copy for case-insensitive checking.

Code
def is_palindrome(s: str) -> bool:
    text = s.lower()
    left = 0
    right = len(text) - 1

    while left < right:
        if text[left] != text[right]:
            return False
        left += 1
        right -= 1

    return True


example = "Level"
print(is_palindrome(example))
Time & Space Complexity

Let n be the number of characters in the input string. Calling lower() takes O(n) time. The two pointers then compare at most about half of the character pairs, which is also O(n) time. Therefore, the total time complexity is O(n). The two pointer variables use O(1) extra space. However, Python creates a new string when lower() is called, so the complete implementation uses O(n) auxiliary space. If the input were already normalized and no copy were created, the pointer algorithm alone would use O(1) auxiliary space.

Where it is used

This pattern is useful when values must be compared from opposite ends of a sequence. It appears in palindrome validation, symmetry checks, array pair problems, and some sorted-array problems where left and right boundaries move toward each other.

Why Interviewers Ask This

This question tests whether the candidate recognizes the two-pointer pattern and can apply it correctly to strings. It also checks loop boundaries, pointer movement, early return, case normalization, and edge-case reasoning. The interviewer wants to see whether the candidate can connect the code to a correctness invariant and explain complexity accurately, including the extra string created by Python's lower() method.

Common interview mistakes

One mistake is comparing the original characters without normalizing letter case, which makes "Level" incorrectly return False. Another mistake is moving only one pointer or moving a pointer in the wrong direction. A candidate may also forget to return False immediately when a mismatch is found. Another common error is placing return True inside the loop, which may return before all mirrored pairs have been checked. It is also incorrect to claim that the complete shown implementation uses O(1) auxiliary space, because lower() creates a new string of length n.

Interview tip

Explain the invariant before writing the loop: every mirrored pair outside left and right has already matched. Then show that a mismatch returns False and a match moves both pointers inward.

Interviewer may ask next
How would the solution change if the comparison must be case-sensitive?

Do not call lower(). Use the original string directly and keep the same two-pointer loop. The time complexity remains O(n). Because no normalized copy is created, the auxiliary space becomes O(1). Correctness is preserved because the algorithm still compares every required mirrored pair, but uppercase and lowercase letters are now treated as different characters.

How would you ignore spaces and punctuation as well as letter case?

Create a normalized string containing only letters and digits, and convert those characters to one case. Then apply the same two-pointer algorithm to that normalized string. The time complexity is O(n), and the auxiliary space is O(n) because the normalized string may contain up to n characters. The tradeoff is extra memory in exchange for simpler comparisons.

82. Print Unique Values from a List of StringsCodingEasy

Question Details

Given a list of strings that may contain repeated values, print each distinct string without printing duplicates. Explain the data structure used, whether the original order is retained, and the time and space complexity.

Short Interview Answer (30-60 seconds)

I would process the list from left to right and keep a set called seen. For each string, I check whether it is already in the set. If it is new, I print it and add it to seen. If it is already present, I skip it. This keeps the order of first appearance because I traverse the original list. The solution takes O(n) expected time and O(k) auxiliary space, where k is the number of distinct strings.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to print every distinct string once, even when the input contains duplicates. A set is a good fit because it can quickly tell us whether a string has already been printed. We still process the original list from left to right, so the printed values keep their first-appearance order.

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?
Print Unique Values from a List of Strings diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a list of strings. Some strings may appear more than once.

For this example:

["apple", "banana", "apple", "orange", "banana", "grape"]

The printed output is:

apple banana orange grape

We print the string values themselves. We do not return indices or build a separate result list.

2. Choose the data structure

We use a set named seen. The set stores every string that has already been printed.

The central invariant is simple: before each iteration, seen contains exactly the distinct strings printed during earlier iterations.

We do not rely on the set to define the output order. We traverse the original list from left to right, and the set is used only to check whether a value has appeared before.

3. Initialize the state

At the beginning, the set is empty:

seen = set()

Nothing has been printed yet. Processing starts with the first string in the list.

4. Walk through the example

Step 1 processes "apple". The set is empty, so "apple" is not in seen. We print "apple" and add it to the set. The set becomes {"apple"}.

Step 2 processes "banana". It is not in seen. We print it and add it. The set becomes {"apple", "banana"}.

Step 3 processes "apple" again. It is already in seen, so we skip it. The set and printed output stay unchanged.

Step 4 processes "orange". It is not in seen. We print it and add it. The set now contains "apple", "banana", and "orange".

Step 5 processes "banana" again. It is already in seen, so we skip it.

Step 6 processes "grape". It is not in seen. We print it and add it. The set now contains all four distinct strings.

Processing stops after the final element. The printed output is apple, banana, orange, and grape.

5. Explain why the result is correct

A string is printed only when it is not already in seen. Immediately after printing a new string, we add it to seen. Therefore, the same string cannot be printed again later.

Because we process the original list from left to right, each distinct string is printed at its first occurrence. This preserves the order of first appearance.

6. Explain the Python implementation

The function creates an empty set and loops through each string in the input.

The condition string not in seen checks whether the current string has already been printed. When the string is new, the function prints it and adds it to the set. Repeated strings fail the condition and are skipped.

The function prints the values directly. It returns None because no separate result list is required.

7. Explain complexity and edge cases

Let n be the total number of input strings. Each string is processed once. Python set lookup and insertion take O(1) time on average, so the total expected time is O(n).

Let k be the number of distinct strings. The set stores those k strings, so the auxiliary space is O(k). In the worst case, every string is distinct and k equals n.

An empty list prints nothing. If all strings are identical, only the first one is printed. If all strings are unique, every string is printed in the original order. String comparison is case-sensitive, so "Apple" and "apple" are treated as different values.

Key Insight / Why This Solution Works

The key idea is to separate traversal order from duplicate detection. We traverse the original list from left to right so the first-appearance order is preserved. A set named seen remembers which strings have already been printed. Before each iteration, seen contains exactly the distinct strings printed from earlier positions. If the current string is not in seen, we print it and add it. Otherwise, we skip it. This avoids repeatedly searching through the earlier part of the list.

Code
def print_unique_strings(strings: list[str]) -> None:
    seen: set[str] = set()

    for string in strings:
        if string not in seen:
            print(string)
            seen.add(string)


if __name__ == "__main__":
    data = ["apple", "banana", "apple", "orange", "banana", "grape"]
    print_unique_strings(data)
Time & Space Complexity

Let n be the total number of strings and k be the number of distinct strings. We examine every input string once. A Python set lookup and insertion take O(1) time on average, so the total expected time is O(n). The set stores up to k distinct strings, so the auxiliary space is O(k). If every string is different, k equals n. The O(n) time is expected time because hash-set operations are average O(1), not guaranteed O(1) in every worst-case situation.

Where it is used

This pattern is useful when software must remove repeated values while preserving the order in which values first appeared. Examples include filtering duplicate usernames from imported data, printing unique log categories, removing repeated tags, and processing each event name only once.

Why Interviewers Ask This

The interviewer is checking whether the candidate can recognize a duplicate-removal problem and select a suitable data structure. The question tests whether the candidate understands how a set detects repeated values while traversal of the original list preserves order. It also evaluates duplicate handling, edge-case reasoning, executable Python code, and accurate explanation of expected time and auxiliary space.

Common interview mistakes

One mistake is converting the list directly to a set and printing the set. That removes duplicates, but it does not clearly preserve the order of first appearance. Another mistake is forgetting to add a printed string to seen, which allows later duplicates to be printed. A candidate may also add the string before checking membership, causing every value to appear already seen. Using a list instead of a set for membership checks can make the solution O(n²). It is also incorrect to describe the hash-based running time as guaranteed O(n) instead of expected O(n).

Interview tip

State the invariant clearly: before each iteration, seen contains exactly the strings already printed. Then explain that traversing the original list preserves first-appearance order while the set prevents duplicate output.

Interviewer may ask next
How would you handle the strings if they arrive one at a time as a stream?

Keep the same seen set between incoming items. For each new string, check whether it is in seen. If it is new, print it and add it. This preserves arrival order. Processing m items takes O(m) expected time, and auxiliary space is O(k), where k is the number of distinct strings received. The tradeoff is that the set may continue growing.

How would you make duplicate checking case-insensitive while printing the first original spelling?

Create a normalized key with string.casefold(). Store the normalized key in seen, but print the original string when that key is new. For example, "Apple" and "apple" would share one key, so only the first spelling would be printed. The expected time remains O(n), and auxiliary space remains O(k). The tradeoff is the extra work and memory used for normalized strings.

83. Reverse a String Using RecursionCodingEasy

Question Details

Given a string, reverse it using recursion. Explain the recursive call, the base case, how characters are combined during the return path, and the time and auxiliary space complexity.

Short Interview Answer (30-60 seconds)

I solve this by recursion. If the string has zero or one character, I return it because it is already reversed. Otherwise, I recursively reverse everything after the first character, then append the first character to the returned string. For "code", the return path builds "e", "ed", "edo", and finally "edoc". For this exact Python implementation, the time complexity is O(n²), and the auxiliary space complexity is O(n²) because slicing and concatenation create new strings.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to reverse a string by using recursion. The idea is to reduce the problem by one character in each call. We save the first character, recursively reverse the remaining substring, and append the saved character while the calls return. This gives the exact result shown in the diagram.

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?
Reverse a String Using Recursion diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a string named s. The function must return a new string containing the same characters in reverse order.

For the example:

Input: "code"

Output: "edoc"

The function returns characters in reversed order. It does not return indices or modify the original string.

2. Define the base case and recursive rule

The base case is:

If len(s) <= 1, return s.

An empty string and a one-character string are already reversed, so recursion can stop.

For a longer string, the rule is:

reverse(s) = reverse(s[1:]) + s[0]

The expression s[1:] is the substring after the first character. The expression s[0] is the first character. The function first reverses the smaller substring. It then adds the saved first character to the end.

3. Follow the recursive calls

The first call is reverse_string("code"). Its length is 4, so it calls reverse_string("ode") and waits to append 'c'.

The second call is reverse_string("ode"). Its length is 3, so it calls reverse_string("de") and waits to append 'o'.

The third call is reverse_string("de"). Its length is 2, so it calls reverse_string("e") and waits to append 'd'.

The fourth call is reverse_string("e"). Its length is 1, so the base case returns "e" immediately.

The recursive inputs are therefore:

"code" -> "ode" -> "de" -> "e"

4. Build the result during the return path

After the base case returns, the waiting calls continue in reverse order.

The call for "de" receives "e" and calculates "e" + "d". It returns "ed".

The call for "ode" receives "ed" and calculates "ed" + "o". It returns "edo".

The call for "code" receives "edo" and calculates "edo" + "c". It returns "edoc".

The exact return sequence is:

"e" -> "ed" -> "edo" -> "edoc"

5. Explain why the algorithm is correct

The base case is correct because a string with zero or one character is already reversed.

For a longer string, assume the recursive call correctly reverses s[1:]. The original first character s[0] must appear after all the other characters in the reversed result. Appending s[0] to reverse(s[1:]) places it in exactly that position.

Therefore, reverse(s[1:]) + s[0] correctly reverses the full string.

6. Explain the Python implementation

The function receives s as a string and returns a string.

It first checks len(s) <= 1. If that condition is true, it returns s.

Otherwise, it creates the smaller substring s[1:] and passes it to the same function. When that call returns, the code adds s[0] to the end of the returned substring.

Python uses the recursion call stack to remember each waiting call. No hash map, queue, or explicit stack is created.

7. Explain complexity and edge cases

Let n be the length of the string.

The time complexity is O(n²) for this exact Python implementation. Each call creates a slice with s[1:]. Each return also creates a new string during concatenation. These operations repeatedly copy characters.

The auxiliary space complexity is O(n²) overall because the recursive calls retain copied substrings whose total size is quadratic. The recursion depth itself is O(n).

An empty string returns "". A one-character string such as "a" returns "a". Repeated characters work normally, so "aab" becomes "baa". Spaces and punctuation are also reversed as normal characters.

Key Insight / Why This Solution Works

The key insight is to solve the same problem on a shorter string. Each call removes the first character and recursively reverses the remaining substring. The call stack remembers the removed characters. During the return path, each saved character is appended to the end of the smaller reversed result. The central invariant is: for every string s with length greater than one, reverse(s) equals reverse(s[1:]) plus s[0]. The base case handles strings of length zero or one.

Code
def reverse_string(s: str) -> str:
    if len(s) <= 1:
        return s

    return reverse_string(s[1:]) + s[0]


if __name__ == "__main__":
    example = "code"
    result = reverse_string(example)
    print(result)  # edoc
Time & Space Complexity

Let n be the number of characters. The time complexity is O(n²) for this exact Python code. The slice s[1:] copies characters in every recursive call. The + operation also creates and copies a new string during every return. These repeated copies add up to quadratic work. The auxiliary space complexity is O(n²) overall because the calls retain copied substrings. The recursion stack has O(n) depth.

Where it is used

This pattern is useful for learning recursive problem solving. It shows how to reduce an input, solve the smaller problem, and combine the result while calls return. Similar ideas are used in recursive string processing, linked-list algorithms, tree traversal, and divide-and-conquer problems. For large strings in real Python programs, an iterative method or s[::-1] is usually more practical.

Why Interviewers Ask This

Interviewers use this problem to check whether a candidate understands recursion. They want to see a correct stopping condition, a smaller recursive input, and a clear explanation of how partial results are combined. The problem also tests whether the candidate can trace the call stack in both directions. In Python, a strong answer should also recognize that string slices and concatenations create new objects, which changes the time and auxiliary space complexity.

Common interview mistakes

A common mistake is forgetting the base case. That causes recursion to continue until Python raises an error. Another mistake is writing s[0] + reverse_string(s[1:]), which keeps the original order instead of reversing it. Some candidates use a different slice but keep the same return logic, which makes the result incorrect. Another mistake is explaining the downward calls but not the return path. Candidates also often claim O(n) time or O(n) space without counting Python slicing, concatenation, and copied substrings.

Interview tip

Write the recursive rule first: reverse(s) = reverse(s[1:]) + s[0]. Then trace "code" down to "e" and back up to "edoc". This clearly shows both the base case and the return path.

Interviewer may ask next
Can we reduce the copying cost while still using recursion?

Yes. Convert the string to a list and recursively swap the left and right characters by using two indices. This avoids creating a new substring in every call. The swaps take O(n) time. The recursion stack uses O(n) auxiliary space, and the character list uses O(n) space. The tradeoff is that the method is more complex and works on a mutable list because Python strings cannot be changed in place.

What should we use for a very large string?

A recursive solution may reach Python's recursion limit, so an iterative method or s[::-1] is safer. The expression s[::-1] creates the reversed string in O(n) time and uses O(n) space for the result. The tradeoff is that it does not demonstrate the recursive process required by the original question.

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

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

86. Minimum Rotations to Type a String with Multiple Circular DialsCodingHard

Question Details

You are given k circular dials containing the letters A through Z and a target string. Every dial initially points to A. In one move, rotate one dial one step clockwise or counterclockwise. A target character can be typed when at least one dial points to that character. Find the minimum total number of rotations required to type the target string in order, and explain the state representation, transition choices, edge cases, and complexity.

Short Interview Answer (30-60 seconds)

I would use dynamic programming over the dial positions. A state is a sorted tuple of the current positions of all k dials. I start with every dial at A. For each target character, I try moving every dial, add the shorter circular distance, sort the new positions, and keep the lowest cost for each state. This preserves all meaningful choices and avoids an unsafe greedy decision. The shown code takes O(n · S · k² log k) time and O(S · k) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We need to type the target string in order using k circular dials. Every dial starts at A. Moving one dial by one letter costs one rotation. A greedy choice is not always safe because the cheapest move now can create a worse dial arrangement for later characters. The diagram uses dynamic programming to keep the minimum cost for every reachable canonical dial configuration.

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 Rotations to Type a String with Multiple Circular Dials diagram
How to Explain It in an Interview
1. Understand the input and required output

The input contains an integer k and a target string made from uppercase letters A through Z.

Each of the k dials starts at A, which has index 0. The remaining letters use indices 1 through 25.

For each target character, at least one dial must point to that character. Typing the character itself has no extra cost. We return the minimum total number of rotations needed to type the complete target string in order.

In the diagram example, k = 2 and target = "CBC". The minimum result is 3.

2. Define the dynamic programming state

A state is a sorted tuple containing the current positions of all k dials.

For example, the state (0, 2) means that one dial points to A and one dial points to C.

The tuple is sorted because the physical names of identical dials do not affect future costs. The configurations (0, 2) and (2, 0) represent the same useful arrangement, so they should share one state.

The dictionary dp maps each canonical state to the minimum cost needed to type the target prefix processed so far and finish in that state.

The central invariant is: after each processed character, dp[state] stores the lowest cost for typing that prefix and ending with exactly that canonical dial configuration.

3. Initialize the state

Every dial starts at A, so every position is 0.

For k = 2, the initial state is (0, 0).

The initial dictionary is {(0, 0): 0}. The cost is 0 because no dial has moved and no target character has been processed.

4. Process each target character

For each target character, we convert it to an index from 0 through 25.

For every current state, we try moving every tuple entry to the target position. Tuple-entry indices are temporary positions inside the sorted state. They are not permanent physical dial identities.

The circular movement cost from position a to position b is:

min(|a - b|, 26 - |a - b|)

This chooses the shorter movement direction around the circular alphabet.

After moving one dial, we sort the resulting k positions. This creates the canonical next state. If different transitions reach the same state, we keep only the smallest total cost.

5. Walk through the example

The input is k = 2 and target = "CBC".

We start with dp = {(0, 0): 0}.

The first character is C, which has index 2. Moving either dial from A to C costs 2. Both choices become the same sorted state (0, 2). The new dictionary is {(0, 2): 2}.

The second character is B, which has index 1. From state (0, 2), moving the tuple entry at position 0 from A to B costs 1 and creates canonical state (1, 2) with total cost 3. Moving the tuple entry at position 1 from C to B also costs 1 and creates canonical state (0, 1) with total cost 3. The new dictionary is {(0, 1): 3, (1, 2): 3}.

The final character is C, which has index 2.

From state (0, 1) with cost 3, moving A to C costs 2 and produces (1, 2) with total cost 5. Moving B to C costs 1 and produces (0, 2) with total cost 4.

From state (1, 2) with cost 3, moving B to C costs 1 and produces (2, 2) with total cost 4. One dial already points to C, so selecting that tuple entry costs 0 and keeps canonical state (1, 2) with total cost 3.

The final states include (1, 2) with cost 3, (0, 2) with cost 4, and (2, 2) with cost 4. The minimum is 3.

One concrete optimal physical sequence before canonical sorting is to move Dial 1 from A to C for cost 2, move Dial 2 from A to B for cost 1, and reuse Dial 1 at C for cost 0. The final answer is 3.

6. Explain why the result is correct

At every target character, the algorithm tries every possible dial move from every reachable state.

It discards a path only when another path reaches the same canonical state with a lower cost. Therefore, no cheaper meaningful configuration is lost.

By induction, after every processed prefix, each stored state has its minimum possible cost. After the complete target is processed, the minimum value in dp is the globally minimum number of rotations.

7. Explain the Python implementation, complexity, and edge cases

The code uses one dictionary for the current target prefix and another dictionary for the next prefix. Each dictionary key is a sorted tuple of dial positions. Each value is the minimum cost for that state.

Let n be the target length and S be the number of reachable canonical states during one target step. Each of the S states tries k dial moves. For each move, the code copies k positions and sorts the resulting tuple in O(k log k) time. The shown code therefore takes O(n · S · k² log k) time.

The dictionaries can store up to S tuples, and each tuple contains k positions. The auxiliary space is O(S · k). Across all possible configurations, S is at most C(k + 25, k), although the reachable set for one target prefix may be smaller.

Important edge cases are an empty target, a character already covered by a dial, clockwise and counterclockwise wrap-around, repeated characters, and k = 1.

Key Insight / Why This Solution Works

The key insight is that the cheapest immediate move is not always part of the cheapest complete sequence. Each move changes the dial arrangement, and that arrangement affects later characters. Dynamic programming preserves these future choices. The state is a sorted tuple of all dial positions. Sorting removes unnecessary identity from identical dials, so equivalent arrangements share one state. For each target character, the algorithm tries moving every dial from every current state, adds the circular distance, and retains the minimum cost for each resulting canonical state. The invariant is that dp[state] is the minimum cost for typing the processed prefix and ending in that state.

Code
from typing import Dict, Tuple


def min_rotations(k: int, target: str) -> int:
    initial = tuple([0] * k)
    dp: Dict[Tuple[int, ...], int] = {initial: 0}

    for ch in target:
        target_pos = ord(ch) - ord("A")
        next_dp: Dict[Tuple[int, ...], int] = {}

        for state, current_cost in dp.items():
            for dial_index in range(k):
                current_pos = state[dial_index]
                diff = abs(current_pos - target_pos)
                move_cost = min(diff, 26 - diff)

                positions = list(state)
                positions[dial_index] = target_pos
                next_state = tuple(sorted(positions))
                candidate = current_cost + move_cost

                if candidate < next_dp.get(next_state, float("inf")):
                    next_dp[next_state] = candidate

        dp = next_dp

    return min(dp.values(), default=0)


if __name__ == "__main__":
    k = 2
    target = "CBC"
    print(min_rotations(k, target))  # 3
Time & Space Complexity

Let n be the number of characters in the target. Let S be the number of reachable canonical dial configurations during one target step. For each character, the code processes up to S states. From each state, it tries k dial entries. For each choice, it copies k positions and sorts them in O(k log k) time. Therefore, the shown code takes O(n · S · k² log k) time. Python dictionary lookup and update are O(1) on average. The dictionaries store up to S tuples of length k, so the auxiliary space is O(S · k). Across all configurations, S is at most C(k + 25, k).

Where it is used

This pattern is useful when several interchangeable resources can handle an ordered sequence of requests and each choice changes the cost of later choices. Examples include movable cursors, robotic arms, machine heads, or identical workers assigned to ordered tasks. Canonical states are useful when resource names do not affect future decisions and equivalent arrangements can be merged.

Why Interviewers Ask This

This question tests whether the candidate can recognize when a greedy choice is unsafe and replace it with dynamic programming. It also checks state design, transition reasoning, circular-distance calculation, and the ability to remove unnecessary identity through canonical sorting. The interviewer can evaluate whether the candidate preserves all meaningful choices, maintains a clear invariant, writes correct dictionary updates, handles wrap-around and repeated characters, and includes tuple copying and sorting in the complexity analysis.

Common interview mistakes

A common mistake is choosing the dial with the smallest immediate rotation cost. That greedy choice can produce a worse arrangement for later characters. Another mistake is keeping permanent physical dial identities in the canonical dynamic programming state. Equivalent arrangements should be sorted into one tuple. Candidates reaching the same state must keep only the lowest cost. It is also easy to forget circular wrap-around and use only the direct distance. Finally, the complexity must include copying and sorting each k-position tuple.

Interview tip

Define dp[state] before writing code. Say that state is a sorted tuple and dp[state] is the minimum cost after the processed prefix. Then explain one transition and why canonical sorting safely merges equivalent dial arrangements.

Interviewer may ask next
How could you reduce the sorting work in each transition?

Because the state is already sorted and only one position changes, we could remove the selected entry and insert the target position into the correct sorted location instead of sorting all k values again. A straightforward Python-list implementation would take O(k) work for each transition. Since each state tries k transitions, the total time would become O(n · S · k²). The auxiliary space would remain O(S · k). The tradeoff is more complicated transition code.

How would the solution change if each dial had a different rotation cost per step?

Physical dial identity would then matter because moving different dials could have different costs. We could no longer sort the positions and merge permutations. The state would keep positions in fixed dial order. For each target character, we would try every dial and multiply its circular distance by that dial's step cost. The invariant would remain the minimum cost for each ordered state. If S is the number of reachable ordered states, the time would be O(n · S · k²) with tuple copying, and the auxiliary space would be O(S · k). The main tradeoff is a larger state space.

87. Two SumCodingEasy

Question Details

Given an integer array and a target value, return the indices of two different elements whose values add up to the target. Assume exactly one valid pair exists, and explain the time and space complexity of your Python solution.

Short Interview Answer (30-60 seconds)

I would solve this with a one pass hash map. The map stores each value I have already seen and its index. For each current value, I calculate the complement needed to reach the target. I check the map before storing the current value, so I cannot reuse the same element. When the complement is found, I return the earlier index and the current index. This takes O(n) expected time and O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem gives an integer array and a target. We must return the indices of two different elements whose values add up to the target. A one pass hash map works well because it lets us check whether the needed earlier value has already appeared.

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?
Two Sum diagram
How to Explain It in an Interview
1. Understand the required output

The input is an integer array and a target value. The answer must contain two indices, not the two values themselves. The problem guarantees exactly one valid pair, and the two indices must refer to different elements.

2. Use a hash map of earlier values

I create an empty dictionary named seen. It stores each processed value as the key and that value's earlier index as the dictionary value. The important rule is that seen contains only elements that appeared before the current index.

3. Calculate the complement before inserting

For each value, I calculate complement = target - value. The complement is the number needed to complete the target. I check whether that complement is already in seen before storing the current value. This order prevents the same array element from being used twice.

4. Walk through the example

The diagram uses nums = [2, 7, 11, 15] and target = 9. At index 0, the value is 2, so the complement is 7. The map is empty, so 7 is not found. I store 2: 0 in the map.

At index 1, the value is 7, so the complement is 2. The map contains 2: 0. Therefore, the earlier index is 0 and the current index is 1. I return [0, 1] and stop. The later elements are not processed.

5. Explain why it works

Before each lookup, the map contains only values from earlier indices. If the complement is present, its stored index is different from the current index. Their values add to the target by definition of the complement. This gives a valid pair.

6. Explain the Python code and complexity

The loop uses enumerate to get each value and its index. Dictionary lookup and insertion are O(1) on average. We process the array at most once, so the expected time is O(n). In the worst case, the map stores up to n values, so the auxiliary space is O(n).

Key Insight / Why This Solution Works

The key insight is to remember values that appeared earlier instead of checking every possible pair. For each current value, the algorithm asks whether the exact complement needed to reach the target is already in the map. The invariant is that the map stores only earlier values and their indices. Checking before insertion prevents reuse of the current element. This is more suitable than the direct nested loop, which may compare O(n²) pairs.

Code
def two_sum(nums: list[int], target: int) -> list[int]:
    seen: dict[int, int] = {}

    for index, value in enumerate(nums):
        complement = target - value

        if complement in seen:
            return [seen[complement], index]

        seen[value] = index

    return []  # Defensive fallback; the stated problem guarantees a solution
Time & Space Complexity

Let n be the number of elements. We process each element at most once and stop when the valid pair is found. Each Python dictionary lookup and insertion is O(1) on average, so the total expected time is O(n). The algorithm uses a dictionary to store earlier values and their indices. In the worst case, that dictionary can contain up to n entries, so the auxiliary space is O(n). This is the standard optimal expected-time approach for an unsorted array.

Where it is used

This pattern is useful when software must find two related records or values quickly. Similar hash map lookups appear in matching transactions, finding complementary quantities, checking previously seen identifiers, and joining small in-memory datasets by key.

Why Interviewers Ask This

Interviewers use this question to see whether a candidate can replace a nested loop with a suitable data structure. They also evaluate whether the candidate preserves original indices, handles duplicate values, avoids reusing the same element, explains a clear invariant, writes correct Python, and gives accurate expected-time and space complexity.

Common interview mistakes

Common mistakes include returning [2, 7] instead of the required indices [0, 1], inserting the current value before checking the complement, and accidentally reusing the same element. Candidates may also sort the array and lose the original indices, forget that duplicate values such as [3, 3] must work, or claim guaranteed O(n) time instead of O(n) expected time for Python dictionary operations.

Interview tip

Before coding, say clearly: “My dictionary stores each earlier value and its index, and I check the complement before inserting the current value.”

Interviewer may ask next
What would change if the problem did not guarantee that a valid pair exists?

The main algorithm would stay the same. I would scan the array and return the two indices as soon as a complement is found. If the loop finishes without finding a pair, I would return a result required by the API, such as an empty list, None, or raise a clear exception. The choice should be stated in the function contract. The expected time remains O(n), and the auxiliary space remains O(n).

Could you solve it with less extra space if the array were sorted?

Yes. For a sorted array, I could use two pointers. One starts at the beginning and one at the end. If their sum is too small, I move the left pointer right. If the sum is too large, I move the right pointer left. This takes O(n) time and O(1) auxiliary space. If the original indices are required and the input is not already sorted, sorting would need index tracking and would increase the time to O(n log n).

88. Binary SearchCodingEasy

Question Details

Given a sorted array of integers and a target value, return the target's index or minus one when it is absent. Implement logarithmic-time binary search and explain boundary handling.

Short Interview Answer (30-60 seconds)

I would use iterative binary search because the array is already sorted. I keep two inclusive boundaries, left and right, and repeatedly check the middle index. If nums[mid] equals the target, I return mid. If nums[mid] is smaller, I search the right half. Otherwise, I search the left half. When left becomes greater than right, the target is absent, so I return minus one. The time complexity is O(log n), and the auxiliary space is O(1).

Detailed Explanation

See the Code while reading this explanation.

The problem gives a sorted array and asks for the index of a target value. Because the values are sorted, we do not need to check every element one by one. The main idea is to compare the target with the middle value and remove half of the remaining search range each time.

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

The input is a sorted array of integers and a target value. The output is the target index if the target exists. If the target is absent, the function returns minus one. The answer is an index, not the value itself.

In the example, nums is [-1, 0, 3, 5, 9, 12] and target is 9. The value 9 appears at index 4, so the expected output is 4.

2. Choose binary search

Binary search fits because the array is sorted. At each step, we check the middle value. If the middle value is too small, everything to its left is also too small. If the middle value is too large, everything to its right is also too large.

The invariant is that the target, if it still exists, must be inside the current left to right interval. Each update keeps that invariant true and makes the interval smaller.

3. Initialize the boundaries

We start with left = 0 and right = len(nums) - 1. These are inclusive boundaries, which means both ends are still part of the search range. The loop continues while left <= right because a one element range is still valid.

The midpoint is computed as left + (right - left) // 2. This gives the middle index of the current range. It also avoids overflow in languages with fixed size integers.

4. Walk through the example

At step 1, left is 0 and right is 5. The middle index is 2, and nums[2] is 3. Since 3 is less than 9, the target must be to the right. We set left = mid + 1, so left becomes 3.

At step 2, left is 3 and right is 5. The middle index is 4, and nums[4] is 9. This equals the target, so we return 4 immediately. No later elements need to be processed.

5. Explain correctness and edge cases

The algorithm is correct because each comparison removes only the half that cannot contain the target. If nums[mid] is smaller than the target, all values at or before mid are too small. If nums[mid] is larger than the target, all values at or after mid are too large.

Important edge cases are an empty array, one element, target at the first index, target at the last index, and target absent. If duplicates are allowed, this implementation returns one matching index, not always the first.

6. Explain the Python code

The code stores the current interval in left and right. Inside the loop, it calculates mid and compares nums[mid] with target. A match returns mid. A smaller middle value moves left to mid + 1. A larger middle value moves right to mid - 1. If the loop ends, no valid index remains, so the code returns -1.

Key Insight / Why This Solution Works

The key insight is that sorted order lets us discard half of the search space after each comparison. The algorithm keeps an inclusive interval from left to right. The central invariant is this: if the target still exists, it must be inside that interval. The midpoint divides the interval into two halves. When nums[mid] is smaller than the target, the left half cannot contain the answer. When nums[mid] is larger than the target, the right half cannot contain the answer. Each update reduces the interval until the target is found or the interval becomes empty.

Code
from typing import List


def binary_search(nums: List[int], target: int) -> int:
    left, right = 0, len(nums) - 1

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

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

    return -1


if __name__ == "__main__":
    nums = [-1, 0, 3, 5, 9, 12]
    target = 9
    print(binary_search(nums, target))
Time & Space Complexity

The time complexity is O(log n), where n is the number of elements in the array. This happens because each loop removes about half of the remaining search range. For the example, the target is found after two checks. The auxiliary space complexity is O(1). The iterative solution uses only a few variables: left, right, and mid. It does not create another array, map, stack, or recursion call stack.

Where it is used

Binary search is useful when data is sorted and we need fast lookup. It is used in search features, index lookup, range checks, and lower level library code. It also appears inside larger algorithms that repeatedly test a sorted range. The important requirement is sorted order. If the data is not sorted, ordinary binary search does not apply unless sorting is done first.

Why Interviewers Ask This

Interviewers ask Binary Search to test boundary reasoning. The algorithm is short, but small mistakes can break it. They want to see if the candidate understands sorted input, midpoint calculation, inclusive boundaries, and when to stop. They also check whether the candidate can explain why the answer is logarithmic time and constant extra space.

Common interview mistakes

A common mistake is using left < right instead of left <= right for this inclusive version. That can skip the final one element interval. Another mistake is moving left to mid or right to mid. That may fail to reduce the interval and can cause an infinite loop. Some candidates return the target value instead of the index. Others forget to return -1 when the target is absent. Another mistake is applying binary search to an unsorted array.

Interview tip

Say clearly that the boundaries are inclusive. Then explain why each comparison safely removes one half. When coding, focus on the loop condition, midpoint calculation, and the two boundary updates. Those are the places where most binary search bugs happen.

Interviewer may ask next
What changes if the array may contain duplicates and we need the first matching index?

The main change is that we cannot return immediately when nums[mid] equals the target. Instead, we record mid as a possible answer and keep searching the left half. That means setting right = mid - 1 after a match. The invariant changes slightly because we are looking for the earliest valid index. If another target exists on the left, we want to find it. The time complexity stays O(log n). The auxiliary space stays O(1). The tradeoff is that the code has one extra answer variable and does not stop at the first match.

What changes if the input array is not sorted?

Binary search no longer works directly. The reason is that the algorithm depends on sorted order to discard half of the range safely. If the array is unsorted, nums[mid] being smaller than target does not tell us where the target may be. One option is to scan the array from left to right. That takes O(n) time and O(1) auxiliary space. Another option is sorting first, but sorting changes index positions unless we store original indices. The tradeoff is between simple scanning and extra work to preserve index information.

89. Median of Two Sorted ArraysCodingHard

Question Details

Given two sorted arrays, return their combined median while meeting logarithmic-time expectations. Explain the partition conditions, handling of unequal sizes, and important boundary cases.

Short Interview Answer (30-60 seconds)

I would binary-search a partition in the shorter array. The two partition positions must place half of the combined elements on the left. I compare the largest values on the left with the smallest values on the opposite right sides. If the partition is invalid, I move it left or right. When both conditions hold, I calculate the median from the boundary values. This takes O(log(min(m, n))) time and O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to find the median of two sorted arrays without fully merging them. Merging would take linear time. Instead, we binary-search a partition in the shorter array. The partition divides the combined values into left and right halves. When every value on the left is less than or equal to every value on the right, the median can be calculated from the partition boundaries.

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?
Median of Two Sorted Arrays diagram
How to Explain It in an Interview
1. Understand the input and required output

The inputs are two sorted integer arrays. The output is their combined median as a floating-point value.

The original example is A = [1, 3] and B = [2]. The combined sorted order would be [1, 2, 3]. The middle value is 2, so the expected answer is 2.0.

We do not actually build the combined array. It is shown only to verify the answer.

2. Binary-search the shorter array

We make nums1 the shorter array. This keeps the binary-search range as small as possible.

For the example, nums1 becomes [2] and nums2 becomes [1, 3]. Their lengths are m = 1 and n = 2.

The total length is 3. We calculate half = (m + n + 1) // 2 = 2. This means the left side of the combined partition must contain two elements.

The binary-search interval is inclusive. It starts with left = 0 and right = m.

3. Define the partition and boundary values

For each binary-search step, i is the partition position in nums1.

We calculate the matching partition in nums2 with j = half - i.

Four values describe the partition boundaries:

maxLeft1 is the largest nums1 value on the left side.

minRight1 is the smallest nums1 value on the right side.

maxLeft2 is the largest nums2 value on the left side.

minRight2 is the smallest nums2 value on the right side.

If a partition is at an array boundary, the code uses negative infinity or positive infinity. This lets the same comparisons work without accessing an invalid index.

4. Walk through the exact example

In iteration 1, left = 0 and right = 1.

We calculate i = (0 + 1) // 2 = 0.

We then calculate j = half - i = 2 - 0 = 2.

The nums1 partition is before the value 2. The nums2 partition is after the value 3.

The boundary values are:

maxLeft1 = negative infinity

minRight1 = 2

maxLeft2 = 3

minRight2 = positive infinity

We check the two partition conditions.

The first condition is true because negative infinity is less than or equal to positive infinity.

The second condition is false because 3 is greater than 2.

This means the nums1 partition is too far left. We move it right by setting left = i + 1 = 1.

In iteration 2, left = 1 and right = 1.

We calculate i = (1 + 1) // 2 = 1.

We then calculate j = half - i = 2 - 1 = 1.

The nums1 partition is after the value 2. The nums2 partition is between 1 and 3.

The boundary values are:

maxLeft1 = 2

minRight1 = positive infinity

maxLeft2 = 1

minRight2 = 3

Now both conditions are true. We have 2 <= 3 and 1 <= positive infinity. The correct partition has been found, so the binary search stops.

5. Calculate the median

The combined length is 3, which is odd.

For an odd combined length, the median is the largest value on the left side of the valid partition.

The left maximum is max(maxLeft1, maxLeft2) = max(2, 1) = 2.

Therefore, the returned median is 2.0.

For an even combined length, the median is the average of the largest left-side value and the smallest right-side value.

6. Explain why the method is correct

The central invariant is that the two partitions place exactly half of the combined elements on the left side.

The partition is valid when maxLeft1 <= minRight2 and maxLeft2 <= minRight1.

These conditions guarantee that every value on the combined left side is less than or equal to every value on the combined right side. The median must therefore be one of the values directly beside the partitions.

If maxLeft1 is greater than minRight2, the nums1 partition is too far right, so we move right to i - 1.

Otherwise, maxLeft2 is greater than minRight1. The nums1 partition is too far left, so we move left to i + 1.

Each update reduces the binary-search interval.

7. Explain complexity and boundary cases

Binary search is performed only on the shorter array. The time complexity is O(log(min(m, n))).

The algorithm uses only a fixed number of variables. Its auxiliary space complexity is O(1).

Important cases include one empty array, arrays with very different lengths, an odd or even combined length, duplicate values, and all values in one array being smaller than all values in the other array.

Both arrays cannot be empty because a median would not exist.

Key Insight / Why This Solution Works

The key insight is that we do not need to merge the arrays. We only need to find a partition that divides the combined sorted values into a left half and a right half. We binary-search partition i in the shorter array and calculate partition j in the other array. The invariant is that the left side contains half of the combined elements. The partition is correct when maxLeft1 <= minRight2 and maxLeft2 <= minRight1. These conditions guarantee that all left-side values come before all right-side values, so the median can be read directly from the four boundary values.

Code
from typing import List


def find_median_sorted_arrays(nums1: List[int], nums2: List[int]) -> float:
    if not nums1 and not nums2:
        raise ValueError("At least one input array must contain a value.")

    # Always binary-search the shorter array.
    if len(nums1) > len(nums2):
        nums1, nums2 = nums2, nums1

    m, n = len(nums1), len(nums2)
    total = m + n
    half = (total + 1) // 2

    left, right = 0, m

    while left <= right:
        i = (left + right) // 2
        j = half - i

        max_left1 = nums1[i - 1] if i > 0 else float("-inf")
        min_right1 = nums1[i] if i < m else float("inf")
        max_left2 = nums2[j - 1] if j > 0 else float("-inf")
        min_right2 = nums2[j] if j < n else float("inf")

        if max_left1 <= min_right2 and max_left2 <= min_right1:
            if total % 2 == 1:
                return float(max(max_left1, max_left2))

            largest_left = max(max_left1, max_left2)
            smallest_right = min(min_right1, min_right2)
            return (largest_left + smallest_right) / 2.0

        if max_left1 > min_right2:
            right = i - 1
        else:
            left = i + 1

    raise ValueError("The input arrays must be sorted.")


if __name__ == "__main__":
    a = [1, 3]
    b = [2]
    median = find_median_sorted_arrays(a, b)
    print(median)  # 2.0
Time & Space Complexity

Let m and n be the lengths of the two arrays. We binary-search only the shorter array. Each iteration removes about half of the remaining partition positions. The time complexity is therefore O(log(min(m, n))). The algorithm does not merge or copy the arrays. It stores only indices, lengths, and four boundary values. The auxiliary space complexity is O(1).

Where it is used

This partition-based binary-search pattern is useful when sorted data is stored in separate collections and combining all records would be expensive. It can appear in analytics systems, database operations, distributed data processing, and services that need a median or another middle-ranked value from already sorted sources.

Why Interviewers Ask This

This question tests whether the candidate can apply binary search to partition positions instead of searching for a specific value. It also checks whether the candidate can maintain an invariant across two arrays, reason carefully about unequal sizes, handle virtual boundary values, choose the correct search direction, distinguish odd and even totals, and justify the required O(log(min(m, n))) time with O(1) auxiliary space.

Common interview mistakes

A common mistake is binary-searching the longer array instead of the shorter one. Another is calculating j incorrectly instead of using j = half - i. Candidates may compare the wrong boundary values or move the wrong binary-search boundary. They may also access i - 1, i, j - 1, or j without handling array boundaries. Other mistakes include using the odd-length formula for an even total, merging the arrays and missing the logarithmic-time requirement, or forgetting that both empty arrays have no valid median.

Interview tip

Before writing code, draw both partition lines and name maxLeft1, minRight1, maxLeft2, and minRight2. Then state the two valid-partition conditions. This makes the binary-search direction and median formula much easier to derive correctly.

Interviewer may ask next
What changes when the combined number of elements is even?

The partition search and validity conditions stay the same. After finding the valid partition, calculate the largest value on the left and the smallest value on the right. Return their average. The time complexity remains O(log(min(m, n))), and the auxiliary space remains O(1).

How does the solution work when one array is empty?

The empty array becomes nums1 because it is the shorter array. Its partition is at index 0. The code uses negative infinity for its missing left value and positive infinity for its missing right value. The median is then read entirely from the non-empty array. The method still uses O(1) auxiliary space. With an empty shorter array, the binary search completes in constant time.

90. Serialize and Deserialize Binary TreeCodingHard

Question Details

Design methods to convert a binary tree into a string and reconstruct the original tree from that string. Preserve structure and values, handle empty children, and explain complexity.

Short Interview Answer (30-60 seconds)

I would use preorder depth first traversal with a null marker. During serialization, I record the current node, then its left subtree, then its right subtree. I write # whenever a child is empty. During deserialization, I read the tokens in the same order with one shared index. A value creates a node, while # returns None. This preserves both values and structure. Both operations take O(n) time. The stored tokens use O(n) space, and recursion uses O(h) stack space.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to convert a binary tree into a string and later rebuild the same tree. We must preserve the node values and the exact positions of empty children. We use preorder depth first traversal because it processes the root before the left and right subtrees. We also store # for every missing child, so the serialized data contains the full tree structure.

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

The serialization method receives the root of a binary tree and returns a string.

The deserialization method receives that string and returns the root of a reconstructed tree.

The reconstructed tree must have the same node values and the same left and right child structure as the original tree. The tree is not assumed to be a binary search tree.

The example tree has root 1. Node 1 has left child 2 and right child 3. Node 2 has left child 4 and right child 5. Node 3 has no left child and has right child 6.

2. Use preorder traversal and null markers

The traversal order is root, left subtree, then right subtree.

When the current node exists, we store its value. When the current position is empty, we store #.

The null markers are required because node values alone do not show the exact shape of a tree. A # token tells us that a specific left or right child is missing.

For the example, the complete serialized string is:

1,2,4,#,#,5,#,#,3,#,6,#,#

3. Serialize the example tree

Start at node 1 and add 1.

Move to the left child, node 2, and add 2.

Move to node 4 and add 4. Its left child is empty, so add #. Its right child is also empty, so add another #.

Return to node 2 and visit node 5. Add 5. Both children of node 5 are empty, so add # and #.

Return to node 1 and visit node 3. Add 3. Its left child is empty, so add #.

Visit the right child, node 6, and add 6. Both children of node 6 are empty, so add # and #.

The 13 tokens are processed in this exact order:

1, 2, 4, #, #, 5, #, #, 3, #, 6, #, #

4. Deserialize the token sequence

First, split the string by commas. Keep one shared index starting at 0.

At index 0, read 1 and create node 1.

At index 1, read 2 and create the left child of node 1.

At index 2, read 4 and create the left child of node 2.

At indices 3 and 4, read # and return None for the left and right children of node 4.

At index 5, read 5 and create the right child of node 2.

At indices 6 and 7, read # and return None for both children of node 5.

At index 8, read 3 and create the right child of node 1.

At index 9, read # and return None for the left child of node 3.

At index 10, read 6 and create the right child of node 3.

At indices 11 and 12, read # and return None for both children of node 6.

Each recursive call reads exactly one token. A value creates a node. A # token returns None. For every created node, the algorithm builds the left subtree before the right subtree.

5. Explain why the result is correct

Preorder traversal fixes the order of the nodes. Every # marker records one missing child position.

The central invariant is that each serialization call writes exactly one token for its current tree position. Each deserialization call consumes exactly one token and returns either one complete subtree or None.

Because serialization and deserialization use the same root, left, right order, the reconstructed tree has the same values and structure as the original tree.

deserialize(serialize(root)) therefore reconstructs the same tree.

6. Explain the Python implementation

The serialize method creates a list named tokens. Its nested dfs function processes one tree position at a time.

If the node is None, dfs appends # and returns. Otherwise, it appends the node value, processes the left child, and then processes the right child. The tokens are joined with commas at the end.

The deserialize method splits the string into a list of tokens. It keeps an index shared by every recursive call.

Each dfs call reads tokens[index] and then increases the index by one. If the token is #, it returns None. Otherwise, it creates a TreeNode, recursively builds its left child, recursively builds its right child, and returns the completed node.

7. Explain complexity and edge cases

Let n be the number of real nodes and h be the height of the tree.

Serialization takes O(n) time. It visits every real node and records every missing child position. A binary tree with n nodes has n + 1 null child positions, so the total work is still linear.

Deserialization also takes O(n) time because it consumes every token once.

The serialized output contains O(n) tokens. Splitting the string during deserialization creates another O(n) token list. The recursive call stack uses O(h) space.

For a balanced tree, h is O(log n). For a fully skewed tree, h is O(n).

An empty tree serializes to # and deserializes to None. A single node contains its value followed by two null markers. Skewed trees work because every missing child is stored. Negative and multi-digit values work because each value is stored as its own comma-separated token.

Key Insight / Why This Solution Works

The key insight is that preorder traversal alone is not enough unless missing children are also recorded. The algorithm therefore writes node values in root, left, right order and writes # for every empty child. The central invariant is that one recursive call represents one tree position. During serialization, that call writes exactly one token. During deserialization, that call consumes exactly one token and returns either a complete subtree or None. Since both operations follow the same order, the original values and structure are preserved.

Code
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional


@dataclass
class TreeNode:
    val: int
    left: Optional[TreeNode] = None
    right: Optional[TreeNode] = None


class Codec:
    def serialize(self, root: Optional[TreeNode]) -> str:
        tokens: list[str] = []

        def dfs(node: Optional[TreeNode]) -> None:
            if node is None:
                tokens.append("#")
                return

            tokens.append(str(node.val))
            dfs(node.left)
            dfs(node.right)

        dfs(root)
        return ",".join(tokens)

    def deserialize(self, data: str) -> Optional[TreeNode]:
        tokens = data.split(",")
        index = 0

        def dfs() -> Optional[TreeNode]:
            nonlocal index

            token = tokens[index]
            index += 1

            if token == "#":
                return None

            node = TreeNode(int(token))
            node.left = dfs()
            node.right = dfs()
            return node

        return dfs()


if __name__ == "__main__":
    root = TreeNode(
        1,
        left=TreeNode(
            2,
            left=TreeNode(4),
            right=TreeNode(5),
        ),
        right=TreeNode(
            3,
            right=TreeNode(6),
        ),
    )

    codec = Codec()

    serialized = codec.serialize(root)
    print("Serialized:", serialized)

    rebuilt_root = codec.deserialize(serialized)
    rebuilt_serialized = codec.serialize(rebuilt_root)
    print("Rebuilt:   ", rebuilt_serialized)

    expected = "1,2,4,#,#,5,#,#,3,#,6,#,#"
    assert serialized == expected
    assert rebuilt_serialized == expected

    print("The rebuilt tree has the same values and structure.")
Time & Space Complexity

Let n be the number of real nodes and h be the height of the tree. Serialization takes O(n) time because it visits every node and every missing child position once. Deserialization takes O(n) time because it reads every token once. The serialized output uses O(n) space. Splitting the serialized string also creates an O(n) token list. The recursive call stack uses O(h) space. For a balanced tree, h is O(log n). For a skewed tree, h can be O(n).

Where it is used

This pattern is useful when a tree must be saved to a file, stored in a database or cache, sent between services, copied across a network, or restored after a program restarts. It is also useful in testing when a program needs to save and rebuild the exact same tree structure.

Why Interviewers Ask This

The interviewer is checking whether you can flatten a recursive data structure without losing information. They want to see correct traversal order, clear recursion base cases, and careful handling of empty children. They are also evaluating whether you can manage shared recursive state, rebuild left and right subtrees in the correct order, write executable Python, and explain output space and recursion stack space accurately.

Common interview mistakes

A common mistake is storing only node values and not storing null markers. That loses the exact tree structure. Another mistake is using a different traversal order during deserialization. Candidates may also forget the None base case, build the right subtree before the left subtree, reset the token index inside each recursive call, or forget to advance the index after reading a token. It is also incorrect to claim O(1) extra space while ignoring the token list and recursion stack.

Interview tip

Explain the one-token-per-tree-position invariant before writing code. Say that a value creates a node and # creates an empty child. Then keep the traversal order root, left, right identical in both methods.

Interviewer may ask next
How would you handle a very deep skewed tree without risking Python's recursion limit?

Use an iterative preorder traversal with an explicit stack. For serialization, push the right child before the left child so the left side is processed first. For deserialization, use a stack of frames that records whether each created node still needs its left or right child. The time complexity remains O(n). The serialized data and explicit stack use O(n) space. The main tradeoff is more complex state management.

How could you reduce the size of the serialized data?

Use a binary format, variable-length integer encoding, or a compact bitmap for null child positions. The same preorder order and structural information must still be preserved. Serialization and deserialization remain O(n), and total storage remains O(n), but the number of bytes per node can be smaller. The tradeoff is that the format becomes harder to read and the encoding code becomes more complex.

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.