3491. Phone Number Prefix
Problem Description
You are given an array of strings called numbers
where each string represents a phone number. Your task is to determine if any phone number in the array is a prefix of another phone number.
A string is considered a prefix of another string if the second string starts with the exact sequence of characters from the first string. For example, "123" is a prefix of "12345", but "124" is not a prefix of "12345".
The function should return true
if no phone number is a prefix of any other phone number in the array. It should return false
if at least one phone number is a prefix of another.
For example:
- If
numbers = ["911", "91125", "222"]
, the function should returnfalse
because "911" is a prefix of "91125" - If
numbers = ["123", "456", "789"]
, the function should returntrue
because no number is a prefix of any other number
The solution approach sorts the array by string length first, then for each string, checks if any of the shorter strings (that come before it in the sorted array) are prefixes of the current string. This optimization ensures we only check strings that could potentially be prefixes (shorter or equal length strings).
Intuition
The key insight is that if string A is a prefix of string B, then A must be shorter than or equal to B in length. This observation leads us to an important optimization: we only need to check if shorter strings are prefixes of longer strings, never the other way around.
By sorting the array by string length, we create a natural ordering where potential prefixes (shorter strings) come before the strings they might be prefixes of (longer strings). This allows us to process the strings in a specific order and avoid redundant comparisons.
For each string in our sorted array, we only need to look backwards at the strings that came before it (which are guaranteed to be shorter or equal in length). If any of these shorter strings is a prefix of our current string, we've found a prefix relationship and can immediately return false
.
The beauty of this approach is its efficiency - we're eliminating impossible prefix relationships from consideration. A string of length 5 can never be a prefix of a string of length 3, so by sorting first, we ensure we never waste time checking such impossible cases.
Additionally, once we find any prefix relationship, we can immediately return false
without checking the remaining strings, making the algorithm even more efficient in cases where prefix relationships exist early in our sorted array.
Solution Approach
The implementation follows the sorting and prefix checking strategy outlined in the reference approach:
Step 1: Sort by Length
numbers.sort(key=len)
We sort the numbers
array based on the length of each string. This ensures that shorter strings come before longer strings. For example, if we have ["12345", "123", "67"]
, after sorting it becomes ["67", "123", "12345"]
.
Step 2: Iterate and Check Prefixes
for i, s in enumerate(numbers):
if any(s.startswith(t) for t in numbers[:i]):
return False
For each string s
at index i
:
- We examine all strings that come before it in the sorted array (
numbers[:i]
) - These previous strings are guaranteed to be shorter or equal in length to
s
- We use Python's
startswith()
method to check ifs
begins with any of these shorter strings - The
any()
function returnsTrue
if at least one stringt
innumbers[:i]
is a prefix ofs
Step 3: Return Result
return True
If we've checked all strings and haven't found any prefix relationships, we return True
, indicating that no phone number is a prefix of another.
Time Complexity: O(n log n + n²m)
where n
is the number of strings and m
is the average string length. The sorting takes O(n log n)
, and for each string, we potentially check all previous strings with prefix comparison taking O(m)
time.
Space Complexity: O(1)
if we don't count the sorting space, or O(n)
considering the space used by the sorting algorithm.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the example numbers = ["911", "91125", "222"]
.
Step 1: Sort by Length First, we sort the array by string length:
- "911" has length 3
- "222" has length 3
- "91125" has length 5
After sorting: ["911", "222", "91125"]
Step 2: Check Each String for Prefix Relationships
Iteration 1 (i=0, s="911"):
- Check if any string in
numbers[:0]
(empty list) is a prefix of "911" - No strings to check, so continue
Iteration 2 (i=1, s="222"):
- Check if any string in
numbers[:1]
(which is["911"]
) is a prefix of "222" - Does "222" start with "911"? No
- Continue to next iteration
Iteration 3 (i=2, s="91125"):
- Check if any string in
numbers[:2]
(which is["911", "222"]
) is a prefix of "91125" - Does "91125" start with "911"? Yes!
- Since we found a prefix relationship, immediately return
False
Result: The function returns False
because "911" is a prefix of "91125".
Let's contrast this with an example that returns True
: numbers = ["123", "456", "789"]
After sorting by length (all same length, order unchanged): ["123", "456", "789"]
- Check "123": no previous strings to check
- Check "456": does it start with "123"? No
- Check "789": does it start with "123" or "456"? No
Since no prefix relationships were found, the function returns True
.
Solution Implementation
1class Solution:
2 def phonePrefix(self, numbers: List[str]) -> bool:
3 """
4 Check if no phone number is a prefix of another phone number.
5
6 Args:
7 numbers: List of phone numbers as strings
8
9 Returns:
10 True if no number is a prefix of another, False otherwise
11 """
12 # Sort numbers by length to check shorter numbers first
13 # This ensures we only check if shorter numbers are prefixes of longer ones
14 numbers.sort(key=len)
15
16 # Iterate through each number with its index
17 for i, current_number in enumerate(numbers):
18 # Check if current number starts with any of the previous (shorter) numbers
19 # numbers[:i] contains all numbers that are shorter than or equal length
20 if any(current_number.startswith(previous_number) for previous_number in numbers[:i]):
21 # Found a prefix relationship, return False
22 return False
23
24 # No prefix relationships found
25 return True
26
1class Solution {
2 /**
3 * Checks if any phone number in the array is a prefix of another phone number.
4 *
5 * @param numbers Array of phone number strings to check
6 * @return true if no number is a prefix of another, false otherwise
7 */
8 public boolean phonePrefix(String[] numbers) {
9 // Sort the array by string length in ascending order
10 // This ensures we check shorter strings as potential prefixes first
11 Arrays.sort(numbers, (a, b) -> Integer.compare(a.length(), b.length()));
12
13 // Iterate through each phone number in the sorted array
14 for (int i = 0; i < numbers.length; i++) {
15 String currentNumber = numbers[i];
16
17 // Check if any shorter number (those before current index) is a prefix
18 for (int j = 0; j < i; j++) {
19 // If a shorter number is a prefix of the current number, return false
20 if (currentNumber.startsWith(numbers[j])) {
21 return false;
22 }
23 }
24 }
25
26 // No prefix relationship found among all numbers
27 return true;
28 }
29}
30
1#include <vector>
2#include <string>
3#include <algorithm>
4#include <ranges>
5
6class Solution {
7public:
8 bool phonePrefix(std::vector<std::string>& numbers) {
9 // Sort numbers by length in ascending order
10 // This ensures we check shorter strings (potential prefixes) first
11 std::ranges::sort(numbers, [](const std::string& a, const std::string& b) {
12 return a.size() < b.size();
13 });
14
15 // Check each number to see if it has any prefix among the previous numbers
16 for (int i = 0; i < numbers.size(); i++) {
17 // Check if current number starts with any of the previous (shorter) numbers
18 // Using ranges to only look at elements before index i
19 if (std::ranges::any_of(numbers | std::views::take(i),
20 [&](const std::string& prefix) {
21 return numbers[i].starts_with(prefix);
22 })) {
23 // Found a number that is a prefix of another - return false
24 return false;
25 }
26 }
27
28 // No number is a prefix of another number
29 return true;
30 }
31};
32
1/**
2 * Checks if any phone number in the array is a prefix of another phone number
3 * @param numbers - Array of phone number strings to check
4 * @returns true if no number is a prefix of another, false otherwise
5 */
6function phonePrefix(numbers: string[]): boolean {
7 // Sort the array by string length in ascending order
8 // This ensures we check shorter strings as potential prefixes first
9 numbers.sort((a: string, b: string) => a.length - b.length);
10
11 // Iterate through each number in the sorted array
12 for (let i: number = 0; i < numbers.length; i++) {
13 // Check if any previous (shorter) number is a prefix of the current number
14 for (let j: number = 0; j < i; j++) {
15 // If a shorter number is a prefix of the current number, return false
16 if (numbers[i].startsWith(numbers[j])) {
17 return false;
18 }
19 }
20 }
21
22 // No prefix relationships found, return true
23 return true;
24}
25
Time and Space Complexity
Time Complexity: O(n² × m + n × log n)
The time complexity consists of two main parts:
- Sorting: The
sort(key=len)
operation takesO(n × log n)
time, wheren
is the number of strings in the array. - Nested comparison: The outer loop runs
n
times. For each iterationi
, the innerany()
function checks up toi
strings (fromnumbers[:i]
), and eachstartswith()
comparison takesO(m)
time wherem
is the average length of strings. In the worst case, this results inO(1 + 2 + ... + (n-1)) × O(m) = O(n² × m)
. - Combined:
O(n × log n + n² × m) = O(n² × m + n × log n)
sincen² × m
dominates whenm ≥ 1
.
Space Complexity: O(m + log n)
The space complexity comes from:
- The sorting algorithm typically uses
O(log n)
space for recursive calls in languages like Python (using Timsort). - The slicing operation
numbers[:i]
creates a new list, but this is temporary and at most containsO(n)
references (not copying the strings themselves). - The strings themselves require
O(m)
space for the comparison operations. - Combined:
O(m + log n)
wherem
represents the space needed for string operations andlog n
for the sorting recursion stack.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Inefficient Repeated Slicing
The current solution uses numbers[:i]
inside the loop, which creates a new list slice for each iteration. This adds unnecessary overhead, especially for large arrays.
Problem Code:
for i, current_number in enumerate(numbers):
if any(current_number.startswith(previous_number) for previous_number in numbers[:i]):
return False
Better Approach:
for i in range(1, len(numbers)):
for j in range(i):
if numbers[i].startswith(numbers[j]):
return False
Pitfall 2: Missing Edge Case - Duplicate Numbers
If the array contains duplicate phone numbers, they are prefixes of each other by definition. The current solution handles this correctly, but it's worth noting that sorting by length alone doesn't guarantee duplicates will be adjacent.
Example: numbers = ["123", "456", "123"]
After sorting by length: ["123", "456", "123"]
- duplicates aren't necessarily next to each other.
Enhanced Solution with Early Duplicate Detection:
def phonePrefix(self, numbers: List[str]) -> bool:
# Check for duplicates first (optional optimization)
if len(numbers) != len(set(numbers)):
return False
numbers.sort(key=len)
for i in range(1, len(numbers)):
for j in range(i):
if numbers[i].startswith(numbers[j]):
return False
return True
Pitfall 3: Alternative O(n log n) Solution Using Lexicographic Sorting
The current O(n²) approach can be optimized. When strings are sorted lexicographically, strings with prefix relationships will be adjacent.
More Efficient Solution:
def phonePrefix(self, numbers: List[str]) -> bool:
# Sort lexicographically instead of by length
numbers.sort()
# Only check adjacent pairs
for i in range(len(numbers) - 1):
if numbers[i+1].startswith(numbers[i]):
return False
return True
This reduces the time complexity from O(n²m) to O(n log n + nm) for the prefix checking phase.
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!