二叉搜索树的概念
二叉搜索树又叫做二叉排序树,他是一个具有以下特性的二叉树。
1. 二叉搜索树的左孩子比父节点小,右孩子比父节点大。
2. 二叉搜索树的左子树的全部节点都小于根节点,右子树的全部节点都大于根节点。
3. 所有节点的左右子树都为二叉搜索树
4. 键值是唯一的,所以二叉搜索树不能有相同的键值。
例如以下数据
int arr[] = { 5, 3, 7, 9, 1, 4 };
按照上面的规则构建成为一个二叉搜索树
根据使用场景的不同,二叉搜索树还分为K模型和KV模型。
K模型:即只有key作为关键码,只需要存储Key即可,关键码即为需要搜索到的值。如STL中的set
KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。如STL中的map
二叉搜索树的作用
排序
中序遍历:先遍历左子树,再遍历根节点,再遍历右子树
而二叉搜索树的特性,左子树小于根节点,右子树大于根节点。
所以通过一趟中序遍历,即可获得排序的结果。
查找
对于二叉搜索树,查找是其主要的功能,STL中的map和set底层也是通过平衡二叉搜索树(红黑树)实现的。
二叉搜索树的查找十分简单,键值比根节点大则进入右子树,键值比根节点小则进入左子树,他的思路有点类似于二分查找,平均时间复杂度为O(log2N)
但是上述情况仅限于二叉搜索树为一个完全二叉树, 如果构建时树为有序数列,则二叉搜索树会退化为单支树,时间复杂度则会变为O(N)
如:
int arr[] = { 1, 3, 4, 5, 7, 9 };
如果要解决这个问题,就得为搜索二叉树加上平衡二叉树的属性,也就是我们通常所说的AVL树。
实现思路
查找
查找是二叉搜索树的核心,实现了这一部分后面的删除和插入也可以直接复用这里的部分代码
直接从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树,相同则返回。如果遍历完还没找到,则说明不存在此树中,返回nullptr
Node* Find(const K& key)
{
//根据二叉搜索树的性质,从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树
Node* cur = _root;
while (cur)
{
//比根节点大则查找右子树
if (key > cur->_key)
{
cur = cur->_right;
}
//比根节点小则查找左子树
else if (key < cur->_key)
{
cur = cur->_left;
}
//相同则返回
else
{
return cur;
}
}
//遍历完则说明查找不到,返回false
return nullptr;
}
插入
插入前半段就可以复用查找的代码,我们首先要找到我们需要插入的位置(为了保证能够找到父节点还需要用一个指针指向父节点),找到了合适的位置后,我们需要判断当前的键值比父节点大还是比父节点小,来决定应该插入到父节点的左子树还是右子树
例如这里要插入8
查找到所在位置
判断是哪一个子树即完成插入
bool Insert(const K& key)
{
//如果此时树为空,则初始化根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = cur;
//找到合适的插入位置
while (cur)
{
//比根节点大则查找右子树
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//比根节点小则查找左子树
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
//相同则返回false,因为搜索树不能存在相同数据
else
{
return false;
}
}
cur = new Node(key);
//判断cur要插入到parent的左子树还是右子树
if (parent->_key > key)
{
parent->_left = cur;
}
else if (parent->_key < key)
{
parent->_right = cur;
}
return true;
}
删除
相较于前面几个,删除会稍微复杂一点,因为对于非叶子节点,删除节点会导致搜索二叉树结构的破坏,所以不能直接删除,需要找到一个节点替换原有节点,再将原有节点删除。
删除分为三种情况。
1. 删除叶子节点
对于叶子节点1, 4, 9,我们可以直接的删除,不需要考虑别的。
2. 删除的节点只有一个子树
例如要删除这里的7,如果要保留原有节点,就需要让被删除节点的父节点,指向删除节点的唯一子树。
如:
3. 删除的节点有左右子树
这也是删除里最为麻烦的一块,例如这里的节点5。如果要删除5,就必须要找到一个能够替换5的节点来替换他,然后再将他删除,这样就不会破坏结构。而如何选择这个节点呢?如果要保持原来的结构,那么这个节点就必须要比左子树所有节点大,比右子树所有节点小,而符合的两个节点,则是左子树的最大节点,和右子树的最小节点。
也就是左子树的最右节点和右子树的最左节点。
这两个任意一个即可
首先我们要找到这个节点的位置,如这里选择的是4,我们就需要先找到这个4的位置。然后把4的值覆盖到被删除节点上。由于我们选择的是左子树的最右节点,所以这个节点必定没有右子树,但是不排除没有左子树。(这个图上没有,但是需要考虑有的情况,没有也没关系,反正接的是nullptr),所以需要借用到第二种情况的思路,把4的左子树接到父节点上后,再删除4.
再接着进一步分析,这里的第一二种情况可以合并处理,因为叶子节点的左右子树都为空,即使让父节点指向这两个空节点,也没有任何问题。
还有一种情况,如果删除的结点是根节点,则需要让删除节点的另一子树成为新的根节点。
bool erase(const K& key)
{
/*
删除有三种情况,一种是删除叶子节点,可以直接删除
第二种情况,如果删除的节点只有一个子树,那么删除这个节点后,就让父节点指向他的这一子树
前两种情况可以合并处理
第三种情况则是左右子树都不为空,此时选择一个来节点来替换他后,再删除,就可以不破坏原有结构
如果要保持原有结构不变化,那么选择的节点必须要和删除节点在中序遍历中是连续的,而满足的只有两个节点,一个是其左子树的最大值,一个是其右子树的最小值。
*/
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
//前两种情况合并处理,如果当前结点只有一个子树,则让父节点指向他的子树
//处理只有右子树时
if (cur->_left == nullptr)
{
//如果当前节点为根节点,则让右子树成为新的根节点
if (cur == _root)
{
_root = cur->_left;
}
else
{
//判断当前节点是他父节点的哪一个子树
if (parent->_right == cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
}
//处理只有左子树时
else if (cur->_right == nullptr)
{
//如果当前节点为根节点,则让左子树成为新的根节点
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
delete cur;
}
//处理左右子树都不为空时,选取左子树的最右节点或者右子树的最左节点
else
{
//这里我选取的是左子树的最右节点
Node* LeftMax = cur->_left;
Node* LeftMaxParent = cur;
//找到左子树的最右节点
while (LeftMax->_right)
{
LeftMaxParent = LeftMax;
LeftMax = LeftMax->_right;
}
//替换节点
std::swap(cur->_kv, LeftMax->_kv);
//判断当前节点是他父节点的哪一个子树, 因为已经是最右子树了,所以这个节点的右子树为空,但是左子树可能还有数据,所以让父节点指向他的左子树
//并且删除最右节点
if (LeftMax == LeftMaxParent->_left)
{
LeftMaxParent->_left = LeftMax->_left;
}
else
{
LeftMaxParent->_right = LeftMax->_left;
}
delete LeftMax;
}
return true;
}
}
return false;
}
删除、拷贝等
这几个和二叉树的删除拷贝一样,直接从根节点递归处理即可。
代码实现
K模型
#include<iostream>
namespace lee
{
template<class K>
class BSTreeNode
{
public:
BSTreeNode(const K& key)
: _left(nullptr)
, _right(nullptr)
, _key(key)
{}
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
typedef BSTree<K> Tree;
public:
BSTree() : _root(nullptr)
{}
~BSTree()
{
destory(_root);
}
BSTree(const Tree& temp) : _root(nullptr)
{
_root = copy(temp._root);
}
Tree& operator=(const Tree& temp)
{
if (this != &temp)
{
//先清空本树
destory(_root);
_root = copy(temp._root);
}
return *this;
}
//现代写法
//Tree& operator=(Tree temp)
//{
// swap(temp);
// return *this;
//}
void swap(Tree& temp)
{
std::swap(_root, temp._root);
}
//递归拷贝节点
Node* copy(const Node* root)
{
if(!root)
return nullptr;
Node* temp = new Node(root->_key);
temp->_left = copy(root->_left);
temp->_right = copy(root->_right);
return temp;
}
//递归销毁全部节点
void destory(Node*& root)
{
Node* node = root;
if (!root)
return;
destory(node->_left);
destory(node->_right);
delete node;
node = nullptr;
}
bool Insert(const K& key)
{
//如果此时树为空,则初始化根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = cur;
//找到合适的插入位置
while (cur)
{
//比根节点大则查找右子树
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//比根节点小则查找左子树
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
//相同则返回false,因为搜索树不能存在相同数据
else
{
return false;
}
}
cur = new Node(key);
//判断cur要插入到parent的左子树还是右子树
if (parent->_key > key)
{
parent->_left = cur;
}
else if (parent->_key < key)
{
parent->_right = cur;
}
return true;
}
bool erase(const K& key)
{
/*
删除有三种情况,一种是删除叶子节点,可以直接删除
第二种情况,如果删除的节点只有一个子树,那么删除这个节点后,就让父节点指向他的这一子树
前两种情况可以合并处理
第三种情况则是左右子树都不为空,此时选择一个来节点来替换他后,再删除,就可以不破坏原有结构
如果要保持原有结构不变化,那么选择的节点必须要和删除节点在中序遍历中是连续的,而满足的只有两个节点,一个是其左子树的最大值,一个是其右子树的最小值。
*/
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
//前两种情况合并处理,如果当前结点只有一个子树,则让父节点指向他的子树
//处理只有右子树时
if (cur->_left == nullptr)
{
//如果当前节点为根节点,则让右子树成为新的根节点
if (cur == _root)
{
_root = cur->_left;
}
else
{
//判断当前节点是他父节点的哪一个子树
if (parent->_right == cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
}
//处理只有左子树时
else if (cur->_right == nullptr)
{
//如果当前节点为根节点,则让左子树成为新的根节点
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
delete cur;
}
//处理左右子树都不为空时,选取左子树的最右节点或者右子树的最左节点
else
{
//这里我选取的是左子树的最右节点
Node* LeftMax = cur->_left;
Node* LeftMaxParent = cur;
//找到左子树的最右节点
while (LeftMax->_right)
{
LeftMaxParent = LeftMax;
LeftMax = LeftMax->_right;
}
//替换节点
cur->_key = LeftMax->_key;
//判断当前节点是他父节点的哪一个子树, 因为已经是最右子树了,所以这个节点的右子树为空,但是左子树可能还有数据,所以让父节点指向他的左子树
//并且删除最右节点
if (LeftMax == LeftMaxParent->_left)
{
LeftMaxParent->_left = LeftMax->_left;
}
else
{
LeftMaxParent->_right = LeftMax->_left;
}
delete LeftMax;
}
return true;
}
}
return false;
}
void _InordTravel(Node* root)
{
if (root == nullptr)
return;
//先遍历左子树
_InordTravel(root->_left);
//遍历根节点
std::cout << root->_key << std::ends;
//遍历右子树
_InordTravel(root->_right);
}
//提供给外界的接口,因为外界无法访问私有成员root
void InordTravel()
{
_InordTravel(_root);
std::cout << std::endl;
}
Node* Find(const K& key)
{
//根据二叉搜索树的性质,从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树
Node* cur = _root;
while (cur)
{
//比根节点大则查找右子树
if (key > cur->_key)
{
cur = cur->_right;
}
//比根节点小则查找左子树
else if (key < cur->_key)
{
cur = cur->_left;
}
//相同则返回
else
{
return cur;
}
}
//遍历完则说明查找不到,返回false
return nullptr;
}
private:
Node* _root;
};
}
KV模型
#include<iostream>
namespace lee
{
template<class K, class V>
class BSTreeNode
{
public:
BSTreeNode(const K& key = K(), const V& value = V())
: _left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key;
V _value;
};
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
typedef BSTree<K, V> Tree;
public:
BSTree() : _root(nullptr)
{}
~BSTree()
{
destory(_root);
}
BSTree(const Tree& temp) : _root(nullptr)
{
_root = copy(temp._root);
}
Tree& operator=(const Tree& temp)
{
if (this != &temp)
{
//先清空本树
destory(_root);
_root = copy(temp._root);
}
return *this;
}
//现代写法
//Tree& operator=(Tree temp)
//{
// swap(temp);
// return *this;
//}
void swap(Tree& temp)
{
std::swap(_root, temp._root);
}
Node* copy(const Node* root)
{
if (!root)
return nullptr;
Node* temp = new Node(root->_key, root->_value);
temp->_left = copy(root->_left);
temp->_right = copy(root->_right);
return temp;
}
void destory(Node*& root)
{
Node* node = root;
if (!root)
return;
destory(node->_left);
destory(node->_right);
delete node;
node = nullptr;
}
bool Insert(const K& key, const V& value)
{
//如果此时树为空,则初始化根节点
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* cur = _root;
Node* parent = cur;
//找到合适的插入位置
while (cur)
{
//比根节点大则查找右子树
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//比根节点小则查找左子树
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
//相同则返回false,因为搜索树不能存在相同数据
else
{
return false;
}
}
cur = new Node(key, value);
//判断cur要插入到parent的左子树还是右子树
if (parent->_key > key)
{
parent->_left = cur;
}
else if (parent->_key < key)
{
parent->_right = cur;
}
return true;
}
bool erase(const K& key)
{
/*
删除有三种情况,一种是删除叶子节点,可以直接删除
第二种情况,如果删除的节点只有一个子树,那么删除这个节点后,就让父节点指向他的这一子树
前两种情况可以合并处理
第三种情况则是左右子树都不为空,此时选择一个来节点来替换他后,再删除,就可以不破坏原有结构
如果要保持原有结构不变化,那么选择的节点必须要和删除节点在中序遍历中是连续的,而满足的只有两个节点,一个是其左子树的最大值,一个是其右子树的最小值。
*/
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
//前两种情况合并处理,如果当前结点只有一个子树,则让父节点指向他的子树
//处理只有右子树时
if (cur->_left == nullptr)
{
//如果当前节点为根节点,则让右子树成为新的根节点
if (cur == _root)
{
_root= cur->_left;
}
else
{
//判断当前节点是他父节点的哪一个子树
if (parent->_right == cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
}
//处理只有左子树时
else if (cur->_right == nullptr)
{
//如果当前节点为根节点,则让左子树成为新的根节点
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
delete cur;
}
//处理左右子树都不为空时,选取左子树的最右节点或者右子树的最左节点
else
{
//这里我选取的是左子树的最右节点
Node* LeftMax = cur->_left;
Node* LeftMaxParent = cur;
//找到左子树的最右节点
while (LeftMax->_right)
{
LeftMaxParent = LeftMax;
LeftMax = LeftMax->_right;
}
//替换节点
std::swap(cur->_kv, LeftMax->_kv);
//判断当前节点是他父节点的哪一个子树, 因为已经是最右子树了,所以这个节点的右子树为空,但是左子树可能还有数据,所以让父节点指向他的左子树
//并且删除最右节点
if (LeftMax == LeftMaxParent->_left)
{
LeftMaxParent->_left = LeftMax->_left;
}
else
{
LeftMaxParent->_right = LeftMax->_left;
}
delete LeftMax;
}
return true;
}
}
return false;
}
void _InordTravel(Node* root)
{
if (root == nullptr)
return;
//先遍历左子树
_InordTravel(root->_left);
//遍历根节点
std::cout << root->_key << ':' << root->_value << std::ends;
//遍历右子树
_InordTravel(root->_right);
}
//提供给外界的接口,因为外界无法访问私有成员root
void InordTravel()
{
_InordTravel(_root);
std::cout << std::endl;
}
Node* Find(const K& key)
{
//根据二叉搜索树的性质,从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树
Node* cur = _root;
while (cur)
{
//比根节点大则查找右子树
if (key > cur->_key)
{
cur = cur->_right;
}
//比根节点小则查找左子树
else if (key < cur->_key)
{
cur = cur->_left;
}
//相同则返回
else
{
return cur;
}
}
//遍历完则说明查找不到,返回false
return nullptr;
}
private:
Node* _root;
};
}