数据结构和算法:树

08_树

标签(空格分隔): 数据结构和算法


8.1 树

8.1.1 树的定义

  • 树(Tree)是n(n>=0)个结点的有限集。当n=0时,成为空树,在任意一棵非空树中:
    • 有且仅有一个特定的称为根(Root)的结点;
    • 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

Tips:

  • n>0时,根结点是唯一的,坚决不可能存在多个根结点
  • m>0时,子树的个数是没有限制的,但它们互相是一定不会相交的

8.1.2 结点分类

  • 结点拥有的子树数称为结点的度(Degree),树的度取树内各结点的度的最大值。
    • 度为0的结点称为叶结点(Leaf)或终端结点;
    • 度不为0的结点称为分支结点或非终端结点,除根结点外,分支结点也称为内部结点。

8.1.3 结点间的关系

  • 结点的子树的根称为结点的孩子(child),相应的,该结点称为孩子的双亲(Parent),同一双亲的孩子之间互称为兄弟(Sibling)。
  • 结点的祖先是从根到该结点所经分支上的所有结点。

8.1.4 结点的层次

  • 结点的层次(Level)从根开始,根为第一层,根的孩子为第二层。
  • 其双亲在同一层的结点互为堂兄弟。
  • 树中结点的最大层次称为树的深度(Depth)或高度。

8.2 树的存储结构

8.2.1 双亲表示法

  • 双亲表示法,言外之意就是以双亲作为索引的关键词的一种存储方式。

  • 假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示其双亲结点在数组中位置的元素。

  • 可以根据某结点的parent指针找到它的双亲结点,所用时间复杂度是O(1),索引到parent的值为-1时,表示找到了树的根。

  • 如果想要知道某结点的孩子,需要遍历整个树结构。

  • 代码实现

//树的双亲表示法结点结构定义
#define MAX_TREE_SIZE 100

typedef int ElemType;

typedef struct PTNode
{
	ElemType data; //结点数据
	int parent;    //双亲位置
}PTNode;

typedef struct 
{
	PTNode nodes[MAX_TREE_SIZE];
	int r;         //根的位置
	int n;         //结点数目
}PTree;

8.2.2 孩子双亲表示法

#define MAX_TREE_SIZE 100

typedef char ElemType;

//孩子结点
typedef struct CTNode
{
	int child;  //孩子结点的下标
	struct CTNode *next; //指向下一个孩子结点的指针
} *ChildPtr;

//表头结构
typedef struct 
{
	ElemType data;  //存放在树中的结点的数据
	int parent;     //存放双亲的下标
	ChildPtr firstchild;  //指向第一个孩子的指针
} CTBox;

//树结构
typedef struct 
{
	CTBox nodes[MAX_TREE_SIZE]; //结点数组
	int r,n;
};

8.3 二叉树

8.3.1 二叉树定义

  • 二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。

8.3.2 二叉树的特点

  • 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。(不是都需要两棵子树,而是最多可以是两棵,没有子树或者有一棵子树也都是可以的。)
  • 左子树和右子树是有顺序的,次序不能颠倒。
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

8.3.3 二叉树的五种基本形态

  • 空二叉树
  • 只有一个根结点
  • 根结点只有左子树
  • 根结点只有右子树
  • 根结点既有左子树又有右子树

8.3.4 特殊二叉树

  • 拥有三个结点的普通树只有两种情况:两层或者三层。但对于二叉树来说,由于要区分左右,所以就演变成五种形态。

  • 斜树

  • 满二叉树

    • 定义
      • 在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
    • 特点
      • 叶子只能出现在最下一层
      • 非叶子结点的度一定是2
      • 在同样深度的二叉树中,满二叉树的结点个数一定最多,同时叶子也是最多。
  • 完全二叉树

    • 定义
      • 对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点位置完全相同,则这棵二叉树称为完全二叉树。
    • 特点
      • 叶子结点只能出现在最下两层
      • 最下层的叶子一定集中在左部连续位置
      • 倒数第二层,若有叶子结点,一定都在右部连续位置
      • 如果结点度为1,则该结点只有左孩子
      • 同样结点数的二叉树,完全二叉树的深度最小
    • 注意
      • 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树

8.3.5 二叉树的性质

性质一
  • 在二叉树的第i层上至多有2^(i-1)个结点(i>=1)
性质二
  • 深度为k的二叉树至多有2^k - 1 个结点(k>=1)
性质三
  • 对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1
性质四
  • 具有n个结点的完全二叉树的深度为[log2n]+1
性质五
  • 如果对一棵有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号,对任一结点i(1<=i<=n)有以下性质
    • 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2](取下限)
    • 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
    • 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1

8.4 二叉树的存储结构

8.4.1 二叉树的顺序存储结构

  • 二叉树的顺序存储结构就是用一堆数组存储二叉树中的各个结点,并且结点的存储位置能体现结点之间的逻辑关系。
元素ABCDEF^
下标1234567

8.4.2 二叉链表

  • 二叉树的存储一般采用链式存储结构
  • 二叉树的每个结点最多有两个孩子,所以可以设计一个数据域和两个指针域,我们称这样的链表叫做二叉链表
lchilddatarchild
  • 结构定义代码
typedef struct BiTNode
{
	ElemType data;
	struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

8.5 二叉树的遍历

  • 二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
  • 二叉树的遍历次序不同于线性结构,线性结构最多也就是分为顺序、循环、双向等简单的遍历方式。
  • 树的结点之间不存在唯一的前驱和后继这样的关系,在访问一个结点后,下一个被访问的结点,面临着不同的选择。

8.5.1 二叉树的遍历方法

  • 二叉树的遍历方式可以很多,如果我们限制了从左到右的习惯方式,那么主要就分为以下四种:
    • 前序遍历
    • 中序遍历
    • 后序遍历
    • 层序遍历
前序遍历
  • 若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。

  • 遍历的顺序为:ABDHIEJCFKG

中序遍历
  • 若二叉树为空,则空操作返回,否则先中序遍历左子树,然后访问根结点,再中序遍历右子树。

  • 遍历的顺序为:HDIBEJAFKCG

后序遍历
  • 若树为空,则空操作返回,否则先从左到右先叶子后结点的方式遍历左右子树,最后访问根结点。

  • 遍历的顺序为:HIDJEBKFGCA

层序遍历
  • 若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。

8.5.2 二叉树的建立和遍历算法

  • 题目要求:建立二叉树并输出每个字符所在的层数。如右图要求输出

    • A 在第 一 层
    • B、C 在第 二 层
    • D、E 在第 三 层
  • 代码实现

//前序遍历

#include <stdio.h>
#include <stdlib.h>

typedef char ElemType;

typedef struct BiTNode
{
	char data;
	struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

//创建一棵二叉树,约定用户遵照前序遍历的方式输入数据
CreateBiTree(BiTree *T)
{
	char c;

	scanf("%c", &c);
	if( ' ' == c )
	{
		*T = NULL;
	}
	else
	{
		*T = (BiTNode *)malloc(sizeof(BiTNode));
		(*T)->data = c;
		CreateBiTree(&(*T)->lchild);
		CreateBiTree(&(*T)->rchild);
	}
}

//访问二叉树结点的具体操作
visit(char c, int level)
{
	printf("%c 位于第 %d 层\n", c, level);
}

//前序遍历二叉树
PreOrderTraverse(BiTree T, int level)
{
	if( T )
	{
		visit(T->data, level);
		PreOrderTraverse(T->lchild, level+1);
		PreOrderTraverse(T->rchild, level+1);
	}
}

int main()
{
	int level = 1;
	BiTree T = NULL;

	CreateBiTree(&T);
	PreOrderTraverse(T, level);

	return 0;
}

8.6 线索二叉树

lchildltagdatartagrchild
  • ltag为0时指向该结点的左孩子,为1时指向该结点的前驱
  • rtag为0时指向该结点的右孩子,为1时指向该结点的后继
#include <stdio.h>
#include <stdlib.h>

typedef char ElemType;

//线索存储标志位
//Link(0):表示指向左右孩子的指针
//Thread(1):表示指向前驱后继的线索
typedef enum {Link, Thread} PointerTag;
typedef struct BiThrNode
{
	char data;
	struct BiThrNode *lchild, *rchild;
	PointerTag ltag;
	PointerTag rtag;
} BiThrNode, *BiThrTree;

//全局变量,始终指向刚刚访问过的结点
BiThrTree pre;

//创建一棵二叉树,约定用户遵照前序遍历的方式输入数据
void CreateBiThrTree(BiThrTree *T)
{
	char c;

	scanf("%c", &c);
	if( ' ' == c )
	{
		*T = NULL;
	}
	else
	{
		*T = (BiThrNode *)malloc(sizeof(BiThrNode));
		(*T)->data = c;
		(*T)->ltag = Link;
		(*T)->rtag = Link;

		CreateBiThrTree(&(*T)->lchild);
		CreateBiThrTree(&(*T)->rchild);
	}
}

//中序遍历线索化
void InThreading(BiThrTree T)
{
	if( T )
	{
		InThreading( T->lchild );  //递归左孩子线索化

		//结点处理

		//如果该结点没有左孩子,设置ltag为Thread,并把lchild指向刚刚访问的结点
		if( !T->lchild )
		{
			T->ltag = Thread;
			T->lchild = pre;
		}

		if( !pre->rchild )
		{
			pre->rtag = Thread;
			pre->rchild = T;
		}

		pre = T; 

		InThreading( T->rchild );  //递归右孩子线索化

	}
}

void InOrderThreading( BiThrTree *p, BiThrTree T )
{
	*p = (BiThrTree)malloc(sizeof(BiThrNode));
	(*p)->ltag = Link;
	(*p)->rtag = Thread;
	(*p)->rchild = *p;
	if( !T )
	{
		(*p)->lchild = *p;
	}
	else
	{
		(*p)->lchild = T;
		pre = *p;
		InThreading( T );
		pre->rchild = *p;
		pre->rtag = Thread;
		(*p)->rchild = pre;
	}
}

void visit( char c )
{
	printf("%c", c );
}

//中序遍历二叉树,非递归
void InOrderTraverse( BiThrTree T )
{
	BiThrTree p;
	p = T->lchild;

	while( p != T )
	{
		while( p->ltag == Link )
		{
			p = p->lchild;
		}
		visit( p->data );

		while( p->rtag == Thread && p->rchild != T )
		{
			p = p->rchild;
			visit(p->data);
		}

		p = p->rchild;
	}
} 

int main()
{
	BiThrTree P, T = NULL;

	CreateBiThrTree( &T );

	InOrderThreading( &P, T );

	printf("中序遍历输出结果为: \n");

	InOrderTraverse( P );
	
	return 0;
} 

8.7 树、森林及二叉树的相互转换

8.7.1 树到二叉树的转换

  • 加线,在树中所有兄弟结点之间加一连线
  • 去线,对每个结点,除了保留与其长子的连线外,去掉该结点与其他孩子的连线
  • 层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明

8.7.2 森林转换为二叉树

  • 先将森林中的每棵树变为二叉树
  • 再将各二叉树的根结点视为兄弟从左到右连在一起,就形成了一棵二叉树

8.7.3 二叉树到树、森林的转换

  • 若结点x是其双亲y的左孩子,则把x的右孩子,右孩子的右孩子,……,都与y用连线连起来
  • 去掉所有双亲到右孩子之间的连线

8.8 树与森林的遍历

8.8.1 树的遍历

  • 树的遍历分为两种方式:先根遍历和后根遍历

  • 先根遍历:先访问树的根结点,然后再依次先根遍历根的每棵子树

  • 后根遍历:先依次遍历每棵子树,然后再访问根结点

  • 先根遍历结果:ABEFCGDHIJ

  • 后根遍历结果:EFBGCHIJDA

8.8.2 森林的遍历

  • 森林的遍历也分为前序遍历和后序遍历,其实就是按照树的先根遍历和后根遍历依次访问森林的每一棵树

  • 树、森林的前根(序)遍历和二叉树的前序遍历结果相同

  • 树、森林的后根(序)遍历和二叉树的中序遍历结果相同

8.9 赫夫曼树

  • 谈到数据压缩,就不能不提到赫夫曼(Huffman)编码,赫夫曼编码是首个实用的压缩编码方案,即使在今天的许多知名压缩算法里,依然可以见到赫夫曼编码的影子
  • 另外,在数据通信中,用二进制给每个字符进行编码时不得不面对的一个问题是如何使电文总长最短且不产生二义性。根据字符出现频率,利用赫夫曼编码可以构造出一种不等长的二进制,使编码后的电文长度最短,且保证不产生二义性。

8.9.1 赫夫曼树定义与原理

  • 先把二叉树简化成叶子结点带权的二叉树(注:树结点间的连线相关的数叫做权,Weight)

  • 结点的路径长度:

    • 从根结点到该结点的路径上的连接数
  • 树的路径长度:

    • 树中每个叶子结点的路径长度之和
  • 结点带权路径长度:

    • 结点的路径长度与结点权值的乘积
  • 树的带权路径长度:

    • WPL(Weight Path Length)是树中所有叶子结点的带权路径长度之和
  • WPL的值越小,说明构造出来的二叉树性能越优

8.9.2 构造赫夫曼树过程

  • 在森林中选出两棵根结点的权值最小的二叉树;
  • 合并两棵选出的二叉树,增加一个新结点作为新二叉树的根,权值为左右孩子的权值之和;

8.10 赫夫曼编码

  • 赫夫曼编码可以很有效地压缩数据(通常可以节省20~90%的空间,具体压缩率依赖于数据的特性)

8.10.1 名词解释

定长编码
  • 像ASCII编码
变长编码
  • 单个编码的长度不一致,可以根据整体出现频率来调节
前缀码
  • 没有任何码字是其他码字的前缀

8.10.2 思路

  • build a priority queue
  • build a huffmanTree
  • build a huffmanTable
  • encode
  • decode
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值