Boundary conditions define the valid domain of indices, ranges, and states in an algorithm.
Boundary conditions define the valid domain of indices, ranges, and states in an algorithm. Many bugs arise not from the main logic, but from incorrect handling of edges such as empty input, single-element cases, or off-by-one errors. This section provides a systematic approach to designing and verifying boundaries.
Problem
Given an algorithm over arrays or strings, ensure correctness for all valid inputs, including edge cases:
empty input
single element
full range
minimal and maximal indicesIndex Ranges
Two common conventions:
inclusive: [l, r]
half-open: [l, r)Half-open ranges are usually safer because:
length = r - l
empty range: l == rExample:
for i := l; i < r; i++ {
// process a[i]
}Invariant:
At iteration i, elements in a[l:i] have already been processedOff-by-One Errors
Typical mistake:
for i := 0; i <= len(a); i++ { // wrongCorrect:
for i := 0; i < len(a); i++ {Reason:
valid indices: 0 to len(a)-1Empty Input
Always check before accessing elements.
if len(a) == 0 {
return
}Accessing a[0] without this check causes a panic.
Single Element
Some algorithms assume at least two elements.
Example:
if len(a) == 1 {
return a[0]
}Two-pointer algorithms must handle:
l == rdepending on whether a single element is valid.
Two Pointers Boundaries
Opposite-end pattern:
for l < r {
...
}Same-direction pattern:
for r < n {
...
}Invariant:
l and r always remain within [0, n]Mistake:
using l <= r when logic requires two distinct elementsSliding Window Boundaries
Window [l, r] or [l, r) must be consistent.
Example with inclusive:
for r := 0; r < n; r++ {
for invalid {
l++
}
}Invariant:
window contains elements from l to r inclusiveCommon error:
mixing r-l and r-l+1 inconsistentlyMatrix Boundaries
Check both dimensions:
if 0 <= r && r < rows && 0 <= c && c < cols {Mistake:
using rows for both dimensionsPrefix Arrays
Prefix array size is n + 1.
p := make([]int, n+1)Access pattern:
p[0] = 0
p[i] = sum of first i elementsRange query:
sum := p[r] - p[l]Mistake:
using p[r-1] or mixing inclusive/exclusive conventionsDifference Arrays
For half-open updates [l, r):
d[l] += x
d[r] -= xFor inclusive [l, r]:
d[l] += x
d[r+1] -= xMistake:
accessing d[r+1] when r is already the last index without extra spaceBinary Search Boundaries
Two standard forms:
Lower Bound
l, r := 0, n
for l < r {
m := (l + r) / 2
if a[m] < target {
l = m + 1
} else {
r = m
}
}Invariant:
[l, r) always contains the answerUpper Bound
l, r := 0, n
for l < r {
m := (l + r) / 2
if a[m] <= target {
l = m + 1
} else {
r = m
}
}Mistake:
using inconsistent updates leads to infinite loopsLoop Termination
Ensure progress in every iteration.
Bad:
for l < r {
if condition {
l++
}
}If condition is false, loop does not progress.
Correct:
for l < r {
if condition {
l++
} else {
r--
}
}Sentinel Values
Sentinels simplify boundary logic.
Example:
a := append(a, math.MaxInt)Now comparisons do not need to check end conditions inside loops.
Use carefully to avoid modifying original data unintentionally.
Edge Case Checklist
Before finalizing an algorithm, test:
n = 0
n = 1
all elements equal
strictly increasing
strictly decreasing
maximal input size
minimal input sizeCommon Pitfalls
Mixing inclusive and half-open ranges in the same function.
Forgetting to handle empty input before indexing.
Using incorrect loop bounds.
Failing to update pointers in all branches.
Incorrect matrix dimension checks.
Assuming sorted input when it is not guaranteed.
Design Pattern
Boundary handling follows:
- Choose a consistent range convention
- State valid index domain
- Maintain invariant within that domain
- Ensure loop progress
- Verify termination condition
Takeaway
Correct boundary handling is as important as the main algorithm. Most runtime errors and subtle bugs come from incorrect index limits and inconsistent range definitions. Use half-open intervals when possible, state invariants explicitly, and test extreme cases early.