LeetCode第8题,字符串转整型。
-
为什么不用一个大循环而选择用分段式的小循环?
合法字符串的格式是分段式的,但问题是,我们不知道实际的字符串的各个段长度为多少(无法定界)。若使用一个大循环轮询处理,需要额外的状态变量与判断语句决定当前状态,代码逻辑会变得更加复杂。
-
如何进行溢出判断?
-
这个问题是写本文的主要动机,我们可以很直接的得到一个可读性很高的、同时也是高赞答案采用的方式:
if(total>Integer.MAX_VALUE/10 || (total==Integer.MAX_VALUE/10 && digit>Integer.MAX_VALUE%10)) return sign == -1 ? Integer.MIN_VALUE : Integer.MAX_VALUE; total = total*10 + digit;
-
这种写法的一个隐藏点是:当输入为“-2147483648”时,按道理是不溢出的,但此处的代码将其视为溢出,然后由于前边读取的负号,返回了不溢出的Integer.MIN_VALUE。
-
那么,除此之外,有没有其它溢出判断的方式呢?
-
笔者开始的想法如下:
int tmp = total; total = total*10 + digit; if(tmp > total) return sign == -1 ? Integer.MIN_VALUE : Integer.MAX_VALUE
-
上述代码的出发点是:溢出时,一方面,total x 10 后的值要减去一个很大的整数;另一方面,有可能 total x 10 会将 Integer 的符号位置1,导致正数变负数。因此,若旧值(tmp)大于新值(total),说明出现溢出。
顺便一提,二进制 x10 等于左移三位加左移一位(x8 + x2)。
-
这乍一看没问题,但测试时,发现仍有一些情况没有考虑到:溢出减值,有可能减去后的新值仍大于旧值;溢出变负,则只是部分情况,因为溢出后仍可能是正值。
举个很简单的例子:定义一个 4bit 的数据类型,2(0010)x 10 后,得到 20(0001,0100),溢出处理后得到 4(0100),4 > 2 > 0。
-
笔者后来又想了种方式:
total = total*10 + digit; if(total%10 != digit) return sign == -1 ? Integer.MIN_VALUE : Integer.MAX_VALUE
-
上述代码的出发点是:没溢出时,total x 10 一定得到一个整十数(也就是个位为0的数),那么加上 digit 再%10,余数一定还是digit。
而若 total x 10 发生了溢出,total 最大值2^31-1,x10 后溢出位只有 2^34,换句话讲,溢出减去的大整数只有—— 2^34, 2^33, 2^32, 2^34 + 2^32——这四种情况,而其对应的个位数分别为——4、2、6、8。此时,若再加上 0~9 的 digit,%10 取个位数必定不是digit。若 total x 10 没有溢出,而加上 digit 后溢出,则同理,由于上限值将 digit 进行了截断,溢出后的个位数也必定不是digit,而是digit的一部分。
-
然而测试时又出现了问题:
-
可以看出,在所有的测试样例中,就只有这一个例子没通过。而对于该例,原本应该触发溢出返回 Integer.MAX_VALUE,但实际却输出 -20,也就是说没有触发溢出,即,我们的溢出判断语句仍有漏网之鱼。
-
问题到底出在哪里呢?看到输出的 负20,笔者好像知道了问题出在哪里,但为了严谨起见,笔者用程序穷举了本题会出现的所有情况进行测试,发现了一系列漏网之鱼:
当 total = [1932735284,2^31-1],total x 10发生溢出,而溢出后皆为负值,且是整十数,故若恰巧digit = 0,溢出判断语句 total%10 != digit 失效。
有意思的是,区间 [1932735284,2^31-1] 共 214748364 个整数,区间每个数 x10 后溢出对应的负整数为 [-2147483640,…,-30,-20,-10],恰为 Interger 能表示的所有负整十数。
-
那么,为什么会这样呢?笔者在仔细分析了一些代表性数据溢出的过程后,得出如下结论:
-
若 total x 10 的低 32 位仍为正数,则 total%10 != digit 可以进行溢出判断,原因如第10条所述;
-
若 total x 10 的低 32 位变为负数,则由于上限值的个位数非0,故在(long)total x 10 减去大整数后,仍有可能得到一个(int)的负整十数,导致溢出判断无效;
-
那么,可以将两种方法相结合,用如下代码进行溢出判断
int tmp = total; total = total*10 + digit; if(total%10 != digit || tmp > total) return sign == -1 ? Integer.MIN_VALUE : Integer.MAX_VALUE;
-
在评论区又找到一种方式,思路是逆运算判等,不过同样有边界情况需要打补丁:total = 214748364,digit = 8:
int tmp = total; total = total*10 + digit; if((total-digit)/10 != tmp || total < 0) return sign == -1 ? Integer.MIN_VALUE : Integer.MAX_VALUE;
-