数据结构------树

Posted taoxiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构------树相关的知识,希望对你有一定的参考价值。

1.  一些基本概念

(1)度

  结点的度degree:结点的子树数

       树的度:树里面各结点度的最大值

 

  度为0的结点:叶结点 leaf 或终端结点    度不为0的:非终端结点、分支结点

(2)层次level

    技术图片

 

  树的深度 depth、高度:层次最大值

  二叉树深度:共N个结点

    一般二叉树平均深度:O(根号N)  

    二叉查找树平均深度:O(log N)

    最坏情况:N-1  斜二叉树

 

2. 二叉树

1.1 特殊二叉树

(1)满二叉树

  所有分支结点都有左、右子树,且所有叶子都在同一层

         技术图片

 

   若有 k 层,则共:2^0 + 2^1 +...+ 2^(k-1) = 2^k -1 个结点

 

(2)完全二叉树 

  n个结点,深度为h,除了第 h 层外其它层结点数都达到最大个数,且第 h 层所有结点都集中在左侧

  特点:

    叶子结点只出现在层次最大的两层:h 层和 h-1 层

    对任一结点,若其右子树最大层次为L,则其左子树最大层次为L或L+1

    同样结点的二叉树,完全二叉树的深度最小

           技术图片

 

 

 

(3)二叉排序树Binary sort tree/二叉查找树

  左子树 < 根 < 右子树,对其中序遍历结果是升序

 

(4)线索二叉树

 

(5)平衡二叉树/AVL树 Self-Balancing Binary Search Tree /Height-Balanced Binary Search Tree

  左子树、右子树都是平衡二叉树,且每一个结点的左子树、右子树深度差绝对值小于等于 1

  是高度平衡的二叉排序树

 

(6)红黑树

  自平衡--二叉查找树

 

(7)赫夫曼树 Huffman Tree

  最优树:带权路径长度WPL最小

 

 

1.2 二叉树性质

(1)第 i 层最多有 2^(i-1) 个结点

2)深度为k的二叉树最多(满二叉树)有2^k -1个结点

(3)任二叉树,若其终端结点(度为0的)数为 a0 ,度为2的结点数为 a2,则 a0 = a2 + 1

  设度为1的结点数为 a1,则树的结点数 n = a0 + a1 + a2

  考虑连线的数目(从斜二叉树的角度去想),n个结点的二叉树有 n-1 条线,则 n-1 = 0*a0 + 1*a1 + 2*a2

 

(4)有n个结点的完全二叉树的深度为 [log2 n] + 1     [ x ]表示小于等于x的最大整数,即向下取整

      对于满二叉树,若结点数为m,深度为k,则 m = 2^k -1 或 k=log2(m+1) 。一个同样深度的完全二叉树,其结点数小于等于2^k -1,但必大于2^(k-1)-1,则此完全二叉树的结点数n:

    2^(k-1)-1<n≤2^k -1,即2^(k-1) ≤n<2^k  取对数,则 k-1≤log2n<k

(5)完全二叉树,n个结点,按层序编号(每层从左到右),对任一结点 i(1≤i≤n)有:

                                         技术图片

 

   i == 1,i是根,无双亲          i>1,其双亲是 [i/2] 向下取整

   2i>n,则i无左子         2i≤n,则其左子是2i

  2i+1>n,则i无右子,否则其右子是2i+1

 

 

1.3  存储结构

(1)顺序存储

  如完全二叉树的顺序存储:

       技术图片

 

 

    对于非完全二叉树,可将缺失的位置填上空,其他位置按完全二叉树的层级编号存储即可

    对于右斜二叉树,深度k,节点数k,需要2^k -1 个存储单元,因此这种顺序存储只适合比较严格定义的完全二叉树

 

(2)链式存储---二叉链表

typedef struct BitNode {
    int data;
    struct BitNode *leftChild,*rightChild;
};

                                                                                       技术图片

 

     如果要从子结点找父结点,则可增加一个父指针域

 

1.4 二叉树遍历

(1)前序遍历

  根---左---右

           技术图片

 

   前序遍历:A B D G H C E I F

void preOrderTraverse(BitTree root) {
    if (root) {
        cout<<root->data<<endl;
        preOrderTraverse(root->leftchild);
        preOrderTraverse(root->rightchild);
    }
}

(2)中序遍历

  左--根--右

  遍历结果:G D H B A E I C F

void inOrderTraverse(BitTree root) {
    if (root) {  
        inOrderTraverse(root->leftchild);
        cout<<root->data<<endl;
        inOrderTraverse(root->rightchild);
    }
}

(3)后序遍历

  左--右--根

  遍历结果:G H D B I E F C A

void postOrderTraverse(BitTree root) {
    if (root) {     
        postOrderTraverse(root->leftchild);
        postOrderTraverse(root->rightchild);
        cout<<root->data<<endl;
    }
}

 

(4)层序遍历

               技术图片

  A B C D E F G H I

 

 

 

1.5 线索二叉树

       技术图片

 

   n个结点,则有2n个指针域;     

  有n-1条线,即n-1个指针域是有用的,则浪费了 2n-(n-1)=n+1个指针域

  

  这里共10个结点,则浪费了11个结点

  中序遍历:H D I B J E A F C G

  根据遍历结果可知一个结点的前驱和后继,利用空的指针域来指向前驱或后继(这种指针就是线索)----构成线索二叉树:

                                      技术图片

 

     H的后继是D,则H的右指针指向D;D的后继是I,已经指向了I;I的后继是B,其右指针指向B............................

    即利用右指针指向该结点的后继,本来有右子的结点,其右指针已经是指向其后继(在中序遍历中,右子是后继)。

    再利用空的左指针指向前驱:

                                       技术图片

 

 

    两种结合起来就变成了一个双向链表:

                      技术图片

 

 

    

    为了区分左子与前驱、右子域后继,还需要增加两个标志:lflag、rflag

    增加了空间消耗,但是带来了访问、增删方便的优点

 

(1)结构

typedef enum {Link,Thread} PointerTag;
//第一个枚举成员默认0,Link==0  表示左右子指针
// Thread==1,表示前驱、后继线索
typedef struct BitThrNode
{
    int data;
    struct BitThrNode *lchild,*rchild;
    PointerTag ltag;
    PointerTag rtag; 
}BitThrNode,*BitThrTree;

 

(2)线索化

   中序遍历过程中修改空指针域

         pre和p就建立了前驱后继双向关系           

static BitThrTree pre = nullptr;
void InThreading(BitThrTree p)
{
    if(p) {
        InThreading(p->lchild);
        if(!p->lchild) {
            p->ltag = Thread;
            p->lchild = pre;  //指向前驱
        }
        if(nullptr != pre && nullptr == pre->rchild) {
            pre->rtag = Thread;
            pre->rchild = p; //指向后继
        }
        pre = p;  //保持pre指向p的前驱
        InThreading(p->rchild);
    }
}

 

(3)

  增加一个头结点,头结点左指针域指向根结点,右指针指向中序遍历最后的结点。中序第一个结点左指针域指向头结点,最后一个结点右指针指向头结点。

  这样可从第一个结点起顺着后继遍历,也可从最后一个结点向前顺前驱遍历。

                  技术图片

 

void InorderTraverse_Thr(BitThrTree t)
{
    BitThrTree p = t->lchild; //p是根结点
    if(nullptr == p) {
        return;
    }
    while(p != t) {
        while(p->ltag == Link) {
            p = p->lchild;  //找到最左侧的结点,即中序遍历的第一个结点
        }
        cout<<p->data<<endl; //操作该结点
        while(p->rtag == Thread && p->rchild != t) {
            p = p->rchild;
            cout<<p->data<<endl;
        }
        p = p->rchild;
    }
}

 

 

1.6 AVL树

(1)AVL树概念

  高度平衡的二叉排序树

  平衡因子Balance Factor:结点左子树深度减右子树深度      -1、0、1

  最小不平衡子树:离插入结点最近的,且平衡因子绝对值大于1的结点为根的子树

 

  查找、插入、删除:平均和最坏情况都是O(logn)  

 

(2)二叉排序树---->AVL树

  二叉排序树/二叉查找树 依据二分查找的思想,便于查找数据,但是插入数据时容易出现数据全部在某一遍的情况。

 

(3)导致不平衡的情况以及左旋、右旋

  LL:插入新结点到根结点左子树的左子树,根的BF由1变为2             需要右旋

  RR:插入新结点到根结点右子树的右子树,根BF由-1变为-2             需要左旋

  LR:插入新结点到根结点左子树的右子树,根BF由1变为2                需要先左后右旋转

  RL:插入新结点到根结点右子树的左子树,根BF由-1变为-2              需要先右后左旋转

 

  右旋:插入结点时,结点倾向于左边            顺时针旋转

                 技术图片                     技术图片

 

 

 

 

  左旋:逆时针

                    技术图片

 

 

 

 

 

 

 

 

(4)AVL树实现

  数组a[10]={3,2,1,4,5,6,7,10,9,8}; 

  ①构建二叉排序树,不旋转

         技术图片

 

 

 

  ②AVL树插入过程

           技术图片            技术图片

 

 

  

        技术图片

 

 

        技术图片   

 

 

    技术图片

 

 

                  技术图片

 

                    技术图片    新插入结点9,结点7不平衡需要左旋,但是左旋后9变成了10的右子,不满足二叉排序树了。先对9、10右旋

 

 

 

                 技术图片    技术图片

 

 

 

 

                技术图片  新假如结点8,6的BF为-2,9的BF为1,需要对9右旋           技术图片

 

 

                               技术图片       

 

 

 (5)Code

typedef struct BitNode
{
    int data;
    int bf;
    struct BitNode *lchild,*rchild;
}BitNode,*BitTree;

 

  ①右旋

  主要有两个变化:原来根与其左子树的指向关系----->变成左子树指向根

                                    原来根左子树的右子树变成了根的左子树

              技术图片   在这里看,就是4和6关系的改变,以及5变成了6的左子树

 

  

void R_Rotate(BitTree *p)
{
    BitTree tmp = (*p)->lchild; //先记下根左的位置,则根的左指向关系可以断开了
    (*p)->lchild = tmp->rchild; 
    tmp->rchild = *p;
    *p = tmp;
}

 

  ②左旋

void L_Rotate(BitTree *p)
{
    BitTree tmp = (*p)->rchild;
    (*p)->rchild = tmp->lchild;
    tmp->lchild = (*p);
    *p = tmp;
}

 

  ③左平衡旋转处理

 

 

(6)

  要查找的集合本身没有顺序,需要经常插入、删除操作,则需要构建AVL树。其查找、插入、删除复杂度都为 O(log n)  

 

 

1.7 红黑树

(1)红黑树特点

  ①每个结点要么是红色,要么是黑色

  ②根结点为黑色

  ③叶子结点都是黑色,且为NULL

  ④连接红色结点的两个子结点都是黑色

  ⑤从任意结点出发到每个叶子结点,路径中包含相同数量的黑色结点

  ⑥新加入到红黑树的结点是红色结点

 

  从根结点到叶子结点的最长路径不大于最短路径的2倍

 

(2)

    红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。

  

 

1.8 赫夫曼树

(1)

  权:结点右各自的权值

  树的带权路径长度WPL:树中所有叶子结点带权路径长度之和

          技术图片   WPL=7*1+5*2+2*3+4*3

 

  WPL最小的树就是 最优二叉树/赫夫曼树

  

(2)构建

  权值越大的点离根越近

  ①从n个结点中取出权值最小的两个结点,作为左右子树构成一个二叉树,且其根的权值为左右孩子权值之和

  ②在原来n个结点中去掉这两个结点,并将新生成的二叉树根的权值加到原来的行列中

  重复①②直到只剩下一棵树

                         技术图片

                                  技术图片

                                   技术图片

                                     技术图片

 

 

(3)Code

typedef struct {
    int weight; //权值
    int parent,left,right;
}HTNode,*HuffmanTree;

 

(4)赫夫曼编码

 

 

 

 

1.9  树树、森林、二叉树的转换

(1)树转换为二叉树

  步骤:

    ①加线。所有兄弟结点之间加一条连线

    ②去线。对树中的每个结点,只保留它与它左孩子的连线,删除它与其他孩子结点之间的连线

    ③层次调整。以树的根结点为轴心,将整棵树顺时针旋转使之层次分明。

              技术图片  

                   技术图片

 

     去线后,旋转。

    B的兄弟C变成了B的右孩子。E的兄弟F变成了E的右孩子,F的兄弟G变成了F的右孩子

    C的兄弟D变成C的右孩子。D的左孩子I不变,I的兄弟J变成了I的右孩子

 

(2)森林转换为二叉树

  森林由很多树组成,可理解为森林中每一棵树都是兄弟。

  步骤:

    ①把每棵树变成二叉树

    ②第一课二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子(因为树转换为二叉树后根结点是没有右孩子的如1.9)。

               技术图片

 

               技术图片

 

 

(3)二叉树转换为树

  步骤:

    ①加线。将结点1的左孩子的所有右孩子结点作为结点1的孩子。

    ②去线。删除原二叉树中所有结点与其右孩子结点的连线

    ③层次调整

                          技术图片

 

                        技术图片

 

 

(4)二叉树转换为森林

  能否转换为森林:根结点有右孩子就能转换为森林

  步骤:

    ①从根结点开始,若右孩子存在,则删除与右孩子的连线,查看分离后的二叉树,若根结点右孩子存在则删除连线.....,直到所有右孩子连线都删除为止

    ②将各个树转换为二叉树

            技术图片

 

                 技术图片

 

 (5)树与森林的遍历

  树的遍历:

    ①先根遍历。先访问根结点,然后依次先根遍历根的每棵子树

    ②后根遍历。先依次后根遍历每棵子树,然后访问根结点

               技术图片

 

     先根遍历:ABEFCDG       后根遍历:EFBCGDA

 

  森林的遍历:

    ①前序遍历。先对第一个树先根遍历,然后第二棵树...

    ②后序遍历。对第一棵树后跟遍历,然后第二棵树...

  技术图片

 

   先序:ABCDEFGHJI            后序:BCDAFEJHIG

 

  这个森林对应的二叉树:

      技术图片

 

   前序遍历:ABCDEFGHJI          中序遍历:BCDAFEJHIG

  森林的先序遍历与对于二叉树的前序遍历相同,而森林的后序遍历与对于二叉树的中序遍历相同

 

   

 

 

 

 

 

 

 

 

 

 

 

 

 

 

   

 

以上是关于数据结构------树的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Python 中绘制回归树

LeetCode810. 黑板异或游戏/455. 分发饼干/剑指Offer 53 - I. 在排序数组中查找数字 I/53 - II. 0~n-1中缺失的数字/54. 二叉搜索树的第k大节点(代码片段

使用 Apollo 客户端的片段组合:约定和样板

Discord.py 如何制作干净的对话树?

VSCode自定义代码片段5——HTML元素结构

VSCode自定义代码片段5——HTML元素结构