文章目录
- 1 二叉搜索树概念
- 2 二叉搜索树的实现
- 2.1 结点的定义
- 2.2 二叉搜索树的插入
- 2.2 二叉搜索树的查找
- 2.3 二叉搜索树的删除
- 2.4 二叉搜索树的默认成员函数
- 2.4.1 拷贝构造
- 2.4.2 析构函数
- 2.4.3 赋值重载
- 3 二叉搜索树的应用
- 3.1 k模型
- 3.2 kv模型
- 4 二叉搜索树的性能分析
1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
①若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
②若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
③它的左右子树也分别为二叉搜索树
2 二叉搜索树的实现
2.1 结点的定义
template <class k>
struct BSTreeNode
{
BSTreeNode<k>* _left;//指向左孩子
BSTreeNode<k>* _right;//指向右孩子
k _key;//数据域
BSTreeNode(const k& key)
: _left(nullptr)
, _right(nullptr)
, _key(key)
{
}
};
2.2 二叉搜索树的插入
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
非递归实现
bool Insert(const k& key)
{
if (_root == nullptr)//根为空,直接new一个新节点作为根
{
_root = new Node(key);
return true;
}
//根不为空,从根开始依次往下找,直到找到插入结点的位置(即cur为空)
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_key < key)//key比当前结点的key要大,去右子树查找
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)//key比当前结点的key要小,去左子树查找
{
parent = cur;
cur = cur->_left;
}
else//二叉搜索树中不允许存储key值相同的结点
{
return false;
}
}
cur = new Node(key);
if (cur->_key < parent->_key)//判断新new出的结点和parent的关系,将两者连接起来
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
递归实现
bool InsertR(const k& key)
{
return _InsertR(_root, key);
}
bool _InsertR(Node*& root, const k& key)
{
if (root == nullptr)//根为空,直接new出一个新节点当做根
{
root = new Node(key);
return true;
}
if (root->_key < key)//key大于当前结点的key,去右子树查找
{
_InsertR(root->_right, key);
}
else if (root->_key > key)//key小于当前结点的key,去左子树查找
{
_InsertR(root->_left, key);
}
else if (root->_key == key)//key等于当前结点的key,返回false
{
return false;
}
}
1 因为_root为私有成员变量,在类外面不能使用,但是递归需要传递_root作为参数,因此这里做了两层嵌套,在类外面调用InsertR()函数,不需要传递任何参数,在类里面通过InsertR()函数调用_InsertR(Node*& root, const k& key) 函数,在类里面可以使用私有变量,从而实现插入操作
2 乍一看代码,只是有查找插入结点的位置,那么是如何插入该结点的呢?这里引用起了很重要的作用
假设插入的值为16,当走到14的时候,通过比较,16大于14,所以应去14的右子树查找,此时把把14->_right传递给了root,因为是引用,root就是14->_right的别名,因为此时root为空,所以直接new一个新结点,即root=new Node(16),也就是 14->_right=new Node(16),一步,new出了新结点也实现了连接
2.2 二叉搜索树的查找
①从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
②最多查找高度次,走到空,还没找到,这个值不存在。
非递归实现
bool find(const k& key)
{
if (_root == nullptr)//根为空,返回false
{
return false;
}
Node* cur = _root;//从根节点开始查找
while (cur)
{
if (cur->_key < key)//key大于当前结点的key,去右子树查找
{
cur = cur->_right;
}
else if (cur->_key > key)//key小于当前结点的key,去左子树查找
{
cur = cur->_left;
}
else//找到了
{
return true;
}
}
//查找到空还没找到,说明没有这个结点
return false;
}
递归实现
bool FindR(const k& key)
{
return _FindR(_root, key);
}
bool _FindR(Node* root, const k& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
_FindR(root->right, key);
}
else if (root->_key > key)
{
_FindR(root->left, key);
}
else
{
return true;
}
}
2.3 二叉搜索树的删除
删除的具体过程:
首先查找元素是否在二叉搜索树中,如果不存在,则返回false, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来待删除节点有四种情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程
如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
情况d:使用替换法,找该删除结点的左子树中的最大结点或右子树中的最小结点与该结点进行替换,然后删除该结点。
情况d中,使用替换法的原因是删除结点以后,该树还要保持二叉搜索树的结构,因此需要找一个等效结点,替换该结点的位置。并且在替换后该结点可以直接进行删除,那么进行替换的这个节点要么没有孩子,要么只有一个孩子。即左子树的最右结点或右子树的最左结点。
非递归实现
bool Erase(const k& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)//查找该结点
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else//找到了要删除的结点
{
if (cur->_left == nullptr)//左为空
{
if (cur == _root)//先要考虑cur为根的情况
{
_root = cur->_right;
}
else//判断要删除的结点和父结点的关系
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else if (cur == parent->_right)
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)//右为空
{
if (cur == _root)//先要考虑cur为根的情况
{
_root = cur->_left;
}
else//判断要删除的结点和父结点的关系
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else if (cur == parent->_right)
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else//左右都不为空
{
//找右子树最小的(即右子树的最左结点),该结点要么没有孩子结点,要么只有一个右孩子结点
Node* min = cur->_right;
Node* minParent = cur;
while (min->_left)//可能不进循环,即cur->_right就是右子树最小的结点,所以minParent不能为空
{
minParent = min;
min = min->_left;
}
swap(cur->_key, min->_key);//找到右子树最小结点后,交换二者的_key值
if (minParent->_left == min)//判断min结点和minParent结点的相对位置
{
minParent->_left = min->_right;
}
else
{
minParent->_right = min->_right;
}
delete min;
min = nullptr;
}
return true;
}
}
//没有找到该结点
return false;
}
递归实现
bool EraseR(const k& key)
{
return _EraseR(_root, key);
}
bool _EraseR(Node*& root, const k& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)//先查找该结点的位置
{
_EraseR(root->_right, key);
}
else if (root->_key > key)
{
_EraseR(root->_left, key);
}
else//找到了
{
Node* del = root;
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;
}
}
再来看看引用在递归删除时候的妙用
假设删除的结点为14
当找到14的时候,此时的root也就是10->_right的别名,因为14的右结点为空,所以root=root->_left也就是10->_right=14->_left ,之后再删除14这个结点,便可完成删除操作
2.4 二叉搜索树的默认成员函数
2.4.1 拷贝构造
BSTree(const BSTree<k>& t)
{
_root = _copyNode(t._root);
}
Node* _copyNode(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* copy = new Node(root->_key);//先拷贝构造出根节点
copy->_left = _copyNode(root->_left);//递归拷贝构造左子树
copy->_right = _copyNode(root->_right);//递归拷贝构造右子树
return copy;
}
2.4.2 析构函数
~BSTree()
{
_destroy(_root);
}
void _destroy(Node* root)
{
if (root == nullptr)
{
return;
}
_destroy(root->_left);//递归析构左子树
_destroy(root->_right);//递归析构右子树
delete root;//删除根节点
root = nullptr;
}
2.4.3 赋值重载
BSTree<k>& operator==(const BSTree<k> t)
{
swap(this->_root, t._root);//简化版的现代写法
return *this;
}
3 二叉搜索树的应用
3.1 k模型
K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
3.2 kv模型
KV模型:每一个关键码key,都有与之对应的值value,即<Key, value>的键值对。该种方
式在现实生活中非常常见:
①英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<english, chinese>就构成一种键值对;
②再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。
改造二叉搜索树为kv结构,即给结点增加一个数据域value,但是在查找的时候还是根据key值去查找
template <class k, class v>
struct BSTreeNode
{
BSTreeNode<k, v>* _left;
BSTreeNode<k, v>* _right;
k _key;
v _value;
BSTreeNode(const k& key, const v& value)
: _left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{
}
};
template <class k, class v>
class BSTree
{
typedef BSTreeNode<k, v> Node;
public:
bool Insert(const k& key, const v& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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, value);
if (cur->_key < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
Node* find(const k& key)
{
if (_root == nullptr)
{
return nullptr;
}
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
bool Erase(const k& key)
{
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << " ";
_InOrder(root->_right);
//递归需要传递参数,但是_root为私有变量,在类外面不能访问,所以套了两层函数
}
Node* _root = nullptr;
};
简单的中英互译
void test1()
{
BSTree<string, string>t;
t.Insert("apple", "苹果");
t.Insert("banana", "香蕉");
t.Insert("left", "左边");
t.Insert("right", "右边");
string str;
while (cin >> str)
{
BSTreeNode<string,string>* ret = t.find(str);
if (ret)
{
cout << "对应的中文:" << ret->_value << " ";
cout << endl;
}
else
{
cout << "对应的中文:" << "没有对应的中文" << " ";
cout << endl;
}
}
}
统计次数
void test2()
{
string arr[] = { "苹果","香蕉","苹果","橘子","香蕉","苹果","香蕉" };
BSTree<string, int>countTree;
for (auto& str : arr)
{
BSTreeNode<string, int>* ret = countTree.find(str);
if (ret)
{
ret->_value++;
}
else
{
countTree.Insert(str, 1);
}
}
countTree.InOrder();
}
4 二叉搜索树的性能分析
插入和删除操作都必须先查找,所以查找效率代表了二叉搜索树中各个操作的性能
对于二叉搜索树的查找,走的就是从根节点到要查找的结点的路径,其比较次数为给定结点在二叉搜索树中的层数,最少可能为1次,即根结点就是要找的结点,最坏情况下为高度次。
所以二叉搜索树的查找性能取决于二叉搜索树的形状
理想形状:二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为O(log2n)
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为O(N)
当退化为单支的时候,二叉搜索树的性能就失去了,如何解决呢?且听下回分析