LeetCode题解随笔:数学运算技巧

目录

一、随机

384. 打乱数组

 382. 链表随机节点

二、位运算

191. 位1的个数

231. 2 的幂

136. 只出现一次的数字

268. 丢失的数字 

645. 错误的集合

 89. 格雷编码

面试题 05.02. 二进制数转字符串

三、阶乘

172. 阶乘后的零

 793. 阶乘函数后 K 个零

三、素数

204. 计数质数

四、一行代码解决的算法题

292. Nim 游戏

 877. 石子游戏

319. 灯泡开关

五、数论

1250. 检查「好数组」


一、随机

384. 打乱数组

class Solution {
public:
    vector<int> nums_ori;
    Solution(vector<int>& nums) {
        this->nums_ori = nums;
    }

    vector<int> reset() {
        return nums_ori;
    }

    vector<int> shuffle() {
        vector<int> nums = this->nums_ori;
        int N = nums.size();
        for (int i = 0; i < N; ++i) {
            // 生成一个 [i, N-1] 区间内的随机数
            int r = rand() % (N - i) + i;
            // 交换 nums[i] 和 nums[r]
            swap(nums[i], nums[r]);
        }
        return nums;
    }
};

Fisher-Yates 洗牌算法

循环 n 次,在第 i 次循环中(0≤i<n):

  • 在 [i,n)中随机抽取一个下标 j;
  • 将第 i 个元素与第 j 个元素交换。

分析洗牌算法正确性的准则:产生的结果必须有 n! 种可能。

 382. 链表随机节点

class Solution {
public:
    ListNode* head;
    Solution(ListNode* head) {
        this->head = head;
    }
    int getRandom() {
        int i = 0;
        int res = 0;
        ListNode* p = this->head;
        while (p != nullptr) {
            i++;
            // 生成一个 [0, i) 之间的整数
            // 这个整数等于 0 的概率就是 1/i
            if (rand() % i == 0) {
                res = p->val;
            }
            p = p->next;
        }
        return res;
    }
};

水塘抽样算法(Reservoir Sampling)

当内存无法加载全部数据时,如何从包含未知大小的数据流中随机选取k个数据,并且要保证每个数据被抽取到的概率相等。

  • k=1:第 i 个元素处有 1/i 的概率选择该元素,1 - 1/i 的概率保持原有的选择。

  • k>1:第 i 个元素处以 k/i 的概率选择该元素,以 1 - k/i 的概率保持原有选择即可

参考连接:水塘抽样(Reservoir Sampling) - 知乎 (zhihu.com)

二、位运算

常用位操作常用的位操作 :: labuladong的算法小抄

191. 位1的个数

    int hammingWeight(uint32_t n) {
        int res = 0;
        while (n) {
            n = n & (n - 1);
            res++;
        }
        return res;
    }

n & (n-1):消除数字 n 的二进制表示中的最后一个 1。

231. 2 的幂

    bool isPowerOfTwo(int n) {
        if (n <= 0)    return false;
        return   (n & (n - 1)) == 0;
    }

 一个数如果是 2 的指数,那么它的二进制表示一定只含有一个 1。

注意:比较运算符的优先级高于位运算符。

136. 只出现一次的数字

    int singleNumber(vector<int>& nums) {
        int res = 0;
        for (int num : nums) {
            res ^= num;
        }
        return res;
    }

 一个数和它本身做异或运算结果为 0,即 a ^ a = 0;一个数和 0 做异或运算的结果为它本身,即 a ^ 0 = a。

异或运算满足交换律,a^b^a=a^a^b=b。因此对数组中的所有数字进行异或运算后,成对的数被消除,结果就是单个的数。

268. 丢失的数字 

    int missingNumber(vector<int>& nums) {
        int n = nums.size();
        int res = 0;
        for (int i = 0; i < n; ++i) {
            res ^= i ^ nums[i];
        }
        res ^= n;
        return res;
    }

 将数组中的n-1和数,和[0,n]的下标组成的这2*n-1个数异或,成对的数被消去,只剩下一个“丢失的数字”即为结果。

645. 错误的集合

    vector<int> findErrorNums(vector<int>& nums) {
        int dup = -1, missing = -1;
        // 将数组值映射到下标,把对应下标元素标记为负数
        for (int i = 0; i < nums.size(); ++i) {
            int mapping_idx = abs(nums[i]) - 1;
            // 已经被标记映射过,说明nums[i]对应元素重复
            if (nums[mapping_idx] < 0) {
                dup = abs(nums[i]);
            }
            else {
                nums[mapping_idx] *= -1;
            }
        }
        // 没有被标记为负数,对应下标即为缺失元素
        for (int i = 0; i < nums.size(); ++i) {
            if (nums[i] > 0)   missing = i + 1;
        }
        return { dup,missing };
    }

 本题没有采用位运算的方法,而采用了映射的方法,将元素值映射为下标后,通过将该下标所在元素置为负数,以判断该元素是否重复出现过。若重复出现过,在置为负数之前,该位置的元素即已经是负数了。

 元素和索引是成对儿出现的,常用的方法是排序、异或、映射

 89. 格雷编码

    vector<int> grayCode(int n) {
        vector<int> res(1 << n);
        for(int i = 0; i < res.size(); ++i){
            res[i] = i ^ (i >> 1);
        }
        return res;
    }

 1238. 循环码排列:当要求第一个整数是start时,只需要将求出的结果的每一项都与 start 进行按位异或运算即可。

面试题 05.02. 二进制数转字符串

    string printBin(double num) {
        string res = "0.";
        while (res.size() <= 32 && num != 0) {
            num *= 2;
            // 获取整数部分
            int digit = num;
            res.push_back(digit + '0');
            num -= digit;
        }
        return res.size() <= 32 ? res : "ERROR";
    }

 

三、阶乘

172. 阶乘后的零

    int trailingZeroes(int n) {
        int res = 0;
        long divisor = 5;
        while (divisor <= n) {
            res += n / divisor;
            divisor *= 5;
        }
        return res;
    }

首先,由于2×5=10,可以将问题转化为:n! 最多可以分解出多少个因子 2 和 5。

因子2的数量远远大于因子5的数量,只需要找n! 最多可以分解出多少个因子5即可。

【此处不太好理解】

比如n=126,由于[126÷5]=25,可以知道[1,126]中5的倍数有25个,这些数至少贡献出1个质因子;

[126÷5²]=5,可以知道[1,126]中5²的倍数有5个,这些数可以至少贡献2个质因子,不过由于它们同样一定是5的倍数,上一步计算中已经计算了作为5的倍数时贡献的25个质因子,这一步只需计算这5个数可以额外贡献的各一个质因子即可。从而res=25+5=30。

进一步,计算[126÷125]=1,又有1个数作为125的倍数,可以额外贡献一个质因子(该数作为5的倍数贡献了一个、作为25的倍数贡献了额外的一个,此时作为125的倍数又额外贡献了一个)。

 793. 阶乘函数后 K 个零

class Solution {
public:
    long trailingZeroes(long n) {
        long res = 0;
        long divisor = 5;
        while (divisor <= n) {
            res += n / divisor;
            divisor *= 5;
        }
        return res;
    }
    int preimageSizeFZF(int k) {
        return (int)(right_bound(k) - left_bound(k) + 1);
    }
    long left_bound(int target) {
        long lo = 0, hi = INT64_MAX;
        while (lo <= hi) {
            long mid = lo + (hi - lo) / 2;
            if (trailingZeroes(mid) < target) {
                lo = mid + 1;
            }
            else if (trailingZeroes(mid) > target) {
                hi = mid - 1;
            }
            else {
                hi = mid - 1;
            }
        }
        return lo;
    }
    long right_bound(int target) {
        long lo = 0, hi = INT64_MAX;
        while (lo <= hi) {
            long mid = lo + (hi - lo) / 2;
            if (trailingZeroes(mid) < target) {
                lo = mid + 1;
            }
            else if (trailingZeroes(mid) > target) {
                hi = mid - 1;
            }
            else {
                lo = mid + 1;
            }
        }
        return hi;
    }
};

本题在上一题的基础上,利用二分查找加速即可,找到阶乘函数后零的个数为k的元素的左边界和右边界。

需要复习二分查找,我使用的方式是左闭区间右闭区间。查找目标元素的左边界时,若找到目标元素,继续收缩二分查找的右边界,直到不满足lo <= hi的条件时,由于上一次满足lo=hi会让hi-1,因此hi恰指向左边界前一个元素,lo恰指向左边界元素。查找目标元素的右边界与之类似。

把握住找到目标元素的情况的处理方式,就不难理解继续寻找左右边界的过程了。

三、素数

204. 计数质数

    int countPrimes(int n) {
        vector<int> isPrime(n, 1);
        for (int i = 2; i * i < n; ++i) {
            if (isPrime[i]) {
                for (int j = i * i; j < n; j += i) {
                    isPrime[j] = 0;
                }
            }
        }
        int res = 0;
        for (int i = 2; i < n; ++i) {
            if(isPrime[i])   res++;
        }
        return res;
    }

本算法参考:如何高效寻找素数 :: labuladong的算法小抄

需要注意:0和1不是素数。 

四、一行代码解决的算法题

292. Nim 游戏

    bool canWinNim(int n) {
        return n % 4 != 0;
    }

 877. 石子游戏

bool stoneGame(vector<int>& piles) {
        return true;
    }

作为第一个拿石头的人,可以控制自己拿到所有偶数堆,或者所有的奇数堆。

可以在第一步就观察好,奇数堆的石头总数多,还是偶数堆的石头总数多,然后步步为营,就一切尽在掌控之中了。

319. 灯泡开关

    int bulbSwitch(int n) {
        return sqrt(n);
    }

电灯一开始都是关闭的,所以某一盏灯最后如果是点亮的,必然要被按奇数次开关。

以第6盏灯为例,第 1 轮会被按,第 2 轮,第 3 轮,第 6 轮都会被按,这是因为6=1*6=2*3,即第n盏灯会被按n的因子数次。如此以来,只有编号为完全平方数的灯有奇数个因子,会被按奇数次

假设现在总共有 16 盏灯,求 16 的平方根等于 4,这就说明最后会有 4 盏灯亮着,它们分别是第 1*1=1 盏、第 2*2=4 盏、第 3*3=9 盏和第 4*4=16 盏。

五、数论

1250. 检查「好数组」

bool isGoodArray(vector<int>& nums) {
        int divisor = nums[0];
        for (int num : nums) {
            divisor = std::gcd(divisor, num);
            if (divisor == 1) {
                break;
            }
        }
        return divisor == 1;
    }

本题解涉及到数论中的「裴蜀定理」:

因此只需要判断数组中所有元素的最大公约数是否为1即可,可以利用std::gcd进行判断。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值