剑指Offer算法课(一)整数

138 阅读6分钟

整数的基本知识

JAVA/JVM中有四种整数类型,分别是byte short intlong,它们在位数、最小值、最大值方面有所区别。如下表。

类型位数最小值最大值
anyn100..00011..11
byte8-2^72^7-1
short16-2^152^15-1
int32-2^31^2^31-1
long64-2^632^63-1

对于n位有符号数来说,由于需要最左侧一位来表示符号,负数能使用的bit比正数多一个,负数为1后全0,正数为0后全1。最小值为-2^(n-1),最大值是2^(n-1)-1。

溢出

关于整数会考察的边界条件不多,溢出是一定要考虑到的,不同类型的整数有能够表达的上界。


面试题1:整数除法

leetcode.cn/problems/xo…

给定两个整数 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:二进制加法

leetcode.cn/problems/JF…

给定两个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的个数

leetcode.cn/problems/w3…

给定一个非负整数n,请计算0n之间的每个数字的二进制表示中 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:只出现一次的数字

leetcode.cn/problems/WG…

给你一个整数数组 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:单词长度的最大乘积

leetcode.cn/problems/as…

给定一个字符串数组 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;
}