本系列往期文章如下:
【LeetCode 程序员面试金典(第 6 版)】第1-4章题目 01.01 ~ 04.10_lzs_lazy-CSDN博客【LeetCode 程序员面试金典(第 6 版)】第五章题目 05.01 ~ 05.08_lzs_lazy-CSDN博客
面试题 08.01. 三步问题 - 力扣(LeetCode) (leetcode-cn.com)
三步问题。有个小孩正在上楼梯,楼梯有 n 阶台阶,小孩一次可以上 1 阶、2 阶或 3 阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模 1000000007。
很经典的上台阶问题,思路很简单。
代码中做了详细注释,具体思路参见代码。
class Solution {
public:
enum{MOD = (int)1e9+7};
int waysToStep(int n) {
vector<int>dp(4, 0);
// 赋初值
dp[0] = 1; dp[1] = 1; dp[2] = 2;
// 遍历
for(int i = 3; i <= n; ++i){
// 如果台阶个数为i,有三种情况可以到达i:
//1. 通过第i-1级台阶1步到i
//2. 通过第i-2级台阶2步到i
//3. 通过第i-3级台阶3步到i
// 需注意 因为MOD=1e9+7,所以int类型最多保存两个MOD不溢出。所以对于每次加法运算都要MOD一下。
dp[3] = ( (dp[2] + dp[1]) % MOD + dp[0]) % MOD;
// 为了节省空间,可以通过整体往前移动3位。因为对于第i级台阶方案数来说,最多只需要前3个台阶的方案数。
dp[0] = dp[1];
dp[1] = dp[2];
dp[2] = dp[3];
}
// 如果n < 3 直接输出初始值即可
return n < 3 ? dp[n] : dp[3];
}// end waysToStep
};
面试题 08.02. 迷路的机器人 - 力扣(LeetCode) (leetcode-cn.com)
设想有个机器人坐在一个网格的左上角,网格 r 行 c 列。机器人只能向下或向右移动,但不能走到一些被禁止的网格(有障碍物)。设计一种算法,寻找机器人从左上角移动到右下角的路径。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gr7gXH9O-1642948367009)(assets/image-20220118141459-wfkiypp.png)]
网格中的障碍物和空位置分别用 1 和 0 来表示。
返回一条可行的路径,路径由经过的网格的行号和列号组成。左上角为 0 行 0 列。如果没有可行的路径,返回空数组。
思路:
由于每个点都可以有两种走法,则每个点最多可以被从两个方向到达。
对于任意可以从(0,0)到达的点(i, j),它可能存在这样的路径:(0, 0)->(i-1, j)->(i, j) 。也可能存在另一条路径:(0,0)->(i, j-1)->(i, j)。
其中(i-1,j)和(i,j-1)也可以有类似的路径。
可以发现上述描述是个递归的过程,是可以由动态规划处理。
代码中提供了详细解释,具体思路细节可见代码。
class Solution {
public:
typedef pair<short, short> pss;
vector<vector<int>> pathWithObstacles(vector<vector<int>>& obstacleGrid) {
//真坑呀,起点也不能走
if(obstacleGrid[0][0]) return vector<vector<int>>();
int r = obstacleGrid.size(); int c = obstacleGrid[0].size();
// 开辟 前驱二维数组 , pre[x][y] = (nx, ny) 表示(0,0)->(nx, ny)->(x, y) 是可行的
vector<vector<pss>>pre(r, vector<pss>(c, pss(-1,-1)));
//标识(0,0)是可以被经过
pre[0][0] = pss(-2, -2);
// 寻找每个点可以经过哪个点到达
for(int i = 0; i < r; ++i){
for(int j = 0; j < c; ++j){
// 障碍物 跳过,所有障碍物的点都不可能从(0,0)到达
if(1 == obstacleGrid[i][j]) continue;
// 从(0, 0)可以到达(i-1,j),并且从(i-1,j)可以达到(i,j)的条件
// pre[i][j]!=(-1,-1):两重含义 1. 可以由(0,0)到达。2.(i,j)点不存在障碍物。
if(i && pre[i - 1][j] != pss(-1, -1)){
pre[i][j] = pss(i - 1, j);
}
// 从(0, 0)可以到达(i,j-1),并且从(i,j-1)可以达到(i,j)的条件
if(j && pre[i][j - 1] != pss(-1, -1)){
pre[i][j] = pss(i, j - 1);
}
}
}
vector<vector<int>>path;
//通过前驱找到路径
int x = r - 1, y = c - 1;
while(pre[x][y].first != -1){
if(0 == x && 0 == y) break;
int nx = pre[x][y].first;
int ny = pre[x][y].second;
path.push_back(vector<int>{x, y});
x = nx; y = ny;
}
// 从终点找到最后都没到起点
if(x != 0 || y != 0) {
path.clear();
return path;
}
path.push_back(vector<int>{0, 0});
//路径翻转
reverse(path.begin(), path.end());
return path;
}
};
面试题 08.03. 魔术索引 - 力扣(LeetCode) (leetcode-cn.com)
魔术索引。 在数组A[0…n-1]中,有所谓的魔术索引,满足条件A[i] = i。给定一个有序整数数组,编写一种方法找出魔术索引,若有的话,在数组A中找出一个魔术索引,如果没有,则返回-1。若有多个魔术索引,返回索引值最小的一个。
直接从小到大遍历即可
class Solution {
public:
int findMagicIndex(vector<int>& nums) {
for(int i = 0; i < nums.size(); ++i) {
if(nums[i] == i) {
return i;
}
}
return -1;
}
};
面试题 08.04. 幂集 - 力扣(LeetCode) (leetcode-cn.com)
幂集。编写一种方法,返回某集合的所有子集。集合中 不包含重复的元素 。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
有两种解法,都要会。
- 二进制枚举
- DFS 枚举
方法一:二进制枚举。
- 如果从包含两个数字的集合中选出子集,总的子集个数为 2 2 = 4 2^2 = 4 22=4 个子集(包括空集)。文字描述这四个子集:空集、只选择第一个数字、只选择第二个数字、两个都选。如果用0和1来表示是否获取某个数字。00:可以表达两个都不选,为空集。01:可以表示选择第一个数字,第二个数字不选。10:可以表示选择第二个数字,第一个数字不选。11:可以表示两个数字都选。
- 为了给出更直观的描述,看0-4的二进制的表示:
0 00
1 01
2 10
3 11结合上述两部分的描述,不难看出如此结论:对于 n 个元素的集合,总的子集个数为 2 n 2^n 2n , 0 , 1 , 2 , . . . , ( 2 n − 1 ) 0,1,2,...,(2^n-1) 0,1,2,...,(2n−1) 整数的二进制表示,恰好可以表示这 2 n 2^n 2n 个不同的子集。
剩下的就是实现的细节了,代码中有详细的注释。建议下面的模板直接背下来,枚举子集时总可以用到。
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
// 二进制枚举
// 放入空子集
vector<vector<int>> ans;
// 遍历每一个子集 s。 1 << n 表示 2 ^ n
for(int s = 0; s < 1 << nums.size(); ++s){
vector<int> sub;
// 获取每个子集 s 的元素
for(int pos = 0; pos < nums.size(); ++pos){
// 第 pos 个数字被选择 ,也即 整数 s 的二进制表示中,从右向左第 pos 位是 1 。
if(s >> pos & 1) {
sub.push_back(nums[pos]);
}
}
ans.push_back(sub);
}
return ans;
}
};
解法二:dfs 枚举。
枚举子集的过程,其实就是每个元素都可以取或者不取,每个元素都进行了判定之后,一定会确定一个子集,这本质就是一个搜索问题。
注意:如果对递归还是浑浑噩噩的话,还是建议多找几个实例,多模拟几遍。大家都是如此过来的,什么时候可以脑补递归过程,才算真正入门了递归。这里就不用文字描述了,文字只会加大理解难度。
具体的看代码,代码中给了详细的注释。
class Solution {
public:
//存储所有的子集
vector<vector<int>> ans;
// pos:表示第pos个元素。sub 表示从 0-pos 这些位置上的元素,所有选取的元素集合。nums仅做传值用,不修改。
// 注意 sub 前面的 & 符号,这说明 整个递归过程共享这一个 sub 集合
void dfs(int pos, vector<int>& sub, const vector<int>& nums){
// pos == nums.size() 表示:0-nums.size()-1的nums.size()个元素都进行了判定。
// 每个元素都进行了取舍判定,那么一定出现了一个新的子集
if(pos == nums.size()) {
ans.push_back(sub);
return;
}
// 对于 第pos个元素。
// 1. 选取该元素,第pos个元素需要放入sub集合中。
sub.push_back(nums[pos]);
// 继续判断 第 pos + 1 个元素的取舍情况。
dfs(pos + 1, sub, nums);
// 2. 不选取该元素,由于前面放进去了,此时需要将其取出,而且一定会位于最后的元素(这里如果无法理解,一定要找实例,模拟就明白了)
sub.pop_back();
// 继续判断 第 pos + 1个元素的取舍情况
dfs(pos + 1, sub, nums);
}
vector<vector<int>> subsets(vector<int>& nums) {
vector<int> sub;
dfs(0, sub, nums);
return ans;
}
};
面试题 08.05. 递归乘法 - 力扣(LeetCode) (leetcode-cn.com)
递归乘法。 写一个递归函数,不使用 * 运算符, 实现两个正整数的相乘。可以使用加号、减号、位移,但要吝啬一些。
观察乘法的特点,同时注意题目条件–位移,所以需要留意一下二进制,观察二进制和乘法的关系。
应该能够想到: 2 a ∗ 2 b = 2 a + b 2^a*2^b=2^{a+b} 2a∗2b=2a+b,这个式子中乘法并不会用到乘法,而是用到加法,并且2的次幂可以通过移位来解决。
本题就是利用这个思路来解决。A 和 B 都可以转化为二进制,这样就可以考虑二进制乘法了。
代码中有详细注释。
class Solution {
public:
//遍历 A 的二进制位,A不断右移,而后通过 A & 1获得最后一个二进制位
int binMul(int A, int depth, int B){
// A 最多向右移动 30次
if(depth == 31) {
return 0;
}
// 如果当前 A二进制的第 depth位为1 ,乘法才有值
if(A & 1) {
int ans = 0;
// 遍历 B的所有二进制位
for(int i = 0; i < 31; ++i){
//此时,B 二进制的第i位和A二进制的第depth位都是1,两者相乘为 2^i * 2^{depth} = 2^{i+depth}
//而 2的次幂 可以通过移位来获得
if(B >> i & 1) {
ans += 1 << (i + depth);
}
}
//当前的值 加上后面的二进制的和
return ans + binMul(A >> 1, depth + 1, B);
}
// 如果当前 A二进制的第depth位为0,当前二进制位的乘法值为0,全靠后面的二进制位决定
return binMul(A >> 1, depth + 1, B);
}
int multiply(int A, int B) {
return binMul(A, 0, B);
}
};
【墨鳌】【100%完美满分算法】【标准解法=快速乘】【反向优化=FFT】 - 递归乘法 - 力扣(LeetCode) (leetcode-cn.com)可以学习一波这个题解。可以说思路很像了,但是实现没有那么优雅。
学习一波具体的实现。
class Solution {
public:
int multiply(int A, int B) {
int ans=0;
long long a=max(A,B);
long long b=min(A,B);
while(b){
if(b&1)ans+=a;
b>>=1;
a+=a;
}
return ans;
}
};