🔢 Problem Statement
LeetCode 14 – Longest Common Prefix
Given an array of strings, return the longest common prefix among them. If no common prefix exists, return an empty string ("").
- Example 1
Input:["flower", "flow", "flight"]→ Output:"fl" - Example 2
Input:["dog", "racecar", "car"]→ Output:""
Constraints
1 ≤ strs.length ≤ 2000 ≤ strs[i].length ≤ 200strs[i]contains only lowercase English letters if non-empty.
👨💻 Solution Strategies
We’ll explore and compare five distinct approaches, from brute force to divide & conquer:
| # | Approach | Time Complexity | Space | Bench Rank |
|---|---|---|---|---|
| 1 | Horizontal Scan | O(S) | O(1) | 4th |
| 2 | Zip-based Scan | O(S) | O(1) | 3rd |
| 3 | Binary Search on Prefix Length | O(S·log m) | O(1) | 2nd |
| 4 | Sort & Compare (first vs last) | O(n·m + n log n) | O(1) | 1st |
| 5 | Divide & Conquer | O(S·log n) | O(log n) | 5th |
Where:
- n = number of strings
- m = length of the shortest string
- S = total number of characters across all strings
📝 Detailed Breakdown
1. Horizontal Scan
Compare each character position across all strings until a mismatch occurs.
for i in range(min_len):
ch = strs[0][i]
if all(s[i] == ch for s in strs):
prefix += ch
else:
break
👍 When to use: Very small input size or whiteboard interviews—simple and intuitive.
2. Zip-based Scan
Transpose the list of strings with zip(*strs) and use set to detect divergence.
for letters in zip(*strs):
if len(set(letters)) == 1:
prefix += letters[0]
else:
break
👍 When to use: Clean Pythonic code; moderate input sizes, readability prioritized.
3. Binary Search on Prefix Length
Binary‐search the possible prefix length range [1..min_len], validating with startswith().
low, high = 1, min_len
while low <= high:
mid = (low + high) // 2
if is_common_prefix(mid):
best = strs[0][:mid]
low = mid + 1
else:
high = mid - 1
👍 When to use: Strings are long (large m), and individual comparisons are expensive.
4. Sort & Compare (First vs Last)
Sort strs lexicographically; only the first and last items need comparison.
strs.sort()
first, last = strs[0], strs[-1]
for i, (a, b) in enumerate(zip(first, last)):
if a != b:
return first[:i]
return first
👍 When to use: Default choice—smallest code footprint and best benchmark performance.
5. Divide & Conquer
Recursively split the array, compute LCP for each half, then merge.
def lcp_range(l, r):
if l == r:
return strs[l]
mid = (l + r) // 2
left = lcp_range(l, mid)
right = lcp_range(mid + 1, r)
# merge two prefixes
return common_prefix(left, right)
👍 When to use: Academic deep dives or parallel execution frameworks.
📊 Benchmark Results
Average runtime per call (seconds):
| n | Sort & Compare | Binary Search | Zip Scan | Horizontal Scan | Divide & Conquer |
|---|---|---|---|---|---|
| 10 | 0.000012 | 0.000012 | 0.000021 | 0.000078 | 0.000061 |
| 100 | 0.000012 | 0.000034 | 0.000106 | 0.000424 | 0.000692 |
| 500 | 0.000017 | 0.000177 | 0.000546 | 0.002052 | 0.002920 |
| 1000 | 0.000022 | 0.000390 | 0.001079 | 0.005615 | 0.005845 |
| 2000 | 0.000052 | 0.000723 | 0.002315 | 0.008572 | 0.011302 |
🧑🏫 When to Use Each Approach
- Sort & Compare: Go-to for most real-world cases—minimal code, fastest in practice.
- Binary Search: When strings are very long and each startswith check is expensive.
- Zip-based Scan: Super readable and concise; perfect for moderate input sizes.
- Horizontal Scan: Ultra-clear and memory-light; good for tiny lists or interview whiteboard.
- Divide & Conquer: Great for teaching or parallel frameworks—even though recursion adds overhead.
💡 Key Takeaways
- Constant‐time factors matter: two O(S) scans can differ drastically in practice.
- Benchmark on realistic data shapes before optimizing.
- Keep code simple unless a clear bottleneck exists.
⏱️ Time Invested
- Analysis & design: 15 min
- Implementation & benchmarking: 30 min
- Writing & review: 10 min
Total: ~ 55 min
🔗 Resources
✍️ Enjoyed this deep dive? Let’s connect on LinkedIn or explore more on GitHub.