【经典结构】二叉树

二叉树

1.基本概念

二叉树是每个节点最多有两个子树的树结构,度可能是0,1,2;

完成二叉树:从左到右依次填满;
满二叉树:除了叶子节点,所有节点都有两个孩子,并且所有叶子节点在同一层;

2.性质

1.完全二叉树除了最后一层外,下一层节点个数是上一层两倍,
如果一颗完全二叉树的节点总数是n,那么叶子节点个数为n/2(n为偶数)或(n+1)/2(n为奇数);
2. if一颗二叉树的层数是n,那么满二叉树的节点数为2^n-1; 叶子节点的总数为2^(n-1)

3.递归在二叉树中的应用

写递归算法的关键就是明确函数的定义是什么,然后相信这个定义,利用这个定义推道出最终结果,绝不要跳入递归的细节 (人的小脑袋才能堆几层栈)

写树相关的算法,简单的就是说,去想象一个最小单元,搞清楚当前最小单元中t节点“该做什么,什么时候做”这两点,然后根据函数定义递归调用子节点。

  • 该做什么:就是我们的最小单元中的root节点(可能不止一个)想要实现功能,需要得到什么信息,然后能做什么;
    • 做点什么能够提供信息给下面的子树利用(先序)
    • 能从下面的子树上获得什么信息然后利用(后序)
  • 什么时候做:刚才写的代码应该放在前序、中序还是后序的代码位置上。

把题目的要求细化,搞清楚根节点应该做什么,然后剩下的事情抛给前/中/后序的遍历框架就行了,难点在于如何通过题目的要求思考出每一个节点需要做什么。
写完之后自己代入一个最基本的看能不能实现功能,会不会出不来,检验一下。

4.遍历

4.1 概念

  • 前序遍历:根节点 -> 左子树 -> 右子树;
  • 中序遍历:左子树 -> 根节点 -> 右子树;
  • 后序遍历:左子树 -> 右子树 -> 根节点;

4.2 递归实现

递归的时候不要太在意实现的细节,其本质上是通过栈来实现的,每次在方法里自己调自己,就把新调用的自己压栈,是一个有去有回的过程。
对于递归,关键就是要清楚函数的功能,什么时候停下来。
对于前序遍历,想先打印根节点,再左再右;那就先输出,再去递归调用,传入当前节点的左子树,左子树同样打印它的根,传入根的左子树,知道整个左子树处理完了,再去处理右子树;

class BinaryTreeTraverse<T>{

     /**
     * 前序遍历(递归实现);
     */
    public static void preOrderByRecursion(TreeNode node){
        if (node == null) return; //递归终止条件;
        System.out.println(node.value); //获取根节点的值;
        preOrderByRecursion(node.left); //左子树的根节点;
        preOrderByRecursion(node.right);//右子树的根节点;
    }
     /**
     * 中序遍历(递归实现)
     */
    public static void inOrderByRecursion(TreeNode node){
        if (node == null) return; //递归终止;
        inOrderByRecursion(node.left); //先左子树;
        System.out.println(node.value); //再根节点;
        inOrderByRecursion(node.right); //再右节点;
    }
     /**
     * 后序遍历(递归实现)
     */
    public static void postOrderByRecursion(TreeNode node){
        if (node == null) return;
        postOrderByRecursion(node.left);
        postOrderByRecursion(node.right);
        System.out.println(node.value);
    }

时间复杂度:0(N),每个节点遍历N次;
空间复杂度:O(N),递归过程中栈的开销;

4.3 迭代实现

前序遍历
前序遍历就是我们来手动实现在递归过程中的栈。
想要实现先左再右,那压栈的时候右先入栈,左再入栈。
如下图所示;
image
规则:
压入根节点;
1.弹出就打印;
2.如有右孩子,压入右;
3.如有左孩子,压入左;
重复1.2.3
思考一下这个过程;其实就是相当于两个孩子来替换栈里的根节点,这就很符合前序的定义,根先走,左子树干到了最顶部,要是我左子树还有左孩子,那我也走,两个孩子来替我,每次都是我先走,我左孩子这边整个都完事了,再来我右孩子,因为我右孩子被压在最下面。

public static void preOrder(TreeNode node){
    Stack<TreeNode> stack = new Stack<>();
    if (node != null){
        stack.push(node); //根节点入栈;
    }
    while (!stack.isEmpty()){
        TreeNode top = stack.pop(); //弹出就打印;
        System.out.println(top.value);
        if (top.right != null) stack.push(top.right); //依次入栈右节点和左节点;
        if (top.left != null) stack.push(top.left);
    }
}

中序遍历
规则:
1.整条左边界依次入栈;
2.条件1执行不了了,弹出就打印;
3.来到弹出节点的右子树上,继续执行条件1;(右树为空,执行2.弹出就打印;右树有节点,执行1.压栈;
说明:将整个树全用左边界去看,都是先处理了左边界,将左边界分解成了左头,头先入,左再入,然后弄不动了,弹出,然后看其右节点,再把右节点里的左依次进去;就这样往返;
如下如所示;

image

public static void inOrder(TreeNode node){
    Stack<TreeNode> stack = new Stack<>();
    while (!stack.isEmpty() || node != null){
        if (node != null){ //条件1;能往左走就往左走;
            stack.push(node);
            node = node.left;
        }else {
            TreeNode top = stack.pop(); //条件2;弹出打印;
            System.out.println(top.value);
            node = top.right; //条件3;来弹出节点右树上,继续1;
        }
    }
}

后序遍历
后序遍历可以用前序遍历来解决,想一下前序遍历:根左右,我们先压右树再压左树。怎么实现根右左呢,可以先压左树再压右树嘛,然后反过来不就是左右根了吗?(反过来用栈来实现,栈一个很大的作用就是实现逆序)

public static void postOrder(TreeNode node){
    Stack<TreeNode> stackA = new Stack<>();
    Stack<TreeNode> stackB = new Stack<>();
    if (node != null){
        stackA.push(node);
    }
    while (!stackA.isEmpty()){
        TreeNode top = stackA.pop();
        stackB.push(top);  //栈A弹出的进入栈B;先实现根右左,B倒序实现左右根;
        if (top.left != null) stackA.push(top.left);
        if (top.right != null) stackA.push(top.right);
    }
    while (!stackB.isEmpty()){
        System.out.println(stackB.pop().value);
    }
}

4.4 Morris实现

二叉树的结构中只有父节点指向孩子节点,孩子节点不能向上指,所以需要栈。
而Morris遍历的实质就是让下层节点也能指向上层,怎么办呢,一个节点有两个指针,左和右,如果这两个指针都有指向具体节点了那指定用不上了。
但是二叉树可是有很多空闲指针啊,比如说所有的叶子节点,它们的指针就都指向null,所以可以利用其right指针指向上层。
这样把下层往上层建立连接以后,cur指针就可以完整的顺着一个链条遍历完整个树。
因为不用堆栈,所以其空间复杂度变为O(1);
如下图所示:

image

cur指针走的顺序:1 2 4 2 5 1 3 6 3 7;
核心: 以某个根节点开始,找到其左子树的最右节点(必然是个叶子节点),然后利用其right指针指向根节点(建立从下到上的连接)

  • 到达两次的是有左子树的节点;
  • 到达一次的是没有左子树的节点;

原则:

  • 1.如果cur无左孩子,cur向右移动(cur=cur.right)【图中有4到2】
  • 2.如果cur有左孩子,找到cur左子树上最右的节点,记为mostRightNode;
    • 1.如果mostRightNode的right指针指向空,让其指向cur,cur向左移动(cur=cur.left)
    • 2.如果mostRightNode的right指针指向cur,让其指向空,cur向右移动(cur=cur.right)【图中由2到5】

(迭代法的中序遍历我们可以将整个树全部分成左边界去看,如上面的图,其实在morris遍历里我们可以将整个树全部分成右边界来看)

public static void morris(TreeNode node){
    if(node == null){
        return;
    }
    TreeNode cur = node;
    TreeNode mostRightNode = null; //记录cur左子树的最右节点;
    while(cur != null){
        mostRightNode = cur.left;
        if(mostRightNode != null){ //cur有左子树,就证明有下一层;
            //找到cur左子树的最右节点(找到cur下一层右边界的最后一个)
            while(mostRight.right != null && mostRightNode != cur){
                //1.最右节点为空说明到头了,找到了;
                //2.最右节点指向上层说明已经处理过了,来过了;
                mostRightNode = mostRightNode.right;
            }
            //走到这里证明跳出上面循环,无非两个原因:
            //1.右边没了;2.右边指向上层了(之前就处理过了);
            if(mostRightNode.right == null){
                mostRightNode.right = cur; //建立从最右节点到cur的连接;
                cur = cur.left; //处理下一个节点;
                continue;
            }else{
                //能到这里说明已经建立了最右节点到cur的连接;
                //也说明cur指的这个节点是第二次到了,断开连接;
                mostRightNode.right = null;
            }
        }
        //cur右移的情况:
        //1.cur没有左子树了(自然要开始处理右子树)
        //2.cur有左子树,但是cur左子树最右节点已经指向cur了(执行完上面else断开后,cur左边已经完全处理好了,开始右移。)
        cur = cur.right;
    }
}

前序遍历

1.对于cur只达到一次的节点(没有左子树),cur达到就打印;
2.对于cur到达两次的节点(有左子树),到达第一次时打印;

public static void preOrderMorris(TreeNode node){
    if (node == null){
        return;
    }
    TreeNode cur = node;
    TreeNode mostRightNode = null;
    while (cur != null){
        mostRightNode = cur.left;
        if (mostRightNode != null){ //到达两次的节点;
            //找到cur左子树的最右节点;
            while (mostRightNode.right != null && mostRightNode.right != cur){
                mostRightNode = mostRightNode.right;
            }
            if (mostRightNode.right == null){
                mostRightNode.right = cur;  //指向上层cur;
                System.out.println(cur.value); //第一次到的时候打印;
                cur = cur.left;
                continue;
            }else{
                mostRightNode.right = null; //第二次到时不打印;
            }
        }else {
            System.out.println(cur.value); //只到达一次的节点;
        }
        cur = cur.right;
    }
}

中序遍历

1.对于cur只达到一次的节点(没有左子树),cur达到就打印;
2.对于cur到达两次的节点(有左子树),到达第二次时打印;

public static void inOrderMorris(TreeNode node){
    if (node == null){
        return;
    }
    TreeNode cur = node;
    TreeNode mostRightNode = null;
    while (cur != null){
        mostRightNode = cur.left;
        if (mostRightNode != null){ //有左子树,到达两次的节点;
            while (mostRightNode.right != null && mostRightNode.right != cur){
                mostRightNode = mostRightNode.right;
            }
            if (mostRightNode == null){
                mostRightNode.right = cur; //第一次不打印;
                cur = cur.left;
                continue;
            }else {
                System.out.println(cur.value); //第二次遇到时打印;
                mostRightNode.right = null;
            }
        }else {
            System.out.println(cur.value); //只到达一次的节点,遇到就打印;
        }
        cur = cur.right;
    }
}

后序遍历

后序遍历比前面两个要复杂一点;
将一个节点的连续右节点当成是一个单链表看,如下图所示:

image

当我们到达最左侧,也就是左边连线已经创建完毕了。
打印 4
打印 5 2
打印 6
打印 7 3 1
我们将一个节点的连续右节点当成一个单链表来看待。
当我们返回上层之后,也就是将连线断开的时候,打印下层的单链表
比如返回到 2,此时打印 4
比如返回到 1,此时打印 5 2
比如返回到 3,此时打印 6
最后别忘记头节点那一串,即1 3 7
那么我们只需要将这个单链表逆序打印就行了。
这里不应该打印当前层,而是下一层,否则根结点会先与右边打印。

public static void postOrderMorris(TreeNode node){
    if (node == null){
        return;
    }
    TreeNode cur = node;
    TreeNode mostRightNode = null;
    while (cur != null){
        mostRightNode = cur.left;
        if (mostRightNode != null){
            while (mostRightNode.right != null && mostRightNode.right != cur){
                mostRightNode = mostRightNode.right;
            }
            if (mostRightNode.right == null){
                mostRightNode.right = cur;
                cur = cur.left;
                continue;
            }else { //能到这里的都是达到过两次的,也就是是有左孩子的。
                mostRightNode.right = null; //这时候是已经返回上层之后,断开了连接,所以打印下层的单链表;
                postMorrisPrint(cur.left);
            }
        }
        cur = cur.right;
    }
    postMorrisPrint(node); //最后把头节点那一串右打印一遍;
}
public static void postMorrisPrint(TreeNode node){
    TreeNode reverseList = postMorrisReverseList(node); //反转链表;
    TreeNode cur = reverseList;
    while (cur != null){
        System.out.println(cur.value);
        cur = cur.right;
    }
    postMorrisReverseList(reverseList); //最后再还原;
}
public static TreeNode postMorrisReverseList(TreeNode node){
    TreeNode cur = node;
    TreeNode pre = null;
    while (cur != null){
        TreeNode next = cur.right;
        cur.right = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}

4.5 层次遍历

层次遍历顾名思义就是一层一层的遍历。从上到下,从左到右,那需要借助什么结构呢?
可以采用队列的结构,利用其先进先出的特性,每一层依次入队,再依次出队。
对该层节点进行出队时,将这个节点的左右节点入队,这样当一层所有节点出队完成后,下一层也入队完成了。

/**
 * 层次遍历
 * 借助队列的结构,每一层依次入队,再依次出队;
 * 对该层节点进行出队操作时,需要将该节点的左孩子和右孩子入队;
 */
public static int layerOrder(TreeNode node){
    if (node == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(node);
    while (!queue.isEmpty()){
        int size = queue.size(); //当前层的节点数量;
        for (int i = 0; i < size; i++){
            TreeNode front = queue.poll();
            System.out.println(front.value);
            if (front.left != null) queue.add(front.left);
            if (front.right != null) queue.add(front.right);
        }
    }
}

5.深度

二叉树的最大深度是根节点到最远叶子结点的距离;

5.1 最大深度

递归实现

1.终止条件:在二叉树为空的时候,深度为1;
2.缩小范围,等价关系:给定一个二叉树,其深度为左子树的深度和右子树的深度的最大值+1;

/**
 * 求最大深度(递归)
 * 最大深度是左子树和右子树的最大深度的大的那个+1;
 */
public static int maxDepthByRecursion(TreeNode node){
    if(node == null) return 0;
    int leftDepth = maxDepthByRecursion(node.left);
    int rightDepth = maxDepthByRecursion(node.right);
    return Math.max(leftDepth,rightDepth)+1;
}

非递归实现(层次遍历)
关键点:每遍历一层,则计数器加+1;直到遍历完成,得到树的深度。
采用二叉树的层次遍历,来计数总共有多少层,采用队列的结构,当前层节点出队,计数器加1,然后把下一层的节点全部入队,直到队为空。

/**
 * 求最大深度(非递归)
 * 层次遍历(BFS)
 * 每遍历一层,则计数器加+1;直到遍历完成,得到树的深度。
 */
public static int maxDepth(TreeNode node){
    if (node == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    int level = 0; //层数;
    queue.add(node);
    while (!queue.isEmpty()){
        level++;
        int levelNum = queue.size(); //每层的节点数;
        for (int i = 0; i < levelNum; i++){
            TreeNode front = queue.poll(); //当前层出队,下一层入队;
            if (front.left != null) queue.add(front.left);
            if (front.right != null) queue.add(front.right);
        }
    }
    return level;
}

5.2 最小深度

二叉树的深度是根节点到最近叶子节点的距离;
递归实现

此题不能像最大深度那样直接求两颗子树的最大然后+1,最大深度可以是因为取大值不会影响一棵树为空的时候。但是取最小就不一样了,如果一棵树为空,那最小的应该是不为空的那边的值,但是还按原来方式就变成了0+1;比如下面这个例子:最小深度应该我2.但是按原来方式写的话最小深度就会变为1.所以,在处理每一个节点的时候,如果有两个孩子,那就可以继续取小+1,如果只有一个孩子,那就只能去递归它的孩子。
image

/**
 * 求最小深度(递归)
 * 注意和求最大深度的区别;
 */
public static int minDepthByRecursion(TreeNode node){
    if (node == null) return 0;
    if (node.right == null && node.left == null) return 1;
    if (node.left == null && node.right != null) return minDepthByRecursion(node.right) + 1;
    if (node.right == null && node.left != null) return minDepthByRecursion(node.left) + 1;
    return Math.min(minDepthByRecursion(node.left), minDepthByRecursion(node.right))+1;
}

非递归实现(层次遍历)
关键点:每遍历一层,则计数器加+1;在遍历的过程中,如果出现了没有叶子节点的节点,那就可以结束了,就是最小深度
采用二叉树的层次遍历,来计数总共有多少层,采用队列的结构,当前层节点出队,计数器加1,然后把下一层的节点全部入队,直到遇到叶子节点或队为空。

/**
 * 求最小深度(非递归)
 */
public static int minDepth(TreeNode node){
    if (node == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    int level = 0;
    queue.add(node);
    while (!queue.isEmpty()){
        level++;
        int levelnum = queue.size();
        for(int i = 0; i < levelnum; i++){
            TreeNode front = queue.poll();
            if (front.left == null && front.right == null){
                return level; //遇到第一个无叶子节点的时候,该节点的深度为最小深度;
            }
            if (front.right != null){
                queue.add(front.right);
            }
            if (front.left != null){
                queue.add(front.left);
            }
        }
    }
    return level;
}

6.重构二叉树

根据二叉树的前序或后序中的一个再加上中序来还原出整个二叉树。
注意: 中序是必须有的,因为其可以明确的把左右子树分开。
先看下3种遍历的特点(如下图):

image

特点

  • 1.前序的第一个节点是root,后序的最后一个节点是root。
  • 2.每种排序的左右子树分布都是有规律的。
  • 3.每一个子树又可以看成是一颗全新的树,仍然遵循上述规律。

6.1 前序+中序

前序的遍历顺序是根左右,中序的遍历顺序是左中右,

递归实现
1.前序的第一个节点是root节点,对应能够找到在中序中的位置。
2.根据中序遍历的特点,在找到的根前边序列是左子树的中序遍历序,后边序列是右子树的中序遍历。
3.求出左边序列的个数,比如设为leftSize,那在前序序列中紧跟着根的leftSize个元素是左子树的前序序列,后边的为右子树的前序序列。
4.这样就又获得了两个子树的前序遍历和中序遍历,开始递归。

/**
 * 根据前序遍历和中序遍历构造二叉树;
 */
public static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder){
    if (preorder == null){
        return null;
    }
    //因为我们要在中序遍历中寻找某个元素的位置,然后划分左右子树
    //用一个map来存储元素在中序遍历中的位置,
    Map<Integer,Integer> map = new HashMap<>();
    for(int i = 0; i < inorder.length; i++){
        map.put(inorder[i], i);
    }
    return buildTreeByPreOrder(preorder, inorder, 0, preorder.length-1, 0, inorder.length-1,map);
}
//传入前序和中序,传入前序的左右边界,中序的左右边界;
private static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder, int preleft, int preright, int inleft, int inright, Map<Integer,Integer> map){
    if (preleft > preright) return null;
    //获得整颗树的根节点:先序中的第一个元素;
    TreeNode root = new TreeNode(preorder[preleft]);
    //得到此元素在中序中的位置,以此进行划分出左右子树;
    int rootIndex = map.get(root);
    //得到左子树的大小;(注意此时不能直接是rootIndex,inleft不总是从0开始的,想一下建立右子树的左子树。
    int leftTreeSize = rootIndex - inleft;
    //左子树的中序:inleft不变,inright为rootIndex-1;
    //左子树的前序:preleft为根后一位,即preleft+1,preright为根后leftTreeSize位,即preleft+leftTreeSize;
    root.left = buildTreeByPreOrder(preorder,inorder,preleft+1, preleft+leftTreeSize, inleft, rootIndex-1, map);
    //右子树的中序:inleft为rootIndex+1,inright不变;
    //右子树的前序:preleft为左子树的右边界+1,即preleft+leftTreeSize+1,preright不变;
    root.right = buildTreeByPreOrder(preorder,inorder,preleft+leftTreeSize+1, preright, rootIndex+1, inright,map);
    return root;
} 

6.2 后序+中序

后序的遍历顺序是左右根,中序的遍历顺序是左根右,

递归实现
1.后序的第一个节点是root节点,对应能够找到在中序中的位置。
2.根据中序遍历的特点,在找到的根前边序列是左子树的中序遍历,后边序列是右子树的中序遍历。
3.求出左边序列的个数,比如设为leftSize,那在后序序列中的leftSize个元素是左子树的后序序列,后边的为右子树的后序序列。
4.这样就又获得了两个子树的后序遍历和中序遍历,开始递归。

/**
 * 根据后序遍历和中序遍历构造二叉树
 */
public static TreeNode buildTreeByPostOrder(int[] postorder, int[] inorder){
    if (postorder == null){
        return null;
    }
    //因为我们要在中序遍历中寻找某个元素的位置,然后划分左右子树
    //用一个map来存储元素在中序遍历中的位置,
    Map<Integer,Integer> map = new HashMap<>();
    for(int i = 0; i < inorder.length; i++){
        map.put(inorder[i], i);
    }
    return buildTreeByPostOrder(inorder, postorder, 0, inorder.length-1, 0, postorder.length-1,map);
}
//传入后序和中序,传入后序的左右边界,中序的左右边界;
private static TreeNode buildTreeByPostOrder(int[] inorder, int[] postorder, int inleft, int inright, int postleft, int postright, Map<Integer,Integer> map){
    if (postleft > postright) return null;
    //获得整颗树的根节点:后序中的最后个元素;
    TreeNode root = new TreeNode(postorder[postleft]);
    //得到此元素在中序中的位置,以此进行划分出左右子树;
    int rootIndex = map.get(root.value);
    //得到左子树的大小;(注意此时不能直接是rootIndex,inleft不总是从0开始的,想一下建立右子树的左子树。
    int leftTreeSize = rootIndex - inleft;
    root.left = buildTreeByPostOrder(inorder, postorder, inleft, rootIndex-1,postleft,postleft+leftTreeSize-1,map);
    root.right = buildTreeByPostOrder(inorder,postorder,rootIndex+1, inright, postleft+leftTreeSize,postright-1,map);
    return root;
}

6.3 层序遍历

除了前中后序外,其实有些题目里都会给出层序遍历的数组,有时候需要能够根据层序遍历来重构出来树;

# 和对二叉树进行层序遍历一样,同样需要借助队列的结构
# 对于每个元素,需要检验其后面两个元素;
class TreeNode:
  def __init__(self, data):
    self.val = data
    self.leftchild = None
    self.rightchild = None
  def create_tree(self, nodelist):
    if nodelist[0] = -1:
      return -1
    queue = deque()
    head = TreeNode(nodelist[0])
    queue.append(head)
    index = 1 #指示列表的位置
    while queue:
      node = queue.popleft()
      cur_node = nodelist[index]
      if cur_node == -1
         node.leftchild = None
      else:
         node.leftchild = TreeNode(cur_node)
         queue.append(node.leftchild)
      index += 1
      if index == len(nodelist):
        return head   #已经到头了
      cur_node = nodelist[index]
      if cur_node == -1
         node.rightchild = None
      else:
         node.rightchild = TreeNode(cur_node)
         queue.append(node.rightchild)
      index += 1
      if index == len(nodelist):
        return head   #已经到头了
    return head

附录(全程序)

package xin.utils;

import jdk.internal.dynalink.beans.StaticClass;
import sun.reflect.generics.tree.VoidDescriptor;

import javax.sound.midi.Soundbank;
import java.awt.font.TransformAttribute;
import java.util.*;

public class Tree {

}

/**
 * 定义一个二叉树节点;
 */
class TreeNode<T>{
    public T value;  //数据;
    public TreeNode<T> left; //左子树;
    public TreeNode<T> right;//右子树;

    public TreeNode(){} //空参的;
    public TreeNode(T value){ //有参的;
        this.value = value;
    }
    public TreeNode(T value, TreeNode left, TreeNode right){
        this.value = value;
        this.left = left;
        this.right = right;
    }
}

class BinaryTreeTraverse<T> {

    /**
     * 前序遍历(递归实现);
     */
    public static void preOrderByRecursion(TreeNode node) {
        if (node == null) return; //递归终止条件;
        System.out.println(node.value); //获取根节点的值;
        preOrderByRecursion(node.left); //左子树的根节点;
        preOrderByRecursion(node.right);//右子树的根节点;
    }

    /**
     * 前序遍历(非递归实现);
     * 本质上就是维持递归实现的栈;
     * 1.先入栈根节点,输出根节点的值,再入栈其右节点,左节点;(为了出栈的时候先出左节点,再出右节点);
     * 2.出栈左节点,输出值,再入栈左节点的右节点、左节点;直到遍历完左子树;
     * 3.出栈右节点,输出值,再入栈右节点的右节点、左节点;直到遍历完右子树;
     * 每次都是出栈一个根节点,如果有孩子,就依次入栈其右节点和左节点。
     * 规则:
     * 压入根节点;
     * 1.弹出就打印;
     * 2.如有右孩子,压入右;
     * 3.如有左孩子,压入左;重复;
     * (相当于两个孩子替换掉了栈里的根节点,这就很符号:根先走了,左子树干到了最顶部,要是我左子树还有孩子,ok,我也走,两个孩子来替我,
     * 要是左子树没孩子了,我自己出去,我这里就完事了;再去看右子树就可以了)
     */
    public static void preOrder(TreeNode node) {
        Stack<TreeNode> stack = new Stack<>();
        if (node != null) {
            stack.push(node); //根节点入栈;
        }
        while (!stack.isEmpty()) {
            TreeNode top = stack.pop(); //弹出就打印;
            System.out.println(top.value);
            if (top.right != null) stack.push(top.right); //依次入栈右节点和左节点;
            if (top.left != null) stack.push(top.left);
        }
    }

    /**
     * 中序遍历(递归实现)
     */
    public static void inOrderByRecursion(TreeNode node) {
        if (node == null) return; //递归终止;
        inOrderByRecursion(node.left); //先左子树;
        System.out.println(node.value); //再根节点;
        inOrderByRecursion(node.right); //再右节点;
    }

    /**
     * 中序遍历(非递归实现)
     * 规则:
     * 1.整条左边界依次压栈;
     * 2.条件1执行不了,弹出就打印;
     * 3.来到弹出节点右树上,继续执行条件1;(右树为空,执行2.弹出打印;右树不为空,执行1;压栈)
     * 说明:将整个树全用左边界去看,都是先处理了左边界,将左边界分解成了左头,头先入,左再入,然后弄不动了,弹出,然后看其右节点,再把右节点里的左依次进去;就这样往返;
     */
    public static void inOrder(TreeNode node) {
        Stack<TreeNode> stack = new Stack<>();
        while (!stack.isEmpty() || node != null) {
            if (node != null) { //条件1;能往左走就往左走;
                stack.push(node);
                node = node.left;
            } else {
                TreeNode top = stack.pop(); //条件2;弹出打印;
                System.out.println(top.value);
                node = top.right; //条件3;来弹出节点右树上,继续1;
            }
        }
    }

    /**
     * 后序遍历(递归实现)
     */
    public static void postOrderByRecursion(TreeNode node) {
        if (node == null) return;
        postOrderByRecursion(node.left);
        postOrderByRecursion(node.right);
        System.out.println(node.value);
    }

    /**
     * 后序遍历(非递归实现)
     * 想一下前序遍历:根左右;过程是先压右孩子,再压左孩子;
     * 如果我们想实现根右左:那就把前序里的换成先压左,再压右;就处理成了 根右左;
     * 然后再从后往前看,就变成了右左根;所以可以再准备一个栈,用来把第一个栈弹出的压到第二个,那第二个弹出的时候就倒过来了;
     * 要记住:栈有实现倒序的功能;
     */
    public static void postOrder(TreeNode node) {
        Stack<TreeNode> stackA = new Stack<>();
        Stack<TreeNode> stackB = new Stack<>();
        if (node != null) {
            stackA.push(node);
        }
        while (!stackA.isEmpty()) {
            TreeNode top = stackA.pop();
            stackB.push(top);  //栈A弹出的进入栈B;先实现根右左,B倒序实现左右根;
            if (top.left != null) stackA.push(top.left);
            if (top.right != null) stackA.push(top.right);
        }
        while (!stackB.isEmpty()) {
            System.out.println(stackB.pop().value);
        }
    }

    /**
     * Morris遍历;
     * 二叉树的结构中只有父节点指向孩子节点,孩子节点不能向上指,所以需要栈。
     * 而morris遍历的实质就是让下层节点能够指向上层。怎么办呢,一个节点有两个指针,左和右,如果这两个指针上都有指向具体的节点肯定就不行了。
     * 但是二叉树上有很多空闲指针,比如所有的叶子节点,它们的指针就指向null,所以可以利用其right指针指向上层的节点。
     * 这样连接后,cur这个指针就可以完整的顺着一个链条遍历完整个树。
     * 核心:以某个根节点开始,找到它左子树的最右侧节点(必然是个叶子节点),然后利用其right指向根节点(完成向上层的返回)。
     * 到达两次的是有左子树的节点;
     * 到达一次的是没有左子树的节点;
     */
    public static void morris(TreeNode node) {
        if (node == null) {
            return;
        }
        TreeNode cur = node;
        TreeNode mostRight = null; //cur左子树的最右节点;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) { //cur有左子树;
                //找到左子树的最右节点;
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right; //1.右边为空了证明到头了(找到了);2.右边指向上层了证明之前就处理过了,结束;
                }
                //走到这里证明跳出上面循环,无非两个原因:
                //1.右边没了;2.右边指向上层了(之前就处理过了);
                if (mostRight.right == null) { //右边走到头了;
                    mostRight.right = cur; //左子树的最右节点指向上层(cur);
                    cur = cur.left;        //cur左移,处理下一个节点;
                    continue; //此次循环结束,开始下一个cur;
                } else {  //证明这个mostRight的right指针已经处理过了,即mostRight已经指向了cur;
                    // 能到这里说明我们已经回到了根节点,并且重复了之前的操作;也说明我们已经完全处理完了此根节点左边的的树了,把路断开;
                    mostRight.right = null;
                }
            }
            //cur右移的情况:
            //1.cur没有左子树了(自然要开始处理右子树)
            //2.cur有左子树,但是cur左子树最右节点已经指向cur了(执行完上面else断开后,cur左边已经完全处理好了,开始右移。)
            cur = cur.right;
        }
    }

    /**
     * 前序遍历(Morris实现);
     * 1.对于cur只到达一次的节点(没有左子树),cur到达就打印;
     * 2.对于cur到达两次的节点,到达第一次时打印;
     */
    public static void preOrderMorris(TreeNode node) {
        if (node == null) {
            return;
        }
        TreeNode cur = node;
        TreeNode mostRightNode = null;
        while (cur != null) {
            mostRightNode = cur.left;
            if (mostRightNode != null) { //到达两次的节点;
                //找到cur左子树的最右节点;
                while (mostRightNode.right != null && mostRightNode.right != cur) {
                    mostRightNode = mostRightNode.right;
                }
                if (mostRightNode.right == null) {
                    mostRightNode.right = cur;  //指向上层cur;
                    System.out.println(cur.value); //第一次到的时候打印;
                    cur = cur.left;
                    continue;
                } else {
                    mostRightNode.right = null; //第二次到时不打印;
                }
            } else {
                System.out.println(cur.value); //只到达一次的节点;
            }
            cur = cur.right;
        }
    }

    /**
     * 中序遍历(Morris实现);
     * 1.对于cur只到达一次的节点(没有左子树),cur到达就打印;
     * 2.对于cur到达两次的节点,到达第二次时打印;
     */
    public static void inOrderMorris(TreeNode node) {
        if (node == null) {
            return;
        }
        TreeNode cur = node;
        TreeNode mostRightNode = null;
        while (cur != null) {
            mostRightNode = cur.left;
            if (mostRightNode != null) { //有左子树,到达两次的节点;
                while (mostRightNode.right != null && mostRightNode.right != cur) {
                    mostRightNode = mostRightNode.right;
                }
                if (mostRightNode == null) {
                    mostRightNode.right = cur; //第一次不打印;
                    cur = cur.left;
                    continue;
                } else {
                    System.out.println(cur.value); //第二次遇到时打印;
                    mostRightNode.right = null;
                }
            } else {
                System.out.println(cur.value); //只到达一次的节点,遇到就打印;
            }
            cur = cur.right;
        }
    }

    /**
     * 后序遍历(morris实现)
     * 后序遍历比前面两个要复杂一点;
     * 将一个节点的连续右节点当做是一个单链表来看待,
     * 当返回上层后,也就是将建立的连线断开后,打印下层的单链表;
     * 单链表逆序打印,就和我们做的把单链表逆序一样。
     */
    public static void postOrderMorris(TreeNode node) {
        if (cur == null) {
            return;
        }
        TreeNode cur = node;
        TreeNode mostRightNode = null;
        while (node != null) {
            mostRightNode = cur.left;
            if (mostRightNode != null) {
                while (mostRightNode.right != null && mostRightNode.right != cur) {
                    mostRightNode = mostRightNode.right;
                }
                if (mostRightNode.right == null) {
                    mostRightNode.right = cur;
                    cur = cur.left;
                    continue;
                } else { //能到这里的都是达到过两次的,也就是是有左孩子的。
                    mostRightNode.right = null; //这时候是已经返回上层之后,断开了连接,所以打印下层的单链表;
                    postMorrisPrint(cur.left);
                }
            }
            cur = cur.right;
        }
        postMorrisPrint(node); //最后把头节点那一串右打印一遍;
    }

    public static void postMorrisPrint(TreeNode node) {
        TreeNode reverseList = postMorrisReverseList(node); //反转链表;
        TreeNode cur = reverseList;
        while (cur != null) {
            System.out.println(cur.value);
            cur = cur.right;
        }
        postMorrisReverseList(reverseList); //最后再还原;
    }

    public static TreeNode postMorrisReverseList(TreeNode node) {
        TreeNode cur = node;
        TreeNode pre = null;
        while (cur != null) {
            TreeNode next = cur.right;
            cur.right = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }

    /**
     * 层次遍历
     * 借助队列的结构,每一层依次入队,再依次出队;
     * 对该层节点进行出队操作时,需要将该节点的左孩子和右孩子入队;
     */
    public static void layerOrder(TreeNode node) {
        if (node == null) return;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(node);
        while (!queue.isEmpty()) {
            int size = queue.size(); //当前层的节点数量;
            for (int i = 0; i < size; i++) {
                TreeNode front = queue.poll();
                System.out.println(front.value);
                if (front.left != null) queue.add(front.left);
                if (front.right != null) queue.add(front.right);
            }
        }
    }
}
class Depth{

    /**
     * 求最大深度(递归)
     * 最大深度是左子树和右子树的最大深度的大的那个+1;
     */
    public static int maxDepthByRecursion(TreeNode node){
        if(node == null) return 0;
        int leftDepth = maxDepthByRecursion(node.left);
        int rightDepth = maxDepthByRecursion(node.right);
        return Math.max(leftDepth,rightDepth)+1;
    }

    /**
     * 求最大深度(非递归)
     * 层次遍历(BFS)
     * 每遍历一层,则计数器加+1;直到遍历完成,得到树的深度。
     */
    public static int maxDepth(TreeNode node){
        if (node == null) return 0;
        Queue<TreeNode> queue = new LinkedList<>();
        int level = 0; //层数;
        queue.add(node);
        while (!queue.isEmpty()){
            level++;
            int levelNum = queue.size(); //每层的节点数;
            for (int i = 0; i < levelNum; i++){
                TreeNode front = queue.poll(); //当前层出队,下一层入队;
                if (front.left != null) queue.add(front.left);
                if (front.right != null) queue.add(front.right);
            }
        }
        return level;
    }

    /**
     * 求最小深度(递归)
     * 注意和求最大深度的区别;
     */
    public static int minDepthByRecursion(TreeNode node){
        if (node == null) return 0;
        if (node.right == null && node.left == null) return 1;
        if (node.left == null && node.right != null) return minDepthByRecursion(node.right) + 1;
        if (node.right == null && node.left != null) return minDepthByRecursion(node.left) + 1;
        return Math.min(minDepthByRecursion(node.left), minDepthByRecursion(node.right))+1;
    }

    /**
     * 求最小深度(非递归)
     */
    public static int minDepth(TreeNode node){
        if (node == null) return 0;
        Queue<TreeNode> queue = new LinkedList<>();
        int level = 0;
        queue.add(node);
        while (!queue.isEmpty()){
            level++;
            int levelnum = queue.size();
            for(int i = 0; i < levelnum; i++){
                TreeNode front = queue.poll();
                if (front.left == null && front.right == null){
                    return level; //遇到第一个无叶子节点的时候,该节点的深度为最小深度;
                }
                if (front.right != null){
                    queue.add(front.right);
                }
                if (front.left != null){
                    queue.add(front.left);
                }
            }
        }
        return level;
    }
}

class BuildBinaryTree{

    /**
     * 根据前序遍历和中序遍历构造二叉树;
     */
    public static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder){
        if (preorder == null){
            return null;
        }
        //因为我们要在中序遍历中寻找某个元素的位置,然后划分左右子树
        //用一个map来存储元素在中序遍历中的位置,
        Map<Integer,Integer> map = new HashMap<>();
        for(int i = 0; i < inorder.length; i++){
            map.put(inorder[i], i);
        }
        return buildTreeByPreOrder(preorder, inorder, 0, preorder.length-1, 0, inorder.length-1,map);
    }
    //传入前序和中序,传入前序的左右边界,中序的左右边界;
    private static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder, int preleft, int preright, int inleft, int inright, Map<Integer,Integer> map){
        if (preleft > preright) return null;
        //获得整颗树的根节点:先序中的第一个元素;
        TreeNode root = new TreeNode(preorder[preleft]);
        //得到此元素在中序中的位置,以此进行划分出左右子树;
        int rootIndex = map.get(root.value);
        //得到左子树的大小;(注意此时不能直接是rootIndex,inleft不总是从0开始的,想一下建立右子树的左子树。
        int leftTreeSize = rootIndex - inleft;
        //左子树的中序:inleft不变,inright为rootIndex-1;
        //左子树的前序:preleft为根后一位,即preleft+1,preright为根后leftTreeSize位,即preleft+leftTreeSize;
        root.left = buildTreeByPreOrder(preorder,inorder,preleft+1, preleft+leftTreeSize, inleft, rootIndex-1, map);
        //右子树的中序:inleft为rootIndex+1,inright不变;
        //右子树的前序:preleft为左子树的右边界+1,即preleft+leftTreeSize+1,preright不变;
        root.right = buildTreeByPreOrder(preorder,inorder,preleft+leftTreeSize+1, preright, rootIndex+1, inright,map);
        return root;
    }

    /**
     * 根据后序遍历和中序遍历构造二叉树
     */
    public static TreeNode buildTreeByPostOrder(int[] postorder, int[] inorder){
        if (postorder == null){
            return null;
        }
        //因为我们要在中序遍历中寻找某个元素的位置,然后划分左右子树
        //用一个map来存储元素在中序遍历中的位置,
        Map<Integer,Integer> map = new HashMap<>();
        for(int i = 0; i < inorder.length; i++){
            map.put(inorder[i], i);
        }
        return buildTreeByPostOrder(inorder, postorder, 0, inorder.length-1, 0, postorder.length-1,map);
    }
    //传入后序和中序,传入后序的左右边界,中序的左右边界;
    private static TreeNode buildTreeByPostOrder(int[] inorder, int[] postorder, int inleft, int inright, int postleft, int postright, Map<Integer,Integer> map){
        if (postleft > postright) return null;
        //获得整颗树的根节点:后序中的最后个元素;
        TreeNode root = new TreeNode(postorder[postleft]);
        //得到此元素在中序中的位置,以此进行划分出左右子树;
        int rootIndex = map.get(root.value);
        //得到左子树的大小;(注意此时不能直接是rootIndex,inleft不总是从0开始的,想一下建立右子树的左子树。
        int leftTreeSize = rootIndex - inleft;
        root.left = buildTreeByPostOrder(inorder, postorder, inleft, rootIndex-1,postleft,postleft+leftTreeSize-1,map);
        root.right = buildTreeByPostOrder(inorder,postorder,rootIndex+1, inright, postleft+leftTreeSize,postright-1,map);

        return root;
    }
}

参考链接

史上最全遍历二叉树详解
二叉树总结
算法基地-二叉树

posted @ 2021-07-26 21:48  Curryxin  阅读(1778)  评论(4编辑  收藏  举报
Live2D