1. 题目解析:理解"范围内总波动值"问题
这道题目要求我们计算给定数字区间内所有整数的"波动值"总和。所谓波动值,是指一个数字中满足特定条件的中间位数量。具体来说:
- 一个数字的波动值定义为其中间位中同时满足"峰值"或"谷值"条件的位数总和
- 峰值条件:当前位数字严格大于其左右相邻位数字
- 谷值条件:当前位数字严格小于其左右相邻位数字
举个例子,数字1324的波动值计算如下:
- 第二位3:比较1-3-2 → 3>1且3>2 → 满足峰值条件 → 波动值+1
- 第三位2:比较3-2-4 → 2<3且2<4 → 不满足任何条件(不是严格小于)
最终波动值为1
注意:只有数字位数≥3时才可能有波动值,因为需要至少一个中间位。对于1位数或2位数,波动值直接为0。
2. 暴力枚举解法详解
2.1 整体算法思路
最直观的解法就是暴力枚举区间内的每个数字,分别计算其波动值后累加。这种方法的优势是思路简单直接,适合作为第一解题思路。
算法流程:
- 初始化总波动值ret=0
- 遍历区间[num1, num2]中的每个整数i:
- 调用islegal(i)计算i的波动值
- 将结果累加到ret中
- 返回最终的总波动值ret
时间复杂度分析:
- 外层循环:O(M),其中M=num2-num1+1(区间内数字个数)
- 内层islegal函数:O(L),L为数字的位数
- 总复杂度:O(M*L)
2.2 波动值计算实现细节
islegal函数的实现有几个关键点需要注意:
java复制public int islegal(int n) {
// 转换为字符串便于处理各位数字
String s = String.valueOf(n);
// 位数不足3直接返回0
if(s.length() < 3) return 0;
// 将各位数字转为整数数组
int[] nums = new int[s.length()];
for(int i = 0; i < s.length(); i++) {
nums[i] = s.charAt(i) - '0';
}
int ret = 0;
// 遍历所有中间位(索引1到length-2)
for(int i = 1; i < s.length() - 1; i++) {
int cur = nums[i], left = nums[i-1], right = nums[i+1];
// 峰值判断
if(cur > left && cur > right) ret++;
// 谷值判断(注意要用else if避免重复计数)
else if(cur < left && cur < right) ret++;
}
return ret;
}
关键细节:使用else if连接峰值和谷值的判断,避免同一个位被同时计为峰值和谷值(虽然数学上不可能,但代码逻辑上需要明确)
2.3 边界情况处理
在实际编码中,需要特别注意以下边界情况:
- 数字位数不足3位:直接返回0
- 数字包含前导零:如0123,但题目说明输入是正整数,所以可以忽略
- 相邻数字相等的情况:根据题目要求,必须严格大于或小于,所以等于的情况不计入波动值
- 大数区间:当num1=1,num2=10^6时,暴力法仍可在合理时间内完成(现代计算机约10^6次基本操作)
3. 算法优化思路
虽然暴力法能够通过题目测试,但对于更大的数字范围可能效率不足。我们可以考虑以下优化方向:
3.1 数学性质分析
观察波动值的定义,可以发现:
- 波动值只与数字的各位相对大小有关,与绝对值无关
- 可以尝试找出数字模式(pattern)与波动值的关系,建立数学公式
3.2 数位DP解法
更高效的解法是使用数位动态规划(Digit DP),其核心思想是将问题分解为数字各位的处理,通过记忆化减少重复计算。
数位DP解法的基本框架:
- 将数字转换为字符数组或字符串
- 设计DP状态表示:
- pos:当前处理到的位数
- tight:前面是否紧贴上限
- prev:前一位数字
- prevPrev:前两位数字
- count:当前已累积的波动值
- 递归处理每一位,根据当前位与前面两位的关系更新波动值
这种解法可以将时间复杂度优化到O(L*10^3),其中L是数字的最大位数。
3.3 预处理与缓存
对于多次查询的情况,可以预处理一定范围内的结果,或者对已计算过的数字进行缓存。例如:
java复制// 使用HashMap缓存已计算的结果
private Map<Integer, Integer> cache = new HashMap<>();
public int islegal(int n) {
if(cache.containsKey(n)) {
return cache.get(n);
}
// 正常计算流程...
cache.put(n, result);
return result;
}
4. 完整Java代码实现
以下是包含优化处理的完整代码实现:
java复制class Solution {
public int totalWaviness(int num1, int num2) {
int ret = 0;
for(int i = num1; i <= num2; i++) {
ret += calculateWaviness(i);
}
return ret;
}
private int calculateWaviness(int n) {
// 优化:先处理明显无波动值的情况
if(n < 100) return 0;
// 转换为字符数组更高效
char[] digits = String.valueOf(n).toCharArray();
int len = digits.length;
if(len < 3) return 0;
int count = 0;
for(int i = 1; i < len - 1; i++) {
int curr = digits[i] - '0';
int prev = digits[i-1] - '0';
int next = digits[i+1] - '0';
// 峰值判断
if(curr > prev && curr > next) {
count++;
}
// 谷值判断
else if(curr < prev && curr < next) {
count++;
}
}
return count;
}
}
代码优化点:
- 添加了n<100的快速返回路径
- 使用char[]代替String操作,略微提升效率
- 将islegal重命名为更具表达力的calculateWaviness
- 移除了不必要的数组创建,直接使用字符计算
5. 测试用例与调试技巧
5.1 典型测试用例
验证算法正确性时,应包含以下测试场景:
-
小数字区间:
- 输入:num1=100, num2=105
- 预期:100(0), 101(1), 102(1), 103(1), 104(1), 105(0) → 总和4
-
包含多种波动情况:
- 输入:num1=131, num2=135
- 131(1), 132(1), 133(0), 134(1), 135(0) → 总和3
-
边界情况:
- 输入:num1=1, num2=99 → 总和0(所有数字位数不足)
- 输入:num1=100, num2=100 → 只计算100的波动值
5.2 调试技巧
当实现出现问题时,可以采用以下调试方法:
- 打印中间结果:
java复制System.out.println("Processing "+i+": waviness="+calculateWaviness(i));
- 单元测试特定数字:
java复制assert calculateWaviness(131) == 1;
assert calculateWaviness(133) == 0;
- 可视化数字模式:
code复制数字:1 3 1
位置:0 1 2
中间位1(3): 比较1-3-1 → 峰值
6. 同类问题扩展
掌握这个问题的解法后,可以尝试解决以下类似问题:
- 统计区间内"单调递增"数字的数量(每位数字≥前一位)
- 计算数字的"旋转对称"性质(如69, 88, 96)
- 统计具有交替奇偶位数字的数量(如奇偶奇偶...)
这些问题都可以使用类似的数位分析技术解决,区别仅在于判断条件的改变。
7. 实际应用场景
虽然这类问题看似理论化,但其核心技能在实际开发中有广泛应用:
- 数据校验:检测身份证号、信用卡号等的有效性(如Luhn算法)
- 数据分析:识别时间序列数据中的波峰波谷
- 密码生成:创建符合特定模式的随机密码
- 金融分析:识别股票价格波动模式
理解数字的位操作技巧,是处理这些实际问题的基础。