目录
前言
二叉树是一种树形结构,本章讨论二叉树的定义、二叉树的性质、二叉树的存储结构、二叉树的运算算法设计。
一、二叉树的定义
二叉树是有限的结点集合,这个集合或为空,或由一个根结点和两颗互不相交的称为左子树和右子树的二叉树组成。
需要注意的是,二叉树和树是两种不同的树形结构,不能认为二叉树就是度为2的树,差别如下:
(1)度为2的树至少有一个结点的度为2,也就是说度为2的树中至少有3个结点;而二叉树可以为空
(2)度为2的树中一个度为1的结点是不分左、右子树的;而二叉树中度为1的树是严格去问左、右子树的。
二、满二叉树和完全二叉树
1)满二叉树
在一颗二叉树中,如果所有分支结点都有左、右孩子结点,并且所有叶子结点都集中在最后一层,则该二叉树为满二叉树。
满二叉树的特点:
(1)叶子结点都在最下面一层。
(2)只有度为0和度为2的结点
(3)含n个结点的满二叉树的高度为,叶子结点的个数为
,度为2的结点个数为
。
2)完全二叉树
若二叉树中最多只有最下面两层的结点的度可以小于2,并且最下面一层的叶子结点都依次排列在该层最左边位置上,则这种二叉树称为完全二叉树。
完全二叉树的特点如下:
(1)叶子结点只可能出现在最下面两层中
(2)最下一层的叶子结点都依次排列在该层最左边的位置上
(3)如果有度为1的结点,只可能有一个,且该结点最多只有左孩子而无右孩子
(4)按层序编号后,一旦出现某编号为i的结点为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点。
三、二叉树的性质
性质1:非空二叉树上叶子结点数等于双分支结点数加1
性质2:非空二叉树的第i曾最多有个结点
性质3:高度为h的二叉树最多有个结点
性质4:对完全二叉树中层序编号为i的结点有:
(1)若,即
,则编号为i的结点为分支结点,否则为叶子结点
(2)若n为奇数,则该二叉树的单分支结点数为0;若n为偶数,则只有一个单分支结点,该结点为编号最大的分支结点。
(3)若编号为i的结点有左孩子结点,则左孩子结点的编号为2i;若编号为i的结点有右孩子结点,则右孩子结点编号为2i+1。
(4)若编号为i的结点有双亲结点,其双亲结点的编号为。
性质5:具有n个结点的完全二叉树的高度为或
。
四、二叉树的存储结构
二叉树的存储结构主要有顺序存储结构和链式存储结构两种
1)二叉树的顺序存储结构
二叉树的顺序存储结构,就是用一组连续的存储单元存放二叉树中的结点。由性质4可知,对于完全二叉树和满二叉树,树中结点的层序可以反映出结点之间的逻辑关系。
例如,对上面图中的完全二叉树,对应的顺序存储结构如下:
为了使数组下标与结点编号一致,我们不适用下标为0的数组元素。
而对于一般的二叉树,我们要对其进行改造,增添一些并不存在的空结点,使其成为一颗完全二叉树的形式,再将其按编号存入数组,以‘#’代替空结点。
显然,顺序存储结构对于完全二叉树或满二叉树比较合适。在顺序存储结构中,查找一个结点的孩子、双亲结点都很方便。
2)二叉树的链式存储结构
二叉树的链式存储结构是指用一个链表来存储一颗二叉树,二叉树中的每一个结点用链表中的一个链结点来存储。在二叉树结点类中,不仅有存储数据元素的data变量,还有lChild和rChild用来表示左孩子和右孩子。这种链式存储结构通常称为二叉链
1.二叉链结点类
二叉链的结点类定义如下:
public class BTNode<E> {
E data; //存放数据元素
BTNode<E> lChild; //指向左孩子结点
BTNode<E> rChild; //指向右孩子结点
BTNode(){
lChild = null;
rChild = null;
}
BTNode(E data){
this.data = data;
lChild = null;
rChild = null;
}
}
2.二叉树类的设计
在二叉链中通过根结点root来表示唯一二叉树,对应的设计如下:
public class BTreeClass {
BTNode<Character> root; //根结点
String btStr; //二叉树括号表示串
public BTreeClass(){
root = null;
}
}
3.二叉树基本运算算法的实现
(1)创建二叉树:对于括号表示串btStr,我们用ch扫描btStr,其中只会有4类字符,分别进行处理:
1)) ch='(':表示前面创建的结点p存在孩子结点,需要将p进栈。然后开始处理p结点的左孩子,因此flag=true,表示下一个处理的结点为左孩子结点
2)) ch=')':表示以栈顶结点为根结点的子树创建完毕,将其退栈
3)) ch=',':表示开始处理右孩子结点,使flag=false
4)) 其他情况:创建一个新结点p,根据flag值建立p结点与栈顶结点的关系。
如此循环直至btStr处理完毕,代码如下:
public void creatBTree(String str){
Stack<BTNode> st = new Stack<BTNode>(); //建立栈st
BTNode<Character> p = null;
boolean flag = true; //用来标识左、右孩子的变量
char ch;
int i = 0;
while (i<str.length()){
ch = str.charAt(i);
switch (ch){
case '(':
st.push(p); //将刚才建立的结点进栈
flag = true;
break;
case ')':
st.pop(); //栈顶结点的子树处理完毕,将其出栈
break;
case ',':
flag = false; //开始处理右孩子
break;
default:
p = new BTNode<Character>(ch); //新建结点
if(root==null){
root = p; //若未建立根结点,将p作为根结点
}else{
if(flag){ //处理左孩子
if(!st.empty()){
st.peek().lChild = p;
}
}else {
if(!st.empty()){ //处理右孩子
st.peek().rChild = p;
}
}
}
break;
}
i++;
}
}
(2)返回二叉链的括号表示串:
其过程使先输出根结点的值,当根结点存在左孩子或右孩子时,输出一个'('字符,然后递归处理左子树;当存在右孩子时,输出','符号,递归处理右子树,最后输出')'。代码如下:
public String toString(){
btStr = "";
toStringR(root);
return btStr;
}
private void toStringR(BTNode<Character> node){
btStr += node.data; //输出根结点的值
if(node.rChild!=null || node.lChild!=null){ //当存在左孩子或右孩子时
btStr += "(";
toStringR(node.lChild); //递归处理左子树
if(node.rChild!=null){ //有右孩子,则输出','
btStr += ",";
}
toStringR(node.rChild); //递归处理右子树
btStr += ")";
}
}
(3)查找值为x的结点:
public BTNode<Character> findNode(char x){
return findNodeR(root,x);
}
private BTNode<Character> findNodeR(BTNode<Character> root,char x){
BTNode<Character> p;
if(root == null){
return null;
}else if(root.data==x){
return root;
}else {
p = findNodeR(root.lChild,x);
if(p!=null){
return p;
}else {
return findNodeR(root.rChild,x);
}
}
}
(4)求高度:
public int height(){
return heightR(root);
}
private int heightR(BTNode<Character> root){
if(root==null){
return 0;
}else {
return Math.max(heightR(root.lChild),heightR(root.rChild))+1;
}
}
五、二叉树的遍历
1.二叉树遍历实现
二叉树遍历是指按照一定的次序访问二叉树中的每个结点,并且每个结点仅能访问一次。对于非空二叉树,有如下3种递归的遍历方法:
1)先序遍历
先序遍历的过程如下:
(1)访问根结点;
(2)先序遍历左子树;
(3)先序遍历右子树;
对应的算法如下:
public static void preOrder(BTreeClass bt){
preOrderR(bt.root);
}
private static void preOrderR(BTNode<Character> t){
if(t!=null){
System.out.println(t.data+" ");
preOrderR(t.lChild);
preOrderR(t.rChild);
}
}
2)中序遍历
中序遍历的过程如下:
(1)中序遍历左子树;
(2)访问根结点;
(3)中序遍历右子树;
对应的算法如下:
public static void midOrder(BTreeClass bt){
midOrderR(bt.root);
}
private static void midOrderR(BTNode<Character> t){
if(t!=null){
midOrderR(t.lChild);
System.out.println(t.data+" ");
midOrderR(t.rChild);
}
}
3)后序遍历
后序遍历的过程如下:
(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点
对应的算法如下:
public static void postOrder(BTreeClass bt){
preOrderR(bt.root);
}
private static void postOrderR(BTNode<Character> t){
if(t!=null){
postOrderR(t.lChild);
postOrderR(t.rChild);
System.out.println(t.data+" ");
}
}
2.递归遍历的应用
1)求给定二叉树中结点的个数
求已给二叉树的结点个数是基于遍历算法的,任何一种遍历算法都可以求出结果,我以先序遍历作为例子。代码如下:
public static int countNode(BTreeClass bTree){
return countNodeR(bTree.root);
}
private static int countNodeR(BTNode<Character> root){
int k;
int l;
int r;
if(root==null){
return 0;
}
k = 1; //根结点计数1,相当于访问根结点
l = countNodeR(root.lChild); //遍历求左子树的结点个数
r = countNodeR(root.rChild); //遍历求右子树的结点个数
return k+l+r;
}
2)从左到右输出所有叶子结点
由于3种遍历方式都是从左到右访问结点的,所以3种方式都可以,我以先序遍历作为例子。代码如下
public static void outputLeaf(BTreeClass bt){
outputLeafR(bt.root);
}
private static void outputLeafR(BTNode<Character> root){
if(root!=null){
if(root.lChild==null && root.rChild==null){ //判断是否为叶子结点
System.out.println(root.data);
}else {
outputLeafR(root.lChild);
outputLeafR(root.rChild);
}
}
}
3)将顺序存储结构转换为二叉链
public static BTreeClass trans(String sb){
BTreeClass bt = new BTreeClass();
bt.root = transR(sb,1);
return bt;
}
private static BTNode<Character> transR(String sb,int i){
if(i<sb.length()){
if(sb.charAt(i)!='#'){
BTNode<Character> p = new BTNode<>(sb.charAt(i));
p.lChild = transR(sb,i*2);
p.rChild = transR(sb,i*2+1);
return p;
}else {
return null;
}
}else {
return null;
}
}
六、线索二叉树
1.线索二叉树的定义
如上图,对于二叉链来说,每个结点有两个指针成员,但叶子结点中的指针成员没有有效指向,存在空指针情况。而遍历二叉树得到的解说是一个关于结点的线性序列,所以我们可以利用这些空指针,来指向该线性序列中的“前驱结点”与“后继结点”。这样的二叉树称为线索二叉树。
由于遍历方式的不同,产生的遍历线性序列也不同,因此规定:当某结点左指针为空时,让该指针指向对于遍历序列的前驱结点;当某结点的右指针为空时,让该指针指向对应遍历序列的后继结点。
那么如何区分左指针指向的是左孩子还是前驱结点,右指针指向的是右孩子还是后继结点呢?因此在结点的存储结构上增加两个表示来区分这两种情况:左标识ltag=0表示指向左孩子结点,ltag=1表示指向右孩子结点;右标识rtag同理。
线索二叉树结点存储结构如下:
2.线索二叉树的实现
二叉树的线索化根据遍历方式的不同而变化,这里我用中序线索化作为例子
1)结点类
public class ThNode {
char data;
ThNode lChild;
ThNode rClild;
int ltag;
int rtag;
ThNode(){
lChild = rClild = null;
ltag = rtag = 0;
}
ThNode(char data){
this.data = data;
lChild = rClild = null;
ltag = rtag = 0;
}
}
2)线索化二叉树类
public class ThreadClass {
ThNode root; //根结点
ThNode head; //头结点
ThNode pre; //用于中序线索化,指向中序前驱结点
String bstr;
public ThreadClass(){
root = null;
}
}
3)复制二叉树
许多情况下,我们可能会先得到一颗普通的二叉树,因此我们需要将二叉树的结点转换成线索二叉树的结点,并复制。我把该方法设置为私有方法,仅能在后续方法中使用:
private void copy(BTreeClass bt){
ThreadClass tt = new ThreadClass();
this.root = copyR(bt.root);
}
private ThNode copyR(BTNode<Character> btNode){
if(btNode!=null){
ThNode thNode = new ThNode(btNode.data);
thNode.lChild = copyR(btNode.lChild);
thNode.rChild = copyR(btNode.rChild);
return thNode;
}
return null;
}
4)线索化二叉树
public void creatThread(BTreeClass bt){
copy(bt);
head = new ThNode();
head.ltag = 0;
head.rtag = 1;
if(root==null){
head.lChild=root;
head.rChild=null;
}else {
head.lChild = root;
pre = head;
Thread(root);
pre.rChild = head;
pre.rtag = 1;
head.rChild = pre;
}
}
private void Thread(ThNode p){
if(p!=null){
Thread(p.lChild);
if(p.lChild==null){
p.lChild=pre;
p.ltag=1;
}else {
p.ltag=0;
}
if(pre.rChild==null){
pre.rChild=p;
pre.rtag=1;
}else {
pre.rtag=0;
}
pre = p;
Thread(p.rChild);
}
}
5)遍历线索二叉树
实现中序遍历的步骤如下:
(1)求中序序列的开始结点:该结点为根结点最左下结点。
(2)对于一个结点p,求其后继结点的过程如下:
如果p结点的rChlid指针为线索,则rChild所指为后继结点
否则p结点的后继结点时其右孩子q的最坐下后继结点。
代码如下:
public void ThInOrder(){
ThNode p = head.lChild;
while (p!=head){
while (p!=head && p.ltag==0){
p = p.lChild;
}
System.out.println(p.data+" ");
while (p.rtag==1 && p.rChild!=head){
p = p.rChild;
System.out.println(p.data+" ");
}
p = p.rChild;
}
}
七、哈夫曼树
1.哈夫曼树的定义
在许多应用中经常给树中的结点赋上一个有意义的数值,将此数值称为该结点的权。从根结点到某个结点之间的路径长度与该结点上权的乘积称为结点的带权路径长度。
在n个带权叶子结点构成的所有二叉树中,带权路径长度最小的二叉树称为哈夫曼树(或最优二叉树)。
2.哈夫曼树的实现
1)哈夫曼算法
哈夫曼算法就是构造哈夫曼树的规律,它的算法如下:
(1)将给定的n个结点构成含有n棵二叉树的森林,每棵二叉树只有一个根结点,没有左、右子树。
(2)在森林中选出两颗结点权值最小的子树左右左、右子树构造一颗新的二叉树,并且使新的二叉树的根结点的权值为其左、右子树上跟的权值之和,称为合并。每次合并森林中会减少一颗二叉树。
(3)重复(2)指到森林中只含有一棵树为止,这棵树便是哈夫曼树。
含有n个叶子结点的哈夫曼树共有2n-1个结点
2)哈夫曼树的构造
这里采用静态数组ht存放哈夫曼树。结点类如下:
public class HTNode { //哈夫曼树结点类
char data; //数据
double weight; //权值
public HTNode parent; //双亲结点
HTNode lchild; //左孩子结点
HTNode rchild; //右孩子结点
boolean flag; //标注是双亲的左孩子还是右孩子
public HTNode(){
parent = null;
lchild = null;
rchild = null;
}
public double getWeight(){
return weight;
}
}
我们将叶子结点存放在ht[0..n-1]部分,将所有构建的非叶子结点存放在ht[n..2n-1]部分,并用hcd字符串数组存放哈夫曼编码。
public class HuffmanClass {
final int MAXSIZE = 100; //最多结点个数
double[] w; //权值数组
String str; //存放字符串
int num; //权值个数
HTNode[] ht; //存放哈夫曼树
String[] hcd; //存放哈夫曼编码
public HuffmanClass(){
ht = new HTNode[MAXSIZE];
hcd = new String[MAXSIZE];
w = new double[MAXSIZE];
}
public void SetData(int num,double[] w,String str){ //设置初始值
this.num = num;
for (int i = 0; i < num; i++) {
this.w[i] = w[i];
}
this.str = str;
}
}
由于哈夫曼树的合并操作是取权值最小的两个根结点进行合并,所有要用到优先队列pq。构造哈夫曼树的过程如下:
(1)创建叶子结点,使其进队
(2)从n到2n-1循环,每次出队两个结点建立ht[i]结点,并使其进队。
public void creatHT(){
Comparator<HTNode> nodeComparator = new Comparator<HTNode>() { //比较器
@Override
public int compare(HTNode o1, HTNode o2) {
return (int)(o1.getWeight() - o2.getWeight());
}
};
PriorityQueue<HTNode> pq = new PriorityQueue<>(nodeComparator); //定义优先队列
for (int i = 0; i < num; i++) { //建立结点并进队
ht[i] = new HTNode();
ht[i].parent = null;
ht[i].data = str.charAt(i);
ht[i].weight = w[i];
pq.offer(ht[i]);
}
for (int i = num; i < (2 * num)-1; i++) { //进行n-1次循环
HTNode p1 = pq.poll(); //出队权值最小的两个结点
HTNode p2 = pq.poll();
ht[i] = new HTNode();
p1.parent = ht[i];
p2.parent = ht[i];
ht[i].weight = p1.weight+p2.weight;
ht[i].lchild = p1;
p1.flag = true;
ht[i].rchild = p2;
p2.flag = false;
pq.offer(ht[i]);
}
}
3.哈夫曼编码
1)哈夫曼编码的定义
将哈夫曼树中的左分支记为0,右分支为1。从根结点到每个叶子结点所经过的分支对应的0和1组成的序列,叫做哈夫曼编码。
上图中由2、5、6、9构建的哈夫曼树中的编码如下:
9:0 6:11 2:100 5:101
2)哈夫曼编码的算法
private String reverse(String s){
String t = "";
for (int i = s.length()-1; i>=0; i--) {
t+=s.charAt(i);
}
return t;
}
public void creatHCode(){
for (int i = 0; i < num; i++) {
hcd[i]="";
HTNode p = ht[i];
while (p.parent!=null){
if(p.flag){
hcd[i]+='0';
}else {
hcd[i]+='1';
}
p=p.parent;
}
System.out.println("hcd:"+hcd[i]);
hcd[i] = reverse(hcd[i]);
}
}
总结
二叉树的内容比较多,但都是从基本二叉树一点一点变过来的。