数据结构day2——二叉树

1.什么是二叉树

由有限结点组成,这棵树或者为空,或者由一个根节点和两棵不相交的二叉树组成。

2.一些概念

1)路径:从结点n1经过n2,n3……到nk的这条线路称为路径,路径长度为k-1.

2)结点的深度与层数:一个结点M的深度和所在层数等于从根节点到M的路径的长度;

3)树的高度:等于最深的结点的深度值+1.

4)满二叉树:二叉树的每个结点,要么是叶结点,要么就一定有两个不为空的子结点;

5)完全二叉树:一棵高度为d的树,除了d-1层,其余每层都是填满的,d-1层的叶结点是从左至右依次填充的。

3.简单满二叉树定理

非空满二叉树的叶结点数目等于其分支节点(包括根节点)数目+1;可轻易地用数学归纳法证明;

4.二叉树定理--简单满二叉树定理扩展

一棵非空二叉树的空子树的数目等于所有结点数目+1;证明方式:将所有的空子树用叶结点代替,则这个树变成了满二叉树,原本的所有结点都会变成分支结点,故该结论成立。

二叉树的ADT:

//二叉树的ADT
public interface BinNode {
	public Object element();
	public Object setElemrnt(Object val);
	public BinNode left();
	public BinNode right();
	public BinNode setLeft(BinNode p);
	public BinNode setRight(BinNode p);
	public boolean isLeaf();
}

5.二叉树的周游

即按照一定的顺序访问二叉树的结点们。

1)前序周游,先访问其根节点再访问其子结点。比如:先列出根节点,再列出其左子树,再列出其右子树。

2)后序周游,比如,先列出左子树,再列出右子树,再列出该结点。

3)中序周游,先列出左子树,再列出该结点,再列出右子树。

6.普通二叉树的实现

1)指针实现二叉树

每个二叉树结点,包含了一个数据区,和两个指针,分别指向左子结点和右子结点。下面是二叉树结点类的实现:

//二叉树的指针实现
public class BinNodePtr implements BinNode{
	private Object element;
	private BinNode left;
	private BinNode right;
	
	BinNodePtr(){
		left=right=null;
	}
	BinNodePtr(Object val,BinNode left,BinNode right){
		this.element=val;
		this.left=left;
		this.right=right;
	}
	BinNodePtr(Object val){
		this.element=val;
	}
	
	@Override
	public Object element() {
		// TODO Auto-generated method stub
		return element;
	}

	@Override
	public Object setElemrnt(Object val) {
		// TODO Auto-generated method stub
		return element=val;
	}

	@Override
	public BinNode left() {
		// TODO Auto-generated method stub
		return left;
	}

	@Override
	public BinNode right() {
		// TODO Auto-generated method stub
		return right;
	}

	@Override
	public BinNode setLeft(BinNode p) {
		// TODO Auto-generated method stub
		return left=p;
	}

	@Override
	public BinNode setRight(BinNode p) {
		// TODO Auto-generated method stub
		return right=p;
	}

	@Override
	public boolean isLeaf() {
		// TODO Auto-generated method stub
		return (right==null)&&(left==null);
	}
	
}

当然,平常设计的时候,考虑到叶结点和分支结点的功能性可能不同,可用分别为叶结点和分支结点各自设计不同的结构。 

2)使用数组实现完全二叉树

因为完全二叉树除了最后一层全是满的,按照从上至下从左至右的顺序给结点们进行编号(从0开始)依次存在数组中,则容易得到结点们的相对位置计算公式,对于位置为 r 的结点:

左子结点:2r+1  (2r+1<n)

右子结点:2r+2    (2r-1<n)

左兄弟结点:r-1   (0<r-1<n, r是偶数)

右兄弟结点:r+1  (r+1<n, r是奇数)

父亲结点:(r-1)/ 2 (0<r<n)

对于n个结点使用长度为n的数组存储,则不需要任何的结构性开销。

7.Huffman编码树(满二叉树)

1)一些概念

编码:比如熟知的ASCII码,采用7位编码(第8位是校验位)来表示128种不同的字符,这种每个字符的代码等长的编码方式,称为固定长度编码。

效率问题:如果每个字符的使用频率相同,则固定长度编码一定是效率最高的一类编码。但是平常生活中容易体验到,每个字符的使用频率是各不相同的,如果说常用的字符使用更短的代码,则有利于节省空间,在文件压缩中常用。

huffman编码:一种不等长的编码,字符的编码长度取决于其使用频率或者某种权。

2)huffman编码树的生成过程

首先,按照权的升序将字母进行排列,选择前两个权重最小的,生成一颗子树,根节点的值为两结点权重之和;

其次,将上面生成的根节点放回字母序列当中,但要保持序列的升序。

重复上面的两个过程直到不剩字母,只剩一个根节点。

3)实现

实例:先构造一个Huffman树的结点类:

//存储字母符号和频率的类
public class LetterFreq {
	private char letter;
	private int freq;
	
	public LetterFreq(int freq,char letter){
		this.freq=freq;
		this.letter=letter;
	}
	public LetterFreq(int freq){
		this.freq=freq;
	}
	public int weight(){
		return freq;
	}
	public char letter(){
		return letter;
	}
}

下面是huffman树的结构:

//huffman树的实现
public class HuffTree {
	private BinNode root;
	
	public HuffTree(LetterFreq val){
		root=new BinNodePtr(val);
	}
	public HuffTree(LetterFreq val,HuffTree left,HuffTree right){
		root=new BinNodePtr(val,left.root(),right.root());
	}
	
	public BinNode root(){
		return root;
	}
	public int weight(){
		return ((LetterFreq)root.element()).weight();
	}
	
}

接下来就根据字母序列实现huffman树的构造即可。

4)huffman树的一些定理

一棵至少两个结点的huffman树,频率最小的两个字母一定是兄弟结点且其深度不比任何结点小。

一棵Huffman树具有最小外部路径权重:对于给定的叶结点集合,建立一棵Huffman树,所有叶结点的路径的加权和是最小的。原因是权大的叶结点深度小,而权小的叶结点则深度大。

5)huffman树在编码中的应用

编码方式:比如,将Huffman树的所有左枝干上全部标为0,又枝干上全部标为1.每个字符则有不同的编码。

反编码方式:对于给定的01序列,从左到右读取01序列直到与某个字符的代码匹配为止。我们可能想到,如果一个字符的代码是一个字符代码的前缀,该如何读取呢?显然,huffman编码是符合前缀特性的,即所有代码互不为对方的前缀,以避免反编码的疑惑性。

为什么Huffman编码满足前缀特性?因为Huffman中,所有字母都在页结点上,其代码的前缀肯定终止在分支结点上,不可能是一个字母,所以字母之间是互不为前缀的。

6)huffman的效率

对于频率既定且相差较大的字母序列,huffman可用确定优化的空间大小。效果也会很好。

但在实际应用中,不同类型的文件所用的字符频率可能各不相同,采用同一种Huffman编码就行不通了。

8.二叉检索树(BST)

二叉检索树出现的原因是方便查找,对于普通的线性表来说,无法根据已知的键值高效率的查找想要的数据。

二叉检索树的特点,二叉检索树中序周游的结果是由大到小顺序排列的。也就是说,任意一个结点,其键值大于左子树的所有结点的键值,小于等于右子树的所有结点的键值。

//简单二叉检索树的实现
public class BST {
	private BinNode root;
	
	BST(){
		root=null;
	}
	public void clear(){
		root=null;
	}
	public void insert(Elem val){
		root=insertHelp(root,val);
	}
	
	public void remove(int key){
		root=removeHelp(root,key);
	}
	public Elem find(int key){
		return findHelp(root,key);
	}
	public void print(){
		if(root==null)
			System.out.println("tree is empty");
		else{
			printHelp(root,0);
		}
	}
	public boolean isEmpty(){
		return root==null;
	}
	
	//实际上插入时只会将新的结点插成一个叶结点,所以实际上只有该结点的父亲结点的子结点指针会发生改变
	//但其他地方的子结点指针赋值也是很有必要的,简化了程序的复杂性。
	private BinNode insertHelp(BinNode root,Elem val){
		if(root==null){
			//树为空时,则插入的结点称为根节点
			return new BinNodePtr(val);
		}
		//否则需要找到合适的位置插入
		Elem item=(Elem)root.element();
		if(item.key()>val.key()){
			//此时,将其插入到根结点的左子树中
			root.setLeft(insertHelp(root.left(),val));
		}
		else{
			root.setRight(insertHelp(root.right(),val));
		}
		return root;
	}
	
	private Elem findHelp(BinNode root,int key){
		if(root==null){
			//如果已经查到叶子结点了,还没有找到这个值,则认为这个值不存在于BST中
			return null;
		}
		Elem item=(Elem)root.element();
		if(item.key()>key){
			//此时在根结点的左子树当中查找
			return findHelp(root.left(),key);
		}
		else if(item.key()==key){
			return item;
		}
		else{
			//在根结点的右子树中查找
			return findHelp(root.right(),key);
		}
	}
	
	private BinNode removeHelp(BinNode root,int key){
		if(root==null)
			return null;
		Elem item=(Elem)root.element();
		if(item.key()>key){
			//从左子树中删除
			root.setLeft(removeHelp(root.left(),key));
		}
		else if(item.key()<key){
			root.setRight(removeHelp(root.right(),key));
		}
		else{
			//右子树为空时,直接用左子树代替被删除结点即可
			if(root.right()==null){
				root=root.left();
			}
			//左子树为空时,直接用右子树代替被删除结点即可
			else if(root.left()==null){
				root=root.right();
			}
			else{
				//两棵子树都不为空,则从右子树中找到最小值来代替该结点
				root.setElement(getmin(root.right()));
				//再从右子树当中删除最小值的结点
				root.setRight(deletemin(root.right()));
			}
		}
		return root;
	}
	
	//获得整棵二叉检索树的最小键值处的数据
	private Elem getmin(BinNode root){
		if(root.left()==null){
			return (Elem)root.element();
		}
		else {
			return getmin(root.left());
		}
	}
	
	//删除最小值的结点
	private BinNode deletemin(BinNode root){
		if(root.left()==null){
			//说明root就是最小的那个了,需要删除root
			root=root.right();
		}
		else {
			//否则要从其左子树中删除一个最小值
			root.setLeft(deletemin(root.left()));
		}
		return root;
	}
	
	//中序周游二叉检索树:输入结点和其所在的层数
	private void printHelp(BinNode root,int level){
		if(root==null){
			return;
		}
		printHelp(root.left(),level+1);
		//按照层数的不同打印不同的空格数,形成树的感觉
		for(int i=0;i<level;i++){
			System.out.print(" ");
		}
		System.out.println(((Elem)root.element()).key());
		printHelp(root.right(),level+1);
	}
	
}

注意:上面删除时,当被删除结点存在两棵子树时,之所以选择右子树中的最小值来代替该结点位置,是因为定义中明确提出,该结点的值是小于或者等于右子树中的值的,这样的话,即使右子树当中允许有重复值,也不会引起混乱。相反,如果采用从左子树中挑选一个最大值来替换删除结点时,假如左子树中存在两个这样的最大值,拿一个出来替换,则出现了左子树中有结点不小于根结点的情况,违背了二叉检索树的定义。

关于二叉检索树的平衡性和效率的讨论:

可以知道,二叉检索树的查找,删除,和插入,都与目标结点所应该在的深度有关,如果是一棵平衡的二叉树(是指二叉树的高度尽可能的小),其深度应该是logn左右,则插入删除查找的平均效率为logn;如果该二叉树极度不平衡,比如按照排序好的顺序一个个插入,就会插成一条链,这是二叉树的查找等的平均效率会变成O(n);

所以,应当尽力保持二叉检索树的平衡,比如将序列打乱,随即插入。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值