数据结构、算法总述:数据结构/算法 C/C++-CSDN博客
二叉搜索树(BST):
二叉搜索树是一种二叉树的树形数据结构,其定义如下:
空树是二叉搜索树。
若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。
若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。
二叉搜索树的左右子树均为二叉搜索树。
eg:
特性:
- 二叉搜索树与普通的二叉树有所不同,如果我们按照中序遍历进行访问结点的值的时候我们可以发现它是一个升序的序列。
- 每个结点的左子树结点上边的值都小于该结点的值,右子树是上边的值都大于该结点的值。
创建节点:
cpp
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
BST树:
cpp
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//成员函数
private:
Node* _root=nullptr;
};
默认成员函数:
构造函数
这里的构造函数直接让编译器默认生成就可以,不需要自己实现,但是后面的拷贝构造函数写了之后编译器就不会默认生成了,但是我们可以强制让它默认生成构造函数,不过要利用C++11的特性,具体看代码:
cpp//强制编译器自己生成构造函数,忽视拷贝构造带来的影响 BSTree() = default;//C++11才支持
拷贝构造函数
注意这里的拷贝构造完成的是深拷贝,这里我们直接用前序递归的方式创建一颗与原来一样的二叉树即可。而递归前序拷贝结点的方式这里我们专门封装一个Copy函数即可。
cppNode* CopyTree(Node* root) { if (root == nullptr) return nullptr; Node* copyNode = new Node(root->_key);//拷贝根结点 //递归创建拷贝一棵树 copyNode->_left = CopyTree(root->_left);//递归拷贝左子树 copyNode->_right = CopyTree(root->_right);//递归拷贝右子树 return copyNode; } //拷贝构造函数--深拷贝 BSTree(const BSTree<K>& t) { _root = CopyTree(t._root); }
赋值运算符重载函数
这里直接给出现代写法:写法很巧妙,假设把t2赋值给t1,t2传参的时候直接利用传值传参调用拷贝构造生成t,t就是t2的拷贝,此时再调用swap函数交换t1和t 的_root根结点即可,而拷贝构造出来的t会在赋值运算符重载结束后自动调用自己的析构函数完成释放。
cpp//赋值运算符重载函数 t1 = t2 BSTree<K>& operator=(BSTree<K> t)//t就是t2的拷贝 { //现代写法 swap(_root, t._root); return *this; }
析构函数
析构函数是为了释放二叉搜索树的所有结点,这里我们优先采用后序的递归释放,可以采用封装一个Destory函数来专门用于递归删除结点
cppvoid DestoryTree(Node* root) { if (root == nullptr) return; //通过递归删除所有结点 DestoryTree(root->_left);//递归释放左子树中的结点 DestoryTree(root->_right);//递归释放右子树中的结点 delete root; } //析构函数 ~BSTree() { DestoryTree(_root);//复用此函数进行递归删除结点 _root = nullptr; }
相关操作:
查找
过程:
在以
root
为根节点的二叉搜索树中搜索一个值为value
的节点。分类讨论如下:
- 若
root
为空,返回false
。- 若
root
的权值等于value
,返回true
。- 若
root
的权值大于value
,在root
的左子树中继续搜索。- 若
root
的权值小于value
,在root
的右子树中继续搜索。
代码(非递归):
cpp
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
//待查值大于当前结点,去右子树
if (cur->_key < key)
{
cur = cur->_right;
}
//待查值小于当前结点,去左子树
else if (cur->_key > key)
{
cur = cur->_left;
}
//找到
else
{
return true;
}
}
return false;
}
代码(递归):
cpp
bool _FindR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (root->_key < key)
{
return _FindR(root->_right, key);
}
else if (root->_key > key)
{
return _FindR(root->_left, key);
}
else
{
return true;
}
}
插入
过程:
在以
root
为根节点的二叉搜索树中插入一个值为value
的节点。分类讨论如下:
若
root
为空,直接返回一个值为value
的新节点。若
root
的权值等于value
,该节点的附加域该值出现的次数自增 。若
root
的权值大于value
,在root
的左子树中插入权值为value
的节点。若
root
的权值小于value
,在root
的右子树中插入权值为value
的节点。
代码(非递归):
cpp
bool Insert(const K& key)
{
//树为空,则直接新增结点,赋值给_root指针
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
//按性质查找插入的位置,并且记录父结点
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
//已有结点,不需要再插入
else
{
return false;
}
}
cur = new Node(key);
//判断是插入父结点的左部还是右部
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
代码(递归):
cpp
bool _FindR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (root->_key < key)
{
return _FindR(root->_right, key);
}
else if (root->_key > key)
{
return _FindR(root->_left, key);
}
else
{
return true;
}
}
删除
过程:
在以
root
为根节点的二叉搜索树中删除一个值为value
的节点。先在二叉搜索树中搜索权值为
value
的节点,分类讨论如下:
若该节点的附加
count
大于 ,只需要减少count
。若该节点的附加
count
为 :
若
root
为叶子节点,直接删除该节点即可。若
root
为链节点,即只有一个儿子的节点,返回这个儿子。若
count
有两个非空子节点,一般是用它左子树的最大值(左子树最右的节点)或右子树的最小值(右子树最左的节点)代替它,然后将它删除。
代码(非递归):
cpp
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
//查找是否有待删除的节点
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 开始删除
// 1、左为空
// 2、右为空
// 3、左右都不为空
if (cur->_left == nullptr)
{
//判断下当前节点是否是_root,若是,无法用parent(当前为nullptr,防止野指针错误)
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else
{
//记录删除节点父节点
Node* minParent = cur;
//找到右子树最小节点进行替换
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(cur->_key, min->_key);
//min在父的左孩子上
if (minParent->_left == min)
//万一最左节点还有右孩子节点,或者是叶子也直接指右为空
minParent->_left = min->_right;
//min在父的右孩子上(待删除节点在根节点,最左节点为根节点的右孩子)
else
minParent->_right = min->_right;
delete min;
min == nullptr;
}
return true;
}
}
return false;
}
代码(递归):
cpp
bool _EraseR(Node* root, const K& key)
{
Node* del = root;
if (root == nullptr)
return false;
if (root->_key < key)
return _EraseR(root->_right, key);
else if (root->_key > key)
return _EraseR(root->_left, key);
else
{
if (root->_left == nullptr)
root = root->_right;
else if (root->_right == nullptr)
root = root->_left;
else
{
//找右数的最左节点替换删除
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(root->_key, min->_key);
//交换后结构改变不是搜索二叉树了,规定范围在右树(因为是右树最左节点替换)再递归
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
应用
(1)纯 key 模型 ,如: 有一个英文词典,快速查找一个单词是否在词典中快速查找某个名字在不在通讯录中
(2)Key-Value 模型,如: ①统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:< 单词,单词出现的次数 > ②梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下:二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log(N)
最差情况下:二叉搜索树退化为单支树(或者类似单支),其平均比较次数为 N
改进:AVL树、红黑树