一、二叉树的术语
1.1 树
树的定义:树是由n(n>=0)个节点组成的有限集合。n=0的树称为空树;n>0时树有以下两个条件构成:
- 有一个特殊的节点称为根节点,它只有后继,没有前驱;
- 除根节点之外的其他节点分为m(m>=0)个互不相交的集合,其中每个集合本身是一棵树,称为根节点的子树。
树是由节点组成的,节点之间具有层次关系的非线性结构。树是递归定义的。
树的术语
1、父母、孩子和兄弟节点
节点的前驱称为父母,节点的后继称为孩子。一棵树中,根节点没有父母,其他节点只有一个父母。有同一个父母的多个节点称为兄弟节点。
2、度
节点的度是节点所拥有的子树的个数。度为0 的节点称为叶子节点。
3、节点层次和树的高度
节点的层次反应节点处于树中的层次位置。约定根节点层次为1,其他节点的层次是其父母节点的层次+1。显然兄弟节点的层次相同。
树的高度或树的深度是树中节点的最大层次数。
4、有序树和无序树
在树的定义中,如果节点的子树之间没有次序,可以交换位置,称为无序树。如果节点的子树从左到右是有次序的,不能交换位置,称为有序树。
1.2 二叉树
二叉树是特殊的树。除叶子节点外,其他节点的度为2。n=0时称为空二叉树,n>0的二叉树有一个根节点和两棵互不相交的、分别称为左子树和右子树的子二叉树构成。
二叉树是有序树,左右子树不能交换位置。
满二叉树和完全二叉树
一棵高度为h的满二叉树是具有
2h−1
个节点的二叉树。满二叉树中的每一层节点数都达到最大。一棵高度为h的完全二叉树,前
h−1
层每一层的节点数达到最大,最后一层从左到右没有空节点。对满二叉树的节点按层,从根节点开始,自上而下,从左到右进行编号。约定根节点的序号为0。一棵具有n个节点的完全二叉树,按照以上方法编号,每个节点的编号和高度为h的满二叉树中编号从0到n-1的节点一一对应。
1.3 二叉树的性质
1、如果根节点的层次为1,则二叉树第i层最多有
2i−1个节点
。
2、在高度为h的二叉树中,最多有
2h−1
个节点。
3、设一棵二叉树的叶子节点数为
n0
,2度节点数为
n2
,则有
n0=n2+1
4、一棵具有n个节点的完全二叉树,其高度为
h=⌊log2n⌋+1
5、一棵具有n个节点的完全二叉树,对序号为
i(0≤i<n)
的节点,有
- 若 i=0 ,则i为根节点,无父母节点;如果i>0,则i的父母节点为 ⌊(i−1)/2⌋ ;
- 若果 2i+1<n ,则i的左孩子序号为 2i+1 ;否则,没有左孩子;
- 若果 2i+2<n ,则i的右孩子序号为 2i+2 ;否则,没有右孩子;
二、二叉树的表示
2.1 存储结构
二叉树主要采取链式存储,顺序存储仅适用于完全二叉树和满二叉树。将一颗完全二叉树的所有节点按节点序号进行顺序存储,根据性质5,可以访问父母节点和孩子节点。比如堆,是完全二叉树,逻辑结构是二叉树,存储和实现的时候采用顺序存储。
如果对非完全的二叉树进行顺序存储,有以下两种方法:
- 仅存储节点,但不能反映节点之间的层次关系
- 通过补充空子树将一颗非完全的二叉树变成完全二叉树,顺序存储节点和空子树。
2.2 链式存储
二叉树通常采用链式存储结构,每个节点至少有两条链分别链接左右子树,才能表达二叉树的层次关系。链式存储结构主要有二叉链表和三叉链表。
二叉链表采用两条链分别连接左右孩子,每个节点有三个属性:数据、左孩子节点链和右孩子节点链。这样每个节点到达其孩子节点花费时间是O(1)。但是这种方式从一个节点到达其父母节点花费时间较长,需要从根节点在二叉树中查找。
三叉链表是在二叉链表的基础上增加一个属性,指向父母节点,这样存储了父母节点和孩子节点的双向关系。每个节点有4个属性:数据、左孩子节点链、右孩子节点链和父母节点链。
三、二叉树的操作
这里的操作采用三叉链表实现。
3.1 二叉树的表示
首先用三叉链表声明节点类
public class BinaryNode <T> {
public T data;
public BinaryNode <T> left;
public BinaryNode <T> right;
public BinaryNode <T> parent;
public BinaryNode(T data, BinaryNode<T> left, BinaryNode<T> right, BinaryNode<T> parent) {
super();
this.data = data;
this.left = left;
this.right = right;
this.parent = parent;
}
public BinaryNode (T data){
this(data,null,null,null);
}
public BinaryNode(){
this(null,null,null,null);
}
}
二叉树类声明如下
public class BinaryTree<T> {
public BinaryNode<T> root;
//构造空二叉树
public BinaryTree(){
this.root=null;
}
//判断二叉树是否为空
public boolean isEmpty(){
return this.root==null;
}
}
3.2 二叉树的遍历
二叉树的遍历是按照一定规则和次序访问二叉树中的所有节点,并且每个节点访问一次。二叉树的遍历主要有先根、中根、后根遍历和层次遍历,遍历方法有递归和非递归。先根、中根、后根是根据访问根节点的顺序命名的。
- 先根:访问根节点,遍历左子树,遍历右子树。
- 中根:遍历左子树,访问根节点,遍历右子树。
- 后根:遍历左子树,遍历右子树,访问根节点。
- 层次遍历:按层遍历每个节点
如图,先根遍历的结果是:ABDEGJKCFHLI
中根遍历的结果是:DBJGKEACHLFI
后根遍历的结果是:DJKGEBLHIFCA
层次遍历的结果是:ABCDEFGHIJKL
二叉树的先根、中根和后根遍历的实现有递归和非递归两种方式
3.2.1 递归
递归方法必须有参数,通过不同的实际参数区别递归调用执行中的多个方法。一棵二叉树由多个子树组成,每个节点都是一颗子树的根。因此,二叉树实现遍历的递归方法以节点为参数,当输入的节点不同时,遍历不同的子树。当节点为空时,递归结束。
同时二叉树需要实现以根节点开始的遍历。每种遍历由两个重载方法实现。
//先根遍历,递归
public void perOrder(){
System.out.print("先根遍历: ");
perOrder(root);
System.out.println();
}
public void perOrder(BinaryNode<T> node ){
if(node!=null){
System.out.print(node.data);
perOrder(node.left);
perOrder(node.right);
}
}
//中根遍历,递归
public void inOrder(){
System.out.print("中根遍历: ");
inOrder(root);
System.out.println();
}
public void inOrder(BinaryNode<T> node){
if(node!=null){
inOrder(node.left);
System.out.print(node.data);
inOrder(node.right);
}
}
//后跟遍历,递归
public void postOrder(){
System.out.print("后跟遍历: ");
postOrder(root);
System.out.println();
}
public void postOrder(BinaryNode<T> node){
if(node!=null){
postOrder(node.left);
postOrder(node.right);
System.out.print(node.data);
}
}
3.2.2 非递归
二叉树的非递归遍历需要设立一个栈辅助。
1、先根遍历:优先访问根结点,然后再分别访问左孩子和右孩子。即对于任一结点,其可看做是根结点,因此可以直接访问,访问完之后,若其左孩子不为空,按相同规则访问它的左子树;当访问完其左子树时,再访问它的右子树。过程如下:
对任意节点node:
step1: 访问节点node,并将节点入栈;
step2:判断节点node的左孩子是否为空,如果空,栈顶元素出栈,并将该节点的右孩子作为当前节点,转到step1;如果不空,node的左孩子作为当前节点,转到step1;
step3: 直到node为null或栈为空,算法停止。
//先根遍历,非递归
public void perOrderLoop(){
System.out.print("先根遍历(非递归): ");
Stack<BinaryNode<T>> s=new Stack<BinaryNode<T>>();
BinaryNode<T> node=this.root;
while(node!=null || !s.isEmpty()){
while(node!=null){
System.out.print(node.data);
s.push(node);
node=node.left;
}
if(!s.isEmpty()){
node=s.pop();
node=node.right;
}
}
System.out.println();
}
2、中根遍历:先访问左孩子,再访问根节点,最后访问右孩子。即对于任一结点,可看做是根结点,先访问左孩子,若其左孩子不为空,按相同规则访问它的左子树;当访问完其左子树时,访问根节点,最后访问它的右子树。过程如下:
对任意节点node:
step1: 将当前节点node入栈;
step2:判断节点node的左孩子是否为空,如果空,栈顶元素出栈,访问,并将该节点的右孩子作为当前节点,转到step1;如果不空,node的左孩子作为当前节点,转到step1;
step3: 直到node为null或栈为空,算法停止。
//中根遍历,非递归
public void inOrderLoop(){
System.out.print("中根遍历(非递归): ");
Stack<BinaryNode<T>> s=new Stack<BinaryNode<T>>();
BinaryNode<T> node=this.root;
while(node!=null || !s.isEmpty()){
while(node!=null){
s.push(node);
node=node.left;
}
if(!s.isEmpty()){
node=s.pop();
System.out.print(node.data);
node=node.right;
}
}
System.out.println();
}
3、后根遍历:后根遍历的实现比较麻烦。遍历根节点前需要先访问左子树和右子树。这里需要为入栈元素做一个标记。每个元素入栈时标记为0,访问完其左子树后,将其标记改为1,节点不出栈,再访问右子树。当右子树访问完之后,再将标记好的节点出栈。过程如下:
对任意基点node:
step0:构建两个栈,一个存放节点,另一个存放和节点对应的标记;
step1:将节点node和其标记入栈;
step2:判断节点的左子树是否为空。如果不空,node的左孩子当做当前节点,转到1;如果为空,返回栈顶元素,但栈顶元素不出栈,转到step3;
step3:如果栈顶元素的标记为1,访问,并出栈,相应的标记也出栈;如果标记为0,将标记改为1,并将该节点的右孩子当做当前节点,转到step1;
step4: 直到node为null或栈为空,算法停止。
//后根遍历,非递归
public void postOrderLoop(){
System.out.print("后根遍历(非递归): ");
Stack<BinaryNode<T>> s=new Stack<BinaryNode<T>>();
Stack<Integer> s2=new Stack<Integer>();
Integer mark=new Integer(1);
BinaryNode<T> node=this.root;
while(node!=null || !s.isEmpty()){
while(node!=null){
s.push(node);
s2.push(new Integer(0));
node=node.left;
}
while(!s.isEmpty() && s2.peek()==mark){
System.out.print(s.pop().data);
s2.pop();
}
if(!s.isEmpty()){
s2.pop();
s2.push(mark);
node=s.peek().right;
}
}
}
3.2.3 层次遍历
二叉树的层次遍历是兄弟优先。两个兄弟节点访问次序是先左后右,连续访问。他们的子节点的访问次序是,左兄弟的所有孩子在右兄弟的所有孩子之前访问。实现层次遍历需要“先进先出”的队列辅助。
//层次遍历
public void levelOrder(){
System.out.print("层次遍历: ");
levelOrder(root);
}
public void levelOrder(BinaryNode<T> node){
Queue<BinaryNode<T>> q=new LinkedList<BinaryNode<T>>();
while(node!=null || !q.isEmpty()){
System.out.print(node.data);
if(node.left!=null) q.offer(node.left);
if(node.right!=null) q.offer(node.right);
node=q.poll();
}
}
3.3 基于遍历的操作
1、求节点个数,采用先根遍历
//求节点个数
public int count(){
return count(root);
}
//递归
public int count(BinaryNode<T> node){
if(node==null) return 0;
return 1+count(node.left)+count(node.right);
}
public int countLoop(){
return countLoop(root);
}
//非递归,采用先根遍历
public int countLoop(BinaryNode<T> node){
Stack<BinaryNode<T>> s=new Stack<BinaryNode<T>>();
int count=0;
while(node!=null || !s.isEmpty()){
while(node!=null){
count++;
s.push(node);
node=node.left;
}
if(!s.isEmpty()){
node=s.pop();
node=node.right;
}
}
return count;
}
2、求高度
求二叉树的高度,最简单的是使用递归。空节点的高度为0,非空节点的高度是其左右子节点中高度较大的值+1.如果用非递归实现,需要使用后根遍历,先求出左右子树的高度,再求节点高度,最好在节点定义时加上高度属性。或者使用层次遍历。这里使用层次遍历。
//求高度
public int height(){
return height(root);
}
//递归
public int height(BinaryNode<T> node){
if(node==null) return 0;
int lh=height(node.left);
int rh=height(node.right);
return (lh>rh)?lh+1:rh+1;
}
//非递归
//这里采用层次遍历的方法
public int heightLoop(BinaryNode<T> node){
if(node==null) return 0;
int h=0;
Queue<BinaryNode<T>> q=new LinkedList<BinaryNode<T>>();
q.offer(node);
while(!q.isEmpty()){
int begin=0;
int last=q.size();
while(begin<last){
node=q.poll();
if(node.left!=null) q.offer(node.left);
if(node.right!=null) q.offer(node.right);
begin++;
}
h++;
}
return h;
}
3、求父母节点
如果二叉树的实现使用二叉链表,节点的属性不包括父母节点,求一个节点的父母节点时需要遍历。如果使用三叉链表,实现很简单。这里使用三叉链表。、
//求父母节点
public BinaryNode<T> parentOf(BinaryNode<T> node){
return node.parent;
}
4、查找和替换
在二叉树中查找一个关键字为key的元素,返回首次出现的节点,如果没有找到,返回null。可以用各种遍历方法查找。在二叉查找树中,因为节点和其子节点的关键字有大小关系,需要按照一定的规则查找,不用全部遍历。
//查找
public BinaryNode<T> search(T key){
return search(root,key);
}
public BinaryNode<T> search(BinaryNode<T> node,T key){
if(node==null || key==null) return null;
if(node.data.equals(key)) return node;
BinaryNode<T> find=search(node.left,key);
if(find==null) find=search(node.right,key);
return find;
}
5、插入和删除
二叉树的插入需要指定插入规则,例如插入节点node为根节点,原根节点作为node的左孩子;或者将node作为p的左孩子插入,原来node的左孩子作为p的左孩子等。在插入过程中,需要改变节点的left,right和parent。例如在查找二叉树中,根据节点关键字的大小,寻找合适的插入位置。例如在平衡二叉树中,在插入过后,破坏了树的平衡,需要旋转来维持。
二叉树的中删除一个节点或者一棵子树,需要修改该节点父母节点的left和right。如果只是删除一个节点而非一棵子树,需要制定删除规则。换言之,删除节点node后,原先以node为根节点的子树变成了森林,需要约定一种规则,使这个森林作为一棵子树。例如查找二叉树。
4.4 用先根和中根序列构造二叉树
二叉树是数据元素之间具有层次关系的非线性结构,二叉树中每个节点的两个子树有左右之分,所以建立一棵二叉树必须要明确以下两点:节点与父母节点和孩子节点之间的层次关系,兄弟节点间左右关系。
二叉树的遍历将二叉树的非线性关系映射成线性关系。已知一棵二叉树可以得到唯一的遍历序列,反之,已知一个遍历序列,不能得到确切的二叉树。以下讨论一种能够唯一确定一棵二叉树的方法。
由于先根序列和后根序列反应父母与节点间的层次关系,中根序列反应兄弟节点间得左右关系。所以,已知先根和中根,或已知中根和后根都能唯一确定一棵二叉树。但是已知先根和后根不能唯一确定一棵二叉树。这里以已知先根(perList)和中根(inList)序列,确定一棵二叉树为例,明确其步骤。
1、由先根次序可知,二叉树的根为perList[0],该根节点必定在inList中,设根节点在inList中的位置为i,即inList[i]=perList[0];
2、由中根次序知,inList[i]之前的节点在根的左子树上,inList[i]后的节点在根的右子树上。因此,根的左子树由i个节点组成,其子序列为
左子树的先根序列:
perList[1].perList[2],...perList[i]
左子树的中根序列:
perList[0].perList[1],...perList[i−1]
根的右子树由n-i-1个节点组成,其子序列为
右子树的先根序列:
perList[i+1].perList[i+2],...perList[n−1]
右子树的中根序列:
perList[i+1].perList[i+2],...perList[n−1]
3、以此递归,可确定一棵二叉树
例如已知一颗二叉树的先根序列是:ABDEGJKCFHLI
中根序列是:DBJGKEACHLFI
根节点为A,i=6,左子树的先根序列是BDEGJK,中根序列为DBJGKE
右子树的先根序列是CFHLI ,中根序列是CHLFI
由此在确定左子树和右子树,确定二叉树后,可得后根遍历序列DJKGEBLHIFCA
实现如下
//用先根和中根序列构造二叉树
public BinaryTree(T[] perList,T[] inList){
this.root=creat(perList,inList,0,0,perList.length);
setParent(root,root.left);
setParent(root,root.right);
}
//递归的方法构造二叉树
//perList :先根遍历序列
//inList:中根遍历序列
//perStart:先根序列中构造开始的位置
//inStart:中根序列中构造开始的位置
//n:序列长度
private BinaryNode<T> creat(T[] perList,T[] inList,int perStart,int inStart,int n){
if(n<=0) return null;
T elem=perList[perStart];
BinaryNode<T> node=new BinaryNode<T>(elem); //创建节点
int i=0;
while(i<n && !elem.equals(inList[inStart+i]))
i++;
node.left=creat(perList,inList,perStart+1,inStart,i);
node.right=creat(perList,inList,perStart+i+1,inStart+i+1,n-i-1);
return node;
}
//已知从node的父母节点到node的连接,设置node的parent
private void setParent(BinaryNode<T> parent,BinaryNode<T> node){
if(node==null) return;
node.parent=parent;
setParent(node,node.left);
setParent(node,node.right);
}
四、Huffman树和Huffman编码
Huffman编码是数据压缩技术中的一种无损压缩方法。
ASCII码是最常用的一种定长编码方案,一个字符由8位二进制数表示。ASCII码中每个字符的编码与字符的使用频率无关。
Huffman编码是一种变长的编码方案,字符的编码根据使用频率的不同而长短不一,使用频率高的字符编码较短,使用频率低的字符编码较长,从而使所有字符的编码长度为最短。
信息论中Huffman编码的实现步骤如下:
1、统计原始数据中信源符号出现的频率,按频率高低排序;
2、将两个最小的频率相加,作为新信源符号的频率;
3、重复以上两个步骤,直到和为1;在每次合并信源符号时,将合并的信源符号赋值0和1;
4、寻找从频率为1处每个信源符号的路径,记录下路径上的1和0,从而得到每个信源符号的编码。
huffman编码的过程,实际上是构建了一棵二叉树,将所有信源符号当做叶节点,字符的使用频率当做叶节点的权值,构造的二叉树是带权外路径最短的二叉树。(一个节点X的带权路径是X节点的权值与从根节点到X节点路径长度的乘积。所有叶节点的带权路径长度之和称为该二叉树的带权外路径长度。WPL)
这样的二叉树也称作huffman树。huffman树定义为带权外路径长度最短的二叉树,也称为最优二叉树。构造huffman树的算法思路是:使权值越大的叶子结点越靠近根节点。给定n个叶子结点和其权值集合,对应的huffman树不唯一,但是WPL唯一。
构造huffman树的算法如下:
1、由给定的n个权值构造具有n棵二叉树的森林,每一棵二叉树只有一个节点;
2、在森林中选取两个根节点权值最小的二叉树,作为左右子树构造一棵新的二叉树,设置新二叉树的根节点的权值为左右子树根节点的权值之和;
3、在森林中删除原来的两棵二叉树,并将新的二叉树加入森林。
4、重复步骤2、3,直到只剩下一棵二叉树,即为所构造的huffman树。
huffman树构造完毕之后,从根节点开始,为每个节点的左子树附上编码0,右子树附上编码1,然后从根节点开始到每个叶子结点的路径上的0和1组成该字符的huffman编码。由于每个字符都是叶子结点,而叶子结点不可能在根到其他叶子结点的路径上,所有任何一个字符的huaffman编码不是另一个字符huffman编码的前缀。
huffman编码的译码过程:给定一串二进制编码S,从S的第一位开始,逐位去匹配二叉树边上标记的0 和1,从huffman树的根节点开始,遇到0向左,遇到1向右。一旦到达一个叶子结点,便译出一个字符。接着从S的下一位开始,重复以上的步骤。
huffman树的构造实现如下:
package binarytree;
import java.util.PriorityQueue;
import java.util.Stack;
import java.util.TreeMap;
/*
* 给定一个字符串,构造huffman树和实现huffman编码
* 1、统计字符串中的每个字符出现的次数
* 2、利用每个字符的频率构造huffman树
* 3、实现huffman编码
* */
public class HuffmanTree<T> {
public HuffmanNode<T> root;
public HuffmanTree(){
this.root=null;
}
public HuffmanTree(T[] list){
this.root=creatTree(list);
setParent(root,root.left);
setParent(root,root.right);
}
//节点类
static class HuffmanNode<T> implements Comparable<HuffmanNode<T>>{
public T data;
public Integer frequence;
public HuffmanNode<T> left;
public HuffmanNode<T> right;
public HuffmanNode<T> parent;
public HuffmanNode(T data, Integer frequence, HuffmanNode<T> left, HuffmanNode<T> right, HuffmanNode<T> parent) {
super();
this.data = data;
this.frequence = frequence;
this.left = left;
this.right = right;
this.parent = parent;
}
public HuffmanNode(T data,Integer frequence){
this(data,frequence,null,null,null);
}
public int compareTo(HuffmanNode<T> node){
return this.frequence-node.frequence;
}
}
private HuffmanNode<T> creatTree(T[] list){
if(list==null) return null;
//首先统计元素个数,使用treemap,然后将treemap中的元素初始化为节点,节点加入优先队列
//统计元素
TreeMap<T,Integer> tm=new TreeMap<T,Integer>();
for(T elem:list){
if(tm.containsKey(elem))
tm.put(elem,tm.get(elem)+1);
else tm.put(elem, 1);
}
//用字符和其个数构造节点,加入优先队列
PriorityQueue<HuffmanNode<T>> p=new PriorityQueue<HuffmanNode<T>>();
for(T elem:tm.keySet()){
p.offer(new HuffmanNode<T>(elem,tm.get(elem),null,null,null));
}
//利用优先队列构造huffman树
HuffmanNode<T> nodel,noder;
while(!p.isEmpty()){
nodel=p.poll();
noder=p.poll();
if(noder==null)
return nodel;
else{
HuffmanNode<T> nodep=new HuffmanNode<T>(null,nodel.frequence+noder.frequence,nodel,noder,null);
p.offer(nodep);
}
}
return null;
}
private void setParent(HuffmanNode<T> parent,HuffmanNode<T> node){
if(node==null || parent==null) return;
node.parent=parent;
setParent(node,node.left);
setParent(node,node.right);
}
//利用Huffman树进行Huffman编码
public String HuffmanEnCode(T[] list){
//对list中的每一个字符编码,输出编码后的字符串
String str="";
if(list==null){
return str;
}
TreeMap<T,String> tm=encode();
for(T elem:list){
str+=tm.get(elem);
}
return str;
}
//对每个字符编码
private TreeMap<T,String> encode(){
//中根遍历,遇到根节点时,向上寻找父母节点,如果是左孩子,编码0,如果是右孩子,编码1,直到根节点
TreeMap<T,String> tm=new TreeMap<T,String>();
Stack<HuffmanNode<T>> s=new Stack<HuffmanNode<T>>();
HuffmanNode<T> node=this.root;
while(!s.isEmpty()|| node!=null){
while(node!=null){
s.push(node);
node=node.left;
}
if(!s.isEmpty()){
node=s.pop();
if(node.left==null && node.right==null)
tm.put(node.data, encode(node));
node=node.right;
}
}
return tm;
}
private String encode(HuffmanNode<T> node){
String str="";
while(node.parent!=null){
str=(node==node.parent.left?"0":"1")+str;
node=node.parent;
}
return str;
}
}