树与二叉树

github仓库:https://github.com/EanoJiang/Data-structures-and-algorithms

定义

1744801497093

结点的度数其实就是这个结点下连的线,比如:A的度 = AB+AC+AD = 3

树的度就是MAX(结点的度)

叶子结点就是没后代的结点

树的基本性质

  1. 所有结点数 = 所有结点的度数之和 + 1(这个1也就是根节点)

    习题:

    1744802125869

    所有节点数 =4 + 20 * 4 + 10 * 3 + 1 * 2 + 10 * 1 + 1 = 123

    123 - 20 - 10 - 1 - 10 = 82

  2. 所有结点数 = 不同度的节点数 之和

    假设所有节点数为n,度0~4的个数为n0~n4,则n = n0 + n1 + n2 + n3 + n4

  3. 1744802850820

    第一层m0,第二层m1,第i层m^(i-1)

  4. 1744802831067

    等比数列求和公式

二叉树

定义

1744803001113

就是每个分支只有二叉的树,子树有左右之分

基本形态

1744803072638

二叉树的性质

  1. 1744803146039

    这是某一层的最多结点数

  2. 1744803158490

  • 这是整个二叉树的最多结点数
  1. n0 = n2 + 1

    对于任何非空的二叉树,度0(叶子)和度2的结点数为n0、n2,那么 n0 = n2 + 1
    n2 = 0 的时候,n0 = 1,每把一个叶子画一个二叉,n2++,n0++

    n = 1 * n1 + 2 * n2 + 1
    n = n0 + n1 + n2
    解得,n0 = n2 + 1

特殊二叉树

满二叉树

1745893049718

完全二叉树

1745893151509

没有左子树,不能有右子树,上一层没铺满,不能有下一层

判断完全二叉树

1745893303713

不是

1745893334094

完全二叉树的性质

1.2.3.就是二叉树的性质

  1. 1745893412155
  2. 1745893420252
  3. 如果总结点数-1是奇数,说明有一个度为1的结点

习题

1745894549231

叶子结点出现在最后2层,这里求的是最多,所以最后一层是第7层

第6层最多的结点数=2^(6-1)=32

第6层的非叶子结点=32-8=24

第7层的最多结点数= 24*2 = 48

前6层的最多结点数=2^6 - 1 = 63

总计=63+48 = 111,选c

1745894768771

n = n0+n1+n2

n0=n2+1

所以,n = 2n2 + n1 + 1

而768-1是奇数,所以有一个度为1的结点,即n1 = 1

解得 n2 = 383

n0 = n2 + 1 = 384,选c

1746142966459

n=n0+n1+n2

n1 = 0

n0=n2+1

所以,n = 2n0 - 1 = 2k -1 ,选a

二叉树的实现

顺序结构实现——除了满二叉树和完全二叉树的其他场景比较浪费空间

链式结构实现

1746143423026

//链式结构实现
typedef char ElemType;

//树结点
typedef struct TreeNode {
    ElemType data;
    TreeNode *lchild;
    TreeNode *rchild;
} TreeNode;

//用树结点指针表示二叉树
typedef TreeNode* BiTree;

创建二叉树

二级指针概念:

1746144696942

指针pp 存的是 指针p的地址

那么*pp 就是 得到p的地址,**pp就是得到p的值

char str[] = "ABDH#K###E##CFI###G#J##";
int idx = 0;

//创建二叉树
// T是二级指针(BiTree**)
//  *T就是对二叉树的结点进行操作
void createTree(BiTree *T)
{
 ElemType ch;
 ch = str[idx++];
 if (ch == '#')
 {
  *T = NULL;
 }
 else
 {
  *T = (BiTree)malloc(sizeof(TreeNode));
  (*T)->data = ch;
  createTree(& ( (*T)->lchild ) );
  createTree(& ( (*T)->rchild ) );
 }
}

遍历

前序遍历

NLR / NRL (根-左-右)/(根-右-左)

不特别说明,一般情况都是先左的遍历

从根节点开始,先从左子结点开始一层层向下递(进栈)并打印,如果左子节点是空就归(出栈),然后开始递右结点,进行如上同样操作,并向上一层层归,归到根节点后,对根节点的右子节点进行同样的操作。

具体动画演示可以看《数据结构(C 语言描述)》的55:00左右进度条

//前序遍历
void preOrder(BiTree T){
    if(T == NULL) return;
    printf("%c ", T->data);
    //递归子树
    preOrder(T->lchild);
    preOrder(T->rchild);
}

中序遍历

LNR / RNL

从根节点开始,先从左子结点开始一层层向下递(进栈),如果左子节点是空就归(出栈)并打印,然后开始递右结点,进行如上同样操作,并向上一层层归,归到根节点后,对根节点的右子节点进行同样的操作。

//中序遍历
void inOrder(BiTree T){
    if(T == NULL) return;
    inOrder(T->lchild);
    printf("%c ", T->data);
    inOrder(T->rchild);
}

后序遍历

LRN / RLN

从根节点开始,先从左子结点开始一层层向下递(进栈),如果左子节点是空就归(出栈),然后开始递右结点,进行如上同样操作,在右结点空的时候归(出栈)并打印,并向上一层层归,归到根节点后,对根节点的右子节点进行同样的操作。

//后序遍历
void postOrder(BiTree T){
    if(T == NULL) return;
    postOrder(T->lchild);
    postOrder(T->rchild);
    printf("%c ", T->data);
}

习题

1746150964391

前:ABDHEICFGJK

中:HDBEIAFCJGK

后:HDIEBFJKGCA

1746151239563

前:ABDEGHCFI

中:DBGEHAFIC

后:DGHEBIFCA

1746151842540

先画出二叉树:

  A
 / \
       B   D
      /   / \  
     C   E   F

后:CBEFDA

二叉树遍历性质

1746152045010

习题

1746152218722

先右后左的中序遍历,RNL,选d

1746152503875

1746153011612

ADB都能画出来,所以选c

1746153077759

 1
         \
          2
           \
            3

选b

(这题不要多选,一般情况只需要考虑先左就行)

1746153908485

  f
       /   \
      c     g
       \   /
        a d
       /   \
      e     b

选b

1746154136577

因为对于顺序结构来说,没有的子树节点需要填NULL,所以相当于是高度为5的满二叉树需要的存储单元,也就是二叉树的最大结点数公式,即2^5 - 1 = 31

线索二叉树

1746154659118

目标:构建一个双向循环链表

1746154913322

会出现空余空间不够用的情况吗?

不会,n个节点有n+1个空

代码实现

1746157926154

typedef char ElemType;

typedef struct ThreadNode {
    ElemType data;
    struct ThreadNode *lchild, *rchild;
    int ltag, rtag;
} ThreadNode;

typedef ThreadNode *ThreadTree;

ltag:0 指向lchild,1指向前驱

rtag:0 指向rchild,1指向后继

中序遍历线索化

1746158328489

#include 
#include 

typedef char ElemType;

typedef struct ThreadNode {
    ElemType data;
    struct ThreadNode *lchild, *rchild;
    int ltag, rtag;
} ThreadNode;

typedef ThreadNode *ThreadTree;

char str[] = "ABDH##I##EJ###CF##G##";
int idx = 0;

ThreadTree prev;

//创建二叉树
void createTree(ThreadTree *T){
    ElemType ch;
 ch = str[idx++];
 if (ch == '#')
 {
  *T = NULL;
 }
 else
 {
  *T = (ThreadTree)malloc(sizeof(ThreadNode));
  (*T)->data = ch;

  createTree(& ( (*T)->lchild ) );
        //lchild有左孩子,则ltag=0
        if( (*T)->lchild != NULL){
            (*T)->ltag = 0;
        }

        createTree(& ( (*T)->rchild ) );
        if( (*T)->rchild != NULL){
            (*T)->rtag = 0;
        }
 }
}

//线索化——加前驱后继的逻辑
void threading(ThreadTree T){
    if(T != NULL){
        //一直往左边遍历
        threading(T->lchild);
        //当前结点的左孩子为空,当前结点的左孩子设定为指向前驱
        if(T->lchild == NULL){
            T->ltag = 1;
            T->lchild = prev;
        }
        //前驱结点的右孩子为空,前驱结点的右孩子设定为指向当前结点(当前结点就是前驱节点的后继)
        if(prev->rchild == NULL){
            prev->rtag = 1;
            prev->rchild = T;
        }
        //更新prev到根节点,往右边遍历
        prev = T;
        threading(T->rchild);
    }
}

//中序遍历线索化
void inOrderThreading(ThreadTree *head ,ThreadTree T){
    *head = (ThreadTree)malloc(sizeof(ThreadNode));
    (*head)->ltag = 0;
    (*head)->rtag = 1;
    (*head)->rchild = (*head);
    if(T == NULL){
        (*head)->lchild = (*head);
    }
    else{
        //头节点的左孩子指向树的根节点
        (*head)->lchild = T;

        //prev:上一个访问的节点是头节点
        prev = (*head);

        //加前驱后继的逻辑
        threading(T);

        //最后一个节点的右孩子指向头节点
        prev->rtag = 1;
        prev->rchild = (*head);

        //头节点的右孩子指向遍历的最后一个节点
        (*head)->rchild = prev;

    }
}

//基于线索的中序遍历
void inOrder(ThreadTree T){
    ThreadTree current = T->lchild;
    while(current != T){
        //如果当前节点有左孩子,则一直往左边遍历
        //没有左孩子,则退出当前循环 输出当前节点
        while(current->ltag == 0){
            current = current->lchild;
        }
        printf("%c",current->data);
        //往右边遍历, 直到右孩子不为空且当前的右孩子是头节点
        while(current->rtag == 1 && current->rchild != T){
            current = current->rchild;
            printf("%c",current->data);
        }
        current = current->rchild;
    }
    printf("\n");
}

int main(){
    ThreadTree head;
    ThreadTree T;
    createTree(&T);
    inOrderThreading(&head,T);
    inOrder(head);
    return 0;
}

习题

1746160668978

后序遍历:dbca

左虚线是前驱,右虚线是后继

根节点的前驱是头节点NULL

选D

1746161131561

 根节点
         / \
        Y   X

后序遍历:YX根

右虚线是后继,所以X的右线索指向根,也就是父节点

选A

1746161359418

中序遍历:debxac

左虚线是前驱,右虚线是后继

所以b、a

选D

哈夫曼树

为什么要学哈夫曼树?

1746161652072

对于这样一个问题,通常用if分支表示

1746161690330

效率很低啊,有没有效率高的方式呢?

有的兄弟有的🤡

基本概念

1746161736209

路径:两个结点之间经过的分支

路径长度:路径上的分支数,也就是看这条路径上有几条线

树的路径长度:从根结点到每一个结点的路径长度之和

结点的权:权重

结点的带权路径长度:从该结点到树根之间的路径长度 * 该结点的权

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

计算WPL

1746185389854

构造哈夫曼树

  1. 先把有权值的叶子结点从小到大排列,形成有序序列
    1746185596032

  2. 取2个最小权值的结点作为新结点N1的子结点,新结点N1的权值就是这2个最小权值的和
    1746185708799

  3. 把N1替换取出的2个结点,加入到有序序列中重新排列1746185794238

  4. 回到步骤2重复操作(取2个最小权值结点,作为新结点)

    1746185936929

    1746185988567

    1746186003996

    1746186118191

本质:让权重大的结点更靠近根结点,这样算WPL的时候就可以做到大权重乘小路径长度。

哈夫曼树的性质

  1. 哈夫曼树是WPL最小的二叉树
  2. 哈夫曼树只有度0(叶子)和度2的结点
  3. 哈夫曼树的叶子结点数为n,那么共有2n-1个结点

n = n0 + n2

n0 = n2 + 1

所以,n = 2 n0 - 1

哈夫曼编码

对于如下的表格:1746186959695

画出哈夫曼树,然后左0右1标号

(也可以是左1右0)

1746186993488

哈夫曼编码结果:

1746187088055

对比原编码,哈夫曼编码显然效率更高

1746187117239

习题

1746187146697

哈夫曼树不一定是完全二叉树,因为不满足 上一层没铺满不能有下一层

选A

1746187442859

前缀编码:任一编码都不是其他编码的前缀

ABC都满足前缀编码

D中110是1100的前缀,选D

1746187642979

0100 011 001 001 011 11 0101
a f e e f g d

选D

1746187844837

选A

(自己画一下)

1746188144533

相当于叶子结点n,总结点115

115 = 2n - 1

解得n = 58

选C

1746188584982

画出哈夫曼树,把每个叶子结点*路径长度加到一起

WPL = 16 2* + 21 * 2 +30 * 2 + 10 * 3 + 12 * 3 = 200

选B

树与二叉树的转换

树-->二叉树

1746188937392

1746188977652

二叉树-->树

1746189259111

1746189273570

森林转二叉树

森林-->二叉树

  1. 把每个树各自转成二叉树

    1746189700226

    1. 所有兄弟结点连线

    2. 只保留每个结点与第一个孩子的连线

      1746189831039

    3. 旋转

      1746189856440

    4. 后面的树也这样操作

      1746189901115

  2. 整合成一个二叉树

    1746190052479

    1746190071406

    1746190082245

详细操作看《数据结构(C 语言描述)》的53:40左右进度条

二叉树-->森林

  1. 拆成多个二叉树
    1. 从根结点开始,右结点存在就删去与右孩子的连线

      1746190229686
      1746190259177

  2. 每个 二叉树->树
    1. 从根结点开始,若结点的左孩存在,就把该结点与左孩的所有右孩相连

    2. 删去兄弟结点的连线

      1746190569006

    3. 对每个二叉树做同样操作

    4. 旋转
      1746190611787

树的层序遍历

综合应用题

详见《数据结构(C 语言描述)》第11集

From:https://www.cnblogs.com/eanojiang/p/18857797
EanoJiang
100+评论
captcha