新的一天,新的开始,继续《剑指offer》!!!
面试题10- I. 斐波那契数列
思路1:在许多的算法与数据结构的书籍中,讲到递归的时候免不了肯定讲斐波那契数列,这是一个很典型的递归过程,写法很简单,但是很明显的缺点是递归中重复计算的部分很多,可以采用记忆化递归改进,如果没有优化,当n大于43时计算时间延迟明显能感受到。递归部分比较容易写出,直接省略。
思路2:当讲到递归特别是能使用记忆化递归的情况中,免不了出现动态规划的影子,动态规划可以减少很多重复的计算,在这个题目中,第n项之和第n-1与第n-2项有关系,因此在使用动态规划解题时只需要维护前两个变量即可,减少了递归中的重复计算以及额外的空间消耗。
class Solution {
public:
int fib(int n) {
//动态规划
int dp_n,dp_0 = 0, dp_1 = 1;
if(n<2) return n;
for(int i=2; i<=n; ++i)
{
dp_n = (dp_0+dp_1)%1000000007;
dp_0 = dp_1;//记录第n-2项信息
dp_1 = dp_n;//记录第n-1项信息
}
return dp_n;
}
};
面试题10- II. 青蛙跳台阶问题
思路:本题思路与斐波那契数列实质上是一样的,只是初始情况不同。由于每次只能上一级或两级台阶,显而易见,到第n层台阶只能有两个途径:要么从第n-2跳两级上来,或者从第n-1跳一级上来。这样就得到和斐波那契数列一样的递推公式:f(n) = f(n-1)+f(n-2)。因此一样可以使用动态规划解决问题,只是这次初始状态不同,从1和1开始而不是从0和1开始。
class Solution {
public:
int numWays(int n) {
int dp_n, dp_0 =1, dp_1 = 1;
if(n<2) return 1;
for(int i=2; i<=n; ++i)
{
dp_n = (dp_0+dp_1)%1000000007;
dp_0 = dp_1;
dp_1 = dp_n;
}
return dp_n;
}
};
面试题11. 旋转数组的最小数字
思路:题目给出的是一个排序数组以某个位置旋转以后的数组,要求找出其中最小的数字,当看到排序数组查找某个数首先想到的肯定是二分查找,但本题并不是单纯的排序数组,而是已经经过旋转后的,这个题目在leetcode主站的当中有两个类似的题目,第一个是旋转数组中不存在重复元素(题目和解答放在这题之后),第二个就是和本题一样的允许重复的数字。允许重复数字需要进行额外的操作,可以先做了没有重复数字的情况。本题有重复数字的情况在原来的基础之上需要进一步改进。
如果数组没有重复:
1、容易看出旋转后的数组,不是完全有序,而是分成了两部分,前半部有序和后半部有序;
2、采用二分查找时,我们需要关注一个target,用nums[mid]与之比较,在此题中,我们可以选择待查找区间的尾元素即nums[hi]作为target,很明显当选取了mid以后,一定会存在两种情况:A、nums[mid]<nums[hi],此时我们能肯定[mid,hi]这部分数组肯定有序,且最小值应该为nums[mid],因此我们令右端点hi收缩至mid;B、如果nums[mid]>nums[hi],此时,说明前半部分一定有序,最小值为nums[lo],但是因为旋转数组的缘故,nums[hi]在此时一定是小于nums[lo]的,不然不可能有nums[mid]>nums[hi]的情况,所以可以肯定最小值一定在[mid+1,hi]之间,此时我们应该收缩lo=mid+1;
3、二分查找最令人头疼的属于边界情况,思路很简单,边界往往很坑,因此由上述判断当nums[mid]>nums[hi]时我们能肯定nums[mid]一定不会是最小值,所以大胆的将lo=mid+1;而反过来时,我们不能确定nums[mid]在[lo,mid]之中的情形,只知道nums[mid]是后半部分的最小值,因此不能令hi=mid-1而只能领hi=mid,以确保如果nums[mid]正好是最小值的时候被跳过的情况。最后二分查找跳出循环时,返回前还要对nums[lo]与nums[hi]做一次判断,取二者最小值返回。
如果数组允许重复:
1、前两种情况还是和之前没有重复一样,即nums[mid]>nums[hi], lo = mid+1; nums[mid]<nums[hi], hi=mid;
2、但是允许重复元素,就存在第三种即nums[mid] == nums[hi],这时候该如何办,此时,存在两种情况:A、[mid,hi]之间的元素都相等;B、[lo,mid]之间的元素都相等,且都等于nums[hi];不论是遇到何种情形,只要我们将hi向左端缩进一个位置即可,此时如果在情况A,hi的缩进不影响最小值的取值,并且在每一步缩进的时候,都有可能将mid继续向前移动,由于[mid,hi]之间的元素是相等的,hi在缩进的时候值还能保持,直到越过mid位置,但在移动hi的同时,mid有可能也在移动,同时存在符合先前的两个条件之一,即可选择其他缩小区间的方式;如果是情况B,hi的缩进有可能就直接导致nums[hi]的改变,但时由于[lo,mid]之间的元素都一样,因此hi的改变也可能会改变原来的条件,而改变区间的取值;这两种情况都避免了大幅度移动区间端点而可能造成的取值问题。
代码如下,有重复元素:
class Solution {
public:
int findMin(vector<int>& nums) {
int lo = 0, hi = nums.size()-1;
while(1<hi-lo)
{
int mid = lo+(hi-lo)/2;
if(nums[mid]>nums[hi])
lo = mid+1;
else if(nums[mid]<nums[hi])
hi = mid;
else
hi-=1;
}
return nums[lo]<nums[hi]?nums[lo]:nums[hi];
}
};
153. 寻找旋转排序数组中的最小值
class Solution {
public:
int findMin(vector<int>& nums) {
int lo=0, hi=nums.size()-1;
while(1<hi-lo)
{
int mid = lo+(hi-lo)/2;
if(nums[mid]>nums[hi])//如果中点值大于右端点,说明最小值在右半边
lo = mid+1;
else//否则,最小值在左半边,不排除恰好为中点值,故不能hi=mid-1;
hi = mid;
}
return nums[lo]<nums[hi]?nums[lo]:nums[hi];
}
};
面试题12. 矩阵中的路径
解题思路:从题目就能看到要查找的是路径,这类排列组合、子集、路径的题目很容易想到的就是回溯法,本题即使用经典回溯法,回溯法有一个经典的框架,比较容易理解。
这里推荐一个:labuladong老哥的链接,老哥总结得很清晰明了,有基本的框架如下:
代码如下(思路都在注释中):
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
int m=board.size(),n=board[0].size();
for(int i=0; i<m; ++i)
for(int j=0; j<n; ++j)
if(backtrack(board,word,i,j,0)) return true;
return false;
}
bool backtrack(vector<vector<char>>& board, string& word, int i, int j, int k)
{
//判断是否匹配,越界或不匹配直接返回false
if(i<0 || i>=board.size() || j<0 || j>=board[0].size() || board[i][j] != word[k]) return false;
//如果k的值等于word的长度,即代表完全匹配,直接返回True
if(k==word.size()-1) return true;
//记录原来的字母,由于要进行递归查询且不能重复使用该字符
char tmp = board[i][j];
//所以将原字符改为不可能的标记*即代表以及选择过了
board[i][j] = '*';
//递归查询下一层,四个方向只要有个符合即可
if(backtrack(board,word,i-1,j,k+1) || backtrack(board,word,i+1,j,k+1) ||
backtrack(board,word,i,j-1,k+1) || backtrack(board,word,i,j+1,k+1))
return true;
//撤销选择
board[i][j] = tmp;
return false;
}
};
面试题13. 机器人的运动范围
解法1:深度优先搜索(DFS)
以下的数学基础参考:机器人的运动范围
基础:我们需要在深度优先搜索中判断下标的数位和是否大于k以便判断能否遍历到该位置,最直接的想法肯定是对一组下标值[row,col]逐位求和来判断,但是这样每次都循环一遍进行求值,而且反复进行,浪费很多时间。那由于深度优先搜索的想法是往一个方向一直前进直至结束,那我们可以利用这个性质进行简便计算下标各位的和,我们发现,当向下即row+1等于10的倍数时,有一个规律如19->20,各位的和由1+9变为2+0,即10->2,此时sum_r的值应该为原来的值减8,如果不是10的倍数,则加1即可。因此在递归进行时对sum_r和sum_c进行判断取值,可以省去循环求下标和的过程。
class Solution {
public:
int movingCount(int m, int n, int k) {
//法1:DFS
//将参数设置为全局变量使递归时能使用判断退出条件
r = m;
c = n;
t = k;
//visit数组记录已访问过的元素
vector<vector<bool>> visit(r,vector<bool>(c,false));
return DFS(visit,0,0,0,0);
}
private:
int r,c,t;
int DFS(vector<vector<bool>>& visit,int row, int col, int sum_r, int sum_c)
{
//递归退出条件
if(row>=r || col>=c || sum_c+sum_r>t || visit[row][col]) return 0;
//标记已访问元素
visit[row][col] = true;
//此处做两处递归,向下或向右
return 1+DFS(visit,row+1,col,(row+1)%10==0?sum_r-8:sum_r+1,sum_c)
+DFS(visit,row,col+1,sum_r,(col+1)%10==0?sum_c-8:sum_c+1);
}
};
解法2:广度优先搜索(BFS)
class Solution {
public:
int movingCount(int m, int n, int k) {
//法2:BFS
int res = 0;
vector<vector<bool>> visit(m,vector<bool>(n,false));
queue<vector<int>> q;
q.push({0,0,0,0});
while(!q.empty())
{
auto tmp = q.front();
q.pop();
int row = tmp[0],col = tmp[1], sum_r=tmp[2],sum_c=tmp[3];
//如果不符合条件则跳过
if(row>=m || col>=n || sum_c+sum_r>k || visit[row][col]) continue;
++res;
visit[row][col] = true;
q.push({row+1,col,(row+1)%10==0?sum_r-8:sum_r+1,sum_c});
q.push({row,col+1,sum_r,(col+1)%10==0?sum_c-8:sum_c+1});
}
return res;
}
};
面试题14- I. 剪绳子
解法1:贪心思想(数学规律)
此法比较难想到,需要了解一些数学规律,但也是效率最高的解法。数学规律分析参考:数学推导
class Solution {
public:
int cuttingRope(int n) {
//法1:数学规律(贪心思想)
if(n<4) return n-1;
int a = n/3, b = n%3;
if(b==0) return pow(3,a);
if(b==1) return pow(3,a-1)*4;
return pow(3,a)*2;
}
};
解法2:动态规划
思路:dp[i]表示长度为i的绳子剪成多段能获得的最大乘积,初始状态很明显为dp[1] = dp[2] =1;
状态转移方程:dp[i] = max(dp[i],max((i-j)j,jdp[i-j]))可以理解为:dp[i]即不需要裁剪保持,(i-j)*j表示,将长度为i的绳子,裁剪为两段j与(i-)的长度,或者是先裁剪一段j以后,再对余下的(i-j)进行相应的裁剪即dp[i-j]。
class Solution {
public:
int cuttingRope(int n) {
//动态规划
vector<int> dp(n+1);
dp[1] = dp[2] =1;
for(int i=3; i<=n; ++i)
{
for(int j=0; j<i; ++j)
dp[i] = max(dp[i],max((i-j)*j,j*dp[i-j]));
}
return dp[n];
}
};
本题还有其他一些奇淫技巧可以优化动态规划的方法,将空间复杂度优化为O(1),时间复杂度优化为O(n),但是都不太容易想到。
面试题14- II. 剪绳子 II
思路:本题与上一题是一样的,只是n的取值更大了,也即是有可能乘积出来得到一个很大的数值,造成整形溢出,因此要进行取模1000000007的处理,思路与之前一样,只是在计算的时候需要循环计算而不能在使用Pow函数计算了,在计算过程中就对1000000007取模,防止溢出,因此选择一个long long 的变量ans存放中间变量结果,最后返回时再进行类型转换,不然中间值有可能整形溢出。
class Solution {
public:
int cuttingRope(int n) {
if(n<4) return n-1;
int a=n/3,b=n%3;
long long ans = 1;
while(--a)
ans=(ans*3)%1000000007;
if(b==0) ans=ans*3%1000000007;
if(b==1) ans=ans*4%1000000007;
if(b==2) ans=ans*6%1000000007;
return (int)ans;
}
};
今日刷题笔记顺利结束,明日继续努力!!!