目录
一、为什么需要树这种数据结构:
二、树的常用术语:
树的定义:
一棵树(tree)是一些节点的集合。这个集合可以是空集;若不是空集,则树由称作根(root)的节点以及0个或多个非空的(子)树组成。
树的常用术语:
节点:树中的一个连接点
根节点: 最顶层的节点
父节点:若一个节点含有子节点,则这个节点成为其子节点的父节点
子节点:子节点是相对于父节点来说的,它是父节点的下一层节点。
叶子节点:最后一层的节点 即没有子节点的节点
节点的权:节点的具体值
节点的度:子节点的个数
路径:从根节点到某一个具体节点所走过的路
层:根结点在1层,其它任一结点的层数是其父结点的层数加1
子树:只要包含了一个结点,就得包含这个结点下的所有节点.
高度:从根节点向下到某个叶节点最长简单路径中边的条数
树的高度:树内所有节点高度的最大值,也就是根节点的高度,也就是树的层数
树的深度:树内所有节点深度的最大值,也就是所有叶子节点深度的最大值,也就是树的层数
森林:多颗子树组成森林
三、二叉树的概念:
二叉树:是指树中的节点最多只能有两个子节点的有序树。
二叉树特点:二叉树的子节点分为左节点和右节点
四、二叉树分类:
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
完全二叉树:如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
五、二叉树的遍历:
二叉树遍历:
遍历是对树的一种最基本的运算,所谓遍历二叉树,就是按一定的规则和顺序走遍二叉树的所有节点,使每一个节点都被访问一次,而且只被访问一次。由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个节点转换成为一个线性序列来表示。
- 前序遍历:
访问顺序:先根节点,再左子树,最后右子树;
- 中序遍历
访问顺序:先左子树,再根节点,最后右子树;
- 后序遍历
访问顺序:先左子树,再右子树,最后根节点
思路很简单,就是递归调用往下找,直至找到叶子结点,然后根据遍历顺序输出即可.
小结:看输出父节点的顺序,就确定是前序,中序,还是后序
代码演示遍历:
package com.fan.tree;
public class BinaryTreeTest {
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, "林冲");
//手动的方式创建二叉树,并手动挂载左右节点
binaryTree.root = root;//一个团体应该现有一个领袖root
//然后是领袖指派他的左膀右臂是谁
root.left = node2;
root.right = node3;
node3.right = node4;
//测试
System.out.println("前序遍历:");//1,2,3,4
binaryTree.preOrder();
System.out.println("中序遍历:");//2,1,3,4
binaryTree.infixOrder();
System.out.println("后序遍历:");//2,4,3,1
binaryTree.postOrder();
}
}
//树 结构
class BinaryTree{
//最基本的树要有根节点的,开始为空
HeroNode root = null;
//树的前序遍历
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("二叉树为空,无法遍历");
}
}
}
//节点类/结点类
class HeroNode{
//类似于链表的节点
int no;
String name;
//类似于左指针和右指针
HeroNode left;//默认为null
HeroNode right;//默认为null
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
//编写前序遍历的方法:根 -->左-->右
public void preOrder(){
//根
System.out.println(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);
}
}
二叉树节点查找:
二叉树前序中序后序查找的代码实现:
package com.fan.tree;
public class BinaryTreeTest {
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, "关胜");
//手动的方式创建二叉树,并手动挂载左右节点
binaryTree.root = root;//一个团体应该现有一个领袖root
//然后是领袖指派他的左膀右臂是谁
root.left = node2;
root.right = node3;
node3.left = node5;
node3.right = node4;
//测试
System.out.println("前序遍历:");//1,2,3,4
binaryTree.preOrder();
System.out.println("中序遍历:");//2,1,3,4
binaryTree.infixOrder();
System.out.println("后序遍历:");//2,4,3,1
binaryTree.postOrder();
//前序遍历查找方法的调用测试
System.out.println("前序遍历方式查找---");
int no =15;//要查找的key
HeroNode resNode = binaryTree.preOrderSearch(no);//这里使用前序遍历进行查找
if(resNode != null){
System.out.println("找到了,信息为:"+resNode);
}else{
System.out.println("没有找到编号为"+no+"的英雄!!!");
}
//中序遍历查找方法的调用测试
System.out.println("中序遍历方式查找---");
int inno =5;//要查找的key
//这里使用中序遍历进行查找
HeroNode inresNode = binaryTree.infixOrderSearch(inno);
if(inresNode != null){
System.out.println("找到了,信息为:"+inresNode);
}else{
System.out.println("没有找到编号为"+inno+"的英雄!!!");
}
//后序遍历查找方法的调用测试
System.out.println("后序遍历方式查找---");
int postno =3;//要查找的key
HeroNode postresNode = binaryTree.postOrderSearch(postno);
if(postresNode != null){
System.out.println("找到了,信息为:"+postresNode);
}else{
System.out.println("没有找到编号为"+postno+"的英雄!!!");
}
}
}
//树 结构
class BinaryTree{
//最基本的树要有根节点的,开始为空
HeroNode root = null;
//树的前序遍历
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("二叉树为空,无法遍历");
}
}
//前序遍历的查找方法
public HeroNode preOrderSearch(int no){
if(root != null){
return root.preOrderSearch(no);
}else{
return null;
}
}
//二叉树中的中序遍历方法
public HeroNode infixOrderSearch(int no){
//从root开始遍历
if(root != null){
return root.infixOrderSearch(no);
}else{
return null;
}
}
//二叉树中的后序遍历方法
public HeroNode postOrderSearch(int no){
if(root != null){
return root.postOrderSearch(no);
}else{
return null;
}
}
}
//节点类/结点类
class HeroNode{
//类似于链表的节点
int no;
String name;
//类似于左指针和右指针
HeroNode left;//默认为null
HeroNode right;//默认为null
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
//编写前序遍历的方法:根 -->左-->右
public void preOrder(){
//根
System.out.println(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);
}
/*注意,前序查找方法也是写在节点类中的,
因为这个节点类形成了一个小三角树,可以进行一些节本的操作
*/
/**
* 输入/** ,点击“Enter”,自动根据参数和返回值生成注释模板
* @param no 查找no
* @return 如果找到就返回该NOde,没有找到返回null
*/
public HeroNode preOrderSearch(int no){//前序查找:根,左右
//比较当前节点是不是,this代表:谁调用我我就代表谁
//根:第一次是root调用的,以后调用都是父节点调用
//注意,除了root节点外,其他节点既是父节点又是子节点
if(this.no == no){
return this;
}
//1.判断当前节点的左子节点 是否为空,如果不为空,则递归前序查找
HeroNode resNode = null;
//左:
if(this.left != null){
resNode = this.left.preOrderSearch(no);
}
//2.如果左递归前序查找,找到节点,则返回
if(resNode != null){//说明左子树 找到了
return resNode;
}
//右:
//3.判断当前节点的右子树 是否为空,如果不为空,则继续向右递归前序查找
if(this.right != null){
resNode = this.right.preOrderSearch(no);
}
return resNode;
}
//中序遍历查找方法
/**
*
* @param no 要查找的key编号
* @return 返回查找到的节点
*/
public HeroNode infixOrderSearch(int no){
//思路:中序查找是:左根右的递归,按照这个次序来
HeroNode resNode = null;//这个resNode主要是用来接收左遍历的结果的
//左:
if(this.left != null){
//如果左边节点不为空的话,一直递归左边节点
resNode = this.left.infixOrderSearch(no);
}
//左边查找到了,就没有必要去右边继续递归查找了
if(resNode != null){
return resNode;
}
//根
if(this.no == no){
return this;//终结的一个地方
}
//右
if(this.right != null){
resNode = this.right.infixOrderSearch(no);
}
return resNode;
}
//后序遍历查找
/**
*
* @param no
* @return
*/
public HeroNode postOrderSearch(int no){
//思路:左右根顺序
HeroNode resNode = null;
//左
if(this.left != null){
resNode = this.left.postOrderSearch(no);
}
if(resNode != null){//左子树找到了,就没有必要走后面的代码了,直接返回
return resNode;
}
//右:在左子树没有找到的时候,继续向 右子树 遍历查找
if(this.right != null){
resNode = this.right.postOrderSearch(no);
}
if(resNode != null){//如果右子树找到了,也没有必要走后面代码,直接返回
return resNode;
}
//根:如果左右子树都没有找到,就比较当前节点是不是
if(this.no == no){
return this;
}
return resNode;
}
}
二叉树的简单删除:
节点类中的删除代码:
//二叉树的删除节点
public void delNode(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.delNode(no);
}
//向右子树 递归删除
if(this.right != null){
this.right.delNode(no);
}
}
二叉树中的节点删除代码:
//二叉树类中的删除节点
public void delNode(int no){
if(root != null){
//如果只有一个root,这里立即判断root是不是要删除的节点
if(root.no == no){
root =null;
}else{
//递归删除
root.delNode(no);
}
}else{
System.out.println("空数,不能删除---");
}
}
测试类:
//测试删除节点
System.out.println("删除前,前序遍历:");
binaryTree.preOrder();
//binaryTree.delNode(5);
binaryTree.delNode(3);
System.out.println("删除后,前序遍历:");
binaryTree.preOrder();
顺序存储的二叉树:
二叉树的存储结构有两种,分别为顺序存储和链式存储。这里介绍二叉树的顺序存储结构。
二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。
普通二叉树转完全二叉树的方法很简单,只需给二叉树额外添加一些节点,将其"拼凑"成完全二叉树即可。如图 1 所示:
图 1 中,左侧是普通二叉树,右侧是转化后的完全(满)二叉树。
解决了二叉树的转化问题,接下来学习如何顺序存储完全(满)二叉树。
顺序存储完全(满)二叉树:
完全二叉树的顺序存储,仅需从根节点开始,按照层次依次(从每一层从左到右)将树中节点存储到数组即可。
完全二叉树的顺序存储 特点:(通常用于完全二叉树 避免内存浪费)
代码演示:
package com.fan.tree;
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
arrBinaryTree.preOrder();
}
}
//编写一个ArrBinaryTree,实现顺序存储二叉树 遍历
class ArrBinaryTree{
private int [] arr;//存储数据节点的数组
public ArrBinaryTree(int[] arr) {
this.arr = arr;
}
//重载preorder
public void preOrder(){
this.preOrder(0);//从数组的第一个元素开始遍历
}
//编写一个方法,完成顺序存储二叉树的前序遍历
/**
* 方法作用:根据数组的下标进行前序遍历
* @param index:数组下标,
*/
public void preOrder(int index){
//如果数组为空,或者arr.length = = 0
if(arr == null || arr.length == 0){
System.out.println("数组为空,不能前序遍历");
}
//根:
//输出当前这个元素,输出第一个元素arr[0]
System.out.println(arr[index]);
//左:向左递归遍历,在数组中找到左子树进行遍历
//利用当前节点下标index求出当前节点的左子节点 下标位置 :index * 2 + 1
if((index * 2 + 1) < arr.length){
preOrder(2*index+1);
}
//右:向右 递归进行遍历
//利用当前节点下标index求出当前节点的右子节点 下标位置 :index * 2 + 2
if((index * 2 + 2)<arr.length){
preOrder(index * 2 + 2);
}
}
}
练习题代码:中序/后序遍历顺序存储二叉树
package com.fan.tree;
public class ArrBinaryTreeDemo2 {
public static void main(String[] args) {
int [] arr ={};
ArrBinaryTree2 arrBinaryTree2 = new ArrBinaryTree2(arr);
System.out.println("前序遍历:");
arrBinaryTree2.infixOrder();//直接开始index=0的
//测试后序遍历
System.out.println("后序遍历:");
arrBinaryTree2.postOrder();//直接开始index=0的
}
}
//顺序存储的二叉树 :底层用数组存储 二叉树
class ArrBinaryTree2{
int[] arr;
public ArrBinaryTree2(int[] arr) {
this.arr = arr;
}
//重载
public void infixOrder(){
this.infixOrder(0);
}
//中序遍历此二叉树
/**
* 方法作用是:使用二叉树的中序遍历去 遍历 顺序存储的二叉树
* @param index:数组的下标
*/
public void infixOrder(int index){
//判断数组为空的情况
if(arr == null || arr.length == 0) {
System.out.println("数组为空,不能遍历!!!");
return;
}
//思路:左 根 右
//左:向左递归遍历
if((2*index+1)<arr.length){//下标不越界时
infixOrder(2*index+1);
}
//根:直接输出
System.out.println(arr[index]);
//右:向右递归遍历
if((2*index+2)<arr.length){
infixOrder(2*index+2);
}
}
//后序遍历此顺序二叉树
public void postOrder(){
postOrder(0);
}
public void postOrder(int index){
//先判断数组为空的情况
if(arr ==null || arr.length==0){
System.out.println("数组为空,不能遍历!!!");
return;
}
//思路:左右根
//左递归遍历
if((index*2+1)<arr.length){
postOrder(index*2+1);
}
//右递归遍历
if((index*2+2)<arr.length){
postOrder(index*2+2);
}
//根:直接输出数组元素
System.out.println(arr[index]);
}
}
线索二叉树:
在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
二叉树的遍历本质上是将一个复杂的非线性结构转换为线性结构,使每个结点都有了唯一前驱和后继(第一个结点无前驱,最后一个结点无后继)。对于二叉树的一个结点,查找其左右子女是方便的,其前驱后继只有在遍历中得到。为了容易找到前驱和后继,有两种方法。一是在结点结构中增加向前和向后的指针,这种方法增加了存储开销,不可取;二是利用二叉树的空链指针。
线索二叉树的优点和不足:
优势:
(1)利用线索二叉树进行中序遍历时,不必采用堆栈处理,速度较一般二叉树的遍历速度快,且节约存储空间。
(2)任意一个结点都能直接找到它的前驱和后继结点。
不足:
(1)结点的插入和删除麻烦,且速度也较慢。
(2)线索子树不能共用。
线索二叉树存储结构:
线索二叉树中的线索能记录每个结点前驱和后继信息。为了区别线索指针和孩子指针,在每个结点中设置两个标志ltag和rtag。
当tag和rtag为0时,leftChild和rightChild分别是指向左孩子和右孩子的指针;否则,leftChild是指向结点前驱的线索(pre),rightChild是指向结点的后继线索(suc)。由于标志只占用一个二进位,每个结点所需要的存储空间节省很多。 [3]
现将二叉树的结点结构重新定义如下:
可以将一棵二叉树线索化为一棵线索二叉树,那么新的问题产生了。我们如何区分一个结点的lchild指针是指向左孩子还是前驱结点呢?例如:对于图2.2所示的结点E,如何区分其lchild的指向的结点J是其左孩子还是前驱结点呢?
为了解决这一问题,现需要添加标志位ltag,rtag。并定义规则如下:
ltag为0时,指向左孩子,为1时指向前驱
rtag为0时,指向右孩子,为1时指向后继
添加ltag和rtag属性后的结点结构如下:
代码演示中序遍历线索化二叉树:
package com.fan.tree;
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
//测试一下中序线索二叉树的功能
Hero root = new Hero(1, "tom");
Hero node2 = new Hero(3, "jack");
Hero node3 = new Hero(6, "sminm");
Hero node4 = new Hero(8, "mary");
Hero node5 = new Hero(10, "king");
Hero node6 = new Hero(14, "dim");
//二叉树,这里手动创建
root.left = node2;
root.right = node3;
node2.left = node4;
node2.right = node5;
node3.left = node6;
//测试中序线索化
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.root = root;//设置根节点
threadedBinaryTree.inthreadedNode();
//测试:以10号节点测试
Hero leftNode = node5.left;
Hero rightNode = node5.right;
System.out.println("10号节点的前驱节点是:"+leftNode);//3
System.out.println("10号节点的后驱节点是:"+rightNode);//1
}
}
class ThreadedBinaryTree{
Hero root;//二叉树一定要有root根节点
/*
为了实现线索化,需要创建要给指向当前节点的前驱节点
的指针,在递归进行线索化时,pre总是保留前一个节点
*/
Hero pre = null;
//编写对二叉树进行中序线索化的方法
public void inthreadedNode(){//重载一下inthreadedNode
this.inthreadedNode(root);
}
/**
*
* @param node:就是当前需要线索化的节点
*/
public void inthreadedNode(Hero node){//开始是节点3,再是节点8
//如果node==null,不能线索化
if(node == null){
return;
}
//1.先线索化左子树
inthreadedNode(node.left);
//2.线索化当前节点
//处理当前节点的前驱节点
/*
以8节点来理解,8节点的left=null,8节点的ltag=1
*/
if(node.left == null){//只有当左孩子为空的时候才处理前驱
//让当前节点的左指针指向前驱节点
node.left = pre;//左指针指向的现在是前驱节点了
//修改当前节点的左指针类型,指向前驱节点
node.ltag = 1;//1代表前驱
}
/*
处理后继节点:当 当前节点的前驱节点的右指针为空时才处理
*/
if(pre != null && pre.right == null){
//让前驱节点的右指针指向当前节点
pre.right = node;
pre.rtag = 1;
}
//每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;//node在遍历的时候移动,pre也要跟着移动
//3.再线索化右子树
inthreadedNode(node.right);
}
}
class Hero{
int no;
String name;
Hero left;//left指针域
Hero right;//right指针域
//标志位:
/*
ltag为0时,指向左孩子,为1时指向前驱
rtag为0时,指向右孩子,为1时指向后继
*/
byte ltag;//我们想要标志位尽量少占用空间
byte rtag;
public Hero(int no, String name) {
this.no = no;
this.name = name;
}
//节点类有前序后续中序的遍历查找方法和 遍历方法
@Override
public String toString() {
return "Hero{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
遍历线索化二叉树:
遍历中序线索二叉树
中序遍历:先遍历左子树,再输出父结点,再遍历右子树
但是因为我们的叶子结点线索化了,左右结点不为null,如果还用以前的遍历方法会造成死循环
根据left/rightType判断是否为子结点,决定是否遍历
在ThreadedBinaryTree添加遍历方法:
代码演示:
//遍历中序线索二叉树
public void threadedList(){
//设置一个变量,存储当前遍历的结点,从root开始
HeroNode node = root;
while (node != null){
//左:
//循环找到 left/rightType == 1 的结点
//当left/rightType == 1,说明该结点是线索化的结点
while (node.getLeftType() == 0){
node = node.getLeft();
}
//根:打印当前结点
System.out.println(node);
//右:
//如果当前结点的右指针指向后继结点,就输出
while (node.getRightType() == 1){
//获取到当前结点的后继结点
node = node.getRight();
System.out.println(node);
}
//替换遍历的结点
node = node.getRight();
}
}
测试代码:
System.out.println("== 遍历线索化二叉树 ==");
threadedBinaryTree.threadedList();
遍历结果:8,3,10,1,14,6,符合中序遍历。