声明:本文部分文章取自于
Java中关于二叉树详解_来学习的小张的博客-CSDN博客_java 二叉树原理
更多关于二叉树详情可以点击上面链接
目录
一:树形结构
二:二叉树
2.1 二叉树的遍历:
2.2 二叉树的查找:
2.3 二叉树删除节点:(简单版)
2.4 顺序存储二叉树:
2.5 线索化二叉树:
一:树形结构
树是一种非线性的数据结构,它是由n(n>=0)
个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
- 有一个特殊的节点,称为根节点,根节点没有前驱节点;
- 除根节点外,其余节点被分成
M(M > 0)
个互不相交的集合T1、T2、......、Tm
,其中每一个集合Ti (1 <= i<= m)
又是一棵与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继;- 树是递归定义的。
- 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:
A
的度为6
;- 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为
6
;- 叶子节点或终端节点:度为
0
的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点;- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点;
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点;
- 根结点:一棵树中,没有双亲结点的结点;如上图:A
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
以下概念仅做了解即可:
- 非终端节点或分支节点:度不为0的节点; 如上图:
D、E、F、G...
等节点为分支节点;- 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:
B、C
是兄弟节点;- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:
H、I
互为兄弟节点;- 节点的祖先:从根到该节点所经分支上的所有节点;如上图:
A
是所有节点的祖先;- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙;
- 森林:由
m(m>=0)
棵互不相交的树的集合称为森林。
二:二叉树
2.1 二叉树的遍历:
使用前序、中序、后续来遍历二叉树
- 前序遍历:先输出父节点,再遍历左子树和右子树
- 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
- 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点
小结:看输出父节点的顺序,就确定是前序、中序还是后序。
分析二叉树的前序,中序,后序的遍历步骤:
1.创建一颗二叉树
2.前序遍历
- 2.1先输出当前节点(初始的时候是root节点)
- 2.2如果左子节点不为空,则递归继续前序遍历
- 2.3如果右子节点不为空,则递归继续前序遍历
3.中序遍历
- 3.1如果当前节点的左子节点不为空,则递归中序遍历
- 3.2输出当前节点
- 3.3如果当前的右子节点不为空,则递归中序遍历
4.后序遍历
- 4.1如果当前节点的左子节点不为空,则递归后序遍历
- 4.2如果当前节点的右子节点不为空,则递归后序遍历
- 4.3输出当前节点
5.前、中、后序遍历代码实现:
- public class BinaryTreeDemo {
- public static void main(String[] args) {
- //先需要创建一颗二叉树
- BinaryTree binaryTree=new BinaryTree();
- //创建需要的结点
- HeroNode root=new HeroNode(1,"宋江");
- HeroNode node2=new HeroNode(2,"吴用");
- HeroNode node3=new HeroNode(3,"卢俊义");
- HeroNode node4=new HeroNode(4,"林冲");
- HeroNode node5=new HeroNode(5,"武松");
- HeroNode node6=new HeroNode(6,"鲁智深");
- HeroNode node7=new HeroNode(7,"杨志");
-
- //说明,我们先手动创建该二叉树,后面我们学习递归的方式创建而常数
- root.setLeft(node2);
- root.setRight(node3);
- node2.setLeft(node4);
- node2.setRight(node5);
- node3.setLeft(node6);
- node3.setRight(node7);
-
- binaryTree.setRoot(root);
-
- //测试:
- System.out.println("前序遍历");//1,2,4,5,3,6,7
- binaryTree.preOrder();
-
- System.out.println("中序遍历");//4,2,5,1,6,3,7
- binaryTree.infixOrder();
- System.out.println("后序遍历");//4,5,2,6,7,3,1
- binaryTree.postOrder();
- }
- }
-
- //定义二叉树
- class BinaryTree{
- private HeroNode root;//根节点
-
- public void setRoot(HeroNode root){
- this.root=root;
- }
- //前序遍历
- public void preOrder(){
- if(this.root!=null){
- this.root.preOrder();
- }else {
- System.out.println("二叉树为空,无法遍历");
- }
- }
- //后序遍历
- public void infixOrder(){
- if(this.root!=null){
- this.root.infixOrder();
- }else {
- System.out.println("二叉树为空,无法遍历");
- }
- }
- //后序遍历
- public void postOrder(){
- if(this.root!=null){
- this.root.postOrder();
- }else {
- System.out.println("二叉树为空,无法遍历");
- }
- }
-
- }
-
- //先创建HeroNode结点
- class HeroNode{
- private int no;
- private String name;
- private HeroNode left;//默认为null
- private HeroNode right;//默认为null
-
- public HeroNode(int no, String name) {
- this.no = no;
- this.name = name;
- }
-
- public int getNo() {
- return no;
- }
-
- public void setNo(int no) {
- this.no = no;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public HeroNode getLeft() {
- return left;
- }
-
- public void setLeft(HeroNode left) {
- this.left = left;
- }
-
- public HeroNode getRight() {
- return right;
- }
-
- public void setRight(HeroNode right) {
- this.right = right;
- }
-
- @Override
- public String toString() {
- return "HeroNode [no=" + no + ", name=" + name + "]";
- }
-
- //编写前序遍历的方法
- public void preOrder(){
- System.out.println(this);//先输出父结点(谁调用这个方法,谁就是this)
- //递归向左子树前序遍历
- if(this.left!=null){
- this.left.preOrder();
- }
- //递归向右子树前序遍历
- if(this.right!=null){
- this.right.preOrder();
- }
- }
- //编写中序遍历的方法
- public void infixOrder(){
- //递归向左子树中序遍历
- if(this.left!=null){
- this.left.infixOrder();
- }
- //输出父结点
- System.out.println(this);
- //递归向右子树中序遍历
- if(this.right!=null){
- this.right.infixOrder();
- }
- }
- //编写后序遍历的方法
- public void postOrder(){
- //递归向左子树后序遍历
- if(this.left!=null){
- this.left.postOrder();
- }
- //递归向右子树后序遍历
- if(this.right!=null){
- this.right.postOrder();
- }
- //输出父结点
- System.out.println(this);
- }
- }
2.2 二叉树的查找:
目的:使用前序、中序、后序的方式来查询指定的节点。
前序查找思路:
- 先判断当前节点的no是否等于要查找的
- 如果是相等的,则返回当前节点
- 如果不相等,则判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
- 如果左递归前序查找,找到节点,则返回,否则继续判断,当前的节点的右子节点是否为空,如果不为空,则继续向右递归前序查找
中序查找思路:
- 判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
- 如果找到,则返回,如果没有找到,就和当前节点比较,如果是则返回当前节点,否则继续进行右递归的中序查找
- 如果右递归中序查找,找到就返回,否则为null
后序查找思路:
- 判断当前节点的左子节点是否为空,如果不为空,则递归后序查找
- 如果找到,就返回,如果没有找到,就判断当前节点的右子节点是否为空,如果不为空,则右递归进行后序查找,如果找到,就返回
- 就和当前节点进行,如果找到则返回,否则返回null
查找指定节点的部分代码实现:
- //编写前序查找的方法
- public HeroNode preSearch(int no){
- //先判断当前节点是否符合
- if(this.no==no){
- return this;
- }
- HeroNode resNode=null;
- //向左递归
- if(this.left!=null){
- resNode=this.left.preSearch(no);
- }
- //向右递归(判断左递归得到的节点是否为空,如果为空,则进行右递归)
- if(resNode==null&&this.right!=null){
- resNode=this.right.preSearch(no);
- }
- return resNode;//如果最终还是没有找到,则返回null
- }
-
- //编写中序查找的方法
- public HeroNode infixSearch(int no){
- HeroNode resNode=null;
- if(this.left!=null){
- resNode=this.left.infixSearch(no);
- }
- if(this.no==no){
- return this;
- }
- if(this.right!=null&&resNode==null){//左边没有找到,找右边
- resNode=this.right.infixSearch(no);
- }
- return resNode;//不管有没有找到,都返回resNode
- }
- //编写后序查找的方法
- public HeroNode postSearch(int no){
- HeroNode resNode=null;
- if(this.left!=null){
- resNode=this.left.postSearch(no);
- }
- if(this.right!=null&&resNode==null){
- resNode=this.right.postSearch(no);
- }
- if(this.no==no){
- return this;
- }
- return resNode;
- }
2.3 二叉树删除节点:(简单版)
规定:如果要删除的节点是叶子节点,则直接删除,如果是非叶子节点,则删除该子树。
思路:
首先先处理:
考虑如果树是空树root,如果只有一个root节点,则等价将二叉树置空。
如果不是,则进行下面步骤:
- 因为我们的二叉树是单向的,所以我们是判断当前节点的子节点是否需要删除节点,而不能去判断当前这个节点是不是需要删除节点。
- 如果当前节点的左子节点不为空,并且左子节点就是要删除节点,就将this.left=null;并且就返回(结束递归删除)
- 如果当前节点的右子节点不为空,并且右子节点就是要删除节点,就将this.right=null,并且就返回(结束递归删除)
- 如果第2和第3步没有删除节点,那么我们就需要向左子树进行递归删除。
- 如果第4步也没有删除节点,则应当向右子树进行递归删除。
节点删除的代码实现:
- //二叉树部分:删除节点
- public void deleteNode(int no){
- if(root!=null){
- //如果只有一个root节点,这里立即判断root是不是要删除节点
- if(root.getNo()==no){
- root=null;
- }else {
- //递归删除
- root.deleteNode(no);
- }
- }else {
- System.out.println("二叉树为空,删除失败");
- }
- }
- //链表部分:
- //递归删除节点
- public void deleteNode(int no){
- //判断当前节点的左子节点
- if(this.left!=null&&this.left.no==no){
- this.left=null;
- return;
- }
- //判断当前节点的右子节点
- if(this.right!=null&&this.right.no==no){
- this.right=null;
- return;
- }
- //左子树递归删除
- if(this.left!=null){
- this.left.deleteNode(no);
- }
- //右子树递归删除
- if(this.right!=null){
- this.right.deleteNode(no);
- }
-
- }
2.4 顺序存储二叉树:
基本说明:从数据存储来啊看,数组存储方式和树的存储方式可以相互转化,即数组可以转换成树,也可以转换成数组。
要求:
(1)图中的二叉树的节点,要求以数组的方式来存放arr【1,2,3,4,5,6,7】
(2)要求在遍历数组arr时,仍然可以以前序遍历、中序遍历和后序遍历的范围广是完成节点的遍历。
思路:
- 顺序二叉树通常只考虑完全二叉树。
- 数组下标为n的元素所对应的左子节点所在的下标为(2*n+1);
- 数组下标为n的元素所对应的右子节点所在的下标为(2*n+2);
- 数组下标为n的元素所对应的父节点为(n-1)/2;
提示:(可以自己对着上图验证一下)
顺序存储二叉树的代码实现:
- public class ArrayBinaryTreeDemo {
- public static void main(String[] args) {
- int[] arr={1,2,3,4,5,6,7};
- ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(arr);
- arrayBinaryTree.preOrder(); //1,2,4,5,3,6,7
- }
- }
- //编写一个ArrayBinaryTree,实现顺序存储二叉树遍历
- class ArrayBinaryTree{
- private int[] arr;//存储数据节点的数组
- public ArrayBinaryTree(int[] arr){
- this.arr=arr;
- }
-
- //编写一个方法,完成顺序存储二叉树的前序遍历
-
- //重载preOrder
- public void preOrder(){
- this.preOrder(0);
- }
- public void preOrder(int index){
- //如果数组为空,或arr.length=0
- if(arr==null || arr.length==0){
- System.out.println("数组为空,不能按照二叉树的前序遍历");
- }
- //输出当前这个元素
- System.out.println(arr[index]);
- //向左递归遍历
- if((index*2+1)<arr.length){
- preOrder(2*index+1);
- }
- //向右递归遍历
- if((index*2+2)<arr.length){
- preOrder(2*index+2);
- }
- }
-
- }
2.5 线索化二叉树:
问题分析:
- 当我们对上面的二叉树进行中序遍历时,数列为{8,3,10,1,6,14};
- 但是6,8,10,14这几个节点的左右指针,并没有完全的利用上
- 如果我们希望充分利用各个节点的左右指针,让各个节点可以指向自己的前后节点,怎么办?
- 解决方案:线索化二叉树
使用场景:
- 线索化的实质就是将二叉链表中的空指针改为指向前驱节点或后继节点的线索;
- 线索化的过程就是修改二叉链表中空指针的过程,可以按照前序、中序、后序的方式进行遍历,分别生成不同的线索二叉树;
- 有了线索二叉树之后,我们再次遍历时,就相当于操作一个双向链表。
- 使用场景:如果我们在使用二叉树过程中经常需要遍历二叉树或者查找节点的前驱节点和后继节点,可以考虑采用线索二叉树存储结构。
线索二叉树基本介绍:
- n个节点的二叉链表中含有(n+1)【公式:2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该节点在某种遍历次序下的钱去和后继节点的指针(这种附加的指针称为“线索”)。
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Thread BinartTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
- 一个节点的前一个节点,称为前驱节点。
- 一个节点的后一个节点,称为后继节点。
思路分析:
说明:当线索化二叉树后,Node节点的属性left和right,有如下情况:
(1)left指向的是左子树,也可能是指向的前驱节点,比如 1 节点left指向的左子树,而 10 节点的left指向的就是前驱节点。
(2)rigth指向的是右子树,也可能是指向后继节点,比如 1 节点right指向的是右子树,而 10 节点的right指向的是后继节点。
- public class ThreadedBinaryTreeDemo {
- /*
- 中序线索化二叉树
- */
- public static void main(String[] args) {
- HeroNode root=new HeroNode(1,"tom");
- HeroNode node2=new HeroNode(3,"jack");
- HeroNode node3=new HeroNode(6,"smith");
- HeroNode node4=new HeroNode(8,"mary");
- HeroNode node5=new HeroNode(10,"king");
- HeroNode node6=new HeroNode(14,"dim");
-
- //二叉树,后面我们要递归创建,现在简单处理使用手动创建
- root.setLeft(node2);
- root.setRight(node3);
- node2.setLeft(node4);
- node2.setRight(node5);
- node3.setLeft(node6);
-
- BinaryTree binaryTree = new BinaryTree();
- binaryTree.setRoot(root);
- binaryTree.threadedNodes();
-
- //测试:
- HeroNode leftNode = node5.getLeft();
- HeroNode rightNode = node5.getRight();
-
- System.out.println("10 节点的前驱节点:"+leftNode);
- System.out.println("10 节点的后继节点:"+rightNode);
-
- }
- }
-
- class BinaryTree{
- private HeroNode root;//根节点
-
- //为了实现线索化,需要创建要给指向当前节点的前驱节点的指针
- //在递归进行线索化时,pre总是保留前一个节点
- private HeroNode pre=null;
-
- public void setRoot(HeroNode root){
- this.root=root;
- }
-
- //重载threadedNodes方法
- public void threadedNodes(){
- this.threadedNodes(root);
- }
-
- //编写对二叉树进行中序线索化的方法
-
- /**
- *
- * @param node:就是当前需要线索化的节点
- */
- public void threadedNodes(HeroNode node){
- //如果node==null,不能线索化
- if(node==null){
- return;
- }
- //(一)先线索化左子树
- threadedNodes(node.getLeft());
- //(二)线索化当前节点
-
- //处理当前节点的前驱节点
- //以8节点来理解
- //8节点的.left=null,8节点的.leftType=1
- if(node.getLeft()==null){
- //让当前节点的左指针指向前驱节点
- node.setLeft(pre);
- //修改当前节点的左指针的类型,指向前驱节点
- node.setLeftType(1);
- }
-
- //处理后继节点
- if(pre!=null&&pre.getRight()==null){
- //让前驱节点的右指针指向当前节点
- pre.setRight(node);
- //修改前驱节点的右指针类型
- pre.setRightType(1);
- }
- //!!! 每处理一个节点后,让当前节点是下一个节点的前驱节点
- pre=node;
-
- //(三)再线索化右子树
- threadedNodes(node.getRight());
- }
-
-
- }
-
-
- //先创建HeroNode结点
- class HeroNode{
- private int no;
- private String name;
- private HeroNode left;//默认为null
- private HeroNode right;//默认为null
- //说明
- //1.如果leftType==0表示指向的是左子树,1则表示指向前驱节点
- //2.如果rightType==0表示指向是右子树,1则表示指向后继节点
- private int leftType;
- private int rightType;
-
- public HeroNode(int no, String name) {
- this.no = no;
- this.name = name;
- }
-
- public int getLeftType() {
- return leftType;
- }
-
- public void setLeftType(int leftType) {
- this.leftType = leftType;
- }
-
- public int getRightType() {
- return rightType;
- }
-
- public void setRightType(int rightType) {
- this.rightType = rightType;
- }
-
- public int getNo() {
- return no;
- }
-
- public void setNo(int no) {
- this.no = no;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public HeroNode getLeft() {
- return left;
- }
-
- public void setLeft(HeroNode left) {
- this.left = left;
- }
-
- public HeroNode getRight() {
- return right;
- }
-
- public void setRight(HeroNode right) {
- this.right = right;
- }
-
- @Override
- public String toString() {
- return "HeroNode [no=" + no + ", name=" + name + "]";
- }
- }
遍历线索化二叉树:
说明:对前面的中序线索化的二叉树,进行遍历
分析:因为线索化后,各个节点指向有变化,因此原来的遍历方式不能使用,这是需要使用新的方式遍历线索化二叉树,各个节点开头通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。遍历的次序应当和中序遍历保持一致。
代码实现:
- //遍历线索化二叉树
- public void threadedList(){
- //定义一个变量,存储当前遍历的节点,从root开始
- HeroNode node=root;
- while(node!=null){
- //循环的找到leftType==1的节点,第一个找到就是8节点
- //后面随着遍历而变化,因为当leftType==1是,说明该节点按照线索化处理后的有效节点
- while(node.getRightType()==0){
- node=node.getLeft();
- }
-
- //打印当前这个节点
- System.out.println(node);
- //如果当前节点的右指针指向的是后继节点,就一直输出
- while (node.getRightType()==1){
- //获取到当前节点的后继节点
- node=node.getRight();
- System.out.println(node);
- }
- //替换这个遍历的节点
- node=node.getRight();
- }
- }