目录
搜索二叉树(BinarySearchTree),又成为查找二叉树。它要么是一颗空树,要么是具有以下性质的树:
- 某一结点,它的左子树存在,则左子树上的所有节点的值都小于根结点的值。
- 某一结点,它的右子树存在,则右子树上的所有节点的值都大于根结点的值。
- 它的左右子树也分别都是二叉搜索树。
int a[] = { 10, 5, 1, 6, 20, 15, 15, 25};
2.二叉搜索树的操作
2.1 二叉搜索树的查找
2.2 二叉树的插入
插入具体过程如下:
a.树为空,直接插入
b.树不空,按搜索二叉树性质查找插入位置,插入新节点
2.3 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
代码中,我们可以将a 和b或c合为一种情况。一共为三种情况
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中, 再来处理该结点的删除问题 (可以找左子树的最右节点或者右子树的最左节点,本文为右子树的的最左节点,也就是右子树,key值最小的节点)
三.二叉搜索树的实现
我们实现采用可以泛型编程,可以复用的形式(采用模板)
下面为二叉树的节点代码,我们这里设置节点为存储一对key,value的值
template <class K,class V> //Binary Search Tree
struct BTreeNode
{
//左孩子指针
BTreeNode<K, V>* _left;
//右孩子指针
BTreeNode<K, V>* _right;
K _key;
V _value;
//有参构造函数 使用参数列表的形式实现成员变量的初始化 初始节点的左右子树均为空
BTreeNode(const K& key, const V& value)
:_left(nullptr)
,_right(nullptr)
,_key(key)
,_value(value)
{
}
};
下面为二叉搜索树类的实现
template <class K, class V> //Binary Search Tree
class BSTree
{
typedef BTreeNode<K, V> Node;
public:
//不允许有重复元素 插入失败返回false
bool insert(const K& key, const V& value)
{
//此时树为空 new一个新的空间 并将该节点置为根节点
if (this->_root == nullptr)
{
//节点为空
//需要给节点开辟空间
_root = new Node(key, value);
return true;
}
//若不空
Node* cur = _root;
Node* pre = nullptr;
//查询插入位置
while (cur != nullptr)
{
//到左子树查询
if (cur->_key < key)
{
pre = cur;
cur = cur->_right;
}
//到右子树查询
else if (cur->_key > key)
{
pre = cur;
cur = cur->_left;
}
//若树中已存在key值为key的节点 则插入失败 返回false
else
{
return false;
}
}
//找到插入位置 需注意 每次插入的节点都为叶子节点
cur = new Node(key, value);
//需要判断是往左子树插还是右子树
if (pre->_key > key)
pre->_left = cur;
else
pre->_right = cur;
//插入成功返回true
return true;
}
//中序遍历子函数
void _InOrder(Node* root)
{
if (root == NULL)
return;
//左子树不空 递归进入左子树
_InOrder(root->_left);
cout << root->_key << "\t:" << root->_value << endl;
//右子树不空递归进入右子树
_InOrder(root->_right);
}
//中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//查找key值为key节点的函数
Node* Find(const K& key)
{
Node* cur = this->_root;
while (cur)
{
if (cur->_key > key)
cur = cur->_left;
else if (cur->_key < key)
cur = cur->_right;
else
//查找成功 返回该节点
return cur;
}
return nullptr;
}
//删除key值为key的节点
bool Erase(const K& key)
{
//找节点
Node* cur = _root;
Node* pre = nullptr;
while (cur)
{
//按照二叉搜索树的规则查找
if (cur->_key > key)
{
pre = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
pre = cur;
cur = cur->_right;
}
else
{
//找到了 开始删除
//叶子节点 直接删除
//非叶子节点
//只有一个子树的话或者为叶节点时 直接用她的父亲节点链接他的子树
if (cur->_left == NULL || cur->_right == NULL)
{
// 左子树为空
if (cur->_left == nullptr)
{
//需要判断该节点是否为根节点 若为根节点 则需要将根节点重新赋值
if(cur!=_root)
{
if (cur == pre->_left)
pre->_left = cur->_right;
else
pre->_right = cur->_right;
}
else
{
_root = cur->_right;
}
//一定要delete 不然会造成内存泄漏
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if(cur!=_root)
{
if (cur == pre->_left)
pre->_left = cur->_left;
else
pre->_right = cur->_left;
}
else
{
_root = cur->_left;
}
delete cur;
cur = nullptr;
}
}
//否则用左子树的最右节点或右子树的最左节点替换 将替换的删除掉
//这里替换的节点一定是满足上面的两种情况的 可以直接删除
//因为右子树的最左节点一定是没有左子树的
else
{
//右子树的最左节点
Node* rightMin = cur->_right;
Node* rightMinPre = cur;
while (rightMin->_left)
{
rightMinPre = rightMin;
rightMin = rightMin->_left;
}
//替代
cur->_key = rightMin->_key;
cur->_value = rightMin->_value;
//删除rightMin
if (rightMinPre->_left == rightMin)
rightMinPre->_left = rightMin->_right;
else
rightMinPre->_right = rightMin->_right;
delete rightMin;
rightMin = nullptr;
}
}
}
return false;
}
private:
Node* _root = nullptr;
};
2.4 测试
这里用一个数组进行测试,中序遍历的结果为一个有序序列
void testBSTree()
{
BSTree<int, int> t;
int a[] = { 5,3,4,1,7,8,2,6,0,9 };
for (auto e : a)
{
t.insert(e, e);
}
t.InOrder();
}
测试结果:
2.5 二叉搜索树的性能分析
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的 深度的函数,即结点越深,则比较次数越多。 但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

完全按搜索二叉树 他的性能最优,为O(LogN) N为二叉树节点个数

上面这种二叉树性能最差,为O(N)
为了解决上述这种情况,又提出了AVL树和红黑树。将在后续文章继续给大家讲解。
3.二叉搜索树的应用
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
(1).以单词集合中的每个单词作为key,构建一棵二叉搜索树
(2).在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型:每一个关键码key,都有与之对应的值Value,即的键值对。该种方式在现实生 活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英 文单词与其对应的中文就构成一种键值对;再比如统计单词次数,统计成功后,给定 单词就可快速找到其出现的次数,单词与其出现次数就是就构成一种键值对。
比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
(1).为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较 Key
(2).查询英文单词时,只需给出英文单词,就可快速找到与其对应的key
void TestBSTree()
{
BSTree<string,string> t;
t.insert("insert", "插入");
t.insert("sort", "排序");
t.insert("shuai", "帅");
t.insert("bst", "二叉搜索树");
t.insert("number", "数字");
string str;
while (cin >> str)
{
BTreeNode<string,string>* ret = t.Find(str);
if (ret)
{
cout << ret->_value << endl;
}
else
cout << "查找失败!" << endl;
}
t.InOrder();
}
统计数量
void TestBSTree()
{
string strArr[] = { "苹果","苹果", "苹果", "西瓜", "苹果", "西瓜", "西瓜", "香蕉", "苹果", "香蕉", "苹果", "苹果", "苹果", "西瓜", };
BSTree<string, int> t;
for (auto str : strArr)
{
BTreeNode<string, int>* ret = t.Find(str);
if (ret)
{
ret->_value++;
}
else
t.insert(str, 1);
}
t.InOrder();
}