文章目录
- 1.剑指 Offer 64. 求1+2+…+n
- 2.1823. 找出游戏的获胜者
- 3.面试题 08.05. 递归乘法
- 4.剑指 Offer 62. 圆圈中最后剩下的数字
- 5.344. 反转字符串
- 6.反转链表
- 7.剑指 Offer 06. 从尾到头打印链表
- 8.486. 预测赢家
1.剑指 Offer 64. 求1+2+…+n
原题链接
这道题在不加题目中各种限制的话十分简单(其实加了也十分简单用等差数列的公式即可.),循环遍历即可求出,那么如果在题目的限制下我们应该怎么作呢?首先while,for,switch,else不能使用了也就是说迭代是不可能的了,那么剩下的就只有递归了。
但是如果使用递归的话,递归的终止条件应该怎么做呢?第一个想法是三目运算符,但是也被限制了。这时候就要介绍一下&&
的一个特点了,我也不知道如何称呼,但是别人都称之为短路特性那么在此也引用了这个称呼.
观看了下面的代码我们自然而然地就理解了,如果n为0的话n&&(n+=sumNums(n-1))
不会被执行,返回0递归结束。这就是利用了&&
的这个特点,如果前件为假直接结束当前语句否则判断后件是否为真,也就是执行了我们的递归过程.
class Solution {
public:
int sumNums(int n) {
n&&(n+=sumNums(n-1));
return n;
}
};
2.1823. 找出游戏的获胜者
原题链接
这就是很经典的约瑟夫环的问题了,这里简单介绍一下详细的可以自行查阅资料:
所谓的约瑟夫环就是,现在有
n
n
n个人在玩一个游戏,游戏的规则是从第一个到最后一个人一次报数,每报到
k
(
k
<
=
n
)
k(k<=n)
k(k<=n)就淘汰掉他并从下一个人开始重新报数,如果人数不够就从第一个人开始接着上一人继续报号,直到剩下最后一人为止,这个人也就是这场游戏的获胜者.
那么对于这类问题的解法有很多很多,链表,迭代,队列,递归等等。不过既然这篇文章的标题叫递归,在这里就介绍递归解法了。
首先我们对递归函数的定义是找到给定人数在给定k的情况下的赢家。
那么首先对于
n
n
n来说,最后的赢家必然是淘汰掉第
k
k
k号后剩余玩家的获胜者,现在我们考虑的是剩下
n
−
1
n-1
n−1人的获胜情况。由于淘汰掉第K号,重新开始报数了,我们不妨把第K+1号作为一号,那么从第
K
+
1
K+1
K+1到第
n
n
n号新的编号就是
1
−
n
−
k
1-n-k
1−n−k,原来的
1
1
1到
k
−
1
k-1
k−1就变为了
n
−
k
+
1
n-k+1
n−k+1剩下的依次类推。
现在我们是有点晕的,再来梳理一下
如上图所示,假如k是5,那么新的1号就是原先的k+1号,2是k+2号,那么在这个图片的帮助下我们发现了一个规律,现在的编号加上k在不超过
n
n
n的情况下就是原来的编号,如果超过了
n
n
n我们直接模上
n
n
n即可,剩下就是我们的递归了,我会在代码部分进行讲解。
class Solution {
public:
int findTheWinner(int n, int k) {
if(n==1) return 1;//如果只剩余一个人就返回1,这是不管在任何编号条件下都成立了,因为我们总是吧他当作编号为1的人
int winner=findTheWinner(n-1,k)+k;
//对于每个n,递归去找赢家,返回了获胜者的编号之后我们先将其加k
return winner%n==0?n:(winner%n);
//如果winner%n==0,自然就是n号了,如果有余数那就摸上n
}
};
对于这个过程,看过代码之后可能更是一头雾水了。那么如何来进行理解呢?对于递归函数,在进行学习的时候我们首先接触的大概都是汉诺塔问题,对于那个递归其代码具体执行过程也是很难追究。但是我们不用去细究他的执行过程,只需要知道递归函数带给我们的结果就是在新编号的规则下的获胜者的编号就够了,还原回去是我们下面要考虑的问题。而还原就是我们的 w i n n e r winner winner要做的事情,他接受到的值就是新编号规则下的编号加上 k k k的数值,如果他不是n我们就%n,否则返回n。整个代码的逻辑就是这么简单。要多多理解还原和递归函数的定义。
3.面试题 08.05. 递归乘法
原题链接
在做这个题之前,我们可以回想一下我们最初接触到乘法的定义就是对于 2 + 2 + 2 + 2 + 2 + 2 + 2 2+2+2+2+2+2+2 2+2+2+2+2+2+2这种情况我们可以写成 2 ∗ 7 2*7 2∗7,也就是7个2相加,那么对于这道题也可以这样做.对于A和B,我们时刻保证A为较小数,然后累加即可。
class Solution {
public:
int multiply(int A, int B) {
if(A>B){
int tmp=A;
A=B;
B=tmp;
}
return A?B+multiply(A-1,B):0;
}
};
4.剑指 Offer 62. 圆圈中最后剩下的数字
原题链接
同样是约瑟夫环问题,按照之前的思路处理。
class Solution {
public:
int lastRemaining(int n, int m) {
if(n==1){
return 0;//由于编号是从0开始,只剩一个人的时候当前编号为0
}
int last=lastRemaining(n-1,m)+m;
return last%n;//也不用考虑n的情况直接返回last%n即可
}
};
5.344. 反转字符串
原题链接
常规的使用迭代即可处理,这里只是联系一下递归
class Solution {
public:
void reverse_(vector<char>&s,int l,int r){
if(l>r){
return ;
}
char tmp=s[l];
s[l++]=s[r];
s[r--]=tmp;
reverse_(s,l,r);
}
void reverseString(vector<char>& s) {
reverse_(s,0,s.size()-1);
}
};
6.反转链表
反转链表我们在刚学习链表的时候肯定都写过,不管是额外开辟了一个链表还是原地修改还是迭代递归等。这里就详细介绍一下递归过程中反转链表的执行过程.可以在过程中自己想一下过程。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==nullptr||head->next==nullptr)
return head;//找到尾结点后一个条件是为了防止链表本身为空
ListNode* ans=reverseList(head->next);//接收到尾结点也是递归执行的过程
head->next->next=head;//找到了尾节点之后我们把当前节点的下一个节点指向当前节点
head->next=nullptr;//当前节点指向空
return ans; //这里返回的ans一直都是尾结点,也是我们翻转后的头节点
}
};
7.剑指 Offer 06. 从尾到头打印链表
原题链接
这道题的话就不需要我们对链表进行反转了,只需要对其进行遍历然后再回溯的过程中依次加入节点的val即可。
class Solution {
public:
vector<int> ans;
void post_order(ListNode* head){
if(head==nullptr)return ;
post_order(head->next);
ans.push_back(head->val);
}
vector<int> reversePrint(ListNode* head) {
ans.clear();
post_order(head);
return ans;
}
};
8.486. 预测赢家
原题链接
也算是一道比较经典的博弈题了,对于这道题我们可以这样想,玩家1获胜的条件是他的得分比玩家2大或者相等也就是 s c o r e 1 > = s c o r e 2 score1>=score2 score1>=score2,过程中玩家均采取最优策略。先来看代码.
class Solution {
public:
int dfs(int l,int r,int scorea,int scoreb,bool isFirst,vector<int>& nums){
if(l>r){
return scorea>=scoreb;
}
if(isFirst){
return dfs(l+1,r,nums[l]+scorea,scoreb,false,nums)||dfs(l,r-1,nums[r]+scorea,scoreb,false,nums);
}else {
return dfs(l+1,r,scorea,scoreb+nums[l],true,nums)&&dfs(l,r-1,scorea,scoreb+nums[r],true,nums);
}
}
bool PredictTheWinner(vector<int>& nums) {
return dfs(0,nums.size()-1,0,0,true,nums);
}
};
递归函数的定义是判断玩家1是否为赢家。其参数意义分别为:
l
,
r
l,r
l,r还未选择的数的左右边界,
s
c
o
r
e
a
,
s
c
o
r
e
b
scorea,scoreb
scorea,scoreb为a,b二人的分数,
i
s
F
i
r
s
t
isFirst
isFirst为
b
o
o
l
bool
bool型来判断A是否先手,
n
u
m
s
nums
nums就是每个元素的得分情况。
由于双方均采取最优策略首先考虑:
1
)
1)
1)如果是玩家1先手的条件下,那么不论是他取左还是取右只要有一个情况下是他获胜那么玩家1必定获胜,而玩家1先手的情况下对
s
c
o
r
e
a
scorea
scorea来进行累加并交换出手顺序。
2
)
2)
2)如果玩家2先手,那么玩家1想要获胜就要不管玩家2选左边还是右边,玩家1都要获胜这种情况下对scoreb进行累加并交换出手顺序.
这种解法十分利于理解,但是执行效率较低,下面介绍一种在递归中运用dp的方法:
class Solution {
public:
int dp[25][25][2];//dp数组前两维仍然是从l到r范围内的分数,最后一维是玩家1还是玩家2先手的状态
//也就是说dp数组统计的是不同状态下玩家1的分数,不再求取玩家2的分数了
int dfs(vector<int> &nums,int l,int r,bool isFirst){
if(l>r){
return 0;
}
if(dp[l][r][isFirst]!=-1){
return dp[l][r][isFirst];//如果已经存在过这种状态直接返回.
}
int get=isFirst?1:0;//根据是否是玩家1的选择进行加分
if(isFirst){//玩家1先手
return dp[l][r][isFirst]=max(
nums[l]*get+dfs(nums,l+1,r,!isFirst),//如果选左,去求[l+1,r]的最合理情况并加上左边界分数
nums[r]*get+dfs(nums,l,r-1,!isFirst)//如果选右,去求[l,r-1]的最合理情况并加上右边界分数,二者取最大
);
}else return dp[l][r][isFirst]=min(//玩家2先手
nums[l]*get+dfs(nums,l+1,r,!isFirst),//如果选左,去求[l+1,r]的最合理情况
nums[r]*get+dfs(nums,l,r-1,!isFirst)//如果选右,去求[l,r-1]的最合理情况二者取最小
);
}
bool PredictTheWinner(vector<int>& nums) {
int sum=0;
for(auto i:nums){
sum+=i;
}
memset(dp,-1,sizeof(dp));
int score=dfs(nums,0,nums.size()-1,true);
return score*2>=sum;//如果score*2>=sum,说明a的分数一定大于等于b
}
};
这种做法的含义就是,我们的dp数组内存储的只是玩家1在不同状况下的分数最合理的分数。如果是A先手,那么我们选在该区间下的分数最大的状况,如果是B先手,我们选择在该区间下分数最小的状况,最后返回
d
p
[
l
]
[
r
]
[
i
s
F
i
r
s
t
]
dp[l][r][isFirst]
dp[l][r][isFirst]即可,不用去管递归内部如何执行的,只需要知道递归函数求取的是我们要求的最合理的情况即可。
这两种方法在时间上分别击败了50%和100%的用户。