华为OD机试高频题:Excel单元格数值统计的递归与记忆化搜索解法
1. 项目概述与核心价值最近在帮几个准备华为OD机试的朋友做模拟练习发现“Excel单元格数值统计”这道题的出现频率相当高。乍一看题目很多人会以为这只是一个简单的字符串解析或者二维数组求和问题但实际动手一写各种边界条件和细节处理就让人头疼不已。这道题之所以能成为机试的常客正是因为它巧妙地融合了字符串处理、递归或栈的应用、数据结构设计以及边界条件判断等多个基础且重要的编程知识点非常考验候选人的基本功和思维严谨性。简单来说这道题模拟了一个简化版的Excel公式计算引擎。你拿到的是一个用字符串表示的二维表格其中某些单元格里不是简单的数字而是像A1B2或SUM(A1:B2)这样的公式。你的任务就是解析这个表格计算出所有单元格的最终数值并统计所有数值单元格即最终值为数字的单元格的总和。题目会明确排除循环引用等复杂情况但公式可能嵌套引用范围可能跨越多行多列。对于正在准备华为OD机试的开发者而言彻底吃透这道题的价值远超题目本身。它不仅能帮你稳稳拿下一道高频题的分数更重要的是其解题思路——递归解析、依赖关系处理、避免重复计算——是解决许多复杂系统设计问题如依赖注入、任务调度、有向无环图计算的微型缩影。无论你主攻C、Java、Python还是JavaScript理解其核心算法并能在代码中清晰实现都是面试官非常看重的“硬通货”。2. 问题深度拆解与核心思路2.1 输入输出格式与约束条件在开始编码前我们必须像解数学题一样先明确已知条件和求解目标。根据常见的题目描述虽然具体参数名可能微调我们可以梳理出以下核心要素输入格式通常是一个字符串数组grid例如[10, A1B2, SUM(A1:A3), 5]。这里grid的索引从0开始隐式定义了单元格的位置。更常见的是给出一个m行n列的描述每个单元格内容为一个字符串。单元格地址规则采用经典的Excel A1 引用样式。列号由大写字母A-Z表示A对应第1列Z对应第26列AA对应第27列以此类推。行号由数字表示从1开始。例如B3表示第2列第3行的单元格。公式类型题目通常支持两种运算符简单二元运算格式为单元格地址1单元格地址2支持,-,*,/。例如A1B2。SUM求和函数格式为SUM(左上角单元格:右下角单元格)。例如SUM(A1:B2)表示求单元格 A1, A2, B1, B2 的和。关键约束这是解题的安全边界无循环引用题目保证公式之间不会形成环状依赖A依赖BB又依赖A。这意味着所有单元格的依赖关系构成一个有向无环图我们可以用递归或拓扑排序安全地求解。公式结果保证为整数虽然过程中可能有除法但题目保证最终所有单元格的值都是整数。这简化了我们的数据类型选择可以用整型。引用始终有效公式中引用的单元格地址一定在表格范围内。输出一个整数即所有数值单元格的数值之和。数值单元格指的是其内容本身就是一个数字字符串如10或者经过公式计算后最终得到一个数字的单元格。2.2 核心算法设计递归求值与记忆化搜索理解了问题后最直观的解法就是对每个单元格进行计算。如果一个单元格是数字直接返回其值如果是公式则解析它获取它所依赖的单元格先计算那些单元格的值再进行运算。这个过程天然适合用递归DFS来实现。我们可以设计一个函数calculate(r, c)用于计算位于第r行、第c列的单元格的最终值。递归函数calculate(r, c)的伪代码逻辑检查缓存如果该单元格的值已经被计算过存储在一个memo[r][c]中则直接返回缓存值。这是记忆化搜索的关键避免对同一单元格的重复计算将时间复杂度从指数级降低到线性。解析内容获取grid[r][c]的字符串content。基础情况如果content不以开头说明它是纯数字直接将其转换为整数存入缓存并返回。递归情况如果content以开头说明它是公式。解析公式判断是二元运算还是SUM函数。获取操作数对于二元运算A1B2解析出两个单元格地址递归调用calculate获取它们的值val1和val2然后执行相应的算术运算。对于SUM函数SUM(A1:B2)解析出左上角(r1, c1)和右下角(r2, c2)坐标遍历这个矩形区域内的每一个单元格递归调用calculate获取其值并累加。存储与返回将计算结果存入memo[r][c]然后返回该值。为什么记忆化搜索如此重要想象一个公式SUM(A1:A100)而A1又是SUM(B1:B100)。如果没有缓存计算A1时会遍历B列100个单元格而后续任何引用A1的公式都会导致这100次计算重复发生。记忆化确保每个单元格只被计算一次极大地提升了效率。其时间复杂度大致为O(m*n)其中m和n是表格的行列数。2.3 关键子问题地址解析与坐标转换算法框架搭好了但里面有两个“螺丝钉”级别的子问题必须解决得漂亮将 Excel 列字母如AA转换为数字索引以及解析SUM(A1:B2)这样的范围描述。1. 列字母转数字索引这本质上是一个26进制数A-Z转10进制的问题但注意没有0A对应1。def col_letter_to_index(letters: str) - int: result 0 for ch in letters: result result * 26 (ord(ch) - ord(A) 1) return result - 1 # 通常我们转换为从0开始的索引例如ABA- 1B- 2。计算过程result 0*26 1 1然后result 1*26 2 28。所以AB对应第28列转换为0起始索引是27。2. 解析单元格地址字符串给定一个如B3的字符串我们需要分离出列字母部分和行数字部分。// Java 示例 public static int[] parseCellAddress(String address) { int i 0; while (i address.length() Character.isLetter(address.charAt(i))) { i; } String colLetters address.substring(0, i); String rowNumStr address.substring(i); int col colLettersToIndex(colLetters); // 调用上面的函数 int row Integer.parseInt(rowNumStr) - 1; // 转换为0起始索引 return new int[]{row, col}; }3. 解析SUM范围对于A1:B2我们需要先按:分割得到A1和B2再分别对它们调用parseCellAddress得到左上角(r1, c1)和右下角(r2, c2)。注意在解析公式字符串时要特别注意字符串的切割和提取。例如A1B2需要先去掉开头的然后根据运算符的位置分割左右操作数。正则表达式是一个强大的工具但对于这道题手动遍历字符串查找运算符位置可能更直观且不易出错。3. 多语言实现详解与代码对比掌握了核心算法我们就可以用代码将其实现。不同语言在字符串处理、数据结构上有其特色但核心逻辑一致。下面我将分别用 C, Java, Python 和 JavaScript 实现并highlight关键点。3.1 C 实现注重效率与明确类型C 实现需要关注内存管理和细节控制。我们使用vectorvectorstring存储表格vectorvectorint存储记忆化结果并用一个特殊值如INT_MIN表示未计算。#include iostream #include vector #include string #include cctype #include climits #include functional #include unordered_map using namespace std; class ExcelCalculator { private: vectorvectorstring grid; vectorvectorint memo; // 记忆化缓存 int rows, cols; // 将列字母如AA转换为0起始的列索引 int colLettersToIndex(const string col) { int index 0; for (char ch : col) { index index * 26 (ch - A 1); } return index - 1; } // 解析单元格地址字符串如B3 - (2, 1) (行列)0起始 pairint, int parseAddress(const string addr) { int i 0; while (i addr.size() isalpha(addr[i])) i; string colStr addr.substr(0, i); string rowStr addr.substr(i); int col colLettersToIndex(colStr); int row stoi(rowStr) - 1; // 行号转0起始 return {row, col}; } // 递归计算单元格(r, c)的值 int calculate(int r, int c) { if (memo[r][c] ! INT_MIN) { return memo[r][c]; } const string content grid[r][c]; int result; // 情况1纯数字 if (content[0] ! ) { result stoi(content); } else { // 情况2公式去掉 string expr content.substr(1); // 判断是否为SUM函数 if (expr.substr(0, 3) SUM) { // 格式SUM(A1:B2) int leftParen expr.find((); int colon expr.find(:); int rightParen expr.find()); string topLeft expr.substr(leftParen 1, colon - leftParen - 1); string bottomRight expr.substr(colon 1, rightParen - colon - 1); auto [r1, c1] parseAddress(topLeft); auto [r2, c2] parseAddress(bottomRight); result 0; // 遍历矩形区域求和 for (int i r1; i r2; i) { for (int j c1; j c2; j) { result calculate(i, j); } } } else { // 情况3二元运算如 A1B2 // 查找运算符位置 int opPos -1; for (int i 1; i expr.size(); i) { // 从1开始因为第一个字符是字母 if (expr[i] || expr[i] - || expr[i] * || expr[i] /) { opPos i; break; } } string leftAddr expr.substr(0, opPos); string rightAddr expr.substr(opPos 1); char op expr[opPos]; auto [lr, lc] parseAddress(leftAddr); auto [rr, rc] parseAddress(rightAddr); int leftVal calculate(lr, lc); int rightVal calculate(rr, rc); switch (op) { case : result leftVal rightVal; break; case -: result leftVal - rightVal; break; case *: result leftVal * rightVal; break; case /: result leftVal / rightVal; break; // 题目保证能整除 } } } memo[r][c] result; return result; } public: int sumOfValues(vectorvectorstring grid) { this-grid grid; rows grid.size(); if (rows 0) return 0; cols grid[0].size(); // 初始化memo用INT_MIN表示未计算 memo.assign(rows, vectorint(cols, INT_MIN)); int totalSum 0; for (int i 0; i rows; i) { for (int j 0; j cols; j) { // 累加所有单元格的最终值 totalSum calculate(i, j); } } return totalSum; } }; // 使用示例 int main() { vectorvectorstring grid { {10, A1B2, 5}, {SUM(A1:A2), 3, C1*2}, {A1, B1-B2, 9} }; ExcelCalculator solver; cout Total sum: solver.sumOfValues(grid) endl; // 需要根据具体表格计算 return 0; }C实现要点使用INT_MIN作为未计算的标记因为有效值可能是负数所以不能用0。字符串解析手动处理查找SUM、括号、冒号和运算符位置逻辑清晰但代码稍长。使用pair或tuple返回坐标使代码更清晰。递归函数是类成员方便访问grid和memo。3.2 Java 实现面向对象与健壮性Java 实现可以利用其丰富的字符串API和集合框架代码结构更清晰。import java.util.*; public class ExcelCalculator { private String[][] grid; private Integer[][] memo; // 使用Integer利用null判断是否已计算 private int rows, cols; public int sumOfValues(String[][] grid) { this.grid grid; this.rows grid.length; this.cols grid[0].length; this.memo new Integer[rows][cols]; int totalSum 0; for (int i 0; i rows; i) { for (int j 0; j cols; j) { totalSum calculate(i, j); } } return totalSum; } private int calculate(int r, int c) { if (memo[r][c] ! null) { return memo[r][c]; } String content grid[r][c]; int result; if (!content.startsWith()) { // 纯数字 result Integer.parseInt(content); } else { String expr content.substring(1); // 去掉 if (expr.startsWith(SUM)) { // 解析 SUM(A1:B2) int leftParen expr.indexOf((); int colon expr.indexOf(:); int rightParen expr.indexOf()); String topLeft expr.substring(leftParen 1, colon); String bottomRight expr.substring(colon 1, rightParen); int[] tl parseAddress(topLeft); int[] br parseAddress(bottomRight); int r1 tl[0], c1 tl[1]; int r2 br[0], c2 br[1]; result 0; for (int i r1; i r2; i) { for (int j c1; j c2; j) { result calculate(i, j); } } } else { // 二元运算 // 查找运算符位置 int opPos -1; for (int i 1; i expr.length(); i) { char ch expr.charAt(i); if (ch || ch - || ch * || ch /) { opPos i; break; } } String leftAddr expr.substring(0, opPos); String rightAddr expr.substring(opPos 1); char op expr.charAt(opPos); int[] leftCoord parseAddress(leftAddr); int[] rightCoord parseAddress(rightAddr); int leftVal calculate(leftCoord[0], leftCoord[1]); int rightVal calculate(rightCoord[0], rightCoord[1]); switch (op) { case : result leftVal rightVal; break; case -: result leftVal - rightVal; break; case *: result leftVal * rightVal; break; case /: result leftVal / rightVal; break; } } } memo[r][c] result; return result; } private int[] parseAddress(String address) { // 分离列字母和行数字 int i 0; while (i address.length() Character.isLetter(address.charAt(i))) { i; } String colPart address.substring(0, i); String rowPart address.substring(i); int col colLettersToIndex(colPart); int row Integer.parseInt(rowPart) - 1; return new int[]{row, col}; } private int colLettersToIndex(String letters) { int index 0; for (int i 0; i letters.length(); i) { char ch letters.charAt(i); index index * 26 (ch - A 1); } return index - 1; } public static void main(String[] args) { ExcelCalculator calc new ExcelCalculator(); String[][] grid { {10, A1B2, 5}, {SUM(A1:A2), 3, C1*2}, {A1, B1-B2, 9} }; System.out.println(Total sum: calc.sumOfValues(grid)); } }Java实现要点使用Integer[][]作为记忆化数组null值天然表示未计算比用特殊值更安全。充分利用String类方法如startsWith(),indexOf(),substring()使字符串解析代码更简洁。健壮性Java的强类型和清晰的异常处理这里为简洁未写全有助于构建更健壮的程序。3.3 Python 实现简洁高效与快速原型Python 以其极致的简洁性著称非常适合快速实现算法原型和面试手写。class ExcelCalculator: def __init__(self): self.memo None self.grid None def col_letters_to_index(self, letters: str) - int: 将Excel列字母如AB转换为0起始的索引 index 0 for ch in letters: index index * 26 (ord(ch) - ord(A) 1) return index - 1 def parse_address(self, address: str) - tuple: 解析单元格地址返回行列元组0起始 i 0 while i len(address) and address[i].isalpha(): i 1 col_str address[:i] row_str address[i:] col self.col_letters_to_index(col_str) row int(row_str) - 1 return row, col def calculate(self, r: int, c: int) - int: 递归计算单元格值带记忆化 if self.memo[r][c] is not None: return self.memo[r][c] content self.grid[r][c] if not content.startswith(): # 纯数字 result int(content) else: expr content[1:] # 去掉 if expr.startswith(SUM): # 处理 SUM(A1:B2) # 找到括号和冒号的位置 left_paren expr.find(() colon expr.find(:) right_paren expr.find()) top_left expr[left_paren1:colon] bottom_right expr[colon1:right_paren] r1, c1 self.parse_address(top_left) r2, c2 self.parse_address(bottom_right) result 0 # 遍历矩形区域求和 for i in range(r1, r2 1): for j in range(c1, c2 1): result self.calculate(i, j) else: # 处理二元运算 A1B2 # 查找运算符位置 op_pos -1 for i in range(1, len(expr)): if expr[i] in -*/: op_pos i break left_addr expr[:op_pos] right_addr expr[op_pos1:] op expr[op_pos] lr, lc self.parse_address(left_addr) rr, rc self.parse_address(right_addr) left_val self.calculate(lr, lc) right_val self.calculate(rr, rc) if op : result left_val right_val elif op -: result left_val - right_val elif op *: result left_val * right_val elif op /: result left_val // right_val # 题目保证整除使用整数除法 self.memo[r][c] result return result def sum_of_values(self, grid: list[list[str]]) - int: 主函数计算所有单元格数值之和 self.grid grid rows, cols len(grid), len(grid[0]) # 初始化记忆化数组None表示未计算 self.memo [[None for _ in range(cols)] for _ in range(rows)] total_sum 0 for i in range(rows): for j in range(cols): total_sum self.calculate(i, j) return total_sum # 使用示例 if __name__ __main__: solver ExcelCalculator() test_grid [ [10, A1B2, 5], [SUM(A1:A2), 3, C1*2], [A1, B1-B2, 9] ] print(fTotal sum: {solver.sum_of_values(test_grid)})Python实现要点使用None初始化记忆化数组判断is not None即可。字符串切片操作非常高效和直观如expr[left_paren1:colon]。str.find()方法可以方便地定位字符。整数除法使用//确保结果为整数符合题目要求。3.4 JavaScript 实现前端思维与异步潜力JavaScript版本在思路上与其他语言一致但需要注意其基于0的数组索引和字符串处理方式。class ExcelCalculator { constructor() { this.grid null; this.memo null; this.rows 0; this.cols 0; } /** * 将Excel列字母转换为0起始的索引 * param {string} letters - 如 AB * returns {number} */ colLettersToIndex(letters) { let index 0; for (let i 0; i letters.length; i) { const ch letters.charCodeAt(i); index index * 26 (ch - A.charCodeAt(0) 1); } return index - 1; } /** * 解析单元格地址字符串 * param {string} address - 如 B3 * returns {[number, number]} - [行, 列]0起始 */ parseAddress(address) { let i 0; // 找到列字母部分的结束位置 while (i address.length /[A-Z]/.test(address[i])) { i; } const colStr address.substring(0, i); const rowStr address.substring(i); const col this.colLettersToIndex(colStr); const row parseInt(rowStr, 10) - 1; // 转换为0起始 return [row, col]; } /** * 递归计算单元格值带记忆化 * param {number} r - 行索引 * param {number} c - 列索引 * returns {number} */ calculate(r, c) { if (this.memo[r][c] ! undefined) { return this.memo[r][c]; } const content this.grid[r][c]; let result; if (!content.startsWith()) { // 纯数字 result parseInt(content, 10); } else { const expr content.substring(1); // 去掉 if (expr.startsWith(SUM)) { // 解析 SUM(A1:B2) const leftParen expr.indexOf((); const colon expr.indexOf(:); const rightParen expr.indexOf()); const topLeft expr.substring(leftParen 1, colon); const bottomRight expr.substring(colon 1, rightParen); const [r1, c1] this.parseAddress(topLeft); const [r2, c2] this.parseAddress(bottomRight); result 0; // 遍历矩形区域 for (let i r1; i r2; i) { for (let j c1; j c2; j) { result this.calculate(i, j); } } } else { // 二元运算如 A1B2 // 查找运算符位置 let opPos -1; for (let i 1; i expr.length; i) { if (-*/.includes(expr[i])) { opPos i; break; } } const leftAddr expr.substring(0, opPos); const rightAddr expr.substring(opPos 1); const op expr[opPos]; const [lr, lc] this.parseAddress(leftAddr); const [rr, rc] this.parseAddress(rightAddr); const leftVal this.calculate(lr, lc); const rightVal this.calculate(rr, rc); switch (op) { case : result leftVal rightVal; break; case -: result leftVal - rightVal; break; case *: result leftVal * rightVal; break; case /: result Math.floor(leftVal / rightVal); break; // 整数除法 } } } this.memo[r][c] result; return result; } /** * 主函数计算所有单元格数值之和 * param {string[][]} grid - 二维字符串数组表示的表格 * returns {number} */ sumOfValues(grid) { this.grid grid; this.rows grid.length; this.cols grid[0].length; // 初始化记忆化数组用undefined表示未计算 this.memo Array.from({ length: this.rows }, () new Array(this.cols)); let totalSum 0; for (let i 0; i this.rows; i) { for (let j 0; j this.cols; j) { totalSum this.calculate(i, j); } } return totalSum; } } // 使用示例 const solver new ExcelCalculator(); const testGrid [ [10, A1B2, 5], [SUM(A1:A2), 3, C1*2], [A1, B1-B2, 9] ]; console.log(Total sum: ${solver.sumOfValues(testGrid)});JavaScript实现要点使用undefined作为未计算的标记memo数组初始化为undefined。字符串方法startsWith(),substring(),indexOf()与Java类似。正则表达式简单判断/[A-Z]/.test(address[i])用于判断字符是否为大写字母。整数除法使用Math.floor()确保结果为整数因为JavaScript只有浮点数除法。4. 常见陷阱、调试技巧与性能优化4.1 高频易错点排查清单在实际编码和调试中以下几个点是出错的重灾区索引转换错误这是最经典的错误。题目中的行号、列号通常是1起始的A1而我们的数组是0起始的。忘记在解析后对行号减1或者列字母转数字后忘记减1都会导致数组越界或引用错误单元格。务必在parseAddress函数中仔细检查row parseInt(rowStr) - 1和col colLettersToIndex(letters) - 1。字符串解析不完整解析SUM(A1:B2)时只提取了A1:B2但忘记处理两边的括号。或者解析二元运算时没有正确找到运算符的位置运算符可能在字符串中间不是第一个字符。建议在写解析逻辑后立刻用几个极端用例测试如A1单单元格引用虽然题目可能不出现、SUM(A1:A1)、A10B2两位数行号。整数除法处理题目保证整除但编程语言中/运算符的行为不同。在Python中/是浮点除法要用//在JavaScript中/是浮点除法要用Math.floor()在Java和C中两个整数相除/是整数除法。必须使用整数除法否则可能导致结果错误或类型问题。记忆化缓存初始化与判断缓存初始化值必须是一个不可能出现在计算结果中的值如C的INT_MINPython/JS的None/undefined。判断时要用!或!进行严格比较。在递归函数开头先查缓存再计算的逻辑不能错。SUM范围遍历边界for (int i r1; i r2; i)这里的很容易错写成导致右下角单元格没有被包含进去。画个简单的坐标图就能避免这个错误。4.2 调试与测试策略面对一个复杂的递归程序如何有效调试构造最小测试用例不要一上来就用复杂表格。从最简单的开始[[5]]- 结果应为5。[[A1], [10]]- 这是一个2x1的表格A1(0,0)引用B1(1,0)的值10。检查依赖关系是否正确。[[SUM(A1:A2)], [1], [2]]- 测试SUM函数。[[A2B1], [3], [5]]- 测试交叉引用。打印递归路径在calculate函数入口添加一行日志如printf(Calculating (%d, %d): %s\n, r, c, grid[r][c]);。这能让你清晰看到递归的调用顺序快速发现死循环或重复计算。可视化记忆化缓存在计算完成后打印出整个memo数组。检查每个单元格的值是否符合预期。这能帮你发现哪些单元格的计算逻辑有误。单元测试函数为colLettersToIndex和parseAddress这两个纯函数编写独立的测试。确保A-0,Z-25,AA-26,B3-(2,1) 等转换绝对正确。4.3 性能优化与进阶思考虽然记忆化搜索已经将复杂度优化到 O(m*n)但在极端情况下如表格非常大或公式嵌套极深递归调用栈可能溢出。我们可以从两个角度思考优化迭代拓扑排序BFS既然依赖关系是无环的我们可以先构建一个图表示每个单元格依赖于哪些其他单元格出边以及被哪些单元格依赖入边即入度。然后进行拓扑排序将所有入度为0的单元格即纯数字单元格放入队列并计算其值。接着遍历依赖它的单元格将其入度减1如果减到0则放入队列。这个过程迭代进行直到所有单元格计算完毕。这种方法完全避免了递归适用于超大规模数据。惰性求值与缓存优化我们的当前实现是“需求驱动”的惰性求值用到时才算。但如果题目要求多次查询不同单元格当前的全局遍历求和方式可能不是最高效的。可以考虑将calculate函数改造为真正的惰性求值并维护一个全局的“脏”标记当某个单元格更新时标记其所有依赖项为“脏”下次查询时重新计算。这更接近真实Excel的行为。一个重要的实操心得在机试或面试中如果时间允许在写完递归记忆化版本后可以向面试官简要提及拓扑排序的思路这能展示你对问题更深层次的理解和知识迁移能力。你可以说“当前基于递归和记忆化的解法在题目约束下是高效且正确的。如果考虑到递归深度限制或更复杂的依赖更新场景还可以用拓扑排序Kahn算法进行迭代求解进一步优化空间和应对动态更新。” 这绝对是加分项。5. 从解题到举一反三核心思维模式这道“Excel单元格数值统计”题其价值远不止于解一道题。它训练的是一种将复杂规则系统分解为可计算单元并处理单元间依赖关系的通用思维模式。这种模式在软件开发中无处不在。构建系统/编译器就像解析Excel公式一样编译器中需要解析表达式、建立抽象语法树AST、并按照依赖关系求值。任务调度与工作流引擎任务之间存在依赖A任务完成才能开始B任务需要检测环循环依赖并确定执行顺序拓扑排序。电子表格、低代码平台、规则引擎这些都是允许用户自定义公式或规则然后由系统在后台解析和计算的典型应用。动态规划与缓存设计记忆化搜索本质上是动态规划的一种形式自顶向下带备忘录。这道题是理解DP重叠子问题和最优子结构的绝佳热身。当你下次遇到诸如“计算有依赖关系的配置项”、“解析自定义公式的报表系统”、“实现一个简单的规则引擎”这类问题时不妨回想一下这道题如何定义计算单元如何解析单元间的引用关系如何避免重复计算如何保证计算顺序你会发现解决问题的内核是相通的。最后在真正的华为OD机试或类似编程考试中除了正确性代码的整洁度、可读性和健壮性同样重要。确保你的函数有清晰的命名如parseAddress,calculate关键步骤有简要的注释并且处理了可能的异常输入尽管题目保证有效但防御性编程是好习惯。将字符串解析、地址转换等逻辑封装成独立函数会让主逻辑清晰很多。祝你在机试中遇到这道题时能从容、清晰地写出满分代码。