【OD机试题解法笔记】根据IP查找城市
题目描述
某业务需要根据终端的IP地址获取该终端归属的城市,可以根据公开的IP地址池信息查询归属城市。
地址池格式如下:
城市名=起始IP,结束IP
起始和结束地址按照英文逗号分隔,多个地址段采用英文分号分隔。比如:
City1=1.1.1.1,1.1.1.2;City1=1.1.1.11,1.1.1.16;City2=3.3.3.3,4.4.4.4;City3=2.2.2.2,6.6.6.6
一个城市可以有多个IP段,比如City1有2个IP段。
城市间也可能存在包含关系,如City3的IP段包含City2的IP段范围。
现在要根据输入的IP列表,返回最佳匹配的城市列表。
注:最佳匹配即包含待查询IP且长度最小的IP段,比如例子中3.4.4.4最佳匹配是City2=3.3.3.3,4.4.4.4,5.5.5.5的最佳匹配是City3=2.2.2.2,6.6.6.6
输入描述
第一行为城市的IP段列表,多个IP段采用英文分号 ‘;’ 分隔,IP段列表最大不超过500000。城市名称只包含英文字母、数字和下划线。最多不超过100000个。IP段包含关系可能有多层,但不超过100层。
第二行为查询的IP列表,多个IP采用英文逗号 ‘,’ 分隔,最多不超过10000条。
输出描述
最佳匹配的城市名列表,采用英文逗号 ‘,’ 分隔,城市列表长度应该跟查询的IP列表长度一致。
备注
无论是否查到匹配正常都要输出分隔符。举例:假如输入IP列表为IPa,IPb,两个IP均未有匹配城市,此时输出为",",即只有一个逗号分隔符,两个城市均为空;
可以假定用例中的所有输入均合法,IP地址均为合法的ipv4地址,满足 (1-255).(0-255).(0-255).(0-255)的格式,且可以假定用例中不会出现组播和广播地址。
用例
输入
City1=1.1.1.1,1.1.1.2;City1=1.1.1.11,1.1.1.16;City2=3.3.3.3,4.4.4.4;City3=2.2.2.2,6.6.6.6
1.1.1.15,3.3.3.5,2.2.2.3
输出
City1,City2,City3
说明
1)City1有2个IP段,City3的IP段包含City2的IP段;
2)1.1.1.15仅匹配City1=1.1.1.11,1.1.1.16,所以City1就是最佳匹配;2.2.2.3仅匹配City3=2.2.2.2,6.6.6.6,所以City3是最佳匹配;3.3.3.5同时匹配为City2=3.3.3.3,4.4.4.4和City3=2.2.2.2,6.6.6.6,但是City2=3.3.3.3,4.4.4.4的IP段范围更小,所以City3为最佳匹配;
思考
难点在于判断 ip 地址包含关系。比如 3.3.3.5
包含于 [3.3.3.3,4
.4.4.4.4
],认为 3.3.3.5
大于 3.3.3.3
,显然前面 3 个十进制数都相等,最后一个 5 > 3。3.3.3.5
小于 4.4.4.4
是怎么得出的呢? 5 > 4, 但是前面的 3 小于 4,这是把 3.3.3.5
看成 3335, 把 4.4.4.4
看成 4444 ,比较 3335 < 4444 ? 假如比较 3.3.255.5
和 4.4.4.4
大小,还是 332555 > 4444 ?显然不对,3.3.255.5
应该小于 4.4.4.4
,左边是ip地址的高位,比较从高位往地位走。ip地址本质上是把一个32位整数转成二进制形式,然后均分成 4 段,每一段的二进制单独转成一个整数,最后用点连接起来形成一个地址字符串,这是点分表示法。这种点分形式从高位往地位进行比较大小和十进制整数类似,但操作起来不方便,可用直接把ip地址还原成整数再比较十进制整数值就方便多了。3.3.255.5
和 4.4.4.4
的二进制表示分别为00000011.00000011.11111111.00000101
、00000100.00000100.00000100.00000100
,去掉分割点,再转成十进制分别为12844805、67372036,显然后者更大。
参考代码
// 将IP地址转换为长整型数的函数
function ipToLong(ip) {// 使用split('.')将IP地址分割为四部分,然后使用reduce方法累加每部分return ip.split(".").reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0);
}// 解析IP地址池的函数
function parseIpPool(ipPool) {const cityIpRanges = {}; // 创建一个对象用于存储城市和对应的IP范围ipPool.split(";").forEach((cityRange) => {const [city, range] = cityRange.split("="); // 分割字符串获取城市名和IP范围const [startIp, endIp] = range.split(","); // 分割字符串获取起始IP和结束IPconst start = ipToLong(startIp); // 将起始IP转换为长整型数const end = ipToLong(endIp); // 将结束IP转换为长整型数if (!cityIpRanges[city]) cityIpRanges[city] = []; // 如果对象中不存在该城市,则初始化一个空数组cityIpRanges[city].push({ start, end }); // 将IP范围对象添加到对应城市的数组中});return cityIpRanges; // 返回解析后的城市IP范围对象
}// 匹配城市的函数
function matchCities(ipPool, queryIPs) {const cityIpRanges = parseIpPool(ipPool); // 调用parseIpPool函数解析IP池return queryIPs.split(",").map((ip) => {const ipNum = ipToLong(ip); // 将查询的IP地址转换为长整型数let bestMatchCity = ""; // 初始化最佳匹配城市为空字符串let smallestRange = Number.MAX_SAFE_INTEGER; // 初始化最小范围为最大安全整数for (const city in cityIpRanges) {// 遍历城市IP范围对象cityIpRanges[city].forEach((range) => {if (ipNum >= range.start && ipNum <= range.end) {// 判断IP是否在当前范围内const rangeSize = range.end - range.start; // 计算当前范围的大小if (rangeSize < smallestRange) {// 如果当前范围小于已知的最小范围bestMatchCity = city; // 更新最佳匹配城市smallestRange = rangeSize; // 更新最小范围}}});}return bestMatchCity; // 返回最佳匹配城市}).join(","); // 将匹配结果数组转换为字符串,并用逗号分隔
}function solution() {const ipPool = readline().split(';').map(item => {const [city, ip] = item.split('=');return [city, ip.split(',').map(ipToLong)];});const checkIps = readline().split(',').map(ipToLong);const result = [];for (let i = 0; i < checkIps.length; i++) {let ip = checkIps[i], found = false, smallestRange = Infinity;for (let [city, [startIp, endIp]] of ipPool) {if (ip >= startIp && ip <= endIp) {found = true;let len = endIp - startIp;if (smallestRange === Infinity) {result.push(city);smallestRange = len;} else if (smallestRange > len) {result[i] = city;smallestRange = len;}}}if (!found) {result.push('');}}console.log(result.join(','));
}const cases = [`City1=1.1.1.1,1.1.1.2;City1=1.1.1.11,1.1.1.16;City2=3.3.3.3,4.4.4.4;City3=2.2.2.2,6.6.6.6
1.1.1.15,3.3.3.5,2.2.2.3`,
];let caseIndex = 0;
let lineIndex = 0;const readline = (function () {let lines = [];return function () {if (lineIndex === 0) {lines = cases[caseIndex].trim().split("\n").map((line) => line.trim());}return lines[lineIndex++];};
})();cases.forEach((_, i) => {caseIndex = i;lineIndex = 0;solution();
});
;