1. LeetCode 165题解析:版本号比较算法详解
版本号比较是软件开发中常见的需求,特别是在依赖管理、软件更新等场景下。这道LeetCode中等难度题目看似简单,但实际编码时需要处理多种边界情况。作为面试高频题,它考察的是开发者对字符串处理、边界条件判断和编码严谨性的把控能力。
我在实际面试中多次遇到候选人在这道题上翻车,主要问题出在忽略前导零处理和不等长版本号比较上。本文将结合Java实现代码,详细拆解解题思路和实现细节,并分享我在刷题和面试中总结的实战经验。
2. 问题分析与核心思路
2.1 题目要求解析
给定两个版本号字符串version1和version2,比较它们的大小。版本号由修订号组成,各修订号之间用点号分隔。每个修订号的数值是其整数转换值(忽略前导零)。比较规则如下:
- 从左到右依次比较每个修订号
- 如果版本号长度不一致,较短版本号缺失的修订号视为0
- 返回结果:
- version1 < version2 → 返回-1
- version1 > version2 → 返回1
- 相等 → 返回0
示例:
- "1.01" == "1.001"(忽略前导零)
- "1.0" < "1.0.1"(后者多一个修订号视为0.1)
- "0.1" < "1.1"(首位比较)
2.2 解题思路拆解
核心处理流程可分为三个步骤:
- 分割版本号:用点号作为分隔符将字符串拆分为修订号数组
- 标准化处理:将每个修订号转为整数(自动去前导零),不足位补0
- 逐位比较:从左到右依次比较对应位置的修订号
注意:Java的split方法对点号需要转义处理,因为点号在正则表达式中表示任意字符
3. Java实现详解
3.1 基础实现代码
java复制class Solution {
public int compareVersion(String version1, String version2) {
String[] levels1 = version1.split("\\.");
String[] levels2 = version2.split("\\.");
int length = Math.max(levels1.length, levels2.length);
for (int i=0; i<length; i++) {
Integer v1 = i < levels1.length ? Integer.parseInt(levels1[i]) : 0;
Integer v2 = i < levels2.length ? Integer.parseInt(levels2[i]) : 0;
int compare = v1.compareTo(v2);
if (compare != 0) {
return compare;
}
}
return 0;
}
}
3.2 关键代码解析
-
字符串分割:
java复制String[] levels1 = version1.split("\\.");- 使用
split("\\.")正确分割点号分隔的字符串 - 需要双反斜杠转义,因为点号在正则中表示任意字符
- 使用
-
长度处理:
java复制int length = Math.max(levels1.length, levels2.length);- 取两个数组的最大长度,确保遍历所有可能的修订号
-
修订号转换与补零:
java复制Integer v1 = i < levels1.length ? Integer.parseInt(levels1[i]) : 0;- 使用三元运算符处理数组越界情况
parseInt自动去除前导零(如"01"→1)
-
比较与提前返回:
java复制int compare = v1.compareTo(v2); if (compare != 0) return compare;- 一旦发现不相等的修订号立即返回结果
- 避免不必要的后续比较
4. 复杂度分析与优化
4.1 时间复杂度
- 字符串分割:O(M + N),M和N分别是两个版本号的长度
- 遍历比较:O(max(L1, L2)),L1和L2分别是分割后的数组长度
- 总体:O(M + N)
4.2 空间复杂度
- 存储分割后的数组:O(M + N)
- 可以优化为O(1)空间(见4.3优化方案)
4.3 优化方案:双指针法
java复制public int compareVersion(String version1, String version2) {
int i = 0, j = 0;
int n = version1.length(), m = version2.length();
while (i < n || j < m) {
int num1 = 0, num2 = 0;
while (i < n && version1.charAt(i) != '.') {
num1 = num1 * 10 + (version1.charAt(i++) - '0');
}
while (j < m && version2.charAt(j) != '.') {
num2 = num2 * 10 + (version2.charAt(j++) - '0');
}
if (num1 < num2) return -1;
if (num1 > num2) return 1;
i++; j++;
}
return 0;
}
优势:
- 无需存储分割后的数组,空间复杂度降为O(1)
- 边解析边比较,可能提前返回
劣势:
- 代码复杂度稍高
- 对指针移动的边界条件处理要求更严格
5. 常见问题与调试技巧
5.1 典型错误案例
-
未转义点号:
java复制// 错误写法 version1.split("."); // 会分割所有字符 -
前导零处理不当:
java复制// 错误示例 if (levels1[i] != levels2[i]) // 直接比较字符串,"01" != "1" -
数组越界:
java复制// 危险写法 Integer.parseInt(levels1[i]); // 当i >= levels1.length时会抛出异常
5.2 调试技巧
-
打印中间结果:
java复制
System.out.println(Arrays.toString(levels1)); System.out.println(Arrays.toString(levels2)); -
边界测试用例:
- 相同版本号:"1.0" vs "1.0"
- 不等长版本号:"1" vs "1.0.1"
- 前导零情况:"01.002" vs "1.2"
- 大数情况:"1234567890.987654321" vs "1234567890.987654321"
-
单步调试:
- 重点关注循环变量的变化
- 检查每个修订号的转换结果
6. 面试实战建议
6.1 面试官考察点
-
基础能力:
- 字符串处理基本功(split、parseInt等)
- 数组遍历与边界处理
-
代码质量:
- 异常处理(如空字符串输入)
- 变量命名和代码可读性
- 提前返回优化
-
扩展思考:
- 能否处理超大版本号(超过int范围)
- 其他分隔符情况(如"1-2-3")
- 字母版本号(如"1.0a")
6.2 回答策略
-
明确题意:
- 先确认比较规则(如是否区分大小写、允许的字符等)
- 举例说明理解("1.01" == "1.001")
-
分步实现:
- 先写分割逻辑
- 再处理转换和比较
- 最后处理不等长情况
-
主动优化:
- 提出双指针方案
- 讨论时间/空间复杂度
7. 实际工程应用
版本号比较在真实项目中很常见,比如:
-
依赖管理:
java复制// Maven/Gradle依赖版本冲突检测 if (compareVersion(currentVersion, minVersion) < 0) { throw new RuntimeException("Version too old"); } -
软件升级:
java复制// 检查是否需要更新 int cmp = compareVersion(localVersion, serverVersion); if (cmp < 0) { showUpdateDialog(); } -
API版本控制:
java复制// 路由到不同版本的API处理器 if (compareVersion(clientVer, "2.0") >= 0) { return new V2Handler(); } else { return new V1Handler(); }
工程实践中的增强考虑:
- 添加null检查
- 支持语义化版本号(SemVer)
- 添加日志记录
- 性能优化(如缓存比较结果)
8. 变种问题与扩展
8.1 变种题目
-
字母版本号:
- 如"1.0a" < "1.0b"
- 需要处理字母和数字混合情况
-
多分隔符:
- 如"1-2-3"和"1.2.3"视为相同
- 需要支持多种分隔符
-
语义化版本:
- 遵循SemVer规范(主版本.次版本.修订号)
- 特殊标记处理(如"1.0.0-alpha" < "1.0.0")
8.2 扩展实现
支持字母版本号的比较:
java复制public int compareVersionWithLetters(String v1, String v2) {
String[] parts1 = v1.split("\\.");
String[] parts2 = v2.split("\\.");
int len = Math.max(parts1.length, parts2.length);
for (int i = 0; i < len; i++) {
String p1 = i < parts1.length ? parts1[i] : "0";
String p2 = i < parts2.length ? parts2[i] : "0";
// 尝试解析为数字
Integer num1 = tryParse(p1);
Integer num2 = tryParse(p2);
// 都是数字的情况
if (num1 != null && num2 != null) {
int cmp = num1.compareTo(num2);
if (cmp != 0) return cmp;
}
// 其他情况按字符串比较
else {
int cmp = p1.compareTo(p2);
if (cmp != 0) return cmp;
}
}
return 0;
}
private Integer tryParse(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return null;
}
}
9. 单元测试建议
完善的测试用例应包含:
java复制@Test
public void testCompareVersion() {
Solution solution = new Solution();
// 相等情况
assertEquals(0, solution.compareVersion("1.01", "1.001"));
assertEquals(0, solution.compareVersion("1.0.0", "1.0"));
// 小于情况
assertEquals(-1, solution.compareVersion("0.1", "1.1"));
assertEquals(-1, solution.compareVersion("1.0", "1.0.1"));
// 大于情况
assertEquals(1, solution.compareVersion("1.0.1", "1.0"));
assertEquals(1, solution.compareVersion("1.2", "1.1.9"));
// 边界情况
assertEquals(0, solution.compareVersion("", ""));
assertEquals(0, solution.compareVersion("1", "1.0.0.0"));
assertEquals(-1, solution.compareVersion("1", "2"));
}
10. 刷题经验分享
这道题我在第一次做时犯了两个典型错误:
-
直接字符串比较:
最初我尝试直接比较字符串(如"1.01"和"1.1"),忽略了前导零和点号数量的影响,导致错误结果。 -
parseInt异常处理:
没有考虑空字符串情况,当遇到连续点号(如"1..2")时会抛出NumberFormatException。
经过多次练习后总结的经验:
- 先标准化再比较:把版本号转换为统一的格式(如去除前导零、补全长度)再比较
- 测试驱动开发:先写测试用例再写实现代码,特别是边界情况
- 双指针练习:虽然split方法更直观,但双指针法是更通用的字符串处理技巧
在面试中遇到这类题目时,建议:
- 先口头说明解题思路
- 边写代码边解释关键点
- 主动提出测试用例
- 讨论可能的优化方向