Bootstrap

【2024年华为OD机试】 (C卷,200分)- 根据IP查找城市(JavaScript&Java & Python&C/C++)

在这里插入图片描述

一、问题描述

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段范围。

输入描述

输入共2行。

  • 第一行为城市的IP段列表,多个IP段采用英文分号 ‘;’ 分隔,IP段列表最大不超过500000。城市名称只包含英文字母、数字和下划线。最多不超过100000个。IP段包含关系可能有多层,但不超过100层。
  • 第二行为查询的IP列表,多个IP采用英文逗号 ‘,’ 分隔,最多不超过10000条。

输出描述

最佳匹配的城市名列表,采用英文逗号 ‘,’ 分隔,城市列表长度应该跟查询的IP列表长度一致。

备注

  • 无论是否查到匹配正常都要输出分隔符。举例:假如输入IP列表为IPa,IPb,两个IP均未有匹配城市,此时输出为",",即只有一个逗号分隔符,两个城市均为空;
  • 可以假定用例中的所有输入均合法,IP地址均为合法的ipv4地址,满足 (1255).(0255).(0255).(0255) 的格式,且可以假定用例中不会出现组播和广播地址;

用例

输入

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地址是否属于一个IP段范围。

解决思路

  1. IP地址转换为整型数值

    • IP地址本质上是4个8位二进制数,因此可以将每个IP地址转换为一个整型数值。例如,IP地址1.1.1.1可以转换为16843009
    • 通过将IP地址转换为整型数值,可以方便地通过数值大小关系判断某个IP地址是否属于某段IP范围。
  2. 遍历待查询的IP地址

    • 对于每个待查询的IP地址,遍历所有的IP段范围,判断该IP地址是否属于某个IP段。
    • 如果该IP地址属于多个IP段,则选择范围最小的那个IP段对应的城市作为最佳匹配。
  3. 输出最佳匹配的城市列表

    • 对于每个查询的IP地址,输出其最佳匹配的城市名。如果没有匹配的城市,则输出空字符串。

关键点

  • IP地址转换:将IP地址转换为整型数值是解决问题的关键步骤。
  • 范围匹配:通过比较IP地址的整型数值与IP段的起始和结束数值,判断IP地址是否属于该IP段。
  • 最佳匹配:在多个匹配的IP段中,选择范围最小的那个作为最佳匹配。

通过以上步骤,可以有效地解决IP地址归属城市查询的问题。

二、JavaScript算法源码

以下是代码的详细注释和讲解,帮助你理解每一部分的功能和实现逻辑:


代码结构

  1. 输入输出处理

    • 使用 readline 模块读取输入数据。
    • 将城市IP列表和待查询的IP列表分别解析为数组。
  2. IP地址转整型

    • 实现 ip2dec 函数,将IPv4地址转换为32位整型数值。
  3. Range类

    • 用于存储每个城市IP段的信息,包括城市名称、起始IP的整型值、结束IP的整型值以及IP段的长度。
  4. 匹配逻辑

    • 遍历待查询的IP地址,逐一与所有城市IP段进行匹配,找到最佳匹配的城市。
  5. 输出结果

    • 将匹配结果以逗号分隔的形式输出。

代码逐行讲解

const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;
  • 使用 readline 模块创建接口,用于从标准输入读取数据。
  • iter 是一个异步迭代器,用于逐行读取输入。
  • readline 是一个异步函数,每次调用会返回输入的一行。

void (async function () {
  // 城市IP列表
  const cities = (await readline()).split(";");
  // 带查询的IP列表
  const queryIps = (await readline()).split(",");
  • 使用 await readline() 读取输入的第一行(城市IP列表),并按 ; 分割成数组。
  • 使用 await readline() 读取输入的第二行(待查询的IP列表),并按 , 分割成数组。

  // IP地址转整型
  function ip2dec(ip) {
    let res = 0;

    const blocks = ip.split(".");
    for (let block of blocks) {
      res = parseInt(block) | (res << 8);
    }

    return res;
  }
  • ip2dec 函数将IPv4地址转换为32位整型数值。
    • 将IP地址按 . 分割成4个部分(例如 1.1.1.1 分割为 ["1", "1", "1", "1"])。
    • 遍历每个部分,将其转换为整数,并通过位运算 (res << 8)| 操作拼接到结果中。
    • 最终返回一个32位整型数值。

  class Range {
    constructor(city, startIpStr, endIpStr) {
      this.city = city;
      // 将IP地址转为整型
      this.startIpDec = ip2dec(startIpStr);
      this.endIpDec = ip2dec(endIpStr);
      this.ipLen = this.endIpDec - this.startIpDec + 1;
    }
  }
  • Range 类用于存储每个城市IP段的信息。
    • city:城市名称。
    • startIpDec:起始IP地址的整型值。
    • endIpDec:结束IP地址的整型值。
    • ipLen:IP段的长度(结束IP - 起始IP + 1)。

  const ranges = [];

  for (let s of cities) {
    const [city, startIpStr, endIpStr] = s.split(/[=,]/);
    ranges.push(new Range(city, startIpStr, endIpStr));
  }
  • 遍历城市IP列表 cities,将每个IP段解析为 Range 对象,并存入 ranges 数组。
    • 使用正则表达式 /[=,]/ 分割字符串,提取城市名称、起始IP和结束IP。
    • 创建 Range 对象并添加到 ranges 数组中。

  const ans = [];

  // 遍历待查询的IP地址
  for (let ip of queryIps) {
    const ipDec = ip2dec(ip);

    // 记录该目标IP地址的最佳匹配城市
    let city = "";
    // 记录最佳匹配城市IP段的长度
    let minLen = Infinity;

    // 将带查询IP与城市IP段列表逐一匹配
    for (let range of ranges) {
      // 如果带查询的IP地址 在某城市的IP段范围内,且该城市的IP段长度更小,则该城市为待查询IP的最佳匹配城市
      if (
        ipDec >= range.startIpDec &&
        ipDec <= range.endIpDec &&
        minLen > range.ipLen
      ) {
        city = range.city;
        minLen = range.ipLen;
      }
    }

    ans.push(city);
  }
  • 遍历待查询的IP列表 queryIps,对每个IP地址进行匹配:
    • 将IP地址转换为整型值 ipDec
    • 初始化 city 为空字符串,minLen 为无穷大(Infinity)。
    • 遍历 ranges 数组,检查当前IP地址是否在某个IP段范围内:
      • 如果满足 ipDec >= range.startIpDec && ipDec <= range.endIpDec,则说明IP地址在该IP段范围内。
      • 如果当前IP段的长度 range.ipLen 小于 minLen,则更新 cityminLen
    • 将最佳匹配的城市名称 city 添加到 ans 数组中。

  console.log(ans.join(","));
})();
  • 将匹配结果数组 ans 用逗号连接成字符串,并输出。

代码逻辑总结

  1. 输入处理

    • 读取城市IP列表和待查询的IP列表,并解析为数组。
  2. IP地址转换

    • 将IPv4地址转换为32位整型数值,方便后续比较。
  3. IP段存储

    • 使用 Range 类存储每个城市IP段的信息。
  4. 匹配逻辑

    • 遍历待查询的IP地址,逐一与所有城市IP段进行匹配,找到范围最小且包含该IP的IP段。
  5. 输出结果

    • 将匹配结果以逗号分隔的形式输出。

示例运行

输入:
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.1.1.15 匹配 City1=1.1.1.11,1.1.1.16
  • 3.3.3.5 匹配 City2=3.3.3.3,4.4.4.4
  • 2.2.2.3 匹配 City3=2.2.2.2,6.6.6.6

通过以上注释和讲解,你应该能够清晰地理解代码的每一部分及其实现逻辑。

三、Java算法源码

以下是代码的详细注释和讲解,帮助你理解每一部分的功能和实现逻辑:


代码结构

  1. Range类

    • 用于存储每个城市IP段的信息,包括城市名称、起始IP的整型值、结束IP的整型值以及IP段的长度。
  2. 主函数

    • 读取输入数据并解析。
    • 遍历待查询的IP地址,逐一与所有城市IP段进行匹配,找到最佳匹配的城市。
    • 输出匹配结果。
  3. IP地址转整型

    • 实现 ip2dec 方法,将IPv4地址转换为64位整型数值。

代码逐行讲解

import java.util.ArrayList;
import java.util.Scanner;
import java.util.StringJoiner;
  • 导入所需的Java工具类:
    • ArrayList:用于动态存储城市IP段信息。
    • Scanner:用于读取输入数据。
    • StringJoiner:用于将匹配结果以逗号分隔的形式输出。

public class Main {
  static class Range {
    String city;
    long startIpDec;
    long endIpDec;
    long ipLen;

    public Range(String city, String startIpStr, String endIpStr) {
      this.city = city;
      // 将IP地址转为整型
      this.startIpDec = ip2dec(startIpStr);
      this.endIpDec = ip2dec(endIpStr);
      this.ipLen = this.endIpDec - this.startIpDec + 1;
    }
  }
  • Range类
    • city:城市名称。
    • startIpDec:起始IP地址的整型值。
    • endIpDec:结束IP地址的整型值。
    • ipLen:IP段的长度(结束IP - 起始IP + 1)。
    • 构造函数接收城市名称、起始IP和结束IP,并将IP地址转换为整型值。

  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);

    ArrayList<Range> ranges = new ArrayList<>();

    // 城市IP列表
    String[] cities = sc.nextLine().split(";");
    // 带查询的IP列表
    String[] queryIps = sc.nextLine().split(",");
  • 主函数
    • 创建 Scanner 对象用于读取输入。
    • 创建 ArrayList<Range> 用于存储城市IP段信息。
    • 读取第一行输入(城市IP列表),并按 ; 分割成数组。
    • 读取第二行输入(待查询的IP列表),并按 , 分割成数组。

    // 提取各个城市IP列表信息
    for (String city : cities) {
      String[] tmp = city.split("[=,]");
      ranges.add(new Range(tmp[0], tmp[1], tmp[2]));
    }
  • 遍历城市IP列表 cities,将每个IP段解析为 Range 对象,并存入 ranges 列表。
    • 使用正则表达式 [=,] 分割字符串,提取城市名称、起始IP和结束IP。
    • 创建 Range 对象并添加到 ranges 列表中。

    StringJoiner sj = new StringJoiner(",");

    // 遍历待查询的IP地址
    for (String ip : queryIps) {
      long ipDec = ip2dec(ip);

      // 记录该目标IP地址的最佳匹配城市
      String city = "";
      // 记录最佳匹配城市IP段的长度
      long minLen = Long.MAX_VALUE;

      // 将带查询IP与城市IP段列表逐一匹配
      for (Range range : ranges) {
        // 如果带查询的IP地址 在某城市的IP段范围内,且该城市的IP段长度更小,则该城市为待查询IP的最佳匹配城市
        if (ipDec >= range.startIpDec && ipDec <= range.endIpDec && minLen > range.ipLen) {
          city = range.city;
          minLen = range.ipLen;
        }
      }

      sj.add(city);
    }

    System.out.println(sj);
  }
  • 匹配逻辑
    • 创建 StringJoiner 对象 sj,用于存储匹配结果。
    • 遍历待查询的IP列表 queryIps,对每个IP地址进行匹配:
      • 将IP地址转换为整型值 ipDec
      • 初始化 city 为空字符串,minLenLong.MAX_VALUE
      • 遍历 ranges 列表,检查当前IP地址是否在某个IP段范围内:
        • 如果满足 ipDec >= range.startIpDec && ipDec <= range.endIpDec,则说明IP地址在该IP段范围内。
        • 如果当前IP段的长度 range.ipLen 小于 minLen,则更新 cityminLen
      • 将最佳匹配的城市名称 city 添加到 sj 中。
    • 输出匹配结果。

  // IP地址转整型
  public static long ip2dec(String ip) {
    long res = 0;

    String[] blocks = ip.split("\\.");

    for (String block : blocks) {
      res = (Integer.parseInt(block)) | (res << 8);
    }

    return res;
  }
  • IP地址转整型
    • 将IPv4地址转换为64位整型数值。
    • 将IP地址按 . 分割成4个部分(例如 1.1.1.1 分割为 ["1", "1", "1", "1"])。
    • 遍历每个部分,将其转换为整数,并通过位运算 (res << 8)| 操作拼接到结果中。
    • 最终返回一个64位整型数值。

代码逻辑总结

  1. 输入处理

    • 读取城市IP列表和待查询的IP列表,并解析为数组。
  2. IP段存储

    • 使用 Range 类存储每个城市IP段的信息。
  3. 匹配逻辑

    • 遍历待查询的IP地址,逐一与所有城市IP段进行匹配,找到范围最小且包含该IP的IP段。
  4. 输出结果

    • 将匹配结果以逗号分隔的形式输出。

示例运行

输入:
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.1.1.15 匹配 City1=1.1.1.11,1.1.1.16
  • 3.3.3.5 匹配 City2=3.3.3.3,4.4.4.4
  • 2.2.2.3 匹配 City3=2.2.2.2,6.6.6.6

通过以上注释和讲解,你应该能够清晰地理解代码的每一部分及其实现逻辑。

四、Python算法源码

以下是代码的详细注释和讲解,帮助你理解每一部分的功能和实现逻辑:


代码结构

  1. IP地址转整型

    • 实现 ip2dec 函数,将IPv4地址转换为32位整型数值。
  2. Range类

    • 用于存储每个城市IP段的信息,包括城市名称、起始IP的整型值、结束IP的整型值以及IP段的长度。
  3. 输入处理

    • 读取城市IP列表和待查询的IP列表,并解析为数组。
  4. 匹配逻辑

    • 遍历待查询的IP地址,逐一与所有城市IP段进行匹配,找到最佳匹配的城市。
  5. 输出结果

    • 将匹配结果以逗号分隔的形式输出。

代码逐行讲解

import re
import sys
  • 导入所需的Python模块:
    • re:用于正则表达式操作。
    • sys:用于获取系统相关的值(如 sys.maxsize)。

# IP地址转整型
def ip2dec(ipStr):
    res = 0

    blocks = ipStr.split(".")
    for block in blocks:
        res = int(block) | (res << 8)

    return res
  • ip2dec 函数
    • 将IPv4地址转换为32位整型数值。
    • 将IP地址按 . 分割成4个部分(例如 1.1.1.1 分割为 ["1", "1", "1", "1"])。
    • 遍历每个部分,将其转换为整数,并通过位运算 (res << 8)| 操作拼接到结果中。
    • 最终返回一个32位整型数值。

class Range:
    def __init__(self, city, startIpStr, endIpStr):
        self.city = city
        # 将IP地址转为整型
        self.startIp = ip2dec(startIpStr)
        self.endIp = ip2dec(endIpStr)
        self.ipLen = self.endIp - self.startIp + 1
  • Range类
    • city:城市名称。
    • startIp:起始IP地址的整型值。
    • endIp:结束IP地址的整型值。
    • ipLen:IP段的长度(结束IP - 起始IP + 1)。
    • 构造函数接收城市名称、起始IP和结束IP,并将IP地址转换为整型值。

# 输入获取
cities = input().split(";")  # 城市IP列表
queryIps = input().split(",")  # 带查询的IP列表
  • 读取输入数据并解析:
    • 使用 input() 读取第一行输入(城市IP列表),并按 ; 分割成数组。
    • 使用 input() 读取第二行输入(待查询的IP列表),并按 , 分割成数组。

# 核心代码
ranges = []

# 提取各个城市IP列表信息
for s in cities:
    # * 用于函数参数自动解构
    ranges.append(Range(*(re.split(r"[=,]", s))))
  • 遍历城市IP列表 cities,将每个IP段解析为 Range 对象,并存入 ranges 列表。
    • 使用正则表达式 r"[=,]" 分割字符串,提取城市名称、起始IP和结束IP。
    • 使用 * 解构参数,创建 Range 对象并添加到 ranges 列表中。

ans = []

# 遍历待查询的IP地址
for ip in queryIps:
    ipDec = ip2dec(ip)

    # 记录该目标IP地址的最佳匹配城市
    best_city = ""
    # 记录最佳匹配城市IP段的长度
    minLen = sys.maxsize

    # 将带查询IP与城市IP段列表逐一匹配
    for ran in ranges:
        # 如果带查询的IP地址 在某城市的IP段范围内,且该城市的IP段长度更小,则该城市为待查询IP的最佳匹配城市
        if ran.endIp >= ipDec >= ran.startIp and minLen > ran.ipLen:
            best_city = ran.city
            minLen = ran.ipLen

    ans.append(best_city)
  • 匹配逻辑
    • 创建空列表 ans,用于存储匹配结果。
    • 遍历待查询的IP列表 queryIps,对每个IP地址进行匹配:
      • 将IP地址转换为整型值 ipDec
      • 初始化 best_city 为空字符串,minLensys.maxsize(表示最大整数值)。
      • 遍历 ranges 列表,检查当前IP地址是否在某个IP段范围内:
        • 如果满足 ran.endIp >= ipDec >= ran.startIp,则说明IP地址在该IP段范围内。
        • 如果当前IP段的长度 ran.ipLen 小于 minLen,则更新 best_cityminLen
      • 将最佳匹配的城市名称 best_city 添加到 ans 列表中。

print(",".join(ans))
  • 将匹配结果列表 ans 用逗号连接成字符串,并输出。

代码逻辑总结

  1. 输入处理

    • 读取城市IP列表和待查询的IP列表,并解析为数组。
  2. IP段存储

    • 使用 Range 类存储每个城市IP段的信息。
  3. 匹配逻辑

    • 遍历待查询的IP地址,逐一与所有城市IP段进行匹配,找到范围最小且包含该IP的IP段。
  4. 输出结果

    • 将匹配结果以逗号分隔的形式输出。

示例运行

输入:
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.1.1.15 匹配 City1=1.1.1.11,1.1.1.16
  • 3.3.3.5 匹配 City2=3.3.3.3,4.4.4.4
  • 2.2.2.3 匹配 City3=2.2.2.2,6.6.6.6

通过以上注释和讲解,你应该能够清晰地理解代码的每一部分及其实现逻辑。

五、C/C++算法源码:

以下是 C语言C++ 版本的代码实现,并附带详细的中文注释和讲解。


C语言版本

代码结构

  1. Range结构体

    • 用于存储每个城市IP段的信息,包括城市名称、起始IP的整型值、结束IP的整型值以及IP段的长度。
  2. IP地址转整型

    • 实现 ip2dec 函数,将IPv4地址转换为32位整型数值。
  3. 输入处理

    • 读取城市IP列表和待查询的IP列表,并解析为数组。
  4. 匹配逻辑

    • 遍历待查询的IP地址,逐一与所有城市IP段进行匹配,找到最佳匹配的城市。
  5. 输出结果

    • 将匹配结果以逗号分隔的形式输出。

代码逐行讲解

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>

#define CITY_NAME_LENGTH 100
#define CITY_LIST_LENGTH 500000
#define QUERY_LIST_LENGTH 700000

typedef struct Range {
    char city[CITY_NAME_LENGTH];
    long startIpDec;
    long endIpDec;
    long ipLen;
} Range;
  • Range结构体
    • city:城市名称。
    • startIpDec:起始IP地址的整型值。
    • endIpDec:结束IP地址的整型值。
    • ipLen:IP段的长度(结束IP - 起始IP + 1)。

// IP地址转整型
long ip2dec(char *ip) {
    long res = 0;

    int n1, n2, n3, n4;
    sscanf(ip, "%d.%d.%d.%d", &n1, &n2, &n3, &n4);

    res = n1 | (res << 8);
    res = n2 | (res << 8);
    res = n3 | (res << 8);
    res = n4 | (res << 8);

    return res;
}
  • ip2dec 函数
    • 将IPv4地址转换为32位整型数值。
    • 使用 sscanf 将IP地址按 . 分割成4个部分。
    • 通过位运算 (res << 8)| 操作拼接到结果中。
    • 最终返回一个32位整型数值。

Range *new_Range(char *city, char *startIpStr, char *endIpStr) {
    Range *range = (Range *) malloc(sizeof(Range));
    strcpy(range->city, city);
    range->startIpDec = ip2dec(startIpStr);
    range->endIpDec = ip2dec(endIpStr);
    range->ipLen = range->endIpDec - range->startIpDec + 1;
    return range;
}
  • new_Range 函数
    • 创建一个 Range 结构体对象。
    • 将城市名称、起始IP和结束IP转换为整型值,并计算IP段长度。
    • 返回创建的 Range 对象。

// 第一行输入
char s1[CITY_LIST_LENGTH];

// 第二行输入
char s2[QUERY_LIST_LENGTH];

Range *ranges[CITY_LIST_LENGTH];
int ranges_size = 0;
  • 定义全局变量:
    • s1:存储第一行输入(城市IP列表)。
    • s2:存储第二行输入(待查询的IP列表)。
    • ranges:存储所有城市IP段信息。
    • ranges_size:记录当前 ranges 的大小。

int main() {
    gets(s1);

    char *token1 = strtok(s1, ";");
    while (token1 != NULL) {
        // 提取各个城市IP列表信息
        char city[CITY_NAME_LENGTH] = {'\0'};
        char startIpStr[10] = {'\0'};
        char endIpStr[10] = {'\0'};

        sscanf(token1, "%[^=]=%[^,],%[^,]", city, startIpStr, endIpStr);
        ranges[ranges_size++] = new_Range(city, startIpStr, endIpStr);

        token1 = strtok(NULL, ";");
    }
  • 输入处理
    • 使用 gets 读取第一行输入(城市IP列表)。
    • 使用 strtok; 分割字符串,提取每个城市IP段。
    • 使用 sscanf 提取城市名称、起始IP和结束IP。
    • 创建 Range 对象并存入 ranges 数组。

    gets(s2);

    // 遍历待查询的IP地址
    char *token2 = strtok(s2, ",");
    while (token2 != NULL) {
        long ipDec = ip2dec(token2);

        // 记录该目标IP地址的最佳匹配城市
        char *city = "";
        // 记录最佳匹配城市IP段的长度
        long minLen = LONG_MAX;

        // 将带查询IP与城市IP段列表逐一匹配
        for (int i = 0; i < ranges_size; i++) {
            // 如果带查询的IP地址 在某城市的IP段范围内,且该城市的IP段长度更小,则该城市为待查询IP的最佳匹配城市
            if (ipDec >= ranges[i]->startIpDec && ipDec <= ranges[i]->endIpDec && minLen > ranges[i]->ipLen) {
                city = ranges[i]->city;
                minLen = ranges[i]->ipLen;
            }
        }

        printf("%s", city);

        token2 = strtok(NULL, ",");

        if(token2 != NULL) {
            printf(",");
        }
    }

    return 0;
}
  • 匹配逻辑
    • 使用 gets 读取第二行输入(待查询的IP列表)。
    • 使用 strtok, 分割字符串,提取每个待查询的IP地址。
    • 将IP地址转换为整型值 ipDec
    • 遍历 ranges 数组,找到范围最小且包含该IP的IP段。
    • 输出最佳匹配的城市名称。

C++版本

代码结构

  1. Range类

    • 用于存储每个城市IP段的信息。
  2. IP地址转整型

    • 实现 ip2dec 函数,将IPv4地址转换为32位整型数值。
  3. 输入处理

    • 读取城市IP列表和待查询的IP列表,并解析为数组。
  4. 匹配逻辑

    • 遍历待查询的IP地址,逐一与所有城市IP段进行匹配,找到最佳匹配的城市。
  5. 输出结果

    • 将匹配结果以逗号分隔的形式输出。

代码逐行讲解

#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <climits>

using namespace std;

class Range {
public:
    string city;
    long startIpDec;
    long endIpDec;
    long ipLen;

    Range(string city, string startIpStr, string endIpStr) {
        this->city = city;
        this->startIpDec = ip2dec(startIpStr);
        this->endIpDec = ip2dec(endIpStr);
        this->ipLen = this->endIpDec - this->startIpDec + 1;
    }

    static long ip2dec(string ip) {
        long res = 0;
        int n1, n2, n3, n4;
        sscanf(ip.c_str(), "%d.%d.%d.%d", &n1, &n2, &n3, &n4);
        res = n1 | (res << 8);
        res = n2 | (res << 8);
        res = n3 | (res << 8);
        res = n4 | (res << 8);
        return res;
    }
};
  • Range类
    • city:城市名称。
    • startIpDec:起始IP地址的整型值。
    • endIpDec:结束IP地址的整型值。
    • ipLen:IP段的长度(结束IP - 起始IP + 1)。
    • ip2dec 函数:将IPv4地址转换为32位整型数值。

int main() {
    string s1, s2;
    getline(cin, s1);  // 读取城市IP列表
    getline(cin, s2);  // 读取待查询的IP列表

    vector<Range> ranges;
    stringstream ss1(s1);
    string token1;
    while (getline(ss1, token1, ';')) {
        string city, startIpStr, endIpStr;
        size_t pos1 = token1.find('=');
        size_t pos2 = token1.find(',');
        city = token1.substr(0, pos1);
        startIpStr = token1.substr(pos1 + 1, pos2 - pos1 - 1);
        endIpStr = token1.substr(pos2 + 1);
        ranges.emplace_back(city, startIpStr, endIpStr);
    }
  • 输入处理
    • 使用 getline 读取两行输入。
    • 使用 stringstreamgetline; 分割字符串,提取每个城市IP段。
    • 使用 substr 提取城市名称、起始IP和结束IP。
    • 创建 Range 对象并存入 ranges 向量。

    stringstream ss2(s2);
    string token2;
    bool first = true;
    while (getline(ss2, token2, ',')) {
        long ipDec = Range::ip2dec(token2);

        string city;
        long minLen = LONG_MAX;

        for (auto &range : ranges) {
            if (ipDec >= range.startIpDec && ipDec <= range.endIpDec && minLen > range.ipLen) {
                city = range.city;
                minLen = range.ipLen;
            }
        }

        if (!first) {
            cout << ",";
        }
        cout << city;
        first = false;
    }

    return 0;
}
  • 匹配逻辑
    • 使用 stringstreamgetline, 分割字符串,提取每个待查询的IP地址。
    • 将IP地址转换为整型值 ipDec
    • 遍历 ranges 向量,找到范围最小且包含该IP的IP段。
    • 输出最佳匹配的城市名称。

总结

  • C语言C++ 版本的实现逻辑基本相同。
  • C++ 版本使用了 stringvector 等STL容器,代码更简洁。
  • C语言 版本使用了 strtoksscanf 进行字符串处理,适合对性能要求较高的场景。

希望以上注释和讲解对你有帮助!

六、尾言

什么是华为OD?

华为OD(Outsourcing Developer,外包开发工程师)是华为针对软件开发工程师岗位的一种招聘形式,主要包括笔试、技术面试以及综合面试等环节。尤其在笔试部分,算法题的机试至关重要。

为什么刷题很重要?

  1. 机试是进入技术面的第一关:
    华为OD机试(常被称为机考)主要考察算法和编程能力。只有通过机试,才能进入后续的技术面试环节。

  2. 技术面试需要手撕代码:
    技术一面和二面通常会涉及现场编写代码或算法题。面试官会注重考察候选人的思路清晰度、代码规范性以及解决问题的能力。因此提前刷题、多练习是通过面试的重要保障。

  3. 入职后的可信考试:
    入职华为后,还需要通过“可信考试”。可信考试分为三个等级:

    • 入门级:主要考察基础算法与编程能力。
    • 工作级:更贴近实际业务需求,可能涉及复杂的算法或与工作内容相关的场景题目。
    • 专业级:最高等级,考察深层次的算法以及优化能力,与薪资直接挂钩。

刷题策略与说明:

2024年8月14日之后,华为OD机试的题库转为 E卷,由往年题库(D卷、A卷、B卷、C卷)和全新题目组成。刷题时可以参考以下策略:

  1. 关注历年真题:

    • 题库中的旧题占比较大,建议优先刷历年的A卷、B卷、C卷、D卷题目。
    • 对于每道题目,建议深度理解其解题思路、代码实现,以及相关算法的适用场景。
  2. 适应新题目:

    • E卷中包含全新题目,需要掌握全面的算法知识和一定的灵活应对能力。
    • 建议关注新的刷题平台或交流群,获取最新题目的解析和动态。
  3. 掌握常见算法:
    华为OD考试通常涉及以下算法和数据结构:

    • 排序算法(快速排序、归并排序等)
    • 动态规划(背包问题、最长公共子序列等)
    • 贪心算法
    • 栈、队列、链表的操作
    • 图论(最短路径、最小生成树等)
    • 滑动窗口、双指针算法
  4. 保持编程规范:

    • 注重代码的可读性和注释的清晰度。
    • 熟练使用常见编程语言,如C++、Java、Python等。

如何获取资源?

  1. 官方参考:

    • 华为招聘官网或相关的招聘平台会有一些参考信息。
    • 华为OD的相关公众号可能也会发布相关的刷题资料或学习资源。
  2. 加入刷题社区:

    • 找到可信的刷题交流群,与其他备考的小伙伴交流经验。
    • 关注知名的刷题网站,如LeetCode、牛客网等,这些平台上有许多华为OD的历年真题和解析。
  3. 寻找系统性的教程:

    • 学习一本经典的算法书籍,例如《算法导论》《剑指Offer》《编程之美》等。
    • 完成系统的学习课程,例如数据结构与算法的在线课程。

积极心态与持续努力:

刷题的过程可能会比较枯燥,但它能够显著提升编程能力和算法思维。无论是为了通过华为OD的招聘考试,还是为了未来的职业发展,这些积累都会成为重要的财富。

考试注意细节

  1. 本地编写代码

    • 在本地 IDE(如 VS Code、PyCharm 等)上编写、保存和调试代码,确保逻辑正确后再复制粘贴到考试页面。这样可以减少语法错误,提高代码准确性。
  2. 调整心态,保持冷静

    • 遇到提示不足或实现不确定的问题时,不必慌张,可以采用更简单或更有把握的方法替代,确保思路清晰。
  3. 输入输出完整性

    • 注意训练和考试时都需要编写完整的输入输出代码,尤其是和题目示例保持一致。完成代码后务必及时调试,确保功能符合要求。
  4. 快捷键使用

    • 删除行可用 Ctrl+D,复制、粘贴和撤销分别为 Ctrl+CCtrl+VCtrl+Z,这些可以正常使用。
    • 避免使用 Ctrl+S,以免触发浏览器的保存功能。
  5. 浏览器要求

    • 使用最新版的 Google Chrome 浏览器完成考试,确保摄像头开启并正常工作。考试期间不要切换到其他网站,以免影响考试成绩。
  6. 交卷相关

    • 答题前,务必仔细查看题目示例,避免遗漏要求。
    • 每完成一道题后,点击【保存并调试】按钮,多次保存和调试是允许的,系统会记录得分最高的一次结果。完成所有题目后,点击【提交本题型】按钮。
    • 确保在考试结束前提交试卷,避免因未保存或调试失误而丢分。
  7. 时间和分数安排

    • 总时间:150 分钟;总分:400 分。
    • 试卷结构:2 道一星难度题(每题 100 分),1 道二星难度题(200 分)。及格分为 150 分。合理分配时间,优先完成自己擅长的题目。
  8. 考试环境准备

    • 考试前请备好草稿纸和笔。考试中尽量避免离开座位,确保监控画面正常。
    • 如需上厕所,请提前规划好时间以减少中途离开监控的可能性。
  9. 技术问题处理

    • 如果考试中遇到断电、断网、死机等技术问题,可以关闭浏览器并重新打开试卷链接继续作答。
    • 出现其他问题,请第一时间联系 HR 或监考人员进行反馈。

祝你考试顺利,取得理想成绩!

;