树和二叉树

树和二叉树

数的定义和基本术语

  1. 度:结点拥有的子树数称为度。
  2. 叶子结点:度为0的结点
  3. 非终端结点(分支结点、内部结点):度不为0的结点
  4. 层次:从根开始,根为第一层,根的孩子为第二层。
  5. 深度:树中结点的最大层次称为树的深度或高度。
  6. 有序树:如果将树中结点的个子树看成从左至右是有次序的(即不能互换),则称为有序树
  7. 森林:对于树种每个结点而言,其子树的集合即为森林。

二叉树

二叉树的子树有左右之分,其次序不能任意颠倒。

二叉树的性质

性质1:在二叉树的第i层上,最多有2^i-1个结点
性质2:深度为k的二叉树最大结点数为:2^k - 1
性质3:对于任何一颗二叉树,如果其终端结点数为n0, 度为2的结点数为n2, 那么n0 = n2 + 1.因为:

  • n = 分支数 + 1
  • n = n0 + n1 + n2
  • 分支数 = n1 + 2*n2

性质4:具有n个结点的完全二叉树,深度为 log(n)向下取整 + 1

二叉树的存储结构

1. 顺序存储结构

用一块连续的地址依次自上而下、自左向右存储完全的二叉树。

2. 链式存储结构

表示二叉树的结点至少有三个域:数据域、左、右指针。

遍历二叉树和线索二叉树

遍历二叉树

先序遍历:

  • 先访问根
  • 然后左、右子树

中序遍历:

//非递归,用栈解决
status InoderTraverse(BiTree T, Status(*visit)(TElemType e)){
    InitStack(S);
    push(S,T);
    while(!Stackempty()){
        while(GetTop(S,p) && p) push(S, p->left);
        pop(S,p);    //空指针退栈
        if(!Stackempty()){
            pop(S,p);
            Visit(p->data);
            push(S, p->right);
        }
    }
    return ok;
}


//递归算法
status InoderTraverse(BiTree T, Status(*visist)(TElemType e)){
    if(T->left) {
        InoderTraverse(T->left, T->left->data);
    }
    if(T) Visit(T->data);
    if(T->right){
        InoderTraverse(T->right, T->right->data);
    }
    return ok;
}
  • 先左子树
  • 右子树

后序遍历:

//后序遍历非递归写法
//中序遍历中,是一路将左儿子压栈、然后弹栈输出、如果它的右儿子不为空再压栈
//后序遍历中,也是一路将左儿子压栈,然后取栈顶元素查看,如果右儿子不为空则压栈,否则输出。
status LastTraverse(BiTree T) {
    InitStack(S);
    push(S,T);
    while(!Stackempty(S)){
        while(T->left) push(S, T->left);
        pop(S, p);       //空指针退栈
        if(!Stackempty()){
            GetTop(S, p);
            if(p->right == NULL) Visit(p->data);
            else push(S, p->right);
        }
    }
    return ok;
}

//递归写法
status LastTraverse(BiTree T) {
    if(T->left) LastTraverse(T->left);
    if(T->right) LastTraverse(T->right);
    Visit(T->data);
}
  • 先左、右子树
  • 后根

线索二叉树

  在前面遍历二叉树的时候,当以二叉链表作为存储结构时,只能找到结点的左右孩子的信息,而不能直接得到结点在任一序列中的前驱和后继信息,这种信息只有在遍历的动态过程中才能得到。

  一种简单的保存这种信息的方法是在每个结点上增加两个指针域fwd和bkwd,分别指示结点在任一次序遍历的时得到的前驱和后继信息。

  现做如下规定:若结点有左子树,那么它的Lchild域指示其左孩子,否则令Lchild指示其前驱。若结点有右子树,那么它的Rchild域指示其右孩子,否则令Rchild指示其后继。为避免混淆,增加两个标志位LTag、RTag,0表示指示孩子,1表示指示前驱或后继。

1. 概念

线索链表: 这种结点结构构成的二叉链表就是线索链表
线索: 指向结点前驱和后继的指针,叫做线索
线索化:对二叉树以某种次序遍历使其变成线索二叉树的过程叫做线索化

2. 中序线索化

//写这种递归的核心:
1. 知道递归框架,如中序遍历:先递归左子树、后遍历根、后遍历右子树
2. 然后知道初始条件:这里preT = NULL;
3. 然后知道在某一个状态的时候,递归函数中该怎么处理。比如到P结点。此时如果有左子树,显然指针指向左子树,否则左边可以前驱线索化。再看此时前驱结点有无右子树,如果有,那么指向右子树,否则可以直接后继线索化


preT = NULL;

status InThread(BiTree p) {
    if(p){
         InThread(p->left);
         if(!p->left){
             p->Ltag = Thread;
             p->left = preT;
         }
         if(!preT->right){
             preT->Rtag = thread;
             preT->right = p;
         }
         preT = p;
         InThread(p->right);
     }
}   

哈夫曼(赫夫曼、最优二叉树)树

构造一颗有n个叶子结点的二叉树,每个叶子结点带权Wi,则其中带权路径长度WPL最小的二叉树称作最优二叉树。

二叉排序树和平衡二叉树

二叉排序树(二叉查找树)

二叉排序树或者是一颗空树,或者是具有下列性质的二叉树。
1. 若它的左子树不为空,则左子树上所有结点的值均小于它的根结点值
2. 若它的右子树不为空,则右子树上所有结点的值均大于它的根结点值
3. 它的左右子树也分别为二叉排序树

1. 插入

// 查找元素
status SearchBST(BiTree T, KeyType key, BiTree f, BiTree &p) {
    //f为T的父亲结点,初始化为NULL, 如果没找到那么f就是叶子结点,这样便于后面插入
    if(!T) {
        p = f;
        return false;
    }
    else if(EQ(key, T->data.key)) {
        p = T;
        return true;
    }
    else if(key < T->data.key) return SearchBST(T->lchild,key,T,p);
    else return SearchBST(T->rchild,key,T,p);
}   

//插入
Status InsertBST(BiTree &T, ElemType e) {
    if(!SearchBST(T, e.key, NULL, p)){
        s = (BiTree)malloc(sizeof(BiTree));
        s->data = e;
        s->lchild = s->rchild  = NULL;
        if(!p) T = s;
        else if(e.key < p->data.key) p->lchild  =s;
        else p->rchild = s;
        return true;
    }
    else return false;
}

二叉排序树的构造过程就是不断插入值的过程。

2. 删除

设删除的元素为P。
1. 如果P有右儿子,把右子树接在P的父亲的儿子上。如果P是父亲的左儿子,那么接在左儿子上,如果P是父亲的右儿子,那么接在右儿子上。然后把P的左儿子(如果有左儿子)接在P的右儿子的左儿子尽头上。
2. 如果P没有右儿子,那么左子树直接接上去
3. 如果P没有左儿子,那么右儿子直接接上去
4. 如果P没有儿子,直接删除

平衡二叉树

或者为一颗空树,或者满足以下的条件:
1. 它的左、右子树均为平衡二叉树
2. 或者左、右子树的深度之差的绝对值不超过1

一般要将二叉排序树平衡化,防止二叉排序树的退化。

平衡二叉树都是在构建的过程中做平衡化,或者在删除操作后做平衡化,所以此时的不平衡状态主要有四种: (旋转点为首先失去平衡的点)
1. LL:在根的左子树的左子树上插入结点,向右的顺时针旋转。
2. LR:在根的左子树的右子树上插入结点,先向左、后向右。
3. RR:在根的右子树的右子树上插入结点,向左旋转。
4. RL:在根的右子树的左子树上插入结点,先向右、后向左。

B- 和 B+树

B-树

B-树是一种平衡的多路查找树,主要用作文件的索引:
一颗m阶的B-树,或为空树,或者满足以下条件:
1. 树中每个结点至多m颗子树,内结点至少 m/2向上取整 颗子树
2. 这里表示每个内结点最多只能有m-1个关键字
3. 内结点信息:(n, A0, K1, A1, K2, A2, …. , Kn, An)
4. 注意叶子结点,其实所有的叶子结点都出现在同一层次上,并且不带信息。(可以看做是外部结点或查找失败的结点,实际上这些结点不存在)

1. 插入

因为B-树结点中的关键字个数必须 大于等于 m/2向上取整。因此每次插入一个关键字的时候,不是在树中添加一个叶子结点,而是首先在某个非终端结点中添加一个关键字,若该结点的关键字个数不超过m-1,则插入完成,否则要产生结点的分裂。这里需要注意,一颗m阶的B-树,每个结点至多m颗子树,所以每个内结点最多只能有m-1个关键字

分裂:
1. 一般找到该结点中的中间点,
2. 把它拉到父亲结点
3. 把它左边的关键字连接到该关键字左边的指针,右边的接到右边指针

2. 删除

  1. 首先找到关键字所在结点,并删除。
  2. 若该结点为非叶结点,且被删关键字为该结点中第i个关键字key[i],则可从指针son[i]所指的子树中找出最小关键字Y,代替key[i]的位置,然后在叶结点中删去Y。 因此,把在非叶结点删除关键字k的问题就变成了删除叶子结点中的关键字的问题了。

删除叶子结点中的关键字:
1. 如果被删关键字所在结点的原关键字个数n>=ceil(m/2),说明删去该关键字后该结点仍满足B-树的定义。这种情况最为简单,只需从该结点中直接删去关键字即可。
2. 如果被删关键字所在结点的关键字个数n等于ceil(m/2)-1,说明删去该关键字后该结点将不满足B-树的定义,需要调整。调整过程为:如果其左右兄弟结点中有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目大于ceil(m/2)-1。则可将右(左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中小(大)于该上移关键字的关键字下移至被删关键字所在结点中。
3. 如果左右兄弟结点中没有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目均等于ceil(m/2)-1。这种情况比较复杂。需把要删除关键字的结点与其左(或右)兄弟结点以及双亲结点中分割二者的关键字合并成一个结点,即在删除关键字后,该结点中剩余的关键字加指针,加上双亲结点中的关键字Ki一起,合并到Ai(是双亲结点指向该删除关键字结点的左(右)兄弟结点的指针)所指的兄弟结点中去。如果因此使双亲结点中关键字个数小于ceil(m/2)-1,则对此双亲结点做同样处理。以致于可能直到对根结点做这样的处理而使整个树减少一层。

B+树

B+树是应文件系统需求而出的一种B-树的变型树。他们的差异如下:
1. 有n颗子树的结点中包含有n个关键字。(关键字两侧没有指针)
2. 所有的叶子结点中包含了全部的关键字信息,以及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3. 所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中的最小或者最大关键字。

键树(字典树)

二叉堆

二叉堆一般都是完全二叉树。
二叉堆满足二个特征:
1. 父结点的键值总是小于或大于所有子结点的键值
2. 左右子树都是二叉堆

如果键值总是大于或等于所有子结点,那么称为最大堆。 如果总是小于或等于所有子结点,那么称为最小堆

堆的存储

一般都用数组存储。i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。

堆的操作

从底开始,考虑每个内结点,调整、筛选为堆,再依次向上。那么直到根结点后肯定结果为堆。

筛选

对于一个堆,输出堆顶元素后,如何将剩下的元素调整为堆。这里就可以考虑为,对于以N为根的二叉树,它的左右子树都是堆,如何调整整颗树为堆。

筛选方法很简单(这里以最小堆为例):
1. 对于根N,比较左、右根,小的与根N替换。因为左右子树都为最小堆,所以左右根选出的最小值必定是整个堆的最小值。
2. 就这样一层层往下筛选,直到叶子结点。

建堆

给出一个序列,比如{49, 38, 65, 97, 76, 13, 27},最后一个非终端结点是第 n/2 向下取整个元素。。这里一共7个元素,所以第一个非终端结点一定是第3个元素,这里是65.

算法思路(建最小堆):
1. 先按照原序列建立一个二叉树。
2. 从第一个非终端结点开始,从该序列的左到右依次做筛选操作。这里先对65做筛选,直到49.

可行性:
对于第一个内结点,以它为根的二叉树肯定是一个两层的二叉树,所以只需要一次筛选便成为了一个堆。然后把它的兄弟结点变为堆。然后依次向上扩展,这个过程刚好就是筛选的过程。所以结果肯定是建成一个堆了。

删除

删除就和筛选差不多。比如删除一个结点N。
1. 这个结点是叶子结点,直接删除
2. 这个结点是内结点。这个时候对以这个结点为根的子树做一次筛选操作,可以把这个内结点沉入叶子结点,然后进行删除操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值