二叉树
树是一种非线性的数据结构,它是由n个结点组成的具有层次关系的集合,把他叫做树是因为它的根朝上,叶子朝下,看起来像一颗倒挂的树。二叉树是一种最多只有两个节点的树型结构。这篇文章会用Java代码手撕二叉树的实现,从概念到实现,到oj题训练,你不仅能学会二叉树,还能加深对它的理解和运用。
1.树形结构的概念
在树形结构中,子树之间不能有交集,否则就不是树型结构,它具有以下的特点:
- 子树是不相交的;
- 除了根节点外,每个节点有且仅有一个父节点;
- 一颗N个结点的树有N-1条边。
树中的相关概念:
结点的度:一个结点含有子树的个数称为该结点的度; 如上图:A的度为6
树的度:一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6
叶子结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等节点为叶结点
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
根结点:一棵树中,没有双亲结点的结点;如上图:A
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推
树的高度或深度:树中结点的最大层次; 如上图:树的高度为4
非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等节点为分支结点
兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点
堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点
结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙
森林:由m(m>=0)棵互不相交的树组成的集合称为森林
树的应用:
树的思想和应用在我们周围应用非常广泛也非常重要,比如树的遍历运用递归的思想,电脑中的文件管理系统(目录和文件)运用树形结构存储。
2.二叉树的性质
二叉树是一个节点的有限集合,要么为空,要么是由一个根节点加上两颗被称为左子树和右子树的二叉树组成。如下图所示:
两种特殊的二叉树:
1. 满二叉树: 一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。
2. 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
二叉树的性质:
- 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2^i-1 (i>0)个结点
- 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是2^k - 1 (k>=0)
- 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+1
- 具有n个结点的完全二叉树的深度k为 log2(n+1)上取整
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
- 若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
- 若2i+1<n,左孩子序号:2i+1,否则无左孩子
- 若2i+2<n,右孩子序号:2i+2,否则无右孩
3.如何构造一个二叉树
常见树的表示方式有很多种,如:双亲表示法,孩子表示法,孩子双亲表示法,孩子兄弟表示法等等。
二叉树用代码进行表示时有两种方法,一是通过数组形式的顺序存储,二是类似于链表的链式存储。
顺序存储:将元素存储到数组中,利用二叉树的性质5进行存储,设i为节点在数组中的下标,则:
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
完全二叉树
一般二叉树
tips: 对于非完全二叉树,则不适合使用顺序方式进行存储,,空间中必须存储空节点,导致空间利用率很低。
链式存储:二叉树的链式存储是通过一个一个的节点引用起来的,常见的表示方式有二叉和三叉表示方式,具体如下:
// 孩子表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
// 孩子双亲表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
Node parent; // 当前节点的根节点
}
4.二叉树的基本操作
二叉树遍历分为四种:
- 前序遍历——访问根结点--->根的左子树--->根的右子树。
- 中序遍历——根的左子树--->根节点--->根的右子树。
- 后序遍历——根的左子树--->根的右子树--->根节点。
- 层序遍历——第一层--->第二层--->第三层--->...最后一层
下面是用代码分别实现这四种遍历方式的过程。
package Mytree;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* Created with IntelliJ IDEA.
* Description:
* User:莎土比亚
* Date:2024-03-08
* Time:18:06
*/
public class Mytree {
class Node{
public Node(int val) {
this.val=val;
}
int val;
Node left;
Node right;
}
private int usedsize;
Node root;
public Mytree(){
root=null;
usedsize=0;
}
public void createBinaryTree(){
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
Node node6 = new Node(6);
root = node1;
node1.left = node2;
node2.left = node3;
node1.right = node4;
node4.left = node5;
node5.right = node6;
}
// 前序遍历
public void preOrder(Node root) {
if(root==null){
return;
}
System.out.print(root.val+" ");
preOrder(root.left);
preOrder(root.right);
}
// 中序遍历
public void inOrder(Node root) {
if(root==null){
return;
}
inOrder(root.left);
System.out.print(root.val+" ");
inOrder(root.right);
}
// 后序遍历
public void postOrder(Node root){
if(root==null){
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.print(root.val+" ");
}
//层序遍历
public void LeOrder(Node root){
Queue<Node> list=new LinkedList<>();
list.add(root);
while (!list.isEmpty()){
Node cur=list.poll();
if(cur!=null){
System.out.print(cur.val+" ");
list.add(cur.left);
list.add(cur.right);
}
}
}
public static void main(String[] args) {
Mytree tree=new Mytree();
tree.createBinaryTree();
System.out.println("前序遍历");
tree.preOrder(tree.root);
System.out.println("\n中序遍历");
tree.inOrder(tree.root);
System.out.println("\n后序遍历");
tree.postOrder(tree.root);
System.out.println("\n层序遍历");
tree.LeOrder(tree.root);
}
}
二叉树的常见方法:
// 获取树中节点的个数
int size(Node root);
// 获取叶子节点的个数
int getLeafNodeCount(Node root);
// 获取第K层节点的个数
int getKLevelNodeCount(Node root,int k);
// 获取二叉树的高度
int getHeight(Node root);
// 检测值为value的元素是否存在
Node find(Node root, int val);
//层序遍历
void levelOrder(Node root);
// 判断一棵树是不是完全二叉树
boolean isCompleteTree(Node root);
创建一个MyTree类实现一个自己的二叉树并包含上述方法:
// 获取树中节点的个数
int size(Node root) {
int num=0;
if(root==null){
return 0;
}
num+=size(root.left);
num+=size(root.right);
num++;
return num;
}
// 获取叶子节点的个数
int getLeafNodeCount(Node root) {
if(root==null){
return 0;
}
if(root.left==null&&root.right==null) {
return 1;
}
return getLeafNodeCount(root.left)+getLeafNodeCount(root.right);
}
// 获取第K层节点的个数
int getKLevelNodeCount(Node root,int k) {
if(k==1){
return 1;
}
return getKLevelNodeCount(root.left,k-1)+getKLevelNodeCount(root.right,k-1);
}
// 获取二叉树的高度
int getHeight(Node root) {
if(root==null){
return 0;
}
return Math.max(getHeight(root.left),getHeight(root.right))+1;
}
// 检测值为value的元素是否存在
Node find(Node root, int val){
if (root==null){
return null;
}
if(root.val==val){
return root;
}
Node left=find(root.left,val);
Node right=find(root.right,val);
if(left!=null){
return left;
}
return right;
}
// 判断一棵树是不是完全二叉树
boolean isCompleteTree(Node root) {
if(root==null){
return true;
}
if((root.left==null&&root.right!=null)||(root.left!=null&&root.right==null)){
return false;
}
return isCompleteTree(root.left)&&isCompleteTree(root.right);
}
总结:解决二叉树相关问题时要善用子问题思路,将原问题简化为从左子树和右子树中求原始结果,在套用方法递归即可得到问题答案。
5.二叉树相关oj题训练
1.翻转二叉树
2.检查两颗二叉树是否相同
3.判断一颗二叉树是否是平衡二叉树
4.根据一棵树的前序遍历和中序遍历构造二叉树
5.根据二叉树创建字符串