目录
二叉搜索树的最小绝对差
题干
思路和代码
方法一:求中序序列
方法二:递归法+双指针法
方法三:迭代法+双指针法
二叉搜索树中的众数
题干
思路和代码
方法一:求中序序列
方法二:递归法+双指针+中序遍历
编辑
方法三:迭代法+双指针+中序遍历
二叉树的公共祖先
题干
思路和代码
编辑
递归法:后序遍历
递归法:优化
总结:二叉搜索树
二叉搜索树的最小绝对差
题干
题目:给你一个二叉搜索树的根节点 root ,返回树中任意两不同节点值之间的最小差值 。差值是一个正数,等于两值之差的绝对值。
注意:0 <= Node.val <= 10^5(节点值非负数)
链接:. - 力扣(LeetCode)
思路和代码
题目是要求 “任意” 两个结点的差值,但问题在于如何遍历任意两个结点?
二叉搜索树的结点是有序的,最小差值只会出现在中序序列的两个相邻元素之间。
方法一:求中序序列
先求二叉搜索树的中序序列数组,再遍历数组从中找差值最小的。而二叉搜索树的结点是有序的,最小差值只会出现在两个相邻元素之间,因此只需要遍历一次数组即可。
class Solution {
public:
void inorder(TreeNode* root, vector<int> &nums){
if (root == nullptr) return;
inorder(root->left,nums);
nums.push_back(root->val);
inorder(root->right,nums);
}
int getMinimumDifference(TreeNode* root) {
vector<int> nums;
inorder(root,nums);
int minValue = INT_MAX; // 记录最小差值
// 遍历中序序列数组,只需要比较相邻两个元素即可
for (int i = 0; i < nums.size()-1; ++i) {
if (nums[i+1] - nums[i] < minValue){
minValue = nums[i+1] - nums[i];
}
}
return minValue;
}
};
方法二:递归法+双指针法
之前的方法是先将中序序列数组求出,再遍历数组,这样相当于遍历了两次二叉树的所有结点。但其实在中序遍历的过程中就可以直接记录最小差值,这样就只需要遍历一次二叉树的所有结点,省时间。
问题是如何在中序遍历的过程中记录最小差值?
我们知道最小差值只会出现在中序序列的两个相邻元素之间,在数组里可以直接使用下标访问相邻元素,而在树中,我们需要设置两个指针,一个指针 cur 指向当前遍历的结点,另一个指针 pre 指向 cur 的上一个结点,这样 pre 和 cur 所指的两个元素就是相邻的。
-
递归参数和返回值:参数是传入的当前结点,无返回值。
-
递归结束的条件:若传入的结点为空,说明已经遍历到最底部,则直接返回。
-
递归顺序:按照 “左中右” 的顺序,我们需要先不断遍历左子树找到最左结点。当 cur 指向最左结点时,pre 暂时为空,则需要让 pre = cur,待 cur 回溯到父节点后,才能比较 pre 和 cur 的差值 与 最小差值,最后再遍历右子树。
class Solution {
public:
int minValue = INT_MAX; // 记录最小差值
TreeNode* pre = nullptr; // 记录上一个结点指针
// 在每一次递归中都要更新,因此要定义为全局变量
void inorder(TreeNode* cur){
// 当前结点为空
if (cur == nullptr) return;
inorder(cur->left); // 遍历左子树
if (pre != nullptr){
minValue = min(abs(pre->val - cur->val),minValue);
}
pre = cur;
inorder(cur->right); // 遍历右子树
}
int getMinimumDifference(TreeNode* root) {
inorder(root);
return minValue;
}
};
方法三:迭代法+双指针法
思路和方法二一样,只是将递归中序遍历换成了迭代法。
class Solution {
public:
int getMinimumDifference(TreeNode* root) {
stack<TreeNode*> tmpNode; // 存储遍历过的序列
TreeNode* cur = root;
TreeNode* pre = nullptr; // 记录上一个结点
int minValue = INT_MAX;
while (cur || !tmpNode.empty()){
if (cur == nullptr){
// 已经找到最左结点
cur = tmpNode.top();
tmpNode.pop();
// ....计算 pre 和 cur 的差值
if (pre != nullptr){
minValue = min(abs(pre->val - cur->val),minValue);
}
pre = cur;
// ....
cur = cur->right; // 遍历右子树
} else{
tmpNode.push(cur); // 暂存插入的结点
cur = cur->left; // 不断遍历左子树直到找到最左结点
}
}
return minValue;
}
};
二叉搜索树中的众数
题干
题目:给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。
如果树中有不止一个众数,可以按 任意顺序 返回。
假定 BST 满足如下定义:
-
结点左子树中所含节点的值 小于等于 当前节点的值
-
结点右子树中所含节点的值 大于等于 当前节点的值
-
左子树和右子树都是二叉搜索树
链接:. - 力扣(LeetCode)
思路和代码
方法一:求中序序列
由于二叉搜索树的有序性,可以先求出其中序序列数组,再遍历数组统计出现频率最高的元素。
注意,在遍历数组统计元素次数时,我们使用了双指针法,即快慢指针法,用慢指针 slow 记录当前元素出现的第一个位置,快指针 fast 记录当前元素出现的最后一个位置,这样当前元素出现的次数就等于 fast - slow + 1。 每统计完一个元素的次数才更新结果集。
class Solution {
public:
// 求中序序列
void inorder(TreeNode* node, vector<int> &nums){
if (node == nullptr) return;
inorder(node->left,nums);
nums.push_back(node->val);
inorder(node->right,nums);
}
// 找众数
vector<int> findMode(TreeNode* root) {
vector<int> nums; // 存储中序序列
inorder(root,nums); // 求中序序列
int maxCount = 0; // 记录最大出现次数
vector<int> result; // 存储众数结果集
// 双指针法遍历数组,统计每个数出现的频率
int slow = 0; // slow 指向相同元素的起始位置
int fast; // fast 指向相同元素的末尾位置
for (fast = 0; fast < nums.size(); ++fast) {
if (fast == nums.size()-1 || nums[slow] != nums[fast+1]){
// 当 fast 遍历到数组末尾,或者 fast 的下一个元素已经是不同元素时
// 说明当前元素已经遍历到末尾
int count = fast-slow+1; // 统计当前元素的出现次数
if (maxCount < count){
maxCount = count;
result.clear(); // 当出现频率更高的元素时,结果集数组要先清空
result.push_back(nums[slow]);
} else if (maxCount == count){
result.push_back(nums[slow]);
}
slow = fast+1;
}
}
return result;
}
};
方法二:递归法+双指针+中序遍历
同样是要用双指针法,一个指针 cur 指向当前结点,另一个指针 pre 指向上一个结点,用 curCount 统计当前结点出现的次数,用 maxCount 统计最大出现次数,用 result 数组统计众数结果集。这里的双指针法和上一个方法中在有序数组里的双指针法不同。
在上一个方法有序数组中,每统计完一个元素的次数才更新 result 。但是在递归遍历中,只要当前结点的出现次数大于或等于 maxCount了,就立即更新 result,哪怕此时当前节点的出现次数还没统计完毕。
-
递归参数和返回值:参数是传入的当前结点 cur 以及结果集数组 result (在递归过程中不断更新 result)。无返回值。
-
递归结束条件:当 cur 为空,要向上层回溯,递归结束。
-
递归顺序:根据中序遍历 “左中右” 的顺序,先遍历左子树统计元素次数,再回到中间结点处理,而后遍历右子树统计元素次数。
class Solution {
public:
int maxCount = 0; // 记录最大出现次数
int curCount = 0; // 记录当前元素的出现次数
TreeNode* pre = nullptr; // 记录上一个元素
void count(TreeNode* cur, vector<int> &result){
if (cur == nullptr) return;
count(cur->left,result); // 遍历左子树
if (pre == nullptr){ // 如果 pre 为空,说明 cur 为中序序列的第一个结点,此时 count 为 1
curCount = 1;
} else if (pre->val == cur->val){
curCount++;
} else {
curCount = 1;
}
if (curCount > maxCount){
maxCount = curCount;
result.clear();
result.push_back(cur->val);
} else if (curCount == maxCount){
result.push_back(cur->val);
}
pre = cur;
count(cur->right,result); // 遍历右子树
}
vector<int> findMode(TreeNode* root) {
vector<int> result;
count(root,result);
return result;
}
};
方法三:迭代法+双指针+中序遍历
思路和上述方法相同,都是在中序遍历过程中记录众数,只不过递归中序遍历改成了迭代中序遍历。
class Solution {
public:
vector<int> findMode(TreeNode* root) {
vector<int> result;
stack<TreeNode*> tmpNode;
TreeNode* pre = nullptr; // 记录 cur 的上一个结点
TreeNode* cur = root;
int maxCount = 0; // 记录最大出现次数
int count = 0; // 记录当前结点的出现次数
while (cur != nullptr || !tmpNode.empty()){
if (cur != nullptr){
tmpNode.push(cur);
cur = cur->left; // 不断向左子树遍历,找到最左边结点
} else{
cur = tmpNode.top();
tmpNode.pop();
if (pre == nullptr){
count = 1; // 如果 pre 为空,说明当前结点是中序序列的第一个结点,count = 1
} else if (pre->val == cur->val){
count++; // 当前结点和之前的结点相同,则结点出现次数递增
} else{
// 当前结点和之前的结点不同,所以 count 更新为 1
count = 1;
}
// 每遍历到一个结点就比较一次
// 比较当前结点的出现次数和最大出现频率
if (count > maxCount){
maxCount = count;
result.clear();
result.push_back(cur->val);
} else if (count == maxCount){
result.push_back(cur->val);
}
pre = cur;
cur = cur->right; // 遍历右子树
}
}
return result;
}
};
二叉树的公共祖先
题干
题目:给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
说明:
-
所有节点的值都是唯一的。
-
p、q 为不同节点且均存在于给定的二叉树中。
链接:. - 力扣(LeetCode)
思路和代码
公共祖先的情况有两种:
-
情况一:两个结点不在同一棵子树中,则需要回溯找到两个结点的公共祖先。
-
情况二:一个结点在另一个结点的子树中,则其中一个结点即为最近公共祖先。
递归法:后序遍历
Q:为什么是后序遍历?
先遍历左右子树,看当前子树是否有p或q结点,并把情况返回给父节点(需要收集左右子树的信息)。当左右子树或当前父节点都存在 p、q ,则说明已经找到公共祖先,且由于是后序遍历,此公共祖先肯定是最近的,符合题意。
-
递归参数和返回值:递归参数是当前结点 node 和要找的 p、q 结点;返回值是 bool 型变量,返回以当前 node 结点为根的子树是否有 p 或 q。
-
递归结束的条件:当 node 结点为空,说明没有 p、q,直接返回false。
-
递归顺序:先递归查询左右子树是否有 p、q,此时有多种情况。
-
左右子树都返回 true,说明左右子树都有 p、q,则当前的父节点即公共祖先(情况一),直接返回 true;
-
左右子树一个 true、一个 false,说明只有其中一棵子树有p或q,则再判断当前父结点是否为 p 或 q。
-
若父节点为 p 或 q,则父节点就是公共祖先(情况二),直接返回 true。
-
若父节点不是 p 或 q,那么仍需要向上层返回,继续寻找公共祖先,不做处理。
-
-
左右子树都为 false,再判断当前父节点是否为 p 或 q,若是,直接返回 true。
-
其余情况均没有找到公共祖先,都需要继续往上返回,返回 leftTree||rightTree(只要有一个子树有 p 或 q,就为 true)。
(注: leftTree 表示左子树是否有 p 或 q,rightTree 表示右子树是否有 p 或 q)
-
class Solution {
public:
TreeNode* ancestor = nullptr; // 记录最近公共祖先,全局变量
bool findAncestor(TreeNode* node, TreeNode* p, TreeNode* q){
if (node == nullptr) return false;
bool leftTree = findAncestor(node->left,p,q);
if (ancestor != nullptr) return true; // 只要找到了公共祖先,就直接返回 true
bool rightTree = findAncestor(node->right,p,q);
if (ancestor != nullptr) return true;
if (leftTree && rightTree && ancestor == nullptr){
// 左右子树都为 true,则都有 p、q
ancestor = node;
return true;
} else if ((leftTree && !rightTree) ||
(!leftTree && rightTree) ){
// 左子树有,右子树没有;或左子树没有,右子树有
if (ancestor == nullptr && (node == p || node == q)){
ancestor = node;
return true;
}
} else{
// 左右子树都没有,但如果当前结点有,直接返回 true
if (node == p || node == q) return true;
}
return leftTree||rightTree;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
bool result = findAncestor(root,p,q);
return ancestor;
}
};
递归法:优化
思路和之前的方法是一样的,只不过我们在递归返回值上做出了调整。由于我们需要记录公共祖先,则可以设定返回值为树的结点。我们在上一个方法中,设定 bool 型变量记录子树中是否有 p、q 结点。在这个方法里,当子树有 p、q 结点时,我们直接返回结点。如果返回结点仍为空,说明还没有找到 p、q。
-
递归参数和返回值:参数是根节点,要查询的 p、q 结点。返回值是结点。
-
递归结束条件:当传入的结点为空,说明遍历到最底部,要想上层返回,递归结束。
-
递归顺序:先遍历左子树查询 p、q,再遍历右子树查询 p、q,并记录左右子树返回的结点情况。
-
先判断当前结点 root 是否为 p或q,若是,则直接返回 root。(为什么?如下解释)
-
当结点 root 为 p 或 q,子树若有 p或q,root 是公共祖先,要返回 root。
-
当结点 root 为 p 或 q,子树没有 p或q,要返回 root,表示找到了 p、q。
-
-
若当前结点 root 不是 p 或 q,则有多种情况。
-
左子树和右子树返回的都不是空指针,说明左右子树都有 p、q,当前结点 node 即为公共祖先,直接返回。
-
左子树返回空,右子树返回非空,说明右子树有 p 或 q,当前结点 node 又不是 p、q,直接返回右子树的返回结点。
-
右子树返回空,左子树返回非空,说明左子树有 p 或 q,当前结点 node 又不是 p、q,直接返回左子树的返回结点。
-
左右子树都返回空,说明一直没有找到 p、q,返回空指针 nullptr。
-
-
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr) return nullptr;
TreeNode* leftTree = lowestCommonAncestor(root->left,p,q);
TreeNode* rightTree = lowestCommonAncestor(root->right,p,q);
// 只要当前结点是 p 或 q,无论子树里有没有 p、q,都直接返回 node
if (root == p || root == q) return root;
// 如果当前结点不是 p 或 q
if (leftTree != nullptr && rightTree != nullptr){
// 左右子树均找到了 p、q,则当前结点 node 就是公共祖先
return root;
} else if (leftTree == nullptr && rightTree != nullptr){
// 左子树没有 p、q,右子树有,直接返回右子树的 p 或 q
return rightTree;
} else if (leftTree != nullptr && rightTree == nullptr){
// 左子树没有 p、q,右子树有,直接返回左子树的 p 或 q
return leftTree;
}
// 以上情况均不满足,说明暂时都没有找到 p、q,返回空指针
return nullptr;
}
};
总结:二叉搜索树
遇到在二叉搜索树上求最值、求差值问题,其实都可以先把二叉搜索树转化为一个有序数组,在有序数组上求解问题会变得简单很多。
另一种方法是在中序遍历的过程中使用双指针法,即在递归遍历中用一个指针 cur 指向当前结点,用一个指针 pre 指向上一个遍历过的结点。