Topics › Strings › Medium String Problems
Compare version numbers
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
A version number is a list of revisions separated by dots, such as "1.0.12". Each revision is a run of digits and may have leading zeros. To compare two version numbers, compare their revisions from left to right as integers (so "001" equals "1"). If one version has fewer revisions, the missing ones count as 0.
Return -1 if version1 < version2, 1 if version1 > version2, and 0 if they are equal.
Constraints (LeetCode 165): 1 ≤ length ≤ 500, only digits and dots, every version is well formed (no empty revisions, no leading or trailing dot), and every revision fits in a 32-bit integer.
Example 1
version1 = "1.2", version2 = "1.10"
answer = -1 // revision 2: 2 < 10 as integers, even though "2" > "10" as text
Example 2
version1 = "1.01", version2 = "1.001"
answer = 0 // both second revisions are the integer 1
Example 3
version1 = "1.0", version2 = "1.0.0.0"
answer = 0 // the missing third and fourth revisions of version1 count as 0
The two traps are visible right in the examples: comparing the whole strings, or individual revisions, as text gets example 1 wrong, and treating “has more revisions” as “is bigger” gets example 3 wrong.
Approach 1: Split and parse
Intuition. Do literally what the statement says. Split each version on dots into an array of revision strings, turn each one into an integer, and walk both arrays in parallel. Where one array has run out, use 0. The first position where the integers differ decides the answer; if none differs, the versions are equal.
Integer.parseInt handles leading zeros for us (parseInt("007") is 7), so all of the integer comparison logic is delegated to the library.
Algorithm.
a = version1.split("\\."),b = version2.split("\\.").- For
ifrom0tomax(a.length, b.length) - 1: letxbeparseInt(a[i])or0ifiis past the end ofa; same fory. - If
x != y, return-1or1. - Return
0.
static int splitAndParse(String version1, String version2) {
String[] a = version1.split("\\."), b = version2.split("\\.");
int len = Math.max(a.length, b.length);
for (int i = 0; i < len; i++) {
int x = i < a.length ? Integer.parseInt(a[i]) : 0; // parseInt drops leading zeros: "001" -> 1
int y = i < b.length ? Integer.parseInt(b[i]) : 0;
if (x != y) return x < y ? -1 : 1;
}
return 0;
}
Complexity. Time O(n + m) for splitting and parsing, where n and m are the string lengths. Space O(n + m) for the two arrays of substrings.
Note the double backslash. split takes a regular expression, and in a regex a bare . matches any character, so split(".") would split everywhere and return an empty array. "\\." is the Java string for the regex \., a literal dot.
Approach 2: Optimal, two pointers without splitting
Intuition. The arrays in Approach 1 are only an intermediate step: we read each revision once, in order, and never go back. So we can parse revisions directly out of the strings with one pointer per string. For the current revision, pointer i reads digits of version1 until it hits a dot or the end, accumulating x = x * 10 + digit; pointer j does the same for version2. Compare x and y, then step both pointers over the dot and repeat.
Missing revisions need no special code. Once a pointer is past the end of its string, its digit loop does not run, so that string contributes 0 for every remaining round, exactly as the rules require. The loop continues while either string has characters left.
Leading zeros are also free: x * 10 + 0 with x = 0 stays 0, so "001" parses to 1.
Algorithm.
- Set
i = j = 0. - While
i < norj < m:- Read digits of
version1fromiup to the next dot intox(or leavex = 0ifi ≥ n); likewiseyfromversion2. - If
x != y, return-1or1. - Advance
iandjpast the dot.
- Read digits of
- Return
0.
static int optimal(String version1, String version2) {
int i = 0, j = 0, n = version1.length(), m = version2.length();
while (i < n || j < m) {
int x = 0, y = 0; // an exhausted string contributes 0
while (i < n && version1.charAt(i) != '.') x = x * 10 + (version1.charAt(i++) - '0');
while (j < m && version2.charAt(j) != '.') y = y * 10 + (version2.charAt(j++) - '0');
if (x != y) return x < y ? -1 : 1;
i++; // step over the '.' (or past the end)
j++;
}
return 0;
}
Dry run on two pairs:
| pair | round | x | y | result |
|---|---|---|---|---|
"1.2" vs "1.10" |
1 | 1 | 1 | equal, continue |
| 2 | 2 | 10 | x < y, return -1 |
|
"1.0" vs "1.0.0.0" |
1 | 1 | 1 | equal |
| 2 | 0 | 0 | equal | |
| 3 | 0 (exhausted) | 0 | equal | |
| 4 | 0 (exhausted) | 0 | equal, loop ends, return 0 |
Complexity. Time O(n + m): each character is read once. Space O(1): a few integers and no substrings.
Approach 3: Revisions of any length, without converting
Intuition. Approaches 1 and 2 both rely on the guarantee that a revision fits in an int. Real version strings sometimes contain long numbers (build timestamps, for instance), and then parseInt throws and the on-the-fly sum silently overflows. We can compare two non-negative integers written in decimal without ever converting them:
- Leading zeros carry no value, so skip them.
- After that, the number with more digits is larger. A number with no remaining digits is
0. - If both have the same number of digits, the first position where the digits differ decides, exactly like comparing words in a dictionary.
Apply this to each pair of revisions in turn, with the same two-pointer walk as before. Each revision is described by the start index of its first significant digit and the count of significant digits.
Algorithm.
- While either string has characters left:
- In each string, skip leading
'0's, remember the start, and move to the next dot. The difference is the number of significant digits. - If the digit counts differ, the one with more digits is larger.
- Otherwise compare digit by digit; the first difference decides.
- Step both pointers over the dot.
- In each string, skip leading
- Return
0.
static int digitStrings(String version1, String version2) {
int i = 0, j = 0, n = version1.length(), m = version2.length();
while (i < n || j < m) {
while (i < n && version1.charAt(i) == '0') i++; // leading zeros carry no value
while (j < m && version2.charAt(j) == '0') j++;
int si = i, sj = j;
while (i < n && version1.charAt(i) != '.') i++;
while (j < m && version2.charAt(j) != '.') j++;
int lenX = i - si, lenY = j - sj; // number of significant digits (0 means the value 0)
if (lenX != lenY) return lenX < lenY ? -1 : 1;
for (int k = 0; k < lenX; k++) { // same length: the first differing digit decides
char cx = version1.charAt(si + k), cy = version2.charAt(sj + k);
if (cx != cy) return cx < cy ? -1 : 1;
}
i++;
j++;
}
return 0;
}
Complexity. Time O(n + m): every character is skipped, measured or compared a constant number of times. Space O(1).
This version passes all the same tests as the other two, and also handles inputs such as "1.99999999999999999999" vs "1.100000000000000000000" (20 digits vs 21, so -1) that would break the integer-based methods.
Edge cases and pitfalls
- Text comparison.
"1.2".compareTo("1.10")is positive, but the right answer is-1. Revisions are integers. - Leading zeros.
"01"equals"1", and"1.01"equals"1.001". Do not compare revision strings directly without stripping zeros. - Different numbers of revisions.
"1.0"equals"1.0.0.0", but"1.0.1"is greater than"1". Only non-zero extra revisions matter, which is why missing revisions must be treated as0rather than ending the comparison. - The regex dot.
split(".")splits on every character. Escape it as"\\.", or use the pointer-based approaches that never split. - Overflow. If revisions may exceed
2^31 - 1, use Approach 3 (orlongfor up to 18 digits). - Loop condition. The two-pointer loop must run while
i < norj < m. Usingandstops early and declares"1"equal to"1.0.1".
Related problems
- Implement ATOI/STRSTR parses digits from a string and guards against overflow, the same concerns as here.
- Encode and Decode Strings shows why naive splitting on a separator can lose information.
- Reverse every word in a string can likewise be solved by walking the string token by token with pointers instead of splitting it.
- Longest Common Prefix compares strings position by position, as Approach 3 does within a revision.