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);
所以,应当尽力保持二叉检索树的平衡,比如将序列打乱,随即插入。