数据结构 第五章(树和二叉树)【下】

写在前面:

  1. 本系列笔记主要以《数据结构(C语言版)》为参考(本章部分图片以及知识点整理来源于王道),结合下方视频教程对数据结构的相关知识点进行梳理。所有代码块使用的都是C语言,如有错误欢迎指出。
  2. 视频链接:第01周a--前言_哔哩哔哩_bilibili
  3. 哈夫曼树部分的代码参考了一位小伙伴分享的代码,特此说明一下,其它C代码均由笔者根据书上的类C代码进行编写。

五、树和森林

1、树的存储结构

(1)双亲表示法:以一组连续的存储单元存储树的结点,每个结点除了数据域data外,还附设一个parent域用以指示其双亲结点的位置。

cpp 复制代码
#define MAXSIZE 100     //数的最大结点数

typedef char TElemType;  //以字符型为例

typedef struct PTNode
{
	TElemType data; //数据元素
	int parent;     //双亲位置域
}PTNode;     //树的结点定义
typedef struct
{
	PTNode node[MAXSIZE];  //结点数组
	int r, n;   //根结点的位置和结点个数
}PTree;      //树的类型定义

enum Status
{
	OVERFLOW,
	ERROR,
	OK
};

①在这种存储结构下,求结点的双亲十分方便,求树的根也很容易,但求结点的孩子时需要遍历整个结构。

②新增数据元素,无需按逻辑上的次序存储,删除叶子结点上的数据元素同理,但如果删除的不是叶子结点,那就需要把以该结点为根的子树中的全部结点删除。

(2)孩子表示法:由于树中每个结点可能有多棵子树,则可把每个结点的孩子结点排列起来,看成一个线性表,且以单链表作为存储结构,则n个结点有n个孩子链表(叶子的孩子链表为空表),而n个头指针又组成一个线性表,为了便于查找,可采用顺序存储结构。

cpp 复制代码
#define MAXSIZE 100     //数的最大结点数

typedef char TElemType;  //以字符型为例

typedef struct CTNode
{
	int child;   //孩子结点在数组中的位置
	struct CTNode *next;   //下一个孩子
};
typedef struct
{
	TElemType data;
	struct CTNode *firstChild;  //第一个孩子
}CTBox;
typedef struct
{
	CTBox nodes[MAXSIZE];
	int n, r;    //结点数和根的位置
}CTree;

enum Status
{
	OVERFLOW,
	ERROR,
	OK
};

(3)孩子兄弟表示法:又称二叉树表示法,或二叉链表表示法,即以二叉链表作为树的存储结构,链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点(树转换为二叉树),分别命名为firstchild域和nextsibling域。

cpp 复制代码
typedef char TElemType;  //以字符型为例

typedef struct CSNode
{
	TElemType data;
	struct CSNode *firstchild, *nextsibling;
}CSNode,*CSTree;

enum Status
{
	OVERFLOW,
	ERROR,
	OK
};

2、森林与二叉树的转换

(1)树转换为二叉树:

①加线:在兄弟结点之间加一连线。

②抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系。

③旋转:以树的根结点为轴心,将整树顺时针转45°。

(2)二叉树转换为树:

①加线:若p结点是双亲结点的左孩子,则将p的右孩子、右孩子的右孩子......沿分支找到的所有右孩子,都与p的双亲用线连起来。

②抹线:抹掉原二叉树中双亲与右孩子之间的连线。

③调整:将结点按层次排列,形成树结构。

(3)森林转换为二叉树:

①将各棵树分别转换成二叉树。

②将每棵树的根结点用线相连。(各棵树的根结点视为兄弟关系)

③以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转构成二叉树型结构。

(4)二叉树转换成森林:

①抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树。

②还原:将孤立的二叉树还原成树。

3、树和森林的遍历

(1)树的遍历:

①先根(次序)遍历,即先访问树的根结点,然后依次先根遍历根的每棵子树。(树的先根遍历序列与这棵树相应二叉树的先序序列相同)

②后根(次序)遍历,即先依次后根遍历根的每棵子树,然后访问树的根结点。(树的后根遍历序列与这棵树相应二叉树的中序序列相同)

③层次遍历,即按层次遍历树的每个结点,如下图所示。

(2)森林的遍历:

①先序遍历森林(效果等同于依次对各个树进行先根遍历):

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

[1]访问森林中第一棵树的根结点。

[2]先序遍历第一棵树中根结点的子树森林。

[3]先序遍历除去第一棵树之后剩余的树构成的森林。

②中序遍历森林(效果等同于依次对各个树进行后根遍历):

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

[1]中序遍历森林中第一棵树的根结点的子树森林。

[2]访问第一棵树的根结点。

[3]中序遍历除去第一棵树之后剩余的树构成的森林。

六、哈夫曼树及其应用

1、哈夫曼树的基本概念

(1)哈夫曼树又称最优树,是一类带权路径长度最短的树。

(2)哈夫曼树相关术语:

①路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。

②路径长度:路径上的分支数目称作路径长度。

③树的路径长度:从树根到每一叶子结点的路径长度之和。

④权:赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述。

⑤结点的带权路径长度:从该结点到树根之间的路径长度与结点上权值的乘积。

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

⑦哈夫曼树:带权路径长度WPL最小的二叉树称作最优二叉树或哈夫曼树。("带权路径最短"是在"度相同"的树中比较而得的,因此有最优二叉树、最优多叉树之分,但只有最优二叉树是哈夫曼树)

(3)几个结论:

①结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树。

②满二叉树不一定是哈夫曼树。

③哈夫曼树中权越大的叶子结点离根结点越近。

④具有相同带权结点的哈夫曼树不唯一。

2、哈夫曼树的构造算法

(1)给定n个权值分别为、...、的结点,构造哈夫曼树的算法描述如下:

①将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。

②构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。

③从F中删除刚才选出的两棵树,同时将新得到的树加入F中。

④重复步骤2和3,直至F中只剩下一棵树为止。

(2)构造算法生成的哈夫曼树具有如下特点:

①每个初始结点最终都成为叶子结点,且权值越小的结点到根结点的路径长度越大。

②哈夫曼树的结点总数为2n − 1。

③哈夫曼树中不存在度为1的结点。

④哈夫曼树并不唯一,但WPL必然相同且为最优。

(3)哈夫曼树算法的实现:

①哈夫曼树的存储表示:

cpp 复制代码
typedef struct
{
	int weight;    //结点的权值
	int parent, lchild, rchild;  //结点的双亲、左孩子、右孩子的下标
} HTNode, *HTree;

②选取根结点权值最小的两棵树:

cpp 复制代码
void Select_Min(const HTree T, int length, int *e1, int *e2) 
{
	int min1, min2;
	min1 = min2 = INT_MAX;
	int pos1, pos2;
	pos1 = pos2 = 0;
	for (int i = 1; i < length + 1; ++i) 
	{
		if (T[i].parent == 0) 
		{
			if (T[i].weight < min1) 
			{
				min2 = min1;
				pos2 = pos1;
				min1 = T[i].weight;
				pos1 = i;
			}
			else if (T[i].weight < min2) 
			{
				min2 = T[i].weight;
				pos2 = i;
			}
		}
	}
	*e1 = pos1;
	*e2 = pos2;
}

③算法核心部分:

cpp 复制代码
void Creat_Huffman(HTree *T, int n) 
{
	if (n <= 1)
		return;
	int m = 2 * n - 1;
	*T = (HTree)malloc(sizeof(HTNode) * (m + 1)); //0号位置不使用,T[m]表示根结点
	for (int i = 1; i < m + 1; ++i)   //将1~m号单元中的双亲、左孩子、右孩子的下标都初始化为0
	{
		(*T)[i].lchild = (*T)[i].rchild = 0;
		(*T)[i].parent = 0;
	}
	for (int i = 1; i < n + 1; ++i)   //输入前n个单元中叶子结点的权值
	{
		scanf(" %d", &(*T)[i].weight);
	}
	/*------------初始化完毕,开始构造哈夫曼树-----------*/
	int min1, min2; //表示第一小和第二小的位置
	for (int i = n + 1; i < m + 1; ++i)   //通过n-1次的选择、删除、合并来创建哈夫曼树
	{
		Select_Min(*T, i - 1, &min1, &min2); //在T[k]中选择两个其双亲域为0且权值最小的结点,返回它们在T中的序号
		(*T)[min1].parent = (*T)[min2].parent = i; //从森林中删除两个结点,将它们的双亲域改为新结点i
		(*T)[i].lchild = min1; //新结点的左孩子
		(*T)[i].rchild = min2; //新结点的右孩子
		(*T)[i].weight = (*T)[min1].weight + (*T)[min2].weight; //结点的权值为左右孩子权值之和
	}
}

3、哈夫曼编码

(1)固定长度编码与可变长度编码:

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

②可变长度编码------允许对不同字符用不等长的二进制位表示。

(2)关于编码的两个概念:

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

②对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支赋予0,对每个右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。

(3)哈夫曼编码的两个性质:

①哈夫曼编码是前缀编码。没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀。

②哈夫曼编码是最优前缀编码。因为哈夫曼树的带权路径长度最短,所以字符编码的总长最短。

(4)构造哈夫曼编码对应的哈夫曼树的方法------字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树。

(5)由哈夫曼树得到哈夫曼编码的算法的具体实现:

cpp 复制代码
typedef char **HuffmanCode;    //动态分配数组存储哈夫曼编码表
HuffmanCode Creat_HuffmanCode(const HTree HT, int n) 
{
	HuffmanCode HC = (char **)malloc(sizeof(char *) * (n + 1));  //分配存储n个字符编码的编码表空间
	char *temp_string = (char *)malloc(sizeof(char) * n);        //分配临时存放每个字符编码的动态数组空间
	temp_string[n - 1] = '\0';     //因为存放字符串所以最后一个位置 '\0'
	for (int i = 1; i < n + 1; ++i)   //逐个字符求哈夫曼编码
	{
		int parent = HT[i].parent; //指向双亲结点,整个算法是从叶子到根逆向求每个字符的哈夫曼编码
		int current = i; //回溯中当前结点
		int start = n - 1; //start指向数组中最后一个位置,即'\0'
		while (parent)     //从叶子结点向上回溯,直到根结点
		{
			if (current == HT[parent].lchild)
				temp_string[--start] = '0';   //如果是左孩子,那么为'0'
			else
				temp_string[--start] = '1';   //如果是左孩子,那么为'1'
			current = parent;                 //当前结点指向双亲结点
			parent = HT[parent].parent;       //继续向上回溯
		}
		HC[i] = (char *)malloc(sizeof(char) * (n - start)); //求出第i个字符的编码,根据它的长度为其分配空间
		strcpy(HC[i], &temp_string[start]); //拷贝字符串
	}
	free(temp_string); //释放临时空间
	return HC;
}

七、算法设计举例

1、例1

(1)问题描述:判别两棵二叉树是否相等。

(2)代码:

cpp 复制代码
bool T1(BiTree Ts1, BiTree Ts2)
{
	if (Ts1 == NULL && Ts2 == NULL)
		return true;
	if ((!Ts1&&Ts2) || (Ts1 && !Ts2) || (Ts1->data != Ts2->data))
		return false;
	else
		return (T1(Ts1->lchild, Ts2->lchild) && T1(Ts1->rchild, Ts2->rchild));
}

2、例2

(1)问题描述:交换二叉树每个结点的左孩子和右孩子。

(2)代码:

cpp 复制代码
void T2(BiTree *T)
{
	if (*T == NULL)
		return;
	BiTree tmp = (*T)->lchild;
	(*T)->lchild = (*T)->rchild;
	(*T)->rchild = tmp;
	T2(&((*T)->lchild));
	T2(&((*T)->rchild));
	return;
}

3、例3

(1)问题描述:设计二叉树的双序遍历算法(对于二叉树的每一个结点来说,先访问这个结点,再按双序遍历它的左子树,然后再一次访问这个结点,接下来按双序遍历它的右子树)。

(2)代码:

cpp 复制代码
void T3(BiTree T)
{
	if (T)
	{
		printf("%c ", T->data);
		T3(T->lchild);
		printf("%c ", T->data);
		T3(T->rchild);
	}
}

4、例4

(1)问题描述:计算二叉树的最大宽度(二叉树所有层中结点个数的最大值)。

(2)代码:

cpp 复制代码
int T4(BiTree T)
{
	if (T == NULL)   //空二叉树宽度为0
		return 0;
	BiTree Q[MAXSIZE];   //元素为二叉树结点指针的队列
	int front = 1;  //头指针
	int rear = 1;   //尾指针
	int last = 1;   //同层最右结点在队列中的位置
	int temp = 0;   //局部宽度
	int maxw = 0;   //最大宽度
	Q[rear] = T;    //根结点入队
	while (front <= last)
	{
		BiTree p = Q[front++];
		temp++;            //同层元素数加一
		if (p->lchild != NULL)
			Q[++rear] = p->lchild;
		if (p->rchild != NULL)
			Q[++rear] = p->rchild;
		if (front > last)
		{
			last = rear;   //last指向下层最右元素
			if (temp > maxw)   //更新当前最大宽度
				maxw = temp;
			temp = 0;
		}
	}
	return maxw;
}

5、例5

(1)问题描述:按层次顺序遍历二叉树的方法,统计树中度为1的结点数目。

(2)代码:

cpp 复制代码
int T5(BiTree T)
{
	BiTNode* p;
	SqQueue qu;
	int i = 0;
	InitQueue(&qu);   //初始化队列(注意这里的QElemType应为BiTNode*)
	EnQueue(&qu, T);  //根结点指针进入队列
	while (qu.front != qu.rear)   //队不为空,则循环
	{
		DeQueue(&qu, &p);         //出队结点P
		printf("%c ", p->data);  //访问结点P
		if ((p->lchild && !p->rchild) || (!p->lchild && p->rchild))
			i++;
		if (p->lchild != NULL)        //有左孩子时将其进队
			EnQueue(&qu, p->lchild);
		if (p->rchild != NULL)        //有右孩子时将其进队
			EnQueue(&qu, p->rchild);
	}
	return i;
}

6、例6

(1)问题描述:求任意二叉树中第一条最长的路径长度,并输出此路径上各结点的值。

(2)代码:

cpp 复制代码
int longest_len = 0;  //全局变量,用于记录当前找到的最长路径长度
void T6(BiTree T, int path[MAXSIZE], TElemType longestpath[MAXSIZE], int len)
{
	//在其它函数中调用该函数时,形参len应该为0
	//(在哪个函数调用,就在哪个函数中输出,输出部分省略;输出longest_len后需要将其置零,以便下次使用)
	int j;
	if (T)
	{
		if (!T->lchild && !T->rchild)//当遇到叶子结点时,该条路径完毕 
		{
			path[len] = T->data;
			if (len > longest_len)   //如果长于longest_len就替换 
			{
				for (j = 0; j <= len; j++)//把路径上各结点的数据复制到longestpath里面
					longestpath[j] = path[j];
				longest_len = len;   //更新longest_len 
			}
		}
		else
		{
			path[len++] = T->data;
			T6(T->lchild, path, longestpath, len);
			T6(T->rchild, path, longestpath, len);
		}
	}
}

7、例7

(1)问题描述:输出二叉树中从每个叶子结点到根结点的路径。

(2)代码:

cpp 复制代码
void T7(BiTree T, TElemType data[MAXSIZE], int len)
{
	//在其它函数中调用该函数时,形参len应该为0
	if (T)
	{
		if (T->lchild == NULL && T->rchild == NULL)
		{
			printf("%c", T->data);
			for (int i = len - 1; i >= 0; i--)
				printf("--%c", data[i]);
			printf("\n");
		}
		else
		{
			data[len++] = T->data;
			T7(T->lchild, data, len);
			T7(T->rchild, data, len);
		}
	}
}
相关推荐
Lenyiin20 分钟前
02.06、回文链表
数据结构·leetcode·链表
爪哇学长23 分钟前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
爱摸鱼的孔乙己25 分钟前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
烦躁的大鼻嘎1 小时前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
C++忠实粉丝1 小时前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
daiyang123...3 小时前
测试岗位应该学什么
数据结构
kitesxian3 小时前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode
薯条不要番茄酱5 小时前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
盼海7 小时前
排序算法(五)--归并排序
数据结构·算法·排序算法
搬砖的小码农_Sky13 小时前
C语言:数组
c语言·数据结构