文章目录
二叉搜索树基本概念与结构
如图所示,这就是一颗二叉搜索树,其是由一个个结点通过指针链接起来的。
1.根节点的值大于左子树的任意结点的值,小于右子树任意结点的值
2.在树内不会出现相同的值
3.每个结点的左子树和右子树也分别为二叉搜索树
基本结构
template<class K>
struct BSTreeNode
{
BSTreeNode(K& key) //结点的构造函数
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
BSTreeNode* _left; //左节点
BSTreeNode* _right; //右节点
K _key; //值
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
BSTree()
:_root(nullptr)
{}
private:
Node* _root; //根节点
};
搜索二叉树常用接口(循环实现)
Insert插入函数
bool Insert(K& key)
{
//当根节点为空,则key一定不存在树中
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = nullptr; //记录父亲结点的位置
while (cur)
{
//若key大于根节点值,到右子树查找
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//若key小于根节点值,到左子树查找
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
//若key等于根的值,则已经存在,返回假
else
return false;
}
cur = new Node(key);
//若key大于父亲结点key,则在父节点右边
if (key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
}
当想要插入的key在二叉树中已经存在时返回false,不存在则在正确位置插入_key = key 的结点
Find查找函数
bool Find(K& key)
{
Node* cur = _root;
while (cur)
{
//若key大于根的值,则只可能在右子树
if (key > cur->_key)
{
cur = cur->_right;
}
//key小于根的值,在左子树
else if (key < cur->_key)
{
cur = cur ->_left;
}
//等于根的值,找到了
else
{
return true;
}
}
//找不到返回false
return false;
}
InOrder中序遍历
void InOrder()
{
//新建一个栈,存储结点指针
stack<Node*> st;
Node* p = _root;
do
{
//若有左节点则将左节点压入栈中
while (p)
{
st.push(p);
p = p->_left;
}
//取出最后一个左节点
p = st.top();
st.pop();
//输出目前最左结点的值
cout << p->_key << " ";
//若这个节点有右子节点,需要进入这个结点的右子树用上述方法实现压栈
if (p->_right)
p = p->_right;
else
//若没有右节点则将p制空,这样循环就不会进入第一层循环,直接可以取到栈中上一个结点(这个结点的父节点)
p = nullptr;
}while(p || !st.empty());
cout << endl;
}
由于二叉搜索树的性质,前序遍历可以将树中的key从小到大输出,可以通过这个接口验证代码的正确性。
Erase删除函数
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
//查找删除结点
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
//找到需要删除的结点cur
//在代码实现过程中,我们发现情况一和情况而的代码是接近相同的删除结点一边为空,父节点接收其另
//一边结点指针,若另一边结点指针为空,则为情况一,若不为空,则为情况二
if (cur->_left == nullptr)
{
//若要删除的是根节点,当其左子树或者右子树为空时需要特殊处理。因为此时parent为空
//正常处理会产生错误访问
if (parent == nullptr)
{
_root = _root->_right;
}
else
{
//将删除结点的非空结点指针交给其父结点
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
return true;
}
else if (cur->_right == nullptr)
{
if (parent == nullptr)
{
_root = _root->_left;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
return true;
}
else
{
//情况三:
//1.查找左子树最大结点
Node* prev = cur;
Node* left_max = cur->_left;
while (left_max->_right)
{
prev = left_max;
left_max = left_max->_right;
}
//替换左子树最大结点和替换结点的值
cur->_key = left_max->_key;
//若left_max有左节点,则交给其父prev
if (prev->_right == left_max)
{
prev->_right = left_max->_left;
}
else
{
prev->_left = left_max->_left;
}
delete left_max;
left_max = nullptr;
return true;
}
}
}
//没有找到指定key,返回假
return false;
}
搜索二叉树常用接口 (递归实现)
InOrder中序遍历(递归)
void InOrderR() //无参数
{
_InOrder(_root); //调用子函数
}
void _InOrderR(Node* root) //有参数
{
if (root == nullptr)
{
return;
}
_InOrderR(root->_left);
cout << root->_key << " ";
_InOrderR(root->_right);
}
private:
Node* _root;
};
//若只写一个函数的话,我们无法直接得到_root,需要添加接口取得_root.为了方便写一个子函数
由于二叉搜索树的性质,前序遍历可以将树中的key从小到大输出,可以通过这个接口验证代码的正确性。
Insert插入函数(递归)
bool InsertR(Node*& root, K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (key > root->_key)
{
return InsertR(root->_right, key);
}
else if (key < root->_key)
{
return InsertR(root->_left, key);
}
else
{
return false;
}
}
bool InsertR(K& key)
{
InsertR(_root, key);
}
Find查找函数(递归)
bool Find(Node*& root, K& key)
{
if (key > root->_key)
{
return Find(root->_right, key);
}
else if (key < root->_key)
{
return Find(root->_left, key);
}
else
{
return true;
}
}
bool Find(K& key)
{
Find(_root, key);
}
Erase删除函数(递归)
bool EraseR(Node*& root, const K& key)
{
//若key大于根节点,则到右子树删,若小于则到左子树删
if (key > root->_key)
{
return EraseR(root->_right, key);
}
else if (key < root->_key)
{
return EraseR(root->_left, key);
}
else
{
//找到删除结点,使用del记录一下
//root是上一个结点的左节点或者右节点指针的引用,我们可以直接改变root就可以改变父节点指针的指向
Node* del = root;
//情况1和2
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
//情况三
else
{
//找左子树最大结点
Node* left_max = root->_left;
Node* prev = root;
while (left_max->_right)
{
prev = left_max;
left_max = left_max->_right;
}
//方法一:
//将左子树的值和删除结点的值进行交换
swap(left_max->_key, root->_key);
//此时值为key的结点肯定没有右节点,就满足情况一或二,因为跟的值肯定大于左子树,所以原来根
//的左子树任然满足搜索二叉树,可以调用Erase输入左子树和key进行删除
return EraseR(root->_left, key);
//方法二:和非递归方法相同
//if (prev->_left == left_max)
//{
// prev->_left = left_max->_left;
//}
//else
//{
// prev->_right = left_max->_left;
//}
//root->_key = left_max->_key;
//del = left_max;
}
//删除指定结点并制空,返回真
delete del;
del = nullptr;
return true;
}
}
为何递归频繁使用子函数
可以发现,在模拟实现递归接口时,我们频繁的使用了子函数,为什么要这样使用呢?
//我们可以看到InOrder函数在递归调用中,使用的root是不同的,所以函数有Node* root的参数
//当我们想要使用中序打印二叉树的时候,我们会发现我们无法直接得到根节点_root;
//为了得到根节点我们需要增加接口得到根节点,并用接口的返回值当作参数,调用的时候会非常奇怪
//为了增加代码的可读性,美观性,增加一个子函数,甚至还可以将子函数定义为私有
void InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
InOrder(root->_left);
cout << root->_key << " ";
InOrder(root->_right);
}
//增加函数得到根节点的地址
Node* get_root()
{
return this->_root;
}
void TestBSTree()
{
BSTree<int> t;
int a[] = { 5,4,2,6,8,3,2,1 };
for (auto e : a)
{
t.Insert(e);
}
t.InOrder(t.get_root()); //调用十分的别扭
}
函数重载的价值也可以在这里体现,两个函数可以同名,但是最好在子函数开头加上_(下划线)进行标识,还可以将子函数定义为private中,更好的体现了封装
搜索树的价值
1.搜索
key搜索模型(判断在不在,有没有)
判断在不在,可以应用在学校门禁系统中。校园卡储存了学生的学号等信息。我们可以将每一栋宿舍的学生的学号插入一个二叉搜索树。每次进门刷卡时校园卡读取学号,在二叉搜索树中进行查找。找得到返回真开门,找不到返回假不开门
key/value搜索模型(通过一个值查找另一个值)
通过一个值来找到另外的值,例如高铁站我们通过刷身份证就可以进站。这是因为我们的身份证上有我们的身份信息,例如身份证号。高铁站将我们的身份证号和票的信息关联起来。这样就可以通过刷卡时读取到的身份证号(key)在搜索树中找到我们的val(票信息),一个人可能有多张票,所以val还可以设计成vector。然后通过比对val和闸机上的比对信息,就可以判断是否开门
2.排序+去重