165. Compare Version Numbers
Problem Description
You need to compare two version strings (version1
and version2
) and determine their relative ordering.
A version string consists of revision numbers separated by dots (.
). For example: "1.2.3"
has three revisions: 1
, 2
, and 3
.
Each revision should be treated as an integer, ignoring any leading zeros. For instance, "01"
should be treated as 1
, and "001"
should also be treated as 1
.
To compare the two version strings:
- Compare revision values from left to right, one revision at a time
- If one version string has fewer revisions than the other, treat the missing revisions as
0
- The comparison stops as soon as a difference is found
The function should return:
-1
ifversion1
is less thanversion2
1
ifversion1
is greater thanversion2
0
if both versions are equal
Example scenarios:
- Comparing
"1.2"
with"1.2.0"
would return0
(equal, since missing revision is treated as0
) - Comparing
"1.01"
with"1.001"
would return0
(equal, since leading zeros are ignored) - Comparing
"1.0"
with"1.0.1"
would return-1
(first is smaller) - Comparing
"2.5"
with"2.3"
would return1
(first is larger)
Intuition
The key insight is that we need to process both version strings simultaneously, comparing revision by revision. Since revisions are separated by dots, we can parse each revision on-the-fly as we traverse the strings.
Think of it like reading two numbers digit by digit, but instead of single digits, we're reading complete revision numbers between dots. When we encounter a dot or reach the end of a string, we know we've completed reading one revision.
The challenge is handling versions with different numbers of revisions. For example, when comparing "1.2"
with "1.2.3"
, after processing the first two revisions, one string is exhausted while the other still has revisions left. The natural approach is to continue processing and treat any missing revision as 0
.
To parse each revision while ignoring leading zeros, we can build the number character by character using the formula: number = number * 10 + current_digit
. This automatically handles leading zeros because 0 * 10 + 5 = 5
, effectively ignoring the leading zero.
Using two pointers (i
and j
) allows us to independently track our position in each version string. We continue the comparison process as long as at least one string has characters remaining to process. At each step:
- Extract the next revision from both strings (or
0
if one string is exhausted) - Compare these revisions
- If they differ, we immediately know which version is larger
- If they're equal, move to the next revision
This approach avoids the need to split the strings or preprocess them into arrays, making it memory-efficient and allowing us to return early as soon as we find a difference.
Solution Approach
The implementation uses a two-pointer approach to traverse both version strings simultaneously. Here's how the algorithm works:
Initialize pointers and lengths:
- Set
i = 0
andj = 0
as pointers forversion1
andversion2
respectively - Store the lengths
m = len(version1)
andn = len(version2)
Main comparison loop:
The outer loop continues while either string has unprocessed characters (i < m or j < n
):
-
Extract current revisions:
- Initialize
a = 0
andb = 0
to store the current revision values - For
version1
: Whilei < m
and the current character isn't a dot, build the revision number usinga = a * 10 + int(version1[i])
, then incrementi
- For
version2
: Similarly, build revisionb
usingb = b * 10 + int(version2[j])
, then incrementj
This multiplication technique naturally handles leading zeros. For example, parsing
"01"
gives:0 * 10 + 1 = 1
- Initialize
-
Compare revisions:
- If
a != b
, we've found a difference:- Return
-1
ifa < b
(version1 is smaller) - Return
1
ifa > b
(version1 is larger)
- Return
- If
-
Move to next revision:
- Increment both pointers by 1 to skip the dot separator:
i = i + 1
andj = j + 1
- If a pointer is already at or beyond the string length, it stays there, and subsequent revisions will be read as
0
- Increment both pointers by 1 to skip the dot separator:
Return equal:
If the loop completes without finding any differences, the versions are equal, so return 0
.
Time Complexity: O(max(m, n))
where m
and n
are the lengths of the version strings, as we traverse each string at most once.
Space Complexity: O(1)
as we only use a constant amount of extra space for variables.
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 trace through comparing version1 = "1.2.3"
and version2 = "1.10"
:
Initial Setup:
i = 0
,j = 0
m = 5
(length of "1.2.3")n = 4
(length of "1.10")
Iteration 1: Compare first revisions
- Extract revision from version1:
- Start with
a = 0
- Read '1':
a = 0 * 10 + 1 = 1
- Stop at '.', so
i = 1
- Start with
- Extract revision from version2:
- Start with
b = 0
- Read '1':
b = 0 * 10 + 1 = 1
- Stop at '.', so
j = 1
- Start with
- Compare:
a = 1
,b = 1
β Equal, continue - Skip dots:
i = 2
,j = 2
Iteration 2: Compare second revisions
- Extract revision from version1:
- Start with
a = 0
- Read '2':
a = 0 * 10 + 2 = 2
- Stop at '.', so
i = 3
- Start with
- Extract revision from version2:
- Start with
b = 0
- Read '1':
b = 0 * 10 + 1 = 1
- Read '0':
b = 1 * 10 + 0 = 10
- Reached end of string, so
j = 4
- Start with
- Compare:
a = 2
,b = 10
β2 < 10
- Return
-1
(version1 is smaller)
The algorithm correctly identifies that "1.2.3" < "1.10" because when comparing the second revision, 2 is less than 10.
Solution Implementation
1class Solution:
2 def compareVersion(self, version1: str, version2: str) -> int:
3 """
4 Compare two version strings and return:
5 -1 if version1 < version2
6 0 if version1 == version2
7 1 if version1 > version2
8 """
9 # Get lengths of both version strings
10 len1, len2 = len(version1), len(version2)
11
12 # Initialize pointers for traversing both version strings
13 ptr1 = ptr2 = 0
14
15 # Continue comparing while at least one version has remaining segments
16 while ptr1 < len1 or ptr2 < len2:
17 # Initialize revision numbers for current segment
18 revision1 = revision2 = 0
19
20 # Parse the current revision number from version1
21 # Continue until we hit a dot or reach the end
22 while ptr1 < len1 and version1[ptr1] != '.':
23 revision1 = revision1 * 10 + int(version1[ptr1])
24 ptr1 += 1
25
26 # Parse the current revision number from version2
27 # Continue until we hit a dot or reach the end
28 while ptr2 < len2 and version2[ptr2] != '.':
29 revision2 = revision2 * 10 + int(version2[ptr2])
30 ptr2 += 1
31
32 # Compare current revision numbers
33 if revision1 != revision2:
34 return -1 if revision1 < revision2 else 1
35
36 # Skip the dot separator and move to next segment
37 ptr1 += 1
38 ptr2 += 1
39
40 # All revision segments are equal
41 return 0
42
1class Solution {
2 /**
3 * Compares two version strings.
4 * @param version1 The first version string (e.g., "1.2.3")
5 * @param version2 The second version string (e.g., "1.2.4")
6 * @return -1 if version1 < version2, 1 if version1 > version2, 0 if equal
7 */
8 public int compareVersion(String version1, String version2) {
9 int version1Length = version1.length();
10 int version2Length = version2.length();
11
12 int index1 = 0;
13 int index2 = 0;
14
15 // Process both version strings simultaneously
16 while (index1 < version1Length || index2 < version2Length) {
17 // Parse the current revision number from version1
18 int revision1 = 0;
19 while (index1 < version1Length && version1.charAt(index1) != '.') {
20 revision1 = revision1 * 10 + (version1.charAt(index1) - '0');
21 index1++;
22 }
23
24 // Parse the current revision number from version2
25 int revision2 = 0;
26 while (index2 < version2Length && version2.charAt(index2) != '.') {
27 revision2 = revision2 * 10 + (version2.charAt(index2) - '0');
28 index2++;
29 }
30
31 // Compare the current revision numbers
32 if (revision1 != revision2) {
33 return revision1 < revision2 ? -1 : 1;
34 }
35
36 // Skip the dot separator for next iteration
37 index1++;
38 index2++;
39 }
40
41 // All revisions are equal
42 return 0;
43 }
44}
45
1class Solution {
2public:
3 int compareVersion(string version1, string version2) {
4 int version1Length = version1.size();
5 int version2Length = version2.size();
6
7 int index1 = 0;
8 int index2 = 0;
9
10 // Process both version strings until we reach the end of both
11 while (index1 < version1Length || index2 < version2Length) {
12 // Parse the next revision number from version1
13 int revision1 = 0;
14 while (index1 < version1Length && version1[index1] != '.') {
15 revision1 = revision1 * 10 + (version1[index1] - '0');
16 index1++;
17 }
18
19 // Parse the next revision number from version2
20 int revision2 = 0;
21 while (index2 < version2Length && version2[index2] != '.') {
22 revision2 = revision2 * 10 + (version2[index2] - '0');
23 index2++;
24 }
25
26 // Compare the current revision numbers
27 if (revision1 < revision2) {
28 return -1;
29 }
30 if (revision1 > revision2) {
31 return 1;
32 }
33
34 // Skip the dot separator if present
35 if (index1 < version1Length) {
36 index1++; // Skip '.'
37 }
38 if (index2 < version2Length) {
39 index2++; // Skip '.'
40 }
41 }
42
43 // All revisions are equal
44 return 0;
45 }
46};
47
1/**
2 * Compares two version strings and returns their relative order
3 * @param version1 - First version string (e.g., "1.2.3")
4 * @param version2 - Second version string (e.g., "1.2.4")
5 * @returns -1 if version1 < version2, 1 if version1 > version2, 0 if equal
6 */
7function compareVersion(version1: string, version2: string): number {
8 // Split version strings into arrays of revision numbers
9 const versionParts1: string[] = version1.split('.');
10 const versionParts2: string[] = version2.split('.');
11
12 // Determine the maximum length between both version arrays
13 const maxLength: number = Math.max(versionParts1.length, versionParts2.length);
14
15 // Compare each revision number from left to right
16 for (let index: number = 0; index < maxLength; index++) {
17 // Convert string to number, treating undefined/missing parts as 0
18 const revisionNumber1: number = Number(versionParts1[index]) || 0;
19 const revisionNumber2: number = Number(versionParts2[index]) || 0;
20
21 // If current revision of version1 is smaller, version1 is earlier
22 if (revisionNumber1 < revisionNumber2) {
23 return -1;
24 }
25
26 // If current revision of version1 is larger, version1 is later
27 if (revisionNumber1 > revisionNumber2) {
28 return 1;
29 }
30
31 // If equal, continue to next revision
32 }
33
34 // All revisions are equal, versions are the same
35 return 0;
36}
37
Time and Space Complexity
Time Complexity: O(max(m, n))
where m
is the length of version1
and n
is the length of version2
.
The algorithm uses two pointers to traverse through both version strings simultaneously. In the worst case, we need to traverse through every character of both strings exactly once. The outer while loop continues until both pointers reach the end of their respective strings. The inner while loops parse individual revision numbers between dots, but each character is still only visited once throughout the entire execution. Therefore, the total time complexity is linear with respect to the combined length of both input strings.
Space Complexity: O(1)
The algorithm uses only a constant amount of extra space. The variables used are:
- Two integer pointers
i
andj
for traversing the strings - Two integer variables
a
andb
for storing the current revision numbers being compared - Two integer variables
m
andn
for storing the lengths of the input strings
Since we don't create any additional data structures that grow with the input size, and we're parsing the version numbers on-the-fly without storing them, the space complexity is constant.
Common Pitfalls
1. Incorrectly Handling Leading Zeros
A common mistake is trying to strip leading zeros as a preprocessing step or using string comparison directly. This can lead to incorrect parsing or comparison logic.
Pitfall Example:
# Wrong approach - comparing strings directly if version1[i:next_dot] < version2[j:next_dot]: # "10" < "9" gives wrong result return -1
Solution:
Convert each revision segment to an integer during parsing. The multiplication method (revision = revision * 10 + int(char)
) naturally handles leading zeros without special cases.
2. Not Handling Different Version Lengths
Failing to properly handle versions with different numbers of revisions, especially when one version has trailing zeros that should be ignored.
Pitfall Example:
# Wrong - stops when shorter version ends while ptr1 < len1 and ptr2 < len2: # Misses remaining revisions # comparison logic
Solution:
Use or
instead of and
in the while condition: while ptr1 < len1 or ptr2 < len2
. This ensures all revisions are processed, with missing revisions defaulting to 0.
3. Pointer Management After Parsing
Forgetting to skip the dot separator or incorrectly incrementing pointers can cause infinite loops or skipped revisions.
Pitfall Example:
# Wrong - might increment past string bounds without checking
while version1[ptr1] != '.': # No bounds check
revision1 = revision1 * 10 + int(version1[ptr1])
ptr1 += 1
ptr1 += 1 # Could go beyond string length unexpectedly
Solution:
Always check bounds before accessing characters: while ptr1 < len1 and version1[ptr1] != '.'
. The final increment (ptr1 += 1
) after the inner loops is safe because subsequent iterations check ptr1 < len1
.
4. Using Split Method Incorrectly
While using split('.')
seems simpler, it can lead to memory inefficiency for very long version strings or issues with edge cases.
Pitfall Example:
# Potentially inefficient for very long strings revisions1 = version1.split('.') revisions2 = version2.split('.') # Need to pad shorter list or handle index out of bounds
Solution: The two-pointer approach avoids creating additional arrays and handles different lengths naturally by treating missing revisions as 0 without explicit padding.
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!