1. 题目解析与需求拆解
LeetCode 967题要求我们生成所有满足特定条件的n位数字:这些数字的每一位与相邻位之间的绝对差必须等于给定的k值,并且数字不能有前导零(除了数字0本身)。这是一个典型的数字构造问题,需要我们在给定的约束条件下系统地生成所有可能的数字组合。
题目给出了几个关键约束条件:
- 数字长度n的范围是2到9
- 差值k的范围是0到9
- 不允许出现前导零(即01、001等无效,但单独的0有效)
理解这些约束条件对解题至关重要。例如当k=0时,意味着所有相邻数字必须相同,因此生成的数字会是111、222这样的重复数字。而当k较大时(如k=7),可能数字组合会大幅减少,因为相邻数字的差值必须恰好为7。
2. 解题思路分析与算法选择
2.1 问题分解
这个问题可以分解为两个主要部分:
- 数字的构建:需要从第一位开始,逐步构建满足条件的数字
- 相邻数字差值的控制:确保每一位新添加的数字与前一位的绝对差等于k
2.2 算法选择
这类生成所有可能组合的问题,通常有两种主流解法:
- 迭代法(BFS思路):从初始数字开始,逐步扩展每一位
- 递归法(DFS思路):通过递归调用构建完整的数字
两种方法各有优劣:
- 迭代法更容易理解和实现,空间复杂度相对可控
- 递归法代码更简洁,但可能有栈溢出的风险(虽然本题n≤9不会出现)
从实际执行效率来看,由于n的范围很小(最大9),两种方法在时间复杂度上差异不大,都是O(2^n)级别(每个数字位最多有2种选择)。
3. 迭代法实现详解
3.1 算法步骤
迭代法的核心思路是:
- 初始化:从1-9开始(避免前导零)
- 对于每一位:
- 取出当前所有数字的最后一位
- 计算可能的下一位数字(当前位±k)
- 筛选有效的数字(0-9范围内)
- 构建新的数字
- 重复直到数字长度达到n
3.2 代码实现与注释
cpp复制class Solution {
public:
vector<int> numsSameConsecDiff(int n, int k) {
vector<int> res{1, 2, 3, 4, 5, 6, 7, 8, 9}; // 初始化1-9
for (int i = 1; i < n; ++i) { // 已经有一位,还需要n-1位
vector<int> cur; // 临时存储当前轮次的结果
for (int num : res) {
int digit = num % 10; // 获取最后一位数字
if (digit + k <= 9) // 检查digit+k是否有效
cur.push_back(num * 10 + digit + k);
if (k != 0 && digit - k >= 0) // 避免重复且digit-k有效
cur.push_back(num * 10 + digit - k);
}
res = cur; // 更新结果
}
return res;
}
};
3.3 关键点解析
- 初始种子选择:从1-9开始,避免了前导零问题
- 数字扩展逻辑:
num % 10获取最后一位数字num * 10 + newDigit实现数字的扩展
- 边界处理:
k != 0的判断避免了当k=0时产生重复结果digit ± k的范围检查确保数字有效
4. 递归法实现详解
4.1 算法思路
递归法采用深度优先搜索(DFS)策略:
- 从1-9每个数字开始构建
- 对于当前数字:
- 如果长度已达n,加入结果集
- 否则,计算可能的下一位数字(当前最后一位±k)
- 递归构建更长的数字
4.2 代码实现与注释
cpp复制class Solution {
public:
vector<int> numsSameConsecDiff(int n, int k) {
vector<int> res;
for (int i = 1; i <= 9; ++i) { // 从1-9开始
helper(i, n - 1, k, res); // 已经有一位,还需n-1位
}
return res;
}
void helper(int num, int n, int k, vector<int>& res) {
if (n == 0) { // 数字长度已达要求
res.push_back(num);
return;
}
int digit = num % 10; // 获取最后一位
if (digit + k <= 9) // 尝试增加digit+k
helper(num * 10 + digit + k, n - 1, k, res);
if (k != 0 && digit - k >= 0) // 尝试增加digit-k
helper(num * 10 + digit - k, n - 1, k, res);
}
};
4.2 递归与迭代的性能对比
虽然两种方法的时间复杂度相同,但在实际运行中:
- 迭代法通常更快,因为没有函数调用开销
- 递归法代码更简洁,但可能使用更多栈空间
- 在LeetCode环境中,两种方法通常都能在0ms完成(最大n=9)
5. 边界条件与特殊案例处理
5.1 特殊k值处理
-
k=0的情况:
- 所有相邻数字必须相同
- 生成的数字如111,222,...,999
- 此时digit+k == digit-k,需要避免重复添加
-
k≥5的情况:
- 可能的数字组合大幅减少
- 例如n=3,k=7时只有[181,292,707,818,929]
5.2 n的边界值
-
n=2:
- 最简单的非平凡情况
- 如n=2,k=1→[10,12,21,23,...,98]
-
n=9:
- 最大长度,但实际组合数可能不多(取决于k)
- 需要确保算法不会因为递归深度或内存消耗过大
5.3 前导零处理
题目明确禁止前导零,这在两种算法中都得到了处理:
- 初始种子只有1-9
- 后续数字通过±k生成,不会产生前导零
6. 算法优化与变种思考
6.1 性能优化方向
虽然当前算法已经足够高效,但还可以考虑:
- 预计算所有可能的数字对(digit, digit±k)
- 并行处理不同的初始数字
- 使用位运算替代部分算术操作
6.2 相关问题扩展
-
统计满足条件的数字个数而非生成所有数字
- 可以使用动态规划方法
- dp[i][d]表示长度为i,最后一位是d的数字个数
-
允许前导零的情况
- 需要修改初始条件
- 但要注意单独的0和多个前导零的区别
-
差值不为固定k的情况
- 如差值在某个范围内
- 需要调整数字生成逻辑
7. 实际编码中的注意事项
-
数字溢出问题:
- 虽然n≤9不会导致int溢出
- 但在更一般的情况下需要考虑大数处理
-
结果去重:
- 当k=0时,digit+k和digit-k相同
- 需要k!=0的判断避免重复
-
容器选择:
- vector在已知最大数量时可以先reserve
- 但本题n≤9时数量级不大,影响不明显
-
测试用例设计:
- 应该包含k=0、k=9等边界值
- 不同n值的组合
- 检查结果是否包含非法数字
8. 复杂度分析与数学背景
8.1 时间复杂度
最坏情况下,每个数字位有2种选择(+k和-k都有效):
- 总时间复杂度O(2^n)
- 但由于n≤9,实际最大为2^9=512次操作
8.2 空间复杂度
主要空间消耗在于存储结果:
- 最坏情况下结果数量也是O(2^n)
- 递归法还有O(n)的调用栈空间
8.3 组合数学视角
这个问题可以建模为:
- 构建长度为n的数字串
- 每个位置的选择受前一个位置限制
- 本质上是在构建有约束的数字序列
9. 不同语言的实现差异
虽然我们以C++为例,但算法思想可以应用于其他语言:
9.1 Python实现特点
python复制def numsSameConsecDiff(n, k):
res = range(1, 10)
for _ in range(n - 1):
cur = []
for num in res:
digit = num % 10
if digit + k < 10:
cur.append(num * 10 + digit + k)
if k > 0 and digit - k >= 0:
cur.append(num * 10 + digit - k)
res = cur
return res
Python需要注意:
- 整数除法与取模的行为
- 列表操作的性能特点
9.2 Java实现特点
java复制class Solution {
public int[] numsSameConsecDiff(int n, int k) {
List<Integer> res = new ArrayList<>();
for (int i = 1; i <= 9; ++i)
helper(i, n - 1, k, res);
return res.stream().mapToInt(i -> i).toArray();
}
void helper(int num, int n, int k, List<Integer> res) {
if (n == 0) {
res.add(num);
return;
}
int digit = num % 10;
if (digit + k <= 9)
helper(num * 10 + digit + k, n - 1, k, res);
if (k != 0 && digit - k >= 0)
helper(num * 10 + digit - k, n - 1, k, res);
}
}
Java需要注意:
- 集合类型的选择
- 基本类型与包装类型的转换
10. 刷题策略与学习建议
10.1 如何高效刷题
-
理解题目是第一要务
- 仔细阅读题目描述
- 手动计算几个示例
- 确认理解所有约束条件
-
从简单案例入手
- 先解决n=2,k=1等简单情况
- 逐步增加复杂度
-
多种解法对比
- 实现迭代和递归两种解法
- 比较它们的优缺点
10.2 常见错误与调试技巧
-
前导零处理不当
- 确保不会生成01这样的数字
- 特别检查n=2,k=0的情况
-
数字生成逻辑错误
- 检查digit±k的范围判断
- 验证数字拼接是否正确
-
结果遗漏或重复
- 检查k=0时的处理
- 确保所有可能路径都被覆盖
10.3 相关题目推荐
-
类似数字生成问题:
-
- Letter Combinations of a Phone Number
-
- Generate Parentheses
-
-
数字性质相关问题:
-
- Add Digits
-
- Happy Number
-
-
更复杂的约束条件:
-
- Numbers With Repeated Digits
-
- Numbers At Most N Given Digit Set
-
