目录
一、随机
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
的概率保持原有选择即可。
二、位运算
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进行判断。