数据结构与算法笔记5 树和二叉树

219 阅读13分钟

1. 树的定义

树:从树根生长,逐级分支 树是n(n0) n (n\ge0) 个结点的有限集合,n=0 n= 0 时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足:

  • 1) 有且仅有一个特定的称为根的结点。
  • 2) 当n>1 n \gt 1 时,其余结点可分为 m(m>0) m(m\gt0) 个互不相交的有限集合 T1T_{1}T2T_{2},...,TmT_{m},其中每个集合本身又是一棵树,并且称为根结点的子树。

树是一种递归定义的数据结构 image.png 空树:结点为0的树。

1.1 非空树的特性:

  1. 有且仅有一个根节点(没有前驱的结点)。
  2. 没有后继的结点成为“叶子结点”(或终端结点)
  3. 有后继的结点称为“分支结点”(或非终端结点)
  4. 除了根结点外,任何一个结点都有且仅有一个前驱。
  5. 每个结点可以有0个或多个后继。

1.2 树的逻辑结构的应用

思维导图,文件管理,行政区划等。

1.3 结点之间的关系和路径

路径指的是经过几条边,路径只能往下不能往上。

image.png

1.4 结点和树的属性描述

属性: 结点的层次(深度):从上往下数 image.png

结点的高度:从下往上数

树的高度(深度):总共有多少层

结点的度:有几个孩子(分支)

非叶子结点的度 > 0
叶子结点的度 = 0

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

1.5 有序树 和 无序树

有序树:

   逻辑上看,树中结点的各子树从左至右是有次序的,不能互换。

无序树:

   逻辑上看,树中结点的各子树从左至右是无次序的,不能互换。

1.6 森林

森林是m(m0) m(m\ge0) 棵互不相交的树的集合。

森林和树的相互转换?

1.7 树的常考性质

1.7.1 结点数 = 总度数+1

1.7.2 度为m的树和m叉树的区别

image.png

1.7.3 度为m的树第i层至多有 mi1m^{i-1} 个结点 (i1) (i\ge1)

同样的,m叉树的第i层也至多有 mi1m^{i-1} 个结点 (i1) (i\ge1)

1.7.4 高度为h的m叉树至多有 mh1m1\frac{m^{h}-1}{m-1}

高度为h、度为m的树至少有 h+m1 h +m-1 个结点

高度为h的m叉树至少有 h h 个结点

1.7.5 有n个结点的m叉树的最小高度为logm(n(m1)+1\left \lceil log_{m}(n(m-1)+1 \right \rceil

image.png hmin=logm(n(m1)+1h_{min} = \left \lceil log_{m}(n(m-1)+1 \right \rceil

2. 二叉树

二叉树是n(n0) n(n\ge0) 个结点的有限集合。

    1. 或者为空二叉树,即n=0 n =0
    1. 或者由一个根节点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。

特点:

    1. 每个结点至多只有两棵子树。
  • 2.左右子树不能颠倒(二叉树是有序树)

2.1 二叉树的五种情况

image.png

2.2 几个特殊的二叉树

2.2.1 满二叉树

一棵高度为h,且含有2h1 2^{h}-1 个结点的二叉树。

image.png

2.2.2 完全二叉树。

即高度同满二叉树相同,不含满二叉树的更高次序结点的二叉树,最多只有一个度为1的结点。 满二叉树是一种特殊的完全二叉树,完全二叉树未必是满二叉树。 如果某个结点只有一个孩子,那么必然是左孩子 image.png

2.2.3 二叉排序树

可用于元素的排序或搜索。 一棵二叉树或者是空二叉树,具有如下性质。 左子树上所有结点的关键字均小于根结点的关键字。 右子树上所有结点的关键字均大于根结点的关键字, 左子树和右子树又各是一棵二叉排序树。

2.2.4 平衡二叉树

树上任一结点的左子树和右子树的深度之差不超过1。 平衡二叉树能有更高的搜索效率

2.3 二叉树的常考性质

2.3.1 叶子结点比二分支结点多一个

设非空二叉树中度为0、1和2的结点个数分别为 n0 n_{0}n1 n_{1}n2 n_{2},则 n0=n2+1n_{0}=n_{2}+1

假设树中结点总数为 nn,则

    1. n=n0+n1+n2 n = n_{0}+ n_{1}+ n_{2}
    1. n=n1+2n2+1 n = n_{1}+ 2n_{2}+ 1

两式相减2-1可得出本结论

2.3.2 二叉树的第i层至多有 2i1 2^{i-1} 个结点 (i\ge1)

2.3.3 高度为h的二叉树至多有2h1 2^{h}-1 个结点(满二叉树)

2.3.4 具有n个(n>0)(n>0) 结点的完全二叉树的高度h为log2(n+1)\left \lceil log_{2}(n+1) \right \rceillog2(n)+1\left \lfloor log_{2}(n)+1 \right \rfloor

image.png

image.png

2.3.5 对于完全二叉树,可以由结点数n推出度为0、1和2的结点个数

image.png

3. 二叉树的存储结构

3.1 二叉树的顺序存储

顺序存储结构定义

#define MAXSIZE 100
struct TreeNode
{
    ElemType value;//结点中的数据元素
    bool isEmpty;//结点是否为空
};

TreeNode t[MAXSIZE]

//初始化时,所有结点标记为空
for(int i =0; i < MAXSIZE; i++)
{
    t[i].isEmpty = true;
}

定义一个长度为MAXSIZE的数组t,按照从上至下,从左至右的顺序,依次存储完全二叉树中的各个结点。 image.png

二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。

image.png 若非完全二叉树有n个结点,结点编号为i,其无法用n和i的关系判断第i个结点是否有左孩子、右孩子,因此需要用2i和2i+1 是否 isEmpty来判断。

image.png 二叉树的顺序存储浪费存储空间,只适合存储完全二叉树。

3.2 二叉树的链式存储

链式存储结构定义

//二叉树的结点(链式存储)
typedef struct BiTNode
{
    ElemType data; //数据域
    struct BiTNode *lChild,*rChild;//左、右孩子指针
} BiTNode,*BiTree;

对于n个结点的二叉链表共有n+1个空链域(可以用于构建线索二叉树)

image.png

image.png

4. 二叉树的遍历

遍历:按照某种次序把所有结点都访问一遍。

对于树而言:

  • 存在基于树的层次特性确定的次序规则——层次遍历。
  • 存在基于树的递归特性确定的次序规则——先/中/后序遍历。

先序遍历:根左右

中序遍历:左根右

后序遍历:左右根

4.1 先序遍历

image.png

4.2 中序遍历

image.png

4.3 后序遍历

image.png

4.4 树的遍历的应用

4.4.1 算术表达式的“分析树”转换,中缀表达式需要加界限符

image.png

4.4.2 求树的深度

image.png

4.5 二叉树的层次遍历

image.png 算法思想:

    1. 初始化一个辅助队列
    1. 根结点入队
    1. 若队列非空,则队头元素出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
    1. 重复3直至队列为空

image.png

4.6 由遍历序列构造二叉树

若只给出一棵二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一棵二叉树 关键是找到树的根结点,并根据中序序列划分左右子树,再找到左右子树的根结点。 image.png

4.6.1 前序+中序遍历序列

image.png

4.6.2 后序+中序遍历序列

image.png

4.6.3 层序+中序遍历序列

image.png

5. 线索二叉树

相比普通二叉树,利用二叉树的 n+1 个空链域,可以方便的从一个指定结点出发,找到其前驱、后继;方便遍历。 普通二叉树想找到指定结点的前驱需要再进行一次相应遍历,很不方便。

image.png

image.png

根据中序遍历、先序遍历和后序遍历的不同序列为线索进行“线索化”,可以得到不同的中序、先序和后序线索二叉树。

5.1 二叉树线索化

初步建成的树,ltag和rtag要设为0。

5.1.1 中序线索化

基于找到中序前驱的办法,可以进一步扩展出中序线索化的思想。

image.png 中序线索化的过程就是一遍中序遍历,一遍线索化。

image.png 教材版代码 image.png

以上对pre定义为引用类型,相当于把局部变量的作用范围扩大到全局。

image.png

5.1.2 先序线索化

image.png 上述代码存在问题,第三个结点的左子树线索会指向前驱,从而使得后续遍历在3结点和2结点之间循环,解决方法是在遍历左子树前增加一个对ltag的判断。

image.png 教材版本代码

image.png

5.1.3 后续线索化

image.png 教材版本代码

image.png

5.2 线索二叉树找前驱和后继

5.2.1 中序线索二叉树找中序后继

  1. 若p->rtag == 1 ,则next = p->rchild;
  2. 若p->rtag == 0 ,则next = p的右子树中最左下结点。

image.png 在此基础上,可以利用线索实现非提递归算法,对中序线索二叉树进行中序遍历;空间复杂度O(1) O(1)

ThreadNode *p =Firstnode(T)//返回的是最左下角的结点;
Nextnode(p)//找到p结点的后继结点 

image.png

5.2.2 中序线索二叉树找中序前驱

  1. 若p->ltag ==1 , 则pre = p->lchild;
  2. 若p->ltag ==0 ,则pre = p的左子树中最右下结点

在此基础上,可以对中序线索二叉树进行逆向的中序遍历

image.png

5.2.3 先序线索二叉树找先序后继

image.png

5.2.4 先序线索二叉树找先序前驱

image.png 如果能找到p的父节点,则存在四种情况

image.png

5.2.5 后续线索二叉树找后序前驱

image.png

5.2.6 后续线索二叉树找后续后继

image.png 如果能找到p的父节点,则存在四种情况

image.png

image.png

6. 树的存储结构

6.1 双亲表示法(顺序存储)

image.png

1) 增:新增数据元素,无需按照逻辑上的次序存储

2) 删:根据删除的是否叶子结点采用不同方法,更改结点数

如果删除的是叶子结点:

  1. 方案1:将该元素删除,“指针”设为 -1
  2. 方案2:将下方的元素挪到要删除元素的位置 删除的不是叶子结点 需要再通过从头遍历查询指定结点

image.png

二叉树也可以用双亲表示法 ,原有的二叉树的结点编号和完全二叉树对应起来的顺序表示法,结点编号不仅反映了存储位置,也隐含了结点之间的逻辑关系。

6.2 孩子表示法(顺序存储+链式存储)

顺序存储各个结点,每个结点中保存孩子链表的表头指针。

image.png

6.3 孩子兄弟表示法(链式存储)

类比二叉链表,结点的左指针指向第一个孩子,右指针指向自己的右兄弟。

image.png

6.4 树和二叉树的转换

用“孩子兄弟表示法”存储的数,在物理上呈现出“二叉树的样子”,一个结点的右边依次往下都是它的“兄弟”,左边是它的“孩子”。

image.png

6.5 森林和二叉树的转换(也是左孩子右兄弟)

森林是mm0 m (m\ge0) 棵互不相交的树的集合。 可以把各个树的根结点视为兄弟关系。 本质:用二叉链表存储森林 image.png

7. 树的遍历

7.1 树的先根遍历

若树非空,先访问根结点,再依次对每棵子树进行先根遍历。 树的先根遍历序列和这棵树对应相应二叉树的先序序列遍历相同

//树的先根遍历伪代码
void PreOrder(TreeNode *R)
{
    if(R != NULL)
    {
        visit(R);//访问根结点
        while(R还有下一个子树T)
            PreOrder(T)'//先根遍历下一棵子树
    }
}

image.png

7.2 树的后根遍历

若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。 树的后根遍历序列和这棵树对应相应二叉树的中序序列遍历相同

image.png

7.3 树的层次遍历(用队列实现)【广度优先遍历】

    1. 若树非空,则根结点入队
    1. 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
    1. 重复(2)直到队列为空 树的先根遍历和后根遍历都可以称为深度优先遍历

8. 森林

森林是mm0 m (m\ge0) 棵互不相交的树的集合。每棵树去掉根结点后,其各个子树又组成森林。

因此也可以对森林运用递归的思想进行遍历。

8.1 森林的先序遍历

    1. 若森林为非空,访问森林中第一棵树的根结点。
    1. 先序遍历第一棵中根结点的子树森林。
    1. 先序遍历出去第一棵树之后剩余的树构成的森林。

效果等同于依次对各个子树进行先根遍历。效果也等同于依次对相应二叉树的先序遍历。

image.png

8.2 森林的中序遍历

若森林为非空,则按如下规则进行遍历:

    1. 中序遍历森林中第一棵树的根结点的子树森林;
    1. 访问第一棵树的根结点。
    1. 中序遍历除去第一棵树之后剩余的树构成的森林。

效果等同于一次对各个树进行后根遍历。等同于依次对二叉树的中序遍历。

image.png

9. 二叉排序树(二叉查找树) BST

具有如下性质的二叉树或空二叉树:

    1. 左子树上所有结点的关键字均小于根结点的关键字;
    1. 右子树上所有结点的关键字均大于根结点的关键字;
    1. 左子树和右子树又各是一棵二叉排序树。

即 左子树结点值<根结点值<右子树结点值

进行中序遍历,可以得到一个递增的有序序列

二叉排序树可用于元素的有序存放,搜索

9.1 二叉排序树的查找

1) 非递归实现(最坏空间复杂度 O(1) O(1)

image.png

2) 递归实现(最坏空间复杂度O(h) O(h)

image.png

9.2 二叉排序树的插入

    1. 若原二叉排序树为空,则直接插入节点;
    1. 否则,若关键字k小于根结点值,则插入到左子树,
    1. 若关键字k大于根结点值,则插入到右子树。

1) 递归实现(最坏空间复杂度OhO(h)

//在二叉排序树插入关键字为k的新结点(递归实现)
int BST_Insert(BSTree &T,int k) //此处使用引用,因为要改变t的指针
{
    if(T == NULL)
    {
        T = (BSTree) malloc (sizeof(BSTNode));
        T->key = k;
        T->lchild=T->rchild=NULL;
        return 1;       //返回1,插入成功
    }
    else if(k == T->key)//树中存在相同关键字的结点,插入失败
        return 0;
    else if(k < T->key) //插入到T的左子树
        return BST_Insert(T->lchild,k);
    else                //插入到T的右子树
        return BST_Insert(T->rchild,k); 
}

2)非递归实现

参考上文。

9.3 二叉排序树的构造

即依次将关键字插入到二叉排序树中

例如:按照str[ ] 中的关键字序列建立二叉排序树

image.png

9.4 二叉排序树的删除

先搜索找到目标结点:

    1. 若被删除节点z是叶结点,则直接删除,不会破坏二叉排序树的性质。
    1. 若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。
    1. 若结点z有左、右两棵子树,则令z的(中序遍历的)直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。

1)用直接后继替代

image.png

2)用直接前驱替代

image.png

9.5 查找效率分析(时间复杂度最坏 Oh O(h))查找效率取决于树高

查找长度:在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度。

度量整棵排序树的查找长度用平均查找长度ASL来度量。

image.png

对二叉排序树要尽可能让树的高度尽可能保持平衡

image.png

10. 平衡二叉树 AVL

10.1 平衡二叉树定义

树上任一接电脑的左子树和右子树的高度之差不超过1。

结点的平衡因子:左子树高 - 右子树高。

平衡二叉树的结点的平衡因子的值只可能是 -1, 0 或者 1。

平衡二叉树结点

typedef struct AVLNode
{
    int key; //数据域
    int balance; //平衡因子
    struct AVLNode * lchild,* rchild;
}AVLNode;*AVLTree;

10.2 平衡二叉树的插入

在平衡二叉树中插入新结点后,查找路径上所有结点都可能受到影响。

需要从插入点往回找到第一个不平衡结点,调整以该节点为根的子树,即调整最小不平衡子树

10.2.1 调整最小不平衡子树

1)调整最小不平衡子树(LL)

在A的左孩子的左子树中插入导致不平衡

LL平衡旋转:右单旋转。

image.png

2)调整最小不平衡子树(RR)

在A的右孩子的右子树中插入导致不平衡

RR平衡旋转:左单旋转。

image.png

右旋和左旋操作的代码实现思路

image.png

3)调整最小不平衡子树(LR)

在A的左孩子的右子树中插入导致不平衡

LR平衡旋转:先左后右旋转。

image.png

image.png

4)调整最小不平衡子树(RL)

在A的右孩子的左子树中插入导致不平衡

RL平衡旋转:先右后左旋转。

image.png

只有左孩子才能右上旋,只有右孩子才能左上旋

10.2.2 查找效率分析

若树高为h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h) O(h)

平衡二叉树:假设以nh n_{h} 表示深度为h h 的平衡树中含有的最少结点数。则有n0=0 n_{0}=0, n1=1 n_{1}=1,n2=2 n_{2}=2, 并且有nh=nh1+nh2+1n_{h}=n_{h-1}+n_{h-2}+1

可以证明含有nn 个结点的平衡二叉树的最大深度为O(log2n) O(log_{2}n),平衡二叉树的平均查找长度为O(log2n) O(log_{2}n)

11. 哈夫曼树

结点的权:有某种现实含义的数值(如:表示结点的重要性等)

结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积

树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length)

WPL=i=1nwiliWPL=\sum_{i=1}^{n}w_{i}l_{i}

11.1 哈夫曼树(最优二叉树)

在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。

image.png

11.2 哈夫曼树的构造

给定n个权值为别为w1w_{1}w2w_{2},...,wnw_{n} 的结点,构造哈夫曼树的算法描述如下: 1)将这n个结点分别作为n棵仅含一个结点的二叉树,构造森林F。 2)构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。 3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。 4)重复步骤2)和3),直至F中只剩下一棵树为止。

image.png 特点

    1. 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
    1. 哈夫曼树的结点总数为2n-1
    1. 哈夫曼树中不存在度为1的结点
    1. 哈夫曼树并不唯一,但WPL必然相同且为最优

11.3 哈夫曼编码

固定长度编码——每个字符用相等长度的二进制位表示。

image.png 可变长度编码——允许对不同字符用不等长的二进制位表示。

若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码

前缀码解码无歧义,非前缀码解码可能存在歧义 image.png

由哈夫曼树得到哈夫曼编码——字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树。

哈夫曼树不唯一,因此哈夫曼编码不唯一,可以用于数据的压缩。