LeetCode 468. 验证IP地址 - 详细解析

发布于:2025-09-09 ⋅ 阅读:(21) ⋅ 点赞:(0)

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长度检查

算法核心思路

  1. 预判断:通过检查是否包含".“或”:"来初步判断IP类型
  2. 分割验证:将IP按分隔符分割成段,验证段数是否正确
  3. 逐段检查:对每个段进行格式和数值范围验证
  4. 字符级验证:确保每个字符都符合对应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,能检测到中间空段

代码优化技巧总结

  1. 关键的 split(-1):确保捕获所有边界情况,包括末尾和中间的空段
  2. 长度预判断:直接检查分割后数组长度,不符合直接返回false
  3. 语义化变量命名:使用 int n = part.length() 提高代码可读性
  4. 语义化方法调用:使用 part.startsWith("0") 替代 part.charAt(0) == '0'
  5. 详细的针对性注释:每个检查都注明具体处理的边界情况
  6. 精确的字符范围检查:IPv6手动检查三个字符范围,不使用Character.isDigit()
  7. 清晰的条件表达式:使用 !(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) 逻辑更直观,表达"不在范围内"
长度预检查 在循环中检查 预先检查数组长度 提早退出,避免无效处理

常见陷阱

  1. 忘记检查前导零(IPv4)
  2. 未正确处理空段(如连续分隔符)
  3. 字符范围检查不全面(IPv6的大小写字母)
  4. 数值范围检查遗漏边界值

网站公告

今日签到

点亮在社区的每一天
去签到