剑指Offer第一章 整数 01
第一章 整数
1.1整数的基础知识
整数是一种基本的数据类型。编程语言可能会提供占据不同内存空间的整数类型, 每种类型能表示的整数范围也不同。例如,Java中有4种不同的整数类型,分别为:
byte:(8位)(-2的7次方~2的7次方-1)
short:(16位)(-2的15次方~2的15次方-1)
int:(32位)(-2的31次方~2的31次方-1)
long:(32位)(-2的63次方~2的63次方-1)
Java中的整数类型都是有符号整数,即如果整数的二进制表示的最高位为0则表示其为正数,如果整数的二进制 的最高位为1则表示其为负数。有些语言(如C/C++)支持无符号整数。无符号整数无论二进制表示的最高位是0还是1,都表示其为一个正数。无符号的32位整数的范围是0~2的32次方-1。
通常,变成语言中的整数运算都遵循四则运算规则,可以使用任意嵌套的小括号。需要注意的是,由于整数的范围限制,如果计算机过超出范围则会产生溢出。产生溢出时运行不会出错,但结果计算会出乎意料。如果除数为0,那么整数的除法在运行时将报错。
面试题1:整数除法
题目: 输入2个int型整数,它们进行除法计算并返回商,要求不得使用乘号*、除号/及求余符号%。当发生溢出时,返回最大的整数值。假设除数不为0。例如,输入15和2,输出15/2的结果,即7。
分析: 这个题目限制我们不能使用称号和除号进行运算。一个直观的解法是基于减法实现除法。例如, 为了求得15/2的商,可以不断地从15里减去2,当减去7个2之后余数是1,此时不能再减去更多的2,因此15/2的商是7,我们可以用一个循环实现这个过程。但这个直观的解法存在一个问题。当被除数很大但除数很小时,减法操作执行的次数会很多。例如,求(2的31次方-1)/1,减1的操作将执行2的32次方-1次,需要很长的时间。如果被除数是n,那么这种解法的时间复杂度为O(n)。我们需要对这种解法进行发优化。
但这个直观的解法稍作调整。当被除数大于除数时,继续比较判断被除数是否大于除数的2倍,如果是,则继续判断被除数是否大于除数的4倍、8倍等。如果被除数最多大于除数的2的k次方倍,那么将被除数减去除数的2的k次方倍,然后将剩余的被除数重复前面的步骤。由于每次将除数翻倍,因此优化后的时间复杂度是O(logn)。
下面以15/2为例子讨论计算的过程。15大于2,也大于2的2倍(即4),还大于2的4倍(即8),但是小于2的8倍(即16)。于是先将15减去8,还剩余7。由于减去的是除数的4倍,减去这部分对应的商是4。接下来对剩余的7和除数2进行比较,7大于2,大于2的2倍(即4),但小于2的4倍(即8),于是将7-4,还剩余3。这一次减去的是除数2的2倍,对应的商事2。然后对剩余的3和除数2进行比较,3大于2,但是小于2的2倍(即4),于是将3减去2,还剩余1。这一次减去的是除数的1倍,对应的商是1。最后剩余的数字是1,比除数小,不能再减去除数了。于是15/2的商是4+2+1,即7。
上述讨论假设被除数和除数都是正整数。如果有负数则可以将它们先转换为正数,计算整数的除法之后再根据需要调整商的正负号。例如,如果计算15/2,则可以先计算15/2,得到的商是7。由于被除数和除数中有一个负数,因此商应该是负数,于是商应该是-7。
将负数转换成整数存在一个小问题。对于32位的整数而言,最小的整数是-2的31次方,最大的数是2的31次方-1。因此。如果将-2的31次方转换位正数则会导致溢出。由于将任意正数转换位负数都不会溢出,因此可以先将正数都转换位负数,用前面优化之后的减法计算两个负数的除法,然后根据需要调整商的正负号。
最后讨论可能的溢出。由于是整数的除法并且除数不等于0,因此商的绝对值一定小于或等于被除数的绝对值。因此,int型整数的除法只有一种情况会导致溢出,即(-2的31次方)/(-1)。这是因为最大的正数位2的31次方-1,2的31次方超出了正数的范围。
再全面地分析了使用减法实现除法的细节之后,我们可以开始编写代码。参考代码如下:
Code
package chapter_01;
/**
* 整数整除
*
* @author : lx
* @date : 2021/08/15 下午 11:22
*/
public class Question01 {
/**
* 最小int型整数
*/
int MIN_INT_VALUE = 0x80000000;
/**
* 最小int型整数的一半
*/
int MIN_INT_VALUE_HALF = 0xc0000000;
/**
* 开放用于调用进行除法的方法
*
* @param dividend
* @param divisor
* @return
*/
public int divide(int dividend, int divisor) {
// 最小数除以-1的反转处理
if (dividend == MIN_INT_VALUE && divisor == -1) {
return Integer.MAX_VALUE;
}
int negative = 2;
// 被除数大于0,反转为负数
if (dividend > 0) {
negative--;
dividend = -dividend;
}
// 除数大于0,反转为负数
if (divisor > 0) {
negative--;
divisor = -divisor;
}
// 做实际除法计算:被除数与除数有且只有一个被反转,计算结果反转,否则,直接使用计算结果
// 即对负数除法的计算
int result = divideCore(dividend, divisor);
return negative == 1 ? -result : result;
}
/**
* 进行除行为的核心方法
*
* @param dividend
* @param divisor
* @return
*/
private int divideCore(int dividend, int divisor) {
// 初始化结果为0
int result = 0;
// 当被除数小于等于除数时循环执行
while (dividend <= divisor) {
// 临时变量存储除数
int value = divisor;
// 初始化商为1
int quotient = 1;
// 当除数小于最小int型整数的一半时 并且 被除数小于等于2倍的除数,因为此题不允许使用*号所以自己加自己
while (value >= MIN_INT_VALUE_HALF && dividend <= value + value) {
// 商值加上商值重新赋值给 变量商
quotient += quotient;
// 除数加上除数重新赋值给 变量临时变量value(上面有提到,临时变量value实际存储的是除数的值)
value += value;
}
// 结果加上商值重新赋值给 变量结果(result)
result += quotient;
// 被除数减去除数重新赋值给 变量被除数
dividend -= value;
}
return result;
}
public static void main(String[] args) {
System.out.println(new Question01().divide(15, 2));
// 控制台打印结果 : 7
}
}
上述代码中的0x80000000为最小的int型整数,即-2的31次方,0xc0000000是它的一半,即-2的30次方。
函数divideCore使用减法实现两个负数的除法。当除数和被除数中有一个负数时,商为负数。因此,在使用函数divideCore计算商之后,需要再根据除数和被除数的负数的个数调整商的正负号。