左神算法初级班四 笔记

树的遍历-递归与非递归形式

先序遍历:课上我们可能被告知按照先中再左再右的顺序打印节点值就是先序。使用递归方式实现非常简单,就是打印语句和递归语句的顺序决定了遍历的形式:

    public static void preOrder(Node root){
        if(root==null)return;
        System.out.print(root.value+" ");
        preOrder(root.lchild);
        preOrder(root.rchild);
    }

现在我们换一种方式来看看各种序,我们有如下图一棵树:

 

在上述递归函数的执行下,所有节点的遍历情况为1-3-2-null-2-null-2-3-null-3-1-5-7-null-7-null-7-5-4-null-4-null-4-5-1,我们发现,除了空节点外,其实每个节点在递归过程中都会被访问三次,而在第一次、第二次、第三次打印就分别对应了先序、中序、后序这三种遍历方式。

现在我们讨论如何将递归改为非递归:

先序非递归版:对于每个节点,先考虑右节点如果不空,则右节点入栈;如果左节点不空,则左节点入栈,这样在栈内,从栈顶到栈底其实是先左再右的。

    public static void preOrderWithStack(Node root){
        if(root==null){
            System.out.println("The tree is empty!");
            return;
        }
        Stack<Node> stack=new Stack<Node>();
        stack.push(root);
        while(!stack.isEmpty()){
            root=stack.pop();
            System.out.print(root.value+" ");
            if(root.rchild!=null)stack.push(root.rchild);
            if(root.lchild!=null)stack.push(root.lchild);
        }
    }

注意,递归函数使用的函数栈比我们设计的栈要高级的多,可以压参数、函数执行现场(当前执行到哪一步以及到这一步时变量值等的信息),所以在递归函数中,一个节点可以被访问三次。但我们此处设计的栈只能访问每个节点两次,虽然可以通过一些方式访问三次(也许可以给每一个Node设计一个访问次数的域,每一个节点被访问到【从父亲过来或者从儿子回溯回来】时,Node访问数+1,当访问数==3时就不再丢回栈内。

中序递归版:

    public static void midOrder(Node root){
        if(root==null)return;
        midOrder(root.lchild);
        System.out.print(root.value+" ");
        midOrder(root.rchild);
    }

中序非递归版:

如果当前root不为空,把root入栈,root=root.lchid;

如果当前root为空,则将栈顶取出给root,打印值,root=root.rchild;

重复上述两个过程

     public static void midOrderWithStack(Node root){
        if(root==null)return;
        Stack<Node> stack=new Stack<Node>();
        while(!stack.isEmpty()||root!=null){
            if(root!=null){
                stack.push(root);
                root=root.lchild;
            }
            else{
                root=stack.pop();
                System.out.print(root.value+" ");
                root=root.rchild;
            }
        }
    }

过程说明,按照我们的逻辑,每次从当前结点开始,将以它为根的左链全部压入栈内,弹出时的情况为左孩子null,中根被弹出,此时应该打印右子树的最左节点,符合上述逻辑。整个流程下来,树的遍历方式为左、中、右。

后序遍历递归版:

    public static void afterOrder(Node root){
        if(root==null)return;
        afterOrder(root.lchild);
        afterOrder(root.rchild);
        System.out.print(root.value+" ");
    }

后序遍历非递归版:我们在先序遍历中是按照中左右的顺序打印,而后续遍历的顺序是左右中,此时我们可以考虑先实现一个中右左的,再将打印行为改为压栈,通过栈逆序后得到左右中。而中右左的实现可以通过将非递归版先右孩子后左孩子逆序得到。

    public static void afterOrderWithStack(Node root){
        if(root==null)return;
        Stack<Node> stack=new Stack<Node>();
        Stack<Node> data=new Stack<Node>();
        stack.push(root);
        while(!stack.isEmpty()){
            root=stack.pop();
            data.push(root);
            if(root.lchild!=null)stack.push(root.lchild);
            if(root.rchild!=null)stack.push(root.rchild);
        }
        while(!data.isEmpty()){
            System.out.print(data.pop().value+" ");
        }
    }

测试结果:

public class printTree {

    public static class Node{
        int value;
        Node lchild,rchild;
        Node(int value){
            this.value=value;
        }
    }

    public static void preOrder(Node root){
        if(root==null)return;
        System.out.print(root.value+" ");
        preOrder(root.lchild);
        preOrder(root.rchild);
    }

    public static void preOrderWithStack(Node root){
        if(root==null){
            System.out.println("The tree is empty!");
            return;
        }
        Stack<Node> stack=new Stack<Node>();
        stack.push(root);
        while(!stack.isEmpty()){
            root=stack.pop();
            System.out.print(root.value+" ");
            if(root.rchild!=null)stack.push(root.rchild);
            if(root.lchild!=null)stack.push(root.lchild);
        }
    }

    public static void midOrder(Node root){
        if(root==null)return;
        midOrder(root.lchild);
        System.out.print(root.value+" ");
        midOrder(root.rchild);
    }

    public static void midOrderWithStack(Node root){
        if(root==null)return;
        Stack<Node> stack=new Stack<Node>();
        while(!stack.isEmpty()||root!=null){
            if(root!=null){
                stack.push(root);
                root=root.lchild;
            }
            else{
                root=stack.pop();
                System.out.print(root.value+" ");
                root=root.rchild;
            }
        }
    }

    public static void afterOrder(Node root){
        if(root==null)return;
        afterOrder(root.lchild);
        afterOrder(root.rchild);
        System.out.print(root.value+" ");
    }

    public static void afterOrderWithStack(Node root){
        if(root==null)return;
        Stack<Node> stack=new Stack<Node>();
        Stack<Node> data=new Stack<Node>();
        stack.push(root);
        while(!stack.isEmpty()){
            root=stack.pop();
            data.push(root);
            if(root.lchild!=null)stack.push(root.lchild);
            if(root.rchild!=null)stack.push(root.rchild);
        }
        while(!data.isEmpty()){
            System.out.print(data.pop().value+" ");
        }
    }

    public static void main(String[] args) {
        Node root=new Node(1);
        root.lchild=new Node(2);
        root.rchild=new Node(3);
        root.lchild.lchild=new Node(4);
        root.lchild.rchild=new Node(5);
        root.rchild.lchild=new Node(6);
        root.rchild.rchild=new Node(7);

        System.out.println("前序遍历:");
        preOrder(root);
        System.out.println();
        preOrderWithStack(root);
        System.out.println("\n==============");

        System.out.println("中序遍历:");
        midOrder(root);
        System.out.println();
        midOrderWithStack(root);
        System.out.println("\n=============");

        System.out.println("后序遍历:");
        afterOrder(root);
        System.out.println();
        afterOrderWithStack(root);
        System.out.println();
    }
}

在二叉树中找到一个节点的后继节点

【题目】 现在有一种新的二叉树节点类型如下:

public class Node { public int value; public Node left; public Node right; public Node parent; public Node(int data) { this.value = data; } }
该结构比普通二叉树节点结构多了一个指向父节点的parent指针。假 设有一 棵Node类型的节点组成的二叉树,树中每个节点的parent指针 都正确地指向自己的父节点,头节点的parent指向null。只给一个在二叉树中的某个节点 node,请实现返回node的后继节点的函数。在二叉树的中序遍历的序列中, node的下一个节点叫作node的后继节点。

思路:首先,中序遍历本身就是按照先左再中再右的顺序打印,题目要求我们找到目标节点的后继节点。

1.如果目标节点含有右子树,所以其后继结点就是右子树的最左节点

2.如果目标节点不含有右子树,我们知道,中序遍历是按照左中右的顺序,所以目标节点一定是一个某一个节点A的前驱,所以目标节点是A的左子树中最后一个打印的节点,也就是说,我们需要找的节点需要满足它的左子树的最后一个节点是目标节点。那么我们只需要从目标节点开始,一步步往上,找到第一个满足当前节点为其父亲左孩子的节点,这个父亲节点就是结果。【方便起见,我们可以让root的parent指向null】

public class FindNextNode {
    public static class Node {
        public int value;
        public Node left;
        public Node right;
        public Node parent;
        public Node(int data) {
            this.value = data;
        }
    }

    public static Node getSuccessorNode(Node node) {
        if (node == null) {
            return null;
        }
        if(node.right!=null){
            return getLeftMost(node.right);
        }else{
            Node parent=node.parent;
            while(parent!=null&&parent.left!=node){
                node=node.parent;
                parent=parent.parent;
            }
            return parent;
        }
    }

    public static Node getLeftMost(Node node) {
        while (node.left != null) {
            node = node.left;
        }
        return node;
    }


    public static void main(String[] args) {
        Node head = new Node(6);
        head.parent = null;
        head.left = new Node(3);
        head.left.parent = head;
        head.left.left = new Node(1);
        head.left.left.parent = head.left;
        head.left.left.right = new Node(2);
        head.left.left.right.parent = head.left.left;
        head.left.right = new Node(4);
        head.left.right.parent = head.left;
        head.left.right.right = new Node(5);
        head.left.right.right.parent = head.left.right;
        head.right = new Node(9);
        head.right.parent = head;
        head.right.left = new Node(8);
        head.right.left.parent = head.right;
        head.right.left.left = new Node(7);
        head.right.left.left.parent = head.right.left;
        head.right.right = new Node(10);
        head.right.right.parent = head.right;

        Node test = head.left.left;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head.left.left.right;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head.left;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head.left.right;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head.left.right.right;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head.right.left.left;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head.right.left;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head.right;
        System.out.println(test.value + " next: " + getSuccessorNode(test).value);
        test = head.right.right; // 10's next is null
        System.out.println(test.value + " next: " + getSuccessorNode(test));
    }
    
}

树的序列化与反序列化:即如何使用字符串表示一棵树,并且可以重建。我们按照不同的方式遍历树,对应着不同的序列化方式。常用的有:先序、中序、后序、层序。

我们对树进行序列化,需要考虑还原的问题,所以对于null指针,我们需要标记,否则我们不知道反序列化的时候建树加深深度到什么时候停止。如下图:

不带有对空指针的标记,上图两棵树的序列化结果都是111,但是却对应着不同的树【我们使用#来表示空指针】,所以两棵树的序列化分别是11##1##、111####。

同时我们还需要一个分隔符用于分隔每个节点的值,否则也会出问题,如下图:

如果不适用分隔符表示,那么两者的序列化结果都是1234####,此时我们不能区分。所以我们引入分隔符_来对节点的值做一个具体的分隔,便于我们可以准确地重建出树,上述的序列化结果分别为1_23_4_#_#_#_#、1_2_34_#_#_#_#。

import java.util.LinkedList;
import java.util.Queue;

public class TreeSerialize {
    public static class Node{
        int value;
        Node lchild,rchild;
        Node(int value){
            this.value=value;
        }
    }

    public static String treeSerialize(Node root){
        if(root==null)return "#_";
        return root.value+"_"+treeSerialize(root.lchild)+treeSerialize(root.rchild);
    }

    public static Node reBuildTree(Queue<Integer> que){
        int value=que.poll();
        Node root;
        if(value==-30303030)return null;
        else root=new Node(value);
        root.lchild=reBuildTree(que);
        root.rchild=reBuildTree(que);
        return root;
    }

    public static Node reverseSerialize(String string){
        Queue<Integer> que=new LinkedList<Integer>();
        int num=0;
        for(int i=0;i<string.length();++i){
            if(string.charAt(i)=='_'){
                que.add(num);
                num=0;
            }
            else if(string.charAt(i)=='#'){
                num=-30303030;
            }else{
                num=num*10+string.charAt(i)-'0';
            }
        }
        return reBuildTree(que);
    }

    public static void main(String[] args) {
        Node root1=new Node(1);
        Node root2=new Node(23);
        Node root3=new Node(4);
        root1.lchild=root2;
        root2.lchild=root3;

        String treeSerizal=treeSerialize(root1);
        System.out.println("序列化结果:"+treeSerizal);

        Node rootNew=reverseSerialize(treeSerizal);
        System.out.println("重建后的序列化结果:"+treeSerialize(rootNew));

    }

}

判断一棵二叉树是否是平衡二叉树[树形Dp问题,可以看一下本菜的另一篇博客总结哦QAQ]

平衡树的定义:对于每一个非叶子节点,他们的左子树和右子树的高度差不超过1。

假设当前我们可以从两个孩子节点拿到需要的信息,那么我们知道:

1.如果左右子树中有一个不平衡了,那么该数不可能平衡

2.如果两个子树都平衡,需要看他们的高度差

因此我们知道,每次需要拿到两个子树的高度和平衡信息,而我们又需要针对所有的节点都判断平衡,因此这就是一个递归问题,判断以当前节点为根的树的平衡性=判断左右子树的平衡性+通过高度判断当前树是否平衡

public class IsBalancedTree {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }

    public static class ReturnData{
        int height;
        boolean isBalance;

        public ReturnData(int height, boolean isBalance) {
            this.height = height;
            this.isBalance = isBalance;
        }
    }

    public static ReturnData isBalance(Node root){
        if(root==null)return new ReturnData(0,true);
        ReturnData leftData=isBalance(root.left);
        if(leftData.isBalance==false)return new ReturnData(0,false);
        ReturnData rightData=isBalance(root.right);
        if(rightData.isBalance==false)return new ReturnData(0,false);
        if(Math.abs(leftData.height-rightData.height)>1){
            return new ReturnData(0,false);
        }
        return new ReturnData(Math.max(leftData.height,rightData.height)+1,true);
    }

    public static void main(String[] args) {
        Node head = new Node(1);
        head.left = new Node(2);
        head.right = new Node(3);
        head.left.left = new Node(4);
        head.left.right = new Node(5);
        head.right.left = new Node(6);
        head.right.right = new Node(7);

        System.out.println(isBalance(head).isBalance);

    }

}

判断一棵树是否是搜索二叉树

注意,当所有节点满足:如果存在左子树,则大于左子树的所有值;如果存在右子树,则小于右子树的所有值。于是显然,我们对于每一个节点,需要收集左子树的最大值和右子树的最小值,以及左右是否各自满足搜索二叉树的要求。

树形dp解法【简单理解树形dp就是,小问题和大问题的具体要求一样,只是树的高度宽度不同罢了】:

public class IsSearchBinaryTree {

    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }


    public static class ReturnType{
        int maxvalue,minvalue;
        boolean isOk;

        public ReturnType(int maxvalue,int minvalue, boolean isOk) {
            this.maxvalue = maxvalue;
            this.minvalue=minvalue;
            this.isOk = isOk;
        }
    }

    public static ReturnType isSBT(Node root){
        ReturnType leftReturn=null,rightReturn=null;
        if(root.left!=null){
            leftReturn=isSBT(root.left);
            if(leftReturn.isOk==false)return new ReturnType(0,0,false);
        }
        if(root.right!=null){
            rightReturn=isSBT(root.right);
            if(rightReturn.isOk==false)return new ReturnType(0,0,false);
        }
        int Lmin,Lmax,Rmin,Rmax;
        Lmin=leftReturn==null?root.value:leftReturn.minvalue;
        Lmax=leftReturn==null?root.value:leftReturn.maxvalue;
        if(Lmax>root.value)return new ReturnType(0,0,false);//当前节点小于左子树最大值
        Rmin=rightReturn==null?root.value:rightReturn.minvalue;
        Rmax=rightReturn==null?root.value:rightReturn.maxvalue;
        if(Rmin<root.value)return new ReturnType(0,0,false);//当前节点大于右子树最小值
        return new ReturnType(Rmax,Lmin,true);//以root为根的最大值来自右子树,最小值来自左子树
    }

    public static boolean isSearchBinaryTree(Node root){
        if(root==null)return true;
        else return isSBT(root).isOk;
    }

    public static void main(String[] args) {
        Node head = new Node(4);
        head.left = new Node(2);
        head.right = new Node(6);
        head.left.left = new Node(1);
        head.left.right = new Node(3);
        head.right.left = new Node(5);

        System.out.println(isSearchBinaryTree(head));
        //System.out.println(isSearchBinaryTree2(head));

    }
}

当然,根据搜索二叉树的性质,我们也知道,它中序遍历的结果是升序的,因此也有解法2:

    public static boolean isSearchBinaryTree2(Node root){
        int pre=-10000000;
        Stack<Node> stack=new Stack<>();
        while(root!=null||!stack.isEmpty()){
            if(root!=null){
                stack.push(root);
                root=root.left;
            }
            else{
                root=stack.pop();
                if(root.value<=pre)return false;
                else pre=root.value;
                root=root.right;
            }
        }
        return true;
    }

判断一棵树是否是完全二叉树

完全二叉树的性质是:对于每一层来说,要么满,要么必须保证左边满,右边空,即不存在某层左边空右边有节点的情况。

思路:

1.对整棵树进行层次遍历

2.对当前节点做判断:

        A.如果当前节点有右孩子却没有左孩子,那么这棵树肯定不是完全二叉树

        B.如果当前节点有左孩子没有右孩子或者没有左右孩子,则下个节点知道最后一个节点都必须是叶子节点

证明B:如果当前节点满足B的情况,那么可以知道B的下一层对应的孩子的位置的右邻居一定不存在,即当前节点的右邻居不存在孩子,即为叶子节点。【最好画图自行理解下QAQ】

    public static boolean isCompleteBinaryTree(Node root){
        Queue<Node> queue=new LinkedList<>();
        queue.add(root);
        boolean isLeaf=false;
        while(!queue.isEmpty()){
            root=queue.poll();
            if(  (isLeaf&&(root.left!=null||root.right!=null)) //如果开启了一个后续都是叶子的阶段却还是遇到了非叶节点 就不是完全二叉树
                    ||  (root.left==null&&root.right!=null) ) return false; //有右孩子却没有左孩子
            if(root.left!=null){
                queue.add(root.left);
            }
            if(root.right!=null){
                queue.add(root.right);
            }else{//因为存在左孩子不存在右孩子的情况已经在前面的循环里被舍弃跳出 所以此处右孩子不存在的情况必定包含 1.左孩子存在右孩子不存在 2.左右孩子都不存在
                isLeaf=true;
            }
        }
        return true;
    }

已知一棵完全二叉树,求其节点的个数

要求:时间复杂度低于O(N),N为这棵树的节点个数

思路:完全二叉树可以看作是一颗满二叉树删掉一些最后一层右边的节点得到的,也正是因为这样,所以完全二叉树必定由满的部分和完全的部分构成。

算法:

1.先让Node指向根节点

2.先拿到左子树的最大深度[LeftH]和右子树[RightH]的最大深度,鉴于完全二叉树的构成性质,左右子树的深度不会超过1;如果左子树深度>右子树深度,那么右子树必是满二叉树,此时右子树的节点数为:2^{RightH}-1.此时累计上节点数2^{RightH}-1+1,+1是Node.,我们让Node=Node.left;如果左子树深度==右子树深度,那么左子树必然是满二叉树,累计个数.所以我们发现,整体的复杂度为O(logN*logN),我们需要经历过logN个节点,在每个节点处需要logN的时间复杂度拿到左右子树的深度.

public class CompleteTreeNodeNumber {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }

    public static int nodeNum(Node head) {
        if(head==null)return 0;
        return nodeOfCBT(head,1,mostLeftLevel(head,1));
    }

    //level:当前层  height:树的最大深度
    public static int nodeOfCBT(Node node, int level, int height) {
        if(level==height)return 1;//一路走到最深层 此时没有被考虑的节点就是当前这个点一个而已
        if(mostLeftLevel(node.right,level+1)==height){//左子树和右子树的深度相同 此时左子树是满二叉树 直接累加左子树
            return (1<<(height-level))+nodeOfCBT(node.right,level+1,height);
        }else{//左子树比右子树深度大1 此时右子树是满二叉树 直接累加右子树
            return (1<<(height-level-1))+nodeOfCBT(node.left,level+1,height);
        }
    }

    public static int mostLeftLevel(Node node, int level) {//一棵完全二叉树的深度取决于其最左节点的深度
        while(node!=null){
            ++level;
            node=node.left;
        }
        return level-1;
    }

    public static void main(String[] args) {
        Node head = new Node(1);
        head.left = new Node(2);
        head.right = new Node(3);
        head.left.left = new Node(4);
        head.left.right = new Node(5);
        head.right.left = new Node(6);
        System.out.println(nodeNum(head));
    }

}

折纸问题[微软面试题QAQ]

【题目】 请把一段纸条竖着放在桌子上,然后从纸条的下边向 上方对折1次,压出折痕后展开。此时 折痕是凹下去的,即折痕 突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折 2 次,压出折痕后展开,此时有三条折痕,从上到下依次是下折 痕、下折痕和上折痕。 给定一个输入参数N,代表纸条都从下边向上方连续对折N次, 请从上到下打印所有折痕的方向。 例如:N=1时,打印: down N=2时,打印: down down up 

我们以折三次为例,标记出每条折痕的生成时间,于是有下图:

我们应该明白,对于第二次生成的折痕来说,由于第一次对折才导致第二次会生成两条,同理第四次会生成4条。所以我们应该关注1-2、2-3而不是从整体把握规律。我们发现:

1.第一条折痕必是向下

2. 1上面的2向下,1下面的2向上,同理,2上面的3向下,2下面的3向上,于是我们可以把这个过程当作一个二叉树扩展一层的过程。规律为:根节点向下,所有非叶节点的左孩子向下,右孩子向上。

public class PaperFolding {

    public static void printFlods(int level){
        printProcess(1,level,true);
    }

    public static void printProcess(int i,int level,boolean isDown){
        if(i>level)return;//超出了总层数 返回
        printProcess(i+1,level,true);
        System.out.println(isDown?"down":"up");
        printProcess(i+1,level,false);
    }

    public static void main(String[] args) {
        printFlods(4);
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值