【数据结构】二叉搜索树(Binary Search Tree) 基本操作和问题

目录

前言

一、查找最小值和最大值

二、二叉树的高度

三、二叉树的遍历--广度优先vs深度优先

二叉树的广度优先遍历:

二叉树的深度优先遍历:

四、判断是否为二叉搜索树(BST)

递归遍历方法:

中序遍历方法:

五、二叉树中删除一个结点

六、二叉树的中序后继节点

结语


 

前言

上一篇我们介绍了什么是二叉搜索树和如何实现它,这篇将会讲解二叉树的基本操作并解决一些有趣的问题。


一、查找最小值和最大值

我们已经知道对于二叉搜索树来说左子树的节点值较小,右子树的节点值较大,所以查找最小值和最大值只需要找到最左边的树叶和最右边树叶,可以通过迭代和递归两种方法,下面以最小值为例

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)数据结构来辅助。具体步骤如下:

  1. 将根节点入队。

  2. 当队列不为空时,重复以下步骤:

    • 从队列中取出一个节点。

    • 访问该节点。

    • 如果该节点有左子节点,将左子节点入队。

    • 如果该节点有右子节点,将右子节点入队。

通过这种方式,可以确保每一层的节点都按照从左到右的顺序被访问。

以下是代码实例:

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;
    }
}

结语

本篇主要介绍了二叉搜索树的一些基本操作和一些有趣的问题,希望能对初学者有所帮助,如有问题请大家帮我指正,十分感谢。后续将会介绍更多有关二叉树的问题。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值