前言
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
一、二叉搜索树是什么
二叉搜索树是以一棵二叉树来组织的。每个节点是一个对象,包含的属性有left,right,p和key,其中,left指向该节点的左孩子,right指向该节点的右孩子,p指向该节点的父节点,key是它的值,如果某节点的孩子节点或父节点不存在,则相应属性设置为NULL。根节点是树中唯一一个父节点为NULL的节点。
BST中的关键字有如下特征:
假设x为BST里的一个节点:
(1)如果y为x的左子树里的一个节点,那么y.key<=x.key;
(2)如果y为x的右子树里的一个节点,那么y.key>=x.key.
二、二叉搜索树的实现
2.1k模型的二叉搜索树的增删查
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>
class Bstree//搜索树
{
typedef BSTreeNode<K> Node;
public:
~Bstree()
{
_Destory(_root);
}
private:
Node*_root;
};
2.1查找节点
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;
}
查找的思路还是比较简单的,比节点小就去左边查找,比节点大
就去右边查找
2.2. 插入节点
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//开始查找
Node* cur = _root;
Node*parent = nullptr;
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;
}
插入的思路其实也不难,先进行查找 查找的值大的话就去右边查找,比他小的话就去左边
查找 ,如果找到空了,就是需要插入的位置, 这里需要主要的点事直接删除可能会引发野指针问题,所以定义了一个父亲指针来保存cur的位置 ,当然还需要判断是插入cur是左是右
2.3. 删除节点(比较难)
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 = cur->_right;//防止删除根节点
}
else
{
if (cur == parent->_left)
{
//第一种如果cur是父亲的左
parent->_left = cur->_right;//父亲的左指向我的右
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_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;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(cur->_key, min->_key);
if (minParent->_left == min)
minParent->_left = min->_right;
else
minParent->_right = min->_right;
delete min;
}
return true;
}
}
return false;
}
首先查找元素是否在二叉搜索树中,如果不存在,则返回 , 否则要删除的结点可能分下面四种情况:a. 要删除的结点无孩子结点b. 要删除的结点只有左孩子结点c. 要删除的结点只有右孩子结点d. 要删除的结点有左、右孩子结点看起来有待删除节点有 4 中情况,实际情况 a 可以与情况 b 或者 c 合并起来,因此真正的删除过程如下:情况 b :删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点 -- 直接删除情况 c :删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点 -- 直接删除情况 d :在它的右子树中寻找中序下的第一个结点 ( 关键码最小 ) ,用它的值填补到被删除节点中,再来处理该结点的删除问题 -- 替换法删除
情况一:独生子女好删除,可以直接拖孤给父亲节点,但需要注意野指针问题
第一种删除左为空 :有两种情况 3可能是父亲的左 ,假设删除的是6 6虽然也是左为空 但是6是父亲的右
所以这里要多做一层判断他到底是父亲的左还是右来分别进行处理。
当然还需要考虑删除跟节点的情况
以上情况处理好后用替换法进行删除就是左子树最大或者右子树最小节点
2.4.递归实现(查找)
bool _FindR(Node*root, const K& key)
{
//查找
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
//转化为去右边查找
return _FindR(root->_right, key);
}
else if (root->_key>key)
{
//左边
_FindR(root->_left, key);
}
else
{
//找到了
return true;
}
}
和前面不一样的地方是,我们这里返回的是节点的指针。
2.5.递归实现(插入)
bool _InsertR(Node*&root, const K& key)//引用目的是右指针的别名防止野指针
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key < key)
{
_InsertR(root->_right, key);
}
else if (root->_key>key)
{
_InsertR(root->_left, key);
}
else
{
return false;
}
}
因为节
点
但是这样就会有一个问题:newnode是一个局部变量,我们无法把这个newnode的局部变量连接到整个二叉树的正确位置。换言之,当递归到合适位置的时候,我们不能把newnode和它的父节点链接到一起!
而解决的方案就是在使用引用传参!
2.6. 递归实现(删除)
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (root->_key < key)
return _EraseR(root->_right, key);
else if (root->_key > key)
return _EraseR(root->_left, key);
else
{
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
// 找右树的最左节点替换删除
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(root->_key, min->_key);
//return EraseR(key); 错的
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
2.7.完整代码
#pragma once
#include<iostream>
using namespace std;
namespace zdc
{
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>
class Bstree//搜索树
{
typedef BSTreeNode<K> Node;
public:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//开始查找
Node* cur = _root;
Node*parent = nullptr;
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;
}
void Inorder()
{
return _Inorder(_root);
cout << endl;
}
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 = cur->_right;//防止删除根节点
}
else
{
if (cur == parent->_left)
{
//第一种如果cur是父亲的左
parent->_left = cur->_right;//父亲的左指向我的右
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_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;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(cur->_key, min->_key);
if (minParent->_left == min)
minParent->_left = min->_right;
else
minParent->_right = min->_right;
delete min;
}
return true;
}
}
return false;
}
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool InsertR()
{
return _InsertR(_root, key);
}
bool EarseR()
{
return _EraseR(_root, key);
}
~Bstree()
{
_Destory(_root);
}
Bstree() = default;//强制编译器生成默认构造
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);//左c节点拷贝左节点
copyRoot->_right = _Copy(root->_right);
return copyRoot;
}
void _Destory(Node*&root)
{
//后序销毁
if (root == nullptr)
{
return;
}
_Destory(root->_left);
_Destory(root->_right);
delete root;
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (root->_key < key)
return _EraseR(root->_right, key);
else if (root->_key > key)
return _EraseR(root->_left, key);
else
{
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
// 找右树的最左节点替换删除
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(root->_key, min->_key);
//return EraseR(key); 错的
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
bool _InsertR(Node*&root, const K& key)//引用目的是右指针的别名防止野指针
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key < key)
{
_InsertR(root->_right, key);
}
else if (root->_key>key)
{
_InsertR(root->_left, key);
}
else
{
return false;
}
}
bool _FindR(Node*root, const K& key)
{
//查找
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
//转化为去右边查找
return _FindR(root->_right, key);
}
else if (root->_key>key)
{
//左边
_FindR(root->_left, key);
}
else
{
//找到了
return true;
}
}
void _Inorder(Node * root)//中序便利 左 跟 右
{
if (root == nullptr)
{
return;
}
_Inorder(root->_left);
cout << root->_key << " ";
_Inorder(root->_right);
}
private:
Node* _root=nullptr;//根节点初始化为0
};
}
总结:二叉查找树的优点:
二叉排序树是一种比较有用的折衷方案.
数组的搜索比较方便,可以直接用下标,但删除或者插入某些元素就比较麻烦.
链表与之相反,删除和插入元素很快,但查找很慢。
二叉排序树就既有链表的好处,也有数组的好处。
在处理大批量的动态的数据是比较有用.
二叉查找树的缺点:
顺序存储可能会浪费空间(在非完全二叉树的时候),但是读取某个指定的节点的时候效率比较高O(0)
链式存储相对二叉树比较大的时候浪费空间较少,但是读取某个指定节点的时候效率偏低O(nlogn)