算法--递归、搜索与回溯
目录
- 原理
- 经典例题
- [面试题 08.06. 汉诺塔问题](https://leetcode.cn/problems/hanota-lcci/)
- [21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/description/)
- [206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)
- [24. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/description/)
- [50. Pow(x, n)](https://leetcode.cn/problems/powx-n/)
- [2331. 计算布尔二叉树的值](https://leetcode.cn/problems/evaluate-boolean-binary-tree/description/)
- [98. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/)
- [46. 全排列](https://leetcode.cn/problems/permutations/submissions/616539728/)
- [1863. 找出所有子集的异或总和再求和](https://editor.csdn.net/md?articleId=146029073)
- [47. 全排列 II](https://leetcode.cn/problems/permutations-ii/description/)
- [22. 括号生成](https://leetcode.cn/problems/generate-parentheses/description/)
- [36. 有效的数独](https://leetcode.cn/problems/valid-sudoku/description/)
- [37. 解数独](https://leetcode.cn/problems/sudoku-solver/description/)
- [130. 被围绕的区域](https://leetcode.cn/problems/surrounded-regions/submissions/617110384/)
- [417. 太平洋大西洋水流问题](https://leetcode.cn/problems/pacific-atlantic-water-flow/)
- [300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/description/)
- [375. 猜数字大小 II](https://leetcode.cn/problems/guess-number-higher-or-lower-ii/)
原理
递归的过程就是树的深度优先遍历的过程,解题时可以定义必要的全局变量。
经典例题
面试题 08.06. 汉诺塔问题
在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。
请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。
你需要原地修改栈。
要想把A中的n个盘子移到C中,只需要先把前n-1个盘子借助C移到B中,再把A中剩余那个盘子移到C中,最后借助A把B中的盘子移到C中。_hanota(A,B,C,s)的含义为把A中的s个盘子通过B转移到C中。
class Solution {
public:
void _hanota(vector<int>& A, vector<int>& B, vector<int>& C,int s) {
if(1==s){
C.push_back(A.back());
A.pop_back();
return;
}
_hanota(A,C,B,s-1);
C.push_back(A.back());
A.pop_back();
_hanota(B,A,C,s-1);
}
void hanota(vector<int>& A, vector<int>& B, vector<int>& C){
_hanota(A,B,C,A.size());
}
};
21. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
选取较小的头结点作为返回的头指针,我们只需要合并除该头结点之外的链表即可,最后将该头结点链接合并的链表。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(nullptr==list1){
return list2;
}
if(nullptr==list2){
return list1;
}
ListNode* head=list1;
ListNode* ret=nullptr;
if(list1->val>list2->val){
head=list2;
ret=mergeTwoLists(list1,list2->next);
}else{
ret=mergeTwoLists(list1->next,list2);
}
head->next=ret;
return head;
}
};
206. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(nullptr==head||nullptr==head->next){
return head;
}
ListNode* rhead=reverseList(head->next);
head->next->next=head;
head->next=nullptr;
return rhead;
}
};
24. 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if(nullptr==head||nullptr==head->next){
return head;
}
ListNode* first=head;
ListNode* second=head->next;;
ListNode* ret=swapPairs(second->next);
second->next=first;
first->next=ret;
return second;
}
};
50. Pow(x, n)
实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,xn )。
要求x16,我们只需要求得x8就可以了
class Solution {
public:
double myPow(double x, int n) {
if(0==n){
return 1;
}
if(1==n||-1==n){
return 1==n?x:1.0/x;
}
int k=n/2;
double ret=n>0?x*myPow(x,k-1):1.0/x*myPow(x,k+1);
ret*=ret;
if(1==n%2||-1==n%2){
ret=n<0?1.0/x*ret:x*ret;
}
return ret;
}
};
2331. 计算布尔二叉树的值
给你一棵 完整二叉树 的根,这棵树有以下特征:
叶子节点 要么值为 0 要么值为 1 ,其中 0 表示 False ,1 表示 True 。
非叶子节点 要么值为 2 要么值为 3 ,其中 2 表示逻辑或 OR ,3 表示逻辑与 AND 。
计算 一个节点的值方式如下:
如果节点是个叶子节点,那么节点的 值 为它本身,即 True 或者 False 。
否则,计算 两个孩子的节点值,然后将该节点的运算符对两个孩子值进行 运算 。
返回根节点 root 的布尔运算值。
完整二叉树 是每个节点有 0 个或者 2 个孩子的二叉树。
叶子节点 是没有孩子的节点。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool evaluateTree(TreeNode* root) {
if(nullptr==root->left){
return root->val;
}
bool left=evaluateTree(root->left);
bool right=evaluateTree(root->right);
return 2==root->val?left||right:left&&right;
}
};
98. 验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
利用二叉搜索树的中序遍历是有序的的性质即可。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
long long int pre=LONG_MIN;
bool isValidBST(TreeNode* root) {
if(nullptr==root){
return true;
}
if(!isValidBST(root->left)||root->val<=pre){
return false;
}
pre=root->val;
return isValidBST(root->right);
}
};
46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
class Solution {
public:
vector<int> path;
vector<bool> record;
vector<vector<int>> res;
void get(vector<int>& nums){
if(path.size()==nums.size()){
res.push_back(path);
return;
}
int i=0;
int m=nums.size();
for(;i<m;++i){
if(true==record[i]){
record[i]=false;
path.push_back(nums[i]);
get(nums);
record[i]=true;
path.pop_back();
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
record.resize(7,true);
get(nums);
return res;
}
};
1863. 找出所有子集的异或总和再求和
一个数组的 异或总和 定义为数组中所有元素按位 XOR 的结果;如果数组为 空 ,则异或总和为 0 。
例如,数组 [2,5,6] 的 异或总和 为 2 XOR 5 XOR 6 = 1 。
给你一个数组 nums ,请你求出 nums 中每个 子集 的 异或总和 ,计算并返回这些值相加之 和 。
注意:在本题中,元素 相同 的不同子集应 多次 计数。
数组 a 是数组 b 的一个 子集 的前提条件是:从 b 删除几个(也可能不删除)元素能够得到 a 。
利用决策树暴力穷举即可,这里需要注意异或的性质:
①a^b=b^a
②a^b^b=a
class Solution {
public:
int sum = 0;
int path = 0;
void get(vector<int>& nums, int pos) {
int i = 0;
int m = nums.size();
for (i = pos; i < m; ++i) {
path ^= nums[i];
sum += path;
get(nums, i + 1);
path ^= nums[i];
}
}
int subsetXORSum(vector<int>& nums) {
get(nums, 0);
return sum;
}
};
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列
class Solution {
public:
vector<int> path;
vector<bool> record;
vector<vector<int>> res;
int next(vector<int>& nums,int i){
int m=nums.size();
for(i++;i<m&&nums[i]==nums[i-1];++i);
return i;
}
void get(vector<int>& nums){
if(path.size()==nums.size()){
res.push_back(path);
return;
}
int i=0;
int m=nums.size();
for(i=0;i<m;){
if(true==record[i]){
record[i]=false;
path.push_back(nums[i]);
get(nums);
path.pop_back();
record[i]=true;
i=next(nums,i);
}else{
++i;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
record.resize(nums.size(),true);
get(nums);
return res;
}
};
22. 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
只要保证以下两点,就可以保证该组合一定是有效的括号组合:
①最后的表达式中左括号的数量等于右括号的数量
②对于任意一个从下标0开始的子串,其左括号的数量大于等于右括号的数量
因此我们只需要暴力枚举每个位置括号可能的种类同时检查其是否满足以上条件即可。
class Solution {
public:
void Get(int n,int k,int start,vector<vector<int>>& ans,vector<int>& path){
if(n-start+1+path.size()<k){
return;
}
if(path.size()==k){
ans.push_back(path);
}
int i=start;
for(;i<=n;++i){
path.push_back(i);
Get(n,k,i+1,ans,path);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> ans;
vector<int> path;
Get(n,k,1,ans,path);
return ans;
}
};
36. 有效的数独
请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次
row[ i ][ j ]表示第 i 行中字数 j 是否已经出现过
column[ i ][ j ]表示第 i 列中字数 j 是否已经出现过
grid[ i ][ j ][ k ]表示以粗实线为分界线的第 i 行 j 列的九宫格内是否已经出现了数字 k
这就达到了以空间换时间的目的
class Solution {
public:
vector<vector<bool>> row;
vector<vector<bool>> column;
vector<vector<vector<bool>>> grid;
bool isValidSudoku(vector<vector<char>>& board) {
row.resize(9,vector<bool>(10,false));
column.resize(9,vector<bool>(10,false));
grid.resize(3,vector<vector<bool>>(3,vector<bool>(10,false)));
int i=0;
int j=0;
for(i=0;i<9;++i){
for(j=0;j<9;++j){
if('.'!=board[i][j]){
int num=board[i][j]-'0';
if(row[i][num]||column[j][num]||grid[i/3][j/3][num]){
return false;
}
row[i][num]=true;
column[j][num]=true;
grid[i/3][j/3][num]=true;
}
}
}
return true;
}
};
37. 解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
class Solution {
public:
bool row[9][10];
bool column[9][10];
bool grid[3][3][10];
bool get(vector<vector<char>>& board,int m,int n){
int x=8==n?m+1:m;
int y=8==n?0:n+1;
if('.'==board[m][n]){
int i=1;
for(;i<=9;++i){
if(row[m][i]||column[n][i]||grid[m/3][n/3][i]) continue;
board[m][n]=i+'0';
row[m][i]=true;
column[n][i]=true;
grid[m/3][n/3][i]=true;
if((8==m&&8==n)||get(board,x,y)) return true;
row[m][i]=false;
column[n][i]=false;
grid[m/3][n/3][i]=false;
}
}else{
if(8 == m && 8 == n) return true;
return get(board,x,y);
}
board[m][n]='.';
return false;
}
void solveSudoku(vector<vector<char>>& board) {
memset(row,false,sizeof(row));
memset(column,false,sizeof(column));
memset(grid,false,sizeof(grid));
int i=0;
int j=0;
for(i=0;i<9;++i){
for(j=0;j<9;++j){
if('.'!=board[i][j]){
int num=board[i][j]-'0';
row[i][num]=true;
column[j][num]=true;
grid[i/3][j/3][num]=true;
}
}
}
get(board,0,0);
}
};
130. 被围绕的区域
给你一个 m x n 的矩阵 board ,由若干字符 ‘X’ 和 ‘O’ 组成,捕获 所有 被围绕的区域:
连接:一个单元格与水平或垂直方向上相邻的单元格连接。
区域:连接所有 ‘O’ 的单元格来形成一个区域。
围绕:如果您可以用 ‘X’ 单元格 连接这个区域,并且区域中没有任何单元格位于 board 边缘,则该区域被 ‘X’ 单元格围绕。
通过 原地 将输入矩阵中的所有 ‘O’ 替换为 ‘X’ 来 捕获被围绕的区域。你不需要返回任何值。
先将与边界连通的所有 ‘O’ 替换成其它的字符 ‘.’,最后再遍历整个矩阵将 ‘.’ 替换成 ‘O’ ,将 ‘O’ 替换成 ‘X’。
class Solution {
public:
vector<int> x={-1,0,1,0};
vector<int> y={0,-1,0,1};
void replace(vector<vector<char>>& board,int m,int n){
int r=board.size();
int c=board[0].size();
if(m<0||n<0||m==r||n==c||'O'!=board[m][n]) return;
board[m][n]='.';
int i=0;
for(;i<4;++i){
replace(board,m+x[i],n+y[i]);
}
}
void solve(vector<vector<char>>& board) {
int m=board.size();
int n=board[0].size();
int i=0;
int j=0;
for(i=0;i<m;++i){
replace(board,i,0);
replace(board,i,n-1);
}
for(j=0;j<n;++j){
replace(board,0,j);
replace(board,m-1,j);
}
for(i=0;i<m;++i){
for(j=0;j<n;++j){
if('O'==board[i][j]) board[i][j]='X';
if('.'==board[i][j]) board[i][j]='O';
}
}
}
};
417. 太平洋大西洋水流问题
有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。
这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。
岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。
返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。
先从太平洋所在的边界开始向内地执行深度优先遍历找到可以流向太平洋的单元格,同时在矩阵中标记出这些单元格;同理,从大西洋所在的边界开始向内地执行深度优先遍历找到可以流向大西洋的单元格,也在在矩阵中标记出这些单元格,最后只要遍历标记矩阵就可以找到既可以流向太平洋也可以流向大西洋的单元格。
class Solution {
public:
int m=0;
int n=0;
vector<vector<bool>> record;
vector<vector<int>> dest1;
vector<vector<int>> dest2;
bool flag=true;
void cover(vector<vector<int>>& heights,int i,int j,int pre){
if(i<0||i==m||j<0||j==n||false==record[i][j]||pre>heights[i][j]){
return;
}
record[i][j]=false;
if(flag){
dest1[i][j]=1;
}else{
dest2[i][j]=1;
}
cover(heights,i+1,j,heights[i][j]);
cover(heights,i-1,j,heights[i][j]);
cover(heights,i,j+1,heights[i][j]);
cover(heights,i,j-1,heights[i][j]);
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
m=heights.size();
n=heights[0].size();
record.resize(m,vector<bool>(n,true));
dest1.resize(m,vector<int>(n,0));
dest2.resize(m,vector<int>(n,0));
int i=0;
int j=0;
for(i=0;i<n;++i){
if(0==dest1[0][i])
cover(heights,0,i,INT_MIN);
}
for(i=1;i<m;++i){
if(0==dest1[i][0])
cover(heights,i,0,INT_MIN);
}
flag=false;
record.resize(0);
record.resize(m,vector<bool>(n,true));
for(i=0;i<n;++i){
if(0==dest2[m-1][i])
cover(heights,m-1,i,INT_MIN);
}
for(i=0;i<m-1;++i){
if(0==dest2[i][n-1])
cover(heights,i,n-1,INT_MIN);
}
vector<vector<int>> res;
for(i=0;i<m;++i){
for(j=0;j<n;++j){
if(dest1[i][j]&&dest2[i][j]){
res.push_back({i,j});
}
}
}
return res;
}
};
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
递归获取以某个下标为起点的最长严格递增子序列的长度,考虑到递归过程中会重复求最长长度,因此在求出某个起点的最长严格递增子序列的长度后,我们可以把这个长度记录下来,下次需要直接获取就可以了。
class Solution {
public:
vector<int> record;
int m=0;
int get(vector<int>& nums,int index){
if(0!=record[index])
return record[index];
int res=1;
int i=index+1;
for(;i<m;++i){
if(nums[index]<nums[i])
res=max(res,get(nums,i)+1);
}
record[index]=res;
return res;
}
int lengthOfLIS(vector<int>& nums) {
m=nums.size();
record.resize(m,0);
int i=0;
int res=0;
for(i=0;i<m;++i){
res=max(res,get(nums,i));
}
return res;
}
};
375. 猜数字大小 II
我们正在玩一个猜数游戏,游戏规则如下:
我从 1 到 n 之间选择一个数字。
你来猜我选了哪个数字。
如果你猜到正确的数字,就会 赢得游戏 。
如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
每当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
对于任意[m , n]之间的数字,我们第一次可以猜[m , n]之间的任一数字 i ,由于我们需要确保获胜,所以我们假设这次猜错了,付出了 i 的代价,此时对方告诉我们是猜大了还是猜小了,我们就会接着在 [m , i-1] 或者 [ i+1 , n]这两个区间的其中一个里面去猜, 假设在[m , i-1] 和 [ i+1 , n]这两个区间里面确保你获胜 的最小现金数为left、right,由于我们是为了确保获胜,所以在[m , n]内本次猜 i 且要确保最后获胜的代价为: i + max(left , right)。在[m , n]内本次猜的 i 不同,最后确保获胜所花费的代价就会不同,意味着一定存在一个 i ,无论最后的答案是什么,只要我们这次猜 i ,最后的代价是最小的且确保可以获胜。
对于任意区间 [m , n] 确保获胜的最小现金数是唯一的,由于我们可能需要多次获取区间 [m , n] 确保获胜的最小现金数,故采用一个二维数组记录它们,需要时直接查询即可。
class Solution {
public:
vector<vector<int>> record;
int get(int left,int right){
if(left>=right) return 0;
if(-1!=record[left][right])
return record[left][right];
int res=INT_MAX;
int i=0;
for(i=left;i<right;++i){
res=min(res,i+max(get(left,i-1),get(i+1,right)));
}
record[left][right]=res;
return res;
}
int getMoneyAmount(int n) {
int m=n+1;
record.resize(m,vector<int>(m+1,-1));
int i=1;
for(;i<m;++i){
record[i][i]=0;
}
return get(1,n);
}
};