哈夫曼树

本文详细介绍了哈夫曼树的概念、构造过程及其优化判断的特性。哈夫曼树是一种带权路径长度最短的二叉树,通过调整节点位置使得带权路径长度最小。在构建过程中,通过选择权值最小的节点组合形成新节点,直到只剩下一个节点为止。哈夫曼树的应用包括前缀编码,它确保了编码的唯一性,常用于数据压缩和高效传输。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树形结构的用处有很多,例如排序时利用树的结构保存节点大小关系,如、二叉树等排序;查找、修改节点时利用树的平衡性来简化操作,如AVL树红黑树,此处利用树的另一项特性:优化判断。

定义

哈夫曼树的学术定义为,带权路径长度最短的二叉树,即节点具有两个属性:

1、权值,可以看作节点表达出的数值大小,或者变换的表示为概率大小

2、路径,可以看作由根节点到达当前节点的分支数,左分支和右分支具有不同意义

带权路径长度即为权值与路径乘积的累加,所以哈夫曼树首先是一棵二叉树,其次通过调整二叉树节点位置,使得带权路径长度最小。

示例


带权路径长度:WPL:1*3+2*3+3*2+7*1=22

上图示例中,灰色节点为给出的原始节点,白色节点为在根据灰色节点构造哈夫曼树时而产生的辅助节点。

由上例可得出哈夫曼树的构造过程:

1、当节点序列中的根节点数量多于一个时,从当前节点序列中选择两个权值最小的根节点,分别作为左右子节点,创建新的根节点,如选择1,2节点,创建节点3

2、从序列中删除上一步选择的两个根节点,将新创建的根节点加入序列

重复执行以上两步,最终节点序列中剩余的一个节点即为最终的根节点。

示例

以节点:[1,2,3,7],为例,创建过程如下

初始状态:四个节点,按照权值由小到大排列


第一步:选择两个权值最小的根节点,即a,b两节点,构建新根节点,规定左子节点权值不大于右子节点权值


第二步:按照规则,继续选择两个权值最小的根节点,构建新根节点


attention:在该步骤中,两个值为3的节点,选择哪一个作为左子节点,按照本程序中实现方式,新创建的节点在添加到序列中时,是从序列起始处进行判断,不大于当前节点权值则设置为当前节点的前面(选择最小权值节点时选择前两个,所以前面一个是左子节点,后面一个是右子节点,此处规矩设置不同,会产生不同的路径)

第三步:依照规则执行


此时构造的树即为哈夫曼树,具有最小带权路径长度值。观察构建过程可以发现,需要设置的建造规则有两处:

1、统一确定左子节点和右子节点的大小关系,例如所有构造都必须使得左子节点的权值不小于右子节点,免得给出相同的原始节点序列,所构造的哈夫曼树结构不同

2、节点按照权值排序的规则,例如两个原始节点或者一个原始节点和一个新建节点,具有相同的权值时,需要统一序列中的前后顺序(序列中的前后顺序也就是确定哪个是左子节点和右子节点),目的仍然是满足构造出的哈夫曼树具有相同的结构

引申

提到哈夫曼树自然少不了前缀编码,前缀编码的依据也就是哈夫曼树所提供的优化判断功能,由树结构可知,每一个原始节点都被构造为哈夫曼树的叶子节点,使得到达任一叶子节点(原始节点)都不可能经过其他叶子节点(原始节点),这听起来像是一句废话,但这确实是前缀编码的定义:不存在一个字符的编码是另一个字符的前缀。

而前缀编码一个很明显的特性就是提供了对不等长编码的优化,即所有讲哈夫曼编码都会提到的例子,给出现频率高的字符分配较短的编码,给出现频率低的字符分配较长的编码,此不等长编码刚好借助哈夫曼树的特征来避免编码与另一个编码的前缀相同的问题,即实现了前缀编码(名字真有意思,前缀编码表示不存在一个编码的前缀等于另一额编码)。

以上图为例,左分支看作0,右分支看作1,则a,b,c,d对应的编码为:

a:000   b:001   c:01   d:1

参考代码

public class t{
	public static void main(String[] args) throws Exception{
		node a=new node('a',3);//表示的字符,出现的频率
		node b=new node('b',1);
		node c=new node('c',5);
		node d=new node('d',7);
		node e=new node('e',2);
		node f=new node('f',9);
		node g=new node('g',6);
		
		node[] arr=new node[]{a,b,c,d,e,f,g};//输入原始节点
		node root=huffman_tree.build_tree(arr);//构造哈弗曼树
		String str="adbfcgd";//待编码字符串
		huffman_tree.get_code(str,root);//输出编码
	}
}
class node{//简洁起见,属性公有访问
	public char value;//节点存储值
	public int weight;//权值
	public node left,right;//左、右子节点
	public node(char value,int weight){//构造叶子节点
		this.value=value;
		this.weight=weight;
	}
	public node(int weight){//利用两个子节点,构造新的节点,不用赋予value存储值
		this.weight=weight;
	}
	//上面几个属性是节点需要的,下面的属性是为了便于操作而添加的
	public node next=null;//构造链表
}
class huffman_tree{
	private static HashMap<Character,String> map=new HashMap<Character,String>();//保存编码,优化查询
	public static node build_tree(node[] arr){//根据输入节点构造哈弗曼树
		map.clear();
		node root=arr[0];
		for(int i=1;i<arr.length;i++){
			root.next=arr[i];
			root=arr[i];
		}
		root=arr[0];
		root=sort(root);//对跟节点链表排序
		/*
		排序后选择两个权值最小的根节点,构造新的树
		删除上步选择的两个最小根节点,插入新的树的根节点
		直到链表中只有一个根节点,也就是整棵树的根节点
		*/
		while(root.next!=null){//当只有第一个节点存在,即只有根节点时停止
			root=build_sub_tree(root);//删除连个根节点,插入一个新的根节点
			//show(root);
		}
		return root;//返回构建完成的哈弗曼树
	}
	private static node sort(node root){//冒泡排序链表
		if(root==null){
			return root;
		}
		node end_point=null;//遍历终点标志
		boolean flag=false;//判断有序标志
		node temp=null;
		node st=null;
		while(root.next!=end_point){//第一层循环
			st=root;
			flag=true;
			while(st.next!=end_point){//第二层循环
				temp=st.next;//st.next太长了,写成temp简便些
				if(st.weight>temp.weight){
					char v=temp.value;
					int w=temp.weight;
					temp.value=st.value;
					temp.weight=st.weight;
					st.value=v;
					st.weight=w;
					flag=false;//设置标志为false,进行下一次二层遍历
				}
				st=st.next;
			}
			end_point=st;//最后部分节点为有序的,终点节点向前移动一位
			if(flag){
				break;
			}
		}
		return root;
	}
	/*
		删除两个,插入一个,调整链表,逐步构造哈弗曼树
	*/
	private static node build_sub_tree(node root){
		node left=root;//第一个节点作为新节点左孩子
		node right=root.next;//第二个节点作为新节点右孩子
		node new_node=new node(left.weight+right.weight);//新根节点
		new_node.left=left;
		new_node.right=right;
		root=right.next;//指向新首节点
		/*
			排序规则为,从起始出开始,不大于当前节点,则作为当前节点的前面节点
		*/
		if(root==null){
			root=new_node;
			return root;
		}
		node pre=null,temp=root;
		while(temp!=null&&temp.weight<new_node.weight){
			pre=temp;
			temp=temp.next;
		}
		if(pre==null){
			root=new_node;
			root.next=temp;
		}else{
			pre.next=new_node;
			new_node.next=temp;
		}
		return root;
	}
	private static void show(node a){//观察链表状态变化
		while(a!=null){
			System.out.print(a.weight+" ");
			a=a.next;
		}
		System.out.println();
	}
	public static void get_code(String str,node root) throws Exception{//根据输入字符串,返回哈弗曼编码
		StringBuilder sb=new StringBuilder();
		StringBuilder result=new StringBuilder();
		for(int i=0;i<str.length();i++){
			sb.setLength(0);
			char t=str.charAt(i);
			if(map.containsKey(t)){//如果已经记录在册,则直接返回
				result.append(map.get(t)+" ");
			}else{//否则查询编码并保存
				if(!get_road(root,t,sb)){
					throw new Exception("节点不存在");
				}else{
					map.put(t,sb.toString());
					result.append(sb.toString()+" ");
				}
			}
		}
		System.out.println(result.toString());
	}
	private static boolean get_road(node root,char c,StringBuilder sb){//获取每个叶节点的路径值,左0,右1
		if(root!=null){//当前节点不为空
			if(root.value==c){//找到直接返回true
				return true;
			}
			sb.append(0);//添加0,找左边
			if(get_road(root.left,c,sb)){
				return true;
			}else{
				sb.deleteCharAt(sb.length()-1);//左边不存在,则把刚刚添加的左边0删除
				sb.append(1);//添加1,找右边
				if(get_road(root.right,c,sb)){
					return true;
				}else{
					sb.deleteCharAt(sb.length()-1);//右边不存在,则把刚刚添加的右边1删除
					return false;
				}
			}
		}
		return false;
	}
}

总结

哈夫曼树作为一种优化判断的树结构,在区分不同频率或者概率上不同的相同类型操作上,利用统计数据得出的规律构建最优化选择树结构,避免了过多的无用分支,提升整体操作性能。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值