题目描述

✅ 468. 验证 IP 地址

image-20260928195009895

image-20260928195009896

题意分析

判断整个字符串是否符合题目规定的 IPv4 或 IPv6 格式,分别返回 "IPv4"、"IPv6";两种格式都不符合时返回 "Neither"。这里只判断格式,不查询地址是否真实存在。

IPv4 由点号分隔的四个十进制段组成,每段必须是 0 到 255 的整数,除单独的 0 外不能有前导零。IPv6 由冒号分隔的八个十六进制段组成,每段包含一到四个十六进制字符,允许前导零和字母大小写。

本题只接受完整的八段 IPv6,不接受双冒号压缩、IPv4 混合表示或空段。任意一个位置不满足规则,整串就不合法。

解法:分段后逐条校验规则

核心思路

[!blue]

两种格式都由固定数量的独立段组成,所以先检查整体分段,再逐段检查局部规则即可。代码根据分隔符分流:含点号就检查 IPv4,否则含冒号就检查 IPv6;两种分隔符混用时,分段后仍会留下不允许的字符,自然判错。

切分时必须保留空段。字符串以分隔符开头、结尾,或者出现连续分隔符,都意味着至少有一段为空。Java 的默认 split 会丢弃尾部空段,要传入 -1;点号在正则中有特殊意义,还需要转义。Go 的 strings.Split 按普通分隔字符串切分,并保留这些空段。

IPv4 先限制每段非空且不超过三位,并拒绝多位数的前导零。再逐字符确认是 ASCII 数字,用 value = value * 10 + digit 累加,最后判断是否超过 255。段长限制保证累加不会溢出;字符规则、前导零规则和数值上限缺一不可。

IPv6 只需检查八段,每段一到四位,字符都属于 0-9、a-f 或 A-F。四位以内的合法十六进制数本来就不会超过 FFFF,因此无需转换成整数。所有段都通过各自规则,才返回对应地址类型。

解题步骤

  1. 根据是否包含点号、冒号选择校验分支;没有对应分隔符则返回 "Neither"。
  2. 保留空段切分字符串,IPv4 必须恰好四段,IPv6 必须恰好八段。
  3. 对每个 IPv4 段,依次检查长度、前导零、十进制字符和累加后的数值上限。
  4. 对每个 IPv6 段,检查长度是否为一到四,以及每个字符是否为合法十六进制字符。
  5. 任一检查失败立即判错;只有全部段通过,才返回 "IPv4" 或 "IPv6"。

代码实现

class Solution {
    public String validIPAddress(String queryIP) {
        if (queryIP.indexOf('.') >= 0) {
            return isIPv4(queryIP) ? "IPv4" : "Neither";
        }

        if (queryIP.indexOf(':') >= 0) {
            return isIPv6(queryIP) ? "IPv6" : "Neither";
        }

        return "Neither";
    }

    private boolean isIPv4(String ip) {
        // 保留尾部空段,才能拒绝末尾多出的点号。
        String[] parts = ip.split("\\.", -1);

        if (parts.length != 4) {
            return false;
        }

        for (String part : parts) {
            // 先限制段长和前导零,再逐字符检查十进制数。
            if (part.isEmpty()
                    || part.length() > 3
                    || (part.length() > 1 && part.charAt(0) == '0')) {
                return false;
            }

            int value = 0;

            for (int i = 0; i < part.length(); i++) {
                char ch = part.charAt(i);

                if (ch < '0' || ch > '9') {
                    return false;
                }

                value = value * 10 + ch - '0';
            }

            if (value > 255) {
                return false;
            }
        }

        return true;
    }

    private boolean isIPv6(String ip) {
        String[] parts = ip.split(":", -1);

        if (parts.length != 8) {
            return false;
        }

        for (String part : parts) {
            if (part.isEmpty() || part.length() > 4) {
                return false;
            }

            for (int i = 0; i < part.length(); i++) {
                if (!isHexDigit(part.charAt(i))) {
                    return false;
                }
            }
        }

        return true;
    }

    private boolean isHexDigit(char ch) {
        return ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'f' || ch >= 'A' && ch <= 'F';
    }
}
import "strings"

func validIPAddress(queryIP string) string {
    if strings.Contains(queryIP, ".") {
        if isIPv4(queryIP) {
            return "IPv4"
        }
        return "Neither"
    }
    if strings.Contains(queryIP, ":") {
        if isIPv6(queryIP) {
            return "IPv6"
        }
        return "Neither"
    }
    return "Neither"
}

func isIPv4(ip string) bool {
    // 切分保留空段,首尾或连续分隔符都会在规则检查中被拒绝。
    parts := strings.Split(ip, ".")
    if len(parts) != 4 {
        return false
    }
    for _, part := range parts {
        // 先限制段长和前导零,再逐字符检查十进制数。
        if len(part) == 0 || len(part) > 3 || len(part) > 1 && part[0] == '0' {
            return false
        }
        value := 0
        for i := 0; i < len(part); i++ {
            if part[i] < '0' || part[i] > '9' {
                return false
            }
            value = value*10 + int(part[i]-'0')
        }
        if value > 255 {
            return false
        }
    }
    return true
}

func isIPv6(ip string) bool {
    parts := strings.Split(ip, ":")
    if len(parts) != 8 {
        return false
    }
    for _, part := range parts {
        if len(part) == 0 || len(part) > 4 {
            return false
        }
        for i := 0; i < len(part); i++ {
            if !isHexDigit(part[i]) {
                return false
            }
        }
    }
    return true
}

func isHexDigit(ch byte) bool {
    return ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'f' || ch >= 'A' && ch <= 'F'
}

复杂度分析

  • 时间复杂度:$O(n)$,n 为字符串长度。分隔符查找、切分和逐段校验各扫描至多线性数量的字符。
  • 空间复杂度:$O(n)$,切分结果需要保存段数组;对于包含大量分隔符的非法输入,段数也可能随输入长度增长。

关键点总结

[!green]

  • 先确认段数和段长,再读取字符或转换数值,避免空段访问和遗漏格式限制。
  • IPv4 同时约束字符、前导零和数值;IPv6 的长度与字符约束已经限制了数值范围。
  • 保留空段是校验整个字符串的必要条件,不能让多余分隔符在切分时消失。
  • 字符串能够转换成数值,不等于它符合题目规定的地址格式。

易错点总结

[!yellow]

  • Java 切分时没有转义点号,或者省略 -1 丢掉尾部空段,都会破坏对原始格式的判断。
  • 只检查 IPv4 的数值范围,没有单独检查多位数前导零,会把不合法的写法当成合法数字。
  • 在检查段是否为空之前就读取首字符,会在连续或末尾分隔符处发生越界。
  • 直接套用通用地址解析器支持的 IPv6 压缩规则,会接受本题明确不采用的格式。
  • 十六进制字符只检查小写,或者允许超过 f 的字母,都会错误判断 IPv6。
  • 只要部分段合法就返回类型,无法排除末尾的非法内容;必须检查段数并遍历所有段。

相似题目

题目 难度 关联与区别
93. 复原 IP 地址 中等 原题生成IPv4分段,本题验证既有地址;每段范围、前导零和段数约束可复用。
65. 有效数字 困难 同样做完整字符串格式校验,必须区分合法前缀与整串合法。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/08020973
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!