数据结构:二叉排序树(递归与非递归函数的全部实现)

引言

树是一个很重要的数据结构,它的作用不仅仅是在于存储文件路径方面。在存储其他数据方面,它可以把很多问题的时间复杂度降低,变成O(logn),但是如果仅仅是普通的建立树,是达不到这种效果的,因为数据和数据之间并没有任何联系,我们查找数据还是需要遍历所有数据,时间复杂度还是O(n),所以为了解决这个问题,我们推出了平衡二叉树,B树,红黑树等等,而这些数据结构我们在之后的文章中会单独讲解。

我们这一次要讲的是这所有数据结构的基础,二叉排序树。

这是一个有很大缺点的数据结构,但是这也是后面数据结构的基础,所以我会把递归和非递归的所有函数全部实现一遍,希望可以对大家有所帮助。

结构

二叉排序树的结构其实很简单,它的遍历选择的是中序遍历,所以对于数据来说,比根节点大的数据放在右边,比根节点小的数据放在左边。之后我们要查找数据就可以减少一半的时间,每次通过比较与结点的大小,可以完全锁定是在这个结点的左边还是在这个节点的右边。所以它的结构体很简单:

cpp 复制代码
struct BSTNode {
    int data;
    BSTNode* left;
    BSTNode* right;
};

代码

cpp 复制代码
// 创建新节点
BSTNode* CreateNode(int value) {
    BSTNode* s = new BSTNode();
    if(s == nullptr) {
        return nullptr;
    }
    s->data = value;
    s->left = s->right = nullptr;
    return s;
}

我们先实现一下查找函数,假设我们已经有了一棵二叉排序树

先是递归函数的实现,我们递归的出口放在了最前面,也就是处理根节点的操作

cpp 复制代码
// 在以root为根节点的树中找到value所在的结点,并返回(递归)
BSTNode* Find1(BSTNode* root, int value){
    if(root == nullptr || root->data == value) {
        return root;
    }
    if(value < root->data) {
        return Find1(root->left, value);
    } else {
        return Find1(root->right, value);
    }
}

然后是非递归的函数

这个函数就是移动指针,只要这个指针不是空,并且没找到这个数据,就通过比较与结点的大小,确定寻找的方向。即使没有找到,那么最后p的值也是nullptr,返回的也正是nullptr

cpp 复制代码
// 非递归(效率其实是一样的)
BSTNode* Find2(BSTNode* root, int value){
    BSTNode* p = root;
    while(p != nullptr && p->data != value) {
        if(value < p->data) {
            p = p->left;
        } else {
            p = p->right;
        }
    }
    return p;
}

然后是插入函数(递归)

对于二叉排序树(不是平衡二叉树),其实插入函数我们没必要想太多,大家可以自己画一个图,我们可以保证每一个插入的数据都是在叶子结点上,也就是说我们根本没有必要对于节点的移动,我们的操作就是根据二叉排序树的性质,一路找,直到找到nullptr,然后创建这个要插入的结点,然后return,这个时候根据递归函数的性质,我们就会不断的返回这个带有这个新结点的新树(可能大家会对这个过程感到疑惑,如果大家对于底层有一定的了解的话,一定知道函数是在栈中的,而栈的特点大家也都知道,所以为什么会有这种回溯的机制,也不必多说了吧~)

cpp 复制代码
// 在以root为根的树中插入数据value(递归)
BSTNode* Insert1(BSTNode* root, int value) {
    if(root == nullptr) {
        return CreateNode(value);
    } else {
        if(value < root->data) {
            root->left = Insert1(root->left, value);
        } else {
            root->right = Insert1(root->right, value);
        }
    }
    return root;
}

对于非递归实现的插入操作,我们就需要双指针了,因为我们需要知道插入结点的父亲是谁,这个过程需要我们手动自己实现,而不是靠程序内部的机制来实现。思路大概都差不多,只是最后一步的时候,p为空的时候,pre就是p要插入的父节点,这个时候比较一下value和pre->data的大小,来确定value应该插入到这个结点的左边还是右边。

cpp 复制代码
// 在以root为根的树中插入数据value(非递归)
/*
双指针,先查找,因为插入的数据肯定不在树中,所以查找结束时,pre指向的是插入位置的父一个结点,p指向的是插入位置
*/
BSTNode* Insert2(BSTNode* root, int value) {
    // 特殊情况,树为空
    if(root == nullptr) {
        return CreateNode(value);
    }
    BSTNode* p = root;
    BSTNode* pre = nullptr; // 查找过程中记录p的父亲
    while(p != nullptr) {
        pre = p;
        if(value < p->data) {
            p = p->left;
        } else {
            p = p->right;
        }
    }

    // 循环结束时,p为空,pre刚好指向x的父亲,判断插入的结点是在左边还是在右边
    if(value < pre->data) {
        pre->left = CreateNode(value);
    } else {
        pre->right = CreateNode(value);
    }
    return root;
}

然后就是我们这一部分最重要的操作了:删除!!!!

为了方便大家理解,我们选择先写递归的方法

思路如下:

我们要删除的结点有三种情况:

0个孩子 1个孩子 2个孩子

第一种情况:我们的操作十分easy,拿到一个指针指向这个结点,然后delete,然后把指针指向nullptr,是不是很easy~~

第二种情况:这个就稍微复杂那么一点点,因为有一个孩子,所以我们需要在删除这个结点之前,把孩子交给这个结点的父亲,所以我们需要判断一下这个孩子是左孩子还是右孩子。

所以,我们需要两个指针,一个指针pre指向父亲,一个指针指向删除的结点p,最后把pre指向p的指针指向p的孩子。

说到这里,想必聪明的各位已经发现,其实情况二和情况一可以合并,因为情况一可以看成有一个孩子,但是这个孩子是nullptr罢了,嘿嘿嘿~

第三种情况:这个就比较复杂了,它就是它,到底是连左边还是右边呢?

因为我们是中序遍历,所以我们有一个很大的优势,就是一个结点的前驱和后继很好找。至于怎么找,我们在写文章-CSDN创作中心里面已经介绍了。找前驱和找后继的意义是什么呢?

1 2 3 4 5 6 7 8 9 如果按中序遍历输出一个二叉排序树,就是这个样子。所以当我们删除其中一个结点,按照数组来理解就是要么这个数的前面全部往后面移动一个,要么这个数的后面全部往前面移动一个。所以我们只需要找到这个数的前驱或者后继,然后把这个前驱或者后继的数据直接覆盖在我们要删除的数据上(这里不要移动指针,因为这样会把问题变得复杂),然后把这个前驱或者后继删除就可以。

这个其实就像把要删除的数据改成了它的前驱或后继,然后把前驱或后继删除。

那么我们又可以想一下,一个数可以作为别人的前驱和后继,那么这个数必然只有一个孩子或者没有孩子。呦呦呦~~

那么问题不又转化成了前面的情况了嘛~~所以代码也就出来了

先用双指针找到要删除的结点,然后判断一下这个结点的度,如果只有0或1个孩子,好说,直接利用pre和child指针,改变结构,然后删除p,如果有两个孩子,找到前驱,然后把前驱标记成p,这样问题直接转为上一个情况,pre标记为这个结点的父亲,就不用单独再写一个函数了

cpp 复制代码
/*
删除结点(非递归)
p的度是0或者1,都看成1,让唯一的孩子继承p的一切关系
声明一个孩子指针,如果p的左孩子是非空就指向左,否则就指向右孩子
p的度是2,让前去或者后继结点t继承p的一切关系
先找到t,然后删除t,而删除t的问题就变成了删除度为0或者1的结点
*/
BSTNode* Delete1(BSTNode* root, int value) {
    if(root == nullptr) {
        return nullptr;
    }
    // 先找到value所在的结点p,以及p的父亲pre
    BSTNode* p = root;
    BSTNode* pre = nullptr;
    BSTNode* child = nullptr;
    while(p != nullptr &&p->data != value) {
        pre = p;
        if(value < p->data) {
            p = p->left;
        } else {
            p = p->right;
        }
    }
    if(p == nullptr) {
        std::cout << "删除失败,结点不存在" << std::endl;
        return root;
    }
    // 如果p的度是2,进行一个转化
    if(p->left != nullptr && p->right != nullptr) {
        // 找到p的前驱结点t
        BSTNode* t = p->left;
        BSTNode* tf = p;
        while(t->right != nullptr){
            tf = t;
            t = t->right;
        }
        p->data = t->data; // 把前驱节点复制过来

        // 问题转化为删除t,仍然用p指向被删除的结点,pre指向被删除的结点的父亲
        p = t;
        pre = tf;
    }

    // 此时被删除的结点p的度一定是1或者0
    if(p->left != nullptr) {
        child = p->left;
    } else {
        child = p->right;
    }
    if(pre->left == p) {
        pre->left = child;
    } else {
        pre->right = child;
    }
    delete p;
    p = nullptr;
    return root;
}

递归

递归的精髓一直都是把最后的结果不断地向上传递,所以我们可以理解成我们不断地去删除一颗树,这颗树越来越小,直到最后找到了这个结点root,然后对root进行情况1和2的判断,把情况2转化为情况1,也就是else分支进行处理

cpp 复制代码
// 删除结点(递归)
BSTNode* Delete2(BSTNode* root, int value) {
    if(root == nullptr) {
        std::cout << "删除失败,树为空" << std::endl;
        return nullptr;
    }
    if(value < root->data){
        root->left = Delete2(root->left, value);
    } else if(value > root->data) {
        root->right = Delete2(root->right, value);
    } else {
        // 此时root就是我们要删除的结点
        if(root->left != nullptr && root->right != nullptr) {
            // 找到root的后继结点
            BSTNode* t = root->right;
            while(t->left != nullptr) {
                t = t->left;
            }
            root->data = t->data; // 把后继节点复制过来
            root->right = Delete2(root->right, t->data);
        } else {
            // 如果root的度是0或者1,直接删除
            BSTNode* p = root;
            if(root->left != nullptr) {
                root = root->left;
            } else {
                root = root->right;
            }
            delete p;
            p = nullptr;
        }
    }
    return root;
}

总结

二叉排序树其实并不完美,因为如果一颗树只有左子树,也就是有n个结点,高度也是n,那么时间复杂度并没有减少,所以才有了后面的平衡二叉树,把这个树变得又矮又胖(当然现实里面我们还是比较希望自己。。。嗯)。但是作为基石,我们很有必要掌握它。

感谢大家阅读!!!希望这篇文章可以帮助到大家理解平衡二叉树~~~~

相关推荐
兰令水1 小时前
leecodecode【二叉树排序+最近公共祖先】【2026.6.2打卡-java版本】
java·数据结构·算法·leetcode
£suPerpanda1 小时前
AtCoder Beginner Contest 453
c++·算法
郝学胜-神的一滴1 小时前
Qt 高级开发 022:栅格布局深度实战
开发语言·c++·qt·软件构建·用户界面
basketball6161 小时前
设计模式入门:3. 装饰器模式详解 C++实现
c++·设计模式·装饰器模式
程序大视界2 小时前
【C++ 从基础到项目实战】C++(三):函数进阶——重载、回调、递归与默认参数
开发语言·c++·cpp
西梅汁2 小时前
C++ 线程间通信(二)
c++
minji...2 小时前
Linux 高级IO(七)多进程、多线程的Reactor反应堆模式扩展、OTOL
linux·运维·c++·多路转接·epoll·reactor反应堆模型
晚风吹红霞2 小时前
C++ list 容器完全指南:从入门到手撕双向链表
c++·链表·list
handler012 小时前
【Linux 网络】:poll/epoll 底层机制与 Reactor 并发模型
linux·运维·服务器·网络·c++·多路转接·多路复用