目录
1.二叉树简介
二叉树一种非线性的数据结构,由n个有限结点组成一个具有层次关系的集合,构成的图像看起来像一棵倒挂起来的树形,一般二叉树具有一个特殊的节点,称为根节点(下图A结点),根节点没有前驱节点,除根节点外,其余节点有且只有一个前驱,可以有0个或多个后继。而0后继的结点也有了特殊的名字叫做叶子结点(下图H-O的结点),就像树的叶子一样没有更小的树枝生长在这个结点上。
如以下图示
2.二叉树的结构
1.二叉树共同特点
二叉树可以有一个或多个甚至0个结点,0个结点的二叉树被称为空树。一棵非空二叉树至少有一个根结点,根节点与其它非叶子结点可以有1-2个分支(分支的个数也被称为结点的度,一棵二叉树可能有度为0、度为1、度为2的结点)。分支一般被称为“左子树”或者“右子树”,左右子树又可以看作是一颗新的二叉树,所以一棵大的二叉树的内部又可以看作很多小的二叉树,也可以说大的二叉树由很多小的而二叉树组成。相信听起来很熟悉,这好像是递归的大问题分解成子问题???没错也正是因为二叉树的这种结构特点,后序二叉树的很多操作都可以通过递归的思想来设计进行代码实现,后面我们会附有代码实现给各位看官参考。
我们现实中树都有一个高度属性,二叉树作为一种树形结构也是如此,二叉树的高度也被称为树的深度,一棵二叉树的高度是由树中结点的最大层次决定的(根为第一层,决定于最下层的一个叶子结点的层次)如上图中的二叉树的深度都为4。
2.满二叉树特点
二叉树我们已经了解二叉树的每一个节点可以有0-2个后继,文章开头我们看到的二叉树就是一棵除最后一层的叶子结点之外其他结点都具有2个后继,这种树外型看起来满满当当,所以我们称这种树形结构为满二叉树。
因为满二叉树看起来十分规整也很有规律,所以也就有一些特有性质
- 叶子结点都在最下一层
- 只有度为0(叶子结点)和度为2的结点
- 高度为n的满二叉树的结点个数为
- 含有n个结点的满二叉树的叶子结点的个数为(n/2)+1,度为2的结点的个数为(n/2)
3.完全二叉树特点
若二叉树的最下层的结点的度可以可以小于2,并且最下面一层的叶子结点都一次排列在该层的最左边位置上,则把这样的二叉树称为完全二叉树。根据定义及图示可以看出满二叉树也是完全二叉树的一种特例。
完全二叉树的特点及性质
- 叶子结点只可能出现在最下面两层中,且最下面一层的叶子结点都依次排列在最左边。
- 如果有度为1的结点,则该结点只有左分支。
- 按层序编号后,一旦出现叶子结点或者只具有左分支的结点则编号大于该结点的都为叶子结点。
- 若结点的编号i<=(n/2)则该结点为分支结点否则为叶子结点。
- 若结点个数n为奇数,则完全二叉树的每一个分支结点都为双分支结点,为偶数则最后一个分支结点为单分支。
3.二叉树的存储
二叉树的存储同样可以使用顺序表存储和链式存储两种实现,但是由于二叉树的数据并不是连续的,使用顺序表存储由于数据密度不高一般会对空间造成很大的浪费所以优先考虑链式存储结构。所以我们也使用链式存储的方式对二叉树进行储存,同时后序对树的个中操作也建立于该基础上。
想要使用链式存储结构对二叉树进行存储首先要创建一个符合二叉树性质的结点类,用于记录每一个结点。基于二叉树的结构我们的链表的结点类的属性需要包含存放该节点数据的数据类型和存放该结点左右子树的结点数据类型。代码如下
class BTNode<E>{
E data;//储存结点的数据
BTNode<E> lchild;//存储左结点
BTNode<E> rchild;//存储右结点
public BTNode() {
lchild = rchild = null;
}
public BTNode(E data) {
this.data = data;
lchild = rchild = null;
}
}
完成结点类的创建之后就可以使用这个类创建结点对象进行二叉树的构建,因此有了结点之后还需要创建一个二叉树的类,用于将结点关系起来给树的创建建立一个根结点,并在此类中创建出实现二叉树的其他操作的方法。代码如下
class BTreeClass{
BTNode<Character> b;
String bstr;
public BTreeClass() {
b = null;
}
// 二叉树的基本操作。。。。
}
4.二叉树基本操作
我们已经了解了该如何构建二叉树的基本元素类,接下来我们来实现对二叉树的一些基本操作包括先序遍历、中序遍历、后序遍历、层序遍历、求树的高度、查找指定值的结点、查找指定结点的父亲结点、删除指定结点、先序中序建树、中序后序建树、二叉树的反序列化建树、输出二叉树的括号表示串等。
前面我们简单提到二叉树的很多操作都可以利用递归的思想解决,所以接下来的很多操作也都借助递归的思想实现。因为同时为了访问的安全性,我们对递归的主体进行封装,只留有一个定义好的安全访问入口。
1.先序遍历
- 访问根结点
- 先序遍历左子树
- 先序遍历右子树
//树的先序遍历
public void xianxu() {//固定入口
xianxu1(b);
}
private void xianxu1(BTNode<Character> t) {//递归主体
if(t!=null) {
System.out.print(t.data);//访问根结点
xianxu1(t.lchild);//遍历左子树
xianxu1(t.rchild);//遍历右子树
}
}
2.中序遍历
- 中序遍历左子树
- 访问根节点
- 中序遍历右子树
//树的中序遍历
public void zhongxu() {
zhongxu1(b);
}
private void zhongxu1(BTNode<Character> t) {
if(t!=null) {
zhongxu1(t.lchild);
System.out.print(t.data);
zhongxu1(t.rchild);
}
}
3.后序遍历
-
后序遍历左子树
-
后序遍历右子树
-
访问根结点
//树的后续遍历
public void houxu() {
houxu1(b);
}
private void houxu1(BTNode<Character> t) {
if(t!=null) {
houxu1(t.lchild);
houxu1(t.rchild);
System.out.print(t.data);
}
}
4.层序遍历
层序遍历与前三种遍历操作的理念不同,二叉树的层序遍历是借助队列使用广度优先搜索的思想来实现的。
- 访问根节点
- 从左到右访问第2层的所有结点
- 从左到右访问第3层的所有结点......第h层的所有结点
//层序遍历
public void cengxu() {
BTNode<Character> p;
Queue<BTNode<Character>> qu = new LinkedList<BTNode<Character>>();
qu.offer(b);
while(!qu.isEmpty()) {
p = qu.poll();
System.out.print(p.data);
if(p.lchild!=null) {
qu.offer(p.lchild);
}if(p.rchild!=null) {
qu.offer(p.rchild);
}
}
}
5.求树的高度
直接上递归代码
//求树的高度Height
public int Height() {
return Height1(b);
}
private int Height1(BTNode<Character> t) {
if(t==null) {
return 0;
}else {
int lchildh = Height1(t.lchild);//左子树高度
int rchildh = Height1(t.rchild);//右子树高度
return Math.max(lchildh, rchildh)+1;//递归模型,取深度大的子树
}
}
6.查找指定值的结点
//查找值为x的结点
public BTNode<Character> FindNode(char x){
return FindNode1(b, x);
}
private BTNode<Character> FindNode1(BTNode<Character> t, char x){
BTNode<Character> p;
if(t==null) {
return null;
}else if(t.data==x) {
return t;
}else {
p = FindNode1(t.lchild, x);
if(p!=null) {
return p;
}else {
return FindNode1(t.rchild, x);
}
}
}
7.查找指定结点的父亲结点
//找结点的父亲结点
private BTNode getParent(BTNode t, BTNode p){
if(t==null){
return null;
}if(t.lchild==p || t.rchild==p){//判断是否为父亲结点
return t;
}
BTNode left = getParent(t.lchild, p);//左子树中查找
if(left != null){
return left;
}
BTNode right = getParent(t.rchild, p);//右子树中查找
if(right != null){
return right;
}
return null;
}
8.删除指定结点
删除操作可以有两种写法,最好是使用要删除结点的父亲结点作为判断等级,判断该级别的孩子结点是否为要删除的结点,避免了使用删除结点作为判断时还有查找删除结点的父亲结点然后再进行删除操作。这里使用的代码演示就是第二种方法,代码过于冗长。
//删除
private void Delete1(BTNode p) {
if(p.lchild==null&&p.rchild==null) {
BTNode t = getParent(b, p);
if(t==null) {
b = null;
}else if(t.lchild==p){
t.lchild = null;
}else {
t.rchild = null;
}
}else if(p.lchild!=null&&p.rchild==null) {
BTNode t = getParent(b, p);
if(t==null) {
b = p.lchild;
}else if(t.lchild==p){
t.lchild = p.lchild;
}else {
t.rchild = p.lchild;
}
}else if(p.lchild==null&&p.rchild!=null) {
BTNode t = getParent(b, p);
if(t==null) {
b = p.rchild;
}else if(t.lchild==p){
t.lchild = p.rchild;
}else {
t.rchild = p.rchild;
}
}else if(p.lchild!=null&&p.rchild!=null) {
BTNode t1 = null;
t1 = getMin(p.rchild);
p.data = t1.data;
BTNode t = getParent(b, p);
if(t==null) {
t1.lchild = b.lchild;
b = t1;
}else if(t.lchild==p){
t.lchild = t1;
}else {
t.rchild = t1;
}
}
9.先序中序建树
因为这个操作的一些特点能够使用此方法以及中序后序建树方法构建的二叉树的每个结点的值都不相同,这种二叉树具有唯一的先序序列、中序序列和后序序列。根据传入的先序序列字符串和中序序列字符串进行递归操作。
如先序序列:ABDGCEF、中序序列:DGBAECF建树过程如下图
代码如下
//前序中序建树
public BTreeClass qzjian(String qian, String zhong) {
b = qzjian1(qian, 0, zhong, 0, qian.length());
return bt;
}
private BTNode<Character> qzjian1(String qian, int i, String zhong, int j, int n){
if(n==0) {
return null;
}
char ch = qian.charAt(i);
BTNode<Character> t = new BTNode<Character>(ch);//创建根结点
int g = j;
while(g<j+n) {//找到根节点在中序中的下标
if(zhong.charAt(g)==ch) {
break;
}
g++;
}
int l, r;//左右子树的长度
l = g-j; r = n-l-1;
//左子树前序起始下标为根节点下标+1,中序起始下标为原起点,长度为中序根的下标-中序原起始下标
t.lchild = qzjian1(qian, i+1, zhong, j, l);
//右子树前序起始下标为根节点下标+左子树长度+1,中序起始下标为根节点+1,长度为原长度-左子树长度-1
t.rchild = qzjian1(qian, i+l+1, zhong, g+1, r);
return t;
}
10.中序后序建树
如中序序列:DGBAECF、后序序列:GDBEFCA建树过程如下图
代码如下
//中序后序建树
public BTreeClass zhjian(String zhong, String hou) {
b = zhjian1(zhong, 0, hou, 0, zhong.length());
return bt;
}
private BTNode<Character> zhjian1(String zhong, int i, String hou, int j, int n){
if(n==0) {
return null;
}
char ch = hou.charAt(j+n-1);//后序的最后一个为根节点
BTNode<Character> t = new BTNode<Character>(ch);
int g = i;
while(g<n+i) {//找到中序中的根节点
if(zhong.charAt(g)==ch) {
break;
}
g++;
}
int l, r;//左右子树长度
l = g-i; r = n-l-1;
//左子树中序起始下标为原起点,后序起始下标为原起点,长度为中序根节点下标-中序原起始下标
t.lchild = zhjian1(zhong, i, hou, j, l);
//右子树中序起始下标为中序根节点下标+1,后序起始下标为后序原起始下标+左子树长度,长度为原长度-左子树长度-1
t.rchild = zhjian1(zhong, g+1, hou, j+l, r);
return t;
}
11.反序列化建树
序列化与反序列化的概念讲解其他文章也有很多,这里只附上Java代码的实现。
//根据给出的先序序列创建二叉树,例:ab##dc###
public void ChuangJian() {
b = ChuangJian1();
}
private BTNode<Character> ChuangJian1() {
if(bstr.length()==0) {
return null;
}
char ch;
BTNode<Character> t;
ch = bstr.charAt(0);
bstr = bstr.substring(1);
if(ch=='#') {
t = null;
}else {
t = new BTNode<Character>(ch);
t.lchild = ChuangJian1();
t.rchild = ChuangJian1();
}
return t;
}
12.输出二叉树的括号表示串
//输出树的括号表示串
public String toString() {
return toString1(b);
}
private String toString1(BTNode<Character> t) {
String bstr="";
if(t!=null) {
bstr += t.data;
if(t.lchild!=null||t.rchild!=null) {
bstr += "(";
toString1(t.lchild);
if(t.rchild!=null) {
bstr += ",";
}
toString1(t.rchild);
bstr += ")";
}
}
return bstr;
}
5.总结
以上内容就是常见二叉树的一些概念阐述和基本操作的代码实现,二叉树一直以来都是十分重要的数据结构,也是很多人数据结构课程的痛点。这篇文章篇幅不长只包含了二叉树的一些基本概念和特性,没有对二叉树的细节和个别操作做细致的解释。但是网络上有很多博文讲解都很详细,只是具体到代码实现很多都是用的c/c++进行实现的,所以此博文的重要目的就是为和想用java进行数据结构二叉树学习的友友们一起探讨。