我所知道的数据结构之树

前面咱们学了不一样的数据结构,今天学习的是一种特别的数据结构: node

首先咱们思考一下:什么是树呢?为何咱们须要学习这种数据结构呢?segmentfault

1、为何须要树这种数据结构

咱们对比以前学习的数据结构,分析看看以前的数据结构有什么特色又有什么缺陷数组

1、数组存储方式的分析数据结构

优势:经过下标方式访问元素,速度快。对于有序数组,可使用二分查找提升检索速度。ide

缺点:若是要操做具体插入值,那么会总体移动(按必定顺序),效率较低post

假如我当前有数组arr {1,3,5,8,10},若此时插入数据:6 那么能放的进去吗?学习

image.png

其实是不能的,由于数组是事先分配空间的,指说原建立好空间长度就不能动态增加,可是数组在动态添加数据的时候,底层有一个动做:数组扩容测试

那么是如何扩容的呢?建立新的数组,并将数据拷贝,以及插入数据后移优化

那么这时会有小伙伴提出:咱们使用集合ArrayList不是能够动态增加吗?this

其实咱们观察集合ArrayList,发现也维护了数组扩容,只是策略不一样。

image.png

那么咱们一块儿看看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为了解决扩容,按照一种策略进行的,仍是会总体移动的,效率比较低,因而咱们看看链式存储方式能不能更好解决问题

2、链式存储方式的分析

优势:在必定程度上对数组存储方式有优化(好比:插入数值节点,只须要将插入节点,连接到链表中便可,删除效率也很好)。

缺点:在进行检索时,效率仍然较低(好比:检索某个值,须要从头节点开始遍历)

咱们发现数组与链式都有各自的优势与缺点,那么接下来介绍新的数据结构: 有什么不一样呢?

2、什么是树?

image.png
image.png

在树的家族中,有一种高频使用的一种树结构二叉树

二叉树中,每一个节点最多有两个分支,即每一个节点最多有两个节点,分别称为左节点与右节点

在二叉树中,还有两个特殊的类型:满二叉树与彻底二叉树

满二叉树:除了叶子节点外,全部节点都有两个节点

彻底二叉树:除了最后一层之外,其余层节点个数都达到最大,而且最后一层的叶子节点向左排列

image.png

彻底二叉树看上去并不彻底,为何这么称呼它?

这和存储二叉树的两种存储方法:链式存储法与数组顺序法。有关了

基于指针的链式存储法,也就是像链表同样,每一个节点有三个字段一个存储数据,两外两个分别存储指向左右节点的指针,以下图所示

image.png

基于数组的顺序存储法,就是按照规律把节点存放在数组里,以下图所示。为了方便按照规律计算,把起始数据放在下标为一的位置上

image.png

image.png

如果非彻底二叉树则会浪费大量的数组存储空间,以下图所示

image.png

树的基本操做

二叉树为例介绍树的操做

  • 对比以前的数据结构,发现有些都是"一对一"的关系,即前面的数据只跟下面的一个数据产生链接关系。如链表、栈、队列等
  • 树结构则是"一对多"的关系,即前面的父节点跟若干个子节点产生了链接关系

与以前的数据结构相比,遍历一个树

有很是经典的三种方法,分别是前序遍历、中序遍历、后序遍历

  • 前序是先输出父节点,再遍历左子树和右子树
  • 中序是先遍历左子树,再输出父节点,再遍历右子树
  • 后序是先遍历左子树,再遍历右子树,最后输出父节点

image.png

3、认识二叉树不一样遍历方式

示例一:使用前序、中序、后序对下面二叉树进行遍历

图片.png

咱们根据图片定义英雄节点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);
    }
}

image.png

咱们测试前序遍历,看看是否结果是[宋江-->吴用-->卢俊-->林冲]

//前序遍历
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 =松江]

增强小练习

图片.png

1.上图的3号节点"卢俊",添加左节点[5,关胜]
2.使用前序、中序、后序 写出各自输出顺序是啥?

4、认识二叉树不一样遍历的查找方式

示例二:使用前序、中序、后序不一样遍历对下面二叉树进行查找

图片.png

分析前序遍历查找思路

  • 判断当前节点是否符合要求等于要查找的,相等则返回当前节点
  • 不相等则,判断当前节点的左节点是否为空,不为空递归前序查找
  • 找到符合要求的节点则返回,不然继续递归查找
  • 不相等则,判断当前节点的右节点是否为空,不为空递归前序查找

image.png

分析中序遍历查找思路

  • 判断当前节点的左节点是否为空,不为空递归中序查找
  • 符合要求的节点则返回,没有则和当前节点进行比较,知足则返回
  • 不相等则进行右递归中序查找

image.png

分析后序遍历查找思路

  • 判断当前节点的左节点是否为空,不为空递归后序查找
  • 符合要求的节点则返回,没有则进行右递归后序查找
  • 符合要求的节点则返回,没有则和当前节点进行比较,知足则返回

image.png

接下来咱们在分别在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=关胜

5、认识二叉树的删除节点

由于目前的二叉树:暂时是没有规则的,后边深刻时再解决怎么把左节点或者右节点提高上去的问题。

示例三:

  • 规则一:若是删除的节点是叶子节点,则删除该节点
  • 规则二:若是删除的节点是非叶子节点,则删除该子树.
  • 目标:删除叶子节点五号和子树三号.

image.png

思路分析

  1. 若是树自己为空,只有一个root节点则等价于二叉树置空
  2. 由于咱们的二叉树是链表单向的,因此删除目标节点时不能直接判断是否删除该节点。
  3. 若是当前节点左子节点不为空,而且左子节点是须要删除的节点就将this.left = null,并结束返回
  4. 若是当前节点右子节点不为空,而且右子节点是须要删除的节点就将this.right = null,并结束返回
  5. 若是当前节点没有删除的节点,则判断左节点是否为空,进行左递归继续删除
  6. 若是左节点左递归没有删除的节点,则判断右节点是否为空,进行右递归继续删除

接下来咱们在分别在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方法
image.png

delNode方法里,判断当前节点宋江左节点是不是[五号:关胜],但左节点当前为[二号:吴用],不知足因而接着判断右节点
image.png

但右节点当前为[三号:卢俊义],不知足条件判断
image.png

因而判断当前左节点是否为空,不为空则进行左递归查询再次进入delNode方法,那么当前节点为[二号:吴用]
image.png

delNode方法里,判断当前节点吴用左节点是不是[五号:关胜],但当前吴用节点左节点为空因而接着判断右节点
image.png

但右节点当前为空,不知足条件判断
image.png

因而判断当前左节点是否为空,不为空则进行左递归查询再次进入,可是很遗憾,吴用的左边是null,则进行判断右节点
image.png

可是吴用右边也是null,则往回溯回到宋江的左递归查询
image.png

接着判断当前宋江右节点是否为空,不为空则进行右递归查询再次进入delNode方法,那么当前节点为[三号:卢俊义]
image.png

delNode方法里,判断当前节点吴用左节点是不是[五号:关胜],知足条件则卢俊义左节点更改成:null

image.png
image.png

相关文章
相关标签/搜索