文章目录
- LeetCode 468. 验证IP地址 - 详细解析
- 题目描述
- IPv4验证规则:
- IPv6验证规则:
- 最优Java解决方案(注释完整版)
- 关键变量含义及代码技巧
- 代码技巧详解
- 1. 前导零检查的最佳实践
- 2. IPv6为什么不能用Character.isDigit()
- 3. 针对性注释设计
- 算法核心思路
- 可视化演示过程
- 测试用例1:有效IPv4 - "172.16.254.1"(优化版演示)
- 测试用例2:有效IPv6 - "2001:0db8:85a3:0:0:8A2E:0370:7334"(优化版演示)
- 测试用例3:无效IPv4(前导零)- "192.168.01.1"(优化版演示)
- 测试用例4:无效IPv4(超出范围)- "256.256.256.256"(优化版演示)
- 测试用例5:无效IPv6(段过长)- "02001:0db8:85a3:0000:0000:8a2e:0370:7334"
- 测试用例6:无效格式(非法字符)- "192.168@1.1"
- 算法复杂度分析
- 关键技术点详解
- String的isEmpty() vs isBlank()方法对比
- 详细对比示例:
- 在IP验证中的应用:
- 安全的null检查:
- split() 方法的 limit 参数
- 代码优化技巧总结
- 代码优势对比
- 常见陷阱
LeetCode 468. 验证IP地址 - 详细解析
题目描述
给定一个字符串 queryIP。如果是有效的 IPv4 地址,返回 “IPv4” ;如果是有效的 IPv6 地址,返回 “IPv6” ;如果不是上述类型的 IP 地址,返回 “Neither” 。
IPv4验证规则:
- 格式:“x1.x2.x3.x4”
- 0 <= xi <= 255
- xi 不能包含前导零(除了"0"本身)
IPv6验证规则:
- 格式:“x1:x2:x3:x4:x5:x6:x7:x8”
- 1 <= xi.length <= 4
- xi 是十六进制字符串(0-9, a-f, A-F)
- 允许前导零
最优Java解决方案(注释完整版)
class Solution {public String validIPAddress(String queryIP) {if (queryIP.contains(".")) {return isValidIPV4(queryIP) ? "IPv4" : "Neither";} else if (queryIP.contains(":")) {return isValidIPV6(queryIP) ? "IPv6" : "Neither";} else return "Neither";}boolean isValidIPV4(String ip) {String[] parts = ip.split("\\.", -1);if (parts.length != 4) {return false;}for (String part : parts) {int n = part.length();if (n == 0 || n > 3) { // 针对""和2555的输入return false;}if (n > 1 && part.startsWith("0")) { // 针对10.01.1.1输入,前导零return false;}for (char c : part.toCharArray()) { // 针对非数字输入if (!Character.isDigit(c)) {return false;}}int num = Integer.parseInt(part); // 针对256.0.0.1输入if (!(num >= 0 && num <= 255)) {return false;}}return true;}boolean isValidIPV6(String ip) {String[] parts = ip.split(":", -1);if (parts.length != 8) {return false;}for (String part : parts) {int n = part.length();if (n == 0 || n > 4) { // 针对空串和11111输入return false;}for (char c : part.toCharArray()) {// 这里不能替换为Character.isDigit(c)if (!((c >= '0' && c <= '9') ||(c >= 'a' && c <= 'f') ||(c >= 'A' && c <= 'F'))) {return false;}}}return true;}
}
关键变量含义及代码技巧
变量名 | 含义 | 作用 |
---|---|---|
queryIP | 输入的待验证IP字符串 | 算法的输入参数 |
parts | 分割后的IP段数组 | 存储按".“或”:"分割的各个部分 |
part | 单个IP段字符串 | 用于验证每个独立的IP段 |
n | IP段的长度 | int n = part.length() 提高代码可读性 |
num | IPv4段的整数值 | 用于检查IPv4段是否在0-255范围内 |
c | 字符变量 | 用于逐字符检查是否符合格式要求 |
代码技巧详解
1. 前导零检查的最佳实践
if (n > 1 && part.startsWith("0")) { // 针对10.01.1.1输入,前导零return false;
}
为什么用 startsWith("0")
而不是 charAt(0) == '0'
?
- 更语义化,表达"以0开头"的意图更清晰
startsWith()
内部已经做了边界检查,更安全
2. IPv6为什么不能用Character.isDigit()
// 这里不能替换为Character.isDigit(c)
if (!((c >= '0' && c <= '9') ||(c >= 'a' && c <= 'f') ||(c >= 'A' && c <= 'F'))) {return false;
}
原因分析:
Character.isDigit(c)
只检查数字字符(0-9)- IPv6需要十六进制字符:数字(0-9) + 字母(a-f, A-F)
- 必须手动检查三个范围:数字、小写字母、大写字母
3. 针对性注释设计
代码中的注释都标明了具体要处理的边界情况:
// 针对""和2555的输入
→ 长度检查// 针对10.01.1.1输入,前导零
→ 前导零检查// 针对非数字输入
→ 字符合法性检查// 针对256.0.0.1输入
→ 数值范围检查// 针对空串和11111输入
→ IPv6长度检查
算法核心思路
- 预判断:通过检查是否包含".“或”:"来初步判断IP类型
- 分割验证:将IP按分隔符分割成段,验证段数是否正确
- 逐段检查:对每个段进行格式和数值范围验证
- 字符级验证:确保每个字符都符合对应IP类型的要求
可视化演示过程
测试用例1:有效IPv4 - “172.16.254.1”(优化版演示)
步骤1:初步判断
queryIP = "172.16.254.1"
包含"." ✓ → 进入IPv4验证步骤2:分割并长度检查
parts = ["172", "16", "254", "1"]
parts.length = 4 ✓ → 段数正确,继续验证步骤3:逐段内联验证
part[0] = "172"
├── 长度检查: 3 ≤ 3 ✓
├── 前导零检查: 首字符'1' ≠ '0' ✓
├── 字符检查: '1','7','2' 全为数字 ✓
└── 数值检查: Integer.parseInt("172") = 172 ≤ 255 ✓part[1] = "16"
├── 长度检查: 2 ≤ 3 ✓
├── 前导零检查: 首字符'1' ≠ '0' ✓
├── 字符检查: '1','6' 全为数字 ✓
└── 数值检查: Integer.parseInt("16") = 16 ≤ 255 ✓part[2] = "254"
├── 长度检查: 3 ≤ 3 ✓
├── 前导零检查: 首字符'2' ≠ '0' ✓
├── 字符检查: '2','5','4' 全为数字 ✓
└── 数值检查: Integer.parseInt("254") = 254 ≤ 255 ✓part[3] = "1"
├── 长度检查: 1 ≤ 3 ✓
├── 前导零检查: 长度=1,无需检查 ✓
├── 字符检查: '1' 为数字 ✓
└── 数值检查: Integer.parseInt("1") = 1 ≤ 255 ✓结果:返回 "IPv4"
测试用例2:有效IPv6 - “2001:0db8:85a3:0:0:8A2E:0370:7334”(优化版演示)
步骤1:初步判断
queryIP = "2001:0db8:85a3:0:0:8A2E:0370:7334"
不包含"." 但包含":" ✓ → 进入IPv6验证步骤2:分割并长度检查
parts = ["2001", "0db8", "85a3", "0", "0", "8A2E", "0370", "7334"]
parts.length = 8 ✓ → 段数正确,继续验证步骤3:逐段内联验证
part[0] = "2001"
├── 长度检查: 4 ≤ 4 ✓
└── 内联十六进制检查:│ '2': '0'≤'2'≤'9' ✓│ '0': '0'≤'0'≤'9' ✓│ '0': '0'≤'0'≤'9' ✓│ '1': '0'≤'1'≤'9' ✓part[1] = "0db8"
├── 长度检查: 4 ≤ 4 ✓
└── 内联十六进制检查:│ '0': '0'≤'0'≤'9' ✓│ 'd': 'a'≤'d'≤'f' ✓│ 'b': 'a'≤'b'≤'f' ✓│ '8': '0'≤'8'≤'9' ✓part[5] = "8A2E"
├── 长度检查: 4 ≤ 4 ✓
└── 内联十六进制检查:│ '8': '0'≤'8'≤'9' ✓│ 'A': 'A'≤'A'≤'F' ✓│ '2': '0'≤'2'≤'9' ✓│ 'E': 'A'≤'E'≤'F' ✓... (其他段类似验证) ...结果:返回 "IPv6"
测试用例3:无效IPv4(前导零)- “192.168.01.1”(优化版演示)
步骤1:初步判断
queryIP = "192.168.01.1"
包含"." ✓ → 进入IPv4验证步骤2:分割并长度检查
parts = ["192", "168", "01", "1"]
parts.length = 4 ✓ → 段数正确,继续验证步骤3:逐段内联验证
part[0] = "192" ✓ (验证通过)
part[1] = "168" ✓ (验证通过)
part[2] = "01"
├── 长度检查: 2 ≤ 3 ✓
├── 前导零检查: 长度>1 且 首字符='0' ✗
└── 立即返回false,无需进行后续计算结果:返回 "Neither"
测试用例4:无效IPv4(超出范围)- “256.256.256.256”(优化版演示)
步骤1:初步判断
queryIP = "256.256.256.256"
包含"." ✓ → 进入IPv4验证步骤2:分割并长度检查
parts = ["256", "256", "256", "256"]
parts.length = 4 ✓ → 段数正确,继续验证步骤3:逐段内联验证
part[0] = "256"
├── 长度检查: 3 ≤ 3 ✓
├── 前导零检查: 首字符'2' ≠ '0' ✓
├── 字符检查: '2','5','6' 全为数字 ✓
└── 数值检查: Integer.parseInt("256") = 256 > 255 ✗结果:返回 "Neither"
测试用例5:无效IPv6(段过长)- “02001:0db8:85a3:0000:0000:8a2e:0370:7334”
步骤1:初步判断
queryIP = "02001:0db8:85a3:0000:0000:8a2e:0370:7334"
不包含"." 但包含":" ✓ → 可能是IPv6步骤2:分割IP
parts = ["02001", "0db8", "85a3", "0000", "0000", "8a2e", "0370", "7334"]
parts.length = 8 ✓ → 段数正确步骤3:逐段验证
part[0] = "02001"
├── 长度检查: 5 > 4 ✗
└── 验证失败!结果:返回 "Neither"
测试用例6:无效格式(非法字符)- “192.168@1.1”
步骤1:初步判断
queryIP = "192.168@1.1"
包含"." ✓ → 可能是IPv4步骤2:分割IP
parts = ["192", "168@1", "1"]
parts.length = 3 ≠ 4 ✗结果:返回 "Neither"
算法复杂度分析
- 时间复杂度:O(n),其中n是字符串长度。需要遍历整个字符串进行分割和验证
- 空间复杂度:O(1),除了存储分割后的段数组,不需要额外空间
关键技术点详解
String的isEmpty() vs isBlank()方法对比
在IP验证中,我们经常需要检查字符串是否为空,了解这两个方法的区别很重要:
方法 | 作用 | Java版本 | 检查内容 |
---|---|---|---|
isEmpty() | 检查长度是否为0 | Java 6+ | 只检查 length() == 0 |
isBlank() | 检查是否为空或只有空白字符 | Java 11+ | 检查 isEmpty() || 全为空白字符 |
详细对比示例:
String str1 = ""; // 空字符串
String str2 = " "; // 一个空格
String str3 = " \t\n "; // 多个空白字符(空格、制表符、换行符)
String str4 = " a "; // 包含非空白字符
String str5 = null; // null值// isEmpty() 结果:
str1.isEmpty() → true ✓ (长度为0)
str2.isEmpty() → false ✗ (长度为1)
str3.isEmpty() → false ✗ (长度为4)
str4.isEmpty() → false ✗ (长度为3)
// str5.isEmpty() → NullPointerException!// isBlank() 结果 (Java 11+):
str1.isBlank() → true ✓ (长度为0)
str2.isBlank() → true ✓ (只有空白字符)
str3.isBlank() → true ✓ (只有空白字符)
str4.isBlank() → false ✗ (包含非空白字符)
// str5.isBlank() → NullPointerException!
在IP验证中的应用:
// 检查IP段是否为空的不同方式
private boolean isValidIPv4Part(String part) {// 方式1:直接检查长度(推荐)if (part.length() == 0) return false;// 方式2:使用isEmpty()(等效)if (part.isEmpty()) return false;// 方式3:使用isBlank()(Java 11+,更严格)if (part.isBlank()) return false; // 会拒绝 " " 这样的空格段// ... 其他验证逻辑
}
安全的null检查:
// 推荐的安全检查方式
public static boolean isNullOrEmpty(String str) {return str == null || str.isEmpty();
}public static boolean isNullOrBlank(String str) {return str == null || str.isBlank(); // Java 11+
}
split() 方法的 limit 参数
String[] parts = ip.split("\\.", -1);
split(regex, limit)
中的 limit
参数控制分割行为:
limit值 | 行为 | 示例 |
---|---|---|
limit > 0 | 最多分割成limit个部分 | "a.b.c".split("\\.", 2) → ["a", "b.c"] |
limit = 0 | 默认行为,移除尾部空字符串 | "a.b.".split("\\.") → ["a", "b"] |
limit < 0 | 保留所有空字符串(包括尾部) | "a.b.".split("\\.", -1) → ["a", "b", ""] |
为什么IP验证必须用 -1
?
// 测试案例对比
String ip1 = "192.168.1."; // 末尾多点号
String ip2 = "192..168.1"; // 连续点号// 不使用-1 (默认行为)
ip1.split("\\.") → ["192", "168", "1"] // 长度=3,错误!应该是4段
ip2.split("\\.") → ["192", "", "168", "1"] // 长度=4,但漏掉了空段检测// 使用-1 (正确行为)
ip1.split("\\.", -1) → ["192", "168", "1", ""] // 长度=4,能检测到末尾空段
ip2.split("\\.", -1) → ["192", "", "168", "1"] // 长度=4,能检测到中间空段
代码优化技巧总结
- 关键的 split(-1):确保捕获所有边界情况,包括末尾和中间的空段
- 长度预判断:直接检查分割后数组长度,不符合直接返回false
- 语义化变量命名:使用
int n = part.length()
提高代码可读性 - 语义化方法调用:使用
part.startsWith("0")
替代part.charAt(0) == '0'
- 详细的针对性注释:每个检查都注明具体处理的边界情况
- 精确的字符范围检查:IPv6手动检查三个字符范围,不使用
Character.isDigit()
- 清晰的条件表达式:使用
!(num >= 0 && num <= 255)
明确表达范围检查
代码优势对比
优化项 | 传统写法 | 当前实现 | 优势说明 |
---|---|---|---|
变量命名 | part.length() 重复调用 | int n = part.length() | 提高可读性,减少重复计算 |
前导零检查 | part.charAt(0) == '0' | part.startsWith("0") | 语义更清晰,表达意图更直接 |
注释设计 | 简单功能注释 | 针对性边界情况注释 | 明确每个检查要处理的具体问题 |
字符范围检查 | 使用库函数或正则 | 手动三范围检查 | IPv6需求下更精确,避免误判 |
条件表达式 | num < 0 || num > 255 | !(num >= 0 && num <= 255) | 逻辑更直观,表达"不在范围内" |
长度预检查 | 在循环中检查 | 预先检查数组长度 | 提早退出,避免无效处理 |
常见陷阱
- 忘记检查前导零(IPv4)
- 未正确处理空段(如连续分隔符)
- 字符范围检查不全面(IPv6的大小写字母)
- 数值范围检查遗漏边界值