二叉搜索树前言:
二叉搜索树是红黑树的铺垫,红黑树是二叉搜索树的优化,学懂二叉树再学红黑树就会相对轻松一点
目录
- 二叉搜索树的特点
- 二叉树的操作
- 二叉搜索树的实现
- 二叉搜索树的缺点
二叉搜索树的特点
- 左子树比根结点的键小
- 右子树比根结点的键大
- 一棵树里没有键相等的两个结点
键值对介绍
键值对是什么?
键值对是一个能存储两个数据的结构体,数据分为键和值,其中一个数据(键)代表着整体数据(进行运算时只考虑其中一个数据,这个数据就叫做键)
为什么有键值对的出现?
通常我们需要将两个数据存储到同一容器,这样就能方便通过键直接找到值。
例如:我们不用键值对存储,我们就要将两个数据存储到两个容器,遍历一遍找到了一个数据,而又要找这个数据的对应数据时,又必须遍历另一个容器,会大大降低效率
二叉树的操作
二叉树的查找:
a、
要查找的数据x从根开始比较。
若x比根的数据小,则往根的左子树去找。
若x比根的数据大,则往根的右子树去找。
b、
最多查找高度次,若找到空,则说明该树里没这个数据
二叉树的插入:
a、
要插入的数据x从根开始比较。
若x比根的数据小,则往根的左子树去找。
若x比根的数据大,则往根的右子树去找。
若x和根的数据相等,则插入失败。(不允许有两个相等的数据)
b、
直到找到为空,创建一个新结点,再将新结点连接起来
二叉树的删除:
情况一:
要删除的结点无左孩子或右孩子
情况二:
要删除的孩子既有左孩子又有右孩子
这种情况就需要用替换法,删除替换掉的结点
谁去替换呢?
要删除的数据x,找到与x数据大小最相近的数据替换即可
a、找到x结点左子树的最右结点
b、或者找到x结点右子树的最左结点
a、找到x结点左子树的最右结点
b、找到x结点右子树的最左结点
二叉搜索树的实现
二叉树的插入:
二叉树的删除:
参考代码:
template<class K, class V>
struct BSTreeNode
{
BSTreeNode(K key = K(), V val = V())
:_key(key)
, _val(val)
{
}
K _key;
V _val;
BSTreeNode* _left = nullptr;
BSTreeNode* _right = nullptr;
};
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
bool Insert(const K& key, const V& val)
{
if (_root == nullptr)
{
_root = new Node(key, val);
return true;
}
Node* cur = _root;
Node* prev = nullptr;
while (cur)
{
if (cur->_key < key)
{
prev = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
prev = cur;
cur = cur->_left;
}
else
{
return false;
}
}
Node* newNode = new Node(key, val);
if (prev->_key > key)
{
prev->_left = newNode;
return true;
}
else
{
prev->_right = newNode;
return true;
}
}
void Erase(const K& key)
{
if (_root == nullptr)
return;
Node* cur = _root;
Node* prev = nullptr;
while (cur)
{
if (cur->_key > key)
{
prev = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
prev = cur;
cur = cur->_right;
}
else
{
break;
}
}
if (cur == nullptr)
return;
if (cur->_right == nullptr)
{
if (_root == cur)
{
_root = cur->_left;
}
else if (prev->_left == cur)
{
prev->_left = cur->_left;
}
else
{
prev->_right = cur->_left;
}
delete cur;
}
else if (cur->_left == nullptr)
{
if (_root == cur)
{
_root = cur->_right;
}
else if (prev->_left == cur)
{
prev->_left = cur->_right;
}
else
{
prev->_right = cur->_right;
}
delete cur;
}
else
{
Node* curFind = cur->_right;
Node* FindPrev = cur;
while (curFind->_left)
{
FindPrev = curFind;
if (curFind)
curFind = curFind->_left;
}
cur->_key = curFind->_key;
if (FindPrev->_right == curFind)
{
FindPrev->_right = cur->_right;
}
else
{
FindPrev->_left = curFind->_right;
}
delete curFind;
}
}
Node* 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 cur;
}
}
return nullptr;
}
void _InOrder(Node* _root)
{
if (_root == nullptr)
{
return;
}
_InOrder(_root->_left);
cout << _root->_key << ":" << _root->_val << " ";
_InOrder(_root->_right);
}
void InOrder()
{
_InOrder(this->_root);
cout << endl;
}
private:
Node* _root = nullptr;
};
void test()
{
BSTree<int, int> t1;
t1.Insert(3, 33);
t1.Insert(2, 22);
t1.Insert(9, 99);
t1.Insert(4, 44);
t1.Insert(8, 88);
t1.Insert(4, 44);
t1.Insert(5, 55);
t1.Insert(11, 1111);
t1.InOrder();
t1.Erase(3);
t1.InOrder();
}
void test2()
{
string strArr[] = { "西瓜", "西瓜", "西瓜", "苹果", "樱桃", "香蕉", "哈密瓜", "苹果" };
BSTree<string, int> coutTree;
for (auto& str : strArr)
{
BSTreeNode<string, int>* cur = coutTree.Find(str);
if (cur == nullptr)
{
coutTree.Insert(str, 1);
}
else
{
cur->_val++;
}
}
coutTree.InOrder();
}
二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:
l
o
g
2
N
log_2 N
log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:
N
2
\frac{N}{2}
2N
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了