目录
前言
上一篇我们介绍了什么是二叉搜索树和如何实现它,这篇将会讲解二叉树的基本操作并解决一些有趣的问题。
一、查找最小值和最大值
我们已经知道对于二叉搜索树来说左子树的节点值较小,右子树的节点值较大,所以查找最小值和最大值只需要找到最左边的树叶和最右边树叶,可以通过迭代和递归两种方法,下面以最小值为例
struct BstNode{
int data;
struct BstNode* right;
struct BstNode* left;
};
//迭代的方法找最小值
int FindMin(struct BstNode* root)
{
if(root == NULL)//如果树是空的
{
printf("Error: Tree is empty\n");
return -1;
}
while(root->left != NULL)
{
root = root->left;
}
return root->data;
}
//递归的方法找最小值
int FindMin(struct BstNode* root)
{
if(root == NULL)//如果树是空的
{
printf("Error: Tree is empty\n");
return -1;
}
else if(root->left == NULL)
{
return root->data;
}
return FindMin(root->left);
}
二、二叉树的高度
首先我们回顾一下树的高度和深度的定义,很多人会把这两个概念搞混。
一个节点的高度的定义是:从该节点到叶子节点的最长边数。
树的高度的定义是:从根节点到叶子节点的最长边数。
一个节点的深度的定义是:从根节点到该节点的边数。
基本上,高度是到最深的叶子节点的距离,而深度是到根节点的距离。
好的,接下来我们来看如何计算树的高度,或者说二叉树的最大深度。
struct Node*{
int data;
struct Node* left;
struct Node* right;
};
//用递归计算树的高度
int FindHeight(struct Node* root)
{
if(root == NULL)
return -1;
return max(FindHeight(root->left),FindHeight(root->right)) +1;//根节点的高度是左子树和右子树中较大者 +1
}
三、二叉树的遍历--广度优先vs深度优先
二叉树的遍历方式主要有两种:广度优先遍历(Breadth-First Traversal)和深度优先遍历(Depth-First Traversal),不同的遍历提供了对结点依次处理的不同方式,可以在遍历过程中对结点进行各种处理。下面我将详细介绍这两种遍历方式。
二叉树的广度优先遍历:
二叉树的广度优先遍历通常叫层次遍历(Level Order Traversal)。层次遍历的规则是若树为空,则空操作返回,否则从树的第一层,也就是根节点开始访问,从上而下逐层遍历,在同一层中,按从左至右的顺序对结点逐个访问。
层次遍历的实现通常使用队列(Queue)数据结构来辅助。具体步骤如下:
-
将根节点入队。
-
当队列不为空时,重复以下步骤:
-
从队列中取出一个节点。
-
访问该节点。
-
如果该节点有左子节点,将左子节点入队。
-
如果该节点有右子节点,将右子节点入队。
-
通过这种方式,可以确保每一层的节点都按照从左到右的顺序被访问。
以下是代码实例:
void LevelOrder (Node *root){
if(root == NULL) return;
queue<Node*> Q;
Q.push(root);
//当至少有一个已发现的结点时
while(!Q.empty()){
Node* current = Q.front();
count<<current->data<<" ";
if(current->left != NULL) Q.push(current->left);//如果该节点有左子节点,将左子节点入队
if(current->right !=NULL) Q.push(current->right);//如果该节点有右子节点,将右子节点入队
Q.pop();//移除队首元素
}
}
时间复杂度是O(n),其中
n
是二叉树中节点的总数。这是因为每个节点都会被访问一次,并且每个节点都会被入队和出队一次。空间复杂度是O(n),层次遍历的空间复杂度主要取决于队列中存储的节点数,在最坏情况下,队列中最多会存储一层的节点数。对于一个平衡二叉树,最底层可能包含大约
n/2
个节点(其中n
是总节点数)。
二叉树的深度优先遍历:
二叉树的深度优先遍历(Depth-First Traversal)有三种常见的方式:前序遍历(Pre-order Traversal)、中序遍历(In-order Traversal)和后序遍历(Post-order Traversal)。每种遍历方式都有其特定的访问顺序。
1.前序遍历:
前序遍历的顺序是:根节点 -> 左子树 -> 右子树
二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归,而且极其简洁明了。代码如下:
//二叉树的前序遍历递归算法
void Preorder (Node* root)
{
if(root == NULL) return;
printf("%c ",root->left);//显示结点数据,可以更改为其他对结点操作
Preorder(root->left);//再先序遍历左子树
Preorder(root->right);//最后先序遍历右子树
}
2.中序遍历:
中序遍历的顺序是:左子树->根节点->右子树
//二叉树的中序遍历递归算法
void Inorder (Node *root)
{
if(root == NULL) return;
Inorder (root->left);//中序遍历左子树
printf("%c ",root->data);//显示结点数据,可以更改为对其他结点操作
Inorder (root->right);//最后中序遍历右子树
}
3.后序遍历:
后序遍历的顺序是:左子树->右子树->根节点
//二叉树的后序遍历递归算法
void Postorder (Node* root)
{
if(root == NULL) return;
Postorder(root->left);//先后序遍历左子树
Postorder(root->right);//再后序遍历右子树
printf("%c ",root->data);//显示结点数据,可以更改为其他对结点操作
}
前序遍历、中序遍历和后序遍历的时间复杂度和空间复杂度是相同的,因为它们都是基于递归或栈的深度优先搜索(DFS)方法。
时间复杂度是O(n),每个节点都被访问一次,因此时间复杂度是线性的,即O(n),其中n是树中节点的总数。
空间复杂度是O(h),其中h是树的高度。在最坏情况下,空间复杂度为O(n);在平均情况下,空间复杂度为O(log n)。
四、判断是否为二叉搜索树(BST)
递归遍历方法:
我们知道二叉搜索树满足左子树上所有结点的值都小于它的根节点,右子树上所有结点的值都大于它的根节点,我们可以利用这一性质判断是否为二叉搜索树。这个思路很容易想到但是效率不是很高,它的时间复杂度为O(n^2)。
我们可以将这个算法进行优化,为每一个结点定义一个允许的范围,这个结点的数据必须在这个范围内,我们可以写出如下的代码。该方法的时间复杂度为O(n).
//这是一个辅助函数,用于递归地检查二叉树是否满足二叉搜索树的性质
bool IsBstUtil(Node *root, int minValue, int maxValue)
{
if(root == NULL) return true;
if(root->data > minValue && root->data < maxValue //检查当前结点的值是否在最大值和最小值之间
&& IsBstUtil(root->left, minValue, root->data)//递归地检查左子树的值是否在最小值和根节点范围内
&& IsBstUtil(root->right, root->data, maxValue))//递归地检查右子树的值是否在根节点和最大值范围内
return true;
else
return false;
}
//这是主函数,用于判断整个二叉树是否是二叉搜索树
bool IsBinarySearchTree(Node* root)//只需传入根节点
{
return IsBstUtil(root,INT_MIN,INT_MAX);
}
中序遍历方法:
对二叉搜索树进行中序遍历可以得到一个递增序列,我们可以通过这个性质判断是否为二叉搜索树,我们用递归地方法实现这个功能。
代码如下:
//利用中序遍历(递归)进行搜索二叉树的判断
int preValue=-1;
bool checkBST(TreeNode* head){
if(root==nullptr){//若为空树则返回true
return true;
}
bool isLeftBst= checkBST(root->left);//递归调用判断左子树是不是搜索二叉树
if(!isLeftBst){
return false;//左子树不是就返回false
}
if(head->val<=preValue){ //如果值降序,说明不是二叉搜索树
return false;
}
else{
preValue=root->val; //更新为当前结点的值
}
return checkBST(root->right); //判断右子树是不是二叉搜索树
}
五、二叉树中删除一个结点
在二叉树中删除一个结点,可以分为一下三种情况:
情况一:没有孩子结点
情况二:只有一个孩子结点
情况三:有两个孩子结点
前两种情况我们可以当成链表去解决,移除对该结点的引用,并释放该结点。第三种情况较为复杂,我们可以将它降为第一或第二种情况,找到右子树中的最小值,将该值复制到要删除的结点中,把最小值原来所在的结点删除;或者找到左子树的最大值,将该值填入要删除的结点中,删除最大值原来所在的结点。
代码如下:
//从二叉搜索树中删除一个结点
#include<iostream>
using namespace std;
struct Node {
int data;
struct Node *left;
struct Node *right;
};
struct Node* Delete(struct Node *root, int data)
{
if(root == NULL) return root;
else if(data < root->data) root->left = Delete(root->left,data);//如果要删除的值小于当前结点的值,则递归调用函数在左子树中删除该值
else if(data > root->data) root->right = Delete(root->right,data);//如果要删除的值大于当前结点的值,则递归调用函数在右子树中删除该值
//要删除的值等于当前结点的值
else
{ //情况1:没有孩子
if(root->left == NULL && root->right == NULL)
{
delete root;
root = NULL;//防止野指针
}
//情况2:只有一个孩子
else if(root->left == NULL)//左孩为空
{
struct Node *temp = root;
root = root->right;
delete temp;
}
else if(root->right == NULL)//右孩为空
{
struct Node *temp = root;
root = root->right;
delete temp;
}
//情况3:有两个孩子
else
{
struct Node *temp = FindMin(root->right);//找到右子树的最小值
root->data = temp->data;//将该值复制到要删除的结点中
root->right = Delete(root->right,temp->data);//删除最小值原来所在结点
}
}
return root;
}
六、二叉树的中序后继节点
在二叉搜索树中还有一个比较有趣的问题是,给定一棵树中的某个值,要找到它的中序后继结点,就是这颗树的下一个比它大的值。
查找中序后继结点的方法:
如果结点有右子树:
- 中序后继结点是右子树中的最小值结点。
- 具体做法是:从右子结点开始,一直向左遍历,直到没有左子结点为止。
如果结点没有右子树:
- 中序后继结点是第一个比该结点大的祖先结点。
- 具体做法是:从当前结点向上遍历,直到找到一个结点是其父结点的左子结点为止。这个父结点就是中序后继结点。
代码如下:
//在二叉搜索树中查找最小值
struct Node* FinfMin(struct Node* root)
{
if(root == NULL)return NULL;
while(root->left != NULL)
root = root->left;
return root;
}
//查找二叉树的中序后继结点
struct Node* Getsuccessor(struct Node* root,int data)
{
//查找结点 - O(h)
struct Node* current = Find(root,data);
if(current == NULL) return NULL;
//情况一:结点有右子树
if(current->right != NULL)
{
return FindMin(current->right);// O(h)
}
//情况二:结点没有右子树 - O(h)
else
{
struct Node* successor = NULL;
struct Node* ancestor = root;
while(ancestor != current)
{
if(current->data < ancestor->data)//该结点的值小于其父结点的值,向左遍历
{
successor = ancestor;//直到该结点是其父结点的最深的左子结点
ancestor = ancestor->left;
}
else
ancestor = ancestor->right;//该结点的值大于其父结点的值,向右遍历
}
return successor;
}
}
结语
本篇主要介绍了二叉搜索树的一些基本操作和一些有趣的问题,希望能对初学者有所帮助,如有问题请大家帮我指正,十分感谢。后续将会介绍更多有关二叉树的问题。