一步一步写二叉查找树

二叉查找树(BST)是二叉树的一个重要的应用,它在二叉树的基础上加上了这样的一个性质:对于树中的每一个节点来说,如果有左儿子的话,它的左儿子的值一定小于它本身的值,如果有右儿子的话,它的右儿子的值一定大于它本身的值。

二叉查找树的操作一般有插入、删除和查找,这几个操作的平均时间复杂度都为O(logn),插入和查找操作很简单,删除操作会复杂一点,除此之外,因为二叉树的中序遍历是一个有序序列,我就额外加上了一个中序遍历操作。

二叉查找树的应用不是很多,因为它最坏的时候跟线性表差不多,大部分会应用到它的升级版,平衡二叉树和红黑树,这两棵树都能把时间复杂度稳定在O(logn)左右。虽然不会用到,但是二叉查找树是一定要学好的,毕竟它是平衡二叉树和红黑树的基础。

接下来一步一步写一个二叉查找树模板。

完整代码:

//二叉查找树节点信息
template<class T>
class TreeNode
{
    public:
        TreeNode():lson(NULL),rson(NULL),freq(1){}
        T data;//值
        unsigned int freq;//频率
        TreeNode* lson;//指向左儿子的坐标
        TreeNode* rson;//指向右儿子的坐标
};
//二叉查找树类的属性和方法声明
template<class T>
class BST
{
    private:
        TreeNode<T>* root;//根节点
        void insertpri(TreeNode<T>* &node,T x);//插入
        TreeNode<T>* findpri(TreeNode<T>* node,T x);//查找
        void insubtree(TreeNode<T>* node);//中序遍历
        void Deletepri(TreeNode<T>* &node,T x);//删除
    public:
        BST():root(NULL){}
        void insert(T x);//插入接口
        TreeNode<T>* find(T x);//查找接口
        void Delete(T x);//删除接口
        void traversal();//遍历接口

};
//插入
template<class T>
void BST<T>::insertpri(TreeNode<T>* &node,T x)
{
    if(node==NULL)//如果节点为空,就在此节点处加入x信息
    {
        node=new TreeNode<T>();
        node->data=x;
        return;
    }
    if(node->data>x)//如果x小于节点的值,就继续在节点的左子树中插入x
    {
        insertpri(node->lson,x);
    }
    else if(node->data<x)//如果x大于节点的值,就继续在节点的右子树中插入x
    {
        insertpri(node->rson,x);
    }
    else ++(node->freq);//如果相等,就把频率加1
}
//插入接口
template<class T>
void BST<T>::insert(T x)
{
    insertpri(root,x);
}
//查找
template<class T>
TreeNode<T>* BST<T>::findpri(TreeNode<T>* node,T x)
{
    if(node==NULL)//如果节点为空说明没找到,返回NULL
    {
        return NULL;
    }
    if(node->data>x)//如果x小于节点的值,就继续在节点的左子树中查找x
    {
        return findpri(node->lson,x);
    }
    else if(node->data<x)//如果x大于节点的值,就继续在节点的左子树中查找x
    {
        return findpri(node->rson,x);
    }
    else return node;//如果相等,就找到了此节点
}
//查找接口
template<class T>
TreeNode<T>* BST<T>::find(T x)
{
    return findpri(root,x);
}
//删除
template<class T>
void BST<T>::Deletepri(TreeNode<T>* &node,T x)
{
    if(node==NULL) return ;//没有找到值是x的节点
    if(x < node->data)
    Deletepri(node->lson,x);//如果x小于节点的值,就继续在节点的左子树中删除x
    else if(x > node->data)
    Deletepri(node->rson,x);//如果x大于节点的值,就继续在节点的右子树中删除x
    else//如果相等,此节点就是要删除的节点
    {
        if(node->lson&&node->rson)//此节点有两个儿子
        {
            TreeNode<T>* temp=node->rson;//temp指向节点的右儿子
            while(temp->lson!=NULL) temp=temp->lson;//找到右子树中值最小的节点
            //把右子树中最小节点的值赋值给本节点
            node->data=temp->data;
            node->freq=temp->freq;
            Deletepri(node->rson,temp->data);//删除右子树中最小值的节点
        }
        else//此节点有1个或0个儿子
        {
            TreeNode<T>* temp=node;
            if(node->lson==NULL)//有右儿子或者没有儿子
            node=node->rson;
            else if(node->rson==NULL)//有左儿子
            node=node->lson;
            delete(temp);
        }
    }
    return;
}
//删除接口
template<class T>
void BST<T>::Delete(T x)
{
    Deletepri(root,x);
}
//中序遍历函数
template<class T>
void BST<T>::insubtree(TreeNode<T>* node)
{
    if(node==NULL) return;
    insubtree(node->lson);//先遍历左子树
    cout<<node->data<<" ";//输出根节点
    insubtree(node->rson);//再遍历右子树
}
//中序遍历接口
template<class T>
void BST<T>::traversal()
{
    insubtree(root);
}

第一步:节点信息

二叉查找树的节点和二叉树的节点大部分是一样的,不同的是,二叉查找树多了一个值出现的次数。如图1显示了二叉查找树的节点信息。

代码如下:

//二叉查找树节点信息
template<class T>
class TreeNode
{
    public:
        TreeNode():lson(NULL),rson(NULL),freq(1){}//初始化
        T data;//值
        unsigned int freq;//频率
        TreeNode* lson;//指向左儿子的坐标
        TreeNode* rson;//指向右儿子的坐标
};

第二步:二叉查找树类的声明

代码如下:

//二叉查找树类的属性和方法声明
template<class T>
class BST
{
    private:
        TreeNode<T>* root;//根节点
        void insertpri(TreeNode<T>* &node,T x);//插入
        TreeNode<T>* findpri(TreeNode<T>* node,T x);//查找
        void insubtree(TreeNode<T>* node);//中序遍历
        void Deletepri(TreeNode<T>* &node,T x);//删除
    public:
        BST():root(NULL){}
        void insert(T x);//插入接口
        TreeNode<T>* find(T x);//查找接口
        void Delete(T x);//删除接口
        void traversal();//遍历接口

};

第三步:插入

根据二叉查找树的性质,插入一个节点的时候,如果根节点为空,就此节点作为根节点,如果根节点不为空,就要先和根节点比较,如果比根节点的值小,就插入到根节点的左子树中,如果比根节点的值大就插入到根节点的右子树中,如此递归下去,找到插入的位置。重复节点的插入用值域中的freq标记。如图2是一个插入的过程。

二叉查找树的时间复杂度要看这棵树的形态,如果比较接近一一棵完全二叉树,那么时间复杂度在O(logn)左右,如果遇到如图3这样的二叉树的话,那么时间复杂度就会恢复到线性的O(n)了。

平衡二叉树会很好的解决如图3这种情况。

插入函数的代码如下:

//插入
template<class T>
void BST<T>::insertpri(TreeNode<T>* &node,T x)
{
    if(node==NULL)//如果节点为空,就在此节点处加入x信息
    {
        node=new TreeNode<T>();
        node->data=x;
        return;
    }
    if(node->data>x)//如果x小于节点的值,就继续在节点的左子树中插入x
    {
        insertpri(node->lson,x);
    }
    else if(node->data<x)//如果x大于节点的值,就继续在节点的右子树中插入x
    {
        insertpri(node->rson,x);
    }
    else ++(node->freq);//如果相等,就把频率加1
}
//插入接口
template<class T>
void BST<T>::insert(T x)
{
    insertpri(root,x);
}

第四步:查找

查找的功能和插入差不多一样,按照插入那样的方式递归下去,如果找到了,就返回这个节点的地址,如果没有找到,就返回NULL。

代码如下:

//查找
template<class T>
TreeNode<T>* BST<T>::findpri(TreeNode<T>* node,T x)
{
    if(node==NULL)//如果节点为空说明没找到,返回NULL
    {
        return NULL;
    }
    if(node->data>x)//如果x小于节点的值,就继续在节点的左子树中查找x
    {
        return findpri(node->lson,x);
    }
    else if(node->data<x)//如果x大于节点的值,就继续在节点的左子树中查找x
    {
        return findpri(node->rson,x);
    }
    else return node;//如果相等,就找到了此节点
}
//查找接口
template<class T>
TreeNode<T>* BST<T>::find(T x)
{
    return findpri(root,x);
}

第五步:删除

删除会麻烦一点,如果是叶子节点的话,直接删除就可以了。如果只有一个孩子的话,就让它的父亲指向它的儿子,然后删除这个节点。图4显示了一棵初始树和4节点被删除后的结果。先用一个临时指针指向4节点,再让4节点的地址指向它的孩子,这个时候2节点的右儿子就变成了3节点,最后删除临时节点指向的空间,也就是4节点。

删除有两个儿子的节点会比较复杂一些。一般的删除策略是用其右子树最小的数据代替该节点的数据并递归的删除掉右子树中最小数据的节点。因为右子树中数据最小的节点肯定没有左儿子,所以删除的时候容易一些。图5显示了一棵初始树和2节点被删除后的结果。首先在2节点的右子树中找到最小的节点3,然后把3的数据赋值给2节点,这个时候2节点的数据变为3,然后的工作就是删除右子树中的3节点了,采用递归删除。

我们发现对2节点右子树的查找进行了两遍,第一遍找到最小节点并赋值,第二遍删除这个最小的节点,这样的效率并不是很高。你能不能写出只查找一次就可以实现赋值和删除两个功能的函数呢?

如果删除的次数不是很多的话,有一种删除的方法会比较快一点,名字叫懒惰删除法:当一个元素要被删除时,它仍留在树中,只是多了一个删除的标记。这种方法的优点是删除那一步的时间开销就可以避免了,如果重新插入删除的节点的话,插入时也避免了分配空间的时间开销。缺点是树的深度会增加,查找的时间复杂度会增加,插入的时间可能会增加。

删除函数代码如下:

//删除
template<class T>
void BST<T>::Deletepri(TreeNode<T>* &node,T x)
{
    if(node==NULL) return ;//没有找到值是x的节点
    if(x < node->data)
    Deletepri(node->lson,x);//如果x小于节点的值,就继续在节点的左子树中删除x
    else if(x > node->data)
    Deletepri(node->rson,x);//如果x大于节点的值,就继续在节点的右子树中删除x
    else//如果相等,此节点就是要删除的节点
    {
        if(node->lson&&node->rson)//此节点有两个儿子
        {
            TreeNode<T>* temp=node->rson;//temp指向节点的右儿子
            while(temp->lson!=NULL) temp=temp->lson;//找到右子树中值最小的节点
            //把右子树中最小节点的值赋值给本节点
            node->data=temp->data;
            node->freq=temp->freq;
            Deletepri(node->rson,temp->data);//删除右子树中最小值的节点
        }
        else//此节点有1个或0个儿子
        {
            TreeNode<T>* temp=node;
            if(node->lson==NULL)//有右儿子或者没有儿子
            node=node->rson;
            else if(node->rson==NULL)//有左儿子
            node=node->lson;
            delete(temp);
        }
    }
    return;
}
//删除接口
template<class T>
void BST<T>::Delete(T x)
{
    Deletepri(root,x);
}

第六步:中序遍历

遍历的方法和二叉树的方法一样,写这个方法的目的呢,是输出这个二叉查找树的有序序列。

代码如下:

//中序遍历函数
template<class T>
void BST<T>::insubtree(TreeNode<T>* node)
{
    if(node==NULL) return;
    insubtree(node->lson);//先遍历左子树
    cout<<node->data<<" ";//输出根节点
    insubtree(node->rson);//再遍历右子树
}
//中序遍历接口
template<class T>
void BST<T>::traversal()
{
    insubtree(root);
}

到此,整个代码就完成了,代码中肯定有很多不完善的地方请指出,我会加以完善,谢谢。

对于二叉查找树不稳定的时间复杂度的解决方案有不少,平衡二叉树、伸展树和红黑树都可以解决这个问题,但效果是不一样的。

时间: 2024-10-19 18:30:25

一步一步写二叉查找树的相关文章

一步一步写平衡二叉树(AVL树)

平衡二叉树(Balanced Binary Tree)是二叉查找树的一个进化体,也是第一个引入平衡概念的二叉树.1962年,G.M. Adelson-Velsky 和 E.M. Landis发明了这棵树,所以它又叫AVL树.平衡二叉树要求对于每一个节点来说,它的左右子树的高度之差不能超过1,如果插入或者删除一个节点使得高度之差大于1,就要进行节点之间的旋转,将二叉树重新维持在一个平衡状态.这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(

.NET跨平台:在Mac上跟着错误信息一步一步手写ASP.NET 5程序

今天坐高铁时尝试了一种学习ASP.NET 5的笨方法,从空文件夹开始,根据运行dnx . kestrel命令的错误信息,一步一步写代码,直至将一个最简单的ASP.NET程序运行起来. 尝试的具体步骤如下. 新建一个空文件夹HelloCnblogs: mkdir HelloCnblogs && cd $_ 在这个空HelloCnblogs文件夹中运行 dnx . kestrel 命令(基于CoreCLR的dnx),运行结果是如下的出错信息: System.InvalidOperationEx

一步一步写jQuery插件

转载自:http://www.cnblogs.com/joey0210/p/3408349.html 前言 如今做web开发,jquery 几乎是必不可少的,就连vs神器在2010版本开始将Jquery 及ui 内置web项目里了.至于使用jquery好处这里就不再赘述了,用过的都知道.今天我们来讨论下jquery的插件机制,jquery有着成千上万的第三方插件,有时我们写好了一个独立的功能,也想将其与jquery结合起来,可以用jquery链式调用,这就要扩展jquery,写成插件形式了,如下

一步一步写算法(之递归和堆栈)

原文:一步一步写算法(之递归和堆栈) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 看过我前面博客的朋友都清楚,函数调用主要依靠ebp和esp的堆栈互动来实现的.那么递归呢,最主要的特色就是函数自己调用自己.如果一个函数调用的是自己本身,那么这个函数就是递归函数. 我们可以看一下普通函数的调用怎么样的.试想如果函数A调用了函数B,函数B又调用了函数C,那么在堆栈中的数据是怎么保存的呢? 函数A ^ 函数B | (地址递减) 函数C |

一步一步写算法(之双向链表)

原文:一步一步写算法(之双向链表) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 前面的博客我们介绍了单向链表.那么我们今天介绍的双向链表,顾名思义,就是数据本身具备了左边和右边的双向指针.双向链表相比较单向链表,主要有下面几个特点: (1)在数据结构中具有双向指针 (2)插入数据的时候需要考虑前后的方向的操作 (3)同样,删除数据的是有也需要考虑前后方向的操作 那么,一个非循环的双向链表操作应该是怎么样的呢?我们可以自己尝试一下: (

一步一步写算法(之字符串查找 上篇)

原文:一步一步写算法(之字符串查找 上篇) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 字符串运算是我们开发软件的基本功,其中比较常用的功能有字符串长度的求解.字符串的比较.字符串的拷贝.字符串的upper等等.另外一个经常使用但是却被我们忽视的功能就是字符串的查找.word里面有字符串查找.notepad里面有字符串查找.winxp里面也有系统自带的字符串的查找,所以编写属于自己的字符串查找一方面可以提高自己的自信心,另外一方面在某

一步一步写算法(之合并排序)

原文:一步一步写算法(之合并排序) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 前面一篇博客提到的快速排序是排序算法中的一种经典算法.和快速排序一样,合并排序是另外一种经常使用的排序算法.那么合并排序算法有什么不同呢?关键之处就体现在这个合并上面. 合并算法的基本步骤如下所示: 1)把0~length-1的数组分成左数组和右数组 2)对左数组和右数组进行迭代排序 3)将左数组和右数组进行合并,那么生成的整个数组就是有序的数据数组 下面

一步一步写算法(之排序二叉树)

原文:一步一步写算法(之排序二叉树) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 前面我们讲过双向链表的数据结构.每一个循环节点有两个指针,一个指向前面一个节点,一个指向后继节点,这样所有的节点像一颗颗珍珠一样被一根线穿在了一起.然而今天我们讨论的数据结构却有一点不同,它有三个节点.它是这样定义的: typedef struct _TREE_NODE { int data; struct _TREE_NODE* parent; str

一步一步写算法(之单词统计)

原文:一步一步写算法(之单词统计) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 在面试环节中,有一道题目也是考官们中意的一道题目:如果统计一段由字符和和空格组成的字符串中有多少个单词? 其实,之所以问这个题目,考官的目的就是想了解一下你对状态机了解多少. (1) 题目分析 从题目上看,如果对一个字符串进行处理,那么可以有下面几种情形:初始状态,字符状态,空格状态,结束状态.那么这几种状态之间应该怎么迁移呢? 初始状态: 如果输入符号是