力扣高频|算法面试题汇总(十一):数学&位运算

力扣高频|算法面试题汇总(一):开始之前
力扣高频|算法面试题汇总(二):字符串
力扣高频|算法面试题汇总(三):数组
力扣高频|算法面试题汇总(四):堆、栈与队列
力扣高频|算法面试题汇总(五):链表
力扣高频|算法面试题汇总(六):哈希与映射
力扣高频|算法面试题汇总(七):树
力扣高频|算法面试题汇总(八):排序与检索
力扣高频|算法面试题汇总(九):动态规划
力扣高频|算法面试题汇总(十):图论
力扣高频|算法面试题汇总(十一):数学&位运算

力扣高频|算法面试题汇总(十一):数学&位运算

力扣链接
目录:

  • 1.只出现一次的数字
  • 2.直线上最多的点数
  • 3.分数到小数
  • 4.阶乘后的零
  • 5.颠倒二进制位
  • 6.位1的个数
  • 7.计数质数
  • 8.缺失数字
  • 9.3的幂

1.只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4

思路
构建一个哈希表统计每个数字出现的次数,统计完后遍历哈希表,获得只出现一次的数字。
该方法时间复杂度:O(n)O(n),空间复杂度:O(n)O(n),需要一个额外的哈希表存储每个数字出现的次数。

思路2:
使用异或操作
一个数字异或其本身等于0。由于本题除了一个数字之外,其余数字均出现了两次,所以挨个异或,最后的那个数字就是只出现一次的数字。
时间复杂度:O(n)O(n), 空间复杂度:O(1)O(1)
C++

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int res = nums[0];
        for(int i = 1; i < nums.size(); ++i){
            res ^= nums[i];
        }
        return res;
    }
};

Python

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        res = nums[0]
        for i in range(1, len(nums)):
            res ^= nums[i]
        return res

2.直线上最多的点数

给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。
示例 1:
输入: [[1,1],[2,2],[3,3]]
输出: 3
解释:
^
|
| o
| o
| o
±------------>
0 1 2 3 4
示例 2:
输入: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
输出: 4
解释:
^
|
| o
| o o
| o
| o o
±------------------>
0 1 2 3 4 5 6

思路
参考思路:暴力法
两点确定一条直线,直线方程可以表示成下边的样子:y2y1x2x1=yy2xx2\frac{y 2-y 1}{x 2-x 1}=\frac{y-y 2}{x-x 2}
所以当来了一个点 (x,y)(x,y) 的时候,理论上,我们只需要代入到上边的方程进行判断即可。
第一个想法是,等式两边分子乘分母,转换为乘法的形式:(y2y1)(xx2)=(yy2)(x2x1)\left(y_{2}-y_{1}\right) *\left(x-x_{2}\right)=\left(y-y_{2}\right) *\left(x_{2}-x_{1}\right)
如果用int存可能会溢出,所以需要long
此外,还有一个方案:y2y1x2x1=yy2xx2\frac{y 2-y 1}{x 2-x 1}=\frac{y-y 2}{x-x 2}
还可以理解成判断两个分数相等,回到数学上,我们只需要将两个分数约分到最简,然后分别判断分子和分母是否相等即可
所以,需要求分子和分母的最大公约数,直接用辗转相除法即可

int gcd(int a, int b) {
    while (b != 0) {
        int temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}

然后 test 函数就可以写成下边的样子。需要注意的是,求了y - y2x - x2 最大公约数,所以要保证他俩都不是 00 ,防止除零错误。

bool test(int x1, int y1, int x2, int y2, int x, int y) {
    int g1 = gcd(y2 - y1, x2 - x1);// 求出最大公约数
    if(y == y2 && x == x2){ // 避免分母为0的情况
        return true;
    }
    int g2 = gcd(y - y2, x - x2);// 求出最大公约数
    // 判断是否在一个直线上
    return (y2 - y1) / g1 == (y - y2) / g2 && (x2 - x1) / g1 == (x - x2) / g2;
}

需要注意的是,因为我们两点组成一条直线,必须保证这两个点不重合。所以我们进入第三层循环之前,如果两个点相等就可以直接跳过:

if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) {
    continue;
}

此外,还需要考虑所有点都相等的情况,这样就可以看做所有点都在一条直线上:

int i = 0;
for (; i < points.size() - 1; i++) {
    if (points[i][0] != points[i + 1][0] || points[i][1] != points[i + 1][1]) {
        break;
    }
}
if (i == points.size() - 1) {
    return points.size();
}

这个算法的时间复杂:O(n3)O(n^3),需要3此遍历所有节点。没有使用额外辅助数组,所以空间复杂度O(1)O(1)
C++

class Solution {
private:
    int gcd(int a, int b) {
        while (b != 0) {
            int temp = a % b;
            a = b;
            b = temp;
        }
        return a;
    }
    bool test(int x1, int y1, int x2, int y2, int x, int y) {
        int g1 = gcd(y2 - y1, x2 - x1);// 求出最大公约数
        if(y == y2 && x == x2){ // 避免点重合,导致分母为0的情况
            return true;
        }
        int g2 = gcd(y - y2, x - x2);// 求出最大公约数
        // 判断是否在一个直线上
        return (y2 - y1) / g1 == (y - y2) / g2 && (x2 - x1) / g1 == (x - x2) / g2;
    }
public:
    int maxPoints(vector<vector<int>>& points) {
        if(points.size() < 3) return points.size();// 点数小于3
        int i = 0;
        // 防止所有点重合
        for(;i< points.size() -1; ++i){
            if(points[i][0] != points[i+1][0] || points[i][1] != points[i+1][1]) break;
        }
        if(i == points.size() -1) return points.size();
        int max = 0;
        // 循环暴力查找
        for(int i = 0; i < points.size(); ++i){
            for(int j = i + 1; j < points.size(); ++j){
                if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) {
                    continue;// 跳过相等的点
                }
                int tempMax = 0;
                // 第三层循环
                for(int k = 0; k < points.size(); ++k){
                    if(k != i && k != j){
                        // 进行判断
                        if(test(points[i][0], points[i][1],
                                points[j][0], points[j][1],
                                points[k][0], points[k][1]))
                            ++tempMax;
                    }
                }
                if(tempMax > max) max = tempMax;
            }
        }
        //加上直线本身的两个点
        return max + 2;
    }
};

Python:

class Solution:
    def gcd(self, a, b):
        while b != 0:
            temp = a % b
            a = b
            b = temp
        return a
    def test(self, x1, y1, x2, y2, x, y):
        g1 = self.gcd(y2 - y1, x2 -x1)
        if y == y2 and x == x2: return True
        g2 = self.gcd(y - y2, x - x2)
        return (y2 - y1)/ g1 == (y - y2)/ g2 and \
                (x2 - x1)/ g1 == (x - x2)/ g2;
                
    def maxPoints(self, points):
        if len(points) < 3 : return len(points)
        count = 0
        for i in range(len(points)-1):
            count = i
            # print("i", i)
            if(points[i][0] != points[i+1][0] or
               points[i][1] != points[i+1][1]):
                break
        if count + 1 == len(points) - 1: return len(points)
        max = 0
        for i in range(len(points)):
            for j in range(i+1, len(points)):
                if points[i][0] == points[j][0] and \
                    points[i][1] == points[j][1]:continue
                        
                tempMax = 0
                for k in range(len(points)):
                    if k != i and k != j:
                        if self.test(points[i][0], points[i][1],
                                points[j][0], points[j][1],
                                points[k][0], points[k][1]):
                            tempMax += 1
                if tempMax > max:
                    max = tempMax
        return max + 2

思路2
参考思路:点斜式方程表示直线
直线方程的另一种表示方式,「点斜式」:yy0=k(xx0)y-y_0=k(x-x_0),换句话,一个点加一个斜率即可唯一的确定一条直线。
所以可以对「点」进行分类然后去求,问题转换成,经过某个点的直线,哪条直线上的点最多。
在这里插入图片描述
当确定一个点后,平面上的其他点都和这个点可以求出一个斜率,斜率相同的点就意味着在同一条直线上。
所以可以用 HashMap 去计数,斜率作为 key,然后遍历平面上的其他点,相同的 key 意味着在同一条直线上。
上边的思想解决了「经过某个点的直线,哪条直线上的点最多」的问题。接下来只需要换一个点,然后用同样的方法考虑完所有的点即可。
当然还有一个问题就是斜率是小数,怎么办。
之前提到过了,用分数去表示,求分子分母的最大公约数,然后约分,最后将 「分子 + “@” + “分母”」作为 key 即可。
最后还有一个细节就是,当确定某个点的时候,平面内如果有和这个重叠的点,如果按照正常的算法约分的话,会出现除 0 的情况,所以我们需要单独用一个变量记录重复点的个数,而重复点一定是过当前点的直线的。
复杂度分析
时间复杂度:O(n2)O(n^2),两个for循环,比思路1减少一个数量级。
空间复杂度: O(n)O(n),每次遍历时,需要O(n)O(n)大小的辅助空间来存储当前点剩下点组合的斜率k,每次循环时都会重新初始化。
C++

class Solution {
private:
    int gcd(int a, int b) {
        while (b != 0) {
            int temp = a % b;
            a = b;
            b = temp;
        }
        return a;
    }
public:
    int maxPoints(vector<vector<int>>& points) {
        if(points.size() < 3) return points.size();// 点数小于3
        int res = 0;
        // 遍历每个点
        for(int i = 0; i < points.size(); ++i){
            int duplicate = 0;
            int max_value =  0; // 保存经过当前点的直线中,最多的点
            unordered_map<string, int> hashMap;
            for(int j = i + 1; j < points.size(); ++j){
                // 求出分子分母
                int x = points[j][0] - points[i][0];
                int y = points[j][1] - points[i][1];
                if(x == 0 && y == 0){// 如果点重合,跳过
                    ++duplicate;
                    continue;
                }
                // 进行约分
                int gcd_value = gcd(x, y);
                x /= gcd_value;
                y /= gcd_value;
                int n=100;
                string str=to_string(n);
                // 构建key 来表示斜率
                string key = to_string(x)+"@"+to_string(y);
                // 该斜率下的直线个数加1
                ++hashMap[key];
                // 获取较大值
                max_value = max(max_value, hashMap[key]);
            }
            //1 代表当前考虑的点,duplicate 代表和当前的点重复的点
            res = max(res, max_value + duplicate + 1);
        }
        return res;
    }
};

Python

class Solution:
    def gcd(self, a, b):
        while b != 0:
            temp = a % b
            a = b
            b = temp
        return a
              
    def maxPoints(self, points):
        if len(points) < 3 : return len(points)
        res = 0
        
        # 遍历每个点
        for i in range(len(points)):
            duplicate = 0
            max_value = 0 
            hashMap = {} # 这个字典必须在这里 防止重复计算
            for j in range(i+1, len(points)):
                x = points[j][0] - points[i][0]
                y = points[j][1] - points[i][1]
                if x == 0 and y == 0:
                    duplicate += 1
                    continue
                gcd_value = self.gcd(x, y)
                x /= gcd_value
                y /= gcd_value
                
                key = str(x) + "@" + str(y)
                if not key in hashMap:
                    hashMap[key] = 1
                else:
                    hashMap[key] += 1
                max_value = max(max_value, hashMap[key])
            res = max(res, max_value + duplicate + 1)
        return res
                

3.分数到小数

给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以字符串形式返回小数。
如果小数部分为循环小数,则将循环的部分括在括号内。
示例 1:
输入: numerator = 1, denominator = 2
输出: “0.5”
示例 2:
输入: numerator = 2, denominator = 1
输出: “2”
示例 3:
输入: numerator = 2, denominator = 3
输出: “0.(6)”

思路:
参考官方解法:长除法
本题有诸多细节。
要点:

  • 不需要复杂的数学知识,只需要数学的基本知识。了解长除法的运算规则。
  • 使用长除法计算4/94/9,循环节很显然就会找到。那么计算4/3334/333却不容易。
  • 注意边界情况!

本题核心思想:当余数出现循环的时候,对应的商也会循环
在这里插入图片描述
算法步骤:

  • 1.使用一个哈希表记录余数出现在小数部分的位置,当发现已经出现的余数,就可以将重复出现的小数部分用括号括起来。
  • 2.过程中余数可能为 0,意味着不会出现循环小数,立刻停止程序。
  • 3.就像 两数相除 问题一样,要考虑负分数以及极端情况,比如说2147483648/1-2147483648/-1

一些测试样例:
在这里插入图片描述
C++

class Solution {
public:
    string fractionToDecimal(int numerator, int denominator) {
        if(numerator == 0) return "0"; // 被除数是0
        string res = "";
        // 如果其中一个数字是负数
        // 异或对布尔进行运算
        if(numerator < 0 ^ denominator < 0) res += "-";
        // 转换成long long 防止溢出
        long long dividend = static_cast<long long>(numerator);
        long long divisor  = static_cast<long long>(denominator);
        // 再取绝对值
        dividend = llabs(dividend);
        divisor = llabs(divisor);
        // 获取商
        res += to_string(dividend/divisor);
        // 获取余数
        long long remainder  = dividend % divisor;
        if(remainder == 0) return res;
        // 余数不为0,说明有小数
        res += ".";
        int index = res.size() - 1; // 获得小数点的下标
        // 构建哈希表 用来记录出现重复数的下标,然后将'('插入到重复数前面就好了
        unordered_map<int, int> hashMap;
        // 长除法
        // 循环条件:余数不为0且余数还没有出现重复数字
        while(remainder != 0 && hashMap.count(remainder) == 0){
            ++index; // 位置加1
            hashMap[remainder] = index;
            //余数扩大10倍,然后求商,和草稿本上运算方法是一样的
            remainder *= 10;
            res += to_string(remainder/divisor);
            remainder %= divisor;
        }
        // 如果出现余数,则在重复的数字前面加'(' ,末尾加')'
        if(hashMap.count(remainder)){
            res.insert(hashMap[remainder], "(");
            res += ")";
        }
        return res;
    }
};

Python

class Solution:
    def fractionToDecimal(self, numerator: int, denominator: int) -> str:
        if numerator == 0: return "0"
        res = ""
        if (numerator < 0) ^ (denominator < 0): res += "-"
        numerator = numerator if numerator >=0 else -numerator
        denominator = denominator if denominator >=0 else -denominator
        
        res += str(int(numerator/denominator))
        
        num = numerator % denominator
        if num == 0: return res
        
        res += "."
        hashMap = {}
        index = len(res) - 1
       
        while num != 0 and not num in hashMap:
            index += 1
            hashMap[num] = index
            num *= 10 
            res += str(int(num/denominator))
            num = num % denominator         
        if num in hashMap:
            res = res[:hashMap[num]] + "(" + res[hashMap[num]:] + ")"
        return res

4.阶乘后的零

给定一个整数 n,返回 n! 结果尾数中零的数量。
示例 1:
输入: 3
输出: 0
解释: 3! = 6, 尾数中没有零。
示例 2:
输入: 5
输出: 1
解释: 5! = 120, 尾数中有 1 个零.
说明: 你算法的时间复杂度应为 O(log n) 。

思路:
最直观的解法:计算n!=1234...nn!=1*2*3*4*...*n,每次计算它的末尾数 0 个数,如果末尾有0,就除以10,可以通过反复检查数字是否可以被 1010 整除来计算末尾 0 的个数
时间复杂度: 低于O(n)O(n),这个详细的推导可看官方解析
空间复杂度:O(logn!)=O(nlogn)O(\log n!)=O(nlogn),为了存储 n!n!,我们需要 O(logn!)O(logn!) 位,而它等于 O(nlogn)O(nlogn)
官方例程如下:

def trailingZeroes(self, n: int) -> int:
        
    # Calculate n!
    n_factorial = 1
    for i in range(2, n + 1):
        n_factorial *= i
    
    # Count how many 0's are on the end.
    zero_count = 0
    while n_factorial % 10 == 0:
        zero_count += 1
        n_factorial //= 10
        
    return zero_count

思路2:
参考官方思路:计算因子 5
在一个阶乘中,我们把所有 11nn 之间的数相乘,这和把所有 11nn 之间所有数字的因子相乘是一样的。
例如,如果 n=16n=16,我们需要查看 111616 之间所有数字的因子。我们只对 22 和 55 有兴趣。包含 55 因子的数字是 510155,10,15,包含因子 22 的数字是 2468101214162、4、6、8、10、12、14、16。因为只三个完整的对,因此 16!16! 后有三个零。
这可以解决大部分情况,但是有的数字存在一个以上的因子。例如,若 i = 25,那么我们只做了 fives += 1。但是我们应该 fives += 2,因为 2525 有两个因子 55
因此,我们需要计算每个数字中的因子55。我们可以使用一个循环而不是 if 语句,我们若有因子 55将数字除以 55。如果还有剩余的因子 55,则将重复步骤(加一个while循环)。
这样就得到了正确答案,但是仍然可以做一些改进。
首先,我们可以注意到因子 22 数总是比因子 55 大。为什么?因为每四个数字算作额外的因子 22,但是只有每 2525 个数字算作额外的因子 55。下图可以清晰的看见:
在这里插入图片描述
因此可以删除计算因子 2 的过程,留下计算因子5的过程。
可以做最后一个优化。在上面的算法中,我们分析了从 11nn 的每个数字。但是只有 5,10,15,20,25,30,...5, 10, 15, 20, 25, 30,...等等,至少有一个因子 55。所以,不必一步一步的往上迭代,可以五步的往上迭代:因此可以修改为:

fives = 0
for i from 5 to n inclusive in steps of 5:
    remaining_i = i
    while remaining_i is divisible by 5:
        fives += 1
        remaining_i = remaining_i / 5

tens = fives

时间复杂度:O(n)O(n),(仍然超时了)
空间复杂度:O(1)O(1),只是用了一个整数变量

官方例程

class Solution:
    def trailingZeroes(self, n: int) -> int:
        zero_count = 0
        for i in range(5, n + 1, 5):
            current = i
            while current % 5 == 0:
                zero_count += 1
                current //= 5

        return zero_count

思路3:
参考官方思路:高效的计算因子 5
思路2仍然太慢。为了得出一个足够快的算法,需要做进一步改进,这个改进能使在对数时间内计算出答案
思考之前简化的算法(但不正确),它不正确是因为对于有多个因子55 时会计算出错,例如 2525
会发现这是执行 n5\frac{n}{5} 的低效方法。我们只对 55 的倍数感兴趣,不是 55 的倍数可以忽略,因此可以简化成:

fives = n / 5
tens = fives

关于解决多重因子的数字:所有包含两个及以上的因子55 的数字都是 2525的倍数。所以我们可以简单的除以 2525 来计算 2525 的倍数是多少。另外,在 n5\frac{n}{5} 已经计算了25一次,所以只需要额外因子n25\frac{n}{25} (而不是2n52*\frac{n}{5}),所以结合起来得到:

fives = n / 5 + n / 25
tens = fives

但是存在有三个因子55的情况,为了得到最终的结果,需要所有的n5n25n125n625\frac{n}{5}、\frac{n}{25}、\frac{n}{125}、\frac{n}{625}等相加。得到:

fives=n5+n25+n125+n625+n3125+=\frac{n}{5}+\frac{n}{25}+\frac{n}{125}+\frac{n}{625}+\frac{n}{3125}+\cdots
这样看起来会一直计算下去,但是并非如此!使用整数除法,最终,分母将大于nn,因此当项等于 0 时,就可以停止计算。
例如当n=12345n=12345时,得到:
fives=123455+1234525+12345125+12345625+123453125+1234516075+1234580375+=\frac{12345}{5}+\frac{12345}{25}+\frac{12345}{125}+\frac{12345}{625}+\frac{12345}{3125}+\frac{12345}{16075}+\frac{12345}{80375}+\dots
等于:
fives=2469+493+98+3+0+0+...=3082fives=2469+493+98+3+0+0+...=3082
在代码中,可以通过循环 55 的幂来计算:

fives = 0
power_of_5 = 5
while n >= power_of_5:
    fives += n / power_of_5
    power_of_5 *= 5
tens = fives

C++

class Solution {
public:
    int trailingZeroes(int n) {
        int zero_count = 0;
        long long current_multiple  = 5;
        while(n >= current_multiple){
            zero_count += n/current_multiple;
            current_multiple *= 5;
        }
        return zero_count;
    }
};

Python:

class Solution:
    def trailingZeroes(self, n: int) -> int:
        zero_count = 0
        current_multiple = 5
        while n >= current_multiple:
            zero_count += n // current_multiple
            current_multiple *= 5
        return zero_count

编写此算法的另一种方法是,不必每次尝试55 的幂,而是每次将 nn 本身除以 55。这也是一样的,因为最终得到的序列是
fives=n5+(n5)5+((n)5)5+fives=\frac{n}{5}+\frac{\left(\frac{n}{5}\right)}{5}+\frac{\left(\frac{(n)}{5}\right)}{5}+\cdots
注意,在第二步中,有n55\frac{\frac{n}{5}}{5},这是因为前一步nn本身除以55
如果熟悉分数规则,会发现n55\frac{\frac{n}{5}}{5}n55=n25\frac{n}{5*5}=\frac{n}{25}是一样的,意味着序列与n5+n25+n125+\frac{n}{5}+\frac{n}{25}+\frac{n}{125}+\cdots。这种编写算法的替代方法是等价的。
C++

class Solution {
public:
    int trailingZeroes(int n) {
        int zero_count = 0;
        while(n > 0){
            n /= 5;
            zero_count += n;
        }
        return zero_count;
    }
};

Python

class Solution:
    def trailingZeroes(self, n: int) -> int:
        zero_count = 0
        while n > 0:
            n //= 5
            zero_count += n
        return zero_count

5.颠倒二进制位

颠倒给定的 32 位无符号整数的二进制位。
示例 1:
输入: 00000010100101000001111010011100
输出: 00111001011110000010100101000000
解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
示例 2:
输入:11111111111111111111111111111101
输出:10111111111111111111111111111111
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。
进阶:
如果多次调用这个函数,你将如何优化你的算法?

思路:
参考官方思路:逐位颠倒
在这里插入图片描述
在这里插入图片描述
关键思想是,对于位于索引 i 处的位,在反转之后,其位置应为 31-i(注:索引从零开始)

  • 从右到左遍历输入整数的位字符串(即 n=n>>1)。要检索整数的最右边的位,应用与运算(n&1)。
  • 对于每个位,我们将其反转到正确的位置(即(n&1)<<power)。然后添加到最终结果。
  • n==0 时,终止迭代。

复杂度分析:
时间复杂度:O(log2N)O(log_2N),有一个循环来迭代输入的最高非零位,即log2Nlog_2N
空间复杂度:O(1)O(1),因为不管输入什么,内存的消耗是固定的。
C++

class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        int ret = 0;
        int power = 31;
        while(n){
            ret += (n & 1)<<power;
            n = n >> 1;
            power -= 1;
        }
        return ret;
    }
};

Python

class Solution:
    # @param n, an integer
    # @return an integer
    def reverseBits(self, n):
        ret, power = 0, 31
        while n:
            ret += (n & 1) << power
            n = n >> 1
            power -= 1
        return ret

思路2
参考官方思路大佬的解析
这种思想可以看作是一种分治的策略,通过掩码将 32 位整数划分成具有较少位的块,然后通过将每个块反转,最后将每个块的结果合并得到最终结果。
在下图中,演示如何使用上述思想反转两个位。同样的,这个想法可以应用到比特块上。
在这里插入图片描述
算法步骤:

  • 首先,将原来的 32 位分为 2 个 16 位的块。
  • 然后将 16 位块分成 2 个 8 位的块。
  • 然后继续将这些块分成更小的块,直到达到 1 位的块。
  • 在上述每个步骤中,将中间结果合并为一个整数,作为下一步的输入

大佬的解析:
既然知道 int 值一共32位,那么可以采用分治思想,反转左右16位,然后反转每个16位中的左右8位,依次类推,最后反转2位,反转后合并即可,同时可以利用位运算在原地反转。
通过debug查看:

  • 首先找一个数 (为了看的清楚用_作分隔,可以忽略):十进制43261596; // 0000 ‭0010 1001 0100 _ 0001 1110 1001 1100‬
  • 左边16位移到右边,右边16位移到左边,然后使用|符号合并起来。 >>:带符号右移。正数右移高位补0,负数右移高位补1。|:按位或逻辑,该位只要有一位为1,结果就为1,这里用来合并。
  • 使用一些有规律的数,将16位,再分成左右8位进行反转后合并,起始数字变为‭0001 1110 1001 1100 _ 0000 0010 1001 0100‬
    0xff00ff00 表示16进制数1111 1111 0000 0000 _ 1111 1111 0000 0000
    0x00ff00ff 表示16进制数0000 0000 1111 1111 _ 0000 0000 1111 1111
  • 重复以上步骤,分组、合并,最后得到反转后的结果。
  • 总结来说就是利用位运算进行反转,同时存储反转后的数,继续分治进行反转,直到全部反转完成,变化过程为
// 原数字43261596
 0000 ‭0010 1001 0100 _ 0001 1110 1001 1100‬ 
// 反转左右16位:
‭ 0001 1110 1001 1100 _ 0000 0010 1001 0100‬ 
// 继续分为8位一组反转:
 1001 1100 0001 1110 _ 1001 0100 0000 0010
// 4位一组反转:
 1100 1001 1110 0001 _ 0100 1001 0010 0000‬
// 2位一组反转:
‭ 0011 1001 0111 1000 _ 0010 1001 0100 0000‬‬
// 这就是43261596反转后的结果:‭964176192‬

C++

class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        n = (n >> 16) | (n << 16);
        n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8);
        n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4);
        n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2);
        n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1);
        return n;
    }
};

Python

class Solution:
    # @param n, an integer
    # @return an integer
    def reverseBits(self, n):
        n = (n >> 16) | (n << 16)
        n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8)
        n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4)
        n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2)
        n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1)
        return n

6.位1的个数

编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。
示例 1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。
示例 2:
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 ‘1’。
进阶:
如果多次调用这个函数,你将如何优化你的算法?

思路:
循环位移:不断对数字11进行与操作,如果为1,则计数加1。每次与操作完之后,进行移位操作。
复杂度分析
时间复杂度:O(1)O(1)。运行时间依赖于数字 nn 的位数。由于这题中 nn 是一个 32 位数,所以运行时间是 O(1)O(1)的。
空间复杂度:O(1)O(1)。没有使用额外空间。

C++

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int count = 0;
        while(n){
            if(n & 1) ++count;
            n >>= 1;
        }
        //cout<<"count:"<<count;
        return count;
    }
};

Python

class Solution:
    def hammingWeight(self, n: int) -> int:
        count = 0
        while n:
            if n & 1 : count += 1
            n >>= 1
        return count

思路2
参考官方思路:位操作的小技巧
不再检查数字的每一个位,而是不断把数字最后一个 ¥1$ 反转,并把答案加一。当数字变成 00 的时候,就知道它没有 11 的位了,此时返回答案。
这里关键的想法是对于任意数字 nn ,将 nnn1n−1 做与运算,会把最后一个 11 的位变成 00 。为什么?考虑 nnn1n - 1的二进制表示。
在这里插入图片描述
在二进制表示中,数字 nn中最低位的 11 总是对应 n1n−1 中的 00 。因此,将 nnn1n−1 与运算总是能把 nn最低位的 11 变成 00 ,并保持其他位不变
复杂度分析
时间复杂度:O(1)O(1) 。运行时间与 nn 中位为 11 的有关。在最坏情况下, nn 中所有位都是 11 。对于 32 位整数,运行时间是 O(1)O(1) 的。
空间复杂度:O(1)O(1) 。没有使用额外空间。

C++

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int count = 0;
        while(n){
            ++count;
            n &= (n-1);
        }
        return count;
    }
};

Python

class Solution:
    def hammingWeight(self, n: int) -> int:
        count = 0
        while n:
            count += 1
            n &= (n-1)
        return count

7.计数质数

统计所有小于非负整数 n 的质数的数量。
示例:
输入: 10
输出: 4
解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。

思路
暴力(最后一个超时)
第一个for循环遍历所有数字,然后判断该数字是否是质数。
时间复杂度:O(n(sqrt(n)))O(n(sqrt(n)))
C++

class Solution {
private:
    bool isPrimes(int num){
        for(int i =2; i <int(sqrt(num)) + 1; ++i){
            if(num % i == 0) return false; // 有额外的因数
        }
        return true;
    }
public:
    int countPrimes(int n) {
        int count = 0;
        for(int i = 2; i < n; ++i){
            if(isPrimes(i)) ++count;                      
        }
        return count;
    }
};

思路2
参考大佬解析: 高效计算isPrimes
首先从 2 开始,知道 2 是一个素数,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8… 都不可能是素数了。
然后发现 3 也是素数,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12… 也都不可能是素数了
也就是以素数为因子的数字不可能是素数了。
参考图示:

在这里插入图片描述
可以优化的地方:
回想刚才判断一个数是否是素数的 isPrime 函数,由于因子的对称性,其中的 for 循环只需要遍历 [2,sqrt(n)] 就够了。这里也是类似的,我们外层的 for 循环也只需要遍历到 sqrt(n)
比如 n=25n = 25i=4i = 4 时算法会标记 4×2=84 × 2 = 84×3=124 × 3 = 12 等等数字,但是这两个数字已经被 i=2i = 2i=3i = 32×42 × 43×43 × 4 标记了。
可以稍微优化一下,让 ji 的平方开始遍历,而不是从 2 * i 开始:

for (int i = 2; i * i < n; i++) 
    if (isPrim[i]) 
        ...

该算法的时间复杂度比较难算,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是:
n/2+n/3+n/5+n/7+...=n×(1/2+1/3+1/5+1/7...)n/2 + n/3 + n/5 + n/7 + ... = n × (1/2 + 1/3 + 1/5 + 1/7...)
括号中是素数的倒数。其最终结果是 O(N * loglogN)

C++

class Solution {
public:
    int countPrimes(int n) {
        vector<bool> isPrimes(n, true); // 初始化
        // 判断数字因子,因子小于sqrt(n)
        for(int i = 2; i * i < n; ++i){
            if(isPrimes[i]){// 如果是质数
                for(int j = i * i; j < n; j += i)
                    isPrimes[j] = false;
            }
        }
        int count = 0;
        for(int i = 2; i < n; ++i)
            if(isPrimes[i]) ++count;
        return count;
    }
};

Python

class Solution:
    def countPrimes(self, n: int) -> int:
        isPrimes = [True] * n
        for i in range(2, int(sqrt(n)) + 1):
            if isPrimes[i]:
                for j in range(i*i, n, i):
                    isPrimes[j] = False
        count = 0
        for i in range(2, n):
            if isPrimes[i]:
                count += 1
        return count

8.缺失数字

给定一个包含 0, 1, 2, …, n 中 n 个数的序列,找出 0 … n 中没有出现在序列中的那个数。
示例 1:
输入: [3,0,1]
输出: 2
示例 2:
输入: [9,6,4,2,3,5,7,0,1]
输出: 8
说明:
你的算法应具有线性时间复杂度。你能否仅使用额外常数空间来实现?

思路:
第一个循环,使用一个哈希表来记录每个数字是否出现,第二个循环,找到哈希表中没有出现数字的id。
时间复杂度:O(n)O(n),空间复杂度O(n)O(n)
C++

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        vector<bool> flag(nums.size()+1);
        for(auto num : nums){
            flag[num] = true;
        }
        for(int i = 0; i < flag.size(); ++i){
            if(!flag[i]) return i;
        }
        return -1;
    }
};

Python

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        idHash = [False] * (len(nums) + 1)
        for num in nums:
            idHash[num] = True
        for i in range(len(idHash)):
            if not idHash[i]: return i
        return -1

思路2
等差数列
题目给的是找到0,1,2,...,n中缺失的那个数字,可以假设没有缺失,那么原数组就是可以排列成一个等差数列,等差数列求和:(0+n)(n+1)/2(0+n)*(n+1)/2之后,挨个减去数组中的每个数字,剩下的那个数字便是缺失的数字。
时间复杂度:O(n)O(n),一次循环即可。
空间复杂度O(1)O(1),只需要用一个变量计算等差数列的和。
C++

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int sum = (0 + nums.size()) * (nums.size() + 1) / 2;
        for(auto num : nums){
            sum -= num;
        }
        return sum;
    }
};

Python:

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        sum = (0 + len(nums))*(len(nums) + 1)//2
        for num in nums: 
            sum -= num
        return sum

思路3
参考官方解析:位运算
由于异或运算(XOR)满足结合律,并且对一个数进行两次完全相同的异或运算会得到原来的数,因此可以通过异或运算找到缺失的数字。

下标 0 1 2 3
数字 0 1 3 4

可以将结果的初始值设为 nn,再对数组中的每一个数以及它的下标进行一个异或运算,即:
在这里插入图片描述
时间复杂度:O(n)O(n)
空间复杂度:O(1)O(1)
C++

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int length = nums.size();
        for(int i = 0; i < nums.size(); ++i)
            length ^= i^nums[i];
        return length;
    }
};

Python

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        length = len(nums)
        for i in range(len(nums)):
            length ^= i ^ nums[i]
        return length

9.3的幂

给定一个整数,写一个函数来判断它是否是 3 的幂次方。
示例 1:
输入: 27
输出: true
示例 2:
输入: 0
输出: false
示例 3:
输入: 9
输出: true
示例 4:
输入: 45
输出: false
进阶:
你能不使用循环或者递归来完成本题吗?

思路:
使用一个循环,不断除3,当被除数小于3时或无法被3整除时停止。判断被除数的大小,如果等于1,则是3的幂,否则不是。
复杂度分析

时间复杂度:O(logb(n))O(log_b(n)),在例子中是 O(logn)O(logn)。除数是用对数表示的。
空间复杂度:O(1)O(1),没有使用额外的空间。

C++

class Solution {
public:
    bool isPowerOfThree(int n) {
        while(n >= 3 and n %3 == 0){
            n /= 3;
        }
        if(n == 1) return true;
        else return false;      
    }
};

Python

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        while n >=3 and n % 3 == 0:
            n = n//3
        return n == 1

思路2:
参考官方思路:整数限制
可以看出 n 的类型是 int。在C++中,int通常有四个字节。它的最大值为 21474836472147483647
知道了 nn 的限制,我们现在可以推断出 nn 的最大值,也就是 33 的幂,是 11622614671162261467。计算如下:

3log3MaxInt=319.56=319=11622614673^{⌊log 3MaxInt⌋}=3^{⌊19.56⌋}=3^{19}=1162261467
因为 3 是质数,所以3193^{19}的除数只有30,31,...,3193^0,3^1,...,3^{19},因此我们只需要将 3193^{19} 除以 nn。若余数为 00 意味着 nn3193^{19}的除数,因此是 33 的幂。
复杂度分析
时间复杂度:O(1)O(1)。我们只做了一次操作。
空间复杂度: O(1)O(1),没有使用额外空间。
C++

class Solution {
public:
    bool isPowerOfThree(int n) {
        return n > 0 && 1162261467 % n == 0;    
    }
};

Python:

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        return n > 0 and 1162261467 % n == 0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章