整数的基础知识
整数是一种基础的数据类型。编程语言可能会提供占据不同内存空间的整数类型,每种类型能表示的整数的范围也不相同。例如,Java 中有 4 种不同的整数类型,分别为 8 位的 byte、16 位的 short、32 位的 int 和 64 位的 long
Java 中的整数类型都是有符号整数,即如果整数的二进制表示的最高位为 0 则表示其为正数,如果整数的二进制表示的最高位 1 则表示其为负数。有些语言(如 C/C++)支持无符号整数。无符号整数无论二进制表示的最高位是 0 还是 1,都表示其为一个正数
通常,编程语言中的整数运算都遵循四则运算规则,可以使用任意嵌套的小括号。需要注意的是,由于整数的范围限制,如果计算结果超出了范围就会产生溢出。产生溢出时运行不会出错,但结果可能会出乎意料。如果除数为 0,那么整数的除法在运行时将报错
面试题 1:整数除法
题目:输入 2 个 int 型整数,它们进行除法计算并返回商,要求不得使用乘号 ‘*’、除号 ‘/’ 及求余符号 ‘%’。当发生溢出时,返回最大的整数值。假设除数不为 0。例如,输入 15 和 2,输出 15 / 2 的结果,即 7
public static int divide(int dividend, int divisor) {
if (dividend == 0x80000000 && divisor == -1) {
return Integer.MAX_VALUE;
}
int negative = 2;
if (dividend > 0) {
negative--;
dividend = -dividend;
}
if (divisor > 0) {
negative--;
divisor = -divisor;
}
int result = divideCore(dividend, divisor);
return negative == 1 ? -result : result;
}
private static int divideCore(int dividend, int divisor) {
int result = 0;
while (dividend <= divisor) {
int value = divisor;
int quotient = 1;
while (value >= 0xc0000000 && dividend <= value + value) {
quotient += quotient;
value += value;
}
result += quotient;
dividend -= value;
}
return result;
}
上述代码中的 0x80000000 为最小的 int 型整数,即 -2^31,0xc0000000 是它的一半,即 -2^30
函数 divideCore 使用减法实现两个负数的除法。当除数和被除数中有一个负数时,商为负数。因此,在使用函数 divideCore 计算商之后,需要再根据除数和被除数的负数的个数调整商的正负号
二进制
整数在计算机中是以二进制的形式表示的。二进制是指数字的每位都是 0 或 1。例如,十进制形式的 2 转化为二进制形式之后是 10……
位运算是把数字用二进制形式表示之后,对每位上 0 或 1 的运算。二进制及其位运算是现代计算机学科的基石,很多底层的技术都离不开位运算,因此与位运算相关的题目也经常出现在面试中。由于人们在日常生活中习惯使用十进制的形式,因此二进制及位运算让很多人难以适应
其实二进制的位运算并不是很难掌握,因为位运算只有 6 种:非、与、或、异或、左移和右移。非运算对整数的二进制按位取反,0 取反得 1,1 取反得 0。下面对 8 位整数进行非运算:
~00001010=11110101
~10001010=01110101
……
但右移时处理最左边位的情形比较复杂。如果数字是一个无符号数值,则用 0 填补最左边的 n 位。如果数字是一个有符号的数值,则用数字的符号位填补最左边的 n 位……下面是对 8 位有符号数值(Java 中的 byte 型整数)进行右移的例子:
00001010>>2=00000010
10001010>>3=11110001
Java 中增加了一种无符号右移位操作符 “>>>”。无论是对正数还是负数进行无符号右移操作,都将在最左边插入 0。下面是对 Java 中 byte 型正数进行无符号右移操作的例子:
00001010>>>2=00000010
10001010>>>3=00010001
其他编程语言(如 C 或 C++)中没有无符号右移位操作符
面试题 2:二进制加法
题目:输入两个表示二进制的字符串,请计算它们的和,并以二进制字符串的形式输出。例如,输入的二进制字符串分别是 “11” 和 “10”,则输出 “101”
public static 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();
}
上述代码中的加法是从字符串的右端开始的,最低位保存在 result 的最左边,而通常数字最左边保存的是最高位,因此,函数 addBinary 在返回之前要将 result 进行翻转
面试题 3:前 n 个数字二进制形式中 1 的个数
题目:输入一个非负数 n,请计算 0 到 n 之间每个数字的二进制形式中 1 的个数,并输出一个数组。例如,输入的 n 为 4,由于 0、1、2、3、4 的二进制形式中 1 的个数分别为 0、1、1、2、1,因此输出数组 [0, 1, 1, 2, 1]
public static 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;
}
上述代码用 “i>>1” 计算 “i/2”,用 “i&1” 计算 “i%2”,这是因为位运算比除法运算和求余运算更高效。这个题目是关于位运算的,因此应该尽量运用位运算优化代码,以展示对位运算相关知识的理解
这种解法的时间复杂度是 O(n)
面试题 4:只出现一次的数字
题目:输入一个整数数组,数组中只有一个数字出现了一次,而其他数字都出现了 3 次。请找出那个只出现一次的数字。例如,如果输入的数组为 [0, 1, 0, 1, 0, 1, 100],则只出现一次的数字是 100
public static 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;
}
}
int result = 0;
for (int i = 0; i < 32; i++) {
result = (result << 1) + bitSums[i] % 3;
}
return result;
}
Java 的 int 型整数有 32 位,因此上述代码创建了一个长度为 32 的数组 bitSums,其中 “bitSums[i]” 用来保存数组 nums 中所有整数的二进制形式中第 i 个数位之和
代码 “(num>>(31-i)) & 1” 用来得到整数 num 的二进制形式中从左数起第 i 个数位。整数 i 先被右移 31-i 位,原来从左数起第 i 个数位右移之后位于最右边。接下来与 1 做位与运算。整数 1 除了最右边一位是 1,其余数位都是 0,它与任何一个数字做位与运算的结果都是保留数字的最右边一位,其他数位都被清零。如果整数 num 从左数起第 i 个数位是 1,那么 “(num>>(31-i)) & 1” 的最终结果就是 1;否则最终结果为 0
下面求 8 位二进制整数 01101100 从左数起的第 2 个(从 0 开始计数)数位。我们先将 01101100 右移 5 位(7-2=5)得到 00000011,再将它和 00000001 做位与运算,结果为 00000001,即 1。8 位二进制整数 01101100 从左边数起的第 2 个数位的确是 1
面试题 5:单词长度的最大乘积
题目:输入一个字符串数组 words,请计算不包含相同字符的两个字符串 words[i] 和 words[j] 的长度乘积的最大值。如果所有字符串都包含至少一个相同字符,那么返回 0。假设字符串中只包含英文小写字母。例如,输入的字符串数组 words 为 [”abcw”, “foo”, “bar”, “fxyz”, “abcdef”],数组中的字符串 “bar” 与 “foo” 没有相同的字符,它们长度的乘积为 9。”abcw” 与 “fxyz” 也没有相同的字符,它们长度的乘积为 16,这是该数组不包含相同字符的一对字符串的长度乘积的最大值
public static 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');
}
}
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;
}
上述代码中的整数 “flags[i]” 用来记录字符串 “words[i]” 中出现的字符。如果 “words[i]” 中出现了某个字符 ch,则对应的整数 “flags[i]” 中从右数起第 ch-’a’ 个数位将被标记为 1
如果两个整数 “flags[i]” 和 “flags[j]” 的与运算的结果为 0,那么它们对应的字符串 “words[i]” 和 “words[j]” 一定没有相同的字符。此时可以计算它们长度的乘积,并与其他不含相同字符的字符串对的长度乘积相比较,最终得到长度乘积的最大值
如果数组 words 的长度为 n,平均每个字符串的长度为 k,那么这种解法的时间复杂度是 O(nk+n^2),空间复杂度是 O(n)
本章小结
本章讨论了最基本的数据类型——整数。编程语言(如 Java)可能定义了多种占据不同内存空间的整数类型,内存空间不同的整数类型的值的范围也不相同
整数在计算机中使用二进制形式表示,每位不是 0 就是 1。位运算是对二进制整数的运算,包括与运算、或运算、非运算、异或运算、左移运算和右移运算。只有深刻理解每种位运算的特点才能在需要的时候灵活地应用合适的位运算解决相应的问题