一、BST的基本概念
二叉树的其中一个重要的应用,是提供一种快速查找数据的方法,即将数据节点按照某种规律形成一棵二叉树,然后利用二叉树特殊的逻辑结构减少搜索数据的次数,提高查找的效率。
这种按照某种规律构建,用来提高搜索性能的二叉树,被称为搜索二叉树(Binary Search Tree),即BST。
具体来说,二叉树提高搜索效率的秘诀在于:按照” 小 - 中 - 大 ”(“ 大 - 中 - 小 ”也是可以的)的规律来存储数据,即对于任意一个节点,都可以明确找到其值 ≥ 其左孩子节点,且 ≤ 其右孩子节点。
如图:
由于树中所有的节点均满足“ 小 - 中 - 大 ”的规律,因此当从根节点开始查找某个节点时,速度比顺序查找要快得多,例如要找节点38,当发现 38 大于根节点 13 后,就可以确定 13 的左子树一定没有 38 ,这就去掉了半边子树的节点。
因此,二叉搜索树又被称为二叉排序树、二叉查找树。
实际上,对于一棵二叉树而言,其搜索节点的时间复杂度,最糟糕的情形是其退化为链表,最乐观的情形是完美或完全二叉树,那么其搜索时间复杂度就是介于:
二、BST的基本操作
(一)节点设计
图解:
示例代码:
// 二叉树的节点设计
typedef struct node
{
// 左子树指针
struct node *lchild;
// 数据域
int *data;
// 右子树指针
struct node *rchild;
}node_t, *node_p, binary_tree;
(二)初始化二叉树(包括根节点、或者其它树节点都可以)
图解:
示例代码:
/**
* @brief 初始化树节点
* @note None
* @param data:根节点的数据
* @retval 成功:返回指向这个根的数据节点的指针
* 失败:返回NULL
*/
node_p BINARY_TREE_InitNode(int *data)
{
// 1、申请堆内存空间
node_p p = malloc(sizeof(node_t));
bzero(p, sizeof(node_t));
// 2、对堆空间进行赋值操作
if ( p != NULL )
{
// 数据域
p->data = malloc(sizeof(int));
p->data[0] = *data;
// 指针域
p->lchild = NULL; // 左子树指针
p->rchild = NULL // 右子树指针
}
else
return NULL;
// 3、成功返回p
return p;
}
(三)插入数据
对于BST来说,插入一个节点主要是要保持其“ 小 - 中 - 大 ”的逻辑不变,因此插入的节点的逻辑可以从根节点开始,一步步寻找新节点的“ 最终归宿 ”,例如在如下BST中,要插入新节点16,最终应该插入到节点17的左孩子处。
在实现插入算法时,由于树状结构本身是递归的,因此可以使用递归函数更优雅地实现。
图解:
示例代码:
/**
* @brief 插入数据
* @note 以二叉排序的方式进行插入(比根节点数据大的放右边,小的放左边)
* @param root_node: 根节点
* new_node: 要插入的新节点
* @retval 成功:返回指向这个二叉树根节点的指针
* 失败:返回NULL
*/
node_p BINARY_TREE_InsertData(node_p root_node, node_p new_node)
{
// 1、设置退出条件
if (root_node == NULL)
return new_node;
// 2、比较数据的大小,看数据要插入左子树,还是右子树
// a、插入到左子树
if ( new_node->data[0] < root_node->data[0])
{
root_node->lchild = BINARY_TREE_InsertData(root_node->lchild, new_node);
}
// b、插入到右子树
else if ( new_node->data[0] > root_node->data[0])
{
root_node->rchild = BINARY_TREE_InsertData(root_node->rchild, new_node);
}
else
{
printf("%d的数据已经存在了,请重新输入\n", new_node->data[0]);
free(new_node);
}
// 3、返回根节点
return root_node;
}
(四)删除数据
删除一个BST的节点同样要遵循一个原则,即删除节点后仍要保持“ 小 - 中 - 大 ”的逻辑关系。
假设要删除的节点是x,思路:
1. 若要删除的节点 < 根节点,则递归地在左子树中删除x;
2. 若要删除的节点 > 根节点,则递归地在右子树中删除x;
3. 若要删除的节点恰好就是根节点,则分如下几种情况:
(1)根节点若有左子树,则用左子树中最大的节点 max 替换根节点,并在左子树中递归删除max;
(2)根节点若有右子树,则用右子树中最小的节点 min 替换根节点,并在右子树中递归删除min;
(3)根节点没有子树,直接删除。
如图,假设在一棵二叉树中要删除节点 15 ,在找到节点之后,判断其有左子树,那么就沿着其左子树找到最右下角(最大)的节点 12 ,替换要删除的节点 15 ,然后再将多余的节点 12 删掉。
如果要删除的节点没有左子树,只有右子树,那么情况是完全对称的,如下图,假设要删除节点 25 ,由于 25 没有左子树,因此找到其右子树中最左下角(最小)的节点 26 ,替换要删除的节点 25 ,然后再将多余的节点 26 删掉。
图解:
示例代码:
/**
* @brief 删除数据
* @note None
* @param root_node: 根节点
* del_data: 要删除的数据
* @retval 成功:返回指向这个二叉树根节点的指针
* 失败:返回NULL
*/
node_p BINARY_TREE_DelData(node_p root_node, int del_data)
{
// 1、如果根节点为空,直接返回NULL即可
if (root_node == NULL)
return NULL;
// 2、若del_data小于根节点,则递归地在左子树上删除它
if ( del_data < *(root_node->data))
{
root_node->lchild = BINARY_TREE_DelData(root_node->lchild, del_data);
}
// 3、若del_data大于根节点,则递归地在右子树中删除它
else if (del_data > *(root_node->data))
{
root_node->rchild = BINARY_TREE_DelData(root_node->rchild, del_data);
}
// 4.若要删除的节点恰好就是根节点,则分如下几种情况:
else
{
// a、根节点若有左子树,则用左子树中最大的节点max替换根节点,并在左子树中递归删除max
if (root_node->lchild != NULL)
{
// 建立中间值(树节点)(左子树最大的节点的值)
node_p max_node = NULL;
// 找到左子树最大值
for (max_node = root_node->lchild; max_node->rchild != NULL; max_node = max_node->rchild);
// 将要删除的根节点,和其左子树中的最大值进行交换
*(root_node->data) = *(max_node->data);
// 将左子树中的最大值删除
root_node->lchild = BINARY_TREE_DelData(root_node->lchild, *(max_node->data));
}
// b、根节点若有右子树,则用右子树中最小的节点min替换根节点,并在右子树中递归删除min
else if (root_node->rchild != NULL)
{
// 建立中间值(树节点)(左子树最大的节点的值)
node_p min_node = NULL;
// 找到右子树最大值
for (min_node = root_node->rchild; min_node->lchild != NULL; min_node = min_node->lchild);
// 将要删除的根节点,和其右子树中的最小值进行交换
*(root_node->data) = *(min_node->data);
// 将右子树中的最小值删除
root_node->rchild = BINARY_TREE_DelData(root_node->rchild, *(min_node->data));
}
else
{
free(root_node->data);
free(root_node);
return NULL;
}
}
// 3、返回root_node
return root_node;
}
(五)遍历数据
图解:
示例代码:
/**
* @brief 前序遍历
* @note None
* @param root_node: 根节点
* @retval None
*/
void BINARY_TREE_PreorderShow(node_p root_node)
{
// 1、如果根节点为空,那就证明执行完了这个二叉树(退出条件)
if (root_node == NULL)
return ;
// 2、先访问根节点
printf("%d\t", root_node->data[0]); // 根
// 3、再次使用前序遍历,遍历左子树、右子树
BINARY_TREE_PreorderShow(root_node->lchild); // 左
BINARY_TREE_PreorderShow(root_node->rchild); // 右
}
/**
* @brief 中序遍历
* @note None
* @param root_node: 根节点
* @retval None
*/
void BINARY_TREE_InfixOrderShow(node_p root_node)
{
// 1、如果根节点为空,那就证明执行完了这个二叉树(退出条件)
if (root_node == NULL)
return ;
// 2、
BINARY_TREE_InfixOrderShow(root_node->lchild); // 左
printf("%d\t", root_node->data[0]); // 根
BINARY_TREE_InfixOrderShow(root_node->rchild); // 右
}
/**
* @brief 后序遍历
* @note None
* @param root_node: 根节点
* @retval None
*/
void BINARY_TREE_EpilogueShow(node_p root_node)
{
// 1、如果根节点为空,那就证明执行完了这个二叉树(退出条件)
if (root_node == NULL)
return ;
// 2、
BINARY_TREE_EpilogueShow(root_node->lchild); // 左
BINARY_TREE_EpilogueShow(root_node->rchild); // 右
printf("%d\t", root_node->data[0]); // 根
}
/**
* @brief 按层遍历
* @note 利用的是队列对其数据进行处理
* @param root_node: 根节点
* @retval None
*/
void BINARY_TREE_FloorShow(node_p root_node)
{
// 1、如果根节点为空,那么就证明执行完了这个二叉树
if (root_node == NULL)
return;
// 2、将根节点的数据入队
link_queue_p p = init_queue(); // 初始化队列
en_queue(p, root_node); // 将根节点的数据入队
// 3、
node_p tmp_p = NULL;
while (!is_empty_q(p)) // 如果链式队列不是空的话,就一直执行下去
{
// a、出队并访问队头
out_queue(p, &tmp_p);
printf("%d\t", *(tmp_p)->data);
// b、依次将左右子树入队
if ( tmp_p->lchild != NULL)
{
en_queue(p, tmp_p->lchild);
}
else if (tmp_p->rchild != NULL)
{
en_queue(p, tmp_p->rchild);
}
}
}
(六)查改数据
图解:
示例代码:
/**
* @brief 查改数据
* @note None
* @param root_node: 根节点
* find_data: 要查找的数据
* @retval None
*/
int* BINARY_TREE_FindeData(node_p root_node, int find_data)
{
// 1、如果根节点为空,那么就证明执行完了这个二叉树
if (root_node == NULL)
return NULL;
// 2、将根节点的数据入队
link_queue_p p = init_queue(); // 初始化队列
en_queue(p, root_node); // 将根节点的数据入队
// 3、
node_p tmp_p = NULL;
while ( !is_empty_q(p) ) // 如果链式队列不是空的话,就一直执行下去
{
// a、出队并访问队头
out_queue(p, &tmp_p);
if (*(tmp_p)->data == find_data)
{
return &(*(tmp_p)->data);
}
// b、依次将左右子树入队
else if ( tmp_p->lchild != NULL)
{
en_queue(p, tmp_p->lchild);
}
else if (tmp_p->rchild != NULL)
{
en_queue(p, tmp_p->rchild);
}
}
}
(七)销毁二叉树
示例代码:
/**
* @brief 销毁二叉树
* @note None
* @param root_node: 根节点
* @retval None
*/
void BINARY_TREE_UnInit(node_p root_node)
{
// 1、如果根节点为空,则直接返回即可
if (root_node == NULL)
return;
// 2、递归的销毁左子树
BINARY_TREE_UnInit(root_node->lchild);
// 3、递归的销毁右子树
BINARY_TREE_UnInit(root_node->rchild);
// 4、销毁根节点
free(root_node->data);
free(root_node);
}
(网页查看链表情况)
提示:为了检验创建出来的二叉树是否正确,可以使用如下代码将二叉树以网页形式展现出来:
#define TREENODE node // 声明自定义的二叉树节点为TREENODE
#include "drawtree.h" // 包含画树代码
int main()
{
...
// 以网页形式展现二叉树
draw(root);
}
其中,node是自己定义的二叉树的节点的类型,在包含 drawtree.h 之前定义 TREENODE 这个宏为 node ,即可展示对应二叉树。
点击下载:drawtree.h
以上内容就是我的课后笔记了。