版本号比较如何逐段解析并忽略前导零?(LeetCode 165)
简化版
版本号按 . 分段比较,每段按整数值比较,前导零不影响大小,缺失段视为 0。可以用 split,也可以用双指针逐段解析,避免额外数组和大整数问题。
详细版
int compareVersion(String v1, String v2) {
int i = 0, j = 0, n = v1.length(), m = v2.length();
while (i < n || j < m) {
long a = 0, b = 0;
while (i < n && v1.charAt(i) != '.') {
a = a * 10 + (v1.charAt(i++) - '0');
}
while (j < m && v2.charAt(j) != '.') {
b = b * 10 + (v2.charAt(j++) - '0');
}
if (a != b) return a < b ? -1 : 1;
i++;
j++;
}
return 0;
}
如果段长度可能非常大,应跳过前导零后用字符串长度与字典序比较,避免 long 溢出。常见 LeetCode 约束下逐段转整数即可。
完整版教学
一、版本号不是普通字符串比较
字符串字典序会认为 "1.10" 小于 "1.2",因为字符 '1' 小于 '2'。但版本号每段是数值比较:
1.10 > 1.2
因为第二段 10 > 2
所以必须按点分段,而不能直接调用字符串比较。
二、比较规则拆成 3 条
版本号比较有固定规则:
| 规则 | 示例 | 结论 |
|---|---|---|
| 逐段比较 | 1.2 vs 1.10 | 第二段决定 |
| 前导零忽略 | 1.01 vs 1.001 | 相等 |
| 缺失段视为 0 | 1.0 vs 1 | 相等 |
只要代码覆盖这 3 条,基本就不会错。
面试抓手:版本号比较的本质是“分段数值比较”,不是字符串整体比较,也不是简单比较段数。
三、双指针逐段解析
用 i 扫 version1,j 扫 version2。每轮读取一个点之前的数字,遇到点或字符串结束就得到一段。
while (i < n && v1.charAt(i) != '.') {
a = a * 10 + v1.charAt(i) - '0';
i++;
}
外层循环条件必须是 i < n || j < m,因为某个版本已经结束时,另一个版本剩余段仍要和 0 比较。
四、为什么缺失段能自然处理
当 i >= n 时,本轮读取不到字符,a 保持 0。这刚好等价于“缺失段视为 0”。
v1 = "1.0.0"
v2 = "1"
比较过程:
1 vs 1
0 vs 0
0 vs 0
如果外层写成 i < n && j < m,后面两个 0 段就不会被检查,虽然这个例子仍相等,但遇到 "1.0.1" vs "1" 就会错。
五、前导零如何处理
如果段能安全转成整数,前导零自然消失:
"001" -> 1
"0001" -> 1
如果段可能超出整数范围,就不能直接累乘。更稳的写法是跳过前导零后比较有效长度,长度相同再逐字符比较。
"000123" 有效部分 "123"
"99" 有效部分 "99"
长度 3 > 2,所以 123 > 99
六、指针移动的边界
每轮解析完后,通常写 i++、j++ 跳过点。即使已经到末尾,多加 1 也不会访问字符,因为下一轮读取前会检查边界。
i 停在 '.' 处 -> i++ 跳过
i 停在 n 处 -> i++ 变 n+1,外层条件自然结束或继续处理另一边
要避免的是在没有检查 i < n 时直接 charAt(i)。
七、常见误区与追问
- 误区:直接用字符串字典序比较版本号。 数字段的大小不等于字符序。
- 误区:
split后只比较共同长度。 多出来的段要和 0 比较。 - 误区:把前导零当成更大长度。 版本段按数值比较,前导零无意义。
- 追问:如果段非常长怎么办? 跳过前导零后比较有效长度,再逐字符比较。
- 追问:为什么外层是
||? 任意一边还有段都要继续,缺失的一边视为 0。 - 追问:时间复杂度是多少? 每个字符最多扫描一次,
O(n+m),额外空间可做到O(1)。
八、加强记忆
版本号比较就记 3 句话:按点切段、段按数值、缺段补 0。双指针写法比 split 更稳,外层用 || 是为了处理尾部段;前导零靠数值解析自然消掉,超大段再切换到有效长度比较。