数据结构与算法(树)

32 阅读2分钟

树常见的考题主要包括二叉树的遍历算法、建立最优二叉树和实现哈夫曼编码的方法。

---------------2023.8.20----------------

今天主要对如何实现哈夫曼编码进行整理。

1.哈夫曼树的构造算法

1.1根据给定的带权结点,选择最小的两个作为树的左右子树;并生成根节点,节点权值为子树节点之和。 1.2将生成的子树加入原节点中,并删除之前加入的两个节点。 1.3重复上述操作,直到只剩一棵树为止。

2.构建哈夫曼树

2.1哈夫曼树算法的实现

哈夫曼树种不存在度为1的节点,有n个节点的哈夫曼树就有2n-1个结点,用大小为2n-1的一维数组来存储。存储结构如下所示。

	 ________________________________
	| weight | parent  | lchild | rchild |
	 ________________________________

typedef struct{
	int weight; //权值
	int parent,lchild,rchild;//父结点,左右孩子结点的下标
}HTNode,*HuffmanTree;

为了实现方便,数组的n号单元不使用,从1号单元,所以数组大小为2n,将叶子节点存储在前1-n个位置,后面n-1个位置存储非叶子结点。

2.2构造哈夫曼树

2.2.1初始化

动态申请2n个单元,然后从1号位置开始,对所有位置进行初始化,将双亲以及左右孩子置为0;再对前n个结点的权重进行赋值。

2.2.2创建树

循环n-1次,从当前森林中选出双亲结点为0,且权重最小的两个树的根节点s1和s2;然后将s1s2在原来的森林中进行删除,也就是将这两个结点的双亲改为n+1;将这两个结点的权值想加,保存在第n+1个位置,并将这个结点的左右孩子的下标改为s1、s2。

2.2.3算法实现

void CreateHuffmanTree(HuffmanTree &HT,int n){
	if(n<=1) return ;
	m = 2*n-1; //数组一共2n-1个元素
	HT = new HTNode[m+1]; //0号未使用
	//对n个结点/森林进行初始化
	for(i = 1;i<=m;i++){
		HT[i].lchild=0;HT[i].rchild=0;HT[i].parent=0;
	}
	//手动输入各个结点的权重
	for(i = 1;i<=n;++i){
		cin>>HT[i].weight;
	}
	//初始化结束,构建哈夫曼树
	for(i = n+1;i<=m;i++){ //合并后的结点存放在第n个位置后
		Select(HT,i-1,s1,s2);//在HT中前i-1个位置选择双亲为0且权重最小的两个结点,用s1和s2返回
		HT[s1].parent=i;HT[s2].parent=i; //删除左右孩子子树
		HT[i].lchild=s1;HT[i].rchild=s2; //合并后结点的左右孩子指向
		HT[i].weight=HT[s1].weight+HT[s2].weight; //合并后的权重
	}
}

3.哈夫曼编码

3.1实现思路

我们采用从叶子结点向上回溯的步骤,从第一个叶子结点开始向上,如果当前结点是这棵树的左孩子,则生成一个编码0,如果是右孩子,则生成一个编码1,将生成的编码存储在一个数组cd中,完成这个步骤后,继续对这课树的根节点向上继续这个过程,这当已经遍历到整个哈夫曼树的根节点时,个过程最多会生成n个编码,这n个编码就是这个叶子结点的哈夫曼编码,不过需要注意的是,如果按照顺序存放在cd中,会导致哈夫曼编码是逆序,因此在存储的时候,我们需要从cd数组的后面向前依次存储,遍历完成一次后,将这个结点生成的哈夫曼编码复制到最终的哈夫曼编码数组里,即可对下一个结点进行上述操作。

3.2代码实现

---------------------------哈夫曼编码表的存储表示----------------------
typedef char **HuffmanCode; //指针数组,用来存放每个哈夫曼编码的首地址

----------------------------------注解------------------------------
//char *s = 'hello world'用来表示整个字符串
//char s[] = 'hello world'是放在数组里,这样可以对每个字符进行操作
--------------------------------------------------------------------

void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){
	HC = new char*[n+1];//第一个位置不用,n个元素,n+1的大小
	cd = new char[n];//每个结点的编码最长为n-1,最后一个位置来存放'\0'
	cd[n-1] = '\0';
	for(i = 1;i<=n;++i){ //总共有n个叶子结点,就循环n次
		start = n-1;//start是编码放在cd中的位置,从后往前放,所以start要在最后
		c = i;f = HT[i].parent;//c指向当前叶子结点,f指向其双亲结点
		while(f!=0){
			--start;
			if(HT[f].lchild == c) cd[start] = '0';//左孩子编码为1
			else cd[start] = '1'; //右孩子编码为0
			c = f;f=HT[f].parent;//向上回溯
		}
		HC[i] = new char[n-start];//为HC的第i个结点申请n-start大小的空间存放编码
		strcpy(HC[i],&cd[start]);//将cd中临时编码复制到HC中	
	}
	delete cd;//释放临时空间
}

---------------2023.8.21----------------

1.二叉树的存储结构

1.1顺序存储结构

----二叉树的顺序存储表示----
#define MAXTSIZE 100
typedef TElemType SqBiTree[MAXTSIZE];
SqBiTree bt;

这种存储方式仅适用于存储完全二叉树,否则会造成很大的空间浪费。

1.2链式存储结构

大部分树的结点都有左右孩子指针以及数据域,这样的结点构成的存储结构称为二叉链表,如果有指向双亲结点的指针域构成的存储结构,称为三叉链表。由n个结点的二叉链表有n+1个空链域,后续可以对空链域进行操作,例如二叉树的线索化。

----二叉树的二叉链表存储表示----
typedef struct BiTNode{
	TElemType data;
	struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;//BiTNode重在表示一个结点,*BiTree重在表示一棵二叉树

2.二叉树的遍历

2.1中序遍历的递归算法

void InOrderTraverse(BiTree T){
	if(T){
		InOrderTraverse(T->lchild);
		cout<<T->data;//对结点进行操作
		InOrderTraverse(T->rchild);
	}
}

2.2中序遍历的非递归算法

void InOrderTraverse(BiTree T){
	InitStack(S);p = T;
	q = new BiTNode; //q用来存储弹出来被访问的结点
	while(p || !StackEmpty(S)){
		if(p){
			push(S,p);
			p = p->lchild;
		}else{
			pop(S,q);
			cout<<q->data;//访问结点
			p = q->rchild;
		}
	}
}