文章目录
一、二叉搜索树的实现
(一)二叉搜索树的中心思想
二叉搜索树又称为二叉排序树或者二叉查找树,如果它的左子树不为空,那么它的左子树的值都小于根节点的值,如果它的右子树不为空,那么它的右子树的值都大于根节点的值,它的左右子树也都是二叉搜索树
它中序遍历出来的val集合是有序的
中序遍历:1,3,4,6,7,8,10,13,14
(二)二叉搜索树的具体构造
1.节点结构
与普通二叉树没区别,一左一右一key
//二叉树的节点结构
template <class K>
struct BinarySearchTreeNode
{
BinarySearchTreeNode(K key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
BinarySearchTreeNode<K>* _left;
BinarySearchTreeNode<K>* _right;
K _key;
}
//二叉搜索树结构
class BSTree
{
typedef BinarySearchTreeNode Node;
public:
//默认构造
BSTree()
:_root(nullptr)
{}
//针对默认构造函数不处理内置类型此操作是强制生成默认构造函数
BSTree()=default;
//析构
~BSTree()
{
Destroy(_root);
}
//拷贝构造
BSTree(const BSTree<K>& t)
{
_root=Copy(t._root);
}
//赋值重载
BSTree<K>& operator=(BSTree<k> t)
{
swap(_root,t._root);
}
//非递归插入
bool Insert(K key);
//非递归查找
bool Find(K key);
//非递归删除
bool Erase(K key);
//插入递归版本
bool Insert(K key)
{
_Insert(_root,key);
}
//查找递归版本
bool Find(K key)
{
_Find(_root,key);
}
//删除递归版本
bool Erase(K key)
{
_Erase(_root,key);
}
//中序遍历
bool InOrder()
{
_InOrder(_root);
}
//保护内部实现接口
protected:
//插入递归实现
bool _Insert(Node* root,K key);
//查找递归实现
bool _Find(Node* root,K key);
//删除递归实现
bool _Erase(Node* root,K key);
//中序递归遍历
void _InOrder(Node* root);
//析构递归实现
void Destroy(Node* root);
//拷贝构造递归实现
Node* Copy(Node* root);
private:
Node* _root;
};
2.插入
非递归实现
1.如果树为空就直接插件新节点赋值个root指针,返回true
2.不为空就利用变量cur指针(初始值_root)找位置,要插入的值小于当前节点的key就往左走,大于往右走,如果等于就返回false,表示插入失败,因为二叉搜索树不允许key冗余;走到nullptr了就创建节点插入,并返回true
3.需要注意的是cur走到nullptr的时候不知道cur是上一个节点的左还是右,cur只是一个变量,将新节点赋值给cur,只是改变了cur变量的值,而没有改变上一个节点的左或者是右,如果cur是上一个节点的左或者右的引用就可以不用parent了;所以还需要用parent来记录cur的父节点,插入的时候还要判断一下左右。
bool insert(const T& key)
{
//判空
if(_root==nullptr)
{
_root=new Node(keyl);
}
//找插入位置
Node* cur=_root;
Node* parent=nullptr;
while(cur)
{
if(val<cur->_key)
{
parent=cur;
cur=cur->_left;
}
else if(key>cur->_key)
{
parent=cur;
cur=cur->_right;
}
else
{
return false;
}
}
//判断左右
if(val<parent->_key)
{
parent->_left=new Node(key);
}
else
{
parent->_right=new Node(key);
}
return true;
}
递归实现
1.思想与非递归大致相同,小于就往左递归,大于就往右递归,等于就返回false,为空就插入并返回true;
bool _Insert(Node*& root,K key)
{
if(root==nullptr)
{
root=new Node(key);
return true;
}
if(key<root->_key)
{
return _Insert(root->_left,key);
}
else if(key<root->_key)
{
return _Insert(root->_left,key);
}
else
{
return false;
}
}
3.删除
非递归实现
1.如果是空就返回false,删除失败
2.如果不是空,就找用cur找key的位置,还是按照小于往左走,反之往右走的原则查找,等于就跳出循环,找的同时用parent记录cur的父节点;
3.如果cur是nullptr就返回false;如果不是,那么就可以分为三种情况删除:
第一种是当前节点有一个孩子(或左或右),删除之后孩子要被parent领养,既然要cur的父节点领养cur的孩子,那么可定也需要判断cur是parent的左还是右。需要注意的是,当删除root的时候,root没有parent,需要直接更新root的位置,如果不处理这个情况,会发生parent的空指针的引用,因为parent初始化为nullptr。
第二种是当前节点没有孩子可以直接删除,这种情况可以融合到第一种情况中处理。
第三种是当前节点有左右两个孩子,那么久需要找一个节点来替代它之后,才能将它删除,这个替代节点可以是当前节点的左子树的最大节点,也就是最右节点,也可以是右子树的最小节点,也就是最左节点;删除后返回true删除成功。
bool Erase(T key)
{
//判空
if(_root==nullptr)
{
return false;
}
//找要删除的元素
Node* cur=_root;
Node* parent=nullptr;
while(cur)
{
if(key<cur->_key)
{
parent=cur;
cur=cur->_left;
}
else if(key>cur->_key)
{
parent=cur;
cur=cur->_right;
}
else
{
break;
}
}
//处理没找的情况
if(cur==nullptr)
{
return false;
}
//处理当前节点没有孩子或者只有一个右孩子的情况
if(cur->_left==nullptr)
{
//处理删除root的情况
if(cur==_root)
{
_root=_root->_right;
{
//判断cur是parent的左还是右
else if(parent->_left==cur)
{
parent->_left=cur->_right;
}
else
{
parent->_right=cur->_right;
}
delete cur;
}
//处理当前节点没有孩子或者只有一个左孩子的情况
else if(cur->_right==nullptr)
{
//处理删除root的情况
if(cur==_root)
{
_root=_root->_left;
{
//判断cur是parent的左还是右
else if(parent->_left==cur)
{
parent->_left=cur->_left;
}
else
{
parent->_right=cur->_left;
}
delete cur;
}
//处理当前节点有两个孩子的情况
else
{
//找当前节点的右树的最小节点
Node* rightMin=cur->_right;
Node* pRightMin=cur;
while(rightMin->_left)
{
pRightMin=rightMin;
rightMin=rightMin->_left;
}
//替换
cur->_key=rightMin->_key;
//右树的最小节点的父节点领养右树的最小节点的右孩子
if(pRightMin->_left==rightMin)
{
pRightMin->_left=rightMin->_right;
}
else
{
pRightMin->_right=rightMin->_right;
}
//删除右树的最小节点
delete rightMin;
}
return true;
}
递归实现
1.判空,空就返回false
2.找key,小于就往左递归,大于就往右递归。
3.删除也是分三种情况,没孩子,有一个孩子,有两个孩子
4.没孩子可以与有一个孩子的情况合并处理,这里与非递归不同的是,不需要当前节点是父节点的左还是右,因为当前节点实参的引用,不是单纯地比指针变量,可以直接赋值。
5.两个孩子的情况也省略了父节点的环节,不同的是,这次节点的值不是赋值,而是交换,交换后要删除的节点就是左子树的最右节点,或者是右子树的最左节点,这两个节点都是只有一个孩子或者是没有孩子,因此可以将问题转化为前两种情况进行处理,找的是左子树就往左递归一步,右子树就往右递归一步即可。
bool _Erase(Node*& root,K key)
{
if(root==nullptr)
{
return false;
}
if(key<root->_key)
{
return _Erase(root->_left,key);
}
else if(key>root->_key)
{
return _Erase(root->_right,key);
}
else
{
Node* del=root;
if(root->_left==nullptr)
{
root=root->_right;
}
else if(root->right==nullptr)
{
root=root->_left;
}
else
{
Node* leftMax=root->_left;
while(leftMax->_right)
{
leftMax=leftMax->_right;
}
swap(leftMax->_key,root->_key);
return _Erase(root->_left,key);
}
delete del;
return true;
}
}
3.查找
非递归实现
1.查找很简单了,其实已经包含在插入和删除中了,找到val就返回true反之返回false
bool Find(K key)
{
if(_root==nullptr)
{
return false;
}
Node* cur=_root;
while(cur)
{
if(key<cur->_key)
{
cur=cur->_left;
}
else if(key>cur->_key)
{
cur=cur->_right;
}
//相等就返回true
else
{
return true;
}
}
//找完了还没有找到就返回false
return false;
}
递归实现
bool _Find(Node* root,K key)
{
if(root==nullptr)
{
return false;
}
if(key<root->_key)
{
return _Find(root->_left,key);
}
if(key>root->_key)
{
return _Find(root->_right,key);
}
else
{
return true;
}
}
4.析构
1.这里用递归实现析构,用递归就必须使用后序遍历的方式实现,因为节点的左右节点不先释放,会造成内存泄漏。
void Destroy(Node* root)
{
if(root==nullptr)
{
return;
}
Destroy(root->_left);
Destroy(root->_right);
delete root;
root=nullptr;
}
5.拷贝构造
1.拷贝构造也是用递归实现,创建新的节点,然后分贝递归创建左节点和右节点,最后返回root
Node* Copy(Node* root)
{
if(root==nullptr)
{
return newNode;
}
Node* newNode=new Node(root->_key);
newNode->_left=Copy(root->_left);
newNode->_right=Copy(root->_right);
return newNode;
}
6. 中序遍历
void _InOrder(Node* root)
{
if(root==nullptr)
{
return ;
}
_InOrder(root->_left);
cout<<root->_key<<" ";
_InOrder(root->_right);
}
二、 二叉搜索树的应用
(一)k模型
k模型即只有可以作为关键吗,结构中只需要存储可以即可,关键码即为需要搜索到值;
比如检查一个单词的拼写是否正确,其原理就是构建一个二叉搜索树,将一个单词集合存放到这个结构中,单词也可以按照ascll码值排序,然后检查这个单词是否存在,存在即正确,不存即不正确;
其次就是排序
(二)kv模型
kv模型每一个关键码key,都有一个对应的值value,即<key,value>的键值对;
可以应用于单词翻译,比如英汉互译,也可以查找一个单词出现的次数;
key 和value可以是同类型的,例如<string,string>;
我们将上面的代码小小的改造一下就可以是kv模型,可以试验一下英汉互译;
思想
1.模板参数要变成来两个模型,一个key一个value
2.node节点需要改变,再加一个value
3.插入需改变,传参要传一个key一个value,插入的时候要插入两个值
4.查找需改变的是返回值,不在是返回bool而是返回node节点
5.删除不改变
代码实现
#pragma once
#include<iostream>
#include<cassert>
#include<string>
using namespace std;
namespace 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)
{
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
{
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
else
{
Node* pminRight = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
pminRight = minRight;
minRight = minRight->_left;
}
cur->_key = minRight->_key;
if (pminRight->_left == minRight)
{
pminRight->_left = minRight->_right;
}
else
{
pminRight->_right = minRight->_right;
}
delete minRight;
}
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 << endl;
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
}
三、 二叉搜索树的性能分析
1.因为不管是插入还是删除都需要先查找,所以查找代表了二叉搜索树性能;
2.二叉搜索树其实是不稳定的,相同的关键码集合,根值不同就会产生不一样的树结构
最右好情况查找平均次数下是在它是完全二叉树或者近似完全二叉树的情况下是log2N
3.最差情况下是N/2