二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
它的左右子树也分别为二叉搜索树。
如:
二叉搜索树的操作
查找
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
插入
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回。
如果存在,可以分为下面三种情况:
1.删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
2.删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
3.在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
中,再来处理该结点的删除问题–替换法删除(与堆中删除数据类似)
二叉搜索树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
a.以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
b.在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。 - **KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。**该种方
式在现实生活中非常常见:
a.比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>就构成一种键值对;
b.再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对.
二叉搜索树的实现
K模型
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _l;
BSTreeNode<K>* _r;
K _k;
BSTreeNode(const K& k)
:_l(nullptr)
, _r(nullptr)
, _k(k)
{}
};
首先定义一个二叉搜索树K模型的结构体。同二叉树一样包含左右孩子节点,还有_k值,一般情况下,二叉搜索树的_k值是唯一的,因为当前节点左孩子的_k小于当前节点的_k,而右孩子的则比当前节点的_k大,所以当插入一个在二叉搜索树中存在的值时,会返回false,不进行插入。
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool insert(const K& k)
{
if (_root == nullptr)
{
_root = new Node(k);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
parent = cur;
cur = cur->_r;
}
else if (cur->_k > k)
{
parent = cur;
cur = cur->_l;
}
else
{
return false;
}
}
cur = new Node(k);
if (parent->_k < k)
{
parent->_r = cur;
}
else
{
parent->_l = cur;
}
return true;
}
在二叉搜索树中查找结点比较简单,根据二叉搜索树的性质,左子树小于根小于右子树能确定,当x大于当前节点的_k时,去右子树中找,小于则去左子树中找,遇到空节点则说明不存在。
bool find(const K& k)
{
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
cur = cur->_r;
}
else if (cur->_k > k)
{
cur = cur->_l;
}
else
{
return true;
}
}
return false;
}
二叉搜索树的删除则比较麻烦,首先因为删除节点后要保证树的完整性,所以find节点时也要标记当前节点的父节点,当找到要删除的节点时,就要判断该节点的孩子节点的情况,可以分为左孩子为空、右孩子为空、左右孩子都存在。(左右孩子都不存在并入1、2情况中)
a.左孩子节点为空:
首先判断当前节点是否为根节点,如果是根节点,直接使右孩子为根节点
若不是根节点,则判断当前节点是父节点的左孩子还是右孩子,如果是左孩子,则父节点的右指针指向该节点的右孩子,否则让父节点的左指针指向当前节点的右孩子
b.右孩子节点为空:
同样先判断当前节点是否为根节点,为根节点则让根节点为左孩子节点
再判断当前节点是父节点左孩子还是右孩子,为左孩子则让父节点的左指针指向被删节点的左孩子,反之让父节点的右指针指向当前节点的左孩子
c.被删节点左右孩子都存在,此时用交换法使被删节点成为前面只有单个孩子或者没有孩子节点的情况。我们可以让被删节点与右子树的最左的节点或左子树的最右节点交换。
交换的合法性:
1.二叉搜索树的性质右>根>左,对于当前节点的右子树的最左节点,在右子树中,最左代表最小,所以它比右子树其他节点小,但它又在被删节点的右子树中,所以它比被删节点大,同时就比被删节点的左子树的所有结点大,所以交换后对右子树最左结点没有影响。
2.因为是右子树的最左结点,它没有左孩子,交换后可以将被删结点带入a情况解决。
左子树的最右节点同理。
bool erase(const K& k)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
parent = cur;
cur = cur->_r;
}
else if (cur->_k > k)
{
parent = cur;
cur = cur->_l;
}
else
{
if (cur->_l == nullptr)
{
if (cur == _root)
{
_root = cur->_r;
}
else
{
if (cur == parent->_l)
{
parent->_l = cur->_r;
}
else
{
parent->_r = cur->_r;
}
}
delete cur;
}
else if (cur->_r == nullptr)
{
if (cur == _root)
{
_root = cur->_l;
}
else
{
if (cur == parent->_l)
{
parent->_l = cur->_l;
}
else
{
parent->_r = cur->_l;
}
}
delete cur;
}
else
{
Node* rightMinParent = cur;
Node* rightMin = cur->_r;
while (rightMin->_l)
{
rightMinParent = rightMin;
rightMin = rightMin->_l;
}
swap(cur->_k, rightMin->_k);
if (rightMinParent->_l == rightMin)
{
rightMinParent->_l = rightMin->_r;
}
else
{
rightMinParent->_r = rightMin->_r;
}
delete rightMin;
}
return true;
}
}
}
完整代码
namespace K_model
{
//K模型
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _l;
BSTreeNode<K>* _r;
K _k;
BSTreeNode(const K& k)
:_l(nullptr)
, _r(nullptr)
, _k(k)
{}
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool insert(const K& k)
{
if (_root == nullptr)
{
_root = new Node(k);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
parent = cur;
cur = cur->_r;
}
else if (cur->_k > k)
{
parent = cur;
cur = cur->_l;
}
else
{
return false;
}
}
cur = new Node(k);
if (parent->_k < k)
{
parent->_r = cur;
}
else
{
parent->_l = cur;
}
return true;
}
bool find(const K& k)
{
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
cur = cur->_r;
}
else if (cur->_k > k)
{
cur = cur->_l;
}
else
{
return true;
}
}
return false;
}
bool erase(const K& k)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
parent = cur;
cur = cur->_r;
}
else if (cur->_k > k)
{
parent = cur;
cur = cur->_l;
}
else
{
if (cur->_l == nullptr)
{
if (cur == _root)
{
_root = cur->_r;
}
else
{
if (cur == parent->_l)
{
parent->_l = cur->_r;
}
else
{
parent->_r = cur->_r;
}
}
delete cur;
}
else if (cur->_r == nullptr)
{
if (cur == _root)
{
_root = cur->_l;
}
else
{
if (cur == parent->_l)
{
parent->_l = cur->_l;
}
else
{
parent->_r = cur->_l;
}
}
delete cur;
}
else
{
Node* rightMinParent = cur;
Node* rightMin = cur->_r;
while (rightMin->_l)
{
rightMinParent = rightMin;
rightMin = rightMin->_l;
}
swap(cur->_k, rightMin->_k);
if (rightMinParent->_l == rightMin)
{
rightMinParent->_l = rightMin->_r;
}
else
{
rightMinParent->_r = rightMin->_r;
}
delete rightMin;
}
return true;
}
}
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_l);
cout << root->_k;
_InOrder(root->_r);
}
private:
Node* _root = nullptr;
};
}
测试一下:
void testBSTree1()
{
int a[] = { 3,7,5,9,11,3,2,6,4,1 ,8};
K_model::BSTree<int> t1;
for (auto& e : a)
{
t1.insert(e);
}
t1.InOrder();
t1.erase(8);
t1.InOrder();
for (auto& e : a)
{
t1.erase(e);
t1.InOrder();
}
}
KV模型
KV模型的具体思路和代码实现与K模型几乎一样,只需加入一个V值与k对应即可
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
bool Insert(const K& k, const V& v)
{
if (_root == nullptr)
{
_root = new Node(k,v);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
parent = cur;
cur = cur->_r;
}
else if (cur->_k > k)
{
parent = cur;
cur = cur->_l;
}
else
{
return false;
}
}
cur = new Node(k,v);
if (parent->_k < k)
{
parent->_r = cur;
}
else
{
parent->_l = cur;
}
return true;
}
Node* Find(const K& k)
{
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
cur = cur->_r;
}
else if (cur->_k > k)
{
cur = cur->_l;
}
else
{
return cur;
}
}
return nullptr;
}
bool Erase(const K& k)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_k < k)
{
parent = cur;
cur = cur->_r;
}
else if (cur->_k > k)
{
parent = cur;
cur = cur->_l;
}
else
{
if (cur->_l == nullptr)
{
if (cur == _root)
{
_root = cur->_r;
}
else
{
if (cur == parent->_l)
{
parent->_l = cur->_r;
}
else
{
parent->_r = cur->_r;
}
}
delete cur;
}
else if (cur->_r == nullptr)
{
if (cur == _root)
{
_root = cur->_l;
}
else
{
if (cur == parent->_l)
{
parent->_l = cur->_l;
}
else
{
parent->_r = cur->_l;
}
}
delete cur;
}
else
{
Node* rightMinParent = cur;
Node* rightMin = cur->_r;
while (rightMin->_l)
{
rightMinParent = rightMin;
rightMin = rightMin->_l;
}
swap(cur->_k, rightMin->_k);
if (rightMinParent->_l == rightMin)
{
rightMinParent->_l = rightMin->_r;
}
else
{
rightMinParent->_r = rightMin->_r;
}
delete rightMin;
}
return true;
}
}
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_l);
cout << root->_k << ":" << root->_v << endl;
_InOrder(root->_r);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
Node* _root = nullptr;
};
测试:
void TestBSTree()
{
BSTree<string, string> dict;
dict.Insert("insert", "插入");
dict.Insert("erase", "删除");
dict.Insert("left", "左边");
dict.Insert("string", "字符串");
dict.Insert("right", "右边");
string str;
while (cin >> str)
{
auto ret = dict.Find(str);
if (ret)
{
cout << str << ":" << ret->_v << endl;
}
else
{
cout << "单词拼写错误" << endl;
}
}
string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };
// 统计水果出现的次
BSTree<string, int> countTree;
for (auto str : strs)
{
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_v++;
}
}
countTree.InOrder();
}
二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:lgN
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N