目录
前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。
一旦发现题目和子树有关,大概率要给函数设置合理的定义和返回值,在后序位置写代码了。
二叉树解题的思维模式分两类(来源:labuladong):
1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个
traverse
函数配合外部变量来实现,这叫「遍历」的思维模式。2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。
无论使用哪种思维模式,你都需要思考:
如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
一、遍历思维模式
543. 二叉树的直径
int max_diameter;
int diameterOfBinaryTree(TreeNode* root) {
MaxDepth(root);
return max_diameter;
}
// 最大直径是左右结点深度和
int MaxDepth(TreeNode* node) {
if (!node) return 0;
int left_max = MaxDepth(node->left);
int right_max = MaxDepth(node->right);
// 遍历最大深度的同时,由于是后序遍历,顺便可以得到但前结点处的最大直径
int cur_diameter = left_max + right_max;
max_diameter = max(max_diameter, cur_diameter);
return 1 + max(left_max, right_max);
}
每一条二叉树的「直径」长度,就是一个节点的左右子树的最大深度之和。借助后序遍历求最大深度的方法,利用全局变量记录最大元素,一次遍历即可获得结果。
二、分解问题思维模式
114. 二叉树展开为链表
// 分治思想,把子树展开为链表
void flatten(TreeNode* root) {
if (!root) return;
// 后序遍历
flatten(root->left);
flatten(root->right);
// 当存在左子树时,把右子树接到左子树的右侧叶节点上
if (root->left) {
TreeNode* cur = root->left;
while (cur->right) cur = cur->right;
cur->right = root->right;
root->right = root->left;
root->left = nullptr;
}
return;
}
注意本题的返回值为void类型,说明题目希望在原地完成拉直操作,分解思想可以解决本问题。
如下图所示(来源:labuladong),对于一个节点 x
,可以执行以下流程:
1、先利用 flatten(x.left)
和 flatten(x.right)
将 x
的左右子树拉平。
2、将 x
的右子树接到左子树下方,然后将整个左子树作为右子树。
652. 寻找重复的子树
// 记录以每个结点为根节点的树结构(采用后序记录,因为要从下往上记录树结构)
unordered_map<string, int> trees;
vector<TreeNode*> res;
// 采用序列化的思想,二叉树的前序/中序/后序遍历结果可以描述二叉树的结构
vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
traversal(root);
return res;
}
string traversal(TreeNode* node) {
if (!node) return "#";
string left_tree = traversal(node->left);
string right_tree = traversal(node->right);
string cur_tree = left_tree + "," + right_tree + "," + to_string(node->val);
// 结果去重:存在且只出现过一次时才记录结果
trees[cur_tree]++;
if (trees[cur_tree] == 2) res.push_back(node);
return cur_tree;
}
参考树的序列化和反序列化方式,含有“#”标记的前序/后序遍历字符串就可以唯一表征树结构,只要记录以每个结点为根节点的树的结构,就可以判断是否是重复子树。
本题要从下往上找,后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。一旦发现题目和子树有关,大概率要给函数设置合理的定义和返回值,在后序位置写代码了。
三、二叉搜索树
230. 二叉搜索树中第K小的元素
class Solution {
public:
int k;
int index;
int res;
int kthSmallest(TreeNode* root, int k) {
this->k = k;
index = 0;
Traversal(root);
return res;
}
void Traversal(TreeNode* node) {
if (!node) return;
Traversal(node->left);
this->index++;
if (this->index == this->k) {
this->res = node->val;
return;
}
Traversal(node->right);
}
};
本题若想将效率提升至log(N)级别,需要让当前节点知道自己的排名m:
1、如果 m == k
,显然就是找到了第 k
个元素,返回当前节点就行了。
2、如果 k < m
,那说明排名第 k
的元素在左子树,所以可以去左子树搜索第 k
个元素。
3、如果 k > m
,那说明排名第 k
的元素在右子树,所以可以去右子树搜索第 k - m - 1
个元素。
也就是说, TreeNode
中的字段应该如下:
class TreeNode {
int val;
// 以该节点为根的树的节点总数
int size;
TreeNode left;
TreeNode right;
}
- 在BST中插入:
TreeNode insertIntoBST(TreeNode root, int val) {
// 找到空位置插入新节点
if (root == null) return new TreeNode(val);
// if (root.val == val)
// BST 中一般不会插入已存在元素
if (root.val < val)
root.right = insertIntoBST(root.right, val);
if (root.val > val)
root.left = insertIntoBST(root.left, val);
return root;
}
95. 不同的二叉搜索树 II[*]
vector<TreeNode*> generateTrees(int n) {
return BuildBSTTrees(1, n);
}
vector<TreeNode*> BuildBSTTrees(int left, int right) {
vector<TreeNode*> res;
if (left > right) {
res.push_back(nullptr);
return res;
};
// 1、穷举 root 节点的所有可能
for (int i = left; i <= right; ++i) {
// 2、递归构造出左右子树的所有合法 BST
vector<TreeNode*> left_trees = BuildBSTTrees(left, i - 1);
vector<TreeNode*> right_trees = BuildBSTTrees(i + 1, right);
// 3、给 root 节点穷举所有左右子树的组合
for (auto left_tree : left_trees) {
for (auto right_tree : right_trees) {
TreeNode* root = new TreeNode(i);
root->left = left_tree;
root->right = right_tree;
res.push_back(root);
}
}
}
return res;
}
本题采用了分治思想,尤其注意递归函数返回值是根节点的数组。每次递归结束结果都会被覆盖,最后一次返回的结果就是1~n的结果。左右子树可能是空节点,因此需要在递归返回条件处加上res.push_back(nullptr)。