目录
零、自定义二叉树
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) {}
};
一、二叉树的遍历
144. 二叉树的前序遍历
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
traversal(root, res);
return res;
}
void traversal(TreeNode* root, vector<int>& v) {
if (root == NULL) return;
v.push_back(root->val);
traversal(root->left, v);
traversal(root->right, v);
}
145. 二叉树的后序遍历
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
traversal(root, res);
return res;
}
void traversal(TreeNode* root, vector<int>& v) {
if (root == NULL) return;
traversal(root->left, v);
traversal(root->right, v);
v.push_back(root->val);
}
迭代法的前序遍历和后序遍历类似,可以使用栈实现:父节点入栈—>父节点出栈并记录—>右孩子入栈、左孩子入栈—>……,只要栈不为空,就重复这一操作。
94. 二叉树的中序遍历
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
traversal(root, res);
return res;
}
void traversal(TreeNode* root, vector<int>& v) {
if (root == NULL) return;
traversal(root->left, v);
v.push_back(root->val);
traversal(root->right, v);
}
递归算法三要素:
- 确定递归函数的参数和返回值: 即递归过程中需要处理的量;
- 确定终止条件:防止溢出;
- 确定单层递归的逻辑: 即每次递归需要处理的信息。
中序遍历迭代法代码如下:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> st;
TreeNode* cur = root;
while (!st.empty() || cur) {
if (cur) {
st.push(cur);
cur = cur->left;
}
else {
cur = st.top();
res.push_back(cur->val);
st.pop();
cur = cur->right;
}
}
return res;
}
589. N 叉树的前序遍历
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
vector<int> preorder(Node* root) {
vector<int> res;
traversal(root, res);
return res;
}
void traversal(Node* node, vector<int>& nums) {
if (node == NULL) return;
nums.push_back(node->val);
if (!node->children.empty()) {
for (auto i : node->children) {
traversal(i, nums);
}
}
}
102. 二叉树的层序遍历
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
queue<TreeNode*> nodeQueue;
if(root != NULL) nodeQueue.push(root);
while (!nodeQueue.empty()) {
int sizeLevel = nodeQueue.size();
vector<int> valueLevel(sizeLevel);
for (int i = 0; i < sizeLevel; i++) {
valueLevel[i] = nodeQueue.front()->val;
if (nodeQueue.front()->left) nodeQueue.push(nodeQueue.front()->left);
if (nodeQueue.front()->right) nodeQueue.push(nodeQueue.front()->right);
nodeQueue.pop();
}
res.push_back(valueLevel);
}
return res;
}
层序遍历用队列实现,每次出队前,入队该结点的左右孩子结点。
层序遍历相关题目:
- 107.二叉树的层次遍历II
- 199.二叉树的右视图
- 637.二叉树的层平均值
- 429.N叉树的层序遍历
- 515.在每个树行中找最大值
- 116.填充每个节点的下一个右侧节点指针
- 117.填充每个节点的下一个右侧节点指针II
- 104.二叉树的最大深度
- 111.二叉树的最小深度
226. 翻转二叉树
TreeNode* invertTree(TreeNode* root) {
invert(root);
return root;
}
void invert(TreeNode* node) {
if (node == NULL) return;
invert(node->left);
invert(node->right);
TreeNode* temp = node->left;
node->left = node->right;
node->right = temp;
}
本题采用递归法,这道题前序遍历和后序遍历都可以,但中序遍历不行。否则交换完左孩子的左右孩子,再交换该结点的左右孩子,最后交换该结点右结点的左右孩子时,右结点实际上是之前的左结点。
二、二叉树的属性
101. 对称二叉树
bool isSymmetric(TreeNode* root) {
if (root == NULL) return true;
return compare(root->left, root->right);
}
bool compare(TreeNode* nodeLeft, TreeNode* nodeRight) {
if (!nodeLeft && nodeRight) return false;
else if (nodeLeft && !nodeRight) return false;
// 没有左/右孩子节点时,当前递归返回true
else if (!nodeLeft && !nodeRight) return true;
else if (nodeLeft->val != nodeRight->val) return false;
// 当前节点相等时,继续递归
bool isSameOutside = compare(nodeLeft->left, nodeRight->right);
bool isSameInside = compare(nodeLeft->right, nodeRight->left);
return isSameOutside && isSameInside;
}
本题采用递归法,对左侧子树采用左右中的遍历方式,对右侧子树采用右左中的遍历方式。若子树不对称,利用isSameOutside && isSameInside一直向上传递false的结果,直至返回最终结果。
- 递归函数的参数和返回值:要比较的是两个树,从而参数是左子树节点和右子树节点,返回值是bool类型;
- 确定终止条件:结点为空/结点都不空;
- 单层递归逻辑:单层递归逻辑用于处理左右结点都不为空,且数值相同的情况。
本题也可以采用迭代法实现,利用队列保存待比较的数值【注意入队顺序】,代码如下:
bool isSymmetric(TreeNode* root) {
if (root == NULL) return true;
queue<TreeNode*> leftAndRight;
leftAndRight.push(root->left);
leftAndRight.push(root->right);
while (!leftAndRight.empty()) {
TreeNode* left = leftAndRight.front();
leftAndRight.pop();
TreeNode* right = leftAndRight.front();
leftAndRight.pop();
// 左右都为空,继续判断
if (!left && !right) continue;
// 左不存在或右不存在,返回false
else if (!left || !right) return false;
// 左右都存在但不相等,返回false
else if (left->val != right->val) return false;
// 左右都存在且相等,入队左右孩子(注意顺序),继续下一次迭代
leftAndRight.push(left->left);
leftAndRight.push(right->right);
leftAndRight.push(left->right);
leftAndRight.push(right->left);
}
return true;
}
572. 另一棵树的子树
bool compare(TreeNode* nodeLeft, TreeNode* nodeRight) {
if (!nodeLeft && nodeRight) return false;
else if (nodeLeft && !nodeRight) return false;
// 没有左/右孩子节点时,当前递归返回true
else if (!nodeLeft && !nodeRight) return true;
else if (nodeLeft->val != nodeRight->val) return false;
// 当前节点相等时,继续递归
bool isSameOutside = compare(nodeLeft->left, nodeRight->left);
bool isSameInside = compare(nodeLeft->right, nodeRight->right);
return isSameOutside && isSameInside;
}
bool isSubtree(TreeNode* root, TreeNode* subRoot) {
if (!root && !subRoot) return true;
else if (!root && subRoot || root && !subRoot) return false;
else return compare(root, subRoot) || isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}
本题采用了双递归的方法,在《100.相同的树》的基础上,递归判断每个结点下的子树是否和目标子树相等。
这里没有采用KMP等算法,有兴趣的读者可以试一试。
111. 二叉树的最小深度
int minDepth(TreeNode* root) {
return GetDepth(root);
}
int GetDepth(TreeNode* node) {
// 终止条件:节点为空时,该层深度为0
if (!node) return 0;
int depthLeft = GetDepth(node->left);
int depthRight = GetDepth(node->right);
// 左空右不空,最小深度在右子树产生
if (!node->left && node->right) {
return 1 + depthRight;
}
// 右空左不空,最小深度在左子树产生
if (node->left && !node->right) {
return 1 + depthLeft;
}
// 左右都不空
return 1 + min(depthLeft, depthRight);
}
在递归逻辑设计阶段,要注意只有左右子树均为空的结点,才是最小深度层所在结点。
222. 完全二叉树的节点个数
int countNodes(TreeNode* root) {
if (!root) return 0;
TreeNode* left = root->left;
TreeNode* right = root->right;
int leftHeight = 0, rightHeight = 0;
// 左子树深度
while (left) {
left = left->left;
leftHeight++;
}
// 右子树深度
while (right) {
right = right->right;
rightHeight++;
}
// 如果是满二叉树
if (leftHeight == rightHeight) {
// 节点数量为2^h - 1;
return (2 << leftHeight) - 1;
}
return countNodes(root->left) + countNodes(root->right) + 1;
}
本题利用完全二叉树的性质,递归时需要注意以下两点:
- 若为满二叉树,可以直接使用2^h-1进行计算。其中次方项利用位运算符'<<'实现;
- 若不是满二叉树,分别递归求解左右子树,左子树和右子树总能递归成一个满二叉树(一个单独的结点也是满二叉树)
110. 平衡二叉树
bool isBalanced(TreeNode* root) {
if (!root) return true;
int leftDepth = getHeight(root->left);
int rightDepth = getHeight(root->right);
if (abs(leftDepth - rightDepth) > 1) {
return false;
}
else {
return isBalanced(root->left) && isBalanced(root->right);
}
}
int getHeight(TreeNode* node) {
// 递归终止条件
if (!node) return 0;
int leftDepth = getHeight(node->left);
int rightDepth = getHeight(node->right);
return max(leftDepth, rightDepth) + 1;
}
类似572.另一棵树的子树,本题采用了双递归。求深度适合用前序遍历,而求高度适合用后序遍历。
本题可以进行简化,在递归查找高度时就判断左右子树是否平衡:
bool isBalanced(TreeNode* root) {
return getHeight(root) == -1 ? false : true;
}
int getHeight(TreeNode* node) {
// 递归终止条件
if (!node) return 0;
int leftHeight = getHeight(node->left);
if (leftHeight == -1) return -1;
int rightHeight = getHeight(node->right);
if (rightHeight == -1) return -1;
// 查找高度时即可对是否平衡进行判断
return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight);
}
257. 二叉树的所有路径
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> res;
vector<int> record;
if (!root) return res;
traversal(root, res, record);
return res;
}
void traversal(TreeNode* node, vector<string>& res, vector<int>& record) {
// 前序遍历:
record.push_back(node->val);
// 递归终止逻辑
if (!node->left && !node->right) {
string sPath;
for (auto i : record) {
sPath += to_string(i);
sPath += "->";
}
sPath.erase(sPath.size() - 2, 2);
res.push_back(sPath);
return;
}
// 左侧节点回溯
if (node->left) {
traversal(node->left, res, record);
record.pop_back();
}
// 右侧节点回溯
if (node->right) {
traversal(node->right, res, record);
record.pop_back();
}
}
这里首先明确是前序遍历方式,递归的过程中蕴含着回溯的思想。额外使用了一个vector<int>数组记录了路径,满足递归终止条件是vector<string>记录结果,同时回溯后path删除上一个结点路径。
404. 左叶子之和
int sumOfLeftLeaves(TreeNode* root) {
if (!root) return 0;
int leftSum = sumOfLeftLeaves(root->left);
int rightSum = sumOfLeftLeaves(root->right);
int midSum = 0;
// 左叶子结点的判断依赖于父节点:父结点左孩子不为空,且其左孩子的左右孩子为空
if (root->left && !root->left->left && !root->left->right) {
midSum = root->left->val;
}
return leftSum + rightSum + midSum;
}
本题应首先明确什么是左叶子结点:首先是某父节点的左孩子,其次其左右孩子都为空。从而该节点的判断依赖于父节点。在递归时,应设计后序遍历的方式,这样回溯到最后中序,才能回到其父节点出,判断出是否满足左叶子结点的条件。
递归三要素:易写出终止条件;返回值为int,输入值为root,与原函数一致,从而可以直接使用原函数,而不需要再设计第二个函数;递归逻辑为左右中(后序),在最后回到中序时记录左孩子结点的值。
513. 找树左下角的值
int findBottomLeftValue(TreeNode* root) {
queue<TreeNode*> qt;
qt.push(root);
int res;
while (!qt.empty()) {
// 层序遍历,每层从右至左
if (qt.front()->right) qt.push(qt.front()->right);
if (qt.front()->left) qt.push(qt.front()->left);
if (!qt.front()->right && !qt.front()->left) res = qt.front()->val;
qt.pop();
}
return res;
}
本题采用层序遍历,每层遍历的方式是从右至左,从而最后一个元素就是最后一层最左侧的元素。
112. 路径总和
bool hasPathSum(TreeNode* root, int targetSum) {
if (!root) return false;
return traversal(root, targetSum - root->val);
}
bool traversal(TreeNode* node, int sum) {
// 递归返回条件:找到目标路径/没找到但遇到叶子结点
if (!node->left && !node->right && sum == 0) return true;
if (!node->left && !node->right) return false;
// 分别递归左右孩子
if (node->left) {
sum -= node->left->val;
// 递归的返回值为false时,即没找到但遇到叶子结点时,把false作为是否返回true的条件
if(traversal(node->left, sum)) return true;
sum += node->left->val;
}
if (node->right) {
sum -= node->right->val;
if (traversal(node->right, sum)) return true;
sum += node->right->val;
}
// 上述流程运行完还没有返回,说明没找到目标路径
return false;
}
本题采用递归方法,搞清楚递归返回条件和递归函数完全终止的条件。递归返回条件是子递归没找到目标,返回上一步继续进行递归的条件,对这道题而言是指if (!node->left && !node->right),即左右孩子都为空;而递归完全终止条件是函数中的return true判断。
如果觉得本题递归方法不好理解,也可以采用迭代法:
//迭代法
bool hasPathSum(TreeNode* root, int targetSum) {
if (!root) return false;
stack<pair<TreeNode*, int>> st;
st.push(make_pair(root, root->val));
while (!st.empty()) {
pair<TreeNode*, int> cur = st.top();
st.pop();
// 是叶子结点且满足条件时返回true
if (!cur.first->left && !cur.first->right && cur.second == targetSum) return true;
if (cur.first->right) {
st.push(make_pair(cur.first->right, cur.second + cur.first->right->val));
}
if (cur.first->left) {
st.push(make_pair(cur.first->left, cur.second + cur.first->left->val));
}
}
return false;
}
这里采用的是前序遍历。迭代法在需要回溯的情况下,需要用额外数组空间记录每个结点的路径长度。
113. 路径总和 II
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> res;
vector<int> path;
if (!root) return res;
path.push_back(root->val);
traversal(root, targetSum - root->val, path, res);
return res;
}
void traversal(TreeNode* node, int pathSum, vector<int>& path, vector<vector<int>>& res) {
if (!node) return;
// 到达根节点,判断是否满足要求并返回
if (!node->left && !node->right) {
if (pathSum == 0) res.push_back(path);
return;
}
// 递归并回溯!
if (node->left) {
path.push_back(node->left->val);
pathSum -= node->left->val;
traversal(node->left, pathSum, path, res);
pathSum += node->left->val;
path.pop_back();
}
if (node->right) {
path.push_back(node->right->val);
pathSum -= node->right->val;
traversal(node->right, pathSum, path, res);
pathSum += node->right->val;
path.pop_back();
}
return;
}
本题和257.二叉树的所有路径很相似,需要用到回溯的思想,在递归左右子树的if条件判断中实现递归并回溯。
注意root为空条件的处理;注意本题中递归函数中的res和path也可以写为Solution类的成员变量。
这里仍然给出迭代法的做法,但不推荐使用。一方面回溯的思想很符合这种题目,另一方面每个节点都要记录路径,耗费的空间较大。
//迭代法
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> res;
vector<int> path;
if (!root) return res;
stack<pair<TreeNode*, vector<int>>> st;
st.push(make_pair(root, vector<int>{ root->val }));
while (!st.empty()) {
pair<TreeNode*, vector<int>> cur = st.top();
st.pop();
// 是叶子结点且满足条件时记录结果
int sum = accumulate(cur.second.begin(), cur.second.end(), 0);
if (!cur.first->left && !cur.first->right && sum == targetSum) {
res.push_back(cur.second);
}
if (cur.first->right) {
vector<int> nPath = cur.second;
nPath.push_back(cur.first->right->val);
st.push(make_pair(cur.first->right, nPath));
}
if (cur.first->left) {
vector<int> nPath = cur.second;
nPath.push_back(cur.first->left->val);
st.push(make_pair(cur.first->left, nPath));
}
}
return res;
}
三、二叉树的修改与构造
106. 从中序与后序遍历序列构造二叉树
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return NULL;
return traversal(inorder, postorder);
}
TreeNode* traversal(vector<int>& inorder, vector<int>& postorder) {
// 递归返回条件
if (postorder.size() == 0) return NULL;
// 1.寻找后序最后一个元素,并建立根节点
int rootValue = postorder[postorder.size() - 1];
TreeNode* root = new TreeNode(rootValue);
// 2.如果只有一个元素,可以直接返回
if (postorder.size() == 1) return root;
// 3.在中序遍历数组中找到切割点
int index = 0;
for (; index < inorder.size(); index++) {
if (inorder[index] == root->val) break;
}
// 4.得到中序左数组(左子树)和中序右数组(右子树)
vector<int> inOrderLeft(inorder.begin(), inorder.begin() + index);
vector<int> inOrderRight(inorder.begin() + index + 1, inorder.end());
// 5.得到对应左子树和右子树的后序数组
postorder.resize(postorder.size() - 1);
vector<int> postOrderLeft(postorder.begin(), postorder.begin() + inOrderLeft.size());
vector<int> postOrderRight(postorder.begin() + inOrderLeft.size(), postorder.end());
// 6.继续递归
root->left = traversal(inOrderLeft, postOrderLeft);
root->right = traversal(inOrderRight, postOrderRight);
return root;
}
本题需要理解从中序+后序构造二叉树的原理,如下图所示(来源:代码随想录):
过程为以下几步:
-
1:如果数组大小为零的话,说明是空节点了。
-
2:如果不为空,那么取后序数组最后一个元素作为节点元素。
-
3:找到后序数组最后一个元素在中序数组的位置,作为切割点
-
4:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
-
5:切割后序数组,切成后序左数组和后序右数组
-
6:递归处理左区间和右区间
与之类似,可以完成105. 从前序与中序遍历序列构造二叉树
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.size() == 0 || inorder.size() == 0) return NULL;
int rootValue = preorder[0];
TreeNode* root = new TreeNode(rootValue);
int index;
for (; index < inorder.size(); index++) {
if (inorder[index] == rootValue) break;
}
vector<int> inOrderLeft(inorder.begin(), inorder.begin() + index);
vector<int> inOrderRight(inorder.begin() + index + 1, inorder.end());
vector<int> preOrderLeft(preorder.begin() + 1, preorder.begin() + 1 + inOrderLeft.size());
vector<int> preOrderRight(preorder.begin() + 1 + inOrderLeft.size(), preorder.end());
root->left = buildTree(preOrderLeft, inOrderLeft);
root->right = buildTree(preOrderRight, inOrderRight);
return root;
}
654. 最大二叉树
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
if (nums.size() == 0) return NULL;
// 寻找数组最大元素
int maxValue = nums[0];
int index = 0;
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > maxValue) {
index = i;
maxValue = nums[i];
}
}
// 建立新树(数组元素个数为1时可以直接返回)
TreeNode* root = new TreeNode(maxValue);
if (nums.size() == 1) return root;
// 递归创建左右子树
if (index > 0) {
vector<int> left(nums.begin(), nums.begin() + index);
root->left = constructMaximumBinaryTree(left);
}
if (index < nums.size() - 1) {
vector<int> right(nums.begin() + index + 1, nums.end());
root->right = constructMaximumBinaryTree(right);
}
return root;
}
本题为了提高效率,可以不在每次递归时都创建数组,而是定义一个递归函数:
TreeNode* traversal(vector<int>& nums, int left, int right)
这样每次递归时都在原数组上进行操作即可。 构造二叉树使用前序遍历,因为要先构造根节点。
617. 合并二叉树
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
// 递归终止条件
if (root1 == NULL) return root2;
if (root2 == NULL) return root1;
// 都不为空时在tree1上创建
root1->val += root2->val;
root1->left = mergeTrees(root1->left, root2->left);
root1->right = mergeTrees(root1->right, root2->right);
return root1;
}
本题的技巧在于可以直接在tree1上创建新树,以及递归终止条件应如何设置。采用的建树方式仍为前序遍历。
297. 二叉树的序列化与反序列化[*]
class Solution {
public:
// Encodes a tree to a single string.
string serialize(TreeNode* root) {
preorder_traversal(root);
return this->preorder_data;
}
// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
list<string> node_val;
string str;
for (auto s : data) {
if (s == ',') {
node_val.push_back(str);
str.clear();
}
else str.push_back(s);
}
return BuildPreTree(node_val);
}
private:
// 采用前序遍历的方式序列化和反序列化二叉树
string preorder_data;
// 用 , 作为分隔符,用 # 表示空指针 null
void preorder_traversal(TreeNode* node) {
if (!node) {
preorder_data.append("#,");
return;
}
preorder_data.append(to_string(node->val) + ",");
// 不需要判断是否存在左右孩子,若为空仍需要添加“#”,用来标记这是根节点
preorder_traversal(node->left);
preorder_traversal(node->right);
return;
}
TreeNode* BuildPreTree(list<string>& node_val) {
if (node_val.front() == "#") {
node_val.erase(node_val.begin());
return nullptr;
}
// 如果当前的元素为 None,则当前为空树
// 否则先解析这棵树的左子树,再解析它的右子树
TreeNode* root = new TreeNode(stoi(node_val.front()));
node_val.erase(node_val.begin());
root->left = BuildPreTree(node_val);
root->right = BuildPreTree(node_val);
return root;
}
};
本题需要掌握两点:1.何时能&如何只通过前序遍历构造一棵树;2.list容器的使用。
一般语境下,单单前序遍历结果是不能还原二叉树结构的,因为缺少空指针的信息,至少要得到前、中、后序遍历中的两种才能还原二叉树。但是这里的
node_val
列表包含空指针的信息,所以只使用node_val
列表就可以还原二叉树。
因此在构造前序遍历序列的时候不需要判断当前节点是否有左右孩子结点,若为叶结点,其后会跟两个"#"标记,在反序列化的递归调用过程中会作为返回标记。
四、二叉搜索树
BST 的中序遍历结果是有序的(升序)。
700. 二叉搜索树中的搜索
TreeNode* searchBST(TreeNode* root, int val) {
if (!root) return NULL;
if (root->val == val) return root;
else if (val < root->val) return searchBST(root->left, val);
else return searchBST(root->right, val);
return NULL;
}
递归过程中是否使用return:搜索一条边,找到节点立即返回时用return;遍历整棵树时不加return。
二叉搜索树也可以使用迭代法遍历,代码如下:
TreeNode* searchBST(TreeNode* root, int val) {
while (root != NULL) {
if (root->val > val) root = root->left;
else if (root->val < val) root = root->right;
else return root;
}
return NULL;
}
98. 验证二叉搜索树[*]
// 记录前一个节点
TreeNode* pre = NULL;
bool isValidBST(TreeNode* root) {
if (!root) return true;
// 中序遍历,先遍历左侧节点
bool left = isValidBST(root->left);
// 判断左孩子和根结点的大小关系 || 根节点和右孩子的大小关系
if (pre && pre ->val >= root->val) return false;
pre = root;
bool right = isValidBST(root->right);
return left && right;
}
二叉搜索树若按照中序遍历,会得到一个有序数组。因此递归时采用中序遍历,用pre记录前一个结点,前一个结点小于下一个结点才满足要求。
530. 二叉搜索树的最小绝对差
int res = INT32_MAX;
TreeNode* pre;
int getMinimumDifference(TreeNode* root) {
traversal(root);
return res;
}
void traversal(TreeNode* node) {
if (!node) return;
traversal(node->left);
if (pre) {
res = node->val - pre->val < res ? node->val - pre->val : res;
}
pre = node;
traversal(node->right);
}
本题仍采用中序遍历的方式,递归求解前序遍历得到的有序数组两数之间的差值。要记住这个前序遍历并记录上一个结点元素的框架。
如采用迭代法,要学会使用中序遍历的迭代法,也要使用一个指针记录上一个结点。
int getMinimumDifference(TreeNode* root) {
stack<TreeNode*> st;
int res = INT32_MAX;
TreeNode* pre = NULL;
TreeNode* cur = root;
// 循环的判断是栈是否为空,或当前指针是否为空
while (cur != NULL || !st.empty()) {
// 遍历左结点至最底层
if (cur) {
st.push(cur);
cur = cur->left;
}
// 左孩子为空后开始输出,并记录栈底元素
else {
cur = st.top();
st.pop();
if (pre) res = cur->val - pre->val < res ? cur->val - pre->val : res;
pre = cur;
// 弹出的元素若存在右孩子,则对其访问
cur = cur->right;
}
}
return res;
}
501. 二叉搜索树中的众数
vector<int> res;
vector<int> findMode(TreeNode* root) {
traversal(root);
return res;
}
TreeNode* pre = NULL;
int maxCnt = 0;
int curCnt = 0;
void traversal(TreeNode* node) {
if (!node) return;
traversal(node->left);
// 第一个结点也要记录
if (pre == NULL) {
curCnt = 1;
}
else if (node->val == pre->val) {
curCnt++;
}
else {
curCnt = 1;
}
pre = node;
// 用curCnt的数值变化来体现一个元素是否被访问完毕
if (curCnt == maxCnt) res.push_back(node->val);
if (curCnt > maxCnt) {
res.clear();
res.push_back(node->val);
maxCnt = curCnt;
}
traversal(node->right);
}
中序遍历得到的序列是有序的,类似双指针法,记录相同元素的数量即可。需要注意的是遇到不同元素时,要把curCnt重置为1,而非重置为0。
五、二叉树的公共祖先
236. 二叉树的最近公共祖先[*]
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 递归返回条件
if (!root || root == p || root == q) return root;
// 采用后序遍历,隐含着回溯,自下而上查找公共结点
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
// 处理left和right的逻辑
if (left != NULL && right != NULL) return root;
if (left == NULL && right != NULL) return right;
else if (left != NULL && right == NULL) return left;
else { // (left == NULL && right == NULL)
return NULL;
}
}
首先,查找公共祖先较为合适的方式是后序遍历,后序遍历天然隐含着回溯的思想。
在写递归函数时,函数的返回值类型和参数易判断;递归终止条件为if (!root || root == p || root == q),即若该节点是要查找的p或q结点就可返回!
找到一个节点是p或者q的时候,直接返回当前节点,无需继续递归子树。如果接下来的遍历中找到了后继节点满足第一种情况则修改返回值为后继节点,否则,继续返回已找到的节点即可。
同时再总结一下递归时是否返回,不返回时需要暂存逻辑信息,后续进行判断。
搜索结点,找到即返回:
if (递归函数(root->left)) return ; if (递归函数(root->right)) return ;
搜索整棵树,找到暂不返回:
left = 递归函数(root->left); right = 递归函数(root->right); left与right的逻辑处理;
后序遍历的一个过程如下(来源:代码随想录):
从这张图可以说明递归时后序的逻辑应当如何处理,以一个父节点下有两个左右目标孩子为例【写递归逻辑时都可以以最简单的为例】:
- 若左右返回值都不为空,说明root结点就是公共结点,不断向上返回即可;
- 若左右返回值都为空,说明没有找到,继续向上回溯查找;
- 比较难理解的是只找到了一个目标结点的情况。如图所示,right不为空时,目标节点“7”会一直返回上去。由于是后序回溯,若遇到上层的目标节点,结果会被覆盖。因此最终返回的是公共祖先节点。
235. 二叉搜索树的最近公共祖先
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root) return root;
// 根节点在区间右侧,向左遍历
if (root->val > p->val && root->val > q->val) {
TreeNode* left = lowestCommonAncestor(root->left, p, q);
if (left) return left;
}
if (root->val < p->val && root->val < q->val) {
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (right) return right;
}
return root;
}
本题可以利用二叉搜索树的性质,不需要采用自下而上的后序遍历,而可以采用前序遍历方式(不存在处理根节点的逻辑),一旦根节点的值落入[p.val, q.val] / [q.val, p.val]内,就说明找到了目标公共祖先节点。
本题利用迭代法实现也非常简单:
while (root) {
if (root->val > p->val && root->val > q->val) {
root = root->left;
}
else if (root->val < p->val && root->val < q->val) {
root = root->right;
}
else {
return root;
}
}
return NULL;
六、二叉搜索树的修改与建立
701. 二叉搜索树中的插入操作
TreeNode* insertIntoBST(TreeNode* root, int val) {
TreeNode* cur = root;
TreeNode* pre = NULL;
while (cur) {
pre = cur;
if (val < cur->val) cur = cur->left;
else cur = cur->right;
}
if(!pre) return new TreeNode(val);
if (val < pre->val) pre->left = new TreeNode(val);
else pre->right = new TreeNode(val);
return root;
}
首先使用迭代法,这里需要注意的是需要在程序中添加root为空的处理逻辑。通过利用pre记录上一个结点,来完成新结点的的建立。
本题若使用递归法,可以通过递归函数的返回值完成子节点的赋值:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (!root) return new TreeNode(val);
if (val < root->val) {
root->left = insertIntoBST(root->left, val);
}
if (val > root->val) {
root->right = insertIntoBST(root->right, val);
}
return root;
}
450. 删除二叉搜索树中的节点
TreeNode* deleteNode(TreeNode* root, int key) {
// 1.没有找到需删除的节点
if (!root) return NULL;
if (root->val == key) {
// 2.目标结点左右子树均为空
if (!root->left && !root->right) {
delete(root);
return NULL;
}
// 3.目标节点左孩子不为空,右孩子为空
else if (root->left && !root->right) {
TreeNode* left = root->left;
delete(root);
// 返回左孩子为根节点
return left;
}
// 4.目标节点右孩子不为空,左孩子为空
else if (!root->left && root->right) {
TreeNode* right = root->right;
delete(root);
// 返回左孩子为根节点
return right;
}
// 5.左右孩子都不空,将左子树移至右子树最左侧结点下
else {
TreeNode* right = root->right;
TreeNode* temp = right;
while (temp->left) {
temp = temp->left;
}
temp->left = root->left;
delete(root);
return right;
}
}
if (root->val > key) {
root->left = deleteNode(root->left, key);
}
if (root->val < key) {
root->right = deleteNode(root->right, key);
}
return root;
}
删除二叉搜索树的结点,有以下五种情况:
- 1:没找到删除的节点,遍历到空节点直接返回了
- 找到删除的节点:
- 2:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 3:删除节点左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 4:删除节点右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 5:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
其中第五种情况如下图所示(来源:代码随想录):
669. 修剪二叉搜索树[*]
TreeNode* trimBST(TreeNode* root, int low, int high) {
if (!root) return NULL;
if (root->val > high) {
auto left = trimBST(root->left, low, high);
return left;
}
if (root->val < low) {
auto right = trimBST(root->right, low, high);
return right;
}
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
本题思路类似于后序遍历, 修剪过程虽然过程需要重构二叉树,但没有想象中的那么复杂。
如上图所示(来源:代码随想录),修建区间为[1,3]。发现不符合的元素处理逻辑如下:
- 【左】若发现小于最小值的结点,向右遍历,并返回右孩子;
- 【右】若发现大于最大值的结点,向左遍历,并返回左孩子;
- 节点为空时就返回空【这也是递归终止条件】;
- 【中】符合要求时,继续递归左右子树,通过递归函数的返回值来移除节点。因此要理解最后对于root->right和root->left的赋值。
538. 把二叉搜索树转换为累加树
TreeNode* convertBST(TreeNode* root) {
int sum = 0;
traversal(root, sum);
return root;
}
void traversal(TreeNode* node, int& sum) {
if (!node) return;
traversal(node->right, sum);
node->val += sum;
sum = node->val;
traversal(node->left, sum);
}
要有把二叉搜索树转换为有序数组的意识!
本题可以判断出来应从“有序数组的最大值”开始操作数组,从而应采用反中序遍历的方式,不断累加即可。
七、平衡二叉搜索树
108. 将有序数组转换为二叉搜索树[*]
TreeNode* sortedArrayToBST(vector<int>& nums) {
return traversal(nums, 0, nums.size() - 1);
}
TreeNode* traversal(vector<int>& nums, int left, int right) {
// 递归终止条件
if (left > right) return NULL;
int mid = left + (right - left) / 2;
TreeNode* newNode = new TreeNode(nums[mid]);
newNode->left = traversal(nums, left, mid - 1);
newNode->right = traversal(nums, mid + 1, right);
return newNode;
}
创建树时采用前序遍历,因为要先有结点才有左右子树。
不要将本题想得过于复杂,要通过有序数组构造平衡二叉搜索树,只需要每次从数组中取出最中间的结点作为根节点,之后依据分治思想,递归构建左右子树即可。
总结
-
涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。
-
求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。
-
求二叉搜索树的属性,一定是中序,利用二叉搜索树的性质。
二叉树问题的两种解题思路:
- 遍历一遍二叉树得出答案【回溯】:如果可以,用一个
traverse
函数配合外部变量来实现。- 通过分解问题计算出答案【动态规划】:写出这个递归函数的定义,并充分利用这个函数的返回值。