1. 问题背景与定义解析
今天想和大家分享一道经典的面试编程题——「旋转数字」。这道题看似简单,但考察了候选人对问题分析、边界条件处理以及代码实现细节的把控能力。我在实际面试中多次遇到候选人在这道题上栽跟头,主要原因是没有完全理解题目要求。
1.1 什么是「好数」?
题目定义了一个特殊概念——「好数」。具体来说,一个数字如果满足以下三个条件,就被称为好数:
- 数字的每一位旋转180度后仍是有效数字(即不能包含3、4、7这些旋转后无效的数字)
- 旋转后的数字与原数字不相同(即至少包含2、5、6、9这些旋转后会变化的数字)
- 数字的每一位都必须参与旋转(这个条件通常隐含在前两个条件中)
举个例子,数字"69"旋转180度后变成"69",但因为6和9互相转换,实际上变成了"69"(看起来一样但内部已经变化),所以它满足好数定义。而数字"88"旋转后仍然是"88",不满足第二个条件。
1.2 数字旋转特性详解
理解数字旋转后的变化是解题的关键。我们可以将所有数字分为三类:
| 数字类型 | 包含数字 | 旋转特性 | 对好数的影响 |
|---|---|---|---|
| 无效数字 | 3、4、7 | 旋转后非数字 | 包含则直接排除 |
| 变化数字 | 2、5、6、9 | 旋转后变成其他数字 | 必须至少包含一个 |
| 不变数字 | 0、1、8 | 旋转后仍为自身 | 仅包含这些数字时不是好数 |
注意:数字"6"旋转后变成"9","9"旋转后变成"6","2"和"5"互相转换。这种对称性变化是判断的关键。
2. 解题思路与算法设计
2.1 暴力解法:逐数检查
最直观的解法是对1到N的每个数字进行逐位检查:
- 遍历从1到N的每个数字
- 对每个数字,检查其每一位:
- 如果发现3、4、7中的任意一个,立即排除
- 如果包含2、5、6、9中的至少一个,标记为候选
- 最终统计满足条件的数字数量
这种解法的时间复杂度是O(N*d),其中d是数字的平均位数。对于N≤10000的常规面试题范围,这个复杂度完全可接受。
2.2 优化思路:数位DP
虽然暴力解法在面试中通常足够,但如果N非常大(比如1e9),我们可以考虑使用数位动态规划(Digit DP)来优化。数位DP的核心思想是将数字视为字符串,按位处理并记忆化状态。
不过在实际面试中,除非特别说明N的范围很大,否则建议先实现暴力解法,因为它更直观且易于编码。如果时间允许,再讨论优化方案。
3. 代码实现与细节解析
3.1 完整代码实现
以下是C++的实现代码,包含详细注释:
cpp复制class Solution {
public:
int rotatedDigits(int n) {
int count = 0;
for (int i = 1; i <= n; ++i) {
if (isGoodNumber(i)) {
++count;
}
}
return count;
}
private:
bool isGoodNumber(int num) {
bool hasChangeableDigit = false;
while (num > 0) {
int digit = num % 10;
// 检查无效数字
if (digit == 3 || digit == 4 || digit == 7) {
return false;
}
// 检查变化数字
if (digit == 2 || digit == 5 || digit == 6 || digit == 9) {
hasChangeableDigit = true;
}
num /= 10;
}
return hasChangeableDigit;
}
};
3.2 关键代码解析
-
主函数rotatedDigits:
- 遍历1到n的所有数字
- 对每个数字调用isGoodNumber进行检查
- 统计满足条件的数字数量
-
辅助函数isGoodNumber:
- 使用hasChangeableDigit标记是否包含变化数字
- 通过num%10和num/10逐位检查
- 遇到3、4、7立即返回false
- 遇到2、5、6、9将标记设为true
- 最后返回标记状态
3.3 边界条件处理
在实际编码中,需要特别注意以下边界情况:
- 数字0的处理(题目通常从1开始)
- 单个数字的情况(如2、5、6、9是好数,0、1、8不是)
- 包含多个变化数字的情况(只要有一个就满足条件)
4. 复杂度分析与优化讨论
4.1 时间复杂度分析
对于暴力解法:
- 外层循环执行N次
- 内层循环执行d次(d为数字的位数)
- 总时间复杂度为O(N*d)
对于N=10000,d=5,总操作次数约为50000,完全在合理范围内。
4.2 空间复杂度分析
两种解法都只使用了常数级别的额外空间:
- 几个临时变量(count、digit等)
- 没有使用与N相关的数据结构
- 空间复杂度为O(1)
4.3 进一步优化思路
如果N非常大,可以考虑以下优化:
- 预处理数字:预先计算并存储某些范围内的好数数量
- 数学推导:利用组合数学计算满足条件的数字数量
- 记忆化搜索:缓存中间结果避免重复计算
不过在实际面试中,除非特别要求,否则实现正确且清晰的暴力解法通常已经足够。
5. 常见错误与面试技巧
5.1 常见错误类型
根据我的面试经验,候选人常犯的错误包括:
- 误解题目要求,特别是对"旋转后不同"的理解
- 遗漏边界条件,如单个数字或包含多个变化数字的情况
- 无效数字检查不完整,漏掉3、4、7中的某一个
- 代码效率问题,如不必要的字符串转换
5.2 面试回答技巧
当面试官提出这个问题时,建议采取以下步骤:
- 先明确题目要求和定义,可以举例说明
- 提出暴力解法并分析复杂度
- 讨论可能的优化方向(即使不实现)
- 编写代码时注意变量命名和注释
- 主动测试边界条件
5.3 测试用例设计
好的测试用例应该包含:
plaintext复制测试用例 预期结果 说明
10 4 2,5,6,9
20 9 增加了12,15,16,19
100 40 包含多种情况
1000 316 大规模测试
1 0 最小边界
2 1 单个好数
11 4 包含不变数字
6. 实际应用与变种问题
6.1 实际应用场景
虽然这个问题看起来是纯数学的,但它可以帮助我们:
- 理解数字的对称性和编码规则
- 练习字符串/数字处理的基本功
- 培养严密的逻辑思维能力
6.2 相关变种问题
类似的数字处理问题还有:
- 计算回文数字的数量
- 统计不含某些数字的数字数量
- 寻找满足特定数学性质的数字
- 数字的字母表示(如罗马数字)
6.3 个人经验分享
我在实际面试中注意到,能够快速准确解决这类问题的候选人通常:
- 对数字操作非常熟练
- 能够清晰定义问题边界
- 编写的代码可读性强
- 能够主动讨论优化空间
这道题虽然不难,但很能体现候选人的基本功和思维严谨性。建议在准备面试时,不仅要写出代码,还要能够解释每个决策背后的思考过程。