1.树的介绍 2.二叉树 3.满二叉树 4.完全二叉树 5.二叉树的遍历算法 6.二叉树的顺序存储结构 7.二叉树的链式存储结构 8.线索二叉树
树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。
图中就是使用树结构存储的集合 {A,B,C,D,E,F,G,H,I,J,K,L,M} 的示意图。对于数据 A 来说,和数据 B、C、D 有关系;对于数据 B 来说,和 E、F 有关系。这就是“一对多”的关系。整个存储形状类似于实际生活中倒着的树,所以称这种存储结构为“树型”存储结构。
树的常用术语:
节点:使用树结构存储的每一个数据元素都被称为“结点”。例如上图中数据元素 A 就是一个结点; 父节点、子结点和兄弟结点:若一个节点含有子节点,则这个节点称为其子节点的父节点。例如上图中A是B、C、D的父节点。反过来B、C、D就成为A的子节点。并且对于B、C、D 来说,它们都有相同的父节点A,它们又互为兄弟节点。 根节点:每一个非空树都有且只有一个被称为根的节点,如果一个节点没有父节点,那么这个节点就是整棵树的根节点。例如上图中A节点就是整棵树的根节点。 叶子节点:如果一个节点没有任何子节点,那么该节点称为叶子节点。例如上图中的K、L、F、G、M、I、J 都是这棵树的叶子节点。 子树:在上图中,A是整棵树的根节点,而B节点,又可以看成是E、F、K、L组成的树的根节点。所以称 B、E、F、K、L 组成的树为整棵树的子树。同样,E、K、L 构成的也是一棵子树,根节点为 E。另外单个结点也是一棵树,只不过根结点就是它本身。 度:对于一个节点,拥有的子树数称为该节点的度,例如上图中根节点 A 下有3个子树,所以节点 A 的度为 3。对于一整颗树而言,一棵树的度是树内各节点的度的最大值,如上图中各个节点的度的最大值为 3,所以整棵树的度的值是 3。 节点的层次:从一棵树的树根开始,树根所在层为第一层,根的孩子节点所在的层为第二层,依次类推。 树的深度:一棵树的深度(高度)是树中节点所在的最大的层次。图 1(A)树的深度为 4。
二叉树介绍:
每个节点最多含有两个子树的树称为二叉树。
二叉树性质:
1.二叉树中,第 i 层最多有 2i-1 个节点。 2.如果二叉树的深度为 K,那么此二叉树最多有 2K-1 个节点。 3.二叉树中,叶子节点数为 n1,度为 2 的节点数为 n2,则 n1=n2+1。
满二叉树介绍:
如果二叉树中除了叶子节点,每个节点的度都为 2,则此二叉树称为满二叉树。
满二叉树性质:
满二叉树除了满足普通二叉树的性质,还具有以下性质: 1.满二叉树中第 i 层的节点数为 2n-1 个。 2.深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2k-1。 3.满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。 4.具有 n 个节点的满二叉树的深度为 log2(n+1)。
完全二叉树介绍:
如果二叉树中除去最后一层节点为满二叉树,且最后一层的节点依次从左到右分布,则此二叉树被称为完全二叉树。满二叉树也是完全二叉树。
完全二叉树性质:
完全二叉树除了具有普通二叉树的性质,它自身也具有一些独特的性质,比如说,n 个结点的完全二叉树的深度为 ⌊log2n⌋+1。(⌊log2n⌋ 表示取小于 log2n 的最大整数。例如,⌊log24⌋ = 2,而 ⌊log25⌋ 结果也是 2。)
层次遍历介绍:
在上述树的术语中,树是有层次的。通过对树中各层的节点从左到右依次遍历(访问),即可实现对这棵二叉树的遍历,此种方式称为层次遍历。如下图所示: 遍历结果为:1 2 3 4 5 6 7
先序遍历介绍:
每遇到一个节点,先访问,然后再遍历其左、右子树。即先根节点->遍历左子树->遍历右子树。(注意,这里的根节点指的是每个节点是其子树的根节点,而不是整棵树的最顶上那个根节点。) 以该图为例,其先序遍历算法访问节点的先后次序为:1 2 4 5 3 6 7
中序遍历介绍:
遍历左子树->根节点->遍历右子树。以上图为例,中序遍历算法访问节点的次序为:4 2 5 1 6 3 7
后序遍历介绍:
遍历左子树->遍历右子树->根节点。以上图为例,后序遍历访问节点的次序为:4 5 2 6 7 3 1
先序、中序、后续遍历总结:
通过判断根节点的访问顺序,确定其是前序、中序还是后序遍历。
二叉树的存储结构有两种,分别为顺序存储和链式存储。
顺序存储二叉树介绍:
二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。
普通二叉树采用顺序存储:
顺序存储只适用于完全二叉树,因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。 普通二叉树转完全二叉树的方法很简单,只需给二叉树额外添加一些无效数据节点,将其"拼凑"成完全二叉树即可。
顺序存储的方法:
完全二叉树的顺序存储,仅需从根节点开始,按照层次依次从左至右将树中节点存储到数组即可。 例如,存储上图所示的完全二叉树,其存储状态在数组中如下图所示: 同样,存储由普通二叉树转化来的完全二叉树也是如此。例如,上述示例普通二叉树的数组存储状态如下图所示:
顺序存储二叉树特点:
1.第i个元素的左子节点为 2 * i + 1 2.第i个元素的右子节点为 2 * i + 2 3.第i个元素的父节点为 (i-1) / 2 i 表示二叉树中第几个节点,对应数组中从0开始编号。
顺序存储二叉树的遍历实现:
package tree; /** * 顺序存储二叉树 */ public class ArrayBinaryTree { public static void main(String[] args) { int[] array = { 1, 2, 3, 4, 5, 6, 7 }; ArrayBinaryTree arrBinaryTree = new ArrayBinaryTree(array); System.out.println("顺序存储二叉树先序遍历:"); arrBinaryTree.preOrderTraverse(); // 1,2,4,5,3,6,7 System.out.println("\n顺序存储二叉树中序遍历:"); arrBinaryTree.inOrderTraverse(); // 4 2 5 1 6 3 7 System.out.println("\n顺序存储二叉树后序遍历:"); arrBinaryTree.postOrderTraverse(); // 4 5 2 6 7 3 1 } private int[] array; public ArrayBinaryTree(int[] array){ this.array = array; } /** * 重载preOrderTraverse */ public void preOrderTraverse(){ this.preOrderTraverse(0); } /** * 递归方式先序遍历 * @param index */ private void preOrderTraverse(int index){ if(array == null || array.length == 0) { throw new RuntimeException("空数组"); } if (index >= array.length){ return; } //输出根节点 System.out.print(array[index]+" "); //向左递归遍历 preOrderTraverse(2*index+1); //向右递归遍历 preOrderTraverse(2*index+2); } /** * 重载inOrderTraverse */ public void inOrderTraverse(){ this.inOrderTraverse(0); } /** * 递归方式中序遍历 * @param index */ private void inOrderTraverse(int index){ if(array == null || array.length == 0) { throw new RuntimeException("空数组"); } if (index >= array.length){ return; } //向左递归遍历 inOrderTraverse(2*index+1); //输出根节点 System.out.print(array[index]+" "); //向右递归遍历 inOrderTraverse(2*index+2); } /** * 重载postOrderTraverse */ public void postOrderTraverse(){ this.postOrderTraverse(0); } /** * 递归方式后序遍历 * @param index */ private void postOrderTraverse(int index){ if(array == null || array.length == 0) { throw new RuntimeException("空数组"); } if (index >= array.length){ return; } //向左递归遍历 postOrderTraverse(2*index+1); //向右递归遍历 postOrderTraverse(2*index+2); //输出根节点 System.out.print(array[index]+" "); } }在上述二叉树的顺序存储,容易发现,其实二叉树并不适合用数组存储,因为并不是每个二叉树都是完全二叉树,普通二叉树使用顺序表存储或多或多会存在空间浪费的现象。
链式存储二叉树介绍:
如上图,作为一颗普通二叉树,若将其采用链式存储,则只需从树的根节点开始,将各个节点及其左右孩子使用链表存储即可。由此上图对应的链式存储结构如下图所示:
链式存储二叉树的节点数据结构:
由上述示例图可知,采用链式存储二叉树时,其节点结构由 3 部分构成: 1.指向左孩子节点的指针(Lchild); 2.节点存储的数据(data); 3.指向右孩子节点的指针(Rchild); 表示该节点结构的 JAVA 语言代码为:
public class BiTNode{ public int data;//数据域 public BiTNode lchild, rchild;//左右孩子引用(指针) }顺序存储二叉树的遍历实现:
package tree; /** * 二叉树节点类 */ class BiTNode{ public int data; public BiTNode lchild, rchild; public BiTNode(int data){ this.data = data; } } /** * 链式存储二叉树 */ public class BinaryTree { public static void main(String[] args) { //创建节点,构造树 BiTNode node1 = new BiTNode(1); BiTNode node2 = new BiTNode(2); BiTNode node3 = new BiTNode(3); BiTNode node4 = new BiTNode(4); BiTNode node5 = new BiTNode(5); BiTNode node6 = new BiTNode(6); BiTNode node7 = new BiTNode(7); node1.lchild = node2; node1.rchild = node3; node2.lchild = node4; node2.rchild = node5; node3.lchild = node6; node3.rchild = node7; BinaryTree binaryTree = new BinaryTree(node1); System.out.println("链式存储二叉树先序遍历:"); binaryTree.preOrderTraverse(); // 1,2,4,5,3,6,7 System.out.println("\n链式存储二叉树中序遍历:"); binaryTree.inOrderTraverse(); // 4 2 5 1 6 3 7 System.out.println("\n链式存储二叉树后序遍历:"); binaryTree.postOrderTraverse(); // 4 5 2 6 7 3 1 } private BiTNode root; public BinaryTree(BiTNode root){ this.root = root; } /** * 重载preOrderTraverse */ public void preOrderTraverse(){ this.preOrderTraverse(this.root); } /** * 递归实现先序遍历 * @param node */ public void preOrderTraverse(BiTNode node){ if (node==null){ return; } //输出根节点 System.out.print(node.data+" "); //向左递归遍历 preOrderTraverse(node.lchild); //向右递归遍历 preOrderTraverse(node.rchild); } /** * 重载inOrderTraverse */ public void inOrderTraverse(){ this.inOrderTraverse(this.root); } /** * 递归实现中序遍历 * @param node */ public void inOrderTraverse(BiTNode node){ if (node==null){ return; } //向左递归遍历 inOrderTraverse(node.lchild); //输出根节点 System.out.print(node.data+" "); //向右递归遍历 inOrderTraverse(node.rchild); } /** * 重载postOrderTraverse */ public void postOrderTraverse(){ this.postOrderTraverse(this.root); } /** * 递归实现后序遍历 * @param node */ public void postOrderTraverse(BiTNode node){ if (node==null){ return; } //向左递归遍历 postOrderTraverse(node.lchild); //向右递归遍历 postOrderTraverse(node.rchild); //输出根节点 System.out.print(node.data+" "); } }线索二叉树前言:
二叉树本身是一种非线性结构,采用任何一种遍历二叉树的方法,都可以得到树中所有结点的一个线性序列。在这个序列中,除第一个节点外,每个节点都有自己的直接前趋;除最后一个节点外,每个节点都有一个直接后继。 例如,上图采用先序遍历的方法得到的节点序列为:1 2 4 5 3 6 7,在这个序列中,节点 2 的直接前趋结点为 1,直接后继节点为 4。
线索二叉树介绍:
当对上图二叉树进行中序遍历时:4 2 5 1 6 3 7,容易发现一个问题,4 5 6 7这几个叶子节点的左右指针,并没有利用上;度为1的节点也有一个空指针域。所以如果用二叉树中空闲的内存空间记录某些节点的前趋和后继元素的位置(不是全部)。这样在遍历二叉树时,就可以利用保存的节点信息,提高遍历的效率。使用这种方法构建的二叉树,即为“线索二叉树”。在有 n 个结点的二叉树中必定存在 n+1 个空指针域。
线索二叉树节点数据结构:
线索二叉树中,如果结点有左子树,则 lchild 指针域指向左孩子,否则 lchild 指针域指向该节点的前趋节点;同样,如果节点有右子树,则 rchild 指针域指向右孩子,否则 rchild 指针域指向该节点的后继节点。 所以为了避免指针域指向的节点的意义混淆,需要改变节点本身的结构,增加两个标志域: leftType和 rightType为标志域:当值为0时,表示对应left、right指针指向的是孩子节点;当值为1时,表示对应left、right指针指向的是前驱、后继节点。 表示该节点结构的 JAVA 语言代码为:
class Node{ public int data; public Node left, right; //指向的子节点的类型:0-left指向左子树 1-left前驱节点 public int leftType; //指向的子节点的类型:0-right指向右子树 1-right后继节点 public int rightType; }线索二叉树遍历代码实现:
下图二叉树的中序遍历结果为:4 2 5 1 6 3 7,进行中序遍历的线索化后,如图所示:红色箭头指向后继节点,蓝色箭头指向前驱节点。
package tree; class Node{ public int data; public Node left, right; //指向的子节点的类型:0-left指向左子树 1-left前驱节点 public int leftType; //指向的子节点的类型:0-right指向右子树 1-right后继节点 public int rightType; public Node(int data){ this.data = data; } } public class ThreadedBinaryTree { public static void main(String[] args) { //创建节点,构造树 Node node1 = new Node(1); Node node2 = new Node(2); Node node3 = new Node(3); Node node4 = new Node(4); Node node5 = new Node(5); Node node6 = new Node(6); Node node7 = new Node(7); node1.left = node2; node1.right = node3; node2.left = node4; node2.right = node5; node3.left = node6; node3.right = node7; ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree(node1); //将二叉树进行线索化重新构建 System.out.println("将二叉树构建为线索化二叉树:"); threadedBinaryTree.inOrderThreadedNodes(); System.out.println("4的后继节点:"+node4.right.data); System.out.println("5的前驱节点:"+node5.left.data+",后继节点:"+node5.right.data); System.out.println("6的前驱节点:"+node6.left.data+",后继节点:"+node6.right.data); System.out.println("7的前驱节点:"+node7.left.data); //遍历 System.out.println("线索化二叉树中序遍历:"); threadedBinaryTree.inOrderTraverse(); } //根节点 private Node root; //辅助前驱节点 private Node preNode; public ThreadedBinaryTree(Node root){ this.root = root; } /** * 重载inOrderThreadedNodes */ public void inOrderThreadedNodes(){ this.inOrderThreadedNodes(this.root); } /** * 构建中序线索化二叉树 * @param node */ public void inOrderThreadedNodes(Node node){ if (node==null){ return; } //线索化左子树 inOrderThreadedNodes(node.left); //线索化当前节点 if (node.left==null){//处理前驱节点 node.left = preNode; node.leftType = 1; } if (preNode!=null && preNode.right==null){//通过辅助前驱节点来处理后继节点的指向 preNode.right = node; preNode.rightType = 1; } //处理完前驱后继节点后,当前节点成为前驱节点 preNode = node; //线索化右子树 inOrderThreadedNodes(node.right); } /** * 中序遍历线索化二叉树 */ public void inOrderTraverse(){ //从当前节点开始中序遍历 Node node = root; while (node!=null){ //当前节点node作为它左右子树的根节点,应该先输出其左子树,查找找到第一个有后继节点的,一定是node的最底层的子树节点 while (node.leftType ==0){ node = node.left; } System.out.print(node.data+" ");//输出该节点node //有指向后继节点的,可以直接遍历输出,当没有后继节点了退出循环 while (node.rightType==1){ node = node.right; System.out.print(node.data+" "); } //当前节点和当前节点的左子树在前面遍历后继结点时都已经输出了,中序遍历左子树和当前节点都输出后便开始访问右子树 node = node.right; } } }