确定<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();
}
};
不断模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);
}
};
我们可以把每个数分解成质数,这样阶乘运算就变成一串质数相乘。而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);
}
};
字符串相加,先把字符串倒转过来,对应位相加,注意进位。位数长的,还要对多余位继续相加。
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;
}
};
方法一:
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;
}
};
本题要实现两个函数:一个是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;
}
};
要根据权重值确定每个数字出现的概率,我们可以用一个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(类型))。
方法一:考虑到链表取任意位置元素比较麻烦,先将其保存到数组中,之后随机生成下标。时间和空间复杂度均为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;
}
};
本题是一道进制转化问题,进制为26,但难点在于这里A-Z,不是对应0~25,而是1-26。我们设十进制num转化后可表示为
则有
显然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;
}
};
字符串加法类型题。
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;
}
};
除 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;
}
};
本题本质上是寻找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;
}
};
寻找数组中出现次数大于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;
}
};
一个数字不断进行平方和赋值操作,有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;
}
};