数据结构——树

一、树的基本概念

1.树的定义

树是n个节点的有限集。在任意一棵非空树中应满足:

(1)有且仅有一个称为根 root 的结点。

(2)当n>1时,其余结点可分为若干个互不相交的集合,且这些集合中的每一集合本身又是一棵树,称为根的子树。

从逻辑结构看

1)树中只有根结点没有前趋;

2)除根外,其余结点有且仅一个前趋

3)树中的结点,可以有零个或多个后继;

4)除根之外的其它结点,都存在唯一一条从根到该结点的路径;    

5)树是一种分支结构。

2.基本术语

(1)度:树中一个节点的孩子个数称为该节点的,树中节点的最大度数称为树的

(2)度大于0的结点称为分支结点,度为0的结点称为叶子结点

(3)结点的深度是从根结点开始自顶向下逐层累加的。结点的高度是从叶结点开始自底向上逐层累加的,树的高度是树中结点的最大层数。

(4)树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树

(5)森林是m棵互不相见的树的集合

3.性质

1)树中的结点数等于所有结点的度数之和加1

2)度为m的树中第i层上至多有m^{i-1}个结点

3)高度为h的m叉树至多有\frac{m^{h}-1}{m-1}个结点

4)具有n个结点的m叉树的最小高度为\lceil\log _{m}(n(m-1)+1)\rceil

二、二叉树

1.二叉树的定义

每个结点至多只有两棵子树的树。二叉树的子树有左右之分,次序不能颠倒,是有序树。

二叉树不等于度为2的有序树

2.几个特殊的二叉树

1)满二叉树。除叶子结点外所有的结点都有左右孩子。每层含有最多的结点。

2)完全二叉树。高度为h,有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应,称为完全二叉树。

3)二叉排序树。左子树上的所有结点的关键字均小于根结点的关键字,右子树上的所有结点的关键字均大于根结点的关键字。

4)二叉平衡树。树上任一结点的左子树和右子树的深度之差不超过1

3.二叉树的性质

1)非空二叉树上的叶子结点树等于度为2的结点树+1.n0=n2+1。扩展到任意一颗树,若结点数为n,则边的数量为n-1.

2)非空二叉树上第k层上至多有2^{k-1}个结点

3)高度为h的二叉树至多有2^{h}-1

4)对完全二叉树按从上到下,从左到右的顺序依次编号1,2,…,n则有以下关系:

当i>1时,结点i的双亲的编号为\lfloor i/2 \rfloor, 当i为偶数时,其双亲的编号为i/2,它是双亲的左孩子,当i为奇数时,其双亲的编号为(i-1)/2,它是双亲的右孩子

当2i<=n时,结点i的左孩子编号为2i,否则无左孩子

当2i+1<=n时,结点i的右孩子编号为2i+1,否则无右孩子

结点i所在层次为\lfloor \log_{2}i \rfloor+1

5)具有n个结点的完全二叉树的高度为\lfloor \log_{2}n \rfloor+1\lceil \log_{2}(n+1) \rceil

4.二叉树的存储结构

1)顺序存储结构

利用性质4,将编号与数组下标一一对应顺序存储在一维数组中。适合满二叉树和完全二叉树,其余的二叉树会造成大量空间的浪费。

2)链式存储结构

二叉链表: 利用指针存储结点的左右孩子,避免了空间的浪费。

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

三叉链表:在二叉链表的基础上增加指向父结点的指针域

typedef struct BiTNode {
	ElemType data;
	struct BiTNoide *lchild, *rchild, *parent;
}BiTNode,*BiTree;

静态二叉链表,使用数组记录左右孩子的数组下标。

typedef struct BiTNode { // 结点结构
         ElemType  data;
         int  lchild, rchild ;
 } BiTNode
 typedef struct BiTree {  // 树结构
         BNode nodes[ MAX_TREE_SIZE ];
         int  num_node;     // 结点数目
         int  root;                // 根结点的位置
 } BiTree;

5.二叉树的遍历

二叉树的遍历是指按某条搜索路径访问树中的每个结点,使得每个结点均被访问一次。

1)先序遍历

若二叉树为空,则返回,否则先访问根节点,再先序遍历左子树,再先序遍历右子树。

void PreOrderVisit(BiTree T) {
	if (T != NULL) {
		visit(T);
		PreOrderVisit(T->lchild);
		PreOrderVisit(T->rchild);
	}
}

2)中序遍历

若二叉树为空,则返回,否则先中序遍历左子树,再访问根节点,再中序遍历右子树。

void InOrderVisit(BiTree T) {
	if (T != NULL) {
		InOrderVisit(T->lchild);
		visit(T);
		InOrderVisit(T->rchild);
	}
}

3)后序遍历

若二叉树为空,则返回,否则先后序遍历左子树,再后序遍历右子树,再访问根节点。

void PostOrderVisit(BiTree T) {
	if (T != NULL) {
		PostOrderVisit(T->lchild);
        PostOrderVisit(T->rchild);
		visit(T);
	}
}

4)层次遍历

先将二叉树根结点入队,然后出队,访问出队结点,若它有左子树,则将左子树根节点入队,若它有右子树,则将右子树根节点入队。然后重复以上过程。直至队列为空。

void LevelOrderVisit(BiTree T) {
	InitQueue(Q);
	BiTree p;
	EnQueue(Q, T);
	while (!IsEmpty(Q)) {
		DeQueue(Q, p);
		visit(p);
		if (p->lchild) EnQueue(Q, p->lchild);
		if (p->rchild) EnQueue(Q, p->rchild);
	}
}

5)由遍历序列构造二叉树

由二叉树的先序序列和中序序列或中序序列和后序序列可以唯一地确定一棵二叉树。

由二叉树的层次序列和中序序列也可以唯一地确定一棵二叉树。

void PreInCreate(BiTree &T, char pre[], char in[], int pre_start, int in_start, int n) {
	int root_start = -1;
	for (int i = in_start; i < n + in_start; i++) {
		if (in[i] == pre[pre_start]) {
			root_start = i;
			break;
		}
	}
	if (root_start == -1) {
		T = NULL;
		return;
	}
	T = (BiTree)malloc(sizeof(BiTNode));
	T->data = in[root_start];
	PreInCreate(T->lchild, pre, in, pre_start+1, in_start, root_start - in_start);
	PreInCreate(T->rchild, pre, in, pre_start + root_start - in_start+1, root_start + 1, n + in_start - root_start - 1);
}

void PostInCreate(BiTree &T, char post[], char in[], int post_start, int in_start, int n) {
	int root_start = -1;
	for (int i = in_start; i < n + in_start; i++) {
		if (in[i] == post[post_start + n - 1]) {
			root_start = i;
			break;
		}
	}
	if (root_start == -1) {
		T = NULL;
		return;
	}
	T = (BiTree)malloc(sizeof(BiTNode));
	T->data = in[root_start];
	PostInCreate(T->lchild, post, in, post_start, in_start, root_start - in_start);
	PostInCreate(T->rchild, post, in, post_start + root_start - in_start, root_start + 1, n + in_start - root_start - 1);
}

void LevelInCreate(BiTree &T, char level[], char in[], int level_start, int in_start, int n) {
	int root_start = -1;
	for (int i = in_start; i < n + in_start; i++) {
		if (in[i] == level[level_start]) {
			root_start = i;
			break;
		}
	}
	if (root_start == -1) {
		T = NULL;
		return;
	}
	T = (BiTree)malloc(sizeof(BiTNode));
	T->data = in[root_start];
	LevelInCreate(T->lchild, level, in, level_start + 1, in_start, root_start - in_start);
	LevelInCreate(T->rchild, level, in, level_start + 1 + root_start - in_start, root_start + 1, n + in_start - root_start - 1);
}

6)非递归的遍历算法

利用栈,以先序序为例,将二叉树根结点指针入栈,然后出栈,获取栈顶元素值(即结点指针),若不为空,则访问该结点,再将右、左子树的根结点指针分别入栈,依次重复出栈、入栈,直至栈空为止。

三、线索二叉树

遍历二叉树的结果可求得结点的一个线性序列,指向线性序列中的“前趋”和 “后继” 的指针,称作“线索”。引入线索二叉树,可以更快地查找结点的前驱和后继。

思想:利用结点的空链域保存线索

typedef struct ThreadNode {
	char data;
	struct ThreadNode *lchild, *rchild;
	int ltag, rtag;    //为0表示左右孩子,为1表示前驱后继线索
}ThreadNode,*ThreadTree;
//递归创建中序线索二叉树
void InThread(ThreadTree &p, ThreadTree &pre) {
	if (p != NULL) {
		InThread(p->lchild, pre);
		if (p->lchild == NULL) {
			p->lchild = pre;
			p->ltag = 1;
		}
		if (pre != NULL && pre->rchild == NULL) {
			pre->rchild = p;
			pre->rtag = 1;
		}
		pre = p;
		InThread(p->rchild, pre);
	}
}
void InThreadCreate(ThreadTree T) {
	ThreadTree pre = NULL;
	if (T != NULL) {
		InThread(T, pre);
		pre->rchild = NULL;
		pre->rtag = 1;
	}
}
//求后继
ThreadTree NextNode(ThreadTree p) {
	if (p->rtag == 0) {
		p = p->rchild;
		while (p->ltag == 0) p = p->lchild;
		return p;
	}
	else return p->rchild;
}
//求前驱
ThreadTree PreNode(ThreadTree p) {
	if (p->ltag == 0) {
		p = p->lchild;
		while (p->rtag == 0) p = p->rchild;
		return p;
	}
	else return p->lchild;
}

中序线索二叉树的任一结点的后继:

  • 若rtag==1,则右孩子为后继
  • 若rtag==0,则其右子树中最左下的结点为后继

中序线索二叉树的任一结点的前驱:

  • 若ltag==1,则左孩子为前驱
  • 若ltag==0,则其左子树中最右下的结点为前驱

先序线索二叉树的任一结点的后继:

  • 若ltag==0,则左孩子就是其后继
  • 若ltag==1,rtag==0,则右孩子是其后继
  • 若rtag==1,则右链域表示其后继

先序线索二叉树的任一结点的前驱,需要使用三叉链表记录父节点

  • 若ltag==1,则被线索化,左链域表示其前驱
  • 若ltag==0,则
    • 若x是根结点,则没有前驱
    • 若x是其双亲的左孩子,或是其双亲的右孩子且其双亲没有左子树,则其双亲是其前驱
    • 若x是其双亲的右孩子,且其双亲有左子树,其前驱为其双亲左子树的最右下结点。(可能是左孩子)

后序线索二叉树的任一结点的后继,需要使用三叉链表记录父节点

  • 若rtag==1,则被线索化,右链域表示其后继
  • 若rtag==0,则
    • 若x是根节点,则没有后继
    • 若x是其双亲的右孩子,或是其双亲的左孩子但其双亲没有右子树,则双亲是其后继
    • 若x是其双亲的左孩子,且其双亲有右子树,则其后继为双亲的右子树最左下结点。(可能是右孩子)

后序线索二叉树的任一结点的前驱:

  • 若ltag==1,则被线索化,左孩子表示其前驱
  • 若rtag==0,有右孩子,则右孩子为其前驱
  • 若rtag==1,没有右孩子,则左孩子为其前驱。

四、树和森林

1.树的存储结构

1)双亲表示法

#define MAX_TREEE_SIZE  100
typedef struct PTNode
{   ElemType  data;
     int   parent;    // 双亲位置域
} PTNode;
typedef struct
{   PTNode  nodes [ MAX_TREE_SIZE ];
     int   r, n;         // r为根的位置,n为结点数
} Ptree;

2)孩子表示法

孩子链表,将树中的每个结点的孩子排列起来,看成一个线性表,采用线性链表进行存贮。

typedef  struct  CTNode           // 孩子结点
{      int    child;
       struct  CTNode   *next;
} * ChildPtr;
typedef  struct 
{      ElemType  data;
       ChildPtr     firstchild;      // 孩子链表头指针
} CTBox;
typedef  struct 
{     CTBox  nodes [ MAX_TREE_SIZE ];
       int   n , r;                          // 结点数和根的位置
} CTree;

3)孩子兄弟表示法(树与二叉树的转换)

typedef  struct  CSNode
{
     ElemType   data;
     struct  CSNode  * firstchild ,     //指向第一个孩子
                                 * nextsibling;  //指向下一个兄弟
} CSNode , *CSTree;

森林和二叉树的转换,将森林中每棵树的根看做是兄弟,按照孩子兄弟法转换。

2.树和森林的遍历

树的遍历

1)先根(序)遍历:先访问树的根结点,然后依次先根遍历根的每棵子树。

2)后根(序)遍历:先依次后根遍历每棵子树,然后访问根结点。

3)按层次遍历:先访问第一层上的结点,然后依次遍历第二层,……第n层的结点。

森林的遍历

先序遍历:若森林不空,则访问森林中第一棵树的根结点;

                  先序遍历森林中第一棵树的子树森林;

                  先序遍历森林中其余树构成的森林

中序遍历:若森林不空,则 中序遍历森林中第一棵树的子树森林;

                  访问森林中第一棵树的根结点;

                  中序遍历森林中其余树构成的森林

 树的遍历与二叉树遍历的关系

森林二叉树
先序遍历先序遍历先序遍历
中序遍历后序遍历中序遍历

五、最优二叉树(哈夫曼树)

1.基本概念

路径:从一个祖先结点到子孙结点之间的分支构成这两个结点间的路径;

路径长度:路径上的分支数目称为路径长度;

结点的权:给树中结点所赋的具有物理意义的值;

结点的带权路径长度:从根到该结点的路径长度与该结点权的乘积。

树的带权路径长度 =树中所有叶子结点的带权路径之和;通常记作 WPL=\sum w_{i}*L_{i}

哈夫曼树:假设有 n 个权值(w1,w2, … , wn),构造有 n 个叶子结点的二叉树,每个叶子结点有一个 wi 作为它的权值。则带权路径长度最小的二叉树称为哈夫曼树。

2.构造方法

  • 根据给定的n个权值 {w1, w2, ……wn},构造n棵只有根结点的二叉树,令其权值为wj
  • 在森林中选取两棵根结点权值最小的树作左右子树,构造一棵新的二叉树,置新二叉树根结点权值为其左右子树根结点权值之和。
  • 在森林中删除这两棵树,同时将新得到的二叉树加入森林中。
  • 重复上述两步,直到只含一棵树为止,这棵树即哈夫曼树
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int min1 = 0xffff, min2 = 0xffff;	//初始化为最大
unsigned int num[10005];
int n;
int n1;
void findmin()
{
	int x = 0, y = n1;
	min1 = num[0];
	//找到最小的
	for (int i = 1; i < n1; i++)
	{
		if (num[i] < min1)
		{
			min1 = num[i];
			x = i;
		}
	}
	//找第二小的
	for (int i = 0; i < n1; i++)
	{
		if (num[i] <= min2 && i != x)
		{
			min2 = num[i];
			y = i;
		}
	}
	//把合并后的节点加入,并删掉一个节点
	num[y] = min1 + min2;
	num[x] = 0xffff;
}
int main()
{
	memset(num, 0xffff, sizeof(num));
	int wpl=0;
	scanf("%d", &n);
	n1 = n;
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &num[i]);
	}
	if (n == 1)
	{
		printf("WPL=0\n");
	}
	else
	{
		while (n > 1)
		{
			findmin();
			wpl = wpl + min1 + min2;
			min1 = 0xffff;
			min2 = 0xffff;
			n--;
		}
		printf("WPL=%d\n", wpl);
	}
}

3.哈夫曼树的特点

哈夫曼树是正则的二叉树:没有度为1的结点

假设有n0个叶节点,则哈夫曼树共有2n0 –1 个结点

4.哈夫曼编码

用赫夫曼树可以构造一种不等长的二进制编码,并且构造所得的赫夫曼编码是一种最优前缀编码,即使得所传电文的总长度最短。        

思想:根据字符出现频率编码,使电文总长最短。   

编码:根据字符出现频率构造Huffman树,然后将树中结点引向其左孩子的分支标为“0”,引向其右孩子的分支标为“1”;每个字符的编码即为从根到每个叶子的路径上得到的0、1序列。

前缀编码:任何一个字符的编码都不是同一字符集中另一个字符的编码前缀,反之会造成译码错误

/*
请编写一个程序,判断输入的n个由1和0组成的编码是否为前缀码。如果这n个编码是前缀码,则输出"YES”;否则输出第一个与前面编码发生矛盾的编码。

输入:
第1行为n(表示下面有n行编码)
第2~n+1行为n个由0或1组成的编码
*/
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
typedef struct TreeNode  
{  
    int flag;//0表示未访问过,1表示已经访问过  
    struct TreeNode *left;  
    struct TreeNode *right;  
}*BiTree, BTree;  
int main()  
{  
    char str[100000];  
    int n, i;  
    int flag = 0;  
    BiTree root, p;  
    root = (BiTree)malloc(sizeof(BTree));  
    root->flag = 0;  
    root->left = NULL;  
    root->right = NULL;  
    scanf("%d", &n);  
    while (n--)  
    {  
        memset(str, 0, sizeof(str));  
        p = root;  
        scanf("%s", str);  
        int len = strlen(str);  
        for (i = 0; i < len; i++)  
        {  
            if (str[i] == '0')  
            {  
                if (p->left == NULL)  
                {  
                    BiTree q;  
                    q = (BiTree)malloc(sizeof(BTree));  
                    q->left = NULL;  
                    q->right = NULL;  
                    p->left = q;  
                    p = p->left;  
                    if (i == len - 1)  
                    {  
                        p->flag = 1;  
                    }  
                    else  
                    {  
                        p->flag = 0;  
                    }  
                }  
                else  
                {  
                    if (i == len - 1 || p->left->flag == 1)  
                    {  
                        flag = 1;  
                        break;  
                    }  
                    else  
                    {  
                        p = p->left;  
                    }  
                }  
            }  
            else if (str[i] == '1')  
            {  
                if (p->right == NULL)  
                {  
                    BiTree q;  
                    q = (BiTree)malloc(sizeof(BTree));  
                    q->left = NULL;  
                    q->right = NULL;  
                    p->right = q;  
                    p = p->right;  
                    if (i == len - 1)  
                    {  
                        p->flag = 1;  
                    }  
                    else  
                    {  
                        p->flag = 0;  
                    }  
                }  
                else  
                {  
                    if (i == len - 1 || p->right->flag == 1)  
                    {  
                        flag = 1;  
                        break;  
                    }  
                    else  
                    {  
                        p = p->right;  
                    }  
                }  
            }  
            else  
            {  
                printf("error.\n");  
            }  
        }  
        if (flag == 1)  
        {  
            printf("%s\n", str);  
            return 0;  
        }  
    }  
    printf("YES\n");  
    return 0;  
}  

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值