索引
- 二叉搜索树
- 1.二叉搜索树概念与性能分析
- 2.二叉树的应用
- 3.二叉树的实现细节
- 4.类中函数递归调用
- 5.二叉树实现总体代码
- 6.二叉进阶oj
- 7.二叉树三种遍历非递归实现
二叉搜索树
1.二叉搜索树概念与性能分析
什么是二叉搜索树
二叉搜索树也被称为二叉排序树,其也可以是一棵空树,若左子树不为空,那么左子树上所有节点值都小于根节点的值,若右子树不为空,那么右子树上的所有结点值都大于根节点且其左右子树的都是二叉搜索树。所以可二叉搜索树是不允许出现元素重复的
eg:
二叉树性质分析与应用
如上图所示可以看出,二叉树在查找时,最好的情况:
二叉树是完全二叉树或者接近完全二叉树,此时时间复杂度是Olog2(N),但如果二叉树是单支二叉树或者类似单支,比较的次数就是数高度也就是数的个数,所以此时的时间复杂度是O(N);
2.二叉树的应用
1.K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值
2.KV模型:每一个关键码Key,都有与之对应的值Value,即<Key,Value>的键值对,该种方式在实际生活中很常见,eg:英汉字典<word,chinese> 统计人的身高<people,height>
3.二叉树的实现细节
二叉树的插入:
1.树为空,此时直接new一个新结点然后赋值给root
2.树不为空,按照上述查找的规则,查找插入的位置,插入新节点
二叉树的删除
二叉树的删除比较复杂,首先需要查找元素是否在二叉搜索树中,如果不存在,那么直接返回false,即使存在,需要删除的结点也要分下面四种情况:
a.删除结点没有孩子
b.要删除的结点只有左孩子
c.要删除的结点只有右孩子
d.要删除的结点既有左孩子,也有有孩子
前面三种比较简单,但是第四种需要详细的考虑一下。
注: 删除的结点有可能就是头结点,这个无论在哪种情况下都得拿出来单独分析。
同时情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用其值与被删除结点交换,再来处理该节点的删除问题。
bool Erase(const K& key)
{
Node* cur = _root;
Node* curParent = nullptr;
while (cur)
{
if (cur->_key < key)
{
curParent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
curParent = cur;
cur = cur->_left;
}
else
{
//此时cur->_key = key
//分情况讨论
//其没有左子树
if (cur->_left == nullptr)
{
//有可能此时cur就是头节点
//此时curParent还是nullptr
//所以要做特判
if (curParent == nullptr)
{
_root = cur->_right;
delete cur;
}
else
{
if (cur == curParent->_left)
{
curParent->_left = cur->_right;
}
else
{
curParent->_right = cur->_right;
}
delete cur;
}
return true;
}
else if (cur->_right == nullptr)
{
if (curParent == nullptr)
{
_root = cur->_left;
delete cur;
}
else
{
if (cur == curParent->_left)
{
curParent->_left = cur->_left;
}
else
{
curParent->_right = cur->_left;
}
delete cur;
}
return true;
}
//左右孩子都不为空
else
{
Node* min_Right_Parent = cur;
Node* min_Right = cur->_right;
while (min_Right->_left)
{
min_Right_Parent = min_Right;
min_Right = min_Right->_left;
}
swap(min_Right->_key, cur->_key);
//有可能上述中循环压根不会进入
if (min_Right == cur->_right)
{
min_Right_Parent->_right = min_Right->_right;
}
else
{
min_Right_Parent->_left = min_Right->_right;
}
delete min_Right;
return true;
}
}
}
return false;
}
4.类中函数递归调用
在二叉树中,因为很容易转换成子问题,并且到叶子结点的时候往往就是截至条件,所以无论是上述的插入还是删除都可以采用递归。递归的缺陷就是如果树很深的话,递归层数太多,有可能会栈溢出。
由于递归需要传结点,指针,但是在类中我们只有一个结点就是根节点,所以我们要采用一种封装的思想。
bool InsertR(const K& key, const V& value)
{
return _InsertR(_root, key, value);
}
bool _InsertR(Node*& root, const K& key, const V& value)
{
if (root == nullptr)
{
root = new Node(key, value);
return true;
}
if (root->_key < key)
return _InsertR(root->_right, key, value);
else if (root->_key > key)
return _InsertR(root->_left, key, value);
else
return false;
}
注意:此时递归参事指针时候,指针必须引用传参,因为如果不传引用的话,root就是一个局部变量,出了作用域就会销毁,无法和其父节点连接,此时可以采取再添加一个父节点参数,或者添加引用
再比如:
析构函数不能直接调用递归,因为类中的析构函数没有参数,所以可以用析构函数调用另外一个递归函数,并且再调用另外一个递归函数时,给其传进去的参数一定是成员变量。
~BSTree()
{
_DestoryTree(_root);
_root = nullptr;
}
void _DestoryTree(Node* root)
{
if (root == nullptr)
return;
_DestoryTree(root->_left);
_DestoryTree(root->_right);
delete(root);
}
5.二叉树实现总体代码
(函数后面有R表示递归函数,没R表示普通函数)
二叉树实现总体代码
6.二叉进阶oj
606. 根据二叉树创建字符串
关键::如果左子树为空,但是右子树不为空str+“()”
同时左右子树都得用()括号括起来
102. 二叉树的层序遍历
**关键:**层序遍历一定要用队列,头结点出来队列时,其左右孩子入队列
236. 二叉树的最近公共祖先
关键寻找p,q结点,开始根节点都是root,如果最初p或q有一个等于root的话,那么此时root就是他们的最近公共祖先。如果p和q分别在root不同的子树,那么此时的root也是最近公共祖先。
优化做法可以找出p,q到到根节点的路径,将路径上的结点存到栈上。类似于单链表找两个链表的公共结点
JZ36 二叉搜索树与双向链表
关键线索化,中序遍历,用cur与pre遍历完整个二叉树注意,起初的pre是nullptr,最后cur到达nullptr的时候遍历结束。注意:参数cur不能是引用传参,其每个结点地址只需要拷贝即但是pre自始自终都是同一个,所以得引用传参
根据一棵树的前序遍历与中序遍历构造二叉树
关键先构造左右子树,再构造根节点,前序遍历理论上而言每个结点都是某个子树的根节点,中序遍历可以确定左右子树的区间范围,root->left = 函数递归(),root->right = 函数递归();
oj代码实现
7.二叉树三种遍历非递归实现
前序遍历:根 左 右
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*>st;
vector<int>vt;
TreeNode*cur = root;
while(cur||!st.empty())
{
while(cur)
{
st.push(cur);
vt.push_back(cur->val);
cur = cur->left;
}
auto top =st.top();
st.pop();
cur = top->right;
}
return vt;
}
直接将每个结点的左路结点走完入栈,然后找此时栈顶的非空右子树,按照之前一样走完左路结点入栈即可
中序遍历: 左 根 右
**关键:**左路结点入栈,一个结点从出栈时,它的左子树就访问完了,所以此时访问他以及他的右子树,本质差不多,只是数组的插入时间不同
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*>st;
vector<int>vt;
TreeNode*cur = root;
while(cur||!st.empty())
{
while(cur)
{
st.push(cur);
cur = cur->left;
}
auto top =st.top();
st.pop();
//从栈里面拿出来,说明此时其左子树已经访问完了
//此时便继续访问其根节点和右子树
vt.push_back(top->val);
cur = top->right;
}
return vt;
}
后序遍历:左右跟
关键 每个结点理论上来说都是根节点,访问根节点要满足两种条件任意一种,如果根节点的右子树为nullptr,又或者是此时成功访问的上一个结点时根节点的右儿子,那么此时就可以访问根节点,否则不行。
vector<int> postorderTraversal(TreeNode* root) {
vector<int>v;
stack<TreeNode*>st;
TreeNode*cur = root;
TreeNode*prev = nullptr;
while(cur||!st.empty())
{
while(cur)
{
st.push(cur);
cur = cur->left;
}
TreeNode*top = st.top();
//如果其没有右子树此时可以出栈 并且push_back
//如何判定根节点的右子树已经进入了
//如果上一个进入的节点是根节点的right那么说明此时
//该根节点左右子树都已访问完毕,便可以继续访问根节点
if(top->right==nullptr||top->right==prev)
{
v.push_back(top->val);
st.pop();
prev = top;
}
else
{
cur = top->right;
}
}
return v;
}