二叉搜索树
1. 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值.
2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3. 它的左右子树也分别为二叉搜索树
2. 二叉搜索树操作
2.1. 二叉搜索树的查找
2.2. 二叉搜索树的插入
插入过程如下图所示:
a. 若树为空,则直接插入
b. 若树不为空,按二叉搜索树性质查找插入位置,插入新节点
2.3. 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
1. 要删除的结点无孩子结点
2. 要删除的结点只有左孩子结点
3. 要删除的结点只有右孩子结点
4. 要删除的结点有左、右孩子结点
实际情况1可以与情况2或者3合并起来,因此真正的删除过程如下:
1. 删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
2. 删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
3. 在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中, 再来处理该结点的删除问题
注:二叉搜索树已经严格有序,所以一般不允许修改
3 二叉搜索树的实现
具体实现详见Github:链接传送门
4. 二叉搜索树的应用
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。 比如:给一个单词word,判断该单词是否拼写正确,具体方式如下: 以单词集合中的每个单词作为key,构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
<单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较Key查询英文单词时,只需给出英文单词,就可快速找到与其对应的key
KV模型的实现:链接传送门
5. 二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:logN
最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N / 2
6. 二叉树常见OJ习题
1. 你需要采用前序遍历的方式,将一个二叉树转换成一个由括号和整数组成的字符串。OJ链接
思路:采用二叉树前序遍历规则进行转化
1. 如果树空,转化结束
2. 如果树非空
a.先转化跟节点
b. 转化根节点的左子树 如果根的左子树是空:右子树也是空,直接返回
右子树非空,左子树位置用()代替.如果根的左子树非空:(递归转化左子树 ), 注意将转化结果内嵌到()中
c. 转化根节点的右子树,如果根的右子树是空:转化结束。
如果根的右子树非空:( 递归转化右子树 ), 注意将转化结果内嵌到()中
代码如下:
class Solution {
public:
string tree2str(TreeNode* t) {
string str;
if(t == nullptr)
return str;
str += to_string(t->val);
if(t->left)
{
str += '(';
str += tree2str(t->left);
str += ')';
}
else if(t->right)
str += "()";
if(t->right)
{
str += '(';
str += tree2str(t->right);
str += ')';
}
return str;
}
};
2. 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。(即逐层地,从左到右访问所有节点)OJ链接
思路:二叉树层序遍历的变形
1. 如果是空树直接返回
2. 层序遍历需要用到队列,定义一个队列,里面放置节点的地址,将根节点如队列
3. 队列非空时,循环进行一下操作:
a. 队列中当前元素都是在同一层的,依次取出遍历,保存到同一个vector中
取到一个节点时候:
保存该节点
如果该节点左子树存在,将该左子树入队列
如果该节点右子树存在,将该节点右子树入队列
将当前已遍历节点从队列中拿出来
b. 本层节点遍历结束后,保存到返回的vector中,此时下一层节点已经全部入队列
代码如下:
vector<vector<int>> levelOrder(TreeNode* root)
{
vector<vector<int>> vv;
queue<TreeNode*> q;
if(root)
q.push(root);
while(!q.empty())
{
int levelSize = q.size();
vector<int> v;
for(size_t i = 0; i < levelSize; i++)
{
TreeNode* front = q.front();
q.pop();
v.push_back(front->val);
if(front->left)
q.push(front->left);
if(front->right)
q.push(front->right);
}
vv.push_back(v);
}
return vv;
}
3. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。OJ链接
思路: 二叉树的最近公共祖先
存在三种情况:
1、p q 一个在左子树 一个在右子树 那么当前节点即是最近公共祖先
2、p q 都在左子树(递归查找)
3、p q 都在右子树(递归查找)
代码如下:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr)
return nullptr;
if(p == root || q == root)
return root;
TreeNode* left = lowestCommonAncestor(root->left,p,q);
TreeNode* right = lowestCommonAncestor(root->right,p,q);
if(left && right)
return root;
return left ? left : right;
}
};
4. 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。OJ链接
思路:二叉搜索树的特点是位于左子树的节点都比父节点小,位于右子树的节点都比父节点大,我们只需要从树的根节点和两个输入的节点进行比较。如果当前节点的指比两个节点都大,那么最低的共同父节点一定在当前节点的左子树中,于是下一步遍历当前节点的左子节点。如果当前节点的指比两个节点都小,那么最低的共同父节点一定在当前节点的右子树中,于是下一步遍历当前节点的右子节点。反之,返回当前节点。
代码如下:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr)
return nullptr;
if(p->val < root->val && q->val < root-> val)
return lowestCommonAncestor(root->left, p, q);
else if(p->val > root->val && q->val > root-> val)
return lowestCommonAncestor(root->right, p, q);
else
return root;
}
};
5. 给定一棵二叉搜索树,请找出其中第k大的节点。OJ链接
思路:二叉搜索树中序遍历是升序排序的,即可以中序遍历将所有数值放入数组中,由此可找到第K大的节点
代码如下:
class Solution {
public:
vector<int> v;
void Inorder(TreeNode* root)
{
if(root == nullptr)
return;
Inorder(root->left);
v.push_back(root->val);
Inorder(root->right);
}
int kthLargest(TreeNode* root, int k) {
Inorder(root);
return v[v.size() - k];
}
};