目录java
前面咱们讲的都是线性表结构,栈、队列等等。今天咱们讲一种非线性表结构,树。树这种数据结构比线性表的数据结构要复杂得多,内容也比较多,首先咱们先从树(Tree)开始讲起。
@面试
树型结构是一种非线性结构,它的数据元素之间呈现分支、分层的特色。算法
树(Tree)是由n(n≥0)个结点构成的有限集合T,当n=0时T称为空树;不然,在任一非空树T中:
(1)有且仅有一个特定的结点,它没有前驱结点,称其为根(Root)结点;
(2)剩下的结点可分为m(m≥0)个互不相交的子集T1,T2,…,Tm,其中每一个子集自己又是一棵树,并称其为根的子树(Subtree)。数组
注意:树的定义具备递归性,即“树中还有树”。树的递归定义揭示出了树的固有特性数据结构
什么是“树”?再好的定义,都没有图解来的直观。因此我在图中画了几棵“树”。你来看看,这些“树”都有什么特征?
你有没有发现,“树”这种数据结构真的很像咱们现实生活中的“树”函数
在有序数组中,能够快速找到特定的值,可是想在有序数组中插入一个新的数据项,就必须首先找出新数据项插入的位置,而后将比新数据项大的数据项向后移动一位,来给新的数据项腾出空间,删除同理,这样移动很费时。显而易见,若是要作不少的插入和删除操做和删除操做,就不应选用有序数组。另外一方面,链表中能够快速添加和删除某个数据项,可是在链表中查找数据项可不容易,必须从头开始访问链表的每个数据项,直到找到该数据项为止,这个过程很慢。 树这种数据结构,既能像链表那样快速的插入和删除,又能想有序数组那样快速查找学习
结点——包含一个数据元素和若干指向其子树的分支
度——结点拥有的子树个数
树的度——该树中结点的最大度数
叶子——度为零的结点
分支结点(非终端结点)——度不为零的结点
孩子和双亲——结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲
兄弟——同一个双亲的孩子
祖先和子孙——从根到该结点所经分支上的全部结点。相应地,以某一结点为根的子树中的任一结点称为该结点的子孙。
结点的层次——结点的层次从根开始定义,根结点的层次为1,其孩子结点的层次为2,……
堂兄弟——双亲在同一层的结点
树的深度——树中结点的最大层次
有序树和无序树——若是将树中每一个结点的各子树当作是从左到右有次序的(即位置不能互换),则称该树为有序树;不然称为无序树。
森林——m(m≥0)棵互不相交的树的有限集合测试
到这里,树就讲的差很少了,接下来说讲二叉树(Binary Tree)this
树结构多种多样,不过咱们最经常使用仍是二叉树,咱们平时最经常使用的树就是二叉树。二叉树的每一个节点最多有两个子节点,分别是左子节点和右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和彻底二叉树。满二叉树又是彻底二叉树的一种特殊状况。3d
二叉树的定义:
二叉树(Binary Tree)是n(n≥0)个结点的有限集合BT,它或者是空集,或者由一个根结点和两棵分别称为左子树和右子树的互不相交的二叉树组成 。
————————————
二叉树的特色:
每一个结点至多有二棵子树(即不存在度大于2的结点);二叉树的子树有左、右之分,且其次序不能任意颠倒。
一、满二叉树:
定义:深度为k且有2k-1个结点的二叉树,称为满二叉树。
特色:每一层上的结点数都是最大结点数
二、彻底二叉树:
定义:
深度为k,有n个结点的二叉树当且仅当其每个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称为彻底二叉树
特色:
特色一 : 叶子结点只可能在层次最大的两层上出现;
特色二 : 对任一结点,若其右分支下子孙的最大层次为l,则其左分支下子孙的最大层次必为l 或l+1
建议看图对应文字综合理解
代码建立二叉树:
首先,建立一个节点Node类
package demo5; /* * 节(结)点类 */ public class Node { //节点的权 int value; //左儿子(左节点) Node leftNode; //右儿子(右节点) Node rightNode; //构造函数,初始化的时候就给二叉树赋上权值 public Node(int value) { this.value=value; } //设置左儿子(左节点) public void setLeftNode(Node leftNode) { this.leftNode = leftNode; } //设置右儿子(右节点) public void setRightNode(Node rightNode) { this.rightNode = rightNode; }
接着建立一个二叉树BinaryTree 类
package demo5; /* * 二叉树Class */ public class BinaryTree { //根节点root Node root; //设置根节点 public void setRoot(Node root) { this.root = root; } //获取根节点 public Node getRoot() { return root; } }
最后建立TestBinaryTree 类(该类主要是main方法用来测试)来建立一个二叉树
package demo5; public class TestBinaryTree { public static void main(String[] args) { //建立一颗树 BinaryTree binTree = new BinaryTree(); //建立一个根节点 Node root = new Node(1); //把根节点赋给树 binTree.setRoot(root); //建立一个左节点 Node rootL = new Node(2); //把新建立的节点设置为根节点的子节点 root.setLeftNode(rootL); //建立一个右节点 Node rootR = new Node(3); //把新建立的节点设置为根节点的子节点 root.setRightNode(rootR); //为第二层的左节点建立两个子节点 rootL.setLeftNode(new Node(4)); rootL.setRightNode(new Node(5)); //为第二层的右节点建立两个子节点 rootR.setLeftNode(new Node(6)); rootR.setRightNode(new Node(7)); } }
下面将会讲的遍历、查找节点、删除节点都将围绕这三个类开展
不难看出建立好的二叉树以下(画的很差,还望各位见谅):
二叉树既能够用链式存储,也能够用数组顺序存储。数组顺序存储的方式比较适合彻底二叉树,其余类型的二叉树用数组存储会比较浪费存储空间,因此链式存储更合适。
咱们先来看比较简单、直观的链式存储法。
接着是基于数组的顺序存储法(该例子是一棵彻底二叉树)
上面例子是一棵彻底二叉树,因此仅仅“浪费”了一个下标为0的存储位置。若是是非彻底二叉树,则会浪费比较多的数组存储空间,以下。
还记得堆和堆排序吗,堆其实就是一种彻底二叉树,最经常使用的存储方式就是数组。
前面我讲了二叉树的基本定义和存储方法,如今咱们来看二叉树中很是重要的操做,二叉树的遍历。这也是很是常见的面试题。
经典遍历的方法有三种,前序遍历、中序遍历和后序遍历
前序遍历是指,对于树中的任意节点来讲,先打印这个节点,而后再打印它的左子树,最后打印它的右子树。
中序遍历是指,对于树中的任意节点来讲,先打印它的左子树,而后再打印它自己,最后打印它的右子树。
后序遍历是指,对于树中的任意节点来讲,先打印它的左子树,而后再打印它的右子树,最后打印这个节点自己。
我想,睿智的你已经想到了二叉树的前、中、后序遍历就是一个递归的过程。好比,前序遍历,其实就是先打印根节点,而后再递归地打印左子树,最后递归地打印右子树。
在以前建立好的二叉树代码之上,咱们来使用这三种方法遍历一下~
依旧是在Node节点类上添加方法:能够看出遍历方法都是用的递归思想
package demo5; /* * 节(结)点类 */ public class Node { //===================================开始 遍历======================================== //前序遍历 public void frontShow() { //先遍历当前节点的内容 System.out.println(value); //左节点 if(leftNode!=null) { leftNode.frontShow(); } //右节点 if(rightNode!=null) { rightNode.frontShow(); } } //中序遍历 public void midShow() { //左子节点 if(leftNode!=null) { leftNode.midShow(); } //当前节点 System.out.println(value); //右子节点 if(rightNode!=null) { rightNode.midShow(); } } //后序遍历 public void afterShow() { //左子节点 if(leftNode!=null) { leftNode.afterShow(); } //右子节点 if(rightNode!=null) { rightNode.afterShow(); } //当前节点 System.out.println(value); } }
而后依旧是在二叉树BinaryTree 类上添加方法,而且添加的方法调用Node类中的遍历方法
package demo5; /* * 二叉树Class */ public class BinaryTree { public void frontShow() { if(root!=null) { //调用节点类Node中的前序遍历frontShow()方法 root.frontShow(); } } public void midShow() { if(root!=null) { //调用节点类Node中的中序遍历midShow()方法 root.midShow(); } } public void afterShow() { if(root!=null) { //调用节点类Node中的后序遍历afterShow()方法 root.afterShow(); } } }
依旧是在TestBinaryTree类中测试
package demo5; public class TestBinaryTree { public static void main(String[] args) { //前序遍历树 binTree.frontShow(); System.out.println("==============="); //中序遍历 binTree.midShow(); System.out.println("==============="); //后序遍历 binTree.afterShow(); System.out.println("==============="); //前序查找 Node result = binTree.frontSearch(5); System.out.println(result); }
若是递归理解的不是很透,我能够分享一个学习的小方法:我建议各位能够这样断点调试,一步一步调,思惟跟上,仔细推敲每一步的运行相信我,你会从新认识到递归!(像下面这样贴个图再一步一步断点思惟更加清晰)
贴一下我断点对递归的分析,但愿对你有必定的帮助~
二叉树遍历的递归实现思路天然、简单,易于理解,但执行效率较低。为了提升程序的执行效率,能够显式的设置栈,写出相应的非递归遍历算法。非递归的遍历算法能够根据递归算法的执行过程写出。至于代码能够尝试去写一写,这也是一种提高!具体的非递归算法主要流程图贴在下面了:
二叉树遍历算法分析:
二叉树遍历算法中的基本操做是访问根结点,不论按哪一种次序遍历,都要访问全部的结点,对含n个结点的二叉树,其时间复杂度均为O(n)。所需辅助空间为遍历过程当中所需的栈空间,最多等于二叉树的深度k乘以每一个结点所需空间数,最坏状况下树的深度为结点的个数n,所以,其空间复杂度也为O(n)。
刚才讲到二叉树的三种金典遍历放法,那么节点的查找一样是能够效仿的,分别叫作前序查找、中序查找以及后序查找,下面代码只之前序查找为例,三者查找方法思路相似~
至于删除节点,有三种状况:
一、若是删除的是根节点,那么二叉树就彻底被删了
二、若是删除的是双亲节点,那么该双亲节点以及他下面的全部子节点所构成的子树将被删除
三、若是删除的是叶子节点,那么就直接删除该叶子节点
那么,我把完整的三个类给贴出来(包含建立、遍历、查找、删除)
依旧是Node节点类
package demo5; /* * 节(结)点类 */ public class Node { //节点的权 int value; //左儿子 Node leftNode; //右儿子 Node rightNode; //构造函数,初始化的时候就给二叉树赋上权值 public Node(int value) { this.value=value; } //设置左儿子 public void setLeftNode(Node leftNode) { this.leftNode = leftNode; } //设置右儿子 public void setRightNode(Node rightNode) { this.rightNode = rightNode; } //前序遍历 public void frontShow() { //先遍历当前节点的内容 System.out.println(value); //左节点 if(leftNode!=null) { leftNode.frontShow(); } //右节点 if(rightNode!=null) { rightNode.frontShow(); } } //中序遍历 public void midShow() { //左子节点 if(leftNode!=null) { leftNode.midShow(); } //当前节点 System.out.println(value); //右子节点 if(rightNode!=null) { rightNode.midShow(); } } //后序遍历 public void afterShow() { //左子节点 if(leftNode!=null) { leftNode.afterShow(); } //右子节点 if(rightNode!=null) { rightNode.afterShow(); } //当前节点 System.out.println(value); } //前序查找 public Node frontSearch(int i) { Node target=null; //对比当前节点的值 if(this.value==i) { return this; //当前节点的值不是要查找的节点 }else { //查找左儿子 if(leftNode!=null) { //有可能能够查到,也能够查不到,查不到的话,target仍是一个null target = leftNode.frontSearch(i); } //若是不为空,说明在左儿子中已经找到 if(target!=null) { return target; } //查找右儿子 if(rightNode!=null) { target=rightNode.frontSearch(i); } } return target; } //删除一个子树 public void delete(int i) { Node parent = this; //判断左儿子 if(parent.leftNode!=null&&parent.leftNode.value==i) { parent.leftNode=null; return; } //判断右儿子 if(parent.rightNode!=null&&parent.rightNode.value==i) { parent.rightNode=null; return; } //递归检查并删除左儿子 parent=leftNode; if(parent!=null) { parent.delete(i); } //递归检查并删除右儿子 parent=rightNode; if(parent!=null) { parent.delete(i); } } }
依旧是BinaryTree 二叉树类
package demo5; /* * 二叉树Class */ public class BinaryTree { //根节点root Node root; //设置根节点 public void setRoot(Node root) { this.root = root; } //获取根节点 public Node getRoot() { return root; } public void frontShow() { if(root!=null) { //调用节点类Node中的前序遍历frontShow()方法 root.frontShow(); } } public void midShow() { if(root!=null) { //调用节点类Node中的中序遍历midShow()方法 root.midShow(); } } public void afterShow() { if(root!=null) { //调用节点类Node中的后序遍历afterShow()方法 root.afterShow(); } } //查找节点i public Node frontSearch(int i) { return root.frontSearch(i); } //删除节点i public void delete(int i) { if(root.value==i) { root=null; }else { root.delete(i); } } }
依旧是TestBinaryTree测试类
package demo5; public class TestBinaryTree { public static void main(String[] args) { //建立一颗树 BinaryTree binTree = new BinaryTree(); //建立一个根节点 Node root = new Node(1); //把根节点赋给树 binTree.setRoot(root); //建立一个左节点 Node rootL = new Node(2); //把新建立的节点设置为根节点的子节点 root.setLeftNode(rootL); //建立一个右节点 Node rootR = new Node(3); //把新建立的节点设置为根节点的子节点 root.setRightNode(rootR); //为第二层的左节点建立两个子节点 rootL.setLeftNode(new Node(4)); rootL.setRightNode(new Node(5)); //为第二层的右节点建立两个子节点 rootR.setLeftNode(new Node(6)); rootR.setRightNode(new Node(7)); //前序遍历树 binTree.frontShow(); System.out.println("==============="); //中序遍历 binTree.midShow(); System.out.println("==============="); //后序遍历 binTree.afterShow(); System.out.println("==============="); //前序查找 Node result = binTree.frontSearch(5); System.out.println(result); System.out.println("==============="); //删除一个子树 binTree.delete(4); binTree.frontShow(); } }
到这里,总结一下,咱们学了一种非线性表数据结构,树。关于树,有几个比较经常使用的概念你须要掌握,那就是:根节点、叶子节点、父节点、子节点、兄弟节点,还有节点的高度、深度、层数,以及树的高度等。咱们平时最经常使用的树就是二叉树。二叉树的每一个节点最多有两个子节点,分别是左子节点和右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和彻底二叉树。满二叉树又是彻底二叉树的一种特殊状况。二叉树既能够用链式存储,也能够用数组顺序存储。数组顺序存储的方式比较适合彻底二叉树,其余类型的二叉树用数组存储会比较浪费存储空间。除此以外,二叉树里很是重要的操做就是前、中、后序遍历操做,遍历的时间复杂度是O(n),你须要理解并能用递归代码来实现。
若是本文章对你有帮助,哪怕是一点点,请点个赞呗,谢谢~
欢迎各位关注个人公众号,一块儿探讨技术,向往技术,追求技术...说好了来了就是盆友喔...