108.将有序数组转换为二叉搜索树

根据数组构造一棵二叉树。本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
因为是有序数组构造二叉搜索树,寻找分割点就比较容易了。分割点就是数组中间位置的节点。
如果数组长度为偶数,中间节点有两个,取哪一个?
取哪一个都可以,只不过构成了不同的平衡二叉搜索树。
例如:输入:[-10,-3,0,5,9]
如下两棵树,都是这个数组的平衡二叉搜索树,取左边元素就是树1,取右边元素就是树2:

递归三部曲:
1、确定递归函数返回值及其参数
cpp
// 左闭右闭区间[left, right]
TreeNode* traversal(vector<int>& nums, int left, int right)
nums 是传入的有序整数数组,left 和 right 是数组索引,用于表示当前递归处理的子数组范围。
2、确定递归终止条件
这里定义的是左闭右闭的区间,所以当区间 left > right的时候,就是空节点了。
3、确定单层递归的逻辑
① 首先取中间位置,然后开始以中间位置的元素构造节点:
TreeNode* root = new TreeNode(nums[mid]);。
② 接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。
③ 最后返回root节点。
一定要明确区间边界,本题取左闭右闭!!!
初始调用: traversal(nums, 0, nums.size() - 1) 表示整个数组范围,从第一个元素(索引 0)到最后一个元素(索引 nums.size()-1)都参与构建。
递归分割:
取中间索引mid = left + (right - left) / 2,用 nums[mid] 构建当前根节点。
左子树的构建范围是 [left, mid-1](包含 left 到 mid-1 的所有元素)。
右子树的构建范围是 [mid+1, right](包含 mid+1 到 right 的所有元素)。
终止条件: 当 left > right 时,表示当前区间没有任何元素(例如左子区间 [left, mid-1] 中如果 left 比 mid-1 大,说明没有元素),返回 nullptr。
核心代码:
cpp
class Solution {
private:
TreeNode* traversal(vector<int>& nums, int left, int right) {
if (left > right) return nullptr;
int mid = left + ((right - left) / 2);
TreeNode* root = new TreeNode(nums[mid]);
root->left = traversal(nums, left, mid - 1);
root->right = traversal(nums, mid + 1, right);
return root;
}
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
TreeNode* root = traversal(nums, 0, nums.size() - 1);
return root;
}
};
注意:
在调用traversal的时候传入的left和right为什么是0和nums.size() - 1,因为定义的区间为左闭右闭。
1、注意是nums[mid],是数组元素!!不是mid。
98. 验证二叉搜索树

思路与解法
中序遍历下,输出的二叉搜索树节点的数值是有序序列。
有了这个特性,验证二叉搜索树,就相当于变成了判断中序序列是不是递增的即可。
解法1:
可以递归中序遍历将二叉搜索树转变成一个数组,代码如下:
cpp
vector<int> vec;
void traversal(TreeNode* root) {
if (root == NULL) return;
traversal(root->left);
vec.push_back(root->val); // 将二叉搜索树转换为有序数组
traversal(root->right);
}
然后只要比较一下,这个数组是否是有序的,注意二叉搜索树中不能有重复元素。
cpp
traversal(root);
for (int i = 1; i < vec.size(); i++) {
// 注意要小于等于,搜索树里不能有相同元素
if (vec[i] <= vec[i - 1]) return false;
}
return true;
整体代码:
cpp
class Solution {
public:
void traversal(TreeNode* root, vector<int>& vec){
if(root==nullptr) return;
traversal(root->left,vec);
vec.push_back(root->val);
traversal(root->right,vec);
}
bool isValidBST(TreeNode* root) {
vector<int> result;
traversal(root,result);
for(int i=1;i<result.size();i++){
if(result[i]<=result[i-1]) return false;
}
return true;
}
};
【注】
1、注意i从1开始,因为需要比较当前元素vec[i]与前一个元素vec[i-1],检查该序列是否严格递增 。
2、注意要小于等于,搜索树里不能有相同元素!!
解法2:
以上代码中,我们把二叉树转变为数组来判断,是最直观的,但其实不用转变成数组,可以在递归遍历的过程中直接判断是否有序。这里不讲了。
230. 二叉搜索树中第K小的元素

核心代码:
cpp
class Solution {
public:
void traversal(TreeNode* root, vector<int>& vec){
if(root==nullptr) return;
traversal(root->left,vec);
vec.push_back(root->val);
traversal(root->right,vec);
}
int kthSmallest(TreeNode* root, int k) {
vector<int> result;
traversal(root,result);
return result[k-1];
}
};
【注】
1、与上一题一样,递归中序遍历将二叉搜索树转变成一个数组后,return result[k-1];即可。
199. 二叉树的右视图

思路与解法
从右侧看二叉树,我们看到的实际上是每一层最右边的节点。因此,问题转化为按层遍历二叉树,并记录每一层最后一个节点。
因此可以使用广度优先搜索方法(BFS),使用队列进行层序遍历。
对于每一层,记录该层最后一个节点的值。
核心代码:
cpp
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
vector<int> results;
if(root==nullptr) return results;
queue<TreeNode*> que;
que.push(root);
while(!que.empty()){
int size = que.size();
for(int i=0;i<size;i++){
TreeNode* node = que.front();
que.pop();
if(i==size-1) results.push_back(node->val);
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
}
return results;
}
};
【注】
1、 for(int i=0;i<size;i++){
是size,一定不能写成que.size( )了!!!
114. 二叉树展开为链表

思路与解法
使用迭代法,每次操作中,我们将左子树整体插入到当前节点和原右子树之间。因为前序遍历的顺序是:根 → 左子树 → 右子树。所以将左子树移到右边,并将原右子树接在左子树后面,正好保持了前序顺序。
步骤(假设当前节点为 cur,且其左子树非空):
1、找到左子树 的最右节点:这个节点是左子树中"最右边"的节点,也就是在左子树的前序遍历中最后一个被访问的节点。
2、将原右子树接到左子树最右节点的右边:这样原右子树的所有节点就会在左子树之后被访问。
3、将左子树整体移到当前节点的右边:即 cur->right = cur->left,然后 cur->left = nullptr。
4、移动 cur 指针:令 cur = cur->right(left已经移动过了,为空了),继续处理下一个节点。


上图是示例演示,中间是cur=1时的处理(找到4,然后把右子树5-6移动到4后面),右边是cur=2时的处理,当cur运动到3开始,都没有左子树,直接移动到最后结束。
核心代码:
cpp
class Solution {
public:
void flatten(TreeNode* root) {
TreeNode* cur=root;
while(cur){
if(cur->left){
TreeNode *pre = cur->left;
while(pre->right){
pre=pre->right;
}
pre->right = cur->right;
cur->right = cur->left;
cur->left = nullptr;
}
cur = cur->right;
}
}
};
【注】
1、注意if和while
if(cur->left){
存在左子树才需要进行指针调整,再定义pre=cur->left
while(pre->right){:定位左子树的最后一个节点
105. 从前序与中序遍历序列构造二叉树

先看从中序与后序遍历序列构造二叉树。
依次确定,首先要确定根节点 。
后序左右中,因此最后一个元素一定是中节点。
以后序数组的最后一个元素为切割点,先切中序数组(根据中分离出左---右---中),根据中序数组,反过来再切后序数组(此时后续的左右中的右也是左右中的顺序)。一层一层切下去,每次后序数组最后一个元素就是节点元素。
流程如图:

如上图,先看后序,最后一个数字是3,所以根节点是3,再用3去分割中序(通过索引从0开始找),9是左,15 20 7是右,然后再看后序,按照中序分割后得到的左右的元素个数(左1个,右3个)分离出后序的左和右,以右子树为例,15 7 20也是按后序排列,因此20是右子树的根节点。依此类推。
那么代码应该怎么写呢?
说到一层一层切割,就应该想到了递归。
来看一下一共分几步:
第一步:如果数组大小为零的话,说明是空节点了。
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
第五步:切割后序数组(根据上一步的中序左数组和中序右数组各自的元素个数 ),切成后序左数组和后序右数组
第六步:递归处理左区间和右区间
不难写出如下代码:(先把框架写出来)
cpp
TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
// 第一步
if (postorder.size() == 0) return NULL;
// 第二步:后序遍历数组最后一个元素,就是当前的中间节点
int rootValue = postorder[postorder.size() - 1];
TreeNode* root = new TreeNode(rootValue);
// 叶子节点,提前终止递归,直接返回当前叶子节点
if (postorder.size() == 1) return root;
// 第三步:找切割点
int Index;
for (Index = 0; Index < inorder.size(); Index++) {
if (inorder[Index] == rootValue) break;
}
// 第四步:切割中序数组,得到 中序左数组和中序右数组
// 第五步:切割后序数组,得到 后序左数组和后序右数组
// 第六步
root->left = traversal(中序左数组, 后序左数组);
root->right = traversal(中序右数组, 后序右数组);
return root;
}
难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。
此时应该注意确定切割的标准,是左闭右开,还有左开右闭,还是左闭右闭,这个就是不变量,要在递归中保持这个不变量。
在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭右闭,必然乱套!
在数组中强调过循环不变量的重要性,在二分查找以及螺旋矩阵的求解中,坚持循环不变量非常重要,本题也是。
首先要切割中序数组,为什么先切割中序数组呢?
切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组的,所以必须先切割中序数组。
中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割,如下代码中我坚持左闭右开的原则:
cpp
// 找到中序遍历的切割点
int Index;
for (Index = 0; Index < inorder.size(); Index++) {
if (inorder[Index] == rootValue) break;
}
// 左闭右开区间:[0, Index)
vector<int> leftInorder(inorder.begin(), inorder.begin() + Index);
// [Index + 1, end)
vector<int> rightInorder(inorder.begin() + Index + 1, inorder.end() );
接下来就要切割后序数组了。
首先后序数组的最后一个元素指定不能要了,这是切割点也是当前二叉树中间节点的元素,已经用了。
后序数组的切割点怎么找?
后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。
此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。
代码如下:
cpp
// postorder 舍弃末尾元素,因为这个元素就是中间节点,已经用过了
postorder.resize(postorder.size() - 1);
// 左闭右开,注意这里使用了左中序数组大小作为切割点:[0, leftInorder.size)
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
// [leftInorder.size(), end)
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
此时,中序数组切成了左中序数组和右中序数组,后序数组切割成左后序数组和右后序数组。
接下来可以递归了,代码如下:
cpp
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
整体思路:
终止条件(看后序,因为后序先确定根节点)------通过后序找到根节点------找切割点------切割中序------要删除末尾再切割后序------递归
做的时候得画图,看纸质笔记!!
整体代码:
cpp
class Solution {
private:
TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
if (postorder.size() == 0) return NULL;
// 后序遍历数组最后一个元素,就是当前的中间节点
int rootValue = postorder[postorder.size() - 1];
TreeNode* root = new TreeNode(rootValue);
// 叶子节点,提前终止递归,直接返回当前叶子节点
if (postorder.size() == 1) return root;
// 找到中序遍历的切割点
int Index;
for (Index = 0; Index < inorder.size(); Index++) {
if (inorder[Index] == rootValue) break;
}
// 切割中序数组
// 左闭右开区间:[0, Index)
vector<int> leftInorder(inorder.begin(), inorder.begin() + Index);
// [Index + 1, end)
vector<int> rightInorder(inorder.begin() + Index + 1, inorder.end() );
// postorder 舍弃末尾元素
postorder.resize(postorder.size() - 1);
// 切割后序数组
// 依然左闭右开,注意这里使用了左中序数组大小作为切割点
// [0, leftInorder.size)
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
// [leftInorder.size(), end)
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return NULL;
return traversal(inorder, postorder);
}
};
【注】
1、Index一定要定义在for循环外面,因为后面要用!!
2、参数一定要是迭代器!

下面看从前序与中序遍历序列构造二叉树。
整体思路:
终止条件(看前序,因为前序先确定根节点)------通过前序找到根节点------找切割点------切割中序------切割前序(不用删末尾了)------递归
cpp
class Solution {
public:
TreeNode* traversal(vector<int>& preorder, vector<int>& inorder){
if(preorder.size()==0) return nullptr;
int rootValue = preorder[0];
TreeNode* root = new TreeNode(rootValue);
if(preorder.size()==1) return root;
int index;
for(index=0;index<inorder.size();index++){
if(inorder[index]==rootValue) break;
}
vector<int> leftInorder(inorder.begin(),inorder.begin()+index);
vector<int> rightInorder(inorder.begin()+index+1,inorder.end());
vector<int> leftPreorder(preorder.begin()+1,preorder.begin()+1+leftInorder.size());
vector<int> rightPreorder(preorder.begin()+1+leftInorder.size(),preorder.end());
root->left = traversal(leftPreorder, leftInorder);
root->right= traversal(rightPreorder, rightInorder);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(inorder.size()==0||preorder.size()==0) return nullptr;
return traversal(preorder, inorder);
}
};
437. 路径总和 III

规定路径方向必须向下
思路与解法
这道题目要求我们计算二叉树中所有路径和等于给定目标值 targetSum 的路径数量。可以利用前缀和 + 哈希表的思路来优化算法。
关键点分析
1、路径的定义:路径不一定从根节点开始,也不一定在叶子节点结束,路径必须是从父节点到子节点进行传递。
2、前缀和的思路:前缀和是一种在数组或树中计算区间和的技巧,可以通过记录路径的前缀和来减少冗余计算,避免重复遍历子树。
前缀和的思想
我们可以通过在遍历树的过程中维护当前路径的前缀和,并使用哈希表记录每个前缀和出现的次数。具体的思想是:
1、在递归过程中,每当访问一个节点时,计算从当前节点到根节点的路径和(前缀和)。
2、如果当前前缀和减去目标值 targetSum 结果在哈希表中出现过,说明有路径和等于 targetSum,因为从某个先前的节点到当前节点的路径和就是 targetSum。
3、通过维护前缀和的哈希表,可以高效地统计满足条件的路径数。
解题步骤
1、前缀和的哈希表:使用哈希表 prefixSum 来记录每个前缀和出现的次数,初始时 prefixSum[0] = 1,表示从根节点开始就有一个路径和为 0。
2、深度优先搜索(DFS):通过递归方式遍历树的每个节点,在递归过程中维护当前路径的前缀和。每访问一个节点,就更新前缀和,并检查是否存在某个前缀和为 current_sum - targetSum。
3、路径计数:每当发现某个路径的和等于目标值时,就更新结果计数。
算法步骤
1、从根节点开始,递归地遍历每个节点。
2、对每个节点,计算从该节点到根节点的路径的前缀和。
3、使用哈希表记录出现过的前缀和,每次计算当前路径的前缀和时,检查 prefixSum[current_sum - targetSum] 是否存在,若存在,则路径和等于目标值,计数加一。
4、递归地访问左右子树,并更新前缀和。
cpp
class Solution {
private:
unordered_map<long,int> prefixCount; //前缀和(key) -> 出现次数(value)
int count=0; //统计路径数目
void traversal(TreeNode* node, long curSum,int targetSum){
if(node==nullptr) return;
curSum=curSum+node->val; //更新从该节点到根节点的路径的前缀和
if(prefixCount.count(curSum-targetSum)){
count+=prefixCount[curSum-targetSum];
}
prefixCount[curSum]++;
traversal(node->left,curSum,targetSum);
traversal(node->right,curSum,targetSum);
prefixCount[curSum]--;
}
public:
int pathSum(TreeNode* root, int targetSum) {
prefixCount[0]=1;
traversal(root,0,targetSum);
return count;
}
};
【注】
1、void traversal(TreeNode* node, long curSum,int targetSum){ curSum要定义成long,否则会溢出。
2、prefixCount[curSum]--; 回溯。当从当前节点返回父节点时,需要撤销当前节点对哈希表的影响,因为当前节点的前缀和只应该在该节点的子树范围内有效。
236. 二叉树的最近公共祖先

注意是最近!
遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。
那么二叉树如何可以自底向上查找呢?
回溯啊,二叉树回溯的过程就是从底到上。
后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。
接下来就看如何判断一个节点是节点q和节点p的公共祖先呢?
情况一: 最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。
判断逻辑 是如果递归遍历遇到q,就将q返回,遇到p就将p返回,那么如果左右子树的返回值都不为空,说明此时的中节点,一定是q和p的最近祖先。
情况二: 很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q(p)。
其实情况一和情况二代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。因为遇到q或者p就返回,这样也包含了q或者p本身就是公共祖先的情况。
核心代码:
递归三部曲:
1、确定递归函数返回值以及参数
需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。
但我们还要返回最近公共节点,可以利用题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。
cpp
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
2、确定终止条件
遇到空的话,因为树都是空了,所以返回空。
如果 root == q,或者 root == p,说明找到 q、p ,则将其返回。这个返回值后面在中节点的处理过程中会用到。
cpp
if (root == q || root == p || root == NULL) return root;
3、确定单层递归逻辑
值得注意的是本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断。
如果递归函数有返回值,如何区分要搜索一条边 ,还是搜索整个树呢?
搜索一条边的写法:
cpp
if (递归函数(root->left)) return ;
if (递归函数(root->right)) return ;
搜索整个树写法:
cpp
left = 递归函数(root->left); // 左
right = 递归函数(root->right); // 右
left与right的逻辑处理; // 中
看出区别了没?
在递归函数有返回值的情况下:
如果要搜索一条边 ,递归函数返回值不为空的时候,立刻返回。
如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)。
那么本题为什么要遍历整棵树呢 ?直观上来看,找到最近公共祖先,直接一路返回就可以了。
因为本题代码的后序遍历中,需要利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回。
那么先用left和right接住左子树和右子树的返回值,代码如下:
cpp
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解。
如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然。
如果left和right都为空,则返回left或者right都是可以的,也就是返回空。
代码如下:
cpp
// 如果左右都非空,说明 p 和 q 分别位于当前节点的两侧,当前节点就是 LCA(最近公共祖先)
if (left != nullptr && right != nullptr) {
return root;
}
// 否则返回非空的那一侧(可能为空,也可能包含 LCA)
return left != nullptr ? left : right;
完整流程:

整体代码:
cpp
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root==p||root==q||root==nullptr) return root;
TreeNode* left=lowestCommonAncestor(root->left,p,q);
TreeNode* right=lowestCommonAncestor(root->right,p,q);
if(left!=nullptr&&right!=nullptr) return root;
else return left!=nullptr?left:right;
}
};
归纳如下三点:
1、求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历 (即:回溯)实现从底向上的遍历方式。
2、在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断 。
3、要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。