一、二叉排序树的概念
二叉排序树(也称二叉搜索树或二叉查找树),它可能是一棵空树,或者是具有以下特性的二叉树:
- 若左子树非空,则左子树上所有结点的值均小于根结点的值。
- 若右子树非空,则右子树上所有结点的值均大于根结点的值。
- 左、右子树也分别是一棵二叉排序树
根据二叉排序树的定义,左子树结点值< 根结点值<右子树结点值,因此对二叉排序树进行中序遍历,可以得到一个递增的有序序列。例如,上图所示二叉排序树的中序遍历序列为1,3,4,6,7,8,10,13,14。
二、二叉排序树的实现
1. 二叉排序树的查找
从根结点开始比较,比根小向左查找,比根大向右查找。如果相等,则查找成功,返回当前结点的指针。若查找失败则返回nullptr。
// 查找结点
Node* Find(const K& key)
{
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
{
cur = cur->_left;
}
else if (key > cur->_key)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
2. 二叉排序树的插入
树为空,则直接新增节点,赋值给root指针。
树不空,按二叉搜索树性质查找插入位置,插入新节点。
插入时需要注意的是:
要添加一个记录cur的父结点,用于创建新结点后的连接。
连接新结点时,由于不知道新结点是父结点的左孩子还是右孩子,因此还要多加上一句判断。
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr; // 用于记录cur的父结点,方便后面进行连接
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(key);
// 连接新建结点与父结点
if (parent->_key > key)
parent->_left = cur;
else if (parent->_key < key)
parent->_right = cur;
return true;
}
3. 二叉排序树的删除
二叉排序树的删除比较复杂,因为如果删除的结点既有左孩子又有右孩子,为了保持二叉排序树的特性,不能随便找一个子树上的结点来替代删除的结点。
首先查找元素是否在二叉搜索树中,如果不存在,则返回,删除操作可能分为如下四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4种情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
情况d:在它的左子树的最大结点或右子树的最小结点,用它的值填补到被删除节点中,再来处理该结点的删除问题–替换法删除
// 删除结点
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr)
{
// 开始找要删除的结点
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else //找到了要删除的结点
{
if (cur->_left == nullptr) // 当要删除的结点左子树为空时
{
if (cur == _root) // 如果删除的是根结点,直接让根指向右子树
{
_root = cur->_right;
}
else // 连接父结点和右子树
{
if (parent->_left == cur) parent->_left = cur->_right;
if (parent->_right == cur) parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr) // 当要删除的结点右子树为空时
{
if (cur == _root) // 如果删除的是根结点,直接让根指向左子树
{
_root = cur->_right;
}
else // 连接父结点和左子树
{
if (parent->_left == cur) parent->_left = cur->_left;
if (parent->_right == cur) parent->_right = cur->_left;
}
delete cur;
}
else // 当删除的结点左子树和右子树都存在,可以找左子树中最右的结点
// 或者右子树中最左的结点来替代
{
Node* maxLeft = cur->_left;
Node* pmaxLeft = cur; // 记录左子树最右结点的父结点
while (maxLeft->_right) // 寻找左子树最右结点
{
pmaxLeft = maxLeft;
maxLeft = maxLeft->_right;
}
cur->_key = maxLeft->_key; // 用最右结点的值替代cur的值
if (pmaxLeft->_left == maxLeft) pmaxLeft->_left = maxLeft->_left;
if (pmaxLeft->_right == maxLeft) pmaxLeft->_right = maxLeft->_left;
delete maxLeft;
}
return true;
}
}
return false;
}
4. 整体代码
namespace key
{
// 二叉排序树结点结构
template<class K>
struct BSTreeNode
{
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;
public:
// 构造函数
BSTree() = default;
// 拷贝构造
BSTree(const BSTree<K>& t)
{
_root = Copy(t._root);
}
// 赋值重载
BSTree& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
// 析构函数
~BSTree()
{
Destroy(_root);
}
// 插入结点
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr; // 用于记录cur的父结点,方便后面进行连接
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(key);
// 连接新建结点与父结点
if (parent->_key > key)
parent->_left = cur;
else if (parent->_key < key)
parent->_right = cur;
return true;
}
// 查找结点
Node* Find(const K& key)
{
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
{
cur = cur->_left;
}
else if (key > cur->_key)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
// 删除结点
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr)
{
// 开始找要删除的结点
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else //找到了要删除的结点
{
if (cur->_left == nullptr) // 当要删除的结点左子树为空时
{
if (cur == _root) // 如果删除的是根结点,直接让根指向右子树
{
_root = cur->_right;
}
else // 连接父结点和右子树
{
if (parent->_left == cur) parent->_left = cur->_right;
if (parent->_right == cur) parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr) // 当要删除的结点右子树为空时
{
if (cur == _root) // 如果删除的是根结点,直接让根指向左子树
{
_root = cur->_right;
}
else // 连接父结点和左子树
{
if (parent->_left == cur) parent->_left = cur->_left;
if (parent->_right == cur) parent->_right = cur->_left;
}
delete cur;
}
else // 当删除的结点左子树和右子树都存在,可以找左子树中最右的结点
// 或者右子树中最左的结点来替代
{
Node* maxLeft = cur->_left;
Node* pmaxLeft = cur; // 记录左子树最右结点的父结点
while (maxLeft->_right) // 寻找左子树最右结点
{
pmaxLeft = maxLeft;
maxLeft = maxLeft->_right;
}
cur->_key = maxLeft->_key; // 用最右结点的值替代cur的值
if (pmaxLeft->_left == maxLeft) pmaxLeft->_left = maxLeft->_left;
if (pmaxLeft->_right == maxLeft) pmaxLeft->_right = maxLeft->_left;
delete maxLeft;
}
return true;
}
}
return false;
}
// 中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
// 递归实现插入结点
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
// 递归实现查找操作
bool FindR(const K& key)
{
return _FindR(_root, key);
}
protected:
Node* Copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
// 递归调用使所有结点均被拷贝
Node* newRoot = new Node(root->_key);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
void Destroy(Node* root)
{
if (root == nullptr) return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
void _InOrder(Node* root)
{
if (root == nullptr) return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
bool _InsertR(Node* root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
}
if (key < root->_key)
{
_InsertR(root->_left, key);
}
else if (key > root->_key)
{
_InsertR(root->_right, key);
}
else
{
return false;
}
}
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (key < root->_key)
{
return _FindR(root->_left, key);
}
else if (key > root->_key)
{
return _FindR(root->_right, key);
}
else
{
return true;
}
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (key < root->_key)
{
return _EraseR(root->_left, key);
}
else if (key > root->_key)
{
return _EraseR(root->_right, key);
}
else
{
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* maxLeft = root->_right;
while (maxLeft)
{
maxLeft = maxLeft->_right;
}
swap(maxLeft->_key, root->_key);
return _EraseR(root->_left, key); // 交换后要删除的值在左子树的最右
}
delete del;
return true;
}
}
private:
Node* _root = nullptr;
};
}
三、二叉排序树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
- KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
// 改造二叉搜索树为KV结构
namespace key_value
{
template <class K, class V>
struct BSTreeNode
{
BSTreeNode(const K& key, const V& value)
:_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;
public:
// 插入结点
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* parent = nullptr; // 用于记录cur的父结点,方便后面进行连接
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(key, value);
// 连接新建结点与父结点
if (parent->_key > key)
parent->_left = cur;
else if (parent->_key < key)
parent->_right = cur;
return true;
}
// 查找结点
Node* Find(const K& key)
{
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
{
cur = cur->_left;
}
else if (key > cur->_key)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
// 删除结点
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr)
{
// 开始找要删除的结点
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else //找到了要删除的结点
{
if (cur->_left == nullptr) // 当要删除的结点左子树为空时
{
if (cur == _root) // 如果删除的是根结点,直接让根指向右子树
{
_root = cur->_right;
}
else // 连接父结点和右子树
{
if (parent->_left == cur) parent->_left = cur->_right;
if (parent->_right == cur) parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr) // 当要删除的结点右子树为空时
{
if (cur == _root) // 如果删除的是根结点,直接让根指向左子树
{
_root = cur->_right;
}
else // 连接父结点和左子树
{
if (parent->_left == cur) parent->_left = cur->_left;
if (parent->_right == cur) parent->_right = cur->_left;
}
delete cur;
}
else // 当删除的结点左子树和右子树都存在,可以找左子树中最右的结点
// 或者右子树中最左的结点来替代
{
Node* maxLeft = cur->_left;
Node* pmaxLeft = cur; // 记录左子树最右结点的父结点
while (maxLeft->_right) // 寻找左子树最右结点
{
pmaxLeft = maxLeft;
maxLeft = maxLeft->_right;
}
cur->_key = maxLeft->_key; // 用最右结点的值替代cur的值
cur->_value = maxLeft->_value;
if (pmaxLeft->_left == maxLeft) pmaxLeft->_left = maxLeft->_left;
if (pmaxLeft->_right == maxLeft) pmaxLeft->_right = maxLeft->_left;
delete maxLeft;
}
return true;
}
}
return false;
}
// 中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
protected:
void _InOrder(Node* root)
{
if (root == nullptr) return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << " ";
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
void TestBSTree3()
{
// 输入单词,查找单词对应的中文翻译
BSTree<string, string> dict;
dict.Insert("string", "字符串");
dict.Insert("tree", "树");
dict.Insert("left", "左边、剩余");
dict.Insert("right", "右边");
dict.Insert("sort", "排序");
dict.Erase("string");
dict.InOrder();
// 插入词库中所有单词
/*string str;
while (cin >> str)
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret == nullptr)
{
cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
}
else
{
cout << str << "中文翻译:" << ret->_value << endl;
}
}*/
}
}
四、二叉排序树的查找效率分析
二叉排序树的查找效率,主要取决于树的高度。若二叉排序树的高度之差的绝对值不超过1(平衡二叉树),则它的平均查找长度为O(logn)。若二叉排序树是一个只有左(右)孩子的单支树,则其平均查找长度为O(n)。
从查找过程看,二叉排序树与二分查找相似。
就平均时间性能而言,二叉排序树的查找和二分查找差不多。但二分查找的判定树唯一,而二叉排序树的查找不唯一,相同的关键字其插入顺序不同可能生成不同的二叉排序树。
就维护表的有序性而言,二叉排序树无须移动结点,只需修改指针即可完成插入和删除操作,平均执行时间是O(logn)。二分查找的对象是有序表,若有插入和删除结点的操作,所花的代价是O(n)。
当有序表是静态查找表时,宜用顺序表作为其存储结构,而采用二分查找实现其找操作;若有序表时动态查找表,则应选择二叉排序树作为其逻辑结构。