目录
前言
一、二叉搜索树
1.二叉搜索树概念
2.二叉搜索树操作
二、二叉搜索树实现
0.定义一个节点
1.定义一棵树
2.增删改查
2.1.查找
2.2.插入
2.3.删除
2.3.1非递归删除法
a.只有左孩子 -- 删除14
b.只有右孩子-- 删除10
c.有左右孩子--删除8
2.3.2递归删除法
三、二叉搜索树应用
1.K模型(解决在不在的问题)
2.KV模型
3.二叉搜索树性能分析
总结
前言
本文中出现的源码已在本地vs2019下测试无误,上传至gitee:
https://gitee.com/a_young/binary-search-tree
一、二叉搜索树
1.二叉搜索树概念
二叉搜索树又称为二叉排序树,它或者是一颗空树,或者具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 左、右子树都是二叉搜索树
2.二叉搜索树操作
int a[] = {8,3,1,10,6,4,7,14,13};
1.二叉搜索树的查找
- 从根节点开始比较,查找,如果比根节点值大,往右走查找,比根节点值小,往左走查找
- 最多查找高度次,走到空节点还没找到,则说明这个值在树中不存在。
2.二叉搜索树的插入
- 若为空树,则直接新增节点,赋值给root指针
- 树不为空,按性质查找插入位置,插入新节点
3.二叉搜索树的删除
- 先查找元素是否在树中,如果不存在,返回。否则要删除的节点分为下面四种情况
- 要删除的结点是叶子节点
- 要删除的节点只有左孩子节点
- 要删除的节点只有右孩子节点
- 要删除的节点有左、右孩子节点
1可以看成2,3的一种情况。
- 只有左孩子节点(如上图 14) :删除该节点,并使被删除节点的父节点指向被删节点的左孩子节点
- 只有右孩子节点:删除该节点,并使被删除节点的父节点指向被删节点的右孩子节点
- 有左右孩子节点:先寻找右树的最小节点(或者左树的最大节点),用它的值填补到被删除节点中,再处理该节点的删除问题。 详细处理代码以及坑往下。
二、二叉搜索树实现
0.定义一个节点
template<class k>
struct BSTreeNode
{
//三个成员
BSTreeNode<T>* _left;
BSTreeNode<T>* _right;
K _key;
//构造函数
BSTreeNode(const k& key)
:_left(nullptr)
,_right(nullptr)
,key(key)
{
}
};
1.定义一棵树
实现构造,拷贝构造,析构。
- 构造的时候用一个节点,初始化为空。
- 拷贝构造,这里注意,必须是深拷贝,浅拷贝容易出现野指针。
- 析构的时候使用后序递归删除即可。
template<class k>
class BSTree
{
typedef BSTreeNode<T> Node;
public:
/* BSTree()
:_root(nullptr)
{}
*/
BSTree() = default; //指定强制生成默认构造
//拷贝构造
BSTree(const BSTree<K> & t)
{
_root = copy(t._root);
}
//跟前序创建类似 后序回来才链接
Node* copy(Node * root)
{
if(root == nullptr)
return nullptr;
Node * new_root = new Node(root ->key);
new_root ->_left = Copy(root->_left);
nre_root->_right = Copy(root->_right);
return new_root;
}
//析构
~BSTree()
{
//使用后序递归
Destory(_root);
}
void Destroy(Node * root)
{
if(root == nullprt)
return ;
Destroy(root->left);
Destroy(root->right);
delete root;
}
//成员函数
//实现增删改查 protected封装
//赋值
BSTree<k>& operator=(BSTree<k> t)
{
swap(_root,t._root);
return *this;
}
private:
Node * _root = nullptr;
2.增删改查
2.1.查找
bool Find(const k& key)
{
Node * cur = _root;
while(cur)
{
if(cur->_ley <key)
cur = cur->right;
else if(cur->_key >key)
cur = cur ->right;
else
{
return true;
}
return false;
}
2.2.插入
//非递归解法
bool Insert(const k& key)
{
//如果是一个空树
if(_root == nullptr)
{
_root = new Node(key);
return true;
}
Node * parent = nullptr;
Node * cur = _root;
while(cur)
{
if (cur->_key < key)
{
parent = cur;
//注意这里,cur更新前不是空,更新后才是空,更新前还可以访问right
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;
}
这里使用递归的方式再来插入,一个很巧妙的使用引用的例子。假设我要在上图里面插入一个2,先去找到合适的位置,1的right是空,在这里new了一个Node,值为2,同时返回,此时这里的Node就是天然的上一层栈帧中的root->right,完美链接。
bool _InsertR(Node& * root, const k& key) { if(root == nullptr) { root = new Node(key); return true; } if (root->_key < key) { return _InsertR(root->_right, key); } else if (root->_key > key) { return _InsertR(root->_left, key); } else { return false; } } bool InsertR(const k& key) { return _InsertR(_root, key); }
2.3.删除
2.3.1非递归删除法
bool _Erase(Node * root,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->leftt;
}
//找到了,开始删除
else
{
//a.只有左孩子
//b.只有右孩子
//c.左右孩子都有
return true;
}
}
bool Erase(const K& key)
{
return bool _Erase(Node * root,const K& key);
}
a.只有左孩子 -- 删除14
通过上面代码进行查找,找到了14,此时parent指向10,cur指向14,14只有左孩子,进行链接,14的左孩子成为10的右孩子,删除14。
if(cur ->_right == nullptr) { if(parent ->_left == cur) parent ->_left = cur->left; else if(parent ->_right == cur) parent ->_right = cur->_right; delete cur; } return true;
但是有特殊情况:如果有一棵树为只有两个节点,要删除的节点为根节点,则它的父节点parent就是空指针,无法进行判断删除。所以这里要进行判断,如果是,则更新root,再删除cur。
if(cur == _root) { _root = cur->_left; }
b.只有右孩子-- 删除10
通过查找找到了10,cur指向10,parent指向8,开始删除
if(cur == _root) { _root = cur->_right; } else if( parent ->_left == cur) { parent->left = cur->_right; } else if( parent ->_right == cur) { parent ->right = cur->_right; } delete cur; return true;
c.有左右孩子--删除8
此时需要找到子树中的左树中的最右节点(最大)或者右数中的最左节点(最小)节点来替换掉根节点,如下我们使用右树中的最左节点去替换。 此时这个最左节点,它也可能有右孩子,肯定没有左孩子。所以我们要进行托孤,即这里要找到这个最左节点的父亲。
假设删除树中的根节点8,树中10有左节点 9,9有自己的右孩子9.5,所以先找到右树的最左节点9,它的父亲10,托孤自己的右孩子9.5,为10的左孩子。9替代掉8,最后删除掉minRight这个位置的结构。
这里也需要小心避坑,如果这里删除8,minRight指向10,没有minRight->left,pminRight为空(所以pminRight不能刚开始就给空,需要给cur)。并且10只有自己的右孩子14,它要成为8的右孩子,所以托孤的时候需要判断。
/*Node* pminRight = nullptr; Node * minRight = cur->_right; while(minRight->_left) { pminRight = minRight; minRight = minRight->left; } cur ->_key = minRight ->_key; pminRight ->left = minRight->_right; delete minRight; */ Node * pminRight = cur; Node * minRight = cur->_right; while(minRight->_left) { pminRight = minRight; minRight = minRight ->left; } cur ->_key = minRight->_key; //进行托孤 if(pminRight->left == minRight->right) pminRight->left = minRight->right; else if(pminRight->right == minRight->left) pminRight->right = minRight->right; delete minRight;
2.3.2递归删除法
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->_right == nullptr)
{
root = root->_left;
}
else if (root->_left == nullptr)
{
root = root->_right;
}
else
{
//这里找左树的最右节点
Node * maxleft = root->_left;
while(maxleft->_right)
{
maxleft = maxleft ->right;
}
swap(root->_key, maxleft->_key);
return _EraseR(root->_left,key);
}
delete del;
return true;
}
三、二叉搜索树应用
1.K模型(解决在不在的问题)
K模型只有key作为关键字,结构中只存储key即可,关键码即为需要搜索到的值。
- 比如门禁系统:芯片中有个人信息,根据个人信息在数据库中查找,如果在,就通过。
2.KV模型
每一个关键码key,都有与之对应的值value,即<key,value>的键值对,该方式在生活中非常常见
- 比如英汉词典就是中文与英文的对应关系,通过英文可以快速找到与之对应的中文,英文单词与其对应的中文<word,chinese>构成一种键值对。
- 统计单词次数,统计结束,给定单词就可以快速找到出现的次数。<key,count>构成一种键值对。
//改造二叉搜索树为kv结构 template<class k, class v> struct BSTNode { BSTNode(const k& key = k(), const v&value = v()) :_pLeft(nullptr),_pRight(nullptr),_key(key),_value(vaule) {} BSTNode<T> * _pleft; BSTNode<T> * _pright; k _key; v _value; }; template<class k, class v> class BSTree { typedef BSTNode<k,v> Node; typedef Node* pNode; public: BSTree(): _pRoot(nullptr){} pNode Find(const K& key); bool Insert(const k& key ,const v& value); bool Erase(const k& key); private: pNode _pRoot; } void TestBSTree() { // 输入单词,查找单词对应的中文翻译 BSTree<string, string> dict; dict.Insert("string", "字符串"); dict.Insert("tree", "树"); dict.Insert("left", "左边、剩余"); dict.Insert("right", "右边"); dict.Insert("sort", "排序"); // 插入词库中所有单词 string str; while (cin>>str) { BSTreeNode<string, string>* ret = dict.Find(str); if (ret == nullptr) { cout << "单词拼写错误,词库中没有这个单词:" <<str <<endl; } else { cout << str << "中文翻译:" << ret->_value << endl; } } } void TestBSTree() { // 统计水果出现的次数 string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" }; BSTree<string, int> countTree; for (const auto& str : arr) { // 先查找水果在不在搜索树中 // 1、不在,说明水果第一次出现,则插入<水果, 1> // 2、在,则查找到的节点中水果对应的次数++ //BSTreeNode<string, int>* ret = countTree.Find(str); auto ret = countTree.Find(str); if (ret == NULL) { countTree.Insert(str, 1); } else { ret->_value++; } } countTree.InOrder(); }
3.二叉搜索树性能分析
插入和删除都必须先查找,则查找的效率代表了二叉搜索树中各个操作的性能。
对于有n个节点的二叉搜索树,若每个元素查找概率相等,查找查毒是二叉树的深度函数,节点越多,比较次数越多。
最优:完全二叉树,O(logN)
最差:单支O(N)
如果退化为单支,二叉搜索树性能很差,所以使用AVL树和红黑树,后续文章继续介绍。
总结
本文主要介绍了二叉搜索树实现以及应用,技术有限,如有错误请指正。