树是一种十分常见且应用广泛的一种数据结构,其形式如下图
基本概念和描述
如上图,结点层次从根开始定义,根的孩子是第二层,一共有四层,树中结点的最大层次称为树的深度或者高度,当前树深度为4。每个结点都有度,有几个子节点就有几个度,如根节点A度为2,D结点度为3,G,H,I,J度都为0。。。度为0的结点又被称为叶子结点或者终端结点,度不为0的结点成为非终端结点或者分支结点,树的度是树内各结点度的最大值
树的存储结构
简单的顺序存储已经不能满足树的实现,需要结合顺序存储和链式存储来实现,有三种便是方法
双亲表示法---每个结点有自己的数据值,还有一个指针指向其父亲结点,如下图
假设根结点A的下标为0,它没有父亲结点,所以父亲结点为-1,结点B的下标为1,父亲结点A的下标为0,结点C的下标为2,父亲结点A的下标为0。。。J结点下标为9,其父亲结点E下标为4,这样就把整个树表示完全
虽然很容易表示,但是这样有一个缺点,找父亲结点简单,只需要通过指针找下标就可以,但是找孩子结点不容易,需要进行遍历,复杂度很高
孩子表示法---只关心每一个单独的结点,结点包含自己的值和子结点的下标,从左向右遍历
还是刚才的那个树,那么结点A从左到右有两个子结点B,C,两个结点下标分别为1,2;结点B有一个子结点D,下标为3。。。结点E有一个子结点J,下标为9.,如果是叶子结点,那么这个链表为空。。。n个头指针又组成一个线性表,采用顺序存储结构,放在一个一维数组中,这样整个树就被表示出来了
从父亲找孩子只需要遍历以父亲结点为头指针的链表即可,那么孩子找父亲呢?其实也是一样的,只需要再设置一个指针指向父亲结点就可以啦,也就是双向链表
孩子兄弟表示法---设置两个指针,分别指向第一个孩子和自己的右兄弟
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的有兄弟如果存在也是唯一的。因此设置两个指针,一个指向第一个孩子结点,一个指向自己的右兄弟,那么父亲找孩子,就先找自己的孩子结点,然后再孩子结点找兄弟结点,但是同样的,孩子找爹就会比较麻烦
二叉树---十分经典的数据结构---每个结点的度最大只能为2
一般建立一个二叉树结点都会有这几个元素:下标,元素值,左孩子结点,右孩子结点
class TreeNode {
private int index;
private String value;
//左右孩子初始值默认为null,因为建立这个点时,我们不知道其孩子的信息
private TreeNode leftChild = null;
private TreeNode rightChild = null;
}
特殊二叉树
斜树---所有节点只有左子树(左斜树)或者只有右子树(右斜树),其实可以理解为线性数据结构
满二叉树---所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的树成为满二叉树
完全二叉树---编号为i的结点与同样深度的满二叉树中编号为i的结点位置完全相同的树成为完全二叉树
二叉树的性质
性质1:在二叉树的第i层最多有2的(n-1)次方个结点
性质2:深度为k的二叉树最多有2的k次方-1个结点
性质3:对任何一个二叉树,如果其终端结点数为n0,度为2的结点数位n2,那么 n0 = n2 + 1
性质4:具有n个结点的完全二叉树深度为以2为底n的对数再加1
性质5:对一棵n个结点的完全二叉树的结点按层从左到右编号,对于任意一个结点i
(1)如果i = 1,则结点i是二叉树的根,无双亲;如果i > 1,则其双亲是结点[i / 2]
(2)如果i = 1, 则结点i无左孩子(结点i为叶子结点),否则其左孩子是结点2i
(3)如果2i + 1 > n,则结点无右孩子,否则其右孩子结点为2i + 1
二叉树的遍历---前序遍历,中序遍历,后序遍历
前序遍历---根-->左-->右
如果二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,然后前序遍历右子树
代码实现:使用递归的方式,先输出根结点,再输出左结点,最后输出右结点
public static void preNode(TreeNode node) {
if(node == null) {
return;
}
System.out.println(node.getValue());
preNode(node.leftChild);
preNode(node.rightChild);
}
中序遍历---左-->根-->右
规则是若树为空,则空操作返回,否则从根节点开始(注意不是先访问根节点),中序遍历根节点的左子树,然后访问根节点,最后中序遍历右子树
代码实现:先输出左结点,再输出根结点,最后输出右结点
public static void midNode(TreeNode node) {
if(node == null) {
return;
}
midNode(node.leftChild);
System.out.println(node.getValue());
midNode(node.rightChild);
}
后续遍历---左-->右-->根
若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根节点
代码实现:先输出左结点,再输出右结点,最后输出根结点
public static void subNode(TreeNode node) {
if(node == null) {
return;
}
subNode(node.leftChild);
subNode(node.rightChild);
System.out.println(node.getValue());
}
层序遍历
树为空则空操作返回,否则从树的第一层即根节点开始访问,从上而下逐层遍历,在同一层,按从左到右的顺序对结点逐个访问,但是不推荐,比较复杂
对于层序遍历,由于这种方式有点复杂,所以被抛弃,没有代码(手动狗头)
遍历的另一种方式---使用栈
之前的三种遍历我们使用的是单纯迭代的方式,而我们其实也可以使用一种数据结构---栈,利用栈后进先出的特点,再结合迭代进行树的遍历,以前序遍历为例进行分析
建立一个栈,将整个树存储在栈中,当树不为空的时候,不断弹出元素,并且是先弹栈,打印元素,再添加新元素进栈
首先将根节点A弹出栈,输出A的元素值,判断A是否有右子树,如果有则进栈,然后判断A是否有左子树,如果有则进栈。然后继续进行判断树是否为空,如果树不为空,则弹出元素,此时后添加的元素先被弹出,以上面的图为例,A既有左子树B,又有右子树C,那么先添加右子树,再添加左子树,因为按照栈后进先出的原则,第二次弹栈就先弹出左子树B,按照这种方式,我们就可以实现从根节点--->左子树--->右子树的遍历啦!
如图对根节点的左子树的遍历演示,右子树遍历方式和左子树是一样的
代码演示
public static void preNodeByStack(TreeNode node) {
if(node == null) {
return;
}
//创建一个栈,存放二叉树
Stack<TreeNode> stack = new Stack<>();
stack.push(node);
//将二叉树中的元素不断弹栈和压栈,直至二叉树为空
while(!stack.isEmpty()) {
//先弹出根节点
TreeNode n = stack.pop();
System.out.println(n.getValue());
//先将根节点的右孩子压栈,再将左孩子压榨,因为想先弹出左孩子,所以左孩子后进栈
if(n.rightChild != null) {
stack.push(n.rightChild);
}
if(n.leftChild != null) {
stack.push(n.leftChild);
}
}
}
树的创建
树的创建方式有很多种,在这里我们只介绍两种
1.直接创建结点,然后根据树的关系直接将每个结点左右子树的关系说明好
(1)创建A~J一共十个结点,并标好各自的下标
TreeNode nodeA = new TreeNode(1, "A"); ........
(2)将结点与自己的孩子进行联系
nodeA.leftChild = nodeB;
这样就构建好一棵树啦
代码演示
public static void createBinaryTree(TreeNode root) {
if(root ==null) {
return;
}
TreeNode nodeB = new TreeNode(2, "B");
TreeNode nodeC = new TreeNode(3, "C");
TreeNode nodeD = new TreeNode(4, "D");
TreeNode nodeE = new TreeNode(5, "E");
TreeNode nodeF = new TreeNode(6, "F");
TreeNode nodeG = new TreeNode(7, "G");
TreeNode nodeH = new TreeNode(8, "H");
TreeNode nodeI = new TreeNode(9, "I");
TreeNode nodeJ = new TreeNode(10, "J");
root.leftChild = nodeB;
nodeB.leftChild = nodeD;
nodeB.rightChild = nodeE;
nodeD.rightChild = nodeG;
root.rightChild = nodeC;
nodeC.rightChild = nodeF;
nodeF.leftChild = nodeH;
nodeF.rightChild = nodeI;
nodeI.rightChild = nodeJ;
}
2.将要创建的树构成一个完全二叉树,没有子树的时候用一个符号来代替,如#,下面红圈代表井号,使用前序遍历的方式创建
此时,将树的所有元素依次放入ArrayList集合中,顺序为ABD##E#GC###FHI
然后将这个集合作为参数传入创建树的方法中,详细代码如下
//给定一个集合,用集合的元素按顺序创建树
public static void createBinaryTreePre(ArrayList<String> list) {
createBinaryTree2(list.size(), list);
}
public static TreeNode createBinaryTree2(int size, ArrayList<String> list) {
//集合为空时,返回null
if (list.size() == 0) {
return null;
}
//记录当前index,下标用于创建结点
int index = size - list.size();
TreeNode node;
//获取list中的第一个元素
String s = list.get(0);
//如果该元素为"#",也就是说这个结点为空,那么移除当前元素,然后返回值为null的结点
if (s.equals("#")) {
node = null;
list.remove(0);
return node;
}
//如果结点不为空,那么创建一个新结点
node = new TreeNode(index, s);
//新结点的下标为0,说明是根结点
if (index == 0) {
root = node;
}
//无论是不是根结点,都要移除这个元素(目的是为了控制index的值)
list.remove(0);
//然后创建左结点和右结点,使用迭代的方式创建好整个树
node.leftChild = createBinaryTree2(size, list);
node.rightChild = createBinaryTree2(size, list);
return node;
}
关于树的相关基本操作
1.建立二叉树的一个结点,每个结点有下标,元素值,左子树,右子树
2.计算二叉树的高度---二叉树的高度也就是将一条路走到黑的最长的距离,如上面的的树的的高度为5(A-C-F-I-J)
3.求二叉树的大小---也就是求二叉树一共有多少个结点
4.遍历二叉树
代码实现
import java.util.Stack;
public class BinaryTree {
public static void main(String[] args) {
TreeNode root = new TreeNode(1, "A");
createBinaryTree(root);
System.out.println("Height : " + getHeight(root));
System.out.println("Size : " + getSize(root));
// System.out.println("preNode : ");
// preNode(root);
// System.out.println("midNode : ");
// midNode(root);
// System.out.println("subNode :");
// subNode(root);
System.out.println("preNodeByStack : ");
preNodeByStack(root);
}
//使用栈的方式遍历树---以前序遍历为例
public static void preNodeByStack(TreeNode node) {
if(node == null) {
return;
}
//创建一个栈,存放二叉树
Stack<TreeNode> stack = new Stack<>();
stack.push(node);
//将二叉树中的元素不断弹栈和压栈,直至二叉树为空
while(!stack.isEmpty()) {
//先弹出根节点
TreeNode n = stack.pop();
System.out.println(n.getValue());
//先将根节点的右孩子压栈,再将左孩子压榨,因为想先弹出左孩子,所以左孩子后进栈
if(n.rightChild != null) {
stack.push(n.rightChild);
}
if(n.leftChild != null) {
stack.push(n.leftChild);
}
}
}
//用后序遍历输出二叉树
public static void subNode(TreeNode node) {
if(node == null) {
return;
}
subNode(node.leftChild);
subNode(node.rightChild);
System.out.println(node.getValue());
}
//用中序遍历遍历二叉树并输出
public static void midNode(TreeNode node) {
if(node == null) {
return;
}
midNode(node.leftChild);
System.out.println(node.getValue());
midNode(node.rightChild);
}
//用前序遍历遍历二叉树并输出
public static void preNode(TreeNode node) {
if(node == null) {
return;
}
System.out.println(node.getValue());
preNode(node.leftChild);
preNode(node.rightChild);
}
//求二叉树的总结点大小,即二叉树的大小
public static int getSize(TreeNode node) {
if(node == null) {
return 0;
}
int i = getSize(node.leftChild);
int j = getSize(node.rightChild);
return 1 + i + j;
}
//求二叉树的高度
public static int getHeight(TreeNode node) {
if(node == null) {
return 0;
}
int i = getHeight(node.leftChild);
int j = getHeight(node.rightChild);
return i > j ? i + 1 : j + 1;
}
//创建一个二叉树
public static void createBinaryTree(TreeNode root) {
if(root ==null) {
return;
}
TreeNode nodeB = new TreeNode(2, "B");
TreeNode nodeC = new TreeNode(3, "C");
TreeNode nodeD = new TreeNode(4, "D");
TreeNode nodeE = new TreeNode(5, "E");
TreeNode nodeF = new TreeNode(6, "F");
TreeNode nodeG = new TreeNode(7, "G");
TreeNode nodeH = new TreeNode(8, "H");
TreeNode nodeI = new TreeNode(9, "I");
TreeNode nodeJ = new TreeNode(10, "J");
root.leftChild = nodeB;
nodeB.leftChild = nodeD;
nodeB.rightChild = nodeE;
nodeD.rightChild = nodeG;
root.rightChild = nodeC;
nodeC.rightChild = nodeF;
nodeF.leftChild = nodeH;
nodeF.rightChild = nodeI;
nodeI.rightChild = nodeJ;
}
}
//创建一个树
//结点类
class TreeNode {
private int index;
private String value;
//左右孩子初始值默认为null,因为建立这个点时,我们不知道其孩子的信息
TreeNode leftChild = null;
TreeNode rightChild = null;
public TreeNode(int index, String value) {
this.index = index;
this.value = value;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}