文章目录
- 一、二叉搜索树概念
- 二、二叉搜索树操作
- 1. 二叉搜索树的查找
- 2. 二叉搜索树的插入
- 3. 二叉搜索树的删除
- 三、二叉搜索树的实现
- 四、二叉搜索树的性能分析
一、二叉搜索树概念
二叉搜索树又称二叉排序树/二次查找树,它是一棵空树或者是每颗子树都具有以下性质的二叉树
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
二、二叉搜索树操作
1. 二叉搜索树的查找
- 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
- 最多查找高度次,走到到空,还没找到,这个值不存在。
2. 二叉搜索树的插入
- 树为空,则直接新增节点,赋值给root指针。
- 树不空,按二叉搜索树性质查找插入位置,插入新节点。
3. 二叉搜索树的删除
- 左为空,父亲指向他的右。 也就是说左子节点为空,让它父节点指向该节点的右子节点,再直接删除该节点。
- 右为空,父亲指向他的左。 也就是说右子节点为空,让它父节点指向该节点的左子节点,再直接删除该节点。
- 左右子节点都不为空时,使用替换法删除
在第一节的例子中,删除1、4、7、13、14、10节点属于前两种情况,删除3、8、10、6节点属于第三种情况。
替换法删除
找到左子树的最右节点或者右子树的最左节点,替换该节点赋值给删除节点,直接删除替换节点,因为替换节点没子节点或者只有一个子节点,再归类到前两种情况。
修改
K模型的搜索二叉树不支持修改,增删查的时间复杂度为O(h),h是树的高度,最坏的情况是h为N。
三、二叉搜索树的实现
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。 该种方式在现实生活中非常常见,例如:刷卡进楼。
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
: _left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
template<class K> //key
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr; //这里父节点指针给nullptr不会报错,因为考虑极端情况只有一个节点,那也会进入下面的循环中,执行parent = cur;,所以父节点->不会报错
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;
}
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;
}
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) // 左为空 ,父亲指向它的右
{
if (cur == _root)//考虑极端情况删除的是_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 (cur == _root)//考虑极端情况删除的是_root根节点的情况
{
_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; // 考虑极端情况,要删除的是根节点,所以minparent不可以为空
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(cur->_key, min->_key);
if (minParent->_left == min) //判断minParent和min的关系
{
minParent->_left = min->_right;
}
else
minParent->_right = min->_right;
delete min;
min = nullptr;
}
return true;
}
}
return false;
}
void InOrder()//中序遍历打印
{
_InOrder(_root);//套用了一层,第一次传过去了_root,后面递归就传他的子树
cout << endl;
}
bool FindR(const K& key)//递归版本的查找
{
return _Find(_root, key);
}
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
BSTree() = default;//C++11 强制编译器生成默认的构造
~BSTree()
{
_Destsory(_root);
}
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
// 传值传参
BSTree<K>& operator = (BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
private:
Node* _Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* copyRoot = new Node(root->_key);
copyRoot->_left = _Copy(root->_left);
copyRoot->_right = _Copy(root->_right);
return copyRoot;
}
void _Destsory(Node*& root)
{
if (root == nullptr)
return;
_Destsory(root->_left);
_Destsory(root->_right);
delete root;
root = nullptr;
}
//_InOrder(_root) 递归调用不能写这个,不然每次传过去都是根节点,只能第一次传递根节点,用一个形参cur来接收根节点,
void _InOrder(Node* cur) //递归必须 显示 的传递子树,而在外面调用的时候要传根节点_root,但是拿不到私有的_root。可以写一个getroot函数去拿到_root,或者像这样
{
if (cur == nullptr)
return;
_InOrder(cur->_left);
cout << cur->_key << " ";
_InOrder(cur->_right);
}
bool _Find(Node* cur, const K& key)
{
if (cur == nullptr)
return false;
if (cur->_key < key)
return _Find(cur->_right, key);
else if (cur->_key > key)
return _Find(cur->left, key);
else
return true; //return true 按顺序依次从开辟的栈帧返回到最外层
}
bool _InsertR(Node*& cur, const K& key)
{
if (cur == nullptr)
{
cur = new Node(key); //cur是一个局部变量,所以要用引用
return true;
}
if (cur->_key < key)
return _InsertR(cur->_right, key);
else if (cur->_key > key)
return _InsertR(cur->_right, key);
else
return false; //天然去重
}
bool _EraseR(Node*& cur, const K& key)
{
if (cur == nullptr)
return false;
if (cur->_key < key)
return _EraseR(cur->_right, key);
else if (cur->_key > key)
return _EraseR(cur->_left, key);
else //删除
{
Node* del = cur;
if (cur->_left == nullptr) //左为空
{
cur = cur->_right;
}
else if (cur->_right == nullptr) //右为空
{
cur = cur->_left;
}
else //左右都不为空
{
//找右树的最左节点
Node* min = cur->_right;
while (min->_left)
{
min = min->_left;
}
swap(cur->_key, min->_key);
return _EraseR(cur->_right, key); //从这个点的右树开始删除key
}
delete del;
del = nullptr;
return true;
}
}
Node* _root = nullptr;
};
应用:
int main()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (auto& e : a)
{
t.Insert(e);
}
//排序+去重
t.InOrder();
t.Erase(3);
t.InOrder();
}
- KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。 该种方式在现实生活中也非常常见,例如:统计某物品出现的次数
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* 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, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
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;
}
bool Erase(const K& key)
{
//...
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
应用:
int main()
{
// 统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}
四、二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树
- 最优情况下,二叉搜索树为完全二叉树,或者接近完全二叉树,其平均比较次数为:log2 N
- 最差情况下,二叉搜索树退化为单支树,或者类似单支,其平均比较次数为:N
如果退化成单支树,二叉搜索树的性能就失去了。将二叉搜素树改进为AVL树和红黑树,不论按照什么次序插入关键码,性能都能达到最优。