目录

题目描述

468. 验证 IP 地址

image-20250420050324314

题意分析

给定字符串,判断它是合法 IPv4(返回 "IPv4")、合法 IPv6(返回 "IPv6"),还是都不是(返回 "Neither")。本题没有算法难度,全部难点在于把规则逐条列清、一条不漏,面试时最好先当着面试官的面把规则复述一遍再动手。

IPv4 的合法规则:

  • 恰好 4 段,用 . 分隔,段与段之间、首尾都不能出现空段("1.1.1.1." 尾部多一个点即非法)。
  • 每段只含十进制数字,不允许正负号、空格或其他字符。
  • 每段数值在 0 到 255 之间。
  • 不允许前导零"01.1.1.1" 无效,"0.1.1.1" 有效——即长度大于 1 的段不能以 0 开头。

IPv6 的合法规则:

  • 恰好 8 段,用 : 分隔,同样不允许空段,因此 "::" 这种现实中合法的缩写在本题中无效。
  • 每段长度 1 到 4,字符只能是十六进制字符 0-9a-fA-F大小写均可
  • 允许前导零(与 IPv4 相反),但整段长度仍不能超过 4:"02001:..." 因为长度 5 而无效。

边界信号:字符串长度至多 50,可能不含任何分隔符(如纯数字 "1e1"),也可能同时混入两种分隔符,这些都应返回 "Neither"

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

核心思路

问题关键:这不是数值转换题,而是格式校验题。IPv4 要同时满足段数、字符、数值范围和前导零规则;IPv6 要满足段数、段长和十六进制字符规则,漏掉任意一项都会误判。

为什么选分段校验:两类地址的分隔符不同,先按 .: 分流,再让两个辅助函数各自负责一套规则,比大正则更容易说明和验证。切分必须保留空段,否则尾随分隔符会被漏掉。

不变量与正确性:进入段循环前已经确认段数正确;每轮结束后,当前段之前的所有段都满足该地址类型的全部规则。遇到任一违规立即返回 false,循环完成说明每个必要条件都成立,因此地址合法。

解题步骤

  1. 字符串含 . 时只尝试 IPv4;否则含 : 时只尝试 IPv6;都不含则返回 "Neither"
  2. IPv4 按 . 切成 4 段,并保留首尾及连续分隔符产生的空段。
  3. 每个 IPv4 段长度必须为 1~3;多位数不能以 0 开头;逐字符确认是 ASCII 数字并累加,值不能超过 255。
  4. IPv6 按 : 切成 8 段;每段长度必须为 1~4,字符只能是 0-9a-fA-F
  5. 任一检查失败就返回 "Neither",全部通过才返回对应类型。

代码实现

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';
    }
}
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)$,切分和校验都只线性扫描字符串。
  • 空间复杂度:$O(n)$,切分出的段占用线性空间;手动扫描段边界可降为 $O(1)$,但会增加实现细节。

关键点总结

  • 校验顺序固定为「段数 → 段长 → 字符 → 额外规则」,便于口述,也便于查漏。
  • Java split 要传 -1 保留尾部空段;"1.1.1.1." 必须判错。
  • IPv4 的前导零与数值范围是两条独立规则;IPv6 允许前导零,但不接受 :: 缩写。
  • 手动检查 ASCII 字符比 parseInt 更合适,可直接拒绝正负号等非数字字符。

易错点总结

  • Java 直接用 split("\\.") 会丢掉尾部空段,可能把 "1.1.1.1." 误判为 IPv4。
  • 只判断数值会放过 "01.1.1.1";多位 IPv4 段必须单独检查前导零。
  • Integer.parseInt 会接受 +1,所以 "+1.2.3.4" 仍应逐字符判错。
  • IPv6 不能把现实中的压缩写法直接套进题目;"2001:db8::1" 含空段,本题返回 "Neither"
  • 十六进制字符要同时支持大小写;8A2E 合法,8G2E 非法。

相似题目

题目 难度 考察点
93. 复原 IP 地址 中等 反向问题:用回溯枚举所有合法 IPv4 切分
165. 比较版本号 中等 同为按 . 分段解析,但比较数值而非校验
393. UTF-8 编码验证 中等 校验对象换成字节的位模式,规则驱动同款思路
65. 有效数字 困难 规则更多且相互嵌套,需要状态机组织校验