数据结构------树与二叉树(二)

309 阅读12分钟

基础知识点

  • (一)树的基本概念
  • (二)二叉树
  1. 二叉树的定义及其主要特性
  2. 二叉树的顺序存储结构和链式存储结构
  3. 二叉树的遍历
  4. 线索二叉树的基本概念和构造
  • (三)树、森林
  1. 树的存储结构
  2. 森林与二叉树的转换
  3. 树和森林的遍历
  • (四)树与二叉树的应用
  1. 二叉搜索树
  2. 平衡二叉树
  3. 哈夫曼(Huffman)树和哈夫曼编码树

知识框架

1~K[9E8G40]363ZHOM(GE_7.png

(三)树、森林

1. 树的存储结构

树的存储结构既可以顺序存储结构又可以链式存储结构,要求是能够唯一的反应树中各结点之间的关系。

三种常用的存储结构

  1. 双亲表示法 采用一组连续空间存储每个结点,同时在每个结点中增设一个伪指针用来指示其双亲结点在数组中的位置。其中,根结点下标为 0,其伪指针域为 -1。 20190801035156878.png 双亲表示法的存储结构:
#define MAX_TREE_SIZE 100		//树中最多结点数定义
typedef struct{			        //树的结点定义
	ElemType data;			//数据元素(数据域)
	int parent;			//双亲位置域(数组下标)
}PTNode;				//树结点类型定义
typedef struct{				//树的类型定义
	PTNode nodes[MAX_TREE_SIZE];	//双亲表示数组
	int n;				//结点数计数
}PTree;					//树类型定义

双亲表示法的存储结构利用了每个结点(根结点除外)只有唯一双亲的性质,可以很快得到每个结点的双亲结点,但搜索(求)结点的孩子结点时需要遍历整个树。

  1. 孩子表示法 将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时n个结点就有n个孩子链表(叶子结点的孩子链表为空表)。 这种存储方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历n个结点中孩子链表指针域所指向的n个孩子链表。 20190801040556399.png

  2. 孩子兄弟表示法(二叉树表示法) 孩子兄弟表示法又称为二叉树表示法,是以二叉链表作为树的存储结构,每个结点包括三部分:结点值、指向结点第一个孩子结点的指针和指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)。

20190801043502949.png

孩子兄弟表示法的存储结构:

typedef struct CSNode{
	ElemType data;					//数据域
	struct CSNode *firstchild, *nextsibling;	
	//指向结点第一个孩子结点的指针和指向结点下一个兄弟结点的指针
}CSNode, *CSTree;

Screenshot_20210608_212628.jpg

该存储方式最大的优点是可以方便地实现树转换为二叉树的操作,易于查找结点的孩子结点。但从当前结点查找其双亲结点较为复杂,时间开销较大。若为每个结点增设一个parent域指向其父结点,则查找结点的父结点也很方便。  

2. 森林与二叉树的转换

  • 树转化为二叉树: 树转换为二叉树的规则: 每个结点左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点,即“左孩子右兄弟”。其中,由于根结点没有兄弟,所以由树转换成的二叉树没有右子树。 20190801045052611.png 树转化成二叉树的画法:
  1. 在兄弟结点之间加一连线
  2. 对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉
  3. 以树根为轴心,顺时针旋转45°
  • 森林转化为二叉树: 森林转化为二叉树的画法:
  1. 将森林中的每棵树转换成相应的二叉树
  2. 每棵树的根也可视为兄弟关系,在每棵树之间加一根连线
  3. 以第一棵树的根为轴心顺时针旋转45° Screenshot_20210608_213714.jpg 森林转换为二叉树的规则: 先将森林的每棵树转换为二叉树,再将第一棵树的根作为转换后的二叉树的根,将第一棵树的左子树作为转换后的二叉树的左子树,将第二棵树作为转换后的二叉树的右子树,将第三棵树作为转换后的二叉树的右子树的右子树,以此类推。
  • 二叉树转化为森林: 二叉树转换为森林的规则:

若二叉树非空,则二叉树的根及其左子树转换为第一棵树的二叉树形式,二叉树根的右子树转换为第二棵树的二叉树形式,以此类推直到最后产生一棵没有右子树的二叉树为止。

20190801045951475.png

树的遍历操作:是以某种方式访问树中的每个结点且仅访问一次。

  • 先根遍历: 若树非空,先访问根结点,再按从左到右的顺序遍历根结点的每棵子树了,其访问顺序与这棵树对应二叉树的先序遍历顺序相同;
  • 后根遍历: 若树非空,按从左到右的顺序遍历根结点的每棵子树,之后再访问根结点,其访问顺序与这棵树对应二叉树的中序遍历顺序相同;
  • 层次遍历: 按层序依次访问各结点,与对应二叉树的层次遍历相同。

森林的遍历操作:

先序遍历森林: 若森林非空,

  • 访问森林中第一棵树的根结点;
  • 先序遍历第一棵树中根结点的子树森林;
  • 先序遍历除去第一棵树后的森林。 中序遍历森林: 若森林非空,
  • 中序遍历森林中第一棵树的根结点的子树;
  • 访问第一棵树的根结点;
  • 中序遍历除去第一棵树后的森林。 20190801051454709.png

(四)树与二叉树的应用

1. 二叉搜索树

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

  • 定义 二叉排序树 (BST),又称为二叉查找树。二叉排序树或是空树,或是具有下列特性的非空二叉树:
  1. 若左子树非空,则左子树上所有结点关键字值均小于根结点的关键字值;
  2. 若右子树非空,则右子树上所有结点关键字值均大于根结点的关键字值;
  3. 左右子树本身也是二叉排序树。 左子树结点值<根结点值<右子树结点值,因此对二叉排序树进行中序遍历时,可以得到一个递增的有序序列。
  • 查找 二叉排序树的非递归算法:
BSTNode  *BST_Serach(BiTree T,ElemType key){
    while(T!=NULL&&key!=T->data){       //若树空或等于根结点值,则结束循环
        if(key<T->data)   T=T->lchild; //小于,则在左子树上查找
        else T=T->rchild;              //大于,则在右子树上查找
    }
    return T;
}
  • 插入 二叉排序树的插入

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

int BST_Insert(BiTree &T,KeyType k){
    if(T==NULL){                //原树为空,新插入结点的记录为根结点
      T=(BiTree)malloc(sizzeof(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)
}

20190803085942239.png

  • 构造 二叉排序树的构造算法:

从一棵空树出发,依次输入元素,将他们插入二叉排序树中的合适位置。每读入一个元素,就建立一个新结点,若二叉排序树非空,则将新结点的值与根结点的值进行比较,若小于根结点的值则插入左子树,否则插入插入右子树;若二叉排序树为空,则将新结点作为二叉排序树的根结点。

void Create_BST(BiTree &T,KeyType str[],int n){
   T=NULL;         //初始时T为空树
   int i=0;
   while(i<n){     //依次将每个关键字插入到二叉排序树中
        BST_Insert(T,str[i]);
        i++;
   }
}
  • 删除
  1. 被删除的结点是叶结点: 直接删除,不会破坏二叉树的性质

  2. 被删除的结点含有一棵左子树或右子树: 让z的子树成为父结点z的子树,替代z的位置

  3. 被删除的结点含有左右子树: 令z的直接前驱(或直接后继)替代z,然后从二叉排序树中删除这个直接前驱(或直接后继),这样就转换成了第一种或第二种情况。

20190803163731358.png

  • 查找效率分析 二叉排序树的查找效率,主要取决于树的高度。

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

  • 若树高h,找到最下层的一个结点需要对比h次
  • 最好情况:n个结点的二叉树最小高度为[log2n] + 1,平均查找长度 = O(log2n)
  • 最坏情况:每个结点只有一个分支,树高h = 结点数n,平均查找长度 = O(n)

查找成功的平均查找长度ASL: 每一个查找长度(深度)乘以查找次数除以查找总次数

2. 平衡二叉树

  • 定义 :在插入和删除二叉树结点时,保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树(Balanced Binary Tree),简称平衡树(AVL)。定义结点左、右子树的高度差为该结点的平衡因子,平衡二叉树结点的平衡因子的值只可能为 -1,0,1。

20190803164705267.png

平衡二叉树的平均查找长度为O(log2(n)),如果只有一个右孩子或左孩子的单支树(类似于有序的单链表),平均查找长度为O(n).

  • 平衡二叉树的插入 每当在二叉排序树中插入(或删除)一个结点时,首先检查其插入路径上的结点是否因为该结点的插入而导致不平衡,若不平衡则先找到插入路径上最小不平衡子树(离插入结点最近的、平衡因子绝对值大于 1 的结点,将以该结点为根的子树),保证二叉排序树特性的前提下,调整各结点的位置关系以重新达到新的平衡。 20190803165458804.png

    LL平衡旋转(右单旋转) 20190803170132255.png

    RR平衡旋转(左单旋转)

20190803170545648.png

LR平衡旋转(先左后右双旋转)

2019080317070283.png

RL平衡旋转(先左后右双旋转)

2019080317074819.png

  • 平衡二叉树的查找

平衡二叉树的查找与二叉排序树的查找过程相同。 在查找过程中,与给定值进行比较的关键字个数不超过树的深度。含有n个结点的平衡二叉树的最大深度为O(log2(n)) 因此平衡二叉树的平均查找长度为 O(log2(n))。

3. 哈夫曼(Huffman)树和哈夫曼编码树

哈夫曼定义:

  • 树中结点被赋予的数值,称为该结点的
  • 从根结点到任意结点的路径长度(经过的边数),与该结点的权值的乘积,称为该结点的带权路径长度
  • 树中所有叶子结点的带权路径长度之和称为该树的带权路径长度,记为WPL=∑(i=1--n)wi*li.式中, wi是第i个叶子结点的权值, l i是根结点到第i个叶子结点的路径长度。
  • 在含有n个带权叶子结点的二叉树中,带权路径长度 (WPL) 最小的二叉树称为哈夫曼树,也称为最优二叉树。

20190803172954103.png

20190803173012101.png

其中,(c)树的 WPL 最小,并且为哈弗曼树。

构造:

  • 构造算法描述:
  1. 给定n个权值分别为 w 1 , w 2 , . . . , w n的结点;
  2. 将上述结点分别作为n棵仅含有一个结点的二叉树,构成森林F;
  3. 构造一个新结点,从 F中选取两颗权值最小的树作为新结点的左、右子树构造一棵新树,并将新结点的权值置为左、右子树权值之和;
  4. 从F 中删除选出的两棵树,并将新树加入森林 F 中;
  5. 重复第 3、4 步骤直到森林 F中只剩下一棵树。 由构造过程可得,哈夫曼树具有以下特点:
  • 每个初试结点最终都成为叶子结点,且权值越小的结点到根结点的路径长度越大;
  • 构造过程共新建结点n-1个,因此哈夫曼树的结点总数为2n-1 个;
  • 每次构造新树都选择两棵树作为左右子树,因此哈夫曼树中不存在度为 1 的结点。 20190803174523690.png

哈夫曼编码

  • 前缀编码:如果没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
  • 固定长度编码:在数据通信中,如果对每个字符用相等长度的二进制表示,称这样的编码方式称为固定长度编码。
  • 可变长度编码:如果允许对不同字符用不等长的二进制表示,则这种编码方式称为可变长度编码。 20190803174836502.png

0 和 1 表示左子树还是右子树没有明确规定。因此,左、右结点的顺序是任意的, 所以构造出的哈夫曼树并不唯一, 但是各哈夫曼树的带权路径长度相同且为最优。(权值:出现的频率或次数,依据权值构造哈夫曼树,小的下面大的上面,使得WPL最小,带权路径长度最小即最优。)