关卡名 | 数字与数学基础问题 | 我会了✔️ |
内容 | 1.数字里统计的处理技巧 | ✔️ |
2.必须熟练掌握溢的处理方法 | ✔️ | |
3.掌握进制的处理方法 | ✔️ |
1. 数字统计专题
统计一下特定场景下的符号,或者数字个数等是一类非常常见的问题 。如果按照正常方式强行统计,可能会非常复杂,所以掌握一些技巧是非常必要的。
1.1 符号统计
LeetCode1822 给定一个数组,求所有元素的乘积的符号,如果最终答案是负的返回-1,如果最终答案是正的返回1,如果答案是0返回0。
仔细分析一下这道题目,如果将所有数都乘起来,再判断正负,工作量真不少,而且还可能溢出。我们发现,一个数如果是 -100 和 -1,对符号位的贡献是完全一样的,所以只需要看有多少个负数,就能够判断最后乘积的符号了。
public int arraySign(int[] nums) {
int prod = 1;
for(int i = 0; i < nums.length; ++i) {
if(nums[i] == 0) {
return 0;//一切归零
}else if(nums[i] < 0) {
//交替就够了,很好的处理技巧
prod = -prod;
}
}
return prod;
}
1.2 阶乘0的个数
很多数学相关算法的关键在于找到怎么通过最简洁的方式来解决问题,而不是硬算。例如:面试题16.05:设计一个算法,算出 n 阶乘有多少个尾随零。
这个题如果硬算,一定会超时,其实我们可以统计有多少个 0,实际上是统计 2 和 5 一起出现多少对,不过因为 2 出现的次数一定大于 5 出现的次数,因此我们只需要检查 5 出现的次数就好了,那么在统计过程中,我们只需要统计 5、10、15、 25、 ... 5^n 这样 5 的整数倍项就好了,最后累加起来,就是多少个 0。代码就是:
public int trailingZeroes(int n) {
int cnt = 0;
for (long num = 5; n / num > 0; num *= 5) {
cnt += n / num;
}
return cnt;
}
数学不仅与算法难以区分 ,很多算法问题还与位运算密不可分,有些题目真不好说是该归类到数学中呢,还是位运算中。我们干脆就放在一起来看。
2 溢出问题
溢出问题是一个极其重要的问题,只要涉及到输出一个数字,都可能遇到,典型的题目有三个:数字反转,将字符串转成数字和回文数。 不过溢出问题一般不会单独考察,甚至面试官都不会提醒你,但他就像捕捉猎物一样盯着你,看你会不会想到有溢出的问题。
2.1 整数反转
LeetCode7 给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^31 − 1] ,就返回 0。假设环境不允许存储 64 位整数(有符号或无符号)。
输入:x = 123 输出:321
输入:x = -123 输出:-321
输入:x = 120 输出:21
输入:x = 0 输出:0
这个题的关键有两点,一个是如何进行数字反转,另一个是如何判断溢出。反转好说,那为什么会有溢出问题呢?例如1147483649这个数字,它是小于最大的32位整数2147483647的,但是将这个数字反转过来后就变成了9463847411,这就比最大的32位整数还要大了,这样的数字是没法存到int里面的,所以就溢出了。
首先想一下,怎么去反转一个整数。用栈?或者把整数变成字符串再反转字符串?都可以但都不好。我们只要一边左移一边处理末尾数字就可以了。以12345为例,先拿到5,再拿到4,之后是3,2,1,然后就可以反向拼接出一个数字了。那如何获得末尾数字呢?好办,循环取模运算即可。例如:
1.将12345 % 10 得到5,之后将12345 / 10=1234
2.将1234 % 10 得到4,再将1234 / 10=123
3.将123 % 10 得到3,再将123 / 10=12
4.将12 % 10 得到2,再将12 / 10=1
5.将1 % 10 得到1,再将1 / 10=0
画成图就是:
这样的话,是不是将循环的判断条件设为x>0就可以了呢?不行!因为忽略了负数的问题,应该是while(x!=0)。去掉符号,剩下的数字,无论正数还是负数,按照上面不断的/10这样的操作,最后都会变成0,所以判断终止条件就是!=0。有了取模和除法操作,就可以轻松解决第一个问题,如何反转。
接下来看如何解决溢出的问题。我们知道32位最大整数是MAX=2147483647,如果一个整数num>MAX,那么应该有以下规律:
nums/10 > MAX/10=214748364,也就是如果底数第二位大于4了,不管最后一位是什么都已经溢出了,如下:
所以我们要从到最大数的1/10时,就要开始判断,也即:
- 如果 num>214748364 那后面就不用再判断了,肯定溢出了。
- 如果num= 214748364,这对应到上图中第三、第四、第五排的数字,需要要跟最大数的末尾数字比较,如果这个数字比7还大,说明溢出了。
- 如果num<214748364,则没问题,继续处理。
这个结论对于负数也是一样的,所以实现代码就是:
public int reverse(int x) {
int res = 0;
while(x!=0) {
//获得末尾数字
int tmp = x%10;
//判断是否大于最大32位整数,也可以使用Integer.MAX_VALUE/10来代替214748364
if (res>Integer.MAX/10 || (res==Integer.MAX/10 && tmp>7)) {
return 0;
}
//判断是否小于最小的32位整数
if (res<-214748364 || (res==-214748364 && tmp<-8)) {
return 0;
}
res = res*10 + tmp;
x /= 10;
}
return res;
}
2.2 字符串转整数
LeetCode8.意思就是字符串转整数(atoi函数),题目比较长,解决过程中要涉及很多异常情况的处理,我们在《字符串》一关进行了详细讲解,请回顾一下相关的内容。
2.3 回文数
LeetCode9 .给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
例如,121 是回文,而 123 不是。
思路解析:
映入脑海的第一个想法是将数字转换为字符串,并检查字符串是否为回文。但是,这需要额外的非常量空间来创建问题描述中所不允许的字符串。
第二个想法是将数字本身反转,然后将反转后的数字与原始数字进行比较,如果它们是相同的,那么这个数字就是回文。 但是,如果反转后的数字大于int.MAX,我们将遇到整数溢出问题。
按照第二个想法,为了避免数字反转可能导致的溢出问题,为什么不考虑只反转 int 数字的一半?毕竟,如果该数字是回文,其后半部分反转后应该与原始数字的前半部分相同。
例如,输入 1221,我们可以将数字 “1221” 的后半部分从 “21” 反转为 “12”,并将其与前半部分 “12” 进行比较,因为二者相同,我们得知数字 1221 是回文。
这个反转思路与链表的反转是一样的,不过要简单多了。
这里还不能忘的问题就是,反转之后数字可能会溢出,因此必须做防范,方法我们上面说了,这里我们可以进一步简化一下:
public boolean isPalindrome(int x) {
// 特殊情况:
// 如上所述,当 x < 0 时,x 不是回文数。
// 同样地,如果数字的最后一位是 0,为了使该数字为回文,
// 则其第一位数字也应该是 0
// 只有 0 满足这一属性
if(x<0 || (x%10==0 && x!=0)){
return false;
}
int revertedNumber = 0;
while(x > revertedNumber){
revertedNumber = revertedNumber*10 + x%10;
x /= 10;
}
// 当数字长度为奇数时,我们可以通过 revertedNumber/10 去除处于中位的数字。
// 例如,当输入为 12321 时,在 while 循环的末尾我们可以得到 x = 12,revertedNumber = 123,
// 由于处于中位的数字不影响回文(它总是与自己相等),所以我们可以简单地将其去除。
return x==revertedNumber x==revertedNumber /10;
}
3.进制专题
进制问题也是一个非常重要的专题,有的直接处理还挺费劲,我们看两道题。
3.1 七进制数
LeetCode504.给定一个整数 num,将其转化为 7 进制,并以字符串形式输出。其中-10^7 <= num <= 10^7。
示例1:
输入: num = 100
输出: "202"
我们先通过二进制想一下7进制数的变化特征。在二进制中,先是0,然后是1,而2就是10(2),3就是11(2),4就是(100)。同样在7进制中,计数应该是这样的:
0 1 2 3 4 5 6 10 11 12 13 14 15 16 20 21 22 ...
给定一个整数将其转换成7进制的主要过程是循环取余和整除 ,最后将所有的余数反过来即可。例如,将十进制数100 转成七进制:
100÷7=14 余 2
14÷7=2 余 0
2÷7=0 余 2
向遍历每次的余数,依次是 2、0、2,因此十进制数 100 转成七进制数是202 。如果num<0,则先对 num 取绝对值,然后再转换即可。使用代码同样可以实现该过程,需要注意的是如果单纯按照整数来处理会非常麻烦,既然题目说以字符串形式返回,那我们干脆直接用字符串类,代码如下:
public String convertToBase7(int num) {
StringBuilder sb = new StringBuilder();
//先拿到正负号
boolean sign = num < 0;
//这样预处理一下,后面都按照正数处理num就行了
if(sign)
num *= -1;
//循环取余和整除
do{
sb.append(num%7 + "");
num/=7;
}while(num > 0);
//添加符号
if(sign)
sb.append("-");
//上面的结果是逐个在末尾加的,需要反过来
return sb.reverse().toString();
}
3.2 进制转换
给定一个十进制数M,以及需要转换的进制数N,将十进制数M转化为N进制数。M是32位整数,2<=N<=16。
这个题目的思路不复杂,但是想写正确却很不容易,甚至越写越糊涂。本题有好几个需要处理的问题:
- 1.超过进制最大范围之后如何准确映射到其他进制,特别是ABCDEF这种情况。简单的方式是大量采用if 判断,但是这样会出现写了一坨,最后写不下去。
- 2.需要对结果进行一次转置。
- 3.需要判断负号。
下面这个是我总结出的最精简,最容易理解的实现方案。注意采取三个措施来方便处理:
- 1.定义大小为16的数组F,保存的是2到16的各个进制的值对应的标记,这样赋值时只计算下标,不必考虑不同进制的转换关系了。
- 2.使用StringBuffer完成数组转置等功能,如果不记得这个方法,工作量直接飙升。
- 3.通过一个flag来判断正数还是负数,最后才处理。
// 要考虑到 余数 > 9 的情况,2<=N<=16.
public static final String[] F = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};
//将十进制数M转化为N进制数
public String convert (int M, int N) {
Boolean flag=false;
if(M<0){
flag=true;
M*=-1;
}
StringBuffer sb=new StringBuffer();
int temp;
while(M!=0){
temp=M%N;
//技巧一:通过数组F[]解决了大量繁琐的不同进制之间映射的问题
sb.append(F[temp]);
M=M/N;
}
//技巧二:使用StringBuffer的reverse()方法,让原本麻烦的转置瞬间美好
sb.reverse();
//技巧三:最后处理正负,不要从一开始就揉在一起。
return (flag? "-":"")+sb.toString();
}