一、定义
二叉树:将二叉树定义为一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一棵(独立的)子二叉树。
二叉查找树(BST):是一棵二叉树,其中每一个节点都含有一个可比较的键(以及相关联的值)且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键。
根结点比所有的左子树中的结点大,比所有右子树结点小。
二、基本实现
二叉树类及结点定义如下图:
//结点类
template<typename K, typename V>
struct BSTreeNode
{
K _key; //结点键
V _val; //节点值
BSTreeNode<K, V>* _left; //左指针
BSTreeNode<K, V>* _right; //右指针
int _nodeCnt; //以该节点为根节点的子树的结点总数
//构造函数
BSTreeNode(const K& key, const V& value, const int& cnt = 0) :
_key(key), _val(value), _nodeCnt(cnt),
_left(nullptr), _right(nullptr)
{}
};
每个结点包含:键、值、左子结点、右子结点和结点计数器。
- 左链接指向一棵由小于该结点的所有键组成的二叉查找树
- 右链接指向一棵由大于该结点的所有键组成的二叉查找树
- 结点计数器给出了以该结点为根结点的子树的结点总数,通用计算方法:
size(x) = size(x.left) + size(x.right) + 1
template<typename K, typename V>
模板类K为键的类型,V为值的类型。
2.1 查找
查找逻辑:
- 如果是树是空的,则查找未命中
- 如果被查找的键和根结点的键相等,查找命中
- 否则,递归地在适当的子树中继续查找。如果被查找的键较小选择左子树,较大则选择右子树。
// 查找
V get(Node* x, const K& key)
{
if (x == nullptr) return nullptr;
int cmp = compareTo(key, x->_key);
if (cmp < 0) return get(x->_left, key); //比当前节点小,查找其左子树
if (cmp > 0) return get(x->_right, key); //比当前节点大,查找其右子树
else return x->_key; //查找命中,返回结点的key
}
和二分查找每次迭代之后查找的区间减半一样,在二叉查找树中,随着结点不断向下查找,当前结点所表示的子树大小也在减小。
查找何时结束?
当找到一个含有被查找的键的结点或者当前子树变为空时。
对于命中的查找,路径在含有被查找的键的结点出结束。对于未命中的查找,路径的终点是一个空链接。
2.2 插入
插入逻辑与递归查找很相似。
- 如果树是空的,就返回一个含有该键值对的新结点
- 如果被查找的键小于根结点的键,继续在左子树中插入该键
- 否则,右子树中插入该键
- 更新当前结点计数器的值
二叉树插入L操作如下图所示:
- 沿着根结点向下开始查找,发现L<M,于是查找M左子树,左子树为空,创建一个新结点
Node(key, Val, 1)
,结点数量为1 - 新结点为M的左子结点
- 沿搜索路径向上更新链接并增加结点计数器的值
// 插入
Node* put(Node* x, const K& key, const V& val)
{
if (x == nullptr) return new Node(key, val, 1);
int cmp = compareTo(key, x->_key);
if (cmp < 0) x->_left = put(x->_left, key, val); //在左子树中找
else if (cmp > 0) x->_right = put(x->_right, key, val); //在右子树中找
else x->_val = val; //找到则更新值
x->_nodeCnt = size(x->_left) + size(x->_right) + 1;
return x;
}
2.3 最小键与最大键
如果根结点的左链接为空,那么一棵二叉查找树中最小的键就是根结点;如果左链接非空,那么树中的最小键就是左子树中的最小键。从图形上来看,最小键是树最左边的那个键,如下图中的11。
同理,若根结点的右链接为空,最大键就为根结点;根结点的右链接非空,最大键就是右子树中的最大键。从图形上看,最大键是树最右边的键,如下图中的40。
下图中,最小键不可能出现在结点19的左分支中,因为12的所有右结点都要比12大;同样最大键也不可能出现在结点37的右分枝中。
例如给定结点7,从19 7 22这颗子树来看,是满足二叉树定义的,但是按照插入的算法逻辑,7只能出现在结点12的左子树中,进而出现在结点11的左子树中,根本不会进入到19这个分支中。
// 最小键
Node* minNode(Node* x)
{
if (x->_left == nullptr) return x;
return minNode(x->_left);
}
// 最大键
Node* maxNode(Node* x)
{
if (x->_right == nullptr) return x;
return maxNode(x->_right);
}
2.4 向上取整和向下取整
向下取整数floor(key):找到小于等于key的最大键。
向上取整数ceiling(key):找到大于等于key的最小键。
floor(20),找到小于等于20的最大键,过程如下:
20<25,floor(20)肯定在25的左子树中;
20>12,floor(20)可能在12的右子树中;
20>19,floor(20)可能在19的右子树中;
20<22,22是叶子结点,其子节点为null,直接返回22的根节点19。
ceiling(26),找到大于等于26的最小键,过程如下:
26>25,ceiling(26)肯定在25的右子树中;
26<31,ceiling(26)可能在31的左子树中;
26<29,ceiling(26)可能在29的左子树中;
26<27,27是叶子结点,其子节点为null, 直接返回结点27。
// 找到小于等于key的最大键
Node* floor(Node* x, const K& key)
{
if (x == nullptr) return nullptr;
int cmp = compareTo(key, x->_key);
if (cmp == 0) return x;
if (cmp < 0) return floor(x->_left, key);
//如果key大于当前结点
Node t = floor(x->_right, key);
if (t != nullptr) return t;//不为nullptr
else return x;//为nullptr
}
// 找到大于等于key的最小键
Node* ceiling(Node* x, const K& key)
{
if (x == nullptr) return nullptr;
int cmp = compareTo(key, x->_key);
if (cmp == 0) return x;
if (cmp > 0) return floor(x->_right, key);
//如果key小于当前结点
Node t = floor(x->_left, key);
if (t != nullptr) return t;//不为nullptr
else return x;//为nullptr
}
- floor与ceiling代码逻辑几乎一样,把floor中的
<
改为>
(同时left改成right) - floor要么返回nullptr,要么返回结点
- floor如果key大于当前节点,那小于等于key的节点可能在当前节点右子树中