二叉搜索树(C++)
一、背景
当我们在应用在n个动态整数当中搜索某个整数的时候会有几种方法
①使用动态数组,平均时间复杂度为O(n)
②使用一个有序的动态数组,使用二分搜索法,虽然它的最坏时间复杂度只有O(logn),但是它的添加、深处的平均时间复杂度过高,为O(n)。
③使用链表与使用动态数组无太大差别,都需要从第一个元素开始搜索,平均时间复杂度也为O(n),过高。
因此我们考虑是否有更为优化的方针
接下来就引出二叉搜索树,其将数据有序排放,搜索的最坏复杂度为O(logn),因为采用链表中的方法将数据连接,添加、删除等方法的最坏时间复杂度也优化为O(logn)。
二、简介
二叉搜索树(Binary Search Tree)是二叉树的一种,简称BST,又被称为二叉查找树,二叉排序树。其特点是:
①任意一个节点的值都大于其左子树所有节点的值
②任意一个节点的值都小于其右子树所有节点的值
③其左右子树也是一棵二叉搜索树
首先是二叉搜索树的创立,我们使用了模板这一方法使代码更完备化,任何类型的数据都可以放入搜索树中。
在节点的创立中,在元素和左右子节点的基础上增加了父节点这一成员方面后面代码的实现。在成员函数中也增加了是否为叶子节点和是否有两个子节点的函数,因为在后期的应用中较多的使用了这些判断,所以对其进行了封装。
在二叉搜索树的创立中只使用了元素个数和根节点这两个元素,就已经能够比较完整的实现了整个代码。而成员函数的实现分为两大块,一个是面向对象放在public区的函数供对象使用,一个是为了更好的实现函数封装放在private区。
本文的二叉搜索树在最基础的增加、删除、元素数量、是否为空、清空、搜索之外增加了前序、中序、后续、层序四大遍历方法,利用递归和迭代两种算法实现了求二叉树的高度并完成了检查是否是完全二叉树的实现。
三、解析
接下来对二叉搜索树的几个函数进行分析。
1、添加
添加时要分为两种情况:
①添加的是根节点 :直接利用构造函数给根节点赋值
②添加的不是根节点:要确定添加节点的位置而且还要增加节点间的连接
而要确定添加节点的位置就要不断的那这些节点和增加的数据进行比较,最后找到空节点再放入。因此我们增加了 compare()内部函数
因为我们定义的是一个泛型的类,可以传入任何数据类型,而且返回值需要是int数据类型,因此对于比较我们要考虑较为全面。
对于整型和字符型我们可以直接进行相减来检测其与0的关系来比价其大小。但是对于浮点数类型相加减后依旧是浮点数类型,直接进行相减可能会有数据的误差,因此我们使用floor() 向下取整和ceil()向上取整的函数对其进行处理。
下面是 int compare(E e1,E e2)
函数的代码。
/**
*return 返回值等于0,代表e1,e2相等,返回值大于0,代表e1大于e2,返回值小于0,代表e1小于e2,
*/
template <typename E>
int BST<E>::compare(E e1, E e2) //因为C++无法实现接口,因此无法对类类型的数据进行自定义比较,可利用Java和C#来实现
{
if (e1 - e2 > 0) //防止传入的数据类型既有整形也有浮点数类型
{
return ceil(e1 - e2); //利用ceil()和floor()函数来把比较的值改成整型返回
}
else if (e1 - e2 < 0)
{
return floor(e1 - e2);
}
else
{
return 0;
}
}
add()方法的实现比较简单,直接看代码后的注释就可以大概理解。
下面是 void add(E element)
函数的代码。
template <typename E>
void BST<E>::add(E element) //添加元素
{
Element_Not_Null_Check(element); //防止添加元素为空
if (root == NULL) //添加第一个节点
{
root = new Node<E> (element, NULL);
size++;
}
//添加不是第一个节点
Node<E> *parent = NULL; //找到父节点
Node<E> *node = root;
int cmp = 0;
while (node != NULL)
{
cmp = compare(element, node->element); //保存比较的值来确定插入左侧还是右侧
parent = node; //保存父节点的位置
if (cmp > 0)
{
node = node->right;
}
else if (cmp < 0)
{
node = node->left;
}
else //相等覆盖
{
node->element = element;
return;
}
}
//看看插入到父节点的哪个位置
Node<E> *newnode = new Node<E>(element, parent);
if (cmp > 0) //大于放在右侧
{
parent->right = newnode;
}
else //小于放在左侧
{
parent->left = newnode;
}
size++;
}
2、遍历
四大遍历方法:
①前序遍历(Preorder Traversal):
访问顺序:根节点->前序遍历左子树->前序遍历右子树
根据上图的数据我们的访问顺序为:
因为7为根节点,访问其左子树便为4,再以4作为根节点,访问其左子树便为2,再以2作为根节点,访问其左子树便为1,1无子节点遍开始访问2的右子树3,以此类推,就可推导出全部的访问顺序。
7->4->2->1->3->5->9->8->11->10->12
根据上文的推导我们很容易的想到递归
首先实现对于前序遍历函数的接口函数
下面是 Node_Pre_Oreder_Traversal(Node<E>* node)
函数的代码。
template <typename E>
void BST<E>::Node_Pre_Oreder_Traversal(Node<E>* node) //前序遍历的接口 访问顺序为:根节点->前序遍历左子树->前序遍历右子树
{
if (node == NULL)
{
return;
}
cout << "元素->" << node->element<<" ";
Node_Pre_Oreder_Traversal(node->left);
Node_Pre_Oreder_Traversal(node->right);
}
然后再来实现面向对象的前序遍历函数
下面是 Pre_Oreder_Traversal()
函数的代码。
template <typename E>
void BST<E>::Pre_Oreder_Traversal() //前序遍历
{
Node_Pre_Oreder_Traversal(root); //从根节点开始遍历
}
②中序遍历(Inorder Traversal):
访问顺序:中序遍历左子树->根节点->中序遍历右子树(左右顺序可颠倒)
根据上图的数据我们的访问顺序为:
先访问左边则先访问根节点7的左子树,然后再以4为根节点则访问其左子树2,然后再以2为根节点访问其左子树1,此时1无左子树因此第一个访问1,接下来访问1对应的根节点2,再访问右子树3…以此类推可以推导出全部的访问顺序。
1->2->3->4->5->7->8->9->10->11->12
根据遍历结果我们可以发现访问出来的元素是从小到大的结果。其实也很容易理解,因为二叉搜索树的元素拜访顺寻本就是若是遇见比自己小的元素则放在左侧,若是比自己大则反之,中序遍历按照从左子节点到根节点再到右子节点便自然而然是从小到大的顺序。以此类推,若是我们的中序遍历的访问顺序写的是:中序遍历左子树->根节点->中序遍历右子树,那么遍历结果就是所有元素的从大到小的顺序。
与前序遍历一样,中序遍历采用的方法也是递归的算法
下面是 Node_In_Oreder_Traversal(Node<E>* node)
函数的代码。
template <typename E>
void BST<E>::Node_In_Oreder_Traversal(Node<E>* node) //中序遍历的接口 访问顺序为:中序遍历左子树->根节点->中序遍历右子树(这种出来的从小到大),若是先右子树则是从大到小
{
if (node == NULL)
{
return;
}
Node_In_Oreder_Traversal(node->left);
cout << "元素->" << node->element << " ";
Node_In_Oreder_Traversal(node->right);
}
然后再来实现面向对象的中序遍历函数
下面是 In_Oreder_Traversal()
函数的代码。
template <typename E>
void BST<E>::In_Oreder_Traversal() //中序遍历
{
Node_In_Oreder_Traversal(root); //从根节点开始遍历
}
③后序遍历(Postorder Traversal):
访问顺序:后序遍历左子树->后序遍历右子树->根节点(左右顺序可颠倒)
根据上图的数据我们的访问顺序为:
先访问左边则先访问根节点7的左子树,然后再以4为根节点则访问其左子树2,然后再以2为根节点访问其左子树1,此时1无左子树因此第一个访问1,接下来访问1对应的根节点的右子树3,再访问根节点2…以此类推可以推导出全部的访问顺序。
1->3->2->5->4->8->10->12->11->9->7
与前两个一样依旧是递归的算法
下面是 Node_Post_Oreder_Traversal(Node<E>* node)
函数的代码。
template <typename E>
void BST<E>::Node_Post_Oreder_Traversal(Node<E>* node) //后序遍历的接口 访问顺序为:后续遍历左子树->后序遍历右子树->根节点
{
if (node == NULL)
{
return;
}
Node_Post_Oreder_Traversal(node->left);
Node_Post_Oreder_Traversal(node->right);
cout << "元素->" << node->element << " ";
}
然后再来实现面向对象的后序遍历函数
下面是 Post_Oreder_Traversal()
函数的代码。
template <typename E>
void BST<E>::Post_Oreder_Traversal() //后序遍历
{
Node_Post_Oreder_Traversal(root); //从根节点开始遍历
}
④层序遍历(Level Order Traversal):
访问顺序:从上到下、从左到右一次访问每一个节点
根据上图的数据我们的访问顺序为:
第一层7,再访问第二层4、9…以此类推可以推导出全部的访问顺序
7->4->9->2->5->8->11->1->3->10->12
可以明显的观测到此遍历方法不再适合使用递归的算法。因为我们访问第一层7后再访问第二层4和9,第三层的访问顺序也是先访问4的子树点再访问9的子节树,第四层也是如此,意味着前面我们先访问的是哪一个后面我们就先访问哪一个的子节点。也就是先进先出的思想,因此我们可以考虑之前所学的存储结构:队列。
实现思路:使用队列
1.将根节点入队
2.循环执行以下操作,直到队列为空
⚪将队头节点A出队,进行访问
⚪将A的左子节点入队
⚪ 将A的右子节点入队
下面是 Level_Oreder_Traversal()
函数的代码。
template <typename E>
void BST<E>::Level_Oreder_Traversal() //层续遍历 访问顺序为:从上到下、从左到右一次访问每一个节点
{
if (root == NULL)
{
return;
}
queue<Node<E>*> que; //使用队列
que.push(root); //首先将根节点入队
while (!que.empty()) //循环操作直到队列为空
{
Node<E>* node = que.front(); //获得队头节点
que.pop(); //将对头结点出队
cout << "元素->" << node->element << " "; //输出队头节点元素
if (node->left != NULL) //若左子节点不为空先将左子节点入队
{
que.push(node->left);
}
if (node->right != NULL) //若右子节点不为空再将右子节点入队
{
que.push(node->right);
}
}
}
当我们掌握的这四个遍历方法,我们可以根据这些遍历来实现其它的功能例如:计算二叉树的高度、判断是否为完全二叉树、删除某个节点…接下来我们就用这些方法来实现。
3、求二叉树的高度
递归算法
二叉树的高度本质上就是根节点的高度,高度也就是当前节点到最远叶子所经历的节点数量。因此我们再写一个获取某一个节点高度的接口来实现求二叉树的高度。
如果某个节点不为空,那么其高度就是其左右子节点当中最大的数再加一。逐次递归就可以求出一个节点的高度。
例如节点4的高度就是节点2和者节点5中高度大的值加一,节点2的高度是左右子节点的最大高度,节点1和3的无子节点所以高度都为1 ,因此节点2的高度为2,节点5无左右子节点因此高度是1,因此节点4的高度为3…以此类推就可以退出根节点的高度
下面是 node_recursion_calcu_height
函数的代码。
template <typename E>
int BST<E>::node_recursion_calcu_height(Node<E>* node) //求高度的接口函数 二叉树的某个节点的高度(递归算法)
{
if (node == NULL)
{
return 0;
}
return 1 + max(node_calcu_height(node->left), node_calcu_height(node->right));
}
然后再来实现面向对象的前序遍历函数
下面是 recursion_calcu_height()
函数的代码。
template <typename E>
int BST<E>::recursion_calcu_height() //求二叉树的高度(递归算法)
{
return node_calcu_height(root);
}
迭代算法
因为求高度便相当于求二叉树的层数,根据之前层序遍历的算法,我们也可以采用队列的方法来求二叉树的高度。
首先若根节点为空的话那整棵树的高度便为0。
我们引入变量int height=0;来记录二叉树的高度。
然后考虑何时height的值会发生变化:每当层序遍历访问完一层时,height进行++直到遍历结束。
接下来我们需要考虑何时访问完一层:这一层的所有节点都已经被取出 ,因此我们需要引入变量int level_size;来储存每一层元素的数量。因为第一层的数量固定为1,所以我们先把其初始化为1。考虑层序遍历的算法:每次队头访问完后将其左右入队,因此一层访问完后下一层的元素数量就是队列的长度que.size();而每当取出一个元素level_size就–,直到这一层所有元素被取出再进入下一层。根据这个思路在while循环中加入一个if()语句判断此层是否访问完,若访问完就层数++进入下一层,level_size重新赋值为que.size(),进行新一轮的–。当所有元素遍历后,height的值也就已经计算完毕。
template <typename E>
int BST<E>::iteration_calcu_height() //求二叉树的高度(迭代算法) 利用层序遍历,有多少层高度就为几
{
if (root == NULL)
{
return 0;
}
int height = 0; //树的高度
int level_size = 1; //储存每一层的元素的数量
queue<Node<E>*> que; //使用队列
que.push(root); //首先将根节点入队
while (!que.empty()) //循环操作直到队列为空
{
Node<E>* node = que.front(); //获得队头节点
que.pop(); //将对头结点出队
level_size--;
if (node->left != NULL) //将左子节点入队
{
que.push(node->left);
}
if (node->right != NULL) //将右子节点入队
{
que.push(node->right);
}
if (level_size == 0) //意味着即将要访问下一层
{
level_size = que.size();
height++;
}
}
return height;
}
4、判断是否为完全二叉树
一棵树是否为完全二叉树是看所有元素是否是从上到下从左到右排布,最后一层的叶子节点是向左对其的。根据这个判断依据我们不难想到利用层序遍历的算法
首先检测是否为空树,如果是空树直接返回false。
如果树不为空,开始层序遍历二叉树(用队列)
⚪如果node.left != NULL && node.right != NULL,将node.left 、node.right 按顺序入队
⚪如果node.left == NULL && node.right != NULL返回false
⚪如果node.left != NULL && node.right == NULL或者如果node.left == NULL && node.right == NULL
√那么后面遍历的二叉树都应该为叶子节点,才是完全二叉树
√否则返回false
根据上边的思路顺序,我们需要对节点进行是否为叶子节点的检测,和是否有两个子节点的检测。
但是按照这样的思路容易出现一种bug就是在层序遍历入队时是左边不等于空就左边入队,右边不等于空就右边入队,但是在上面的思路中是左右都不为空才入队,当有了这样的bug的时候就会出现下面的误判。
在上面的案例中,节点2不是左右子节点都存在,因此节点2的子节点1就未入队,因而就没有所有节点都遍历。
因此我们在最后一组判断中增加一个是否拥有左子节点的判断,如果有就入队。这样就修复了bug。
template <typename E>
bool BST<E>::Check_Is_Complete() //检查是否为完全二叉树
{
//方法一:
if (root == NULL) //如果树为空,返回false
{
return false;
}
queue<Node<E>*> que; //使用队列
que.push(root); //首先将根节点入队
bool leaf = false;
while (!que.empty()) //循环操作直到队列为空
{
Node<E>* node = que.front(); //获得队头节点
que.pop(); //将对头结点出队
if (leaf&&!node->Check_Is_Leaf()) //若要求为叶子节点但不是叶子节点则返回false
{
return false;
}
if (node->Is_Has_TwoChildren()) //如果左右都不为空即有子节点就按顺序入队
{
que.push(node->left);
que.push(node->right);
}
else if (node->left == NULL && node->right != NULL) //如果左边为空右边有子节点则不是完全二叉树,返回false
{
return false;
}
else //其它情况即为左节点为空,那么接下来其他节点都应该为为叶子节点,否则返回false
{
leaf = true;
if (node->left != NULL) //由于第二个判断语句是在左右都有子节点才入队,会出现左子节点依旧有子节点存在的bug情况,需要在此再增加一个判断语句修复bug
{
que.push(node->left);
}
}
}
return true; //若到最后每一个节点都符合则返回true
}
但是上文的代码还是有点乱而且有重复判断的地方,因此我们根据上面的思路调整新的算法。首先引入层序遍历的代码,保证所有的元素都能不重复的遍历一遍
第一个判断语句是左子树是否为空,如果为空就是上面思路的第二种情况,因此在下方加入一个else if()遇见判断其对应的右子树是否为空,如果非空就返回false。
第二个判断语句是右子树是否为空,如果为空就是上面思路的第三种情况。
剩下的便是上面思路的第一种情况。
这样一来所有的情况都考虑进去了,还减少了上面思路的第一种情况的判断,是一种更为优化的思路。
template <typename E>
bool BST<E>::Check_Is_Complete() //检查是否为完全二叉树
{
//方法二: 比方法一减少了重复判断
if (root == NULL)
{
return false; //如果是空树,直接返回false
}
queue<Node<E>*> que; //使用队列
que.push(root); //首先将根节点入队
bool leaf = false;
while (!que.empty()) //循环操作直到队列为空
{
Node<E>* node = que.front(); //获得队头节点
que.pop(); //将对头结点出队
if (leaf && !node->Check_Is_Leaf()) //如果要求是叶子节点但是检查不是叶子节点,则返回错误
{
return false;
}
if (node->left != NULL) //若左子节点为空将左子节点入队
{
que.push(node->left);
}
else if (node->right != NULL) //如果左边为空但是右边不为空
{
return false;
}
if (node->right != NULL) //如果右边不为空将右子节点入队
{
que.push(node->right);
}
else //右边为空则要求接下来都是叶子节点
{
leaf = true;
}
}
}
5、求前驱节点
前驱节点:中序遍历时的前一个节点
因为前驱节点是不公开的,因此我们将它写为私有函数。
因为是二叉搜索树,所有前驱节点就是前一个比它小的节点
思路顺序:
①node.left != NULL
⚪predecessor = node.left.right.right…
√终止条件:right为NULL
②node.left == NULL && node.parent != NULL
⚪predecessor = node.parent.parent…
√终止条件:node在parent的右子树中
③node.left == NULL && node.parent == NULL
⚪无前驱节点
template <typename E>
Node<E>* BST<E>::Find_predecessor(Node<E>* node) //根据中序遍历寻找某个节点的前驱节点
{
if (node == NULL)
{
return NULL;
}
Node<E>* p = node->left;
if (p != NULL) // 如果左子节点不为空则找到其左子树的最右边的节点 直到right为NULL
{
while (p->right != NULL)
{
p = p->right;
}
return p;
}
//若左子节点为空则找从父节点、祖父节点中寻找其前驱节点
while (node->parent != NULL && node == node->parent->left)
{
node = node->parent;
}
//结束循环时node->parent == NULL 或者 node == node->parent->right
return node->parent;
}
6、求后继节点
后继节点的思路和前驱节点的思路一致,只需要把求前驱节点函数的左右子节点交换就可求出,我就不过多赘述了。
template <typename E>
Node<E>* BST<E>::Find_successor(Node<E>* node) //根据中序遍历寻找某个节点的后继节点
{
if (node == NULL)
{
return NULL;
}
Node<E>* p = node->right;
if (p != NULL) // 如果右子节点不为空则找到其左子树的最左边的节点 直到left为NULL
{
while (p->left != NULL)
{
p = p->left;
}
return p;
}
//若右子节点为空则从父节点、祖父节点中寻找其前驱节点
while (node->parent != NULL && node == node->parent->right)
{
node = node->parent;
}
//结束循环时node->parent == NULL 或者 node == node->parent->left
return node->parent;
}
7、删除元素
①度为0的节点——直接删除
⚪node == node.parent.left
√ node.parent.left=NULL
⚪node == node.parent.right
√ node.parent.right=NULL
⚪node.parent == NULL
√ root=NULL
②度为1的节点——用子节点代替原节点的的位置(child为子节点)
⚪node是左子节点
√ child.parent = node. parent
√ node.parent.left = child
⚪node是右子节点
√ child.parent = node. parent
√ node.parent.right= child
⚪node是根节点
√ root=child
√ child.parent = NULL
③度为2的节点
⚪先用前驱或者后继节点的值覆盖原节点的值
⚪然后删除相应的前驱或者后继节点
根据上文讲的前驱后继节点的性质我们可以得知如果一个节点的度为2,那么它的前驱、后继节点的度只能是1或0,接下来的问题就转换为删除度为1或0的节点
因为面向对象的删除函数是要想要删除一个元素,就要找到这个元素对应的节点,因此我们要写一个接口函数来找到这个元素对应的节点。
从根节点通过逐次比较来寻找元素,如果比较的值为0就是所需要的元素,否则就继续往下找直到为空,若没有找到就返回空。
template <typename E>
Node<E>* BST<E>::Find_Node(E element) //找到某个元素所对应的节点
{
Node<E>* node = root;
int cmp = 0;
while (node != NULL)
{
cmp = compare(element, node->element);
if (cmp == 0)
{
return node;
}
else if (cmp > 0)
{
node = node->right;
}
else
{
node = node->left;
}
}
return NULL;
}
找到这个元素对应的节点我们应该写一个接口函数来删除这个节点
template <typename E>
void BST<E>::Node_Remove(Node<E>* node) //删除某个节点
{
if (node == NULL)
{
return;
}
size--;
if (node->Is_Has_TwoChildren()) //如果有两个子节点即度为2
{
Node<E>* s = Find_successor(node); //找到其对应的后继节点
node->element = s->element; //用后继节点覆盖要删除的度为2的节点
node = s; //删除后继节点
}
//下方直接全部处理度为1和度为0的节点 即删除node(node的度必然为0或者是1)
Node<E>* replacement = node->left != NULL ? node->left : node->right;
if (replacement != NULL) //证明node为度为1的节点
{
replacement->parent = node->parent; //更改parent
//更改parent的left、right的指向
if (node->parent == NULL)
{
root = replacement;
}
else if (node=node->parent->right)
{
node->parent->right = replacement;
}
else if (node=node->parent->left)
{
node->parent->left = replacement;
}
}
else if (node->parent==NULL) //node为叶子节点并且是根节点
{
root = NULL;
}
else //node为叶子节点并且不是根节点
{
if (node == node->parent->right) //如果叶子节点为父节点的右节点
{
node->parent->right = NULL;
}
else 如果叶子节点为父节点的左节点
{
node->parent->left = NULL;
}
}
}
最后使用函数删除这个节点对应的元素
template <typename E>
void BST<E>::remove(E element) //删除元素
{
Node_Remove(Find_Node(element));
}
四、实现
首先是 BinarySearchiTree.h
的代码。
#pragma once
#include <iostream>
#include <queue>
using namespace std;
template<typename E>
class Node //创建节点
{
public:
E element;
Node<E> *left;
Node<E> *right;
Node<E> *parent;
Node(E element, Node<E>* parent) //构造函数
{
this->element = element;
this->parent = parent;
left = NULL;
right = NULL;
}
bool Check_Is_Leaf()//检查是否为叶子节点
{
return left == NULL && right == NULL;
}
bool Is_Has_TwoChildren() //是否拥有两个子节点
{
return left != NULL && right != NULL;
}
~Node() {} //析构函数
};
template<typename E >
class BST
{
private:
int size;
Node<E>* root;
int compare(E e1, E e2); //比较两个元素
void Element_Not_Null_Check(E element); //检查元素是否为空
void Node_Pre_Oreder_Traversal(Node<E>* node); //前序遍历的接口 访问顺序为:根节点->前序遍历左子树->前序遍历右子树
void Node_In_Oreder_Traversal(Node<E>* node); //中序遍历的接口 访问顺序为:中序遍历左子树->根节点->中序遍历右子树(这种出来的从小到大),若是先右子树则是从大到小
void Node_Post_Oreder_Traversal(Node<E>* node); //后序遍历的接口 访问顺序为:后续遍历左子树->后序遍历右子树->根节点
int node_recursion_calcu_height(Node<E>* node); //求高度的接口函数 二叉树的某个节点的高度(递归算法)
Node<E>* Find_predecessor(Node<E>* node); //根据中序遍历寻找某个节点的前驱节点
Node<E>* Find_successor (Node<E>* node); //根据中序遍历寻找某个节点的后继节点
Node<E>* Find_Node(E element); //找到某个元素所对应的节点
void Node_Remove(Node<E>* node); //删除某个节点
public:
int calcu_size(); //元素的数量
bool isEmpty(); //是否为空
void clear(); //清空所有元素
void add(E element); //添加元素
void remove(E element); //删除元素
bool contains(E element); //是否包含某元素
void Pre_Oreder_Traversal(); //前序遍历
void In_Oreder_Traversal(); //中序遍历
void Post_Oreder_Traversal(); //后续遍历
void Level_Oreder_Traversal(); //层续遍历
int recursion_calcu_height(); //求二叉树的高度(递归算法)
int iteration_calcu_height(); //求二叉树的高度(迭代算法)
bool Check_Is_Complete(); //检查是否为完全二叉树
~BST() {}
};
然后是整个 BinarySearchiTree.cpp
的实现代码
#include "BinarySearchiTree.h"
template <typename E>
int BST<E>::calcu_size() //元素的数量
{
return size;
}
template <typename E>
bool BST<E>::isEmpty() //是否为空
{
return size == 0;
}
template <typename E>
void BST<E>::clear() //清空所有元素
{
root = NULL;
size = 0;
}
template <typename E>
void BST<E>::add(E element) //添加元素
{
Element_Not_Null_Check(element); //防止添加元素为空
if (root == NULL) //添加第一个节点
{
root = new Node<E> (element, NULL);
size++;
}
//添加不是第一个节点
Node<E> *parent = NULL; //找到父节点
Node<E> *node = root;
int cmp = 0;
while (node != NULL)
{
cmp = compare(element, node->element); //保存比较的值来确定插入左侧还是右侧
parent = node; //保存父节点的位置
if (cmp > 0)
{
node = node->right;
}
else if (cmp < 0)
{
node = node->left;
}
else //相等覆盖
{
node->element = element;
return;
}
}
//看看插入到父节点的哪个位置
Node<E> *newnode = new Node<E>(element, parent);
if (cmp > 0)
{
parent->right = newnode;
}
else
{
parent->left = newnode;
}
size++;
}
template <typename E>
void BST<E>::remove(E element) //删除元素
{
Node_Remove(Find_Node(element));
}
template <typename E>
bool BST<E>::contains(E element) //是否包含某元素
{
return Find_Node(element) != NULL;
}
template <typename E>
void BST<E>::Pre_Oreder_Traversal() //前序遍历
{
Node_Pre_Oreder_Traversal(root); //从根节点开始遍历
}
template <typename E>
void BST<E>::In_Oreder_Traversal() //中序遍历
{
Node_In_Oreder_Traversal(root); //从根节点开始遍历
}
template <typename E>
void BST<E>::Post_Oreder_Traversal() //后序遍历
{
Node_Post_Oreder_Traversal(root); //从根节点开始遍历
}
template <typename E>
void BST<E>::Level_Oreder_Traversal() //层续遍历 访问顺序为:从上到下、从左到右一次访问每一个节点
{
if (root == NULL)
{
return;
}
queue<Node<E>*> que; //使用队列
que.push(root); //首先将根节点入队
while (!que.empty()) //循环操作直到队列为空
{
Node<E>* node = que.front(); //获得队头节点
que.pop(); //将对头结点出队
cout << "元素->" << node->element << " "; //输出队头节点元素
if (node->left != NULL) //将左子节点入队
{
que.push(node->left);
}
if (node->right != NULL) //将右子节点入队
{
que.push(node->right);
}
}
}
template <typename E>
int BST<E>::recursion_calcu_height() //求二叉树的高度(递归算法)
{
return node_calcu_height(root);
}
template <typename E>
int BST<E>::iteration_calcu_height() //求二叉树的高度(迭代算法) 利用层序遍历,有多少层高度就为几
{
if (root == NULL)
{
return 0;
}
int height = 0; //树的高度
int level_size = 1; //储存每一层的元素的数量
queue<Node<E>*> que; //使用队列
que.push(root); //首先将根节点入队
while (!que.empty()) //循环操作直到队列为空
{
Node<E>* node = que.front(); //获得队头节点
que.pop(); //将对头结点出队
level_size--;
if (node->left != NULL) //将左子节点入队
{
que.push(node->left);
}
if (node->right != NULL) //将右子节点入队
{
que.push(node->right);
}
if (level_size == 0) //意味着即将要访问下一层
{
level_size = que.size();
height++;
}
}
return height;
}
template <typename E>
bool BST<E>::Check_Is_Complete() //检查是否为完全二叉树
{
//方法一:
/*if (root == NULL) //如果树为空,返回false
{
return false;
}
queue<Node<E>*> que; //使用队列
que.push(root); //首先将根节点入队
bool leaf = false;
while (!que.empty()) //循环操作直到队列为空
{
Node<E>* node = que.front(); //获得队头节点
que.pop(); //将对头结点出队
if (leaf&&!node->Check_Is_Leaf()) //若要求为叶子节点但不是叶子节点则返回false
{
return false;
}
if (node->Is_Has_TwoChildren()) //如果左右都不为空即有子节点就按顺序入队
{
que.push(node->left);
que.push(node->right);
}
else if (node->left == NULL && node->right != NULL) //如果左边为空右边有子节点则不是完全二叉树,返回false
{
return false;
}
else //其它情况即为左节点为空,那么接下来其他节点都应该为为叶子节点,否则返回false
{
leaf = true;
if (node->left != NULL) //由于第二个判断语句是在左右都有子节点才入队,会出现左子节点依旧有子节点存在的bug情况,需要在此再增加一个判断语句修复bug
{
que.push(node->left);
}
}
}
return true; //若到最后每一个节点都符合则返回true
*/
//方法二: 比方法一减少了重复判断
if (root == NULL)
{
return false; //如果是空树,直接返回false
}
queue<Node<E>*> que; //使用队列
que.push(root); //首先将根节点入队
bool leaf = false;
while (!que.empty()) //循环操作直到队列为空
{
Node<E>* node = que.front(); //获得队头节点
que.pop(); //将对头结点出队
if (leaf && !node->Check_Is_Leaf()) //如果要求是叶子节点但是检查不是叶子节点,则返回错误
{
return false;
}
if (node->left != NULL) //若左子节点为空将左子节点入队
{
que.push(node->left);
}
else if (node->right != NULL) //如果左边为空但是右边不为空
{
return false;
}
if (node->right != NULL) //如果右边不为空将右子节点入队
{
que.push(node->right);
}
else //右边为空则要求接下来都是叶子节点
{
leaf = true;
}
}
}
/**
*return 返回值等于0,代表e1,e2相等,返回值大于0,代表e1大于e2,返回值小于0,代表e1小于e2,
*/
template <typename E>
int BST<E>::compare(E e1, E e2) //因为C++无法实现接口,因此无法对类类型的数据进行自定义比较,可利用Java和C#来实现
{
if (e1 - e2 > 0) //防止传入的数据类型既有整形也有浮点数类型
{
return ceil(e1 - e2); //利用ceil()和floor()函数来把比较的值改成整型返回
}
else if (e1 - e2 < 0)
{
return floor(e1 - e2);
}
else
{
return 0;
}
}
template <typename E>
void BST<E>::Element_Not_Null_Check(E element) //对传入元素是否为空进行检测
{
if (element == NULL)
{
throw "element must not be null !";
}
}
template <typename E>
void BST<E>::Node_Pre_Oreder_Traversal(Node<E>* node) //前序遍历的接口 访问顺序为:根节点->前序遍历左子树->前序遍历右子树
{
if (node == NULL)
{
return;
}
cout << "元素->" << node->element<<" ";
Node_Pre_Oreder_Traversal(node->left);
Node_Pre_Oreder_Traversal(node->right);
}
template <typename E>
void BST<E>::Node_In_Oreder_Traversal(Node<E>* node) //中序遍历的接口 访问顺序为:中序遍历左子树->根节点->中序遍历右子树(这种出来的从小到大),若是先右子树则是从大到小
{
if (node == NULL)
{
return;
}
Node_In_Oreder_Traversal(node->left);
cout << "元素->" << node->element << " ";
Node_In_Oreder_Traversal(node->right);
}
template <typename E>
void BST<E>::Node_Post_Oreder_Traversal(Node<E>* node) //后序遍历的接口 访问顺序为:后续遍历左子树->后序遍历右子树->根节点
{
if (node == NULL)
{
return;
}
Node_Post_Oreder_Traversal(node->left);
Node_Post_Oreder_Traversal(node->right);
cout << "元素->" << node->element << " ";
}
template <typename E>
int BST<E>::node_recursion_calcu_height(Node<E>* node) //求高度的接口函数 二叉树的某个节点的高度(递归算法)
{
if (node == NULL)
{
return 0;
}
return 1 + max(node_calcu_height(node->left), node_calcu_height(node->right));
}
template <typename E>
Node<E>* BST<E>::Find_predecessor(Node<E>* node) //根据中序遍历寻找某个节点的前驱节点
{
if (node == NULL)
{
return NULL;
}
Node<E>* p = node->left;
if (p != NULL) // 如果左子节点不为空则找到其左子树的最右边的节点 直到right为NULL
{
while (p->right != NULL)
{
p = p->right;
}
return p;
}
//若左子节点为空则找从父节点、祖父节点中寻找其前驱节点
while (node->parent != NULL && node == node->parent->left)
{
node = node->parent;
}
//结束循环时node->parent == NULL 或者 node == node->parent->right
return node->parent;
}
template <typename E>
Node<E>* BST<E>::Find_successor(Node<E>* node) //根据中序遍历寻找某个节点的后继节点
{
if (node == NULL)
{
return NULL;
}
Node<E>* p = node->right;
if (p != NULL) // 如果右子节点不为空则找到其左子树的最左边的节点 直到left为NULL
{
while (p->left != NULL)
{
p = p->left;
}
return p;
}
//若右子节点为空则从父节点、祖父节点中寻找其前驱节点
while (node->parent != NULL && node == node->parent->right)
{
node = node->parent;
}
//结束循环时node->parent == NULL 或者 node == node->parent->left
return node->parent;
}
template <typename E>
Node<E>* BST<E>::Find_Node(E element) //找到某个元素所对应的节点
{
Node<E>* node = root;
int cmp = 0;
while (node != NULL)
{
cmp = compare(element, node->element);
if (cmp == 0)
{
return node;
}
else if (cmp > 0)
{
node = node->right;
}
else
{
node = node->left;
}
}
return NULL;
}
template <typename E>
void BST<E>::Node_Remove(Node<E>* node) //删除某个节点
{
if (node == NULL)
{
return;
}
size--;
if (node->Is_Has_TwoChildren()) //如果有两个子节点即度为2
{
Node<E>* s = Find_successor(node); //找到其对应的后继节点
node->element = s->element; //用后继节点覆盖要删除的度为2的节点
node = s; //删除后继节点
}
//下方直接全部处理度为1和度为0的节点 即删除node(node的度必然为0或者是1)
Node<E>* replacement = node->left != NULL ? node->left : node->right;
if (replacement != NULL) //证明node为度为1的节点
{
replacement->parent = node->parent; //更改parent
//更改parent的left、right的指向
if (node->parent == NULL)
{
root = replacement;
}
else if (node=node->parent->right)
{
node->parent->right = replacement;
}
else if (node=node->parent->left)
{
node->parent->left = replacement;
}
}
else if (node->parent==NULL) //node为叶子节点并且是根节点
{
root = NULL;
}
else //node为叶子节点并且不是根节点
{
if (node == node->parent->right) //如果叶子节点为父节点的右节点
{
node->parent->right = NULL;
}
else 如果叶子节点为父节点的左节点
{
node->parent->left = NULL;
}
}
}
最后是 main.cpp
的检测代码
#include <iostream>
#include "BinarySearchiTree.h"
#include "BinarySearchTree.cpp"
using namespace std;
int main()
{
int date[] = { 7,4,9,2,5,8,11,1,3,10,12};
BST<int> *bst = new BST <int>();
for (int i = 0; i < 11; i++)
{
bst->add(date[i]);
}
cout<<"元素个数为:" << bst->calcu_size()<<endl;
cout<<"元素是否为空:" << bst->isEmpty()<<endl;
cout << "前序遍历:";
bst->Pre_Oreder_Traversal();
cout << endl;
cout << "中序遍历:";
bst->In_Oreder_Traversal();
cout << endl;
cout << "后序遍历:";
bst->Post_Oreder_Traversal();
cout << endl;
cout << "层序遍历:";
bst->Level_Oreder_Traversal();
cout << endl;
bst->remove(7);
cout << "删除后:" << endl;
bst->Level_Oreder_Traversal();
return 0;
}
代码检测效果
五、复杂度分析
如果是乱序添加节点(7、4、9、2、5、8、11):
若查找的是数字8,则从上到下进行两两比较,从头节点出发,8比7大,则向右侧查找,查找到的为9,发现8比9小,则向左查找,最后查找到8这个数字。
根据上文的分析,很容易发现查找某个元素的次数是与树的高度相关的,最好的情况是在第一层就找到,最坏的情况是在最后一层找到,最终复杂度分析为:O(h)=O(log n)
如果是从小到大添加节点(2、4 、5 、7、8、9)
附:
1.本博客是由学习小码哥视频所总结文章
2.代码都通过合格检测,请放心食用~