前面我们学了不同的数据结构,今天学习的是一种特别的数据结构:树
首先我们思考一下:什么是树呢?为什么我们需要学习这种数据结构呢?
一、为什么需要树这种数据结构
我们对比之前学习的数据结构,分析看看之前的数据结构有什么特点又有什么缺陷
一、数组存储方式的分析
优点:通过下标方式访问元素
,速度快。对于有序数组
,可以使用二分查找
提高检索速度。
缺点:如果要操作具体插入值
,那么会整体移动(按一定顺序),效率较低
假如我当前有数组arr {1,3,5,8,10}
,若此时插入数据:6
那么能放的进去吗?
实际上是不能
的,因为数组是事先分配空间
的,指说原创建好空间长度就不能动态增长
,但是数组在动态添加数据
的时候,底层有一个动作:数组扩容
。
那么是如何扩容的呢?创建新的数组,并将数据拷贝,以及插入数据后移
那么这时会有小伙伴提出:我们使用集合ArrayList不是可以动态增长吗?
其实我们观察集合ArrayList,发现也维护了数组扩容,只是策略不同。
那么我们一起看看ArrayList 源码
private static final Object [] DEFAULTCAPACITY_ EMPTY_ ELEMENTDATA = {};
//ArrayList的构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_ EMPTY_ ELEMENTDATA ;
}
我们发现ArrayList无参构造器,上来时将空数值给到elementData数组。
那么elementData是什么?
transient 0bject [] elementData;
其实是一个对象Object数组
,也就是说ArrayList维护的0bject [] elementData数组
在ArrayList容量不够的时候,有方法grow()按照不同的策略进行扩容,但仍然是一个数组扩容
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity - oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copy0f(elementData, newCapacity);
}
小结
- ArrayList 中维护了一个
Object类型的数组elementData
. - 当创建对象时, 如果使用的是
无参构造器
,则初始elementData容量为0 (jdk7是10)
- 如果使用的是
指定容量capacity的构造器
,则初始elementData 容量为capacity
. - 当
添加元素
时: 先判断是否需要扩容
,如果需要扩容
,则调用grow方法,否则直接添加元素到合适位置
- 如果使用的是
无参构造器
,如果第一次添加
,需要扩容
的话,则扩容elementData为10
,如果需要再次扩容的话
,则扩容elementData为1.5倍
。 - 如果使用的是
指定容量capacity的构造器
,如果还需要扩容,则直接扩容elementData为1.5倍。
我们发现ArrayList为了解决扩容,按照一种策略进行的,还是会整体移动的,效率比较低,于是我们看看链式存储方式能不能更好解决问题
二、链式存储方式的分析
优点:在一定程度上对数组存储方式有优化
(比如:插入数值
节点,只需要将插入节点,链接到链表中
即可,删除效率也很好)。
缺点:在进行检索时,效率仍然较低(比如:检索某个值
,需要从头节点开始
遍历)
我们发现数组与链式都有各自的优点与缺点,那么接下来介绍新的数据结构:树
有什么不同呢?
二、什么是树?
在树的家族中,有一种高频使用
的一种树结构
:二叉树
。
在二叉树
中,每个节点最多有两个分支
,即每个节点最多有两个节点,分别称为左节点与右节点
。
在二叉树中,还有两个特殊的类型:满二叉树与完全二叉树
。
满二叉树:除了叶子节点外,所有节点都有两个节点
。
完全二叉树:除了最后一层以外,其他层节点个数都达到最大,并且最后一层的叶子节点向左排列
完全二叉树看上去并不完全,为什么这么称呼它?
这和存储二叉树的两种存储方法:链式存储法与数组顺序法
。有关了
基于指针的链式存储法
,也就是像链表
一样,每个节点有三个字段
。一个存储数据,两外两个分别存储指向左右节点的指针
,如下图所示
基于数组的顺序存储法
,就是按照规律把节点存放在数组里
,如下图所示。为了方便按照规律计算,把起始数据放在下标为一的位置上
若是非完全二叉树则会浪费大量的数组存储空间,如下图所示
树的基本操作
以二叉树
为例介绍树的操作
- 对比之前的数据结构,发现有些都是"
一对一
"的关系,即前面的数据只跟下面的一个数据产生连接关系
。如链表、栈、队列等 - 树结构则是"
一对多
"的关系,即前面的父节点跟若干个子节点产生了连接关系
与之前的数据结构相比,遍历一个树
。
有非常经典的三种方法,分别是前序遍历、中序遍历、后序遍历
- 前序是先输出父节点,再遍历左子树和右子树
- 中序是先遍历左子树,再输出父节点,再遍历右子树
- 后序是先遍历左子树,再遍历右子树,最后输出父节点
三、认识二叉树不同遍历方式
示例一:使用前序、中序、后序对下面二叉树进行遍历
我们根据图片定义英雄节点HeroNode 信息
//创建英雄节点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);
//递归向左节点进行前序遍历
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);
}
}
我们创建一颗二叉树 BinaryTree 信息
//定义二叉树
class BinaryTree{
private HeroNode root; //根节点
public void setRoot(HeroNode root) {
this.root = root;
}
//root根节点前序遍历的方法
public void preOrder(){
if(this.root!=null){
this.root.preOrder();
}else{
System.out.println("二叉树为空,无法遍历");
}
}
//root根节点中序遍历的方法
public void infixOrder(){
if(this.root!=null){
this.root.infixOrder();
}else{
System.out.println("二叉树为空,无法遍历");
}
}
//root根节点后序遍历的方法
public void postOrder(){
if(this.root!=null){
this.root.postOrder();
}else{
System.out.println("二叉树为空,无法遍历");
}
}
}
现在让我们如图所示创建二叉树与英雄节点来测试遍历看看
public class BinaryTreeDemo {
public static void main(String [] agrs){
//创建二叉树
BinaryTree tree =new BinaryTree();
//创建英雄节点
HeroNode root = new HeroNode(1,"松江");
HeroNode node2 =new HeroNode(2,"吴用");
HeroNode node3 =new HeroNode(3,"卢俊");
HeroNode node4 =new HeroNode(4,"林冲");
//手动创建二叉树依赖
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
//将root 节点给到二叉树
tree.setRoot(root);
}
}
我们测试前序遍历
,看看是否结果是[宋江-->吴用-->卢俊-->林冲]
//前序遍历
tree.preOrder();
HeroNode [no =1, name =松江]
HeroNode [no =2, name =吴用]
HeroNode [no =3, name =卢俊]
HeroNode [no =4, name =林冲]
我们测试中序遍历
,看看是否结果是[吴用-->松江-->卢俊-->林冲]
//中序遍历
tree.infixOrder();
HeroNode [no =2, name =吴用]
HeroNode [no =1, name =松江]
HeroNode [no =3, name =卢俊]
HeroNode [no =4, name =林冲]
我们测试后序遍历
,看看是否结果是[吴用-->林冲-->卢俊-->松江]
//后序遍历
tree.postOrder();
HeroNode [no =2, name =吴用]
HeroNode [no =4, name =林冲]
HeroNode [no =3, name =卢俊]
HeroNode [no =1, name =松江]
加强小练习
1.上图的3号节点"卢俊",添加左节点[5,关胜]
2.使用前序、中序、后序 写出各自输出顺序是啥?
四、认识二叉树不同遍历的查找方式
示例二:使用前序、中序、后序不同遍历对下面二叉树进行查找
分析前序遍历查找思路
- 判断当前节点是否符合要求等于要查找的,相等则返回当前节点
- 不相等则,判断当前节点的左节点是否为空,不为空递归前序查找
- 找到符合要求的节点则返回,否则继续递归查找
- 不相等则,判断当前节点的右节点是否为空,不为空递归前序查找
分析中序遍历查找思路
- 判断当前节点的左节点是否为空,不为空递归中序查找
- 符合要求的节点则返回,没有则和当前节点进行比较,满足则返回
- 不相等则进行右递归中序查找
分析后序遍历查找思路
- 判断当前节点的左节点是否为空,不为空递归后序查找
- 符合要求的节点则返回,没有则进行右递归后序查找
- 符合要求的节点则返回,没有则和当前节点进行比较,满足则返回
接下来我们在分别在HeroNode、BinaryTree添加代码
//添加前序、中序、后序查找代码
class HeroNode {
//省略之前序、中序、后序遍历代码
/**
* @param no 查找no
* @return 如果找到返回node,没有返回null
*/
public HeroNode preOrderSearch(int no){
System.out.println("进入前序遍历查找~~~~");
//比较当前节点看看是不是
if(this.no == no){
return this;
}
//1.判断当前节点的左节点是否为空,不为空递归前序查找,找到符合要求的节点则返回
HeroNode resnode = null;
if(this.left!=null){
resnode = this.left.preOrderSearch(no);
}
//说明左节点找到了,相等
if(resnode != null){
return resnode;
}
//不相等则,判断当前节点的右节点是否为空,不为空递归前序查找
if(this.right != null){
resnode = this.right.preOrderSearch(no);
}
return resnode;
}
/**
* @param no 查找no
* @return 如果找到返回node,没有返回null
*/
public HeroNode infixOrderSearch(int no){
//1.判断当前节点的左节点是否为空,不为空递归中序查找,找到符合要求的节点则返回
HeroNode resnode = null;
if(this.left!=null){
resnode = this.left.infixOrderSearch(no);
}
//说明符合要求的节点找到了,相等
if(resnode != null){
return resnode;
}
System.out.println("进入中序遍历查找~~~~");
//没有则和当前节点进行比较,满足则返回
if(this.no == no){
return this;
}
//不相等则,判断当前节点的右节点是否为空,不为空递归前序查找
if(this.right != null){
resnode = this.right.infixOrderSearch(no);
}
return resnode;
}
/**
* @param no 查找no
* @return 如果找到返回node,没有返回null
*/
public HeroNode postOrderSearch(int no){
//1.判断当前节点的左节点是否为空,不为空递归后序查找,找到符合要求的节点则返回
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;
}
System.out.println("进入后序遍历查找~~~~");
//没有则和当前节点进行比较,满足则返回
if(this.no == no){
return this;
}
return resnode;
}
}
//添加前序、中序、后序查找代码
class BinaryTree{
//省略之前序、中序、后序遍历代码
//root节点前序查找方法
public HeroNode preOrderSearch(int no){
if(this.root != null){
return this.root.preOrderSearch(no);
}else{
return null;
}
}
//root节点中序查找方法
public HeroNode infixOrderSearch(int no){
if(this.root != null){
return this.root.infixOrderSearch(no);
}else{
return null;
}
}
//root节点后序查找方法
public HeroNode postOrderSearch(int no){
if(this.root != null){
return this.root.postOrderSearch(no);
}else{
return null;
}
}
}
[温馨提示]:小伙伴一定要添加好关胜英雄数据
并关联起树的关系
哦
//创建英雄节点
HeroNode node5 =new HeroNode(5,"关胜");
//手动关联二叉树依赖关系
node3.setLeft(node5);
使用前序遍历查找
测试[编号:5 关胜]
看看,看看是否找到
并是否四次找到
System.out.println("==========================使用前序遍历查找方式");
HeroNode resNode = tree.preOrderSearch(5);
if (resNode != null) {
System. out.printf("找到了,信息为no=%d name=%s", resNode.getNo(), resNode. getName());
} else {
System.out. printf("没有找到no = %d的英雄",5);
}
运行输出结果如下:
==========================使用前序遍历查找方式
进入前序遍历查找~~~~
进入前序遍历查找~~~~
进入前序遍历查找~~~~
进入前序遍历查找~~~~
找到了,信息为no=5 name=关胜
使用中序遍历查找
测试[编号:5 关胜]
看看,看看是否找到
并是否三次找到
System.out.println("==========================使用中序遍历查找方式~~~");
HeroNode resNode = tree.infixOrderSearch(5);
if (resNode != null) {
System. out.printf("找到了,信息为no=%d name=%s", resNode.getNo(), resNode. getName());
} else {
System.out. printf("没有找到no = %d的英雄",5);
}
运行输出结果如下:
==========================使用中序遍历查找方式~~~
进入中序遍历查找~~~~
进入中序遍历查找~~~~
进入中序遍历查找~~~~
找到了,信息为no=5 name=关胜
使用后序遍历查找
测试[编号:5 关胜]
看看,看看是否找到
并是否二次找到
System.out.println("==========================使用后序遍历查找方式~~~");
HeroNode resNode = tree.postOrderSearch(5);
if (resNode != null) {
System. out.printf("找到了,信息为no=%d name=%s", resNode.getNo(), resNode. getName());
} else {
System.out. printf("没有找到no = %d的英雄",5);
}
运行输出结果如下:
==========================使用后序遍历查找方式~~~
进入后序遍历查找~~~~
进入后序遍历查找~~~~
找到了,信息为no=5 name=关胜
五、认识二叉树的删除节点
因为目前的二叉树:暂时是没有规则的
,后边深入时再解决怎么把左节点或者右节点提升上去的问题。
示例三:
- 规则一:如果
删除
的节点是叶子节
点,则删除该节点
- 规则二:如果
删除
的节点是非叶子节
点,则删除该子树
. - 目标:
删除
叶子节点五号
和子树三号
.
思路分析
- 如果树本身为空,只有一个root节点则等价于二叉树置空
- 因为我们的二叉树是链表单向的,所以删除目标节点时不能直接判断是否删除该节点。
- 如果当前节点左子节点不为空,并且左子节点是需要删除的节点就将this.left = null,并结束返回
- 如果当前节点右子节点不为空,并且右子节点是需要删除的节点就将this.right = null,并结束返回
- 如果当前节点没有删除的节点,则判断左节点是否为空,进行左递归继续删除
- 如果左节点左递归没有删除的节点,则判断右节点是否为空,进行右递归继续删除
接下来我们在分别在HeroNode、BinaryTree添加代码
//添加删除节点代码
class HeroNode {
//省略之前序、中序、后序遍历代码
//省略之前序、中序、后序查找代码代码
//递归删除结点
//1.如果删除的节点是叶子节点,则删除该节点
//2.如果删除的节点是非叶子节点,则删除该子树
public void delHerNode(int no){
//思路
// 因为我们的二叉树是链表单向的,所以删除目标节点时不能直接判断是否删除该节点。
// 1.如果当前节点左子节点不为空,并且左子节点是需要删除的节点就将this.left = null,并结束返回
if(this.left != null && this.left.no == no){
this.left = null;
return;
}
// 2.如果当前节点右子节点不为空,并且右子节点是需要删除的节点就将this.right = null,并结束返回
if(this.right != null && this.right.no == no){
this.right = null;
return;
}
// 3.如果当前节点没有删除的节点,则判断左节点是否为空,进行左递归继续删除
if(this.left != null){
this.left.delHerNode(no);
}
// 4.如果左节点左递归没有删除的节点,则判断右节点是否为空,进行右递归继续删除
if(this.right != null){
this.right.delHerNode(no);
}
}
}
//添加删除节点代码
class BinaryTree{
//省略之前序、中序、后序遍历代码
//省略之前序、中序、后序查找代码代码
public void delHerNode(int no){
//如果树本身为空,只有一个root节点则等价于二叉树置空
if(root !=null){
//如果只有一个root结点,这里立即判断root是不是就是要删除结点
if(root.getNo() == no){
root = null;
}else{
root.delHerNode(no);
}
}else{
System.out.println("空树!不能删除");
}
}
}
使用删除节点
测试[编号:5 关胜]
看看,看看是否成功
System.out.println("==============前序遍历显示删除前数据");
tree.preOrder();
System.out.println("==========================================删除叶子节点五号:关胜");
tree.delHerNode(5);
System.out.println("==============前序遍历显示删除后数据");
tree.preOrder();
运行结果如下:
==============前序遍历显示删除前数据
HeroNode [no =1, name =松江]
HeroNode [no =2, name =吴用]
HeroNode [no =3, name =卢俊义]
HeroNode [no =5, name =关胜]
HeroNode [no =4, name =林冲]
==========================================删除叶子节点五号:关胜
==============前序遍历显示删除后数据
HeroNode [no =1, name =松江]
HeroNode [no =2, name =吴用]
HeroNode [no =3, name =卢俊义]
HeroNode [no =4, name =林冲]
图解分析删除节点
执行删除[叶子节点五号:关胜]
,看看是如何进行的吧!
当执行方法时,先判断当前root 是否为空
,紧接着判断当前root 节点no 是否等于需要删除的节点no
,不满足条件进入delNode方法
delNode方法里,判断当前节点宋江左节点
是否是[五号:关胜]
,但左节点当前为[二号:吴用]
,不满足于是接着判断右节点
但右节点当前为[三号:卢俊义]
,不满足条件判断
于是判断当前左节点是否为空
,不为空则进行左递归查询再次进入delNode方法,那么当前节点为[二号:吴用]
delNode方法里,判断当前节点吴用左节点
是否是[五号:关胜]
,但当前吴用节点左节点为空于是接着判断右节点
但右节点当前为空,不满足条件判断
于是判断当前左节点是否为空,不为空则进行左递归查询再次进入,但是很遗憾,吴用的左边是null,则进行判断右节点
但是吴用右边也是null,则往回溯回到宋江的左递归查询
接着判断当前宋江右节点
是否为空,不为空则进行右递归查询再次进入delNode方法,那么当前节点为[三号:卢俊义]
delNode方法里,判断当前节点吴用左节点是否是[五号:关胜]
,满足条件则卢俊义左节点更改为:null
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。