← 返回题目列表

版本号比较如何逐段解析并忽略前导零?(LeetCode 165)

中等 第 17 / 25 题 更新于 2026/08/01
字符串算法双指针模拟版本号

简化版

版本号按 . 分段比较,每段按整数值比较,前导零不影响大小,缺失段视为 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相等
缺失段视为 01.0 vs 1相等

只要代码覆盖这 3 条,基本就不会错。

面试抓手:版本号比较的本质是“分段数值比较”,不是字符串整体比较,也不是简单比较段数。

三、双指针逐段解析

iversion1jversion2。每轮读取一个点之前的数字,遇到点或字符串结束就得到一段。

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 更稳,外层用 || 是为了处理尾部段;前导零靠数值解析自然消掉,超大段再切换到有效长度比较。