1. 项目背景与需求解析
在互联网应用开发中,IP地址定位是一个常见需求。我们经常需要根据用户IP快速确定其所在城市,用于内容分发、广告投放或安全风控等场景。华为OD的这道机试题正是模拟了这一实际业务需求。
题目核心是:给定一组IP区间与城市的对应关系,以及若干个待查询IP,要求高效返回每个IP对应的城市。这本质上是一个典型的区间覆盖问题——我们需要在大量数据中快速定位某个值所属的区间。
关键难点在于:IPv4地址理论上约有42亿个可能值(2^32),不可能为每个IP单独存储城市信息。必须找到一种空间效率高、查询速度快的存储和检索方案。
2. 技术方案选型
2.1 数据结构对比
常见解决方案有以下几种:
| 方案 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 线性扫描 | O(n) | O(1) | 数据量极小 |
| 排序+二分查找 | O(logn) | O(n) | 静态数据 |
| 线段树 | O(logn) | O(n) | 动态数据 |
| 前缀树 | O(1) | O(n) | IP前缀匹配 |
经过分析,本题具有以下特点:
- IP区间数据是静态的(不会频繁修改)
- 需要支持大量查询
- 区间可能重叠
因此排序+二分查找是最佳选择。虽然线段树也能解决,但实现复杂度更高,对于静态数据优势不明显。
2.2 IP地址处理
IPv4地址本质是32位无符号整数,但通常表示为"a.b.c.d"的点分十进制格式。我们需要:
- 将IP字符串转换为整数
- 处理区间比较
- 处理CIDR表示法(如192.168.1.0/24)
转换示例代码:
uint32_t ipToInt(const string& ip) { uint32_t num = 0; size_t start = 0; for(int i=0; i<4; ++i) { size_t end = ip.find('.', start); string part = ip.substr(start, end-start); num = (num << 8) + stoi(part); start = end + 1; } return num; }3. 核心算法实现
3.1 区间数据结构设计
首先定义区间结构体:
struct IpRange { uint32_t start; // 区间起始IP(整数形式) uint32_t end; // 区间结束IP string city; // 对应城市 // 重载小于运算符用于排序 bool operator<(const IpRange& other) const { return start < other.start; } };3.2 预处理阶段
- 将所有IP区间转换为整数形式
- 按起始IP排序:
vector<IpRange> ranges; // ... 读取数据填充ranges ... sort(ranges.begin(), ranges.end());3.3 查询阶段
使用二分查找定位IP所在区间:
string findCity(uint32_t ip, const vector<IpRange>& ranges) { int left = 0, right = ranges.size() - 1; string result = "unknown"; while(left <= right) { int mid = left + (right - left)/2; if(ranges[mid].start <= ip) { if(ip <= ranges[mid].end) { return ranges[mid].city; } left = mid + 1; } else { right = mid - 1; } } return result; }4. 性能优化技巧
4.1 边界条件处理
实际数据中常见特殊情况:
- 区间重叠(如[1.1.1.1, 2.2.2.2]和[1.5.0.0, 1.6.0.0])
- 区间包含(如[1.0.0.0, 3.0.0.0]包含[2.0.0.0, 2.255.255.255])
解决方案:
- 预处理时合并重叠区间
- 查询时记录最后一个匹配的区间
4.2 内存优化
对于海量数据(如全球IP分配表):
- 使用位压缩存储城市ID而非字符串
- 构建分层索引结构
- 考虑使用Bloom Filter快速过滤不可能匹配的查询
5. 完整实现示例
#include <iostream> #include <vector> #include <algorithm> using namespace std; struct IpRange { /* 同上 */ }; class IpCityMapper { private: vector<IpRange> ranges; public: void addRange(const string& startIp, const string& endIp, const string& city) { ranges.push_back({ipToInt(startIp), ipToInt(endIp), city}); } void prepare() { sort(ranges.begin(), ranges.end()); // 可选:合并重叠区间 } string query(const string& ip) { uint32_t num = ipToInt(ip); // 二分查找实现同上 } static uint32_t ipToInt(const string& ip) { /* 同上 */ } }; int main() { IpCityMapper mapper; // 添加示例数据 mapper.addRange("1.0.0.0", "1.0.0.255", "北京"); mapper.addRange("1.0.1.0", "1.0.3.255", "上海"); mapper.prepare(); cout << mapper.query("1.0.0.100") << endl; // 输出:北京 cout << mapper.query("1.0.2.200") << endl; // 输出:上海 cout << mapper.query("2.0.0.1") << endl; // 输出:unknown return 0; }6. 常见问题与解决方案
6.1 性能瓶颈分析
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 查询速度慢 | 数据未排序 | 确保预处理时调用prepare() |
| 内存占用高 | 城市字符串重复 | 使用字符串池或数字ID |
| 结果错误 | IP转换出错 | 检查ipToInt()的边界处理 |
6.2 实际应用建议
对于生产环境,建议:
- 使用内存映射文件处理超大数据集
- 考虑使用GeoIP等专业库
- 添加LRU缓存高频查询
在华为OD机试中注意:
- 明确处理输入输出格式
- 添加必要注释说明算法思路
- 测试边界条件(如最小/最大IP值)
7. 扩展思考
这种区间覆盖问题的解法可以推广到许多类似场景:
- 时间区间查询(如会议日程安排)
- 数值范围匹配(如税率计算)
- 版本号区间判断
在C++实现中,可以进一步优化:
- 使用lower_bound替代手写二分
- 考虑使用STL的partition_point
- 对于动态数据,改用std::set维护有序区间
我在实际开发中发现,这类问题的核心在于选择合适的数据结构和预处理策略。对于静态数据,排序+二分查找的组合几乎总是最佳选择,它提供了O(logn)的查询效率,而预处理成本只需支付一次。