树的结构:
概念:
从结构上来看,树是一种非线性的数据结构,是一个由n(n≥0)个结点组成的集合
结构上的特点:
①有一个特殊的结点,叫做根结点,根节点没有前驱结点
②除了根结点以外的结点被分为互不相交的集合(子树,由子树再去细化为具体的叶子结点)
③树是递归定义的
这种就不是树,因为同一层的结点之间不能相交
关于树的一些概念(重要):
结点的度:一个结点含有子树的个数称为该结点的度(如结点10的度是3)
树的度:一棵树中,最大的结点的度称为树的度 (如图树的度为7)
树的高度:根结点到最底层叶子结点的层数就是树的高度(如图树的高度是4)
叶子结点:没有子树的结点,即度为 0 的结点 (如 2 3 5 7 8 9 11 12 13 14都是叶子结点)
双亲结点:有子树的结点,即度不为 0 的结点
树的层次:从根结点开始为第1层,依次往下类推
深度:具体到某个叶子结点所在的层数即是深度 (如图结点12的深度是4,结点10的深度是3)
树的结构定义:
cpp
class Tree{
static class TreeNode{
public TreeNode child; //子树
public TreeNode nextbro; //同一根结点的兄弟结点
public char val; //树的值
public TreeNode(char val)
{
this.val = val;
}
}
public TreeNode root;
}
二叉树
1.概念
度最大为2的树就是二叉树
2.结构特点
二叉树要么为空,要么由就是一个根结点和左右两棵子树组成。
满二叉树和完全二叉树
满二叉树 :一颗二叉树如果每一层的结点数都达到最大数目,则称之为满二叉树(第k层的最大节点数为2的k次方个)
完全二叉树 :完全二叉树是一种特殊的二叉树,对于一个有n个结点的二叉树,其结点从0~n依次对应则称这种数为完全二叉树
二叉树的性质:
- 若规定 根结点的层数为 1 ,则一棵 非空二叉树的第 i 层上最多有 2^(i-1)
(i>0) 个结点 - 若规定只有 根结点的二叉树的深度为 1 ,则 深度为 K 的二叉树的最大结点数是2^k-1个
(k>=0) - 对任何一棵二叉树 , 如果其 叶结点个数为 n0, 度为 2 的非叶结点个数为 n2, 则有 n0 = n2 + 1
- 具有 n 个结点的完全二叉树的深度 k 为 log 2 (n+1) 上取整
看到这可能对于第三个性质会有疑问,为什么?
二叉树的创建
用穷举法创建一棵树:
java
public class Tree{
static class TreeNode{
private TreeNode left;
private TreeNode right;
public char val;
public TreeNode(char val)
{
this.val = val;
}
}
public TreeNode root;
public static void main(String[] args){
Tree tree = new Tree(A);
TreeNode A = new TreeNode('a');
TreeNode B = new TreeNode('b');
TreeNode C = new TreeNode('c');
TreeNode D = new TreeNode('d');
TreeNode E = new TreeNode('e');
A.left = B;
B.left = C;
B.right = D;
A.right = E;
}
}
现在我们穷举法创建的树长这样
是否真的长这样,我们接下来用遍历验证一下
二叉树的遍历:
**前序遍历:**前序遍历是从树的根开始遍历,然后是遍历树的左子树,最后遍历右子树(根左右)
**中序遍历:**中序遍历是从树的左子树开始遍历,然后遍历树的根结点,最后遍历右子树(左根右)
**后序遍历:**后序遍历是从树的左子树开始遍历,然后遍历树的右子树,最后遍历根结点(左右根)
**层序遍历:**层序遍历就是一棵有N个结点的树,从上到下,从左往右依次遍历对应结点(了解即可)
用递归实现前三种遍历:
前序遍历:
java
//前序遍历
public void preOrder(TreeNode root)
{
if(root == null)
{
return;
}
System.out.print((root.val)+" ");
preOrder(root.left);
preOrder(root.right);
}
我们再来看上面举的那个例子,然后理解一下递归的过程
直到root结点走到最底层的左子树为空,开始返回结点,然后再进行右子树递归
以此类推,每次递归都会先进行左子树递归,然后遇到左子树为空就返回到上一层递归,再进行右子树遍历,再遍历右子树的左子树,为空就再返回,再遍历右子树....
举一反三,中序遍历就是前序遍历的区别就是:前序遍历递归是先打印根结点------左子树------右子树,而中序遍历递归先打印左子树------根结点------右子树。同理,后序遍历就是先打印左子树------右子树------根结点。
java
//前序遍历
public void preOrder(TreeNode root)
{
if(root == null)
{
return;
}
System.out.print((root.val)+" ");
preOrder(root.left);
preOrder(root.right);
}
//中序遍历
public void inOrder(TreeNode root)
{
if(root == null)
{
return;
}
inOrder(root.left);
System.out.print((root.val)+" ");
inOrder(root.right);
}
//后序遍历
public void postOrder(TreeNode root)
{
if(root == null)
{
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.print((root.val)+" ");
}
用非递归的方法进行三种遍历:
我们仍以上面我们穷举法创建的树为例子,这个问题我们用栈来解决。
思路:定义一个引用先进行左子树遍历,每次往左走一步把一个左子树结点压入栈,然后把左子树结点的值放到链表中,当左子树为空的时候,获取栈顶的结点(此时栈顶的元素就是当前子树的根结点) ,然后往右走一步遍历右子树,之后在右子树让引用再次往左走一步遍历左子树
java
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if(root == null)
{
return list;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null||!stack.isEmpty())
{
while(cur!= null)
{
stack.push(cur);
list.add(cur.val);
cur = cur.left;
}
TreeNode top = stack.pop();
cur = top.right;
}
return list;
}
中序遍历与前序遍历大差不差,中序遍历和前序遍历的差别就在于中序遍历不需要定义引用,并且在每次遍历到左子树为空时才将栈顶元素弹出,然后才将弹出去的结点的值放到list中去(前序遍历是每次压栈的同时把结点的值放到list)
注:每次获取栈顶元素都是当前子树的根结点,将当前的栈顶元素弹出之后,下一个栈顶元素就是上一层树的根节点。
145. 二叉树的后序遍历 - 力扣(LeetCode) (非递归实现)
后序遍历相较于前面两种遍历方式就比较复杂那么一点,后序遍历的难点就在于解决先遍历左子树然后遍历右子树。
当左子树遍历完成,如何让引用直接走到右子树?
这时候只能用到另一个引用top,这个引用直接peek()栈顶元素(相当于子树的根),然后当cur遍历完左子树之后并且右子树不为空的情况下,直接用top往右遍历然后赋值给cur,这样就能达到目的,当左右子树都遍历过之后,将栈顶元素弹出,并放到list
最大的坑就再弹出栈并且放元素的这一步,这一步很多人只考虑了当右子树为空的情况,当栈顶元素弹出之后,再次peek()的是上一层树的根结点,再遍历右子树,可是此时的右子树已经遍历过了,然后就陷入死循环...
死循环的原因:
这就得用一个引用prev来记录右子树,在左子树遍历完之后,先检验右子树是否为空之后还要再检验右子树是否已经被打印,如果打印过就直接将栈顶元素弹出去
java
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if(root == null)
{
return list;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev = null;
while(cur!= null || !stack.isEmpty())
{
while(cur!= null)
{
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if(top.right == null|| top.right== prev) //后面的表达式是确保右边的子树是被打印过的,当右子树被打印过后就直接弹出栈顶元素
{
stack.pop();
list.add(top.val);
prev = top;
}
else{
cur = top.right;
}
}
return list;
}