二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
- 键值是唯一的,所以二叉搜索树不能有相同的键值
例如:以下数据构建二叉树
int a [] = {5,3,4,1,7,8,2,6,0,9};
二叉搜索树的实现
1.树的结点与树
树的结点,有3个属性,数据,左孩子,右孩子,最初左孩子与右孩子都是空;树,只有一个根,最初构造为空树。
代码如下:
template<class T>
struct BTreeNode
{
typedef BTreeNode<T> Node;
//三个属性值
Node* _left;//左子树
Node* _right;//右子树
T _val;//数据
BTreeNode(const T& val = T())
:_left(nullptr)
,_right(nullptr)
,_val(val)
{}
};
template<class T>
class BTree
{
public:
typedef BTreeNode<T> Node;
BTree()
:_root(nullptr)
{}
private:
Node* _root;//根
};
2.查找
若根节点不为空:
如果根节点val == 查找key,返回所找到的结点
如果根节点val >查找key,在其左子树继续查找
如果根节点val < 查找key,在其右子树继续查找
若最后没有找到key,返回nullptr
代码如下:
Node* find(const T& val)
{
Node* cur = _root;
while(cur)
{
//相等返回结点
if(cur->_val == val)
return cur;
//比根节点小则查找左子树
else if(cur->_val > val)
cur = cur->_left;
//比根节点大则查找右子树
else
cur = cur->_right;
}
//没有找到返回nullptr
return cur;
}
2.插入
插入的具体过程:
-
若树为空,则直接插入
-
树不空:
- 遍历树,当存在与插入数据相同的结点时,插入失败,直接返回false
- 若当前结点比插入结点的值大,则继续遍历当前结点的左子树,若当前结点比插入结点的值小,则继续遍历当前结点的右子树,直到找到一个空的位置,就是插入结点的位置
- 找到插入结点的位置后,若比插入位置的父结点大(为了能够找到父结点,还需要一个指针指向父结点),就把结点连在父结点的右边,否则就连在父结点的左边。
代码如下:
bool insert(const T& val)
{
//如果是空树,则直接插入结点,返回true
if(_root == nullptr)
{
_root = new Node(val);
return true;
}
Node* cur = _root;
Node* parent = nullptr;//指向父结点的指针
//若不是空树,需要遍历找到插入结点的位置
while(cur)
{
parent = cur;
//相同的结点,直接返回false,因为搜索树中不能出现相同的值
if(cur->_val == val)
return false;
//比根节点小则继续查找左子树
else if(cur->_val > val)
cur = cur->_left;
//比根节点大则继续查找右子树
else
cur = cur->_right;
}
//创建结点
cur = new Node(val);
//插入:判断插入到父结点的左边还是右边
if(parent->_val > val)
parent->_left = cur;
else
parent->_right = cur;
return true;
}
测试:
二叉搜索树的中序遍历,会将树中的数据升序排列,我们可以据此来简单验证插入代码是否有错。
中序遍历代码:
void _inorder(Node* root)
{
if(root)
{
_inorder(root->_left);//先遍历左子树
cout << root->_val << " ";//打印根结点
_inorder(root->_right);//再遍历右子树
}
}
//由于类外不能访问根结点,所以要进行包装
void inorder()
{
_inorder(_root);
cout << endl;
}
可以通过随机数的方法,进行大量数据的验证:
3.拷贝
树存在资源,要进行深拷贝,浅拷贝会出现二次释放的问题。
拷贝思想:通过递归,依次拷贝原二叉树的结点,并且保存原二叉树的结构,拷贝完成后,再依次自下而上进行结点的连接。
代码如下:
BTree(const BTree<T>& bt)
:_root(copy(bt._root))
{}
Node* copy(Node* root)
{
if(root == nullptr)
return nullptr;
Node* newNode = new Node(root->_val);
newNode->_left = copy(root->_left);
newNode->_right = copy(root->_right);
return newNode;
}
可以根据代码,画一下拷贝的过程,有助于更好的理解。
4.销毁与析构
这也是一个递归操作,具体过程:
- 先销毁左子树
- 再销毁右子树
- 最后销毁根结点
代码如下:
void destory(Node* root)
{
if(root)
{
destory(root->_left);
destory(root->_right);
delete root;
}
}
~BTree()
{
if(_root)
{
destory(_root);
_root = nullptr;
}
}
5.删除结点
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
- 要删除的结点是叶子结点
-
如果叶子结点是根节点,则直接删除叶子结点,并且将根节点置空
-
如果不是根节点,判断该结点是父亲结点的左孩子还是右孩子,如果是左孩子则将左孩子置空并删除此结点;若是右孩子,则将右孩子的置空并删除此结点
-
代码如下:
//遍历搜索二叉树,找到要删除的结点
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_val == val)//找到结点,结束循环
break;
parent = cur;
if(cur->_val > val)
cur = cur->_left;
else
cur = cur->_right;
}
//如果cur为空,说明没有找到要删除的结点,则直接返回false
if(cur == nullptr)
return false;
//判断cur是否是叶子结点
if(cur->_left == nullptr && cur->_right == nullptr)
{
//判断这个结点是否是根结点
if(cur == _root)
{
//根结点置空
_root = nullptr;
}
else
{
//判断要删除的结点是父亲结点的哪一边
if(parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
}
//删除结点
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;
}
//删除cur
delete cur;
}
- 要删除的结点只有右孩子结点
这与要删除的结点只有左孩子的思想一致,下面简单说说。- 若这个结点是根节点,则让根节点指向它的右孩子,并删除需要删除的结点
- 不是跟根结点,若需要删除的结点在它父结点的左边,那父结点的左边就去连要删除结点的右边;若需要删除的结点在它父结点的右边,那父结点的右边就去连要删除结点的右边。
- 若这个结点是根节点,则让根节点指向它的右孩子,并删除需要删除的结点
代码如下:
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;
}
-
要删除的结点有左、右孩子结点
删除既有左孩子又有右孩子的结点是四种情况最麻烦的情况,比如说我们删除结点5,若我们直接删除,就会破坏树的结构;我们需要找到一个结点(叶子结点或者只有一边的结点),将其和结点5交换,再删除它,这样就归于上面三种情况的一种,删除起来比较简单,并且也不会破坏结构。那么需要交换的结点怎么找?结点5是根结点,选一个结点来代替它的位置,那么一定要大于左子树并且小于右子树,这样看来就只有两种结点符合:左子树的最大结点,右子树的最小结点。具体位置就是左子树的最右结点,或者右子树的最左结点。下面都以左子树的最右结点来说。具体流程:
- 先找到要删除结点的左子树的最右结点(leftRightMost)
- 将要删除的结点与leftRightMost交换
- 此时删除leftRightMost,这个结点没有右子树,但是不一定没有左子树,所以leftRightMost的父亲结点(要判断leftRightMost是父亲结点的左孩子还是右孩子),连接的就是leftRightMost的左子树,如果没有左子树,父亲结点连接nullptr。
代码如下:
else//需要删除的结点既有左孩子也有右孩子
{
Node* leftRightMost = cur->_left;
parent = cur;
//找到左子树的最有结点
while(leftRightMost->_right)
{
parent = leftRightMost;
leftRightMost = leftRightMost->_right;
}
//交换左子树的最右结点和需要删除的结点
swap(cur->_val, leftRightMost->_val);
if(parent->_left == leftRightMost)
parent->_left = leftRightMost->_left;
else
parent->_right = leftRightMost->_left;
delete leftRightMost;
}
erase接口的代码总结:
bool erase(const T& val)
{
//遍历搜索二叉树,找到要删除的结点
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_val == val)//找到结点,结束循环
break;
parent = cur;
if(cur->_val > val)
cur = cur->_left;
else
cur = cur->_right;
}
//如果cur为空,说明没有找到要删除的结点,则直接返回false
if(cur == nullptr)
return false;
//判断cur是否是叶子结点
if(cur->_left == nullptr && cur->_right == nullptr)
{
//判断这个结点是否是根结点
if(cur == _root)
{
//根结点置空
_root = nullptr;
}
else
{
//判断要删除的结点是父亲结点的哪一边
if(parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
}
//删除结点
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;
}
//删除cur
delete cur;
}
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//需要删除的结点既有左孩子也有右孩子
{
Node* leftRightMost = cur->_left;
parent = cur;
//找到左子树的最有结点
while(leftRightMost->_right)
{
parent = leftRightMost;
leftRightMost = leftRightMost->_right;
}
//交换左子树的最右结点和需要删除的结点
swap(cur->_val, leftRightMost->_val);
if(parent->_left == leftRightMost)
parent->_left = leftRightMost->_left;
else
parent->_right = leftRightMost->_left;
delete leftRightMost;
}
return true;
}
测试:
二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:㏒₂N
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2
二叉搜索树的应用
K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
KV模型二叉搜索树都是通过key值来查找遍历;插入时,是插入key,value值;删除是通过key值来删除。
K模型二叉搜索树全部代码总结:
#include <iostream>
using namespace std;
#include<time.h>
template<class T>
struct BTreeNode
{
typedef BTreeNode<T> Node;
//三个属性值
Node* _left;//左子树
Node* _right;//右子树
T _val;//数据
BTreeNode(const T& val = T())
:_left(nullptr)
,_right(nullptr)
,_val(val)
{}
};
template<class T>
class BTree
{
public:
typedef BTreeNode<T> Node;
BTree()
:_root(nullptr)
{}
Node* find(const T& val)
{
Node* cur = _root;
while(cur)
{
if(cur->_val == val)
return cur;
else if(cur->_val > val)
cur = cur->_left;
else
cur = cur->_right;
}
return cur;
}
BTree(const BTree<T>& bt)
:_root(copy(bt._root))
{}
Node* copy(Node* root)
{
if(root == nullptr)
return nullptr;
Node* newNode = new Node(root->_val);
newNode->_left = copy(root->_left);
newNode->_right = copy(root->_right);
return newNode;
}
bool insert(const T& val)
{
//如果是空树,则直接插入结点,返回true
if(_root == nullptr)
{
_root = new Node(val);
return true;
}
Node* cur = _root;
Node* parent = nullptr;//指向父结点的指针
//若不是空树,需要遍历找到插入结点的位置
while(cur)
{
parent = cur;
//相同的结点,直接返回false,因为搜索树中不能出现相同的值
if(cur->_val == val)
return false;
//比根节点小则继续查找左子树
else if(cur->_val > val)
cur = cur->_left;
//比根节点大则继续查找右子树
else
cur = cur->_right;
}
//创建结点
cur = new Node(val);
//插入:判断插入到父结点的左边还是右边
if(parent->_val > val)
parent->_left = cur;
else
parent->_right = cur;
return true;
}
void _inorder(Node* root)
{
if(root)
{
_inorder(root->_left);
cout << root->_val << " ";
_inorder(root->_right);
}
}
bool erase(const T& val)
{
//遍历搜索二叉树,找到要删除的结点
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_val == val)//找到结点,结束循环
break;
parent = cur;
if(cur->_val > val)
cur = cur->_left;
else
cur = cur->_right;
}
//如果cur为空,说明没有找到要删除的结点,则直接返回false
if(cur == nullptr)
return false;
//判断cur是否是叶子结点
if(cur->_left == nullptr && cur->_right == nullptr)
{
//判断这个结点是否是根结点
if(cur == _root)
{
//根结点置空
_root = nullptr;
}
else
{
//判断要删除的结点是父亲结点的哪一边
if(parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
}
//删除结点
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;
}
//删除cur
delete cur;
}
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//需要删除的结点既有左孩子也有右孩子
{
Node* leftRightMost = cur->_left;
parent = cur;
//找到左子树的最有结点
while(leftRightMost->_right)
{
parent = leftRightMost;
leftRightMost = leftRightMost->_right;
}
//交换左子树的最右结点和需要删除的结点
swap(cur->_val, leftRightMost->_val);
if(parent->_left == leftRightMost)
parent->_left = leftRightMost->_left;
else
parent->_right = leftRightMost->_left;
delete leftRightMost;
}
return true;
}
void inorder()
{
_inorder(_root);
cout << endl;
}
void destory(Node* root)
{
if(root)
{
destory(root->_left);
destory(root->_right);
delete root;
}
}
~BTree()
{
if(_root)
{
destory(_root);
_root = nullptr;
}
}
private:
Node* _root;//根
};
KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。 该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word,chinese>就构成一种键值对。
二叉搜索树KV模型代码总结:
#include <iostream>
using namespace std;
#include<time.h>
template<class K, class V>
struct BTreeNode
{
typedef BTreeNode<K, V> Node;
Node* _left;//左子树
Node* _right;//右子树
K _key;
V _value;
BTreeNode(const K& key = K(), const V& val = V())
:_left(nullptr)
,_right(nullptr)
,_key(key)
,_value(val)
{}
};
template<class K, class V>
class BTree
{
public:
typedef BTreeNode<K, V> Node;
BTree()
:_root(nullptr)
{}
Node* find(const K& key)
{
Node* cur = _root;
while(cur)
{
if(cur->_key == key)
return cur;
else if(cur->_key > key)
cur = cur->_left;
else
cur = cur->_right;
}
return cur;
}
BTree(const BTree<K, V>& bt)
:_root(copy(bt._root))
{}
Node* copy(Node* root)
{
if(root == nullptr)
return nullptr;
Node* newNode = new Node(root->_key, root->_value);
newNode->_left = copy(root->_left);
newNode->_right = copy(root->_right);
return newNode;
}
bool insert(const K& key, const V& val)
{
//如果是空树,则直接插入结点,返回true
if(_root == nullptr)
{
_root = new Node(key, val);
return true;
}
Node* cur = _root;
Node* parent = nullptr;//指向父结点的指针
//若不是空树,需要遍历找到插入结点的位置
while(cur)
{
parent = cur;
//相同的结点,直接返回false,因为搜索树中不能出现相同的值
if(cur->_key == key)
return false;
//比根节点小则继续查找左子树
else if(cur->_key > key)
cur = cur->_left;
//比根节点大则继续查找右子树
else
cur = cur->_right;
}
//创建结点
cur = new Node(key, val);
//插入:判断插入到父结点的左边还是右边
if(parent->_key > key)
parent->_left = cur;
else
parent->_right = cur;
return true;
}
void _inorder(Node* root)
{
if(root)
{
_inorder(root->_left);
cout << root->_key<< "-->" << root->_value << " ";
_inorder(root->_right);
}
}
bool erase(const K& key)
{
//遍历搜索二叉树,找到要删除的结点
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_key == key)//找到结点,结束循环
break;
parent = cur;
if(cur->_key > key)
cur = cur->_left;
else
cur = cur->_right;
}
//如果cur为空,说明没有找到要删除的结点,则直接返回false
if(cur == nullptr)
return false;
//判断cur是否是叶子结点
if(cur->_left == nullptr && cur->_right == nullptr)
{
//判断这个结点是否是根结点
if(cur == _root)
{
//根结点置空
_root = nullptr;
}
else
{
//判断要删除的结点是父亲结点的哪一边
if(parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
}
//删除结点
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;
}
//删除cur
delete cur;
}
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//需要删除的结点既有左孩子也有右孩子
{
Node* leftRightMost = cur->_left;
parent = cur;
//找到左子树的最有结点
while(leftRightMost->_right)
{
parent = leftRightMost;
leftRightMost = leftRightMost->_right;
}
//交换左子树的最右结点和需要删除的结点
swap(cur->_key, leftRightMost->_key);
swap(cur->_value,leftRightMost->_value);
if(parent->_left == leftRightMost)
parent->_left = leftRightMost->_left;
else
parent->_right = leftRightMost->_left;
delete leftRightMost;
}
return true;
}
void inorder()
{
_inorder(_root);
cout << endl;
}
void destory(Node* root)
{
if(root)
{
destory(root->_left);
destory(root->_right);
delete root;
}
}
~BTree()
{
if(_root)
{
destory(_root);
_root = nullptr;
}
}
private:
Node* _root;//根
};