二叉搜索树(二叉排序树、二叉查找树)
二叉树值一种特殊的二叉树,它要么是空树,要么满足以下条件:
若左子树存在,则左子树上的所有结点都一定小于根结点,反之,右子树的所有结点都一定大于根节点,并且除了根节点外,所有的子树也满足这个条件。
二叉搜索树的中序遍历
二叉搜索树有个特别的地方,那就是,用中序遍历这棵树的时候,得到的将会是一组递增的数据,原理就不说了,懂得搜索树的原理和中序遍历的原理的话,在纸上画一画或者脑子里想一想就知道了
题目:
783. 二叉搜索树节点最小距离
给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
示例 :
输入:root = [1,0,48,null,null,12,49]
输出:1
最容易理解的递归遍历
class Solution {
public:
vector<int>res;
void traverse(TreeNode* p)//遍历链表函数
{
if(p == nullptr) return ;//递归基
traverse(p->left);
res.emplace_back(p->val);//中序遍历并把值写入数组
traverse(p->right);
}
int minDiffInBST(TreeNode* root) {
traverse(root);
int result = INT_MAX , N = res.size();
//特殊情况,size为0或1的情况下根本就没有差值,怎么求哦
if(N < 2) return 0;
for(int idx = 1 ; idx < N; idx++)
{
result = min(result , res[i] - res[i - 1]);
}
return result;
}
};
前缀树
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
208. 实现 Trie (前缀树)
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
来源:力扣(LeetCode)
class Trie {
vector<Trie*> children;
bool isend;
public:
//每个结点都有26个指向(可能存在的)子结点的指针,并且默认isend为false
Trie():children(26 , nullptr) , isend(false){}
void insert(string word) {
Trie* node = this;
for(char ch : word)
{
ch = ch - 'a';
if(node->children[ch] == nullptr)
{
node->children[ch] = new Trie();
}
node = node -> children[ch];
}
node=>isend = true;//别忘了改成true,表示这是一个完整的字符串
}
// 一个个的匹配字符,
bool search(string word) {
Trie* node = this;
for(char ch : word)
{
ch = ch - 'a';
node = node -> children[ch];
if(node = node -> children[ch] == nullptr)
return flase;
}
//一定要return isend,如果所有的字符都匹配成功,但是isend是false的话,
//还是等于没有插入这个字符串,因为插入的时候最后一步就isend改成true了
//不是true就说明没有插入过这个字符串
return node->isend;
}
//和上面那个search几乎一样,只有最后的return是返回true,因为只搜索前缀
//所以不要求最后的isend是true;
bool startsWith(string prefix) {
Trie* node = this;
for(char ch : word)
{
ch = ch - 'a';
node = node -> children[ch];
if(node = node -> children[ch] == nullptr)
return flase;
}
return true;
}
};
- 二叉树的层序遍历
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回:[[3],[9,20],[15,7]]
来源:力扣(LeetCode)
思路:
用队列做中转,遇见一个结点,就先把结点的左右子结点加入队列中,按照队列的性质,他们会排到当前节点的后面,这里要注意,在队列中加入子结点之前,要保存当前队列中的结点数,这个数代表这一层有多少结点,然后用循环把这一层的结点值输出,每输出一个,就把队列里的那个结点删掉
然后开始下一层的循环输出
层序遍历就这么简单,而且很多相关的题目都只是稍微改改就能套模板了,掌握了只能说是血赚了
代码:
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*>buf;
if(root) buf.push(root);
vector<vector<int>> res;
while(!buf.empty())//队列为空的话,就说明已经到了最后一层,并且输出完毕了
{
vector<int>temp;
int size = buf.size();//保存当前层数结点数
for(int i = 0;i<size;++i)
{
TreeNode* node = buf.front();
//输出结点值,因为我们是先加左子结点,再加右子结点,所以用队列输出正好满足要求
temp.emplace_back(node->val);
buf.pop();
//这里顺序别反了,题目的要求就是从左往右的层序遍历
if(node->left) buf.push(node->left);
if(node->right) buf.push(node->right);
}
//当前层结点值输出完以后,就改进行下一层了,反正下一层结点已经在循环中被放入队列中了
res.emplace_back(temp);
}
return res;
}
};
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
来源:力扣(LeetCode)
理解了二叉树的遍历的话,这题还是挺简单的
中序遍历就像把一颗树给拍扁了一样
每个结点的左子树都在结点左边,右子树在结点右边,非常的完美,因此,要构造一棵树,必须要有中序遍历,而前后遍历有其中一个就行了
那么怎么利用中序遍历呢?很简单啊,从前序遍历的角度来看,最开始的值即是整颗树的根节点,然后再中序遍历中找到这个根节点,我们就能知道根节点的左右子树都有哪些数值了,并且对左边部分进行搜索,看是不是越界,如果没越界,就说明根节点是有左子树的,并且左子树的根节点就是前序数组中的下一个值,没有就返回一个nullptr,然后递归的搜索,把树的左子结点,左子结点的左子结点,左子结点的左子结点的左子结点…全部构建出来,然后开始下一步,构建左子结点的左子结点的左子结点的右子结点,左子结点的左子结点的右子结点…直到树的左子树被完整的构建出来,然后递归终于回到了根节点,开始构建右子结点的左子结点…(这个顺序也是先左后右),直到把整棵右子树树构建出来…到了这一步,根节点得左右子树都构建完成,即整棵树都构建完成
先左后右就是因为前序遍历的规则,中左右
到了这一步,后序遍历+中序遍历的思路也很容易想出来了,后序是左右中,因为要构建树,必须得获得结点值,所以从后往前遍历后序数组,就变成了中右左,和前序不太一样诶,没事,改一改代码顺序就完事了
代码(前序+中序):
class Solution {
unordered_map<int, int>map;//用哈希表来快速查找结点在中序数组中的下标
int preIdx;//前序数组的下标,用来遍历前序数组
TreeNode* build(const vector<int>& preorder,const int L,const int R)
{
if(L > R) return nullptr;//如果到达数组的边界
int val = preorder[preIdx];//获得当前前序数组到的结点的值
int idx = map[val];//得到当前结点在中序数组的下标
++preIdx;//推进前序数组的遍历
TreeNode* root = new TreeNode(val);//构建当前结点
root->left = build(preorder, L, idx-1);//构建左子结点
root->right = build(preorder, idx+1, R);//构建右子结点
return root;//返回构建的结点去到上一层
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
map.clear();//初始化,对题目来说没啥用
preIdx = 0;//初始化
int size = inorder.size();
for(int i = 0;i<size;++i) map[inorder[i]] = i; //构建哈希表
return build(preorder,0,size-1);//返回根结点
}
};
代码(后序+中序):
class Solution {
unordered_map<int,int>map;
int idx;//后序数组的下标
TreeNode* build(const vector<int>& postorder, const int L, const int R)
{
if(L > R) return nullptr;
int val = postorder[idx];
int tempIdx = map[val];
--idx;//从后往前遍历后序数组
TreeNode* root = new TreeNode(val);
root->right = build(postorder,tempIdx+1,R);//和前序相比,值是变了上下顺序
root->left = build(postorder,L,tempIdx-1);//先右后左
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
map.clear();
idx = inorder.size()-1;//因为要从后面遍历后序数组,所以初值也不一样了
int size = inorder.size();
for(int i = size-1;i>=0;--i) map[inorder[i]] = i;//填入哈希表,顺序无所谓的
return build(postorder,0,size-1);
}
};
226. 翻转二叉树
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
这题还是很简单的,做来放松一下,如果没想通的话,就转换一下视角,不要在意结点的交换了,
想成子树的交换,子树怎么决定的呢?根结点呗!,所以交换每一个结点的左右结点就可以了…特别简单
代码:
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(!root) return nullptr;//递归基
TreeNode*tempNode = root->left;/****************************/
root->left = root->right;//交换左右子结点
root->right = tempNode;/*********************************/
invertTree(root->left);//递归的交换左右子结点的左右子结点
invertTree(root->right);
return root;
}
};
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
思路照上图看即可,如果一棵树是对称的,那么 左->左 一 定等于 右->右 ,即外侧相等
然后左->右 一定等于 右->左 ,即内侧相等不理解的话,在纸上多画一下就好了
我们递归的对每一个结点的左右结点进行判断即可
稍微需要注意一下的情况就是空结点
left == nullptr && right == nullptr true
left == nullptr && right != nullptr false
left != nullptr && right == nullptr false
然后就只需判断left->val 与 right->val是否相等了,如果不相等就return false
相等的话就继续递归,往下寻找
代码:
class Solution {
bool check(TreeNode* left, TreeNode* right)
{//这里改了一下判断的条件的写法,可以更简洁,但是逻辑还是一样的
if(!left && !right)return true;
else if(!left || !right) return false;
else if(left->val!=right->val)return false;
return check(left->left, right->right) && check(left->right, right->left);
}
public:
bool isSymmetric(TreeNode* root) {
if(root)
return check(root->left,root->right);
else
return false;
}
};
543. 二叉树的直径
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
来源:力扣(LeetCode)
思路:
我就不说了,直接放一个大佬的题解吧,用了50多页的ppt完整详细的展示了递归的过程
👉点这里
我就在代码注释里讲讲自己的理解
代码:
class Solution {
int res;
int length(TreeNode* root)
{
if(!root) return 0;//从下往上求出长度
int L = length(root->left);//求左子树的最大深度
int R = length(root->right);//求右子树的最大深度
res = max(res,L+R+1);//更新res,+1是算上了根节点
//这里要注意一下,这个返回值不是整棵数的最大半径,而是树的最大深度
//最大半径 = 左子树的最大深度 + 右子树的最大深度,这里是做选择,
//在左子树的两个子结点中,选一个最深的
//所以上面我们才能用L和R接收返回值,并且用L+R+1的方式更新res
return max(L,R)+1;
}
public:
int diameterOfBinaryTree(TreeNode* root) {
res = 1;
if(root) length(root);
return res-1;//我们算的是结点数,但是题目求得是直径,是边的数目,但是没问题,-1就好了
}
};
这一题和上一题的最大直径思路还挺像的,都是对左右结点分开求值,然后用一个全局变量maxSum更新最大值
不过上一题只记录结点数,这一题还需要算结点值之和。
思路:
无论以哪个结点为基准,我们也只能选左右两个结点作为路径开头
所以左右边的路径一定得最大,从左子结点开始,又有两个选择,左还是右?当然右子结点也是一样的两个选择,我们又得从这两个选择中选最大的一个…
于是我们用递归只返回当前结点左右两条路中的一路就行了,反正也只有一条路会被使用,不过既然要比较,所以两条路都得算出来
你可能会问,为什么只返回一路?另外一路呢?路径和怎么办?
别忘了我们的全局变量maxSum啊,都求出了当前结点的左右子树和了,那当前结点的最大路径和不就是root->val + leftSum + rightSum吗!!再用这个更新maxSum不就完事了?
至于为什么最大路径和是root->val + leftSum + rightSum,是优化过后得情况,不优化的话,情况就像下面一样
一般来说,对于每一个结点的最大路径和,都可能有以下四种取值情况:
1: 左子树最大和 + 当前结点值 + 右子树最大和 左右子树最大和都是正数
2: 左子树最大和 + 当前结点值 (不要右子树) 右子树最大和为负数
3: 当前结点值 + 右子树最大和 (不要左子树) 左子树最大和为负数
4: 只要当前结点值,不要左右子树 左右子树最大和都为负数
不管什么情况,我们都在上面四种情况中选最大的那个就完事了
优化
这里可以简化一下,如果子树和为负数,我们就直接不要了,把他当作0,不存在,这样就可以简化掉
2,3,4的情况了,就算子树中可能会有负数,我们也只会把他当不存在,所以最后,左子树最大和(leftSum) + 当前结点值(root->val) + 右子树最大和(rightSum)就是最大值!怎么样?直接排除3个情况!!
代码:
class Solution {
int maxSum;
int MaxSum(TreeNode* root)
{
if(!root) return 0;
int leftSum = max(0, MaxSum(root->left));//求左路最大和以及用max来无视负数情况
int rightSum = max(0, MaxSum(root->right));同上
int curSum = root->val + leftSum + rightSum;//记录当前结点的最大路径和
maxSum = max(maxSum, curSum);//更新全局最大路径和
return root->val + max(leftSum, rightSum);//返回当前结点的最大单路路径和
}
public:
int maxPathSum(TreeNode* root) {
maxSum = INT_MIN;
MaxSum(root);
return maxSum;
}
};
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
来源:力扣(LeetCode)
思路:
也没啥思路啦,众所周知二叉搜索树的中序遍历是个升序序列,那我们就中序遍历一遍呗,然后看做过序列是不是升序不就完事了
说说优化的过程:
一开始是完整的遍历完数组,然后看数组是不是升序
后来在中序遍历的时候就判断新加入数组的数是否大于原数组的最后一位
最后发现只需比较两个数字,完全没必要用数组,简化成两个变量,一个代表上一次中序遍历到的值,一个代表当前节点的值
需要注意的事情:
不要用第一个结点的值与preNum进行比较,因为preNum的初值是无意义的,当然也可以用INT_MIN,但是题目很贴心的在案例里加入了第一个结点值就为INT_MIN的情况,于是我们就得用LONG_MIN,
但我很不爽用long,于是我选择用一个计数跳过第一次比较,其实怎么都无所谓啦。看个人喜好
代码:(计数跳过第一次)
class Solution {
bool res;
pair<int, int>preNum;//first是值,second是计数,其实也可以分开来写啦
void inord(TreeNode* root)
{
if(!root) return ;
inord(root->left);
//second别忘了++啊,要计数的
if(preNum.second++>0 && root->val <= preNum.first)
{
res = false;
return;
}
preNum.first = root->val;//保存当前值,好与下一个值作比较
inord(root->right);
}
public:
bool isValidBST(TreeNode* root) {
preNum = {INT_MIN,0};//初始化
res = true;//初始化
if(root)
{
inord(root);
return res;
}
else
return false;
}
};
236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
来源:力扣(LeetCode)
写这题就是会想太复杂,或者说是没想通。其实只需要注意一点:
如果有一个结点,左子树包含p(或q), 右子树也包含p(或q), 那他一定是这两结点的最近公共祖先
如果不是最近的,那就不可能左右子树都包含了,只可能是一边的子树同时包含p和q
又到了搬运大佬ppt的时候了,这里👉
整个函数的意义是求出当前(子)树中包含着p或q的子树
如果不是最近最近公共祖先,那必定只有一边有包含的关系,另一半一定为null,
当发现某根节点的左右子树都不为null时,那么这个结点就是我们要找的祖先,我们返回这个结点,
如果有一个为空,我们就返回不为空的那个子树,因为我们还要继续往上寻找,如果两个都为空,就返回null
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root || root->val == p->val || root->val == q->val) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(left && right)return root;
if(left)return left;
else return right;
}
};
剑指 Offer 33. 二叉搜索树的后序遍历序列
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。
参考以下这颗二叉搜索树:
5
/ \
2 6
/ \
1 3
示例 1:
输入: [1,6,3,2,5]
输出: false
示例 2:
输入: [1,3,2,6,5]
输出: true
来源:力扣(LeetCode)
思路:递归分治
首先得知道二叉搜索树的定义:
左子树中所有节点的值 < 根节点的值;右子树中所有节点的值 > 根节点的值;其左、右子树也分别为二叉搜索树。
既然知道大小关系,那就递归得判断每一个结点是否符合条件不就行了
顺便说一说前中后序遍历的具体表达,下面图中,我用红色表示根节点,绿色表示左子树,蓝色表示右子树
就这一题来说,根据搜索树的性质,右子树的结点都比根节点大,于是我们从当前子树的最左边开始往后搜索,搜索第一个比根节点大的数字,这个数字之后的所有数字都是根节点的右子树,即蓝色范围,然后我们在判断蓝色范围内是不是所有值都比根节点大,如果有一个不满足,就说明不是搜索树
判断完当前结点后,再继续判断左右子结点,只有整颗树搜友子Jedi啊你都满足,才能证明这是二叉搜索树
代码:
class Solution {
bool Check(vector<int>& postorder, int left, int right)
{
if(left>=right)return true;//如果只有一个结点,跟着没有结点,就没必要判断了
int mid = left;
while(postorder[mid]<postorder[right])++mid;//找到蓝色(右子树)的开头
int bufNum = mid;
while(bufNum++<right)
{//判断右子树中是不是所有结点都大于根节点
if(postorder[bufNum]<postorder[right])
return false;
}
//继续判断左右子树
return Check(postorder,left, mid-1) && Check(postorder, mid, right-1);
}
public:
bool verifyPostorder(vector<int>& postorder) {
return Check(postorder, 0, postorder.size()-1);
}
};
297. 二叉树的序列化与反序列化
虽然是一题困难题,但是只要明白了题意,这也就是两个中等题合在一起而已
一:遍历一颗树并把结果存在字符串里
二:再用这个字符串构建一棵树,当然,构建出来的的树必须和原树一样
就这样而已,题目甚至没有要求格式,就是说,你用前中后,或者层序遍历都行,为了和题目的示例一样(还有就是个人喜好,我比较喜欢层序遍历),我就用了层序遍历,遍历树就不写了,主要是用完整的信息构建树,以前还真没写过,都是不完整的,不包含空结点的
完整信息的层序i案例结果构建树其实也很简单,先把所有结点都构建出来,存到数组里,然后再遍历数组并构建树
class Codec {
public:
string serialize(TreeNode* root) {//层序遍历
string res;
queue<TreeNode*>que;
que.push(root);
while(!que.empty())
{
int size = que.size();
for(int i = 0;i<size;++i)
{
TreeNode*temp = que.front();
que.pop();
if(temp == nullptr)
res+='#';
else
res+=(to_string(temp->val));
res+=',';
if(temp)//只有当结点有效是才填入左右子结点,不然会一直填入nullptr
{
que.push(temp->left);
que.push(temp->right);
}
}
}
res.pop_back();
while(res.back()=='#' || res.back()==',')res.pop_back();//就算限制也还会多出来一些无效的空指针
return res;
}
TreeNode* deserialize(string data) {
if(data.empty())return nullptr;//判断一下特殊情况
vector<TreeNode*>nodes;//建立结点数组
for(int R = 0;R<data.size();++R)
{
int L = R;
while(R<data.size()&&data[R]!=',')++R;//移动右指针
if(data[L]=='#')
nodes.push_back(nullptr);
else
nodes.emplace_back(new TreeNode(stoi(data.substr(L, R-L))));//提取字符串转成数字并建立结点
}int j = 1, size = nodes.size();
for(int i = 0;i<size;++i)
{//构建树,j要一直跑在i的前面,因为j指向i的子结点
if(!nodes[i])continue;//空结点就不管了
if(j<size)nodes[i]->left = nodes[j++];
if(j<size)nodes[i]->right = nodes[j++];
}
return nodes[0];
}
};
99. 恢复二叉搜索树
给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?
来源:力扣(LeetCode)
这一题如果不追求进阶条件的话,还是很轻松的,看一眼题解自己随便写写就完事了,追求进阶的话有点复杂,以后有空再挑战吧…
都知道搜索树的中序遍历是递增序列,就从这里下手,一旦发现逆序了,就说明找到了一个出错的结点
这里可以分三种情况:
1:完全没出错
2:出错的两个点在中序遍历里是相邻的
3:不相邻
这种情况,第一次发现错误结点,需要记录前驱,第二则要记录当前结点
我们用一个指针记录前驱结点,然后每次都用当前节点的值与前驱结点的值作比较,在用两个指针记录出错的结点,出现逆序就记录,并且在记录第一个结点的时候,默认把第二个指针指向当前结点,这样情况2和3就统一了
class Solution {
TreeNode* p1;//第一个需要交换的结点
TreeNode* p2;//第二个
TreeNode* pre;//前驱
void traverse(TreeNode* node)//中序遍历
{
if(!node)return;
traverse(node->left);
if(!pre)pre = node;
else{
if(node->val<pre->val){
if(!p1){//一旦出现逆序,且p1为null
p1 = pre;//记录为前一个结点
p2 = node;//因为需要交换的两个结点可能是相邻,所以默认先让p2记录下来再说
}else{//如果又出现一次逆序,就更新p2
p2 = node;
}
}
pre = node;//比较完后记录当前结点
}
traverse(node->right);
}
public:
void recoverTree(TreeNode* root) {
if(root)traverse(root);
if(p1 && p2){
int temp = p1->val;
p1->val = p2->val;
p2->val = temp;
}
}
};