Common DSA Interview Mistakes
Knowing an algorithm and using it correctly under interview pressure are two different skills. Most candidates who fail a DSA interview didn’t fail because they’d never heard of binary search or backtracking — they failed because of a small, repeatable mistake: an off-by-one loop bound, a mutable default argument, a wrong guess about a built-in’s complexity, or simply not clarifying the problem before coding. This lesson catalogs the mistakes that show up again and again, shows you the buggy code next to the fix, and gives you a checklist to run against your own solutions before you say \”done.\”
Overview: Why Interview Mistakes Aren’t About Not Knowing the Algorithm
Interview mistakes cluster into four categories, and it helps to know which one you’re looking at because the fix is different for each:
- Logic bugs — the algorithm is conceptually right but the implementation is wrong: off-by-one loop bounds, a missing or incorrect recursive base case, an index that should have been
i - 1but isi. - Complexity misconceptions — the code runs and gives the right answer, but the candidate is wrong about how fast it is, often because they misjudge the cost of a Python built-in operation.
- Python-specific gotchas — language quirks that don’t exist in every language, like mutable default arguments silently sharing state across calls, or comparing objects with
iswhen==was meant. - Process mistakes — never clarifying constraints (can the array be empty? are there duplicates? can numbers be negative?), never stating a complexity out loud, and never testing an edge case before declaring victory.
Consider a candidate asked for Two Sum. They immediately write a nested loop, get a correct answer, and stop — without ever saying O(n^2) out loud or asking whether a faster approach is expected. That’s not a logic bug; the code is correct. It’s a process mistake, and it’s one of the most common reasons a technically-correct answer still gets a weak interview score. The rest of this lesson works through concrete, code-level examples of each category so you can recognize them in your own solutions.
Time and Space Complexity: Where Assumptions Go Wrong
A large share of DSA interview mistakes are really complexity mistakes in disguise: the candidate assumes an operation is cheap when it isn’t, and that assumption quietly turns an O(n) solution into an O(n^2) one. Know these cold:
| Operation | Commonly assumed | Actual complexity | Why |
|---|---|---|---|
x in some_list |
O(1) | O(n) | A list has no index by value; Python scans element by element until it finds a match or reaches the end. |
x in some_set / x in some_dict |
O(n) | O(1) average | Hash tables compute a hash of x and jump straight to its bucket instead of scanning. |
some_list.insert(0, x) / some_list.pop(0) |
O(1) | O(n) | Every remaining element has to shift one slot to keep the list contiguous in memory. |
result += piece in a string loop, n times |
O(n) total | O(n^2) total | Strings are immutable, so each += allocates a brand-new string and copies everything built so far. |
| Naive recursive Fibonacci | O(n) | O(2^n) | Without memoization, the same subproblems (e.g. fib(3)) are recomputed from scratch many times over. |
The fixes follow directly from the table: use a set/dict for membership checks, use collections.deque instead of a list when you need to pop from the front (its popleft() is O(1) because it’s a doubly linked list of blocks), build strings with a list and \"\".join(...) instead of repeated concatenation, and memoize (or convert to bottom-up dynamic programming) whenever recursive calls overlap. Space complexity mistakes are usually simpler: candidates forget that the call stack of a recursive solution counts as O(depth) extra space, or forget that a hash map used for memoization or seen-tracking is O(n) space even though it makes the time complexity better.
Examples
Example 1: Two Sum — from O(n^2) assumption to a correct O(n) hash map
This is the canonical case of trading space for time once you notice the brute-force nested loop is doing repeated linear in checks that could be O(1) instead.
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 []
nums = [2, 7, 11, 15]
target = 9
result = two_sum(nums, target)
print(result)
Output:
[0, 1]
Instead of checking every pair (O(n^2)), the function walks the array once and, for each value, checks whether the number that would complete the pair (target - value) has already been seen. Because seen is a dict, that check is O(1) average, so the whole function is O(n) time and O(n) space for the dict.
Example 2: Valid Palindrome — a process mistake made visible
A common interview slip is jumping straight to code without asking \”does case matter? do punctuation and spaces count?\” This two-pointer solution bakes those clarified constraints directly into the code.
def is_valid_palindrome(text: str) -> bool:
left, right = 0, len(text) - 1
while left < right:
while left < right and not text[left].isalnum():
left += 1
while left < right and not text[right].isalnum():
right -= 1
if text[left].lower() != text[right].lower():
return False
left += 1
right -= 1
return True
first = \"A man, a plan, a canal: Panama\"
second = \"race a car\"
print(is_valid_palindrome(first))
print(is_valid_palindrome(second))
Output:
True
False
Two pointers start at opposite ends and move inward. The inner while loops skip over any character that isn’t alphanumeric before the comparison happens, and .lower() makes the comparison case-insensitive. For \"race a car\", once the spaces are skipped the pointers land on 'e' and 'a', which don’t match, so the function correctly returns False. Time complexity is O(n) since each pointer moves inward at most n/2 times total; space is O(1) beyond the input.
Example 3: Subsets — recursion with a clean, visible base case
Backtracking problems are where missing base cases and mutable-default bugs like to hide. This version keeps the base case implicit but correct (the recursion naturally stops when start reaches len(nums), since the for loop then has nothing to iterate) and never shares mutable state between calls.
def subsets(nums: list[int]) -> list[list[int]]:
result: list[list[int]] = []
def backtrack(start: int, path: list[int]) -> None:
result.append(path.copy())
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
nums = [1, 2, 3]
print(subsets(nums))
Output:
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
Notice path.copy(): since path is the same list object mutated throughout the whole recursion (append then pop, the classic backtracking pattern), appending path itself instead of a copy would store a reference that keeps changing after the fact, leaving result full of the same final empty list. This is a close cousin of the mutable-default bug covered below — both come from forgetting that lists are mutable and shared by reference.
How It Works Step by Step
Walking through Example 1’s two_sum([2, 7, 11, 15], 9) index by index shows exactly why the hash map approach is correct and where a candidate might lose track of state:
| index | value | complement (target – value) | complement in seen? | action |
|---|---|---|---|---|
| 0 | 2 | 7 | No (seen is empty) | store seen[2] = 0 |
| 1 | 7 | 2 | Yes, at index 0 | return [0, 1] immediately |
The function never looks ahead and never revisits an index twice — each index is examined exactly once, which is exactly what makes the algorithm O(n). A common misreading of this code is to think it checks whether value itself (not its complement) is in seen; tracing it by hand like this is the fastest way to catch that kind of misunderstanding before it costs you in an interview.
Common Mistakes
Mistake 1: Off-by-one loop bound in binary search
The single most common binary search bug is using < where <= is required. When the search space narrows to exactly one candidate (left == right), a while left < right loop exits without ever checking that last element.
def binary_search_buggy(arr: list[int], target: int) -> int:
left, right = 0, len(arr) - 1
while left < right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
arr = [1, 3, 5]
print(binary_search_buggy(arr, 5))
Output:
-1
Trace it: left=0, right=2, loop runs since 0 < 2, mid=1, arr[1]=3 < 5 so left=2. Now left == right == 2, so left < right is False and the loop exits — without ever checking arr[2], which is the target. The fix is to keep searching while a single candidate remains:
def binary_search(arr: list[int], target: int) -> int:
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
arr = [1, 3, 5]
print(binary_search(arr, 5))
Output:
2
With <=, the loop still runs when left == right, so the last remaining candidate always gets checked. Always trace your bounds against a one-element and two-element input — that’s where this class of bug hides.
Mistake 2: Mutable default argument
A default argument value is created exactly once, when the function is defined — not once per call. If that default is a mutable object like a list, every call that omits the argument shares and mutates the same object.
def collect_evens(nums: list[int], acc: list[int] = []) -> list[int]:
for value in nums:
if value % 2 == 0:
acc.append(value)
return acc
first = collect_evens([1, 2, 3, 4])
second = collect_evens([5, 6, 7])
print(first)
print(second)
Output:
[2, 4, 6]
[2, 4, 6]
Both calls omit acc, so both reuse the exact same default list object. The first call appends 2 and 4 to it; the second call appends 6 to that same object. Since first and second are both references to that one list, printing either shows the fully accumulated [2, 4, 6] — not the two separate results a candidate almost certainly intended. The fix is to default to None and create a fresh list inside the function body:
def collect_evens(nums: list[int], acc: list[int] | None = None) -> list[int]:
if acc is None:
acc = []
for value in nums:
if value % 2 == 0:
acc.append(value)
return acc
first = collect_evens([1, 2, 3, 4])
second = collect_evens([5, 6, 7])
print(first)
print(second)
Output:
[2, 4]
[6]
Now each call that omits acc gets its own brand-new list, created fresh inside the function body every time. This pattern matters most in backtracking and recursive-accumulator code, exactly the style used in Example 3, so get in the habit of using None defaults any time a parameter is a list, dict, or set.
Best Practices
- Restate the problem and clarify constraints out loud — can the input be empty, contain duplicates, or contain negative numbers? — before writing any code.
- State your brute-force solution’s time and space complexity out loud, then explicitly look for a way to trade space for time (hashing, sorting, two pointers, sliding window) before you start typing the optimized version.
- Before trusting a loop’s bounds, mentally trace it against a one-element and a two-element input; that’s where off-by-one bugs are easiest to catch.
- Never use a mutable object (list, dict, set) as a default argument value; default to
Noneand initialize inside the function. - Know the real complexity of the Python operations you reach for —
list.pop(0)andlist.insert(0, x)are O(n),collections.deque‘spopleft()/appendleft()are O(1). - Give every recursive function a base case you can point to in the code, and check that every recursive call moves strictly closer to it.
- Before declaring a solution finished, test it against at least one edge case out loud: empty input, a single element, all-duplicate values, and a target that isn’t present.
- State your final time and space complexity explicitly at the end — if you’re not sure, that’s the moment to work it out, not to guess and move on.
Practice Exercises
- Group Anagrams. Given a list of strings, group the words that are anagrams of each other into sublists (order of groups doesn’t matter). Hint: two words are anagrams if and only if their sorted-character tuples are equal, so a sorted tuple makes a natural hash key for a
defaultdict(list). What’s the time complexity in terms of the number of wordsnand the max word lengthk? - Bug hunt: find the maximum. A candidate writes a function that initializes
largest = 0before scanning a list to find its maximum value. What input does this silently return the wrong answer for, and why? Fix the initialization so it works for every possible list of at least one integer, including one made entirely of negative numbers. - Spot the hidden O(n^2). A function builds a comma-separated report by looping over n items and doing
report += str(item) + \",\"on each iteration. What is the real time complexity of this loop, and why does it differ from what a candidate who only counts loop iterations would guess? Rewrite it so building the report is O(n).
Summary
- DSA interview mistakes fall into four buckets: logic bugs, complexity misconceptions, Python-specific gotchas, and process mistakes (not clarifying, not testing edge cases).
- Common Python complexity traps:
x in listis O(n) but O(1) average forset/dict;list.pop(0)/insert(0, x)are O(n); repeated string+=in a loop is O(n^2) total, not O(n). - Off-by-one loop bounds are the classic binary search bug — use
<=, not<, whenever a single remaining candidate still needs to be checked, and verify by tracing a one-element input. - Never default a mutable argument (list, dict, set) to a literal like
[]; useNoneand initialize fresh inside the function, or every omitted-argument call will share and corrupt the same object. - Two Sum’s hash-map solution runs in O(n) time and O(n) space by trading space for a single linear pass instead of nested O(n^2) comparisons.
- Always state your final time and space complexity out loud, and test at least one edge case, before calling a solution finished.
