Common DSA Mistakes to Avoid
Most failed DSA solutions fail for a handful of repeat reasons — not exotic algorithm errors, but off-by-one bounds, overflow, mutating a collection mid-loop, and language quirks that do something other than what you’d expect. Learn this list and you’ll skip the bugs that cost everyone else hours. Each section names the trap and shows the fix.
Off-by-one errors
The classic: < vs <=, or starting at 0 vs 1. The fix is to decide whether your range is inclusive or exclusive and write every bound consistently. For binary search, the invariant “the answer is in [lo, hi]” must hold at every step:
// search [lo, hi] inclusive; loop while the range is non-empty
int lowerBound(const std::vector<int>& a, int target) {
int lo = 0, hi = (int)a.size(); // hi is exclusive here
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (a[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo; // first index with a[i] >= target
}
int lowerBound(int[] a, int target) {
int lo = 0, hi = a.length; // hi is exclusive
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (a[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo; // first index with a[i] >= target
}
function lowerBound(a, target) {
let lo = 0, hi = a.length; // hi is exclusive
while (lo < hi) {
const mid = lo + ((hi - lo) >> 1);
if (a[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo; // first index with a[i] >= target
}
def lower_bound(a, target):
lo, hi = 0, len(a) # hi is exclusive
while lo < hi:
mid = (lo + hi) // 2
if a[mid] < target:
lo = mid + 1
else:
hi = mid
return lo # first index with a[i] >= target
Tip: Pick one convention — half-open
[lo, hi)is the most error-resistant — and use it everywhere. Computingmid = lo + (hi - lo) / 2also avoids the overflow trap below.
Integer overflow
In C++ and Java, int wraps silently around 2³¹. A sum of large values, a product, or (lo + hi) in binary search can overflow and produce garbage or an infinite loop. Use a 64-bit type for accumulators:
long long total = 0; // not int
for (int x : a) total += x; // safe for large arrays
long total = 0; // not int
for (int x : a) total += x; // safe for large arrays
// JS numbers are doubles: exact only up to 2^53 - 1.
// Use BigInt when sums can exceed that:
let total = 0n;
for (const x of a) total += BigInt(x);
# Python ints are arbitrary precision — no overflow.
total = sum(a)
JavaScript and Python won’t wrap like C++/Java, but JS loses precision above 2⁵³, so reach for BigInt on huge sums.
Mutating a collection while iterating
Adding to or removing from a list/map during a loop over it causes skipped elements or runtime errors. Iterate over a copy, build a new collection, or collect changes and apply them after:
#include <vector>
// Build a new vector instead of erasing during iteration.
std::vector<int> removeEvens(const std::vector<int>& a) {
std::vector<int> out;
for (int x : a) if (x % 2 != 0) out.push_back(x);
return out;
}
import java.util.*;
// Use an Iterator's remove(), or build a new list.
List<Integer> removeEvens(List<Integer> a) {
List<Integer> out = new ArrayList<>();
for (int x : a) if (x % 2 != 0) out.add(x);
return out;
}
// filter returns a new array; never splice inside a for loop over the same array.
function removeEvens(a) {
return a.filter((x) => x % 2 !== 0);
}
# Iterate over a copy, or use a comprehension to build a new list.
def remove_evens(a):
return [x for x in a if x % 2 != 0]
Pitfall: In Java, removing from a list inside an enhanced
forthrowsConcurrentModificationException. In Python, deleting from a list you’re looping over silently skips elements. Both are fixed by building a new collection.
Wrong complexity assumptions
Two traps here. First, hidden costs: list.insert(0, x) is O(n), x in my_list is O(n), and string concatenation in a loop can be O(n²). Second, assuming average case when the grader tests the worst case — a hash map is O(1) average but O(n) under adversarial collisions, and quicksort is O(n log n) average but O(n²) on a bad pivot. Always size your idea against the constraints: at n = 10⁵, an O(n²) plan does ~10¹⁰ operations and will time out. Revisit Big-O notation when in doubt.
Language gotchas
JavaScript’s default sort is lexicographic. [10, 2, 1].sort() gives [1, 10, 2] because it sorts by string. Always pass a comparator for numbers. Python’s mutable default arguments are created once and shared across calls — a default [] accumulates between calls. Use None as the sentinel:
#include <algorithm>
#include <vector>
// C++ sort is numeric by default for ints — no gotcha here.
std::vector<int> v = {10, 2, 1};
std::sort(v.begin(), v.end()); // {1, 2, 10}
import java.util.Arrays;
// int[] sorts numerically; but Integer[] / collections sort by natural order too.
int[] v = {10, 2, 1};
Arrays.sort(v); // {1, 2, 10}
// WRONG: [10, 2, 1].sort() -> [1, 10, 2] (lexicographic!)
// RIGHT: pass a numeric comparator:
const v = [10, 2, 1];
v.sort((a, b) => a - b); // [1, 2, 10]
# WRONG: shared mutable default accumulates across calls.
# def add(x, bucket=[]): bucket.append(x); return bucket
# RIGHT: use None as the sentinel and create a fresh list each call.
def add(x, bucket=None):
if bucket is None:
bucket = []
bucket.append(x)
return bucket
Going deeper: More language traps worth knowing — C++ iterator invalidation after
push_backon avector; Java integer caching making==work for smallIntegers but fail for large ones (use.equals); JavaScript’s0.1 + 0.2 !== 0.3floating-point surprise. When a result is bafflingly wrong, suspect the language before the algorithm.
A pre-submit checklist
- Bounds use one consistent convention; checked
<vs<=. - Accumulators are 64-bit (or BigInt) where values can be large.
- No collection is mutated while being iterated.
- The chosen complexity fits the constraints in the worst case.
- Sorts use explicit comparators; no mutable default arguments.
Avoiding these is most of the battle. To keep getting better, see continuing your DSA journey.