IP路由最长匹配
问题背景
最长前缀匹配 (Longest Prefix Match) 是 IP 路由器的一项核心功能。当路由器接收到一个 IP 数据包时,它需要查询自己的本地路由表,为该数据包找到最合适的下一跳路径。这个决策过程基于将数据包的“目的IP地址”与路由表中的条目进行匹配。
核心概念
-
IP地址 (IPv4):
一个32位的二进制数字,通常为了方便阅读而写成点分十进制字符串形式(例如 192.168.0.3)。
-
路由表条目 (Routing Entry):
格式为 entryIp/m,例如 10.166.50.0/23。
entryIp: 一个网络地址。m: 掩码长度 (Mask Length) ,表示用于网络匹配的、从左到右的二进制位数。例如,m=24意味着只比较IP地址的前24位。
匹配规则
一条路由 entryIp/m 与一个目的IP地址 dstIp 匹配,当且仅当:
将 entryIp 和 dstIp 都转换为32位二进制数后,它们最左边的 m 位完全相同。
-
特例:默认路由:
路由 0.0.0.0/0 的掩码长度 m 为 0,意味着它需要比较 0 位前缀。因此,它与任何目的IP地址都永远匹配。
选择规则 (如何找到最佳匹配)
从所有匹配的路由中,路由器会根据以下规则选出唯一的最佳路由:
- 最长匹配优先: 优先选择掩码长度
m最大的那条路由。m越大,代表匹配得越精确。 - 顺序优先 (Tie-Breaker) : 如果有多条路由的掩码长度
m相同且均为最大值,则选择在输入的路由表ipTable中最先出现的那一条。
任务要求
给定一个目的IP地址 dstIp 和一个本地路由表 ipTable,请找出最佳匹配的路由。
- 如果存在匹配,则返回该路由字符串。
- 如果没有任何路由匹配(包括默认路由),则返回字符串
empty。
输入格式
-
第一行: 一个字符串
dstIp,表示目的IP地址。 -
第二行: 一个字符串数组
ipTable,表示本地路由表。1 <= ipTable.length <= 10000。ipTable中每个元素的掩码长度m的范围为[0, 32]。
输出格式
- 一个字符串,表示最长匹配的路由;如果没有任何路由匹配,则输出字符串
empty。
限制与要求
- 时间限制: C/C++
1000ms, 其他语言2000ms - 内存限制: C/C++
256MB, 其他语言512MB
样例说明
样例 1
-
输入:
dstIp:"192.168.0.3"ipTable:["10.166.50.0/23", "192.0.0.0/8", "10.255.255.255/32", "192.168.0.1/24", "127.0.0.0/8", "192.168.0.0/24"]
-
输出:
"192.168.0.1/24" -
解释:
-
寻找所有匹配的路由:
"192.0.0.0/8":dstIp的前8位 (192) 匹配。匹配,m=8。"192.168.0.1/24":dstIp的前24位 (192.168.0) 匹配。匹配,m=24。"192.168.0.0/24":dstIp的前24位 (192.168.0) 匹配。匹配,m=24。- 其他路由不匹配。
-
应用选择规则:
- 最长匹配: 匹配的路由中,最大掩码长度
m为 24。候选路由为"192.168.0.1/24"和"192.168.0.0/24"。 - 顺序优先: 在这两个候选路由中,
"192.168.0.1/24"在输入列表中出现得更早。
- 最长匹配: 匹配的路由中,最大掩码长度
-
最终结果: 选择
"192.168.0.1/24"。
-
样例 2
-
输入:
dstIp:"202.96.96.68"ipTable:["200.18.24.0/24"]
-
输出:
"empty" -
解释:
dstIp的第一部分202与路由200.18.24.0/24的第一部分200不匹配。没有其他路由,因此没有任何匹配项。
样例 3
-
输入:
dstIp:"10.110.32.77"ipTable:["127.0.0.1/8", "0.0.0.0/0"]
-
输出:
"0.0.0.0/0" -
解释:
"127.0.0.1/8"不匹配。"0.0.0.0/0"是默认路由,永远匹配。- 由于这是唯一匹配的路由,它就是最佳匹配。
import java.util.Arrays;
import java.util.Scanner;
import java.util.stream.Collectors;
public class Solution {
/**
* 将点分十进制的 IPv4 地址字符串转换为 32 位数值。
* 为了避免符号位问题,使用 long 类型来存储无符号的 32 位整数值。
*
* @param ipStr IP 地址字符串,例如 "192.168.0.3"
* @return 对应的 32 位数值
*/
private long ipToLong(String ipStr) {
String[] parts = ipStr.split("\\.");
long result = 0;
// 遍历四个部分
for (int i = 0; i < 4; i++) {
// 将当前部分解析为 long
long partValue = Long.parseLong(parts[i]);
// 将该部分的值左移相应的位数,然后通过“按位或”操作合并到结果中
// part[0] 左移 24 位, part[1] 左移 16 位, part[2] 左移 8 位, part[3] 不移位
result |= (partValue << (24 - 8 * i));
}
return result;
}
/**
* 在路由表中为给定的目的IP地址查找最长匹配的路由。
*
* 核心思想:
* 1. **数值化处理**:将点分十进制的IP地址字符串转换为32位的整数(用long存储以避免符号问题),
* 这样可以方便地进行位运算。
* 2. **掩码匹配**:对于一条路由 `entryIp/m`,要判断它是否与 `dstIp` 匹配,
* 需要比较它们二进制表示的前 `m` 位是否相同。这可以通过创建一个前 `m` 位为1、
* 后 `32-m` 位为0的子网掩码 `mask` 来实现。如果 `(dstIpValue & mask) == (entryIpValue & mask)`,
* 则说明匹配成功。
* 3. **遍历查找最优解**:遍历整个路由表,对每一条路由进行匹配检查。
* - 维护一个变量 `maxMaskLength` 记录当前找到的最佳匹配的掩码长度。
* - 维护一个变量 `bestMatchRoute` 记录对应的路由字符串。
* - 当找到一个匹配的路由时,如果它的掩码长度 `m` 大于 `maxMaskLength`,
* 就更新 `maxMaskLength` 和 `bestMatchRoute`。
* - 由于我们是按输入顺序遍历的,如果遇到掩码长度相同的匹配项,我们不会更新,
* 这就自然满足了“按先后顺序输出最先的”这一 tie-breaker 规则。
*
* @param dstIp 目的IP地址字符串
* @param ipTable 路由表,每个元素是 "ip/掩码长度" 格式的字符串
* @return 最长匹配的路由字符串;如果没有匹配的,返回 "empty"。
*/
public String findLongestMatch(String dstIp, String[] ipTable) {
// --- 1. 预处理 ---
// 将目的IP地址转换为数值形式,方便进行位运算
long dstIpValue = ipToLong(dstIp);
// --- 2. 初始化结果变量 ---
String bestMatchRoute = "empty"; // 存储找到的最佳匹配路由字符串,默认为 "empty"
int maxMaskLength = -1; // 存储找到的最佳匹配的掩码长度。初始化为 -1,
// 这样即使是 /0 的默认路由也能成为第一个有效匹配。
// --- 3. 遍历路由表 ---
for (String routeEntry : ipTable) {
// a. 解析路由条目,分离出 IP 和掩码长度
String[] parts = routeEntry.split("/");
String entryIpStr = parts[0];
int maskLength = Integer.parseInt(parts[1]);
// b. 将路由条目的 IP 也转换为数值形式
long entryIpValue = ipToLong(entryIpStr);
// c. 创建子网掩码以进行匹配检查
// 掩码是一个 32 位的数字,前 m 位是 1,后面 32-m 位是 0。
// 当 m=0 时,掩码应为 0。当 m=32 时,掩码应为全 1 (0xFFFFFFFF)。
// 使用 -1L (long类型的-1,其二进制表示所有位都是1) 左移 32-m 位可以得到这个效果。
// 特殊处理 m=0 的情况,因为在Java中左移32位是无效操作(相当于移动0位)。
long mask = (maskLength == 0) ? 0L : (-1L << (32 - maskLength));
// d. 检查是否匹配:
// 将目的IP和路由IP都与掩码进行“按位与”(&)操作,
// 这个操作会保留它们的前 m 位,并将后面的位置零。
// 如果结果相等,说明它们的前 m 位是相同的,即匹配成功。
if ((dstIpValue & mask) == (entryIpValue & mask)) {
// e. 更新最佳匹配
// 如果当前匹配的掩码长度 > 之前找到的最大掩码长度
if (maskLength > maxMaskLength) {
// 更新最大掩码长度
maxMaskLength = maskLength;
// 更新最佳匹配的路由为当前路由
bestMatchRoute = routeEntry;
}
// 如果 maskLength == maxMaskLength,我们不做任何事,
// 因为题目要求“按给出的先后顺序输出最先的”,
// 而我们已经找到了一个同样长度的匹配,所以保留第一个找到的。
}
}
// --- 4. 返回结果 ---
return bestMatchRoute;
}
}
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取第一行:目的IP地址
String dstIp = scanner.nextLine();
// 读取第二行:路由表数组的字符串表示
String ipTableLine = scanner.nextLine();
scanner.close();
// --- 解析输入 ---
// 解析形如 ["10.166.50.0/23", "192.0.0.0/8"] 的字符串
String[] ipTable = parseStringArray(ipTableLine);
// 清理可能存在的引号
String cleanedDstIp = dstIp.replaceAll(""", "").trim();
// 创建 Solution 类的实例并调用方法
Solution solution = new Solution();
String result = solution.findLongestMatch(cleanedDstIp, ipTable);
// 输出结果
System.out.println(result);
}
/**
* 辅助方法:解析形如 ["a", "b", "c"] 的输入字符串。
* @param line 输入行
* @return 字符串数组
*/
private static String[] parseStringArray(String line) {
if (line == null || line.length() <= 2) { // 至少应包含 "[]"
return new String[0];
}
// 移除首尾的 '[' 和 ']'
line = line.trim();
if (line.startsWith("[") && line.endsWith("]")) {
line = line.substring(1, line.length() - 1);
}
if (line.isEmpty()) {
return new String[0];
}
// 按 ", " 或 "," 分割,并去除每个元素首尾的引号 " 和空格
return Arrays.stream(line.split(","))
.map(s -> s.trim().replaceAll(""", ""))
.toArray(String[]::new);
}
}