目录
一、树
和线性表一样,树也可以用于存放数据元素,不过不同的是,树是一种非线性的数据结构,是由n(n ≥ 0)个有限结点组成的一个具有层次关系的集合,把其称为树是因为它看起来就像一棵倒挂着的树,也因此它是根朝上,而叶朝下的。下面是树的一些相关概念及图解(图源网络):
- 根结点:没有父结点的结点称为根结点。如下图的A即为根结点。
- 叶结点:没有子结点的结点称为叶结点。如下图的G、H、I均为叶结点。
- 空树:n=0时即为空树。
- 子树:一个结点及该结点以下的所有子结点所组成的树。
- 结点的度:该结点直接相连的子结点的个数。如下图中的A结点,其度为2。
- 树的度:一棵树中,最大的结点的度就称为树的度。如下图中的树,其度为2。
- 结点的层次:一棵树中,根结点为第1层,根结点的子结点为第2层,根结点的孙子结点为第3层,以此类推。
- 树的深度或高度:树中结点的最大层次。如下图中的树,其高度为4。
可以发现,在一棵树中,除了根结点外,剩余结点都被分成了互不相交的集合,而每个集合又都是类似树的结构(其实就是子树),所以树其实是由递归定义的。需要注意,在树形结构中,子树一定是互不相交的,否则就不是树。
二、二叉树
1.二叉树的基本概念
二叉树(Binary Tree)同样是由n(n ≥ 0)个结点构成的有限集,n=0时为空树,n>0时为非空树。对于非空树,二叉树要求每个结点最多有两棵子树(即二叉树不存在度大于2的结点),且分为左子树和右子树(左右次序不能颠倒),而左子树和右子树其本身又都是二叉树。
很明显,以上定义属于递归定义,所以二叉树的相关操作往往都是使用递归进行实现。
2.二叉树的形态
2.1五种基本形态
2.2特殊形态
- 满二叉树:一个二叉树,如果每一层的结点数都达到最大值,那么这个二叉树就叫做满二叉树。
- 完全二叉树:对于深度为k,有n个结点的二叉树,当且仅当每个结点都与深度为k的满二叉树中编号从1~n的结点一一对应时,这样的二叉树就是完全二叉树。前(k-1)层都是满的,最后第k层满或不满均可(不过要求所有结点集中在最左边)。
3.二叉树的存储
二叉树的存储一般可以使用两种存储结构,一种是顺序存储,另一种是链式存储。
3.1顺序存储结构
顺序存储结构就是使用数组来进行存储。显然,对于一棵树,我们不仅要存储其中的数据元素,同时也要存储各个结点之间的关系,而数组又是一组地址连续的存储单元,所以为了在顺序存储结构中准确得到各结点之间的映射关系,二叉树中的所有结点都必须按照规定的顺序存放,这里的顺序就是指从上到下一层一层存放,每层按从左到右的顺序,具体来说如下:
- 完全二叉树:自根结点开始,从上往下、从左往右依次进行存储
- 非完全二叉树:先将其变换为对应的完全二叉树,空缺位置用特殊符号代替(比如#、null等),然后再按完全二叉树的存储方式进行存储
显然,对于左图这种完全二叉树,顺序存储结构还是非常合适的。但是,对于右图的非完全二叉树,我们就需要用特殊字符去填充空位,因此会造成较大的空间浪费,并且随着树越来越深、越来越复杂,这种浪费必然会逐渐加大。所以,对于一般的二叉树,链式存储无疑是更好的选择。
3.2链式存储结构
链式存储结构就是用链表来表示一棵二叉树,即用“链”来指示各结点之间的逻辑关系。用链表表示二叉树时,每个结点由三个域组成,即数据域(data)、左指针域(leftChild)、右指针域(rightChild),数据域用于存放当前结点的数据元素,左指针域存放当前结点的左孩子所在结点的地址,右指针域存放当前结点的右孩子所在结点的地址。注意,另外还需要一个头指针指向根结点。
图示如下:
4.二叉树的遍历
对于之前的那些线性数据结构,我们很容易想到“从前往后”或者“从后往前”进行遍历,这与我们一贯的思维契合,但是对于树这种一对多的结构,从前往后或者从后往前的遍历方式就不太适用了,所以我们需要规定好遍历顺序,避免遍历时发生混乱,造成某些结点被重复访问。
由于二叉树的结构特性,其遍历顺序就可以看作是根结点、左子树、右子树这三者的一个先后访问顺序,因此定义了以下四种二叉树遍历的方式,今天我们主要用到的是前三种。
- 前序遍历(Preorder Traversal):即访问根结点在访问左右子树之前,具体来说就是先访问当前根结点,然后访问左子树,最后再访问右子树,记为“根 左 右”。
- 中序遍历(Inorder Traversal):即访问根结点在访问左右子树之中(间),具体来说就是先访问左子树,然后访问根结点,最后再访问右子树,记为“左 根 右”。
- 后序遍历(Postorder Traversal):即访问根结点在访问左右子树之后,具体来说就是先访问左子树,然后访问右子树,最后再访问根结点,记为“左 右 根”。
- 层次遍历:自上而下、自左而右的逐层进行遍历,这一层遍历完之后再遍历下一层。
三、代码实现
1.二叉树的创建及初始化
由于每棵二叉树都可以从根结点出发,找到其余所有的结点,所以这里不需要在BinaryCharTree类中再创建一个结点类,直接定义成员变量即可。其中,value是二叉树中存储的数据元素,所以用char修饰,而左子树leftChild和右子树rightChild是树形结构,始终属于BinaryCharTree类,所以用BinaryCharTree定义。需要注意,初始化时,结点的左右指针域都必须为空。
public class BinaryCharTree {
/**
* The value
*/
char value;
/**
* The left child
*/
BinaryCharTree leftChild;
/**
* The right child
*/
BinaryCharTree rightChild;
/**
*********************
* The first constructor.
*
* @param paraName The value.
*********************
*/
public BinaryCharTree(char paraName) {
value = paraName;
leftChild = null;
rightChild = null;
} // of constructor
接着,我们自己手动创建一个二叉树。由于最后返回的肯定是一个二叉树,属于BinaryCharTree类型, 所以我们创建方法时要用BinaryCharTree进行修饰,代码如下:
/**
*********************
* Manually construct a tree. Only for testing.
*********************
*/
public static BinaryCharTree manualConstructTree() {
// Step 1. Construct a tree with only one node.
BinaryCharTree resultTree = new BinaryCharTree('a');
// Step 2. Construct all Nodes. The first node is the root.
// BinaryCharTree tempTreeA = resultTree.root;
BinaryCharTree tempTreeB = new BinaryCharTree('b');
BinaryCharTree tempTreeC = new BinaryCharTree('c');
BinaryCharTree tempTreeD = new BinaryCharTree('d');
BinaryCharTree tempTreeE = new BinaryCharTree('e');
BinaryCharTree tempTreeF = new BinaryCharTree('f');
BinaryCharTree tempTreeG = new BinaryCharTree('g');
// Step 3. Link all Nodes.
resultTree.leftChild = tempTreeB;
resultTree.rightChild = tempTreeC;
tempTreeB.rightChild = tempTreeD;
tempTreeC.leftChild = tempTreeE;
tempTreeD.leftChild = tempTreeF;
tempTreeD.rightChild = tempTreeG;
return resultTree;
} // of manualConstructTree
第一步,创建根结点;第二步,创建其余所有的结点;第三步,链接所有结点;最后,一定要记得写return语句。
该二叉树用图表示为:
2.二叉树的三种遍历
在上文我们提到过,由于二叉树是递归定义,所以它的相关操作基本上都是用的递归算法来完成,二叉树的遍历就是如此。
首先,是前序遍历,即“根 左 右”的顺序。先访问当前根结点,然后再访问当前根结点的左子树,如果该左子树不为空,那么就将该左子树作为新的根结点,开始新一轮的前序遍历,直到到达叶结点,访问右子树时同理。代码如下:
/**
*********************
* Pre-order visit.
*********************
*/
public void preOrderVisit() {
System.out.print("" + value + " ");
if(leftChild != null) {
leftChild.preOrderVisit();
} // of if
if(rightChild != null) {
rightChild.preOrderVisit();
} // of if
} // of preOrderVisit
类比前序遍历,我们可以很容易地得到中序遍历和后序遍历,只需要更改一下输出语句的位置即可,代码如下:
/**
*********************
* In-order visit.
*********************
*/
public void inOrderVisit() {
if(leftChild != null) {
leftChild.inOrderVisit();
} // of if
System.out.print("" + value + " ");
if(rightChild != null) {
rightChild.inOrderVisit();
} // of if
} // of inOrderVisit
/**
*********************
* Post-order visit.
*********************
*/
public void postOrderVisit() {
if(leftChild != null) {
leftChild.postOrderVisit();
} // of if
if(rightChild != null) {
rightChild.postOrderVisit();
} // of if
System.out.print("" + value + " ");
} // of postOrderVisit
3.计算二叉树的深度
二叉树的深度计算,同样可以使用递归进行实现。如果当前根结点的左右子树均为空,那么就直接返回1;如果当前根结点的左(右)子树不为空,则把该左(右)子树作为新的根结点,重新调用getDepth()方法,直到当前根结点的左右子树均为空(即当前结点为叶结点)。
显然,最先得出的是底层结点的深度,然后将该层结点的深度作为tempLeftDepth或者tempRightDepth返回给它的根结点,再从下往上以此类推,最终得到树的深度。而在计算当前根结点的深度时,肯定是取左子树和右子树的较大者,再加上结点本身这一层(也就是在tempLeftDepth和tempRightDepth较大者的基础上再+1)。
由以上分析,我们可以得到简化的递推公式如下:
这里的递推公式是简化后的递推公式,它忽略了某些结点并不是左右子树均存在;此外,公式第二行中的tempLeftDepth(i+1)、tempRightDepth(i+1)与第三行中的tempLeftDepth(i+1)、tempRightDepth(i+1)并不指代同一个值,而是指当前结点的左右子树的深度。
根据以上递推公式,用代码模拟如下:
/**
*********************
* Get the depth of the binary char tree.
*
* @return The depth.
*********************
*/
public int getDepth() {
if((leftChild == null) && (rightChild == null)) {
return 1;
} // of if
// The depth of the left child.
int tempLeftDepth = 0;
if(leftChild != null) {
tempLeftDepth = leftChild.getDepth();
} // of if
// The depth of the right child.
int tempRightDepth = 0;
if(rightChild != null) {
tempRightDepth = rightChild.getDepth();
} // of if
if(tempLeftDepth >= tempRightDepth) {
return tempLeftDepth + 1;
} else {
return tempRightDepth + 1;
} // of if
} // of getDepth
4.计算二叉树的结点个数
有了二叉树深度计算的基础,我们再计算二叉树的结点个数就更容易了。同样利用递归算法,如果当前根结点的左右子树均为空,则返回1;如果当前根结点的左(右)子树不为空,则把该左(右)子树作为新的根结点,再次调用函数getNumNodes(),直到当前根结点的左右子树均为空(即到达叶结点)。
同样的,最先得出的也是底层结点的个数,然后将其作为tempLeftNodes或tempRightNodes返回给它的根结点,它的根结点再把返回得到的tempLeftNodes和tempRightNodes相加,并加上根结点本身(即+1),然后接着向上返回,最终得到总共的结点个数。
所以,递推公式简化总结如下(简化的内容与上面二叉树深度计算一样):
根据以上递推公式,用代码进行模拟,如下:
/**
*********************
* Get the number of nodes of the binary char tree.
*
* @return The number of nodes.
*********************
*/
public int getNumNodes() {
if((leftChild == null) && (rightChild == null)) {
return 1;
} // of if
// The number of nodes of the left child.
int tempLeftNodes = 0;
if(leftChild != null) {
tempLeftNodes = leftChild.getNumNodes();
} // of if
// The number of nodes of the right child.
int tempRightNodes = 0;
if(rightChild != null) {
tempRightNodes = rightChild.getNumNodes();
} // of if
// The total number of nodes.
return tempLeftNodes + tempRightNodes + 1;
} // of getNumNodes
5.完整的程序代码
package datastructure.tree;
/**
* Binary tree with char type elements.
*
*@auther Xin Lin 3101540094@qq.com.
*/
public class BinaryCharTree {
/**
* The value
*/
char value;
/**
* The left child
*/
BinaryCharTree leftChild;
/**
* The right child
*/
BinaryCharTree rightChild;
/**
*********************
* The first constructor.
*
* @param paraName The value.
*********************
*/
public BinaryCharTree(char paraName) {
value = paraName;
leftChild = null;
rightChild = null;
} // of constructor
/**
*********************
* Manually construct a tree. Only for testing.
*********************
*/
public static BinaryCharTree manualConstructTree() {
// Step 1. Construct a tree with only one node.
BinaryCharTree resultTree = new BinaryCharTree('a');
// Step 2. Construct all Nodes. The first node is the root.
// BinaryCharTree tempTreeA = resultTree.root;
BinaryCharTree tempTreeB = new BinaryCharTree('b');
BinaryCharTree tempTreeC = new BinaryCharTree('c');
BinaryCharTree tempTreeD = new BinaryCharTree('d');
BinaryCharTree tempTreeE = new BinaryCharTree('e');
BinaryCharTree tempTreeF = new BinaryCharTree('f');
BinaryCharTree tempTreeG = new BinaryCharTree('g');
// Step 3. Link all Nodes.
resultTree.leftChild = tempTreeB;
resultTree.rightChild = tempTreeC;
tempTreeB.rightChild = tempTreeD;
tempTreeC.leftChild = tempTreeE;
tempTreeD.leftChild = tempTreeF;
tempTreeD.rightChild = tempTreeG;
return resultTree;
} // of manualConstructTree
/**
*********************
* Pre-order visit.
*********************
*/
public void preOrderVisit() {
System.out.print("" + value + " ");
if(leftChild != null) {
leftChild.preOrderVisit();
} // of if
if(rightChild != null) {
rightChild.preOrderVisit();
} // of if
} // of preOrderVisit
/**
*********************
* In-order visit.
*********************
*/
public void inOrderVisit() {
if(leftChild != null) {
leftChild.inOrderVisit();
} // of if
System.out.print("" + value + " ");
if(rightChild != null) {
rightChild.inOrderVisit();
} // of if
} // of inOrderVisit
/**
*********************
* Post-order visit.
*********************
*/
public void postOrderVisit() {
if(leftChild != null) {
leftChild.postOrderVisit();
} // of if
if(rightChild != null) {
rightChild.postOrderVisit();
} // of if
System.out.print("" + value + " ");
} // of postOrderVisit
/**
*********************
* Get the depth of the binary char tree.
*
* @return The depth.
*********************
*/
public int getDepth() {
if((leftChild == null) && (rightChild == null)) {
return 1;
} // of if
// The depth of the left child.
int tempLeftDepth = 0;
if(leftChild != null) {
tempLeftDepth = leftChild.getDepth();
} // of if
// The depth of the right child.
int tempRightDepth = 0;
if(rightChild != null) {
tempRightDepth = rightChild.getDepth();
} // of if
if(tempLeftDepth >= tempRightDepth) {
return tempLeftDepth + 1;
} else {
return tempRightDepth + 1;
} // of if
} // of getDepth
/**
*********************
* Get the number of nodes of the binary char tree.
*
* @return The number of nodes.
*********************
*/
public int getNumNodes() {
if((leftChild == null) && (rightChild == null)) {
return 1;
} // of if
// The number of nodes of the left child.
int tempLeftNodes = 0;
if(leftChild != null) {
tempLeftNodes = leftChild.getNumNodes();
} // of if
// The number of nodes of the right child.
int tempRightNodes = 0;
if(rightChild != null) {
tempRightNodes = rightChild.getNumNodes();
} // of if
// The total number of nodes.
return tempLeftNodes + tempRightNodes + 1;
} // of getNumNodes
/**
*********************
* The entrance of the program.
*
* @param args Not used now.
*********************
*/
public static void main(String[] args) {
BinaryCharTree tempTree = manualConstructTree();
System.out.println("Pre-order visit: ");
tempTree.preOrderVisit();
System.out.println("\r\nIn-order visit: ");
tempTree.inOrderVisit();
System.out.println("\r\nPost-order visit: ");
tempTree.postOrderVisit();
System.out.println("\r\n\nThe depth is: " + tempTree.getDepth());
System.out.println("The number of the nodes is: " + tempTree.getNumNodes());
} // of main
} // of class BinaryCharTree
运行结果
总结
今天,我们主要学习了树及二叉树的基础知识,同时也利用递归实现了二叉树的前序遍历、中序遍历、后序遍历、深度计算、节点个数计算。二叉树的这三种遍历各有不同的适用场景,前序遍历适用于二叉树的镜像变换、表达式求值等,中序遍历适用于二叉搜索树的排序,而后序遍历可以用来计算深度、结点数目等。
总的来说,树是一种非常重要且广泛使用的数据结构,其非常适合于表示具有层级或嵌套关系的数据,在这种表示中,每个结点就代表一个实体,而结点之间的连接就可以很好的表示这些实体之间的层级或包含关系;同时,树也是一个非常重要的工具,不仅可以用于编程,在数据库管理系统、操作系统、计算机网络等方面也有应用,还有机器学习中的决策树、图像处理中的区域分割树、表达式求值中的表达式树等。
在今天的代码模拟中,我们重点使用了递归思想,可见递归与树形结构有着相当紧密的联系,递归的分而治之思想与树形结构的分层次思想不谋而合,所以用好递归其实也是我们学好树形结构的基础。在day16介绍递归算法时,我们说递归的核心就是找出递推公式,而今天我们再总结一条,递归的另一重要思想就是暂且搁置当前任务,等待最内层(最底层)递归完成后,再由内而外的逐步进行。