本篇为学习 清华大学-邓俊辉-数据结构课 实现的二叉搜索树(Binary Search Tree
),主要集中于查找、插入以及删除操作。
数据结构实现的顺序:节点 → \rightarrow → 二叉树 → \rightarrow → 二叉搜索树
节点Node
为了方便操作,这里直接将Node
定义为二叉树BinaryTree
和搜索树BST
的友元friend
类,你也可以不定义为友元类,而改成给节点实现查询和修改操作。
API
// 获得节点高度的宏 空节点高度-1
#define getHeight(node) ((node) ? (node)->height : -1)
class Node
{
friend class BinaryTree;
friend class BST;
private:
int data;
Node* parent; // 父节点
Node* left, * right; // 左右子节点
int height; // 高度
public:
Node(int d = 0, Node* p = NULL, Node* l = NULL, Node* r = NULL, int h = 0)
: data(d), parent(p), left(l), right(r), height(h) {};
Node* insertAsLeft(int data); // 给当前节点加入左子
Node* insertAsRight(int data); // 给当前节点加入右子
Node* getSuccessor() const; // 得到中序遍历下的后继节点
Node* getTallerChild(); // 得到高度较高的那一个子节点
bool isRight() const; // 判断当前节点是否为某个节点的右子
bool isLeft() const; // 判断当前节点是否为某个节点的左子
};
这里有六个方法以及一个宏,主要是为了方便之后二叉树的实现中对节点的调用:
insertAsLeft
和insertAsRight
用于分别在当前节点左和右插入子节点,并返回对应子节点。getSuccessor
返回当前节点在树的中序遍历中的直接后继节点。getTallerChild()
返回当前节点的孩子中高度较高的那个。isRight
和isLeft
判断当前节点是否为某个节点的子节点。- 宏
getHeight(node)
得到节点的高度,封装为宏而不是直接取node->height
是为了防止节点为空的情况。
节点方法的实现
比较简单,不做过多的解释,后面有2点需要说明。
Node* Node::insertAsLeft(int data)
{
if (!left) left = new Node(data, this);
return left;
}
Node* Node::insertAsRight(int data)
{
if (!right) right = new Node(data, this);
return right;
}
Node* Node::getSuccessor() const
{
Node* successor;
// 如果有右子,那么后继在右子树中
if (right) {
successor = right;
while ((successor->left)) successor = successor->left;
}
// 如果没有,那么后继在祖先中
else {
successor = this->parent;
while (successor && successor->isRight()) successor = successor->parent;
if(successor) successor = successor->parent;
}
return successor;
}
Node* Node::getTallerChild()
{ // 使用了前面定义的宏getHeight
return (getHeight(left) >= getHeight(right)) ? left : right;
}
bool Node::isRight() const
{
// 若为根则不是
if (!parent) return false;
return parent->right == this;
}
bool Node::isLeft() const
{
// 若为根则不是
if (!parent) return false;
return parent->left == this;
}
要注意以下两点:
插入子节点时如果位置上已经有节点,那么直接返回当前已有的子节点,不进行插入操作。
取当前节点中序遍历下的直接后继有两种情况:
- 若当前节点有右子树,那么沿右子树的左子不断向下到最后一个节点,这个节点就是直接后继。
如图所示,
2
为current
的直接后继:- 若当前节点没有右子树,那么直接后继就是将当前节点作为其左子树中一员的最近祖先。
如下图所示,两棵树的
1
都是current
的直接后继:
二叉树BinaryTree
下面实现了一个简单的二叉树及基本功能,为后面实现搜索树做准备。
API
class BinaryTree
{
protected:
int size_; // 树的节点总数
Node* root_; // 根节点
int updateHeight(Node* node); // 更新node的高度
void updateHeightAbove(Node* node); // 更新node及其所有祖先的高度
public:
BinaryTree() : size_(0), root_(NULL) {};
~BinaryTree();
int size() const { return size_; };
bool empty() const { return root_ == NULL; };
Node* insertAsRoot(int data); // 创建根节点
Node* insertAsLeft(Node* subroot, int data); // 插入左子节点
Node* insertAsRight(Node* subroot, int data); // 插入右子节点
};
方法的实现
~BinaryTree()
析构函数只需使用任意一种方法遍历树并删除节点就可以了,下面使用层序遍历的方法实现:
BinaryTree::~BinaryTree()
{
if (root_) {
queue<Node*> q;
q.emplace(root_);
while (!q.empty()) {
Node* node = q.front();
q.pop();
if (node->left) q.emplace(node->left);
if (node->right) q.emplace(node->right);
delete node;
}
}
}
insertAsXxx()
在不同位置插入节点,一共有三个方法:
// 创建根节点:
Node* BinaryTree::insertAsRoot(int data)
{
if (!root_) {
size_ = 1;
root_ = new Node(data);
}
return root_;
}
// 为subroot加入左子节点
Node* BinaryTree::insertAsLeft(Node* subroot, int data)
{
if (!subroot) return NULL;
++size_;
subroot->insertAsLeft(data);
updateHeightAbove(subroot);
return subroot->left;
}
// 为subroot加入右子节点
Node* BinaryTree::insertAsRight(Node* subroot, int data)
{
if (!subroot) return NULL;
++size_;
subroot->insertAsRight(data);
updateHeightAbove(subroot);
return subroot->right;
}
updateHeight()
与updateHeightAbove()
更新树中指定节点的高度。
注意:节点的高度是指当前节点到根节点的路径中的边个数,根节点高度为0
,如果为空树那么根节点高度-1
。
// 更新node的高度
int BinaryTree::updateHeight(Node* node)
{// 高度为子节点中高度最高的那个加一
node->height = 1 + max(node->left->height, node->right->height);
return node->height;
}
// 更新node及其所有祖先的高度
void BinaryTree::updateHeightAbove(Node* node)
{
while (node) {
updateHeight(node);
node = node->parent;
}
}
二叉搜索树BST
API
搜索树继承了上面的BinaryTree
。
class BST: public BinaryTree
{
protected:
Node* hot_; // 下面解释
Node*& searchIn(Node*& root, int data);// search的辅助函数
void removeAt(Node*& target); // remove的辅助函数
public:
Node*& search(int data); // 查找
Node*& insert(int data); // 插入
bool remove(int data); // 删除
};
3个操作:
search
查找并返回查找结果insert
插入元素并返回该元素对应节点remove
删除指定值的节点
注意:
hot_
用于在各项操作中记录父节点,这样便于跨函数操作,具体作用可以看下面的代码实现。- 上面的操作中大多使用了
Node*&
,这类似于Node**
,这样可以修改引用的那个指针指向的位置。
方法的实现
search(data)
方法:从根开始搜索,若当前节点值小于data
,则转向左子;若大于,则转向右子;若找到了,则返回目标节点;若到树叶后还是没找到,则返回NULL
。
下面给出实现,以及各个变量在搜索结束时的位置图示方便理解。
Node*& BST::search(int data)
{
hot_ = NULL;
return searchIn(root_, data);
}
Node*& BST::searchIn(Node*& root, int e) {
// 终止条件为树找到目标或到叶子还未找到
if (!root || root->data == e) return root;
// 记录当前节点
hot_ = root;
// 递归查找
return searchIn((root->data > e ? root->left : root->right), e);
}
下面是三种情况下hot_
与return
节点在搜索结束时的位置:
- 搜索
25
:
- 搜索
17
,注意此时返回的节点为15
右子的引用(此时为空):
- 搜索
10
,此时hot_
为10
的空父节点:
insert(data)
先用刚刚实现的search(data)
查找要插入数据是否已经存在,若是,直接返回,若否,则在search
返回的那个节点处进行插入。
Node*& BST::insert(int data)
{
Node*& node = search(data);
// 如果已有要插入数据就直接返回
if (node) return node;
++size_;
node = new Node(data, hot_);
// 更新祖先高度
updateHeightAbove(node);
return node;
}
由于函数中先进行了一次search
,此时hot_
指向的正是将要加入的新节点的父节点,而search
返回的那个指针引用node
正是这次数据应该存放的新节点,如代码中6
行所示,将node
指向新节点同时将hot_
指定为父节点即可。
remove(data)
有2种情况:
- 若要删除节点至多
1
个子节点,则令其父节点指向其子节点或NULL
。 - 若要删除节点有
2
个子节点,那么找到要删除节点中序遍历下的直接后继(在Node
中已实现),交换数据,然后对该后继(有一个右子或没有子,此时为情况1
)进行删除操作。
具体实现的细节已在代码中注释:
bool BST::remove(int data)
{
Node*& target = search(data);
if (target == NULL) return false;
removeAt(target); // 删除的实际逻辑在这里面
--size_;
updateHeightAbove(hot_); // 在removeAt中已经修改了hot_,以此再更新高度
return true;
}
Node* BST::removeAt(Node*& target)
{
Node* willDelete = target; // 要删除的节点
Node* succ; // 接替者
if (!target->left) succ = target = target->right;
else if (!target->right) succ = target = target->left;
else {
// 取得中序遍历后继节点
Node* successor = target->getSuccessor();
// 将后继的值赋给当前要删除节点
target->data = successor->data;
// 要删除的节点改为后继节点
willDelete = successor;
// 后继只 可能有右子树 或 仅为叶子节点,因此该后继的父亲节点只需将左子指向该后继的右子
((successor == target->right) ? successor->parent->right : successor->parent->left)
= succ = successor->right;
}
// 记录操作节点的父节点
hot_ = willDelete->parent;
// 接替者获得其父亲
if (succ) succ->parent = hot_;
delete willDelete;
return succ;
}
所有二叉搜索树的代码已上传github
,sir, this way.