递归专题
2. 两数相加
题目描述
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
-
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807. -
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0] -
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
提示:
每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
解析
由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。
我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry,则它们的和为 n1+n2+carry;其中,答案链表处相应位置的数字为 (n1+n2+carry) mod 10,而新的进位值为 ⌊(n1+n2+carry)/10⌋。
如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0 。
此外,如果链表遍历结束后,有 carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。
代码
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *head = nullptr, *tail = nullptr;
int carry = 0;
while (l1 || l2) {
int n1 = l1 ? l1->val: 0;
int n2 = l2 ? l2->val: 0;
int sum = n1 + n2 + carry;
if (!head) {
head = tail = new ListNode(sum % 10);
} else {
tail->next = new ListNode(sum % 10);
tail = tail->next;
}
carry = sum / 10;
if (l1) {
l1 = l1->next;
}
if (l2) {
l2 = l2->next;
}
}
if (carry > 0) {
tail->next = new ListNode(carry);
}
return head;
}
};
21. 合并两个有序链表
题目描述
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
-
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4] -
示例 2:
输入:l1 = [], l2 = []
输出:[] -
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
提示:
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列
解析
我们可以如下递归地定义两个链表里的 merge 操作:
list1[0]+merge(list1[1:],list2) list1[0]<list2[0]
list2[0]+merge(list1,list2[1:]) otherwise
也就是说,两个链表头部值较小的一个节点与剩下元素的 merge 操作结果合并。
如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
代码
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == nullptr) {
return l2;
} else if (l2 == nullptr) {
return l1;
} else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};
24. 两两交换链表中的节点
题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
-
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3] -
示例 2:
输入:head = []
输出:[] -
示例 3:
输入:head = [1]
输出:[1]
提示:
链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100
解析
可以通过递归的方式实现两两交换链表中的节点。
递归的终止条件是链表中没有节点,或者链表中只有一个节点,此时无法进行交换。
如果链表中至少有两个节点,则在两两交换链表中的节点之后,原始链表的头节点变成新的链表的第二个节点,原始链表的第二个节点变成新的链表的头节点。链表中的其余节点的两两交换可以递归地实现。在对链表中的其余节点递归地两两交换之后,更新节点之间的指针关系,即可完成整个链表的两两交换。
用 head 表示原始链表的头节点,新的链表的第二个节点,用 newHead 表示新的链表的头节点,原始链表的第二个节点,则原始链表中的其余节点的头节点是 newHead.next。令 head.next = swapPairs(newHead.next),表示将其余节点进行两两交换,交换后的新的头节点为 head 的下一个节点。然后令 newHead.next = head,即完成了所有节点的交换。最后返回新的链表的头节点 newHead。
代码
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* newHead = head->next;
head->next = swapPairs(newHead->next);
newHead->next = head;
return newHead;
}
};
50. Pow(x, n)
题目描述
实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,xn )。
-
示例 1:
输入:x = 2.00000, n = 10
输出:1024.00000 -
示例 2:
输入:x = 2.10000, n = 3
输出:9.26100 -
示例 3:
输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25
提示:
-100.0 < x < 100.0
-231 <= n <= 231-1
n 是一个整数
要么 x 不为零,要么 n > 0 。
-104 <= xn <= 104
解析
「快速幂算法」的本质是分治算法。举个例子,如果我们要计算 x^64 我们可以按照x → x^2 → x^4 → x^8 → x^16 → x^32 → x^64 的顺序,从 x 开始,每次直接把上一次的结果进行平方,计算 6 次就可以得到 x^64的值,而不需要对 x 乘 63 次 x。
直接从左到右进行推导看上去很困难,因为在每一步中,我们不知道在将上一次的结果平方之后,还需不需要额外乘 x。但如果我们从右往左看,分治的思想就十分明显了:
当我们要计算 x^n 时,我们可以先递归地计算出 y=x⌊n/2⌋,其中⌊a⌋ 表示对 a 进行下取整;
根据递归计算的结果,如果 n 为偶数,那么 x^n = y^2;如果 n 为奇数,那么 x^n = y^2×x;
递归的边界为 n=0,任意数的 0 次方均为 1。
由于每次递归都会使得指数减少一半,因此递归的层数为 O(logn),算法可以在很快的时间内得到结果。
代码
class Solution {
public:
double quickMul(double x, long long N) {
if (N == 0) {
return 1.0;
}
double y = quickMul(x, N / 2);
return N % 2 == 0 ? y * y : y * y * x;
}
double myPow(double x, int n) {
long long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
};
143. 重排链表
题目描述
给定一个单链表 L 的头节点 head ,单链表 L 表示为:
L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
-
示例 1:
输入:head = [1,2,3,4]
输出:[1,4,2,3] -
示例 2:
输入:head = [1,2,3,4,5]
输出:[1,5,2,4,3]
提示:
链表的长度范围为 [1, 5 * 104]
1 <= node.val <= 1000
解析
因为链表不支持下标访问,所以我们无法随机访问链表中任意位置的元素。
因此比较容易想到的一个方法是,我们利用线性表存储该链表,然后利用线性表可以下标访问的特点,直接按顺序访问指定元素,重建该链表即可。
代码
class Solution {
public:
void reorderList(ListNode *head) {
if (head == nullptr) {
return;
}
vector<ListNode *> vec;
ListNode *node = head;
while (node != nullptr) {
vec.emplace_back(node);
node = node->next;
}
int i = 0, j = vec.size() - 1;
while (i < j) {
vec[i]->next = vec[j];
i++;
if (i == j) {
break;
}
vec[j]->next = vec[i];
j--;
}
vec[i]->next = nullptr;
}
};
206. 反转链表
题目描述
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
-
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1] -
示例 2:
输入:head = [1,2]
输出:[2,1] -
示例 3:
输入:head = []
输出:[]
提示:
链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000
解析
递归版本稍微复杂一些,其关键在于反向工作。假设链表的其余部分已经被反转,现在应该如何反转它前面的部分?
假设链表为:
n1→…→nk−1→nk→nk+1→…→nm→∅
若从节点 nk+1 到 nm 已经被反转,而我们正处于 nk。
n1→…→nk−1→nk→nk+1←…←nm
我们希望 nk+1 的下一个节点指向 nk。
所以,nk.next.next=nk。
需要注意的是 n1 的下一个节点必须指向 ∅。如果忽略了这一点,链表中可能会产生环。
代码
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) {
return head;
}
ListNode* newHead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
};
241. 为运算表达式设计优先级
题目描述
给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
生成的测试用例满足其对应输出值符合 32 位整数范围,不同结果的数量不超过 104 。
-
示例 1:
输入:expression = “2-1-1”
输出:[0,2]
解释: ((2-1)-1) = 0 (2-(1-1)) = 2 -
示例 2:
输入:expression = “23-45”
输出:[-34,-14,-10,-10,10]
解释: (2*(3-(45))) = -34 ((23)-(45)) = -14 ((2(3-4))5) = -10 (2((3-4)5)) = -10 (((23)-4)*5) = 10
提示:
1 <= expression.length <= 20
expression 由数字和算符 '+'、'-' 和 '*' 组成。
输入表达式中的所有整数值在范围 [0, 99]
解析
用「动态规划」这种「自底向上」的方法来求解原始的表达式的全部可能计算结果。同样我们用 dp[l][r]={v0,v1,…}来表示对应表达式 ops[l:r] 在按不同优先级组合数字和运算符的操作下能产生的全部可能结果。此时边界情况就是当表达式不存在任何算符时,即 l=r 时,对应的结果集合中就只有一个数字。
dp[l][r]={ops[l]} , l=r & ops[l]≥0
代码
class Solution {
public:
const int ADDITION = -1;
const int SUBTRACTION = -2;
const int MULTIPLICATION = -3;
vector<int> diffWaysToCompute(string expression) {
vector<int> ops;
for (int i = 0; i < expression.size();) {
if (!isdigit(expression[i])) {
if (expression[i] == '+') {
ops.push_back(ADDITION);
} else if (expression[i] == '-') {
ops.push_back(SUBTRACTION);
} else {
ops.push_back(MULTIPLICATION);
}
i++;
} else {
int t = 0;
while (i < expression.size() && isdigit(expression[i])) {
t = t * 10 + expression[i] - '0';
i++;
}
ops.push_back(t);
}
}
vector<vector<vector<int>>> dp((int) ops.size(), vector<vector<int>>((int) ops.size()));
for (int i = 0; i < ops.size(); i += 2) {
dp[i][i] = {ops[i]};
}
for (int i = 3; i <= ops.size(); i++) {
for (int j = 0; j + i <= ops.size(); j += 2) {
int l = j;
int r = j + i - 1;
for (int k = j + 1; k < r; k += 2) {
auto& left = dp[l][k - 1];
auto& right = dp[k + 1][r];
for (auto& num1 : left) {
for (auto& num2 : right) {
if (ops[k] == ADDITION) {
dp[l][r].push_back(num1 + num2);
}
else if (ops[k] == SUBTRACTION) {
dp[l][r].push_back(num1 - num2);
}
else if (ops[k] == MULTIPLICATION) {
dp[l][r].push_back(num1 * num2);
}
}
}
}
}
}
return dp[0][(int) ops.size() - 1];
}
};
394. 字符串解码
题目描述
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
-
示例 1:
输入:s = “3[a]2[bc]”
输出:“aaabcbc”示例 2:
输入:s = “3[a2[c]]”
输出:“accaccacc”示例 3:
输入:s = “2[abc]3[cd]ef”
输出:“abcabccdcdcdef”示例 4:
输入:s = “abc3[cd]xyz”
输出:“abccdcdcdxyz”
提示:
1 <= s.length <= 30
s 由小写英文字母、数字和方括号 '[]' 组成
s 保证是一个 有效 的输入。
s 中所有整数的取值范围为 [1, 300]
解析
本题中可能出现括号嵌套的情况,比如 2[a2[bc]],这种情况下我们可以先转化成 2[abcbc],在转化成 abcbcabcbc。我们可以把字母、数字和括号看成是独立的 TOKEN,并用栈来维护这些 TOKEN。具体的做法是,遍历这个栈:
如果当前的字符为数位,解析出一个数字(连续的多个数位)并进栈
如果当前的字符为字母或者左括号,直接进栈
如果当前的字符为右括号,开始出栈,一直到左括号出栈,出栈序列反转后拼接成一个字符串,此时取出栈顶的数字(此时栈顶一定是数字,想想为什么?),就是这个字符串应该出现的次数,我们根据这个次数和字符串构造出新的字符串并进栈
重复如上操作,最终将栈中的元素按照从栈底到栈顶的顺序拼接起来,就得到了答案。注意:这里可以用不定长数组来模拟栈操作,方便从栈底向栈顶遍历。
代码
class Solution {
public:
string getDigits(string &s, size_t &ptr) {
string ret = "";
while (isdigit(s[ptr])) {
ret.push_back(s[ptr++]);
}
return ret;
}
string getString(vector <string> &v) {
string ret;
for (const auto &s: v) {
ret += s;
}
return ret;
}
string decodeString(string s) {
vector <string> stk;
size_t ptr = 0;
while (ptr < s.size()) {
char cur = s[ptr];
if (isdigit(cur)) {
// 获取一个数字并进栈
string digits = getDigits(s, ptr);
stk.push_back(digits);
} else if (isalpha(cur) || cur == '[') {
// 获取一个字母并进栈
stk.push_back(string(1, s[ptr++]));
} else {
++ptr;
vector <string> sub;
while (stk.back() != "[") {
sub.push_back(stk.back());
stk.pop_back();
}
reverse(sub.begin(), sub.end());
// 左括号出栈
stk.pop_back();
// 此时栈顶为当前 sub 对应的字符串应该出现的次数
int repTime = stoi(stk.back());
stk.pop_back();
string t, o = getString(sub);
// 构造字符串
while (repTime--) t += o;
// 将构造好的字符串入栈
stk.push_back(t);
}
}
return getString(stk);
}
};
486. 预测赢家
题目描述
给你一个整数数组 nums 。玩家 1 和玩家 2 基于这个数组设计了一个游戏。
玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0] 或 nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1 )。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。
如果玩家 1 能成为赢家,返回 true 。如果两个玩家得分相等,同样认为玩家 1 是游戏的赢家,也返回 true 。你可以假设每个玩家的玩法都会使他的分数最大化。
-
示例 1:
输入:nums = [1,5,2]
输出:false
解释:一开始,玩家 1 可以从 1 和 2 中进行选择。 如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。 所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。 因此,玩家 1 永远不会成为赢家,返回 false 。示例 2:
输入:nums = [1,5,233,7]
输出:true
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。 最终,玩家 1(234 分)比玩家 2(12分)获得更多的分数,所以返回 true,表示玩家 1 可以成为赢家。
提示:
1 <= nums.length <= 20
0 <= nums[i] <= 107
解析
为了判断哪个玩家可以获胜,需要计算一个总分,即先手得分与后手得分之差。当数组中的所有数字都被拿取时,如果总分大于或等于 0,则先手获胜,反之则后手获胜。
由于每次只能从数组的任意一端拿取数字,因此可以保证数组中剩下的部分一定是连续的。假设数组当前剩下的部分为下标 star 到下标 end,其中 0≤start≤end<nums.length。如果 start=end,则只剩一个数字,当前玩家只能拿取这个数字。如果 start<end,则当前玩家可以选择 nums[start] 或 nums[end],然后轮到另一个玩家在数组剩下的部分选取数字。这是一个递归的过程。
计算总分时,需要记录当前玩家是先手还是后手,判断当前玩家的得分应该记为正还是负。当数组中剩下的数字多于 1 个时,当前玩家会选择最优的方案,使得自己的分数最大化,因此对两种方案分别计算当前玩家可以得到的分数,其中的最大值为当前玩家最多可以得到的分数。
代码
class Solution {
public:
bool PredictTheWinner(vector<int>& nums) {
return total(nums, 0, nums.size() - 1, 1) >= 0;
}
int total(vector<int>& nums, int start, int end, int turn) {
if (start == end) {
return nums[start] * turn;
}
int scoreStart = nums[start] * turn + total(nums, start + 1, end, -turn);
int scoreEnd = nums[end] * turn + total(nums, start, end - 1, -turn);
return max(scoreStart * turn, scoreEnd * turn) * turn;
}
};
779. 第K个语法符号
题目描述
我们构建了一个包含 n 行( 索引从 1 开始 )的表。首先在第一行我们写上一个 0。接下来的每一行,将前一行中的0替换为01,1替换为10。
例如,对于 n = 3 ,第 1 行是 0 ,第 2 行是 01 ,第3行是 0110 。
给定行数 n 和序数 k,返回第 n 行中第 k 个字符。( k 从索引 1 开始)
-
示例 1:
输入: n = 1, k = 1
输出: 0
解释: 第一行:0示例 2:
输入: n = 2, k = 1
输出: 0
解释: 第一行: 0 第二行: 01示例 3:
输入: n = 2, k = 2
输出: 1
解释: 第一行: 0 第二行: 01
提示:
1 <= n <= 30
1 <= k <= 2n - 1
解析
我们可以尝试写表中的前几行:
0
01
0110
01101001
……
我们可以注意到规律:每一行的后半部分正好为前半部分的“翻转”——前半部分是 0 后半部分变为 1,前半部分是 1,后半部分变为 0。且每一行的前半部分和上一行相同。我们可以通过「数学归纳法」来进行证明。
有了这个性质,那么我们再次思考原问题:对于查询某一个行第 k 个数字,如果 k 在后半部分,那么原问题就可以转化为求解该行前半部分的对应位置的“翻转”数字,又因为该行前半部分与上一行相同,所以又转化为上一行对应对应的“翻转”数字。那么按照这样一直递归下去,并在第一行时返回数字 0 即可。
代码
class Solution {
public:
int kthGrammar(int n, int k) {
if (k == 1) {
return 0;
}
if (k > (1 << (n - 2))) {
return 1 ^ kthGrammar(n - 1, k - (1 << (n - 2)));
}
return kthGrammar(n - 1, k);
}
};