当前位置: 首页 > news >正文

LeetCode 比较版本号:从 split 解法到双指针优化,彻底讲懂这道题

一、题目描述

LeetCode 上有一道经典字符串题:比较版本号

题目大意是:

给你两个版本号字符串version1version2,请你比较它们的大小。

版本号由若干个修订号组成,修订号之间用点号.分隔。

例如:

"1.0.3" "1.02.4" "2.1"

每一个被.分开的部分,叫做一个修订号。

题目要求:

  1. 从左到右依次比较每一个修订号。

  2. 修订号比较时,要转换成整数。

  3. 前导零需要忽略。

  4. 如果某个版本号的修订号数量较少,那么缺失的部分当成0

  5. 如果version1 > version2,返回1

  6. 如果version1 < version2,返回-1

  7. 如果二者相等,返回0


二、先看几个例子

示例一

version1 = "1.01" version2 = "1.001"

表面上看:

"01" "001"

字符串不一样。

但是题目说,修订号要转换成整数,并忽略前导零。

所以:

Number("01") === 1 Number("001") === 1

因此:

"1.01" 和 "1.001" 相等

返回:

0

示例二

version1 = "1.0" version2 = "1.0.0"

拆开后:

version1 = ["1", "0"] version2 = ["1", "0", "0"]

version1少了最后一个修订号。

题目规定,缺失的修订号当成0

所以可以理解成:

version1 = ["1", "0", "0"] version2 = ["1", "0", "0"]

因此二者相等,返回:

0

示例三

version1 = "1.0.1" version2 = "1"

拆开后:

version1 = ["1", "0", "1"] version2 = ["1"]

逐段比较:

第一段:1 == 1 第二段:0 == 0 第三段:1 > 0

所以:

"1.0.1" > "1"

返回:

1

三、为什么不能直接比较字符串?

很多初学者看到这个题,第一反应可能是直接比较:

if (version1 > version2) return 1; if (version1 < version2) return -1; return 0;

这样是不对的。

比如:

version1 = "1.10" version2 = "1.2"

从版本号规则来看:

第一段:1 == 1 第二段:10 > 2

所以:

"1.10" > "1.2"

但是如果按字符串比较,"10""2"会按照字符顺序比较。

字符串"10"的第一个字符是"1",字符串"2"的第一个字符是"2"

因为:

"1" < "2"

所以字符串比较会错误地认为:

"10" < "2"

这显然不符合版本号比较规则。

所以这道题不能直接比较整个字符串,必须按照.分割出来的每一段数字进行比较。


四、普通解法:split 分割

最容易想到的解法是:

  1. 使用split(".")把两个版本号拆成数组。

  2. 找到两个数组中较长的长度。

  3. 从左到右逐段比较。

  4. 每一段都转成数字。

  5. 如果某个数组没有当前段,就当成0

代码如下:

var compareVersion = function(version1, version2) { const arr1 = version1.split("."); const arr2 = version2.split("."); const len = Math.max(arr1.length, arr2.length); for (let i = 0; i < len; i++) { const num1 = Number(arr1[i] || 0); const num2 = Number(arr2[i] || 0); if (num1 > num2) { return 1; } if (num1 < num2) { return -1; } } return 0; };

这段代码比较直观,适合刚开始理解这道题。


五、split 解法的问题

split解法虽然简单,但是它会创建额外数组。

比如:

"1.02.003.4".split(".")

会得到:

["1", "02", "003", "4"]

也就是说,它会先把整个字符串全部拆开,然后再比较。

但是实际上,比较版本号的时候,并不一定需要知道后面所有修订号。

比如:

version1 = "2.0.0.0.0" version2 = "1.999.999.999"

第一段比较:

2 > 1

结果已经确定了。

后面的内容根本不需要再看。

所以,split解法有一个缺点:

空间复杂度是 O(n + m)

其中nm分别是两个版本号字符串的长度。

那么有没有办法不创建数组,一边扫描一边比较呢?

答案就是:双指针


六、双指针解法核心思想

双指针解法的核心是:

不提前拆分字符串,而是用两个指针分别扫描version1version2,每次取出当前修订号进行比较。

定义两个指针:

let i = 0; let j = 0;

其中:

i 指向 version1 当前扫描的位置 j 指向 version2 当前扫描的位置

每一轮循环中,我们分别从version1version2中读取一个修订号。

比如:

version1 = "1.01.3" version2 = "1.001.2"

第一轮读取:

version1 当前修订号:1 version2 当前修订号:1

第二轮读取:

version1 当前修订号:01,转换成整数是 1 version2 当前修订号:001,转换成整数是 1

第三轮读取:

version1 当前修订号:3 version2 当前修订号:2

发现:

3 > 2

所以直接返回:

1

七、如何一边扫描一边把字符串转成数字?

假设当前要读取这一段:

"123"

我们可以从左到右扫描。

初始:

num = 0

读到字符'1'

num = num * 10 + 1 num = 0 * 10 + 1 num = 1

读到字符'2'

num = num * 10 + 2 num = 1 * 10 + 2 num = 12

读到字符'3'

num = num * 10 + 3 num = 12 * 10 + 3 num = 123

所以代码可以这样写:

num = num * 10 + Number(version[i]);

这就是手动把字符串数字转换成整数。


八、为什么前导零会自动被忽略?

比如当前修订号是:

"001"

初始:

num = 0

读到第一个'0'

num = 0 * 10 + 0 num = 0

读到第二个'0'

num = 0 * 10 + 0 num = 0

读到'1'

num = 0 * 10 + 1 num = 1

所以"001"最后得到的数字就是1

前导零自然就被忽略了。

这也是双指针解法非常巧妙的地方。


九、双指针完整代码

var compareVersion = function(version1, version2) { let i = 0; let j = 0; const n = version1.length; const m = version2.length; while (i < n || j < m) { let num1 = 0; let num2 = 0; while (i < n && version1[i] !== ".") { num1 = num1 * 10 + Number(version1[i]); i++; } while (j < m && version2[j] !== ".") { num2 = num2 * 10 + Number(version2[j]); j++; } if (num1 > num2) { return 1; } if (num1 < num2) { return -1; } i++; j++; } return 0; };

十、代码详细讲解

1. 初始化两个指针

let i = 0; let j = 0;

i用来遍历version1

j用来遍历version2

它们都从字符串的第一个字符开始扫描。


2. 保存字符串长度

const n = version1.length; const m = version2.length;

后面循环时需要判断指针有没有越界,所以先保存两个字符串的长度。


3. 外层循环为什么用||

while (i < n || j < m) {

这里一定要注意,是||,不是&&

原因是:只要有一个版本号还没扫描完,就应该继续比较。

比如:

version1 = "1.0.1" version2 = "1"

version2扫描完之后,version1后面还有:

.0.1

这些内容仍然需要和version2缺失的修订号0比较。

如果写成:

while (i < n && j < m)

那么只要有一个字符串结束,循环就停止了。

这样会错误地认为:

"1.0.1" 和 "1" 相等

但实际上:

"1.0.1" > "1"

所以外层循环必须写成:

while (i < n || j < m)

4. 每一轮重新计算当前修订号

let num1 = 0; let num2 = 0;

每次进入外层循环,都表示要比较一个新的修订号。

所以num1num2都要重新初始化为0


5. 读取 version1 当前修订号

while (i < n && version1[i] !== ".") { num1 = num1 * 10 + Number(version1[i]); i++; }

这段代码的作用是:从当前位置i开始,一直读取到下一个.为止。

例如:

version1 = "1.02.3"

第一轮时,i指向:

1.02.3 ^ i

读取到数字1后,i继续往后走。

i指向.时:

1.02.3 ^ i

说明当前修订号已经读完了。

于是内部循环停止。

此时num1就是当前修订号的整数值。


6. 读取 version2 当前修订号

while (j < m && version2[j] !== ".") { num2 = num2 * 10 + Number(version2[j]); j++; }

这段逻辑和version1完全一样。

它负责读取version2的当前修订号。


7. 比较当前两个修订号

if (num1 > num2) { return 1; } if (num1 < num2) { return -1; }

版本号比较是从左到右逐级比较。

只要当前修订号已经分出大小,整个版本号的大小就确定了。

例如:

version1 = "2.0.0" version2 = "1.999.999"

第一段:

2 > 1

所以直接返回1

后面的:

0.0 999.999

已经不需要再比较了。


8. 为什么最后要i++j++

i++; j++;

内部循环停止时,指针通常会停在.的位置。

比如:

version1 = "1.02.3"

读取完第一段1后,i停在:

1.02.3 ^ i

下一轮我们不希望从.开始读,而是希望跳过这个点,从下一个数字开始读。

所以需要:

i++;

这样i就移动到:

1.02.3 ^ i

j++的作用也是一样。


9. 字符串结束后继续i++会不会出错?

不会。

因为读取修订号的时候有判断:

while (i < n && version1[i] !== ".")

如果i已经超过了字符串范围,i < n不成立,就不会进入循环。

此时:

num1 = 0

刚好表示缺失的修订号为0

比如:

version1 = "1" version2 = "1.0.0"

第一轮比较:

num1 = 1 num2 = 1

相等。

之后version1已经结束了。

第二轮:

num1 = 0 num2 = 0

第三轮:

num1 = 0 num2 = 0

最后所有修订号都相等,所以返回:

0

这正好符合题意。


十一、完整执行过程示例

以这个例子为例:

version1 = "1.0.1" version2 = "1"

初始:

i = 0 j = 0

第一轮

读取version1当前修订号:

num1 = 1

读取version2当前修订号:

num2 = 1

比较:

num1 == num2

继续下一轮。


第二轮

version1读取到第二段:

num1 = 0

version2已经结束,缺失部分当成0

num2 = 0

比较:

num1 == num2

继续下一轮。


第三轮

version1读取到第三段:

num1 = 1

version2仍然已经结束,缺失部分当成0

num2 = 0

比较:

num1 > num2

所以返回:

1

说明:

"1.0.1" > "1"

十二、复杂度分析

version1的长度为nversion2的长度为m

双指针扫描过程中,每个字符最多被访问一次。

所以时间复杂度是:

O(n + m)

整个过程中,只使用了:

i j num1 num2 n m

这些变量,没有创建额外数组。

所以空间复杂度是:

O(1)

十三、split 解法和双指针解法对比

解法思路时间复杂度空间复杂度优点缺点
split 解法先按.分割成数组,再逐段比较O(n + m)O(n + m)简单直观需要额外数组
双指针解法一边扫描一边读取当前修订号O(n + m)O(1)空间更优代码理解稍难

实际刷题时,如果只是想快速通过,split解法完全可以。

但如果想写出更优解,或者面试时体现对空间复杂度的优化,双指针解法更合适。


十四、这道题的关键点总结

这道题看起来是字符串比较,实际上考察的是:

  1. 能不能正确理解版本号的比较规则。

  2. 能不能避免直接字符串比较的错误。

  3. 能不能处理前导零。

  4. 能不能处理版本号长度不一致的情况。

  5. 能不能用双指针优化空间复杂度。

最终双指针解法可以概括为一句话:

用两个指针分别扫描两个版本号字符串,每次读取一个修订号,转成整数后比较;如果当前修订号不同,立即返回结果;如果全部相同,则返回 0。


十五、最终推荐代码

var compareVersion = function(version1, version2) { let i = 0; let j = 0; const n = version1.length; const m = version2.length; while (i < n || j < m) { let num1 = 0; let num2 = 0; while (i < n && version1[i] !== ".") { num1 = num1 * 10 + Number(version1[i]); i++; } while (j < m && version2[j] !== ".") { num2 = num2 * 10 + Number(version2[j]); j++; } if (num1 > num2) { return 1; } if (num1 < num2) { return -1; } i++; j++; } return 0; };

这份代码的优势是:不需要split创建数组,直接在原字符串上扫描,空间复杂度为O(1),是这道题更优雅的写法。

http://www.jsqmd.com/news/895048/

相关文章:

  • SQL核心技能全景图:DDL数据定义、DML安全操作、DQL高级查询、多表JOIN与窗口函数实战
  • CJ 4DPLEX 与科视 Christie 续签合作协议
  • 前门准则扩展:图模型视角下因果效应识别条件的放宽与验证
  • 【Lindy翻译工作流自动化实战指南】:20年本地化专家亲授5大不可跳过的自动化陷阱与避坑清单
  • 上蔡2026年亲测:靠谱电瓶品牌盘点
  • Cortex-M7 DSM仿真调试数据库缺失问题解决方案
  • API集成稳定性实战:防御静默变更与构建弹性架构
  • 海克斯大乱斗:一刀流(上篇)
  • STM32 USB自供电设备连接检测问题解决方案
  • 联邦学习梯度泄漏难题:基于区块链的群智学习如何破局?
  • 林散之的“当代草圣”都是被人吹出来的,说这话的人不在少数,那你再吹出来一个试试
  • 2026国内污水处理行业发展现状,一体化设备定制、刮泥机及沉淀池优质厂家综合推荐 - 栗子测评
  • 对接LangSmith
  • 3分钟学会专业LRC歌词制作!歌词滚动姬让你的音乐作品更专业
  • 构建AI代理智能数据管道:从手动投喂到自动化摄取
  • Claude提示词工程实战:从120条“秘密代码”中验证有效技巧与避坑指南
  • 明宣宗 朱瞻基
  • SkiaSharp + ViewFaceCore实战:手把手教你打造带标注保存功能的人脸识别Demo
  • 基于关节角度与1D-CNN的步态识别:原理、实现与工程应用
  • 强化学习与正则化Dropout优化中文任务型对话系统
  • A/B测试实战指南:从原理到实践,构建数据驱动决策体系
  • NMRPFlash实用指南:三步修复变砖的Netgear路由器
  • 车载OTA升级失败率超19%?:Lovable边缘协同升级框架揭秘——从断网续传到签名验签零信任加固全流程
  • 联控 Lionconit ITC-1705 工业平板电脑在 MES 系统中的应用方案
  • 避坑指南:用CCS9.0和普中开发板搞定TMS320F28335点灯(附完整工程模板)
  • 2026年快速温变试验箱厂家、高低温试验箱厂家推荐及冷热冲击试验箱厂家技术实力与市场格局解析 - 栗子测评
  • 多智能体系统共识机制:从Paxos到PBFT的工程选型与实战指南
  • APM Agent假活监控盲区:构建元监控体系确保可观测性真实有效
  • 非技术创始人实战:基于AI网关的LLM智能路由与成本优化
  • 块聚合模型:解决空间数据错配,实现高分辨率风险预测