常见数据结构-二叉树(下)二叉查找树

简介: 常见数据结构-二叉树(下)二叉查找树

一,什么是二叉查找树

二叉查找树(Binary Search Tree)是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。

二叉查找树最大的特点就是,支持动态数据集合的快速插入、删除、查找操作。

二叉查找树的定义是,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。结构如下图所示:

网络异常,图片无法展示
|


1.1,二叉查找树的查找操作

查找的数据等于根节点则返回;小于根节点则在左子树中递归查找;大于根节点则在右子树中递归查找。

二叉树的根节点定义如下:

// Definition for a binary tree node->
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
复制代码

二叉搜索树中的搜索的 C++ 代码如下:

class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        if(root == NULL || root->val==val) return root;
        if(root->val < val) return searchBST(root->right, val);
        if(root->val > val) return searchBST(root->left, val);
        return NULL;
    }
};
复制代码

1.2,二叉查找树的插入操作

二叉查找树的插入过程有点类似查找操作。新插入的数据一般都是在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小关系。

class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (root == nullptr){
            return new TreeNode(val);
        }
        TreeNode* pos = root;
        while(pos!=nullptr){
            if(val < pos->val){
                if(pos->left == nullptr){
                    pos->left = new TreeNode(val);
                    return;
                }
                else{
                    pos = pos->left;
                }
            }
            else{
                if(pos->right == nullptr){
                    pos->right = new TreeNode(val);
                    return;
                }
                else{
                    pos = pos->right;
                }               
            }
        }
        return root;
    }
};
复制代码

1.2,二叉查找树的删除操作

针对要删除节点的子节点个数的不同,有以下五种情况:

  • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  • 找到删除的节点
  • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回 NULL 为根节点
  • 第三种情况:要删除的节点不是叶子节点,且没有右节点但是有左节点。使用它的前驱节点值进行替代,然后再递归的向下删除前驱节点。
  • 第四种情况:要删除的节点不是叶子节点且拥有右节点,则该节点可以由该节点的后继节点值进行替代,然后可以从后继节点的位置递归向下操作以删除后继节点。
  • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
  • Successor 代表的是中序遍历序列的下一个节点。即比当前节点大的最小节点,简称后继节点。 先取当前节点的右节点,然后一直取该节点的左节点,直到左节点为空,则最后指向的节点为后继节点 (右子树的最小节点)
  • Predecessor 代表的是中序遍历序列的前一个节点。即比当前节点小的最大节点,简称前驱节点。先取当前节点的左节点,然后取该节点的右节点,直到右节点为空,则最后指向的节点为前驱节点 (左子树的最大节点)

1,递归代码如下

// 对应 leetcode450,leetcode运行结果有问题!
class Solution {
public:
    // 返回节点的后继节点值
    int successor(TreeNode* root){
        root = root->right;
        while(root->left != nullptr) root = root->left;
        return root->val;
    }
    // 返回节点的前驱节点值
    int predecessor(TreeNode* root){
        root = root->left;
        while(root->right != nullptr) root = root->right;
        return root->val;
    }
    TreeNode* deleteNode(TreeNode* root, int key){
        if (root == nullptr) return nullptr;
        if(key > root->val) root->right = deleteNode(root->right, key);
        else if(key < root->val ) root->left = deleteNode(root->left, key);
        else{  // 找到了待删除节点, delete the current node
            // 左右子树都没有节点
            if(root->left == nullptr && root->right == nullptr) root = nullptr;
            // 只有左子树有节点
            else if(root->left != nullptr ){
                root->val = predecessor(root);
                root->left = deleteNode(root->left, root->val);
            }
            // 只有右子树有节点或者左右子树都有节点
            else{
                root->val = successor(root);
                root->right = deleteNode(root->right, root->val);
            }
        }
        return root;
    }
};
复制代码

2,非递归代码如下

// 对应 leetcode450,leetcode运行结果有问题!
class Solution{
public:
    TreeNode* deleteNode(TreeNode* root, int key){
        TreeNode* p = root; // 指向要删除的节点,初始化为根节点
        TreeNode* pp = nullptr; // pp 指向 p 的父节点
        // 根据 key 值查找待删除的节点及其父节点
        while(pp != nullptr && p->val != key){
            pp = p;
            if(key > p->val) p = p->right;
            else p = p->left;
        }
        if (pp == nullptr) return nullptr;  // 没有找到待删除节点
        // 1, 要删除的节点有两个子节点
        if (p->left != nullptr && p->right != nullptr){ // 查找右子树的最小节点
            TreeNode* minP = p->right;
            TreeNode* minPP = p;  // minPP 是 minP 的父节点
            while(minP->left != nullptr){
                minPP = minP;
                minP = minP->left;
            }
            // 右子树最小节点的值替换到待删除节点上,然后只要删除 minP 节点(无子节点或者只有一个右子节点 )即可
            p->val = minP->val;
            p = minP;
            pp = minPP;  
        }
        TreeNode* child; // p 的字节点
        // 2,要删除的节点有一个子节点或者没有子节点
        if (p->left != nullptr ) child = p->left;  // 有左节点
        else if(p->right != nullptr) child = p->right; // 有右节点
        else child = nullptr;  // 没有子节点
        // 真正的删除节点操作
        if(pp == nullptr) root = child; // 删除的是根节点
        else if(pp->left == p) pp->left = child;
        else pp->right = child;
        return root;
    }
};
复制代码

二叉查找树除了查找、插入、删除操作,还支持快速地查找最大节点和最小节点、前驱节点和后继节点操作。同时,二叉查找树的中序遍历输出的是有序数据序列,时间复杂度为 O(n)O(n)O(n)

二,支持重复数据的二叉查找树

和散列表一样,二叉查找树节点中也可以存储包含很多字段的对象,针对节点存在重复数据的情况,有两种方法:

  • 通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。
  • 每个节点仍然只存储一个数据。在查找插入位置的过程中,如果碰到一个节点的值,与要插入数据的值相同,我们就将这个要插入的数据放到这个节点的右子树,也就是说,把这个新插入的数据当作大于这个节点的值来处理。

三,二叉查找树的时间复杂度分析

二叉查找树的形态各式各样,查找、插入、删除操作的时间复杂度也各不一样,如下图第一种二叉查找树的根节点左右子树极度不平衡,查找的时间复杂度变成了 O(n)O(n)O(n)

网络异常,图片无法展示
|


总结

在二叉查找树中,查找、插入、删除等很多操作的时间复杂度都跟树的高度成正比。两个极端情况的时间复杂度分别是 O(n)O(n)O(n)O(logn)O(logn)O(logn),分别对应二叉树退化成链表的情况和完全二叉树。为了避免时间复杂度的退化,针对二叉查找树,又设计了一种更加复杂的树-平衡二叉查找树,时间复杂度可以做到稳定的 O(logn)O(logn)O(logn)

参考资料

《数据结构与算法之美》-二叉树基础(下)


相关文章
|
2天前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
31 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
2天前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
27 12
|
2天前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
27 10
|
2天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
16 2
|
16天前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
78 5
|
2月前
|
机器学习/深度学习 存储 算法
数据结构实验之二叉树实验基础
本实验旨在掌握二叉树的基本特性和遍历算法,包括先序、中序、后序的递归与非递归遍历方法。通过编程实践,加深对二叉树结构的理解,学习如何计算二叉树的深度、叶子节点数等属性。实验内容涉及创建二叉树、实现各种遍历算法及求解特定节点数量。
107 4
|
2月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
262 9
|
2月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
42 1
|
2天前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
112 75