整数的基本知识
JAVA/JVM中有四种整数类型,分别是byte short int 和 long,它们在位数、最小值、最大值方面有所区别。如下表。
| 类型 | 位数 | 最小值 | 最大值 |
|---|---|---|---|
| any | n | 100..00 | 011..11 |
| byte | 8 | -2^7 | 2^7-1 |
| short | 16 | -2^15 | 2^15-1 |
| int | 32 | -2^31 | ^2^31-1 |
| long | 64 | -2^63 | 2^63-1 |
对于n位有符号数来说,由于需要最左侧一位来表示符号,负数能使用的bit比正数多一个,负数为1后全0,正数为0后全1。最小值为-2^(n-1),最大值是2^(n-1)-1。
溢出
关于整数会考察的边界条件不多,溢出是一定要考虑到的,不同类型的整数有能够表达的上界。
面试题1:整数除法
给定两个整数 a 和 b ,求它们的除法的商 a/b ,要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%' 。当发生溢出时,返回最大的整数值。假设除数不为0。例如,输入15和2,输出7。
ANSWER
从除法的定义出发,把a用b除,是要找出最多可以有多少个b累加得到a。因此该题目最基础的解法是,不断从a中减去b,直到无法再减为止,记录下此时所减去b的个数,就是它们的商a/b。当a很大而b很小时,循环次数会很多,时间复杂度为O(a)。
上述解法的核心思想在于,从a中不断减去b,是线性的减法,这里可以采用倍数减法,尝试减去1、2、4、8...个b,直到找出最大的个数。
dividend是被除数a,divisor是除数b。
private int divideCore(int dividend, int divisor) {
int result =0;
while (dividend <= divisor) {
int value = divisor; // 用来*2
int quotient = 1; // 当前倍率
while (value >= 0xc0000000 && dividend <= value + value) { // 0xc0000000是-2^30
quotient += quotient;
value += value;
}
result += quotient; // 增加已经减去的除数次数
dividend -= value; // 从被除数里扣除当前除数
}
return result;
}
public int divide( int dividend, int divisor) {
if (dividend == 0x80000000 && divisor == -1) { // 0x80000000就是-2^31
return Integer.MAX_VALUE;
}
int negative = 2;
if (dividend > 0) {
negative--;
dividend *= -1; // 统一归为负数,方便计算,正数会溢出
}
if (divisor > 0) {
negative--;
divisor *= -1;
}
int result = divideCore(dividend, divisor);
return negative == 1 ? -result : result; // 使用negative记录两者符号异同
}
面试题2:二进制加法
给定两个01字符串a和b,请计算它们的和,并以二进制字符串的形式输出。
输入为非空字符串且只包含数字1和0。
二进制基础知识
- 常用操作与(&)或(|)异或(^)
- 左移(<<)右移(>>)都会保留符号
- 无符号右移操作符是>>>
ANSWER
从加法原理进行求解,从右向左逐位相加,逢2进1。
public String addBinary(String a, String b) {
StringBuilder result = new StringBuilder();
int i = a.length() - 1;
int j = b.length() - 1;
int carry = 0;
while (i>=0 || j>=0) { // 把两个字符串全部扫描完成
int digitA = i>=0 ? a.charAt(i--) - '0' : 0;
int digitB = j>=0 ? b.charAt(j--) - '0' : 0;
int sum = digitA + digitB + carry;
carry = sum >= 2 ? 1 : 0;
sum = sum >= 2 ? sum - 2 : sum;
result.append(sum);
}
if (carry == 1) result.append(1); // 不要遗漏最后的进位
return result.reverse().toString(); // append增加到最右侧一位,翻转
}
面试题3:前n个数字二进制形式中1的个数
给定一个非负整数
n,请计算0到n之间的每个数字的二进制表示中 1 的个数,并输出一个数组。
ANSWER
技巧:i & (i-1)可以将正数i最右边的1变成0
方法1:利用上一次计算结果,时间复杂度O(n)
public int[] countBits(int num) {
int[] result = new int[num+1]; // result[0]=0
for (int i=0; i<num; i++) {
int j = i;
while (j!=0) { // 计算j当中1的个数
result[i]++;
j = j & (j-1); // 将j最右侧的1变成0,直至全部为0
}
}
return result;
}
方法2:根据i/2计算,时间复杂度O(n)
- 若i是偶数,它与i/2含有的1个数相同
- 若i是奇数,它比i/2含有的1个数多一个
- 用i>>1计算i/2,用i&1计算i%2
public int[] countBits(int num) {
int[] result = new int[num+1];
for (int i=1; i<=num; i++) {
result[i] = result[i>>1] + i&1;
}
return result;
}
面试题4:只出现一次的数字
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
ANSWER
题目如果问的是“其它数字出现2次,找出那个只出现1次的数字”,则可以用异或的方式求解。本题需要将所有数字按位相加后除以3,未能除尽的位就是目标数字的位。
public int singleNumber(int[] nums) {
int[] bitSums = new int[32];
for (int num : nums) {
for (int i=0; i<32; i++) {
bitSums[i] += (num >> (31-i) & 1; // 从左向右数第i位的值
}
}
int result = 0;
for (int i=0; i<32; i++) {
result = (result << 1) + bitSums[i] % 3;
}
return result
}
面试题5:单词长度的最大乘积
给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。
示例 1: 输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"] 输出: 16 解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。
ANSWER
该题目的关键在于判断两个单词是否有相同的字母,有哈希表和位计算两种方法。
哈希表法,时间复杂度O(nk+n^2),空间复杂度O(n)
public int maxProduct(String[] words) {
boolean[][] flags = new boolean[words.length][26];
for (int i=0; i<words.length; i++) {
for (char c : words[i].toCharArray()) {
flags[i][c-'a'] = true; // c-'a'将char转化为0~25的int
}
}
int result = 0;
for (int i=0; i<words.length; i++) {
for (int j=i+1; j<words.length; j++) { // 从i+1开始遍历,而非从0开始
int k = 0;
for (; k<26; k++) {
if (flags[i][k] && flags[j][k]) {
break; // 这两个词存在相同字母,跳过
}
}
if (k == 26) { // 两个词不存在相同字母
int prod = words[i].length() * words[j].length();
result = Math.max(result, prod);
}
}
}
return result;
}
位运算法,时间复杂度O(nk+n^2),空间复杂度O(n)
核心思想是用bit的0、1代表false、true,这种方法优于前一个解法,因为前一个解法在判断两个单词是否存在相同字母时,需要进行26次布尔运算,而这种方法只需要一次。
public int maxProduct(String[] words) {
int[] flags = new int[words.length];
for (int i=0; i<words.length; i++) {
for (char ch: words[i].toCharArray()) {
flags[i] |= 1 << (ch - 'a'); // flags保存了所有单词内的字母分布
}
}
int result = 0;
for (int i=0; i<words.length(); i++) {
for (int j=i+1; j<words.length(); j++) {
if ((flags[i] & flags[j] == 0) { // 不含相同字母
int prod = words[i].length() * words[j].length();
result = Math.max(result, prod);
}
}
}
return result;
}