Hello,大家晚上好,我是猿码。夜深人静宿自宽,不写博客枉疯癫!
今天为大家带来的是一道 super easy 的算法题,由于它不涉及到分治(Divide and Conquer)、动态规划(DP)、递归(Recursive call)以及二分(Binary search)等算法,因此算不上经典,但作为了解 JVM 的 Happend-before 或程序执行流程优化可见一斑,前者可能有些牵强。
📕 题目:加一(Plus one)
📝 题目介绍:
给定一个由 整数 组成的 非空 数组 所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
tip:
1 <= digits.length <= 100
0 <= digits[i] <= 9
来源:力扣(LeetCode)
💡 题目分析
关于两个数字相加的程序模拟需要注意的相关细节,我在前几日的两数之和中有介绍,朋友们可以前往阅读。此处,就不赘述了。但,一个数字加 1 可能需要向高位进 1 位是这类题的核心,稍后我们会通过代码看看如何处理这个过程。
原题中要求以 int 类型的数组返回结果,大家都知道数组是固定大小的数据结构,一旦定义并指定长度,就无法直接改变其长度,区别于可变数组 ArrayList,而 ArrayList 除了有特殊的扩容机制外,只是在数组的基础上使用了 System.arrayCopy 来实现原始数组与扩容之后数组的元素交换。考虑到最高位可能会进 1 会改变原数字的初始长度,进而需要考虑:若在原数组中存储两数之和,必然会与数组的特性相冲突。因此,此处我们需要先创建一个长度为 n + 1 的数组,作为备用,n 为原始数组的长度。这样,“两全” 的情况就可以解决了。
- 999 + 1
- 888 + 1
接着,就是如何不留痕迹地在处理第一种“最高位不进1”的情况的同时,将第二种情况也一并处理掉,那么留在最后的一步,就是解决最高位补 1 的操作了。
🖊 代码实现
public int[] plusOne(int[] digits) {
int n = digits.length, r = 1;
// 若最高位需进 1 的备用数组
int[] nD = new int[n + 1];
// 倒叙遍历,低位算起
for (int j = n - 1; j >= 0; j--) {
// 若任何位满足与 1 相加不超过 9,则直接返回参数 digits
// 注意这里使用的是 +=
if((digits[j] += r) <= 9)
return digits;
else {
// 反之,由于刚刚对 digits[j] 进行判断时已经赋了值,若能跳入这个 if 子句
// 说明 digits[j] 一定为 9,在原数组 i 位置在减去 10,留下余数,
// 同时将原数的 j 位置的值赋值给备用数组 j + 1 位置
nD[j + 1] = (digits[j] -= 10);
// 当然也可以这么写 nD[j + 1] = digits[j] = 0;
}
}
// 如果程序执行到这里,说明 digits 一定是 999
nD[0] = 1;
return nD;
}
时间复杂度:, 为输入数组的长度
空间复杂度:,这里如果细看,其实是 O(n + 1) 的空间复杂度,但大O标记法是标记算法复杂度最坏情形也就是性能上界的标准,因此需要去掉常数项 O(1)。
✂ 代码分析
该算法中,第一个 if 判断条件,大家如果细看,是先对 digits[j] 进行与 1 相加并赋值,在判断其是否超出 9,若满足条件,就直接返回结果,那么下图中代码标记为序号 1 的地方的作用就很明显,或者可以理解为:为 else 子句做铺垫。
序号 3,原代码是 nD[j + 1] = (digits[j] -= 10); 其实可以走下图序号 3 和 4 的写法。但前者,能突出 JVM 中 Happen-before 的执行顺序。Happen-before 可以理解为:某个已执行的指令是另外一个待执行指令的前驱,
int a = 10;
int b = a + 1;
如果前者不执行,后者也不会执行。JVM 在编译这段代码时,不会对其进行重排序。
🎓 总结
- 麻雀虽小,五脏俱全。每一段程序都有其背后的执行逻辑支撑,掌握细节,等于掌握大局
- 代码的优化除了结构就是细节
- 算法是赤裸的数学思想,语言则是它华丽的衣裳
好了,祝大家明日有一个好的开始,谢谢大家的阅读!