MOOC 数据结构 | 4. 树(中)

1. 什么是二叉搜索树

查找问题:

  • 静态查找与动态查找
  • 针对动态查找,数据如何组织?

二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:

  1. 非空左子树的所有键值小于其根结点的键值。
  2. 非空右子树的所有键值大于其根结点的键值。
  3. 左、右子树都是二叉搜索树

:不是二叉搜索树,对于结点10来说,其右子树应该比10大。

:是二叉搜索树。

:是二叉搜索树。

2. 二叉搜索树操作的特别函数

2.1 二叉搜索树的查找操作:Find

  • 查找从根结点开始,如果树为空,返回NULL
  • 若搜索树非空,则根结点关键字和X进行比较,并进行不同处理:
  1. X小于根结点键值,只需在左子树中继续搜索;
  2. 如果X大于根结点的键值,在右子树中进行继续搜索;
  3. 若两者比较结果是相等,搜索完成,返回指向此结点的指针。

Position Find(ElementType X, BinTree BST)
{
    if (!BST) 
        return NULL;  /*查找失败*/
    if(X > BST->Data)
        return Find(X, BST->Right);  /*在右子树中继续查找*/
    else if (X < BST->Data)
        return Find(X, BST->Left);   /*在左子树中继续查找*/
    else /*X == BST->Data*/
        return BST;  /*查找成功,返回结点的找到结点的地址*/
}

递归实现的效率较低,且都是“尾递归”(尾递归就是在程序要返回的时候进行递归),从编译角度来讲,“尾递归”都可以用循环来实现。

□ 由于非递归函数的执行效率高,可将“尾递归”函数改为迭代函数

Position Find(ElementType X, BinTree BST)
{ 
    while( BST ) 
    {
        if(X > BST->Data)   
            BST = BST->Right;   /*向右子树中移动,继续查找*/
        else if (X < BST->Data)
            BST = BST->Left;    /*向左子树中移动,继续查找*/
        else   /*X == BST->Data*/
            return BST;   /*查找成功,返回结点的找到结点的地址*/
    }
    return NULL; /*查找失败*/ 
}

查找效率决定于树的高度  ---- 树不要往一边倒,只有左子树或右子树,所有结点链成一条链,高度就是n - 1。所以就会引入后面讲的平衡二叉树。

2.2  查找最大和最小元素

最大元素一定是在树的最右分枝的端结点

最小元素一定是在树的最左分枝的端结点

//查找最大元素的递归函数
Position FindMax(BinTree BST)
{
    if (!BST)  
        return NULL;
    else if(!BST->Right)
        return BST;
    else 
        return FindMax(BST->Right);
}

//查找最大元素的迭代函数:
Position FindMax(BinTree BST)
{
    if(BST)
    {
        while(BST->Right)
            BST = BST->Right;  /*沿右分支继续查找,直到右子树为空*/
    }
    return BST;
}
//查找最小元素的递归函数:
Position FindMin(BinTree BST)
{
    if (!BST) 
        return NULL;   /*空的二叉搜索树,返回NULL*/
    else if (!BST->Left)
        return BST;  /*找到最左叶结点并返回*/
    else
        return FindMin(BST->Left);  /*沿左分支继续查找*/
}
//查找最小元素的迭代函数
Position FindMin(BinTree BST)
{
    if( BST )
    {
        while(BST->Left)
            BST = BST->Left;
    }
    return BST;
}

2.3 二叉搜索树的插入

【分析】关键是要找到元素应该插入的位置(要保证插入完毕后这棵树还是二叉搜索树),可以采用与Find类似的方法。

1. 要插入35,先与根结点进行比较(35 > 30),所以就往右走。

2.与41进行比较(35 < 41),所以就往左边走。

3. 与33比较(35 > 33),就往右走,发现右边是空的,于是就进行插入。

4. 插入35。

二叉搜索树的插入算法:

BinTree Insert(ElementType X, BinTree BST)
{
    if (!BST)
    {
        /*若原树为空,生成并返回一个结点的二叉搜索树*/
        BST = malloc(sizeof(struct TreeNode));
        BST->Data = X;
        BST->Left = BST->Right = NULL;
    }
    else /*开始找要插入元素的位置*/
    {
        if(X < BST->Data)
            BST->Left = Insert(X, BST->Left); /*递归插入左子树*/
        else if (X > BST->Data)
            BST->Right = Insert(X, BST->Right);  /*递归插入右子树*/
        /*else X已经存在,什么都不做*/
    }
    return BST;
}

上面的if ( !BST )分支执行的时机:BST指向33结点时,再进行递归,BST->Right = NULL,就申请空间构造一个结点并返回。

即是BST->Right = Insert(X, BST->Right); 中insert结果返回了,返回的结果就赋值给了BST->Right,也就是:

【例】以一年十二个月的英文缩写为键值,按从一月到十二月顺序输入,即输入序列为(Jan, Feb, Mar, Apr, May, Jun, July, Aug, Sep, Oct, Nov, Dec)   

这里的大小指键值的英文字典序。

2.4 二叉搜索树的删除

□ 考虑三种情况

  • ①要删除的是叶结点:直接删除,并再修改其父结点指针---置为NULL

【例】删除35

先要找到35,然后再删除,最后将33的右结点置为NULL

  • ②要删除的结点只有一个孩子结点:将其父结点的指针指向要删除结点的孩子结点

【例】删除33

先要找到33(查找过程),然后将35挂在41的左边即可:

--->  

  • ③要删除的结点有左、右两棵子树:用另一结点替代被删除结点:右子树的最小元素 或者 左子树的最大元素

【例】删除41

(1)取右子树中的最小元素替代

-->  

(2) 取左子树中的最大元素替代

-->

无论左子树的最大值还是右子树的最小值,都只有一个儿子。

BinTree Delete(ElementType X, BinTree BST)
{
    Position Tmp;
    if (!BST) 
        printf("要删除的元素未找到");
    else if (X < BST->Data)
        BST->Left = Delete(X, BST->Left);   /*左子树递归删除,Delete返回的是删除后的左子树的根结点地址,然后赋值给Left*/
    else if (X > BST->Data)
        BST->Right = Delete(X, BST->Right); /*右子树递归删除*/
    else  /*找到要删除的结点*/
        if (BST->Left && BST->Right)  /*被删除结点有左右两个子结点*/
        {
            Tmp = FindMin(BST->Right);  /*在右子树中找最小元素填充删除结点*/
            BST->Data = Tmp->Data;
            BST->Right = Delete(BST->Data, BST->Right); /*在删除结点的右子树中删除最小元素*/
        }
        else  /*被删除结点有一个或无子结点*/
        {
            Tmp = BST;
            if (!BST->Left)  /*有右孩子或无子结点*/
                BST = BST->Right;
            else if (!BST->Right)  /*有左孩子或无子结点*/
                BST = BST->Left;
            free(Tmp);
        }
    return BST;
}

3. 什么是平衡二叉树

【例】搜索树结点不同插入次序,将导致不同的深度和平均查找长度ASL

按照“字典顺序”比较大小

"平衡因子"(Balance Factor,简称BF):BF(T)= {\color{Blue} h_L-h_R},其中h_Lh_R分别为T的左、右子树的高度。

(AVL是提出该概念的科学家名字的缩写)

1. (对于结点4来说,左右子树的高度差为0,是平衡的;但是对于结点3来说,左子树高度为2,右子树高度为0,所以高度差为2,是不平衡的。所以这棵树不是平衡树)。

2. 

3. (对于结点7来说,左右子树的高度差为2,所以不是平衡二叉树)

3.1 平衡二叉树的高度能达到log_2n吗?

n_h高度为h的平衡二叉树的最少结点数(此处的高度为边的条数)。结点数最少时:

所以当高度为h的时候,结点数最少无非是下面两种情况:

通过与斐波那契序列的关系比较:

可以得出n_hh是指数关系。进一步得出结论:

4.平衡二叉树的调整

4.1 RR旋转

插入顺序:Mar、May、Nov

1、一开始只有Mar,Mar结点的平衡因子为0

2、来了May,

3、来了Nov

可以看到Mar结点的平衡因子为-2,所以这不是一棵平衡二叉树。因此就要调整:

概括一下,是如下的这种模式:

 

一开始是一棵平衡二叉树,在B的右子树上又插入了一个结点,所以A结点就不平衡了,因为插入的结点在A的右子树的右子树上,所以进行RR旋转,将B放到A的位置,A链接到B的左边,A_L还是链接在A的左子树上,而因为还是一棵二叉搜索树,B_L比B小,比A大,所以链接到A的右边。

实例1:如下树上插入结点15

这就导致了结点5不平衡,二者的关系是破坏者在被破坏者的右子树的右子树上,所以进行RR旋转:

视频讲解过程:链接:https://pan.baidu.com/s/11gV-yFijp2ivB714flTLHw  提取码:s6eu 
 

实例2:插入结点13

       

破坏者也是在被破坏者的右子树的右子树上,所以也是RR旋转:

4.2 LL旋转

插入结点Apr之后,Mar结点不平衡了,破坏者在被破坏者的左子树的左子树上,所以,进行对Apr、Aug和Mar结点进行LL旋转。可以看到,旋转并不一定是发生在根结点上。

 

在A结点的左子树的左子树上插入了一个结点(可能在左边,可能在右边),导致了结点A不平衡,于是进行LL调整,将B结点置为根结点,被破坏者A作为右子树,B_R比B大,比A小,所以链接在A的左边。

4.3 LR旋转

原来有一棵平衡树:

插入结点Jan:

结点May不平衡了,破坏者(Jan)在被破坏者(May)的左子树的右子树上,所以进行LR旋转:

基本方法:重点关注May、Aug和Mar三个结点,将这三个结点调平衡。三者中,中间的值时Mar,所以这三个结点调成这样的状态:

4.4  RL旋转

插入结点Feb:

Aug结点的平衡性被破坏了,破坏者(Feb)在被破坏者(Aug)的右子树的左子树上,所以进行RL旋转:

基本模式就是把Aug、Jan和Dec三个结点进行调整:

四种模式如何判别,就看插入者把哪个结点破坏了,插入者与被破坏者之间的关系。

例子:

插入结点June:

破坏者(June)在被破坏者(Mar)的左子树的右子树上,所以进行LR旋转,即调整Mar、Dec和Jan三个结点:

再插入Oct:

破坏了结点May的平衡性,破坏者(Oct)在被破坏者(May)的右子树的右子树上,所以进行RR旋转:

再插入结点Sept:

是平衡的,虽然树结构没有变化,但是平衡因子是要变化的。

5. 小白专场:是否同一棵二叉搜索树

5.1 题目

04-树4 是否同一棵二叉搜索树 (25 分)

给定一个插入序列就可以唯一确定一棵二叉搜索树。然而,一棵给定的二叉搜索树却可以由多种不同的插入序列得到。例如分别按照序列{2, 1, 3}和{2, 3, 1}插入初始为空的二叉搜索树,都得到一样的结果。于是对于输入的各种插入序列,你需要判断它们是否能生成一样的二叉搜索树。

输入格式:

输入包含若干组测试数据。每组数据的第1行给出两个正整数N (≤10)和L,分别是每个序列插入元素的个数和需要检查的序列个数。第2行给出N个以空格分隔的正整数,作为初始插入序列。最后L行,每行给出N个插入的元素,属于L个需要检查的序列。

简单起见,我们保证每个插入序列都是1到N的一个排列。当读到N为0时,标志输入结束,这组数据不要处理。

输出格式:

对每一组需要检查的序列,如果其生成的二叉搜索树跟对应的初始序列生成的一样,输出“Yes”,否则输出“No”。

输入样例:

4 2
3 1 4 2
3 4 1 2
3 2 4 1
2 1
2 1
1 2
0

输出样例:

Yes
No
No

5.2 题意理解

□ 给定一个插入序列就可以唯一确定一棵二叉搜索树。然而,一棵给定的二叉搜索树却可以由多种不同的插入序列得到。

     ☆例如,按照序列{2, 1 ,3}和{2, 3, 1}插入初始为空的二叉搜索树,都得到一样的结果。

       

□问题:对于输入的各种插入序列,你需要判断它们是否能生成一样的二叉搜索树。

对于输入样例:3 1 4 2 和 3 4 1 2 生成的二叉搜索树相同,所以输出Yes。

3 2 4 1 这个输入序列对应的二叉搜索树和 3 1 4 2 对应的二叉搜索树不同,所以输出No。

5.3 求解思路

两个序列是否对应相同搜索树的判别

1、分别建两棵搜索树的判别方法

          根据两个序列分别建树,再判别树是否一样(根、左子树、右子树是不是一样,递归)

2、不建树的判别方法

两个序列的第一个数都是3,所以两棵树的根结点都是3,根据根结点3,可以将序列后面的数分为两堆--比3大,比3小,顺序还是按照原来的顺序。比较两个左子树是否一样,两个右子树是否一样。所以,这两棵搜索树是一样的:

来看另一组数据:

显然,左子树不同(一个左子树根结点是1,一个左子树根结点是2),(采用递归)。

3、建一棵树,再判别其他序列是否与该树一致

  1. 搜索树表示
  2. 建搜索树T
  3. 判别一序列是否与搜索树T一致

5.3.1 搜索树表示

typedef struct TreeNode *Tree;
struct TreeNode
{
    int v;
    Tree Left, Right;
    int flag;   /*判别一个序列是否与树一致,结点访问标记*/
};

5.3.2 程序框架搭建

               

5.3.3 如何建搜索树

 

构造新结点:

    

插入结点:

   

代码:

Tree NewNode(int V)
{
    Tree T = (Tree)malloc(sizeof(struct TreeNode));
    T->v = V;
    T->Left = T->Right = NULL;
    T->flag = 0; //没被访问过的结点flag = 0,被访问过的结点flag = 1 
    return T;
}

Tree Insert(Tree T, int V)
{
    if(!T)
        T = NewNode(V);
    else 
    {
        if (V > T->v)
            T->Right = Insert(T->Right, V);
        else 
            T->Left = Insert(T->Left, V);
    }
    return T;
}

Tree NewNode(int V)
{
    Tree T = (Tree)malloc(sizeof(struct TreeNode));
    T->v = V;
    T->Left = T->Right = NULL;
    T->flag = 0; //没被访问过的结点flag = 0,被访问过的结点flag = 1 
    return T;
}

Tree Insert(Tree T, int V)
{
    if(!T)
        T = NewNode(V);
    else 
    {
        if (V > T->v)
            T->Right = Insert(T->Right, V);
        else 
            T->Left = Insert(T->Left, V);
    }
    return T;
}

5.3.3 如何判别

对于上面的例子,比较过程:

序列(3 2 4 1)

①先看3,是T的根结点,将flag标记为1;

②接下来找2,为了找2,要从3开始找,比较过程就是树中的3 -> 1 ->2,3此前碰到过,而1没有碰到过。因此可以判定,序列与树不一致。

 

bug在于比如序列3 2 4 1,比较到1的时候发现与T不一致,此时就直接return,然后进入下一个序列的比较,该序列中的4 1 会被当做下一个序列的数。所以发现不一致的时候不能立即return,要把这个序列后面的数都读完。

另外两个小的函数ResetT和FreeTree:

void ResetT(Tree T)  /*清除T中各结点的flag标记*/
{
    if(T->Left)
        ResetT(T->Left);
    if(T->Right)
        Reset(T->Right);
    T->flag = 0;
}

void FreeTree(Tree T)  /*释放T的空间*/
{
    if (T->Left)
        FreeTree(T->Left);
    if(T->Right)
        FreeTree(T->Right);
    free(T);
}

5.3.4 完整代码

#include <stdio.h>
#include <stdlib.h>

typedef struct TreeNode *Tree;
struct TreeNode
{
    int v;
    Tree Left, Right;
    int flag;   /*判别一个序列是否与树一致,结点访问标记*/
};

Tree NewNode(int V)
{
    Tree T = (Tree)malloc(sizeof(struct TreeNode));
    T->v = V;
    T->Left = T->Right = NULL;
    T->flag = 0; //没被访问过的结点flag = 0,被访问过的结点flag = 1
    return T;
}

Tree Insert(Tree T, int V)
{
    if(!T)
        T = NewNode(V);
    else
    {
        if (V > T->v)
            T->Right = Insert(T->Right, V);
        else
            T->Left = Insert(T->Left, V);
    }
    return T;
}

Tree MakeTree(int N)
{
    Tree T;
    int i, V;

    scanf("%d",&V);  //读入第一个数
    T = NewNode(V);  //为这个数构造一个结点,T中只包含了一个结点
    for(i = 1; i < N; i++)
    {
        scanf("%d", &V);
        T = Insert(T, V);  //当前读入的数插入到结点中
    }
    return T;
}

int check(Tree T, int V)
{
    if(T->flag)
    {
        if(V < T->v)
            return check(T->Left, V);
        else if ( V > T->v)
            return check(T->Right, V);
        else
            return 0; //序列中有重复出现的数,也作为不一致处理
    }
    else
    {
        if ( V == T->v)
        {
            T->flag = 1;
            return 1;
        }
        else
            return 0;
    }
}

int Judge(Tree T, int N)
{
    int i, V, flag = 0;  /*flag:0表示目前还一致,1表示已经不一致*/
    scanf("%d", &V);
    if (V != T->v)   //判断树根是否相同
        flag = 1;
    else
        T->flag = 1; //标记该结点已经访问过

    for(i = 1; i < N; i++)
    {
        scanf("%d", &V);
        if((!flag) && (!check(T, V)))  //如果flag = 1,即已经不一致了,就不用check了,直接继续读下一个
            flag = 1;
    }
    if(flag)
        return 0;
    else
        return 1;
}

void ResetT(Tree T)  /*清除T中各结点的flag标记*/
{
    if(T->Left)
        ResetT(T->Left);
    if(T->Right)
        ResetT(T->Right);
    T->flag = 0;
}

void FreeTree(Tree T)  /*释放T的空间*/
{
    if (T->Left)
        FreeTree(T->Left);
    if(T->Right)
        FreeTree(T->Right);
    free(T);
}

int main()
{
    int N,L,i;
    Tree T;

    scanf("%d", &N);
    while(N)
    {
        scanf("%d", &L);
        T = MakeTree(N);
        for(i = 0; i < L;i++)
        {
            if(Judge(T, N))
                printf("Yes\n");
            else
                printf("No\n");
            ResetT(T);
        }
        FreeTree(T); //一组数据处理完,要进入下一组数据前,要把当前的树Free
        scanf("%d", &N); //第二组输入
    }
    return 0;
}

PTA系统已关闭测试,输入样例测试Pass:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值