当前位置: 首页 > 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.rkmt.cn/news/1399837.html

相关文章:

  • XShell免费版的安装配置教程(附安装包)
  • 上蔡2026年亲测:靠谱电瓶品牌盘点
  • Cortex-M7 DSM仿真调试数据库缺失问题解决方案
  • STM32 USB自供电设备连接检测问题解决方案
  • 拒绝被官转割韭菜!Cursor / Claude Code 接入自定义 API 避坑与终极省钱指南
  • 3分钟学会专业LRC歌词制作!歌词滚动姬让你的音乐作品更专业
  • 八年Java老兵,三个月投了上百份简历没找到下家——2026年的招聘市场到底怎么了?
  • SSE实践(1)
  • Linux 批量添加 IP 并通过 systemd 开机自动恢复(适用于 Ubuntu / CentOS)
  • AI编码智能体配置优化:嵌套AGENTS文件架构设计与工程实践
  • acados实战:从环境搭建到部署的8个典型错误与解决方案
  • 2026工业低压配电柜源头厂家怎么选?靠谱智能工业配电柜品牌与实力厂商汇总推荐 - 栗子测评
  • 内网环境RPA自动化实践:自定义API与离线运行方案
  • 联邦学习梯度泄漏难题:基于区块链的群智学习如何破局?
  • DeepMetaForge:基于BEiT与深度元数据融合的皮肤病变分类框架
  • Laravel团队构建可复制AI交付体系:从混乱到秩序的实战指南
  • AWS自动化模式实战:25个事件驱动与工作流设计精解
  • 告别死记硬背:一张图+实战代码,带你搞懂CPAL中IL函数的核心分类与用法
  • CMSCure:动态UI内容管理引擎,告别应用商店审核实现实时更新
  • 2026年牵手红娘服务权威推荐深度分析:婚恋市场真实匹配效率低与用户信任缺失痛点 - 品牌推荐
  • 分配free空間給ubuntu server
  • 欧盟AI法案合规指南:SaaS企业五个月实战计划与风险应对
  • Air1601 RGB 屏硬件设计参考要点
  • 影刀RPA店群自动化成本优化实战:资源弹性伸缩与闲置治理
  • AI应用用户额度与用量管控系统架构设计与工程实践
  • Kaldi AISHELL-1实战:如何用G2P和Chain模型将中文ASR字错率降到10%以下
  • 会议录音整理太慢梳理不清?会议录音总结推荐供你参考
  • 整理会议录音工具口碑推荐|经过筛选的实用选择建议
  • 安装完UltraISO电脑多出个‘CD驱动器’删不掉?教你彻底关闭虚拟光驱功能
  • 从AlphaFold到药物推荐:用Python实战图机器学习,解决5个真实世界问题