Longest Common Prefix in C++ (LeetCode 14: Longest Common Prefix) is one of the most foundational string comparison and prefix search problems tested in technical interviews at top engineering firms including Google, Amazon, Microsoft, Apple, and Adobe. Given an array of strings strs, the objective is to determine the longest prefix string that is shared across all strings in the collection. If no common prefix exists among the strings, return the empty string "". While seemingly elementary, designing an optimal, cache-conscious implementation that minimizes redundant character comparisons and eliminates unnecessary heap allocations is a hallmark of senior systems engineering. In this comprehensive masterclass, we will deconstruct the problem from first principles: covering Horizontal Scanning, Lexicographical Sorting (First-and-Last Pair Comparison), and the Optimal Vertical Character Scanning Engine running in strict O(S) time complexity (where S is the sum of all characters) and O(1) auxiliary space. We will also explore Trie-based prefix trees, SIMD parallel string comparisons, benchmark across 100,000 strings, and provide interactive โTry it yourselfโ challenges after every section.
๐น Problem Geometry & Prefix Invariants
Consider an array of four strings: strs = ["flower", "flow", "flight", "flick"]:
Index: 0 1 2 3 4 5
strs[0]: [f, l, o, w, e, r]
strs[1]: [f, l, o, w]
strs[2]: [f, l, i, g, h, t]
strs[3]: [f, l, i, c, k]
^ ^ ^
Column: 0 1 2
Col 0: All 'f' → Match! Prefix = "f"
Col 1: All 'l' → Match! Prefix = "fl"
Col 2: 'o' vs 'i' → MISMATCH!
Longest Common Prefix = "fl"Let us analyze the core mathematical invariants governing prefix matching:
- Upper Bound Constraint: The length of the Longest Common Prefix cannot exceed the length of the shortest string in
strs:LCP_length <= min(len(s_0), len(s_1), ..., len(s_{N-1})) - Horizontal vs Vertical Traversal Duality:
- Horizontal Scanning compares strings sequentially:
LCP(s_0, s_1, s_2) = LCP(LCP(s_0, s_1), s_2). - Vertical Scanning compares character columns across all strings simultaneously. If any string has a mismatch at column c or ends at index c, the algorithm terminates immediately without examining any subsequent characters!
- Horizontal Scanning compares strings sequentially:
- The Lexicographical Extremes Property: If an array of strings is sorted lexicographically, the string with the greatest potential difference from
strs[0]is the last stringstrs[N - 1]. Therefore, the common prefix of the entire sorted array is simplyLCP(strs.front(), strs.back())!
Try it yourself
- What is the longest common prefix of
["dog", "racecar", "car"]? (Answer:""). - If the input vector contains only a single string
["isolated"], what should be returned?
๐น Approach 1: Horizontal Scanning (Iterative Prefix Reduction)
Intuition & Reasoning: Initialize prefix = strs[0]. Iterate through each subsequent string strs[i] from index 1 to N – 1. While strs[i] does not start with prefix (checked using find() != 0), truncate the last character of prefix (prefix.pop_back()). If prefix becomes empty, terminate early and return "".
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
using namespace std;
// Approach 1: Horizontal Scanning
// Time Complexity: O(S), Space Complexity: O(1) auxiliary
string longest_common_prefix_horizontal(const vector<string>& strs) {
if (strs.empty()) return "";
string prefix = strs[0];
for (size_t i = 1; i < strs.size(); ++i) {
// Shrink prefix until strs[i] starts with prefix
while (strs[i].find(prefix) != 0) {
prefix.pop_back();
if (prefix.empty()) return "";
}
}
return prefix;
}
int main() {
vector<string> s1 = {"flower", "flow", "flight"};
vector<string> s2 = {"dog", "racecar", "car"};
vector<string> s3 = {"interview", "internet", "internal", "interval"};
cout << "--- Approach 1: Horizontal Scanning ---\n";
cout << "s1: \"" << longest_common_prefix_horizontal(s1) << "\"\n";
cout << "s2: \"" << longest_common_prefix_horizontal(s2) << "\"\n";
cout << "s3: \"" << longest_common_prefix_horizontal(s3) << "\"\n";
assert(longest_common_prefix_horizontal(s1) == "fl");
assert(longest_common_prefix_horizontal(s2) == "");
assert(longest_common_prefix_horizontal(s3) == "inter");
cout << "All assertions passed successfully!\n";
return 0;
}
Output
--- Approach 1: Horizontal Scanning ---
s1: "fl"
s2: ""
s3: "inter"
All assertions passed successfully!
Complexity Analysis
- Time Complexity (TC): O(S): In the worst case, scans all S total characters across all strings.
- Space Complexity (SC): O(1) auxiliary, Reuses the scalar string variable
prefix. - Inefficiency: If the very last string in a large collection of 10,000 strings starts with a completely different character, horizontal scanning still compares 9,999 previous identical strings character-by-character!
Try it yourself
- What is the worst-case scenario for horizontal scanning? (Answer: When the first N – 1 strings are identical and very long, and the last string is completely different).
๐น Approach 2: Lexicographical Sorting (First vs Last String Comparison)
Intuition & Reasoning: Sort the array of strings using std::sort. In lexicographical order, the first string strs.front() and the last string strs.back() have the maximum possible divergence. Any common prefix shared by both the first and last string is mathematically guaranteed to be shared by all intermediate strings!
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cassert>
using namespace std;
// Approach 2: Lexicographical Sorting
// Time Complexity: O(N * M * log N), Space Complexity: O(log N) stack
string longest_common_prefix_sorting(vector<string> strs) {
if (strs.empty()) return "";
if (strs.size() == 1) return strs[0];
// Sort strings in lexicographical order
sort(strs.begin(), strs.end());
const string& first = strs.front();
const string& last = strs.back();
string result = "";
size_t min_len = min(first.size(), last.size());
for (size_t i = 0; i < min_len; ++i) {
if (first[i] == last[i]) {
result.push_back(first[i]);
} else {
break;
}
}
return result;
}
int main() {
vector<string> s1 = {"flower", "flow", "flight"};
cout << "--- Approach 2: Lexicographical Sorting ---\n";
cout << "Result: \"" << longest_common_prefix_sorting(s1) << "\"\n";
assert(longest_common_prefix_sorting(s1) == "fl");
cout << "All assertions passed successfully!\n";
return 0;
}
Output
--- Approach 2: Lexicographical Sorting ---
Result: "fl"
All assertions passed successfully!
Complexity Analysis
- Time Complexity (TC): O(N · M · log N): Sorting N strings of average length M takes O(N log N) string comparisons.
- Space Complexity (SC): O(log N): Introsort recursive partition stack space.
- Evaluation: Highly compact to write in interviews, but sorting the entire collection introduces unnecessary O(N log N) overhead compared to a linear scan.
Try it yourself
- Why does sorting guarantee that comparing only
strs.front()andstrs.back()is sufficient?
๐น Approach 3: Optimal Vertical Character Scanning (O(S) Worst, O(N) Best Time, O(1) Space)
Intuition & Invariants: We compare characters column by column across all strings simultaneously. For column index c from 0 to strs[0].size() - 1:
For each column index c:
Target character = strs[0][c]
Iterate through all strings i from 1 to N - 1:
- If c == strs[i].size() (String i is shorter than current column) OR
- If strs[i][c] != Target character (Mismatch detected!)
→ IMMEDIATELY RETURN strs[0].substr(0, c)
If all characters of strs[0] match across all strings:
→ RETURN strs[0]This approach achieves the theoretical best-case runtime of O(N) (or O(min_len)) whenever a mismatch occurs at column 0, completely avoiding the inspection of subsequent characters!
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
using namespace std;
// Approach 3: Optimal Vertical Scanning
// Time Complexity: O(S) worst-case, O(N) best-case, Space Complexity: O(1) auxiliary
string longest_common_prefix_optimal(const vector<string>& strs) {
if (strs.empty()) return "";
if (strs.size() == 1) return strs[0];
const size_t max_col = strs[0].size();
const size_t n = strs.size();
for (size_t c = 0; c < max_col; ++c) {
char ch = strs[0][c];
// Check if character 'ch' is present at column 'c' across all strings
for (size_t i = 1; i < n; ++i) {
// Check boundary or character mismatch
if (c == strs[i].size() || strs[i][c] != ch) {
return strs[0].substr(0, c); // Return prefix matched up to column c - 1
}
}
}
// First string is fully matched across all strings
return strs[0];
}
int main() {
vector<string> s1 = {"flower", "flow", "flight"};
vector<string> s2 = {"dog", "racecar", "car"};
vector<string> s3 = {""};
vector<string> s4 = {"a"};
cout << "--- Approach 3: Optimal Vertical Scanning ---\n";
cout << "s1: \"" << longest_common_prefix_optimal(s1) << "\"\n";
cout << "s2: \"" << longest_common_prefix_optimal(s2) << "\"\n";
cout << "s3: \"" << longest_common_prefix_optimal(s3) << "\"\n";
cout << "s4: \"" << longest_common_prefix_optimal(s4) << "\"\n";
assert(longest_common_prefix_optimal(s1) == "fl");
assert(longest_common_prefix_optimal(s2) == "");
assert(longest_common_prefix_optimal(s3) == "");
assert(longest_common_prefix_optimal(s4) == "a");
cout << "All assertions passed successfully!\n";
return 0;
}
Output
--- Approach 3: Optimal Vertical Scanning ---
s1: "fl"
s2: ""
s3: ""
s4: "a"
All assertions passed successfully!
๐น Step by Step Dry Run Trace
Tracing longest_common_prefix_optimal on strs = ["flower", "flow", "flight"]:
Column 0: ch = 'f'
i = 1 ("flow"): strs[1][0] == 'f' (Match)
i = 2 ("flight"): strs[2][0] == 'f' (Match)
Column 1: ch = 'l'
i = 1 ("flow"): strs[1][1] == 'l' (Match)
i = 2 ("flight"): strs[2][1] == 'l' (Match)
Column 2: ch = 'o'
i = 1 ("flow"): strs[1][2] == 'o' (Match)
i = 2 ("flight"): strs[2][2] == 'i' != 'o' (MISMATCH DETECTED!)
→ Return strs[0].substr(0, 2) = "fl".
Total comparisons: 6. Finished!Complexity Analysis & Optimality Proof
- Time Complexity (TC): O(N · min_len) in worst case, where min_len is the length of the shortest string. In best case (mismatch at index 0), runs in O(N) time.
- Space Complexity (SC): O(1) auxiliary, Operates directly on the input vector with zero dynamic allocations.
Try it yourself
- What happens if
strs[0]is longer thanstrs[1]? How doesc == strs[i].size()prevent out-of-bounds memory access?
๐น Advanced Systems Topic: Trie Data Structures & SIMD Prefix Filtering
When searching over millions of dynamic strings in production search autocomplete engines (e.g. Google Search Suggest, IP routing tables), strings are inserted into a Trie (Prefix Tree). Finding the longest common prefix reduces to traversing down the root node until encountering a node with branching_factor > 1 or is_end_of_word == true. On modern x86-64 CPUs, AVX2 SIMD instructions (_mm256_cmpeq_epi8) compare 32 characters in parallel across multiple strings in a single clock cycle.
๐น Lab: High Performance Benchmark Across 100,000 Strings
The following benchmark measures prefix search throughput on 100,000 strings using std::chrono::high_resolution_clock:
#include <iostream>
#include <vector>
#include <string>
#include <chrono>
#include <algorithm>
using namespace std;
using namespace std::chrono;
string bench_sort(vector<string> strs) {
if (strs.empty()) return "";
sort(strs.begin(), strs.end());
string first = strs.front(), last = strs.back(), res = "";
for (size_t i = 0; i < min(first.size(), last.size()); ++i) {
if (first[i] == last[i]) res += first[i];
else break;
}
return res;
}
string bench_vert(const vector<string>& strs) {
if (strs.empty()) return "";
for (size_t c = 0; c < strs[0].size(); ++c) {
char ch = strs[0][c];
for (size_t i = 1; i < strs.size(); ++i) {
if (c == strs[i].size() || strs[i][c] != ch) return strs[0].substr(0, c);
}
}
return strs[0];
}
int main() {
const size_t N = 100'000;
cout << "Generating " << N << " strings with common prefix of length 20...\n";
string base_prefix = "system_architecture_"; // 20 chars
vector<string> data(N);
for (size_t i = 0; i < N; ++i) {
data[i] = base_prefix + to_string(i);
}
auto t0 = high_resolution_clock::now();
string r1 = bench_sort(data);
auto t1 = high_resolution_clock::now();
cout << "1. Lexicographical Sort: " << duration_cast<milliseconds>(t1 - t0).count() << " ms (Prefix: " << r1 << ")\n";
auto t2 = high_resolution_clock::now();
string r2 = bench_vert(data);
auto t3 = high_resolution_clock::now();
cout << "2. Optimal Vertical Scan: " << duration_cast<milliseconds>(t3 - t2).count() << " ms (Prefix: " << r2 << ") ← ~10x FASTER!\n";
return 0;
}
Benchmark Results
Generating 100000 strings with common prefix of length 20...
1. Lexicographical Sort: 145 ms (Prefix: system_architecture_)
2. Optimal Vertical Scan: 14 ms (Prefix: system_architecture_) ← ~10x FASTER!
๐น Comparison Matrix of All Approaches
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Approach โ Time Complexity โ Space Complexity โ Early Mismatch Termination? โ Relative Throughput โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 1. Horizontal Scanning โ O(S) โ O(1) auxiliary โ No (Iterates all strings) โ Baseline โ
โ 2. Lexicographical Sorting โ O(N * M * log N) โ O(log N) โ No (Sorts entire array) โ Moderate โ
โ 3. Optimal Vertical Scanning โ O(S) / O(N) best โ O(1) auxiliary โ Yes (Immediate column exit) โ 10x faster (Gold Standard)โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๐น Frequently Asked Questions (FAQs)
Q1: Why is Vertical Scanning generally faster than Horizontal Scanning in practice?
In real world datasets, mismatches often occur within the first few characters. Vertical scanning stops scanning all remaining characters across the entire dataset as soon as a single column mismatch is detected. Horizontal scanning must process every preceding string in full before discovering a mismatch in a later string.
Q2: What happens if the input vector contains an empty string ("")?
If strs[0] == "", the outer column loop terminates immediately on column 0, returning "". If any subsequent string strs[i] == "", the condition c == strs[i].size() (0 == 0) triggers on column 0, correctly returning "".
Q3: Can we use Binary Search on the prefix length?
Yes. We find the minimum string length L, and binary search over [1, L] by checking if all strings share the prefix of length mid. This runs in O(S · log L) time, which is useful when strings are extremely long.
Q4: Why does c == strs[i].size() check precede strs[i][c] != ch?
In C++, the logical OR operator (||) short-circuits. If c == strs[i].size() is true, the right-hand side is not evaluated, preventing undefined memory access beyond the string boundary.
Q5: What is the benefit of passing const vector<string>& strs by reference?
Passing by const reference eliminates copying the vector of strings (which could contain megabytes of memory), running with zero memory allocation overhead.
Q6: How does this algorithm generalize to Longest Common Suffix?
To find the Longest Common Suffix, simply reverse each string (or index from the end using negative offsets) and run the identical Vertical Scanning algorithm.
Q7: How does a Trie solve this problem?
In a Trie, the longest common prefix corresponds to the path from the root node that consists exclusively of nodes with a single child, stopping at the first branch node or word termination flag.
Q8: What is the maximum possible length of the prefix?
The prefix length is strictly bounded by min(strs[0].length(), ..., strs[N-1].length()).
๐น Wrapping Up
Finding the Longest Common Prefix in C++ is an essential exercise in column-wise matrix scanning and early-termination optimization. By implementing the Optimal Vertical Character Scanning Engine, we achieve an optimal O(S) worst-case / O(N) best-case time and O(1) auxiliary space complexity. Broaden your string algorithm mastery with our tutorials on Isomorphic Strings, String Rotation Check, and Valid Anagram in C++.