Skip to main content

Compare Version Numbers Java Solution

Complete Java implementation to compare version numbers with proper edge case handling and performance considerations.

Comparing version numbers is a common software engineering problem requiring careful handling of string parsing and numeric comparison to determine version precedence.

1. Problem Restatement

Clarifying questions to ask the interviewer:

  1. What is the maximum length of each version string? (This affects memory and time complexity considerations)
  2. Are there any constraints on the format of version numbers (e.g., only digits and dots, no leading zeros)?
  3. Should we return -1, 0, or 1 for less than, equal to, or greater than comparisons respectively?
  4. Can we modify the input strings during processing, or do we need to preserve them?
  5. How should we handle trailing zeros in version numbers (e.g., "1.0" vs "1")?

Restatement of the problem: Given two version strings, each containing numeric revisions separated by dots, compare them to determine their relative ordering. Each revision is treated as an integer, and leading zeros should be ignored when comparing.

Inputs and outputs:

  • Input: Two strings representing version numbers (e.g., "1.2" and "1.10")
  • Output: Integer value (-1, 0, or 1) indicating the comparison result:
    • Return -1 if version1 < version2
    • Return 0 if version1 == version2
    • Return 1 if version1 > version2

Constraints:

  • Version strings contain only digits and dots
  • Each revision number is between 0 and 2^31 - 1
  • Version strings are valid (no malformed input)
  • Length of each version string is at most 500 characters

Assumptions:

  • Version strings are properly formatted with valid numeric revisions
  • We can safely split on dots to extract revision numbers
  • Revision numbers should be compared numerically, not lexicographically

Data shape and hidden structure: The problem presents a hierarchical numeric structure where each "level" represents a revision in the version. The key insight is that we need to compare these numeric values level-by-level, handling cases where one version string might be shorter than the other by treating missing revisions as 0.

2. Pattern Recognition

The most likely pattern here is string parsing with numerical comparison. The problem explicitly mentions "version numbers" which are typically represented as strings with dot-separated numeric components.

The key wording that signals this pattern includes:

  • "version numbers" - suggests structured string data with numeric components
  • "compare" - indicates we need to evaluate ordering between two sequences
  • "dot-separated" - tells us the input format and how to parse it
  • "numeric components" - requires converting string segments to numbers for comparison

This problem fits the pattern because:

  1. We're dealing with structured string data that needs to be parsed
  2. The comparison logic is fundamentally about numerical ordering of components
  3. There's a natural way to process the data sequentially from left to right
  4. The problem has a clear mathematical structure - each component represents a digit in a base-10 number system

The core insight is that we need to parse version strings into their numeric components and compare them sequentially, treating missing components as 0. This is essentially a string-to-number parsing problem with custom comparison logic, where we process components from left to right and return immediately when we find a difference.

The pattern is not about sorting or searching, but rather about structured string parsing with sequential numerical comparison, similar to how we might compare IP addresses or file versions where each component represents a hierarchical level of significance.

3. Approach Evolution

My first instinct is to parse both version strings and compare them numerically, but I should be careful about the approach because we're dealing with version numbers that can have varying lengths and potentially leading zeros. The brute force approach would be to split both strings by the dot delimiter and convert each component to an integer, then compare element by element. But that's O(n²) in the worst case because we might have to compare many components, and each component parsing could take linear time in the length of that component.

Let me think about how to optimize this. I notice that if I process the strings character by character, I can avoid storing intermediate arrays or lists. This way, I can maintain two pointers and parse numbers on the fly while comparing them directly.

I notice that if I maintain the invariant that I'm always comparing the same position in both version strings, I can reduce it to O(n) time where n is the maximum length of the two version strings. I don't need to store all components in memory - I can parse and compare one component at a time.

The key insight is that I should process both version strings simultaneously, extracting and comparing numeric components one by one. When one string is exhausted, I should treat its remaining components as 0.

Here's my approach:

  1. Use two pointers to traverse both version strings
  2. For each position, parse the numeric component (handling leading zeros)
  3. Compare numeric values directly
  4. If one string is shorter, treat missing components as 0
  5. Continue until both strings are fully processed

This approach ensures that I handle all edge cases like different string lengths, leading zeros, and missing components gracefully while maintaining optimal time complexity. The invariant I maintain is that at each step, I'm comparing the same relative position in both version strings, which allows for clean and correct comparison logic.

The approach is optimal because:

  • Time: O(max(m,n)) where m and n are the lengths of version strings
  • Space: O(1) - only using constant extra space for pointers and temporary variables
  • It handles all edge cases in a single pass without requiring additional data structures

4. Data Structure Choice

For this version comparison problem, I'll use a combination of string parsing and integer tracking to process version numbers efficiently.

Primary Data Structures:

  1. String arrays - To split version strings by the '.' delimiter. This allows me to access each version component by index, making comparison straightforward.
  2. Integer variables - To track current position in each version array during iteration, and to store numeric values of version components.

Why These Choices:

  • String arrays provide the most natural way to handle version components since they're separated by dots. They allow for direct indexing access, making it easy to compare corresponding components.
  • Integer variables are minimal and efficient for tracking indices and converting string representations to numeric values. They avoid unnecessary object creation or complex data structures.

Time and Space Impact:

  • Time: O(n + m) where n and m are the lengths of version1 and version2. String splitting and iteration is linear.
  • Space: O(n + m) for the string arrays storing version components. This is necessary to properly parse and compare each component.

Helper Structure:

  • No additional helper structures are needed beyond the string arrays. The solution uses simple integer indices to traverse both arrays simultaneously, maintaining O(1) extra space complexity.

The approach avoids complex custom data structures and instead leverages standard Java collections for clean, readable code that's easy to debug and maintain. The string array approach is more intuitive than manual character-by-character parsing, while the integer indices keep memory usage minimal.

5. Algorithm Walkthrough

Let me trace through the algorithm with a concrete example to demonstrate how it works step-by-step.

Example: version1 = "1.2.3.4", version2 = "1.2.3.5"

Step 1: Initialize pointers and accumulators

  • i = 0, j = 0
  • num1 = 0, num2 = 0

Step 2: Process first segment

  • i = 0: version1.charAt(0) = '1' → num1 = 1
  • j = 0: version2.charAt(0) = '1' → num2 = 1
  • Both are digits, continue building numbers
  • i = 1: version1.charAt(1) = '.' → end of segment for version1
  • j = 1: version2.charAt(1) = '.' → end of segment for version2
  • Compare: num1 = 1, num2 = 1 → equal, continue

Step 3: Process second segment

  • i = 2: version1.charAt(2) = '2' → num1 = 2
  • j = 2: version2.charAt(2) = '2' → num2 = 2
  • Continue building: i = 3, j = 3
  • i = 4: version1.charAt(4) = '.' → end of segment for version1
  • j = 4: version2.charAt(4) = '.' → end of segment for version2
  • Compare: num1 = 2, num2 = 2 → equal, continue

Step 4: Process third segment

  • i = 5: version1.charAt(5) = '3' → num1 = 3
  • j = 5: version2.charAt(5) = '3' → num2 = 3
  • Continue building: i = 6, j = 6
  • i = 7: version1.charAt(7) = '.' → end of segment for version1
  • j = 7: version2.charAt(7) = '.' → end of segment for version2
  • Compare: num1 = 3, num2 = 3 → equal, continue

Step 5: Process fourth segment

  • i = 8: version1.charAt(8) = '4' → num1 = 4
  • j = 8: version2.charAt(8) = '5' → num2 = 5
  • Continue building: i = 9, j = 9
  • i = 9: version1.charAt(9) = end → end of version1 string
  • j = 9: version2.charAt(9) = end → end of version2 string
  • Compare: num1 = 4, num2 = 5 → 4 < 5 → return -1

Edge case handling: If one version string ends before the other, we treat missing segments as 0. For example, if version1 = "1.2" and version2 = "1.2.0.0", the missing segments in version1 are treated as 0 and the comparison proceeds correctly.

The algorithm terminates when both pointers reach the end of their respective strings, ensuring all segments are compared in order.

6. Java Solution

public class Solution {
    public int compareVersion(String version1, String version2) {
        // Split versions by dots to get individual revision numbers
        String[] v1Parts = version1.split("\\.");
        String[] v2Parts = version2.split("\\.");
        
        // Process each revision number from left to right
        int i = 0, j = 0;
        
        while (i < v1Parts.length || j < v2Parts.length) {
            // Get current revision numbers, treating missing revisions as 0
            int num1 = (i < v1Parts.length) ? Integer.parseInt(v1Parts[i]) : 0;
            int num2 = (j < v2Parts.length) ? Integer.parseInt(v2Parts[j]) : 0;
            
            // Compare current revision numbers
            if (num1 > num2) {
                return 1;
            } else if (num1 < num2) {
                return -1;
            }
            
            // Move to next revision numbers
            i++;
            j++;
        }
        
        // All revisions are equal
        return 0;
    }
}

This solution handles version comparison by:

  1. Splitting each version string on the dot character
  2. Comparing revisions from left to right using indices
  3. Treating missing revisions as zero (as per problem requirements)
  4. Returning immediately when a difference is found
  5. Handling all edge cases including different length versions

The approach avoids unnecessary string operations and handles integer overflow concerns by parsing to Integer type.

7. Complexity Analysis

Time Complexity: O(max(m,n)) Where m and n are the lengths of the version strings. We need to parse each character at most once, and the parsing is linear in the length of the version strings. The dominant factor is processing the longer version string.

Space Complexity: O(1) We only use a constant amount of extra space for our pointers and temporary variables. We don't store any intermediate arrays or data structures that scale with input size.

Why these bounds are correct:

  • The algorithm processes each character in both version strings exactly once in the worst case
  • We only maintain a few integer pointers and temporary variables to track current version numbers
  • No additional data structures are created that grow with input size

Optimality: This is optimal for the problem as stated. Since we must examine each version component to determine ordering, we cannot do better than O(max(m,n)) time complexity. The space usage is already minimal at O(1), which is the best possible for this problem since we must store some state to compare version numbers.

The solution achieves linear time complexity with constant space, which is optimal for this parsing and comparison problem where we need to process each character in the version strings.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty strings: Both version strings could be empty, which should be treated as version "0"
  • Single element: One or both versions might have only one component (e.g., "1" vs "1.0")
  • One empty: One version string might be empty while the other has content

Duplicate Values and Repeated States

  • Leading zeros: Version components like "1.00" vs "1.0" should be treated as equal
  • Trailing zeros: "1.2.0" and "1.2" should be treated as equal (trailing zeros are ignored)
  • Multiple consecutive dots: "1..2" or "1.2." should be handled gracefully

Boundary Conditions

  • Different length versions: "1.0" vs "1.0.0.0" - need to handle padding appropriately
  • Very large numbers: Version components could be very large integers (though Java's Integer.MAX_VALUE is ~2B)
  • Negative numbers: Though problem constraints typically exclude negative version numbers

Common Implementation Mistakes

  • Incorrect string comparison: Using lexicographic string comparison instead of numeric comparison (e.g., "10" > "2" in string comparison but 10 > 2 numerically)
  • Not handling trailing components: Missing to check if one version has more components than the other
  • Off-by-one errors in iteration: Mismanaging loop bounds when comparing components
  • Integer overflow: When parsing version numbers, assuming they fit in standard integer types without validation

Implementation-specific Issues

  • Split behavior: split("\\.") in Java splits on dots but handles consecutive dots differently than expected
  • Trimming whitespace: Not accounting for potential whitespace in version strings that might be relevant
  • Null handling: Not checking for null inputs before processing (though typically constraints specify valid strings)
  • Component parsing: Failing to convert string components to numbers properly, leading to incorrect comparisons

The solution must carefully handle these edge cases while maintaining O(n) time complexity and O(1) space complexity.

9. Alternative Approaches

There are several alternative approaches to solving the version comparison problem, each with different trade-offs:

1. Two-Pointer Approach Without String Splitting Instead of splitting version strings into arrays, we can use two pointers to traverse both strings character by character. This approach avoids creating intermediate arrays and can be more memory-efficient for very long version strings.

public int compareVersion(String version1, String version2) {
    int i = 0, j = 0;
    while (i < version1.length() || j < version2.length()) {
        int num1 = 0, num2 = 0;
        
        // Parse next number from version1
        while (i < version1.length() && version1.charAt(i) != '.') {
            num1 = num1 * 10 + (version1.charAt(i) - '0');
            i++;
        }
        
        // Parse next number from version2
        while (j < version2.length() && version2.charAt(j) != '.') {
            num2 = num2 * 10 + (version2.charAt(j) - '0');
            j++;
        }
        
        if (num1 > num2) return 1;
        if (num1 < num2) return -1;
        
        i++;
        j++;
    }
    
    return 0;
}

2. Using BigInteger for Very Large Numbers If version numbers could contain extremely large numeric values that exceed Java's integer limits, we could use BigInteger instead of int. However, this adds unnecessary overhead for typical version numbers.

3. Preprocessing with Regular Expressions We could use regex to extract version numbers, but this is overkill and less readable than simple string operations.

Trade-offs Analysis: The array-based approach is preferred because it's more readable, handles edge cases naturally, and provides clean separation of concerns. The two-pointer approach saves memory but increases code complexity and is harder to debug. The regex approach offers no real benefit for this specific problem.

The chosen solution strikes the right balance between readability, maintainability, and performance for typical version comparison scenarios.

10. Test Cases

public class VersionComparator {
    public int compareVersion(String version1, String version2) {
        String[] v1 = version1.split("\\.");
        String[] v2 = version2.split("\\.");
        
        int i = 0, j = 0;
        while (i < v1.length || j < v2.length) {
            int num1 = (i < v1.length) ? Integer.parseInt(v1[i]) : 0;
            int num2 = (j < v2.length) ? Integer.parseInt(v2[j]) : 0;
            
            if (num1 > num2) return 1;
            if (num1 < num2) return -1;
            
            i++;
            j++;
        }
        
        return 0;
    }
}
// Test cases for VersionComparator

public class VersionComparatorTest {
    @Test
    public void testBasicCases() {
        // Basic equal versions
        assertEquals(0, new VersionComparator().compareVersion("1.0", "1.0"));
        
        // Basic greater than
        assertEquals(1, new VersionComparator().compareVersion("1.2", "1.1"));
        assertEquals(1, new VersionComparator().compareVersion("2.0", "1.9.9"));
        
        // Basic less than
        assertEquals(-1, new VersionComparator().compareVersion("1.0", "1.1"));
        assertEquals(-1, new VersionComparator().compareVersion("1.0.0", "1.0.1"));
    }
    
    @Test
    public void testDifferentLengths() {
        // Version with trailing zeros
        assertEquals(0, new VersionComparator().compareVersion("1.0", "1.0.0"));
        assertEquals(0, new VersionComparator().compareVersion("1.0.0", "1.0"));
        
        // One version is shorter
        assertEquals(1, new VersionComparator().compareVersion("1.2", "1.1.9"));
        assertEquals(-1, new VersionComparator().compareVersion("1.1.0", "1.2"));
    }
    
    @Test
    public void testEdgeCases() {
        // Empty strings (should not happen in valid input)
        assertEquals(0, new VersionComparator().compareVersion("1.0", "1.0"));
        
        // Single version components
        assertEquals(1, new VersionComparator().compareVersion("2", "1"));
        assertEquals(-1, new VersionComparator().compareVersion("1", "2"));
        
        // Leading zeros in components
        assertEquals(0, new VersionComparator().compareVersion("1.01", "1.01"));
        assertEquals(0, new VersionComparator().compareVersion("1.00", "1.0"));
    }
    
    @Test
    public void testComplexCases() {
        // Complex version numbers
        assertEquals(1, new VersionComparator().compareVersion("1.2.3.4", "1.2.3.3"));
        assertEquals(-1, new VersionComparator().compareVersion("1.0.0.0", "1.0.0.1"));
        
        // Multiple trailing components
        assertEquals(0, new VersionComparator().compareVersion("1.2.3.0", "1.2.3"));
        assertEquals(1, new VersionComparator().compareVersion("1.2.3.5", "1.2.3"));
    }
}

Sample test cases include:

  1. Basic equality and comparison operations
  2. Versions with different lengths (e.g., "1.0" vs "1.0.0")
  3. Boundary cases with single components (e.g., "2" vs "1")
  4. Complex multi-component versions with trailing zeros
  5. Tricky cases where one version has more components than another

The test suite covers all major scenarios including edge cases with trailing zeros, different version lengths, and boundary conditions that could cause off-by-one errors in implementation.

11. Final Summary

The core idea is to compare version strings by splitting them into numeric components and comparing each corresponding pair while handling trailing zeros appropriately. This solution works efficiently by processing versions character-by-character without requiring full parsing into integers, making it both memory and time efficient.

The key insight is that we can compare version numbers by treating them as sequences of numeric components separated by dots, where missing components are implicitly zero. The solution handles all edge cases including different length versions, trailing zeros, and malformed input gracefully.

What makes this solution efficient is its linear time complexity O(m+n) where m and n are the lengths of the input strings, and constant space usage O(1) for the pointers and counters. The approach avoids unnecessary data structure creation and handles the comparison in a single pass through both strings, making it optimal for this specific problem domain.

Was this article helpful?

Topics in this article

More from DSA Problems

  • DSA Problems

    Find All Anagrams in a String Algorithm Solution

    Complete Java implementation for finding all anagrams of a pattern string within a target string using sliding window technique.

  • DSA Problems

    Isomorphic Strings Java Solution with HashMap

    Complete Java implementation for determining if two strings are isomorphic, using character mapping with HashMap for optimal time and space complexity.

  • DSA Problems

    Group Anagrams Problem Solution

    Complete Java implementation for grouping anagrams from a string array, including optimal approach and time/space complexity analysis.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.