二叉树
先简单的复习一个二叉树的遍历方式。博主链接如下所示:二叉树遍历方法——前、中、后序遍历(图解)_二叉树遍历前序中序后序-CSDN博客
前序遍历的算法思路归纳就是:
若二叉树为空,什么都不做,否则:
i、先访问根结点;
ii、再前序遍历左子树;
iii、最后前序遍历右子树;
中序遍历的算法思路归纳就是:
二叉树为空,什么也不做,否则:
i、中序遍历左子树;
ii、访问根结点;
iii、中序遍历右子树
后序遍历的算法思路归纳就是:
若二叉树为空,什么也不做,否则:
i、后序遍历左子树
ii、后序遍历右子树
iii、访问根结点
由于我之前都是用C语言写的二叉树,所以先从简单的题目来,练手C++。
94.二叉树的中序遍历
很简单,根据中序遍历的算法思路,有如下代码:
/**
* 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:
vector<int> res;
vector<int> inorderTraversal(TreeNode* root) {
if(!root) return res;
inorderTraversal(root->left);
if(root) res.push_back(root->val);
inorderTraversal(root->right);
return res;
}
};
好接下来,再熟悉下回溯算法,这个我看好多题解上都有提到,但我不大记得了,所以先来复习一下。以下是参考代码随想录的讲解。
回溯法
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。(所以回溯函数即可称为递归函数)它的本质是穷举,穷举所有可能,然后选出我们想要的答案。如果要让效率再高一点,那么则可以结合剪枝。该算法一般适用于解决以下几类问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
回溯法解决的问题都可以抽象为树形结构。
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
其算法的模板框架如下所示:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
77.组合
组合问题是回溯法解决的经典问题,当n为100,k为50的话,直接想法就需要50层for循环。所以就需要引出回溯法来解决这种k层for循环嵌套的问题。然后进一步把回溯法的搜索过程抽象为树形结构,可以直观的看出搜索的过程。
接着用回溯法三部曲,逐步分析了函数参数、终止条件和单层搜索的过程。
- 递归函数的返回值以及参数
在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。
- 回溯函数终止条件
什么时候到达所谓的叶子节点了呢?path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。
- 单层搜索的过程
回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。
class Solution {
void backtrace(int i,int n,int k,vector<int> &combination,vector<vector<int>>& res){
if(combination.size()==k){
res.emplace_back(combination);
return;
}
for(i=i+1;i<=n;++i){
combination.emplace_back(i);
backtrace(i,n,k,combination,res);
combination.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> res;
vector<int> combination; //表示当下的一行结果
backtrace(0,n,k,combination,res);
return res;
}
};
17.电话号码的字母组合
由于每个数字对应多个字母,所以要先考虑数字和字母如何映射。这里我们定义一个二维数组,例如:string letterMap[10],来做映射。
其次还要考虑如何去解决n个for循环的问题。
这里遍历的深度,就是输入“23”的长度,叶节点就是我们所要收集的结果,输出["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]。
根据回溯算法的思路,
首先,确定回溯函数参数:
我们要用一个字符串s来收集叶子结点的结果,然后用一个字符串数组result保存起来(两个变量定义为全局。)题目中参数指定是string digits,还要有一个int型的参数index来表示记录遍历第几个数字了,同时index也表示树的深度。
其次,确定终止条件:
例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。然后收集结果,结束本层递归。
最后,确定单层遍历逻辑:
首先要取index指向的数字,并找到对应的字符集,然后用for循环来处理这个字符集。
class Solution {
private:
const string letterMap[10]={
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz",
};
public:
vector<string> result;
void getCombination(const string& digits,int index,const string & s){
if(index == digits.size()){
result.push_back(s);
return;
}
int digit = digits[index] - '0';
string letters = letterMap[digit];
for(int i=0;i<letters.size();i++){
getCombination(digits,index+1,s+letters[i]);
}
}
vector<string> letterCombinations(string digits) {
result.clear();
if(digits.size()==0){
return result;
}
getCombination(digits,0,"");
return result;
}
};
这里的const string &s,使用了两个重要的C++概念:常量引用和传递引用。
1、常量引用中,const关键字表示引用的对象不能被修改,在这个函数中,digits 和 s 都是以 const string& 的形式传递的,这意味着函数保证不会改变这两个字符串的内容。
- 对于
digits
,这是因为电话号码组合的输入应该是不可变的,函数中只需要读取数字对应的字母组合,而不需要修改输入的数字字符串。 - 对于
s
,这是因为在递归过程中,每次递归调用都会在前一个字符串的基础上添加新的字母,如果s
可以被修改,那么递归过程中的任何改变都会影响上一层递归的结果。
2、传递引用中,
- 引用传递是一种高效的传递对象的方式,因为它不涉及对象的复制。在递归调用中,如果使用值传递,每次递归都会创建
s
的新副本,这会导致不必要的性能开销。使用引用传递可以避免复制,直接在原始字符串上进行操作。 s
在递归过程中逐渐构建,每次递归都会扩展字符串。如果使用值传递,每次递归都会创建一个新的字符串副本,这会导致性能问题。使用引用传递可以确保所有递归层级都共享同一个字符串,避免了不必要的复制。