LeetCode刷题笔记(9):数学问题

204. 计数质数

确定<n的质数个数

方法一:埃氏筛

从2开始把2、3、4、5....的对应倍数标记为非质数,剩下的就是质数。

class Solution {
public:
    int countPrimes(int n) {
      if(n<=2) return 0;
      vector<bool> prime(n,false);
      int cnt = n-2;//统计质数个数,-2是去掉1和n
      for(int i=2;i*i<n;i++){
        int product = i*2;        
        while(product<n){
          if(!prime[product]){
            prime[product] = true;
            --cnt;
          }        
          product += i;
        }
      }
      return cnt;
    }
};

改进:注意到4的倍数一定是2的倍数,因此,已经被标记为和数的倍数不需要再筛,因为其倍数已被其因数筛过了。

class Solution {
public:
    int countPrimes(int n) {
      if(n<=2) return 0;
      vector<bool> prime(n,false);
      int cnt = n-2;//统计质数个数,-2是去掉1和n
      for(int i=2;i*i<n;i++){
        if(!prime[i]){
          int product = i*2;        
          while(product<n){
            if(!prime[product]){
              prime[product] = true;
             --cnt;
            }        
            product += i;
          }
        }
      }
      return cnt;
    }
};

方法二:线性筛

在埃式筛中,本质上是用质数2、3、5...的2、3、4、5、6...倍来筛选。同样的,这些倍数也可以只用质数,因为每个数都可以分解为质数积。

class Solution {
public:
    int countPrimes(int n) {
      //if(n<=2) return 0;
      vector<int> prime;//存储质数
      vector<bool> isPrime(n,true);
      for(int i=2;i<n;i++){
        if(isPrime[i]){//质数加入列表
          prime.push_back(i);
        }
        //取质数表中的数与i相乘
        for(int j=0; j<prime.size() && i*prime[j]<n; ++j){
          isPrime[i*prime[j]] = false;
        }
      }
      return prime.size();
      
    }
};

504. 七进制数

不断模7,除以7即可,最后用to_string转化为字符串。

class Solution {
public:
    string convertToBase7(int num) {
      int ans = 0, wei = 1;
      while(num!=0){
        ans += wei*(num%7);
        num /= 7;
        wei *= 10;
      }
      return to_string(ans);
    }
};

172. 阶乘后的零

我们可以把每个数分解成质数,这样阶乘运算就变成一串质数相乘。而10 = 2*5,又2的数量远多于5,所以0的个数就等于分解出质因数5的个数。

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

415. 字符串相加

字符串相加,先把字符串倒转过来,对应位相加,注意进位。位数长的,还要对多余位继续相加。

class Solution {
public:
    string addStrings(string num1, string num2) {
      string ans;
      reverse(num1.begin(), num1.end());
      reverse(num2.begin(), num2.end());
      int i=0, carry = 0;
      if(num1.length() < num2.length()){
        swap(num1,num2);
      }
      while(i<num2.length()){
        //对应位相加
        int sum = num1[i] - '0' + num2[i] -'0' + carry;        
        ans.push_back(sum%10 + '0');
        carry = sum/10;
        ++i;
      }
      while(i<num1.length()){
        int sum = num1[i] - '0' + carry;
        ans.push_back(sum%10 + '0');
        carry = sum/10;
        ++i;
      }
      if(carry!=0) ans.push_back(carry + '0');
      reverse(ans.begin(), ans.end());
      return ans;
    }
};

326. 3 的幂

方法一:

n是3的幂,则说明n不断除以3后取模都为0.复杂度为O(log3(n))。

class Solution {
public:
    bool isPowerOfThree(int n) {
      if(n<=0) return false;//n<=0肯定不是3的幂
      while(n>1){
        if(n%3) return false;
        n/=3;
      }
      return true;
    }
};

方法二:

设n =3^x,如果n是3的幂,则x一定是整数。

class Solution {
public:
    bool isPowerOfThree(int n) {
      return fmod(log10(n)/log10(3),1) == 0;
    }
};

语法:double fmod(double x, double y) 返回 x 除以 y 的余数。如果x是整数,那么除以1余数为0.

方法三:

int范围内3的幂最大值为3^19 = 11622261467,因此该范围内3的幂一定能被11622261467整除。

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

384. 打乱数组

本题要实现两个函数:一个是shuffle函数,随机打乱数组;一个是reset函数返回打乱之前的数组。

先介绍经典的Fisher-Yates洗牌算法。我们要从n个元素中随机取出k个元素可以这样操作,

第1轮,从1~n中随机选出一个数,与1号位置元素交换;

第2轮,从2~n中随机选出一个数,与2号位置元素交换;

。。。

第k轮,从k~n中随机选出1个数,与k号位置元素交换。

容易证明,1~k位置元素出现概率都是1/n,实现了随机取元素。

对于本题要打乱顺序,也可以对整个数组进行随机交换,就实现了等概率的打乱。

class Solution {
private:
    vector<int> origin;//保存原数组
    vector<int> nums;//用于进行数组打乱
public:
    Solution(vector<int>& nums) {
      this->nums = nums;
      //深拷贝赋值,把nums值赋给origin,进行保存
      this->origin.resize(nums.size());//分配实际内存
      //复制过程,也可以直接用copy函数
      //copy(nums.begin(), nums.end(),origin.begin());
      for(int i=0;i<nums.size();i++){
        origin[i] = nums[i];
      }
    }
    
    vector<int> reset() {
      return origin;
    }
    
    vector<int> shuffle() {
      //Fisher-Yates洗牌算法打乱顺序
      //反向洗牌,从0~i中随机选取一个元素,交换到i位置上
      for(int i = nums.size()-1; i>=0 ;--i){
        swap(nums[i], nums[rand()%(i+1)]);
      }
      return nums;
    }
};

要注意,保留原数组时应使用深拷贝,否则origin和nums是指向同一块内存,打乱之后原数组也丢失了。

语法:1)resize()函数,resize()是分配容器的内存大小,还可以赋予初值;区别reserve()只是设置容器容量大小,但并没有真正分配内存,且不能设置初值。

2) copy(a[i], a[j], b[k]):把a[i]-a[j-1]范围内的值赋给b[k]开始的位置。

这一步还可以用move函数:origin = std:: move(nums) 

move告诉编译器我们有一个左值,但我们希望像一个右值一样处理它。注意:调用move意味着承诺:除了对nums赋值和销毁它以外,我们不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。

class Solution {
private:
    vector<int> origin;//保存原数组
    vector<int> nums;//用于进行数组打乱
public:
    Solution(vector<int>& nums) {
      this->nums = nums;     
      //也可以用move函数
      origin = std::move(nums);
    }
    
    vector<int> reset() {
      return origin;
    }
    
    vector<int> shuffle() {
      //Fisher-Yates洗牌算法打乱顺序
      //反向洗牌,从0~i中随机选取一个元素,交换到i位置上
      for(int i = nums.size()-1; i>=0 ;--i){
        swap(nums[i], nums[rand()%(i+1)]);
      }
      return nums;
    }
};

528. 按权重随机选择

要根据权重值确定每个数字出现的概率,我们可以用一个odds数组存储每个位置之前所有数组之和(即前缀和)。权重数组范围是[w[0], ... sum],我们随机取[0, sum-1]中的某个数字idx,用二分法确定第一个大于idx的下标就是答案。

class Solution {
private:
    vector<int> odds;
    int sum = 0;
public:
    Solution(vector<int>& w) {
      odds.resize(w.size());
      for(int i=0; i<w.size(); i++){ 
        sum += w[i];       
        odds[i] = sum;        
      }
    }
    
    int pickIndex() {
      int idx = rand() % sum;
      //返回第一个大于idx的下标           
      int l = 0, r = odds.size();
      while(l<r){
        int mid = (l+r)/2;
        if(odds[mid] <= idx){
          l = mid+1;
        }else{
          r = mid;
        }
      }
      return l;
    }
};

可以用C++的库函数简洁代码。

class Solution {
private:
    vector<int> odds;
public:
    Solution(vector<int>& w) {
      odds = std::move(w);
      partial_sum(odds.begin(), odds.end(), odds.begin());
    }
    
    int pickIndex() {
      int idx = rand() % odds.back();
      //返回第一个大于idx的下标           
      return upper_bound(odds.begin(),odds.end(),idx) - odds.begin();
    }
};

1)partial_sum(a.begin(), a.end(), b.begin()); 从指定的a数组范围计算前缀和,并将结果保存到b数组中的指定位置,其参数设置与copy函数一致

2)upper_bound/lower_bound函数返回值是找到数字对应的地址,即一个指针。减去数组首元素指针即为下标值(指针之差 = 地址差/sizeof(类型))。

382. 链表随机节点

方法一:考虑到链表取任意位置元素比较麻烦,先将其保存到数组中,之后随机生成下标。时间和空间复杂度均为O(N)。

class Solution {
vector<int> nums;
public:
    Solution(ListNode* head) {
      while(head){
        nums.push_back(head->val);
        head = head->next;
      }
    }
    
    int getRandom() {
      return nums[rand()%nums.size()];
    }
};

方法二:水塘抽样

水塘抽样算法适用于数据量很大的情况,即当内存无法加载全部数据时,如何从包含未知大小的数据流中随机选取k个数据,并且要保证每个数据被抽取到的概率相等。

具体操作:遍历序列(数组或链表等),对遍历到的第 i 个节点,随机选择区间 [0,i)内的一个整数,如果其等于 0,则将答案置为该节点值,否则答案不变。

其原理和前面介绍的洗牌方法类似。

对本题,时间复杂度仍为O(n),但空间复杂度降为O(1)

class Solution {
ListNode* head;
public:
    Solution(ListNode* head) {
      this->head = head;
    }
    
    int getRandom() {
      //水塘抽样
      int i=1, ans = 0;
      ListNode* node = head;//这里不能直接用head去遍历,否则下次调用时头结点已经丢失了
      while(node){
        if(rand()%i==0) ans = node->val;//rand%i==0,更新选中答案
        node = node->next;
        ++i;
      }
      return ans;
    }
};

168. Excel表列名称

本题是一道进制转化问题,进制为26,但难点在于这里A-Z,不是对应0~25,而是1-26。我们设十进制num转化后可表示为

\alpha _{n-1}\alpha _{n-2}...\alpha _{0}

则有 

num = \sum_{i=0}^{n-1}(\alpha _{i} - 'A' + 1)*26^{^{i}}

显然a0 = (num - 1 + 'A’ )%26。再把num除以26,a1 =  (num - 1 + 'A’ )%26。之后的数字重复该过即可。

简单来说,就是每次取模之前,把进制移动的1减去,就对应回0~25了。

class Solution {
public:
    string convertToTitle(int columnNumber) {      
      string ans;      
      while(columnNumber){
        --columnNumber;
        ans.push_back(columnNumber%26 + 'A');
        columnNumber /= 26;
      }
      reverse(ans.begin(),ans.end());
      return ans;
    }
};

67. 二进制求和

 字符串加法类型题。

class Solution {
public:
    string addBinary(string a, string b) {
      string ans;
      reverse(a.begin(),a.end());
      reverse(b.begin(),b.end());
      if(a.length() < b.length()){
        swap(a,b);
      }
      int i=0, carry=0;      
      while(i<b.length()){
        int sum = a[i] - '0' + b[i] - '0' + carry;
        ans += sum%2 + '0';
        carry = sum/2;
        ++i;
      }
      while(i<a.length()){
        int sum = a[i] - '0' + carry;
        ans += sum%2 + '0';
        carry = sum/2;
        ++i;
      }
      if(carry) ans += carry + '0';
      reverse(ans.begin(),ans.end());
      return ans;
    }
};

238. 除自身以外数组的乘积

除 nums[i] 之外其余各元素的乘积可以看成两部分,一部分是左边的前缀积(不包括nums[i]),一部分是右边的后缀积(不包括nums[i])。因此只需要正序和逆序遍历一遍,求得各位置的前缀积和后缀积再相乘即可。

这种正反各遍历数组一次的方法是一种常见的思想,如135题也是类似的思想。当每个位置元素和左右位置都有关时,正、反向遍历可以分别利用左、右位置关系。

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
      int n = nums.size();
      vector<int> ans(n);
      ans[0] = 1;
      //先求前缀积
      for(int i=1; i<n; ++i){
        ans[i] = ans[i-1]*nums[i-1];
      }
      //求后缀积
      int pro = 1;
      for(int i=n-2; i>=0; --i){
        pro *= nums[i+1];
        ans[i] *= pro;
      }
      return ans;
    }
};

462. 最少移动次数使数组元素相等 II

本题本质上是寻找x使 |a[0] - x| + |a[1] - x| + ... + |a[n-1] - x|值最小。由数学知识可得,x取中位数时,上式最小(偶数个数时,取中间两数 [p, q] 范围内任一数字均可)。找中位数的方法可以用快排的划分过程实现,要学会掌握。

class Solution {
public:
    int partition(vector<int>& nums, int st, int ed){//划分过程,返回最终下标
      //随机选主元
      if(st >= ed) return ed;
      int p = st + rand()%(ed-st+1);//主元随机选择[st,ed]中一个
      swap(nums[p],nums[st]);
      int pivot = nums[st], l = st, r = ed;
      while(l<r){
        while(nums[r]>=pivot && l<r){
          --r;
        }        
        nums[l] = nums[r];        
        while(nums[l]<=pivot && l<r){
          ++l;
        }        
        nums[r] = nums[l];
      }
      nums[l] = pivot;
      return l; 
    }

    int select(vector<int>& nums, int st, int ed, int k){//寻找排序后数组下标为k的元素
      int idx = partition(nums,st,ed);
      if(idx > k) idx = select(nums,st,idx - 1,k);
      else if(idx < k) idx = select(nums,idx + 1, ed, k);
      return idx;
    }

    int minMoves2(vector<int>& nums) {
      int n = nums.size();
      //寻找中位数
      int avg = nums[select(nums,0,n-1,n/2)], sum = 0;
      for(int a: nums){
        sum += abs(a - avg);
      }
      return sum;
    }
};

 169. 多数元素

寻找数组中出现次数大于1半的元素。

方法一:哈希

统计每个数字出现次数,返回次数大于1半的元素。

class Solution {
public:
    int majorityElement(vector<int>& nums) {
      unordered_map<int,int> mp;
      for(int a : nums){
        ++mp[a];
        if(mp[a] > nums.size()/2) return a;
      }      
    }
};

方法二:摩尔投票算法

由于本题的众数出现次数大于一半,所以众数的个数大于其它所有元素个数和。用cnt标识当前众数比其它元素多的个数,当cnt<0时,说明其不是众数,更换众数。

注意:该算法仅在众数个数大于一半时才有效,小于等于一半的众数不能用此方法。

class Solution {
public:
    int majorityElement(vector<int>& nums) {
      int candidate = -1, cnt = 0;
      for(int a : nums){
        if(a == candidate) ++cnt;
        else if(--cnt<0){
          candidate = a;
          cnt = 1;
        }
      } 
      return candidate;  
    }
};

202. 快乐数

一个数字不断进行平方和赋值操作,有3种可能:

1)最终抵达1

2)出现循环,始终无法抵达1

3)可能不断增大。可以发现10^13-1的平方和为1053,而1053的平方和小于9999的平方和324,而324的平方和小于999的平方和243。因此10^13以内数字经过反复平方和计算,最终都会小于243,因此对本题int范围内数字,不可能出现不断增大的情况。

因此只需考虑情况1和2。我们把数字看成一个个结点,平方和的过程看成链表,那么题目就变成了链表的环路检测,因此我们可以用快慢指针进行判断。如果fast和slow指针最终相等时不为1,那么久不是快乐数。

class Solution {
public:
    int squareSum(int n){
      int sum = 0;
      while(n){
        sum += (n%10) * (n%10);
        n/=10;
      }
      return sum;
    }

    bool isHappy(int n) {
      int slow = n, fast = squareSum(n);
      while(fast!=1 && slow != fast){
        slow = squareSum(slow);
        fast = squareSum(squareSum(fast));
      }
      return fast == 1;
    }    
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值