一篇文章带你玩遍二叉树

写在前面

二叉树这种结构,说简单也简单,说难也难;简单在它清晰的结构让人一看就会,难在有些代码他是一敲就废;希望有这样问题的朋友看过这篇文章能帮你们摆脱这样的困扰。

一、二叉树的遍历

二叉树的遍历分为三种,分别是前中后序遍历;前中后实际上都是代表父节点在遍历过程中所处的位置;例如前序遍历的顺序就是:父节点->左节点->右节点,只要你的每一个节点都按照这个顺序来遍历,那么最终结果就是前序遍历,接下来我们来看看具体的操作是什么样的。(实际上二叉树的遍历都可以分为递归和非递归两种解法,递归的方式比较简单,我们这里就不写了,但是思想是一样的)

前序遍历

设置一个辅助栈和一个循环,进一次循环就弹出一个节点,然后先后遍历右节点和左节点

    public static void preOrderUnRecur(Node head){
        if(head != null){
            Stack<Node> stack = new Stack<>();
            stack.add(head);
            while(!stack.isEmpty()){
                head = stack.pop();
                System.out.println(head.value + " ");
                if (head.right != null) stack.push(head.right);
                if (head.left != null) stack.push(head.left);
            }
        }
        System.out.println();
    }

那么现在我们知道了如何按照父右左的顺序打印前序遍历,我们也可以做到按照父左右的顺序打印出一个新的遍历顺序,只需要交换左右节点压栈的顺序就行了。这个有什么用呢?假如我们再多准备一个栈,此时每一次从辅助栈A中弹出的节点不进行打印,而是进入辅助栈B中,最后等所有节点都进入了辅助栈B,我们统一将节点进行弹出打印,我们会得到什么结果?此时得到的就是后序遍历结果。

后序遍历

    public static void posOrderUnRecur(Node head){
        if(head != null){
            Stack<Node> stackA = new Stack<>();
            Stack<Node> stackB = new Stack<>();
            stackA.add(head);
            while(!stackA.isEmpty()){
                head = stackA.pop();
                stackB.push(head);
                if (head.left != null) stackA.push(head.left);
                if (head.right != null) stackA.push(head.right);
            }
            while(!stackB.isEmpty()){
                System.out.println(stackB.pop().value + " ");
            }
        }
        System.out.println();
    }

中序遍历

中序遍历的处理方式不同于前两种。对于任何一个二叉树的节点,我们都先将当前节点的左边界全部压入栈中,然后开始弹出节点,每弹出一个节点都检查它有没有右节点,有的话继续将右节点的左边界压入栈中;重复这样的操作直到所有节点全部结束。

    public static void inOrderUnRecur(Node head){
        if (head != null){
            Stack<Node> stack = new Stack<>();
            while(!stack.isEmpty() ||head != null){
                if (head == null) {
                    stack.push(head);
                    head = head.left;
                } else {
                    head = stack.pop();
                    System.out.println(head.value + " ");
                    head = head.right;
                }
            }
        }
        System.out.println();
    }

广度优先遍历(BFS)

简单来说就是横向遍历二叉树的每一层,比较简单,直接看代码怎么写。

    public static void wide(Node head){
        if (head == null) return;
        Queue<Node> queue = new LinkedList<>();
        queue.add(head);
        while(!queue.isEmpty()){
            Node current = queue.poll();
            System.out.println(current.value);
            if (current.left != null) queue.add(current.left);
            if (current.right != null) queue.add(current.right);
        }
    }

 Question:根据广度优先遍历的思想,返回当前二叉树最大的宽度

这个问题其实很好理解,从横向的角度看二叉树,使用一个变量记录当前最大宽度,最后返回这个变量max就可以了。那么问题在于我们如何能够确定每一个节点到底是在哪一层呢,只需要创建一个HashMap,每一次将当前节点压入栈的时候同时将这个节点所在层数记录到map里,然后每次在同一层的时候就进行统计,这过程当然需要额外的几个辅助变量,下面代码中的变量名字可以直接反映出作用。(其实操作起来很简单,但是需要大家自己动手去敲一遍,边敲边理解)

    public static int findWidest(Node head){
        if (head == null) return 0;
        Queue<Node> queue = new LinkedList<>();
        HashMap<Node, Integer> levelMap = new HashMap<>();
        levelMap.put(head, 1);
        int currentLevel = 1;
        int currentLevelNodeNum = 0;
        int max = Integer.MIN_VALUE;
        queue.add(head);
        while(!queue.isEmpty()){
            Node current = queue.poll();
            int currentLevel_Temp = levelMap.get(head);
            if (currentLevel_Temp == currentLevel){
                currentLevelNodeNum++;
            }else{
                max = Math.max(currentLevelNodeNum, max);
                currentLevelNodeNum = 1;
                currentLevel++;
            }
            if (current.left != null) {
                levelMap.put(head.left, currentLevel + 1);
                queue.add(current.left);
            }
            if (current.right != null) {
                levelMap.put(head.right, currentLevel + 1);
                queue.add(current.right);
            }
        }
        return max;
    }

上面的方法是用了一个额外的数据结构HashMap,那有没有没不用HashMap的方法呢,当然是有的咯。 

我们先思考一下如果不适用额外的数据结构,还要能清楚的知道当前我们在哪一层,应该怎么办呢?相信很多同学的第一反应都是一样的:每个节点到底在第几层知道了,我才能知道是不是跳到下一层了呀。然而!实际上我们并不需要知道当前我们在第几层了,只需要我们能够捕捉到二叉树进入下一层这个时间点就可以了,至于到底是第几层进入第几层,其实在这道题中并不重要,下面我们讲讲思路:(有条件的旁友,拿出你的笔和纸,按照下面的思路自己画一画,很容易理解)

  • 首先,我们首先设立两个Node类型的变量:currentEndNode,nextEndNode;第一个currentEndNode代表的是当前层最后一个节点是哪一个,第二个nextEndNode代表的是下一层的最后一个节点是哪一个,其他的几个变量跟上面使用HashMap的方法一样;
  • 当我们从根节点出发,因为二叉树只有一个根节点,所以将cu
  • rrentEndNode指向这个根节点,NextEndNode指向null;并且此时将当前节点压入栈中,这个时候我们的初始化步骤算是结束了;
  • 然后按照第一种解法的思路,先将当前节点弹出栈,并且将当前层的节点数量(currentLevelNodeNum)更新,接着依次将当前节点的左右节点压入栈中,并且每将一个节点压入栈中,就将nextEndLevel指向这个刚刚进栈的节点;此时我们发现弹出的节点等于currentEndNode,也就是说当前这一层已经结束了,所以在我们移动到下一层之前需要更新我们的max值以及将nextEndNode所指向的节点复制给currentEndNode,并且将nextEndNode指向null,以及将当前的currentLevelNodeNum重置为0;
  • 现在我们继续按照遍历的顺序来到下一层,当前节点变为刚刚根节点的左节点,同样执行上一步的逻辑,如果在比较当前节点和currentEndNode的时候我们发现不想等,说明我们还没有走到当前层的结尾,那么继续进行相同的操作;
  • 按照这样的逻辑一直走,最后返回max,就能够得到我们要的答案。

深度优先遍历(DFS)

这个就更简单了,跟前序遍历的操作流程一模一样,可以直接参考上文中讲到的前序遍历。

二、二叉树的种类

1、搜索二叉树

定义:对于任何一个二叉树的节点,当前节点值都比它左子树的值大(如果有左子树),比右子树的值小(如果有右子树)。

如何判断一颗二叉树是不是搜索二叉树的方法就是中序遍历,因为搜索二叉树的中序遍历结果一定是一个升序遍历;或者可以使用简单的递归来完成检查的操作:

    public static int preValue = Integer.MIN_VALUE;
    
    public static boolean isBST(Node head){
        if (head == null) return true;
        boolean isLeftBST = isBST(head.left);
        if (!isLeftBST) {
            return false;
        }
        if (head.value <= preValue) {
            return false;
        } else {
            preValue = head.value;
        }
        return isBST(head.right);
    }

还有第二种写法

    public static class ReturnData{
        public Integer max;
        public Integer min;
        public boolean isBST;

        public ReturnData(Integer max, Integer min, boolean isBST) {
            this.max = max;
            this.min = min;
            this.isBST = isBST;
        }
    }

    public static ReturnData bstProcess(Node x){
        if (x == null) return null;
        ReturnData leftData = bstProcess(x.left);
        ReturnData rightData = bstProcess(x.right);
        int max = x.value;
        int min = x.value;
        if (leftData != null) {
            min = Math.min(min, leftData.min);
            max = Math.max(max, leftData.max);
        }
        if (rightData != null) {
            min = Math.min(min, rightData.min);
            max = Math.min(max, rightData.max);
        }
        boolean isBST = true;
        if (leftData != null && (!leftData.isBST || leftData.max >= x.value)) isBST = false;
        if (rightData != null && (!rightData.isBST || rightData.min <= x.value)) isBST = false;
        return new ReturnData(max, min, isBST);
    }

2、完全二叉树

定义:二叉树除了最下面一层以外的每一层都是满的,最后一层可以不满,但是必须满足从左到右都是依次变满的(也就是可以理解为连续的意思)。

怎么判断呢?

  1. 在进行宽度遍历的过程中发现有任何一个节点只有右子树而没有左子树,返回false;
  2. 在不违反第一个条件的情况下,如果遇到了第一个左右子树不双全的节点,那么后续的节点都应该是叶节点。
    public static boolean isCBT(Node head){
        if (head == null) return true;
        LinkedList<Node> queue = new LinkedList<>();
        boolean leaf = false;
        Node left = null;
        Node right = null;
        queue.add(head);
        while (!queue.isEmpty()) {
            head = queue.poll();
            left = head.left;
            right = head.right;
            if ((leaf && (left != null || right != null)) || (left == null && right != null)) return false;
            if (left != null) queue.add(left);
            if (right != null) queue.add(right);
            if (left == null || right == null) leaf = true;
        }
        return true;
    }

3、平衡二叉树

 定义:对于任何一个节点来说,它的左右子树的高度差不超过1。

所以为了判断是否平衡,我们只需要知道对于任何一个节点来说,它的高度是多少以及它是否是平衡的节点。

    public static boolean isBalanced(Node head){
        return process(head).isBalanced;
    }

    public static class ReturnType{
        public boolean isBalanced;
        public Integer height;

        public ReturnType(boolean isBalanced, Integer height) {
            this.isBalanced = isBalanced;
            this.height = height;
        }
    }

    public static ReturnType process(Node x){
        if (x == null) return new ReturnType(true, 0);
        ReturnType leftResult = process(x.left);
        ReturnType rightResult = process(x.right);
        int height = Math.max(leftResult.height, rightResult.height) + 1;
        boolean isBalanced = leftResult.isBalanced && rightResult.isBalanced && Math.abs(leftResult.height - rightResult.height) < 2;
        return new ReturnType(isBalanced, height);
    }

4、 满二叉树

定义:二叉树节点数量满足公式:2^{_{k}}-1,k为二叉树的深度。这样的二叉树就是满二叉树,所以判断的方法也很简单:

    public static boolean checkF(Node x){
        if (x == null) return true;
        fDataInfo dataInfo = f(x);
        return dataInfo.nodeNum == (1 << dataInfo.height - 1);
    }


    public static class fDataInfo{
        public Integer height;
        public Integer nodeNum;

        public fDataInfo(Integer height, Integer nodeNum) {
            this.height = height;
            this.nodeNum = nodeNum;
        }
    }

    public static fDataInfo f(Node x){
        if (x == null) return new fDataInfo(0, 0);
        fDataInfo leftData = f(x.left);
        fDataInfo rightData = f(x.right);
        Integer height = Math.max(leftData.height, rightData.height) + 1;
        Integer nodeNum = leftData.nodeNum + rightData.nodeNum + 1;
        return new fDataInfo(height, nodeNum);
    }

三、二叉树的常见考题

Question1:返回两个节点node1和node2的最低公共祖先

这个问题的可能性可以分为两种:第一种情况,node1和node2中有一个是对方的某一层的父节点,这意味着两个节点中必有一个为对方的最低公共祖先;第二种情况,两个节点都不是对方的某一层的父节点,这代表需要按照常规思维向上寻找最低公共父节点;那么我们直接看代码:

    public static Node findLowestAncestor(Node head, Node o1, Node o2){
        if (head == null || head == o1 || head == o2) return head;
        Node left = findLowestAncestor(head.left, o1, o2);
        Node right = findLowestAncestor(head.right, o1, o2);
        if (left != null && right != null){
            return head;
        }
        return left != null ? left : right;
    }

我们对这段代码进行一个解析:

  • 第一个if完成的是判断逻辑的base case;我们假设当下遇到一种情况是node1和node2中有一个就是整个二叉树的head节点,那么直接返回当前这个head(node1或者node2)就可以;但是假如我们遇到的是之前分析的两种情况呢?我们接着分析后面代码;
  • 根据使用递归处理二叉树问题的常用逻辑,设置两个Node分别用于接受当前节点的左右子树返回的信息,所以第二行和第三行代码就是用于接受左右子树信息的;
  • 第二个if判断做的处理是,假如我发现我的左右子树中都不等于空,那么一定说明当前这个节点就是最低公共父节点;肯定有同学觉得有点confuse了,为什么我说如果左右返回都不是空就能断定这个节点是要的节点呢?接着看下一步;
  • 最后一句的return其实完成的操作就是判断左右子树中是否包含了node1或者node2;因为在递归检查过程中之遥我们发现当前节点就是node1或者node2,那么我们在base case(也就是第一个if的判断中就会返回当前这个节点,node1或者node2)就会开始返回node1或者node2,而其他的节点都会返回null,所以才能够在第三步中进行那样的判断。

 Question2:返回指定节点的后继节点(已知每个node的父节点)

后继节点的定义是后序遍历的顺序中指定节点的下一个节点;那么按照一般思路来说只要我们将整个树全部按照后序遍历的方式进行一次遍历,我们就可以找到结果,但是这样的操作复杂度是O(N)的,实际上我们可以做到一个O(K)的复杂度的算法(k是两个节点之间的实际距离),如下:

    public static Node getNextNode(Node node){
        if (node == null) return node;
        if (node.right == null) {
            return getLeftMost(node.right);//有右子树的情况,那么直接找右子树的最左节点
        } else {//没有右子树
          Node parent = node.parent;
          while (parent != null && parent.left != node){// 当前节点是其父节点的右节点
              node = parent;
              parent = node.parent;
          }
          return parent;
        }
    }
    
    public static Node getLeftMost(Node node){
        if (node == null) return node;
        while (node.left != null){
            node = node.left;
        }
        return node;
    }

这个算法实际上将我们的问题分成了三种情况;

  • 一种是指定节点有右子树的情况,这种情况下我们需要的后继节点就在它的右子树的最左边,这一结论可以按照后序遍历的思路来验证一下;
  • 第二种情况是当前节点没有右子树并且它不是最后一个节点,那么怎么找它的后继节点呢?只要一直向父节点查询,第一个属于自己父节点的左子树的节点的父节点就是我们要找的后继节点(也就是说每当我们向上查找一个父节点,我们就验证一下当前这个节点是不是它的父节点的左子树,不是的话继续往上找,什么时候发现当前这个节点是自父节点的左子树,那么这个父节点就是目标节点的后继节点);
  • 第三种情况就是当前这个目标节点是整个二叉树的最后一个节点,那么它的后继节点就是null。

 Question3:二叉树的序列化和反序列化

序列化:把二叉树翻译成一个String类型的数据,每个二叉树都应该是唯一的;

反序列化:将String类型的数据翻译成二叉树。

这个问题实在是太简单了,可以使用前中后三种遍历方式来进行,需要注意的点只有:在面对null的时候,使用一个确定的特殊符号来表示,并且每一个节点的值在序列化后使用一个符号来作为间隔符号;掌握了前中后三种遍历方式的同学做这种问题都会控制不住笑出来的叭。

    public static String serialisePre(Node head){
        if (head == null) return "#_";
        String res = head.value + "_";
        res += serialisePre(head.left);
        res += serialisePre(head.right);
        return res;
    }

    public static Node reconByPreString(String preStr){
        String[] values = preStr.split("_");
        Queue<String> queue = new LinkedList<String>();
        for (int i = 0; i < values.length; i++){
            queue.add(values[i]);
        }
        return reconPreOrder(queue);
    }

    public static Node reconPreOrder(Queue<String> queue){
        String value = queue.poll();
        if (value.equals("#")) return null;
        Node head = new Node(Integer.valueOf(value));
        head.left = reconPreOrder(queue);
        head.right = reconPreOrder(queue);
        return head;
    }

Question4:现在给你一张长条状的纸条,确定纸条的一方在上方,一方在下方,并且确定纸条的其中一面对准自己。现在将纸条的下方从面朝自己这个方向对折上来,再展开,发现自己面对的这一面多了一个凹痕;将纸条还原至第一次对折的状态,再一次进行相同的对折,再展开,发现又多了两条痕迹,一个凸起,一个凹陷。现给定一个整数N,代表将纸条进行N次指定方式的对折,请依次打印出纸条展开后从上到下的折痕的凹陷情况。 

看起来是不是觉得这个问题非常的抽象,但是我们从最开始的几步仔细分析可以发现,每一次对折都会在原来的每一条折痕的上下两方都产生一凹一凸两条新的折痕;假如我们将当前这个纸条展开后横过来并且脑海中将它拉长我们可以发现,这张纸条的折痕情况和二叉树的分布是一样的,都是左子树凹陷,右子树凸起,对折次数N就是二叉树的层数,所以我们可以写出下面的代码:

    public static void printAllFolds(int N){
        printProcess(1, N, true);
    }

    public static void printProcess(int i, int N, boolean down){
        if (i > N) return;
        printProcess(i + 1, N, true);
        System.out.println(down ? "凹" : "凸");
        printProcess(i + 1, N, false);
    }

 那么长的题目,这么短的解答,说明我们只要能将实际问题转化为熟知的数据结构,就能简单快捷的解决问题。

 

持续更新。。。。

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值