二叉查找树

简单介绍树,基础说多了也没什么意思,这里主要实现二叉查找树

树,是一种非线性的数据结构,是一种特殊的(没有回路的)图,主要用在涉及到一个问题具有分支情况下的建模。树的若干定义都很简单,像父亲,儿子,根,兄弟,祖先,树叶,树枝,深度,高度等。在很多情况下,我们主要讨论二叉树(Binary Tree),也就是每一个节点最多只有两个子节点的情况。有一些比较特殊的二叉树值得讨论,比如:满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树。完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。完全二叉树有时采用线性存储(即堆),但其余的二叉树一般采用二叉链存储结构。

另外,对于一般的树,我们也通常采用孩子兄弟链存储结构,节点一般如下定义:

struct treeNode
{
    ElemType element;
    treeNode * FirstChild;  //第一个孩子
    treeNode * NextSibling; //下一个兄弟节点
};

而对于二叉树,因为最多只有两个孩子,所以一般采用如下节点定义:

typedef struct treeNode
{
    ElemType element;
    treeNode * left;  //左孩子
    treeNode * right; //右孩子
} * BiTree, * pNode;

树的遍历

在二叉树的一系列操作之中,我们比较关心插入、删除、求高度、遍历等操作。这里对于所有的二叉树,给出求高度与遍历的方法。遍历一般采用递归,当然也可以不用递归,直接创建栈来模拟递归调用用户栈的过程。所以有递归遍历与非递归遍历的实现,在实际编写程序我们更倾向于使用递归遍历。这里给出实现。

求任意节点的高度:

int Height(pNode p)
{
    if(p == NULL)
        return -1;
    else
        return max(Height(p->left), Height(p->right)) + 1;
}//这里max为求两者之中最大值得函数,使用宏定义#define max(a,b) (a>b?a:b)即可

二叉树的先序、中序、后序递归遍历:

void pre_traverse(BiTree T) //先序,根左右
{
    if(T == NULL)
        return ;
    cout << T->element << " ";
    pre_traverse(T->left);
    pre_traverse(T->right);
}
void in_traverse(BiTree T)  //中序,左根右
{
    if(T == NULL)
        return;
    in_traverse(T->left);
    cout << T->element << " ";
    in_traverse(T->right); 
}
void post_traverse(BiTree T) //后序,左右根
{
    if(T == NULL)
        return;
    post_traverse(T->left);
    post_traverse(T->right);
    cout << T->element << " ";
}

非递归先序遍历:

void pre_traverse_nonrecursive(BiTree T)
{
    if(T == NULL)
        return;
    stack<pNode> S;
    S.push(T);
    while(! S.empty())
    {
        pNode tmp = S.top();
        cout << tmp->element << " ";
        S.pop();
        if(tmp->right)
            S.push(tmp->right);
        if(tmp->left)
            S.push(tmp->left);
    }
}

中序及后序改造一下即可,不做实现,这里非递归遍历并没有节省多少空间,只是自定义的栈代替了用户栈。空间复杂度依然为O(n),时间复杂度同为O(n)。其实还存在空间复杂度为O(1)的遍历方法,实现过程稍微复杂一点点,远不如递归遍历来得那么简洁,等到有必要这样实现的时候再实现一下。很多情况下我们也可以构造线索二叉树,使得遍历不用栈和队列的辅助,优化时间。有的时候需要层序遍历,层序遍历即是二叉树层数从上到下从左到右,依次输出节点,使用队列辅助进行。

二叉查找树

二叉查找树(Binary serach tree)亦称二叉搜索树,二叉排序树,特点是其中序遍历序列是单调的(一般实现时选择递增)。也就是说,它是用来存储、查找与排序用的。当我们使用线性结构数组时,查找很方便,但插入和删除却要O(n)的平均复杂度。使用链表时,插入删除很方便,查找的平均复杂度却是O(n)。所以引入了非线性结构二叉查找树,弥补数组和链表的缺点,使得查找、插入、删除操作的平均时间复杂度均是O(logn)。

二叉查找树的递归定义:

  • 对于任意一个节点,如果左子树不空,则左子树的所有节点值均小于根节点值
  • 对于任意一个节点,如果右子树不空,则右子树的所有节点值均大于根节点值
  • 左子树、右子树也是二叉查找树

实现

首先,采用二叉链存储结构,节点定义,同一般二叉树。

typedef int ElemType;
typedef struct treeNode
{
    ElemType element;
    treeNode * left;
    treeNode * right;
}* pNode, * BSTree;

函数声明:

void Clear(BSTree &T);            //清空一颗树
pNode Find(BSTree T,ElemType x);  //查找元素x
pNode FindMax(BSTree T);          //查找最大值
pNode FindMin(BSTree T);          //查找最小值
void Insert(BSTree &T,ElemType x);//Insert理论情况不会出错,返回值void 
bool Delete(BSTree &T,ElemType x);//删除失败时返回 false 
ElemType Retrieve(pNode p);       //取节点值
//先序、中序、后序遍历,见上面
void pre_traverse(BSTree T);
void in_traverse(BSTree T);
void post_traverse(BSTree T);

清空二叉树:
这里一定要确保传入的是一棵树,就算是空树,而不是一个未初始化为NULL的指针,否则的话程序很有可能会崩溃,发生SIGSEGV错误,也就是递归得不到终止,则栈空间将被用尽,俗称爆栈。
注:windows下用户栈大概是1MB,Linux下一般是8MB,可通过编译指令更改,所以一定要注意不能爆栈,递归一定要能终止且层数不能过多,这里二叉查找树的递归层数最大为树深度即O(logn),复杂度低,一般情况不会爆栈。

//Please ensure the parameter is a tree 
void Clear(BSTree &T)
{
    if(T != NULL)
    {
        Clear(T->left);
        Clear(T->right);
        delete T;
        T = NULL;
    }
}

查找元素x:

pNode Find(BSTree T,ElemType x)
{
    if(T == NULL)
        return NULL;
    if(x < T->element)
        return Find(T->left,x);
    else if(x > T->element)
        return Find(T->right,x);
    else
        return T;
}

查找最大值与最小值:

pNode FindMax(BSTree T)
{
    while(T->right != NULL)
        T = T->right;
    return T;
}
pNode FindMin(BSTree T)
{
    while(T->left != NULL)
        T = T->left;
    return T;
}

取节点值:

ElemType Retrieve(pNode p)
{
    if(p == NULL)
    {
        cout << "This node doesn't exist !" << endl;
        return 0;
    }
    return p->element;
}

插入一个值为x的节点:
这里插入时,如果存在元素x,则什么也不做,否则总是插入在路径的最后一个节点上。插入时,如果关键字x存在,则根据需要做该做的事,有的时候关键字仅仅是结构的一个域,则需要将该节点插入。某些情况下,关键字存在则不需要做任何事。我们甚至可以采用懒惰删除,即之只标记被删除的节点,表明它不属于这棵树,而不实际地将其从树中移走。在需要的时候,比如关键字相同的节点很多的时候,我们可以在树节点定义中加上一各域,表示该节点出现的频率,删除时将该域减1,插入时加1。上面的方法可以省去很多内存操作,提高效率,根据需求实现。

void Insert(BSTree &T,ElemType x)
{
    if(T == NULL)
    {
        T = new treeNode;
        T->element = x;
        T->left = T->right = NULL;
    }
    else if(x > T->element)
        Insert(T->right,x);
    else if(x < T->element)
        Insert(T->left,x);
    //else x is aleardy exist , we'll do nothing
    //and you can do the things you want
}

删除一个节点值为x的元素:
若节点不存在,则不进行任何操作。关键字为x的节点存在的三种情况:

  • 该节点为树叶,即没有左右孩子,则直接删除,并将其父节点对应域更改为NULL即可。
  • 该节点有一个孩子,则直接使其父节点的对应域指向其孩子节点,并释放其空间即可。
  • 该节点有来左右两个孩子,则用其右子树的最小值的来替换该节点(替换相应域即可),并删除这个用来替换的节点,显然这个节点一定只有0个或者一个孩子节点,则直接递归调用上述过程即可。
bool Delete(BSTree &T,ElemType x)
{
    if(T == NULL)
    {
        cout << "The element you want to delete is not in this tree !" << endl;
        return false;
    }
    else if(x > T->element)
        return Delete(T->right,x);
    else if(x < T->element)
        return Delete(T->left,x);
    else if(T->left && T->right)//the node has two children
    {
        pNode tmp = FindMin(T->right);
        T->element = tmp->element;
        Delete(T->right,T->element);      //删除已经替换上来的节点
        return true;
    }
    else //the node has one or zero children
    {
        pNode tmp = T;
        T = T->left == NULL ? T->right : T->left;   //改变T指针的指向为T的唯一的孩子节点或者NULL 
        delete tmp;
        return true;
    }
}

二叉查找树的查找、删除、插入的最坏时间复杂度均为O(logn),因为二叉查找树的平均深度为O(logn)。

但二叉查找树依然存在一些缺陷,如:
- 如果原来就是排好序的,那么按照顺序插入,则二叉树就变成了链表一样的形状,效率急剧降低。
- 在进行了很多次的插入和删除之后,删除中的一个步骤(总是用待删除节点右子树的最小值的来替换该节点)就会导致树枝呈现出向左边倾斜的趋势,在多次Insert和Delete后,平均操作复杂度会逐渐上升。

要解决以上问题,可以通过平衡二叉树,或者以后将介绍的近乎平衡的AVL树等数据结构来解决。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值