【算法】牛客网算法初级班(二叉树结构)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ARPOSPF/article/details/81915836

二叉树结构


实现二叉树的先序、中序、后序遍历,包括递归方式和非递归方式

题目:

用递归和非递归方式,分别按照二叉树先序、中序和后序打印所有的节点。我们约定:先序遍历顺序为根、左、右;中序遍历顺序为左、根、右;后续遍历顺序为左、右、根。

思考:

用非递归方式实现二叉树的先序遍历,具体过程如下:

  1. 申请一个新的栈,记为stack。然后将头节点head压入stack中。
  2. 从stack中弹出栈顶节点,记为cur,然后打印cur节点的值,再将节点cur的右孩子(不为空的话)先压入stack中,最后将cur的左孩子(不为空的话)压入stack中.
  3. 不断重复步骤2,直到stack为空,全部过程结束。

用非递归方式实现二叉树的中序遍历,具体过程如下:

  1. 申请一个新的栈,记为stack。初始时,令变量cur=head。
  2. 先把cur节点压入栈中,对以cur节点为头的整棵子树来说,依次把左边界压入栈中,即不停地令cur=cur.left,然后重复步骤1.
  3. 不断重复步骤2,直到发现cur为空,此时从stack中弹出一个节点,记为node。打印node的值,并且让cur=node.right,然后继续重复步骤2.
  4. 当stack为空且cur为空时,整个过程停止。

用非递归方式实现二叉树的后序遍历稍显麻烦,有两种方式:

方法一:使用两个栈实现后序遍历的过程,具体如下:

  1. 申请一个栈,记为s1,然后将头节点head压入s1中。
  2. 从s1中弹出的节点记为cur,然后依次将cur的左孩子和右孩子压入s1中。
  3. 在整个过程中,每一个从s1中弹出的节点都放进s2中。
  4. 不断重复步骤2和步骤3,直到s1为空,过程停止。
  5. 从s2中依次弹出节点并打印,打印的顺序就是后序遍历的顺序。

方法二:使用一个栈实现后序遍历的过程,具体如下:

1.申请一个栈,记为stack,将头节点压入stack,同时设置两个变量head和cur。在这个流程中,head代表最近一次弹出并打印的节点,cur代表stack的栈顶节点,初始时head为头节点,cur为null。

2.每次令cur等于当前stack的栈顶节点,但是不从stack中弹出,此时分以下三种情况:

①如果cur的左孩子不为null,并且head不等于cur的左孩子,也不等于cur的右孩子,则把cur的左孩子压入stack中。head的意义是最近一次弹出并打印的节点,所以如果head等于cur的左孩子或右孩子,说明cur的左子树与右子树已经打印完毕,此时不应该再将cur的左孩子放入stack中,否则,说明左子树还没处理过,那么此时将cur的左孩子压入stack中。

②如果条件①不成立,并且cur的右孩子不为null,head不等于cur的右孩子,则把cur的右孩子压入stack中。含义是如果head等于cur的右孩子,说明cur的右子树已经打印完毕,此时不应该再将cur的右孩子放入stack中,否则,说明右子树还没处理过,此时将cur的右孩子压入stack中。

③如果条件①和条件②都不成立,说明cur的左子树和右子树已经打印完毕了,那么从stack中弹出cur并打印,然后令head=cur。

3.一直重复步骤2,直到stack为空,过程停止。

代码:

import java.util.Stack;

/**
 * 实现二叉树先序、中序、后序遍历,递归或非递归方式
 */
public class OrderRecur {
    public class Node {
        int value;
        public Node left;
        public Node right;

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

    /**
     * 递归方式先序遍历
     *
     * @param head
     */
    public void preOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        System.out.print(head.value + " ");//首先打印根节点
        preOrderRecur(head.left);
        preOrderRecur(head.right);
    }

    /**
     * 递归方式中序遍历
     *
     * @param head
     */
    public void inOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        inOrderRecur(head.left);
        System.out.print(head.value + " ");//左子树遍历完之后打印根节点
        inOrderRecur(head.right);
    }

    /**
     * 递归方式后序遍历
     *
     * @param head
     */
    public void posOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        posOrderRecur(head.left);
        posOrderRecur(head.right);
        System.out.print(head.value + " ");//最后打印根节点
    }

    /**
     * 非递归方式实现先序遍历
     */
    public static void preOrderUnRecur(Node head) {
        System.out.print("pre-order: ");
        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();
    }

    /**
     * 使用非递归方式实现中序遍历
     *
     * @param head
     */
    public static void inOrderUnRecur(Node head) {
        System.out.print("in-order: ");
        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.print(head.value + " ");
                    head = head.right;
                }
            }
        }
        System.out.println();
    }

    /**
     * 使用两个栈实现后序遍历
     *
     * @param head
     */
    public static void posOrderUnRecur1(Node head) {
        System.out.println("pos-order: ");
        if (head != null) {
            Stack<Node> s1 = new Stack<>();
            Stack<Node> s2 = new Stack<>();
            s1.push(head);
            while (!s1.isEmpty()) {
                head = s1.pop();
                s2.push(head);
                if (head.left != null) {
                    s1.push(head.left);
                }
                if (head.right != null) {
                    s1.push(head.right);
                }
            }
            while (!s2.isEmpty()) {
                System.out.print(s2.pop().value + " ");
            }
        }
        System.out.println();
    }

    /**
     * 只用一个栈实现后序遍历
     *
     * @param head
     */
    public static void posOrderUnRecur2(Node head) {
        System.out.print("pos-order: ");
        if (head != null) {
            Stack<Node> stack = new Stack<>();
            stack.push(head);
            Node curr = null;
            while (!stack.isEmpty()) {
                curr = stack.peek();
                if (curr.left != null && head != curr.left && head != curr.right) {
                    stack.push(curr.left);
                } else if (curr.right != null && head != curr.right) {
                    stack.push(curr.right);
                } else {
                    System.out.print(stack.pop().value + " ");
                    head = curr;
                }
            }
        }
        System.out.println();
    }
}

如何较为直观地打印二叉树?

题目:

二叉树可以用常规的三种遍历结果来描述其结构,但是不够直观,尤其是二叉树中有重复值的时候,仅通过三种遍历的结果来构造二叉树的真实结构更是难上加难,有时则根本不可能。给定一棵二叉树的头节点head,已知二叉树节点值的类型为32位整型,请实现一个打印二叉树的函数,可以直观地展示树的形状,也便于画出真实的结构。

思考:

  1. 设计打印的样式。首先,二叉树大概的样子是把打印结果顺时针旋转90°;接下来,怎么清晰地确定任何一个节点的父结点呢?如果一个节点打印结果的前缀与后缀都有‘H’,说明这个节点是头节点,当然就不存在父结点。如果一个节点打印结果的前缀与后缀都有‘v’,表示父结点在该节点所在列的前一列,在该节点所在行的下方,并且是离该节点最近的节点。如果一个节点打印结果的前缀与后缀都有‘^’,表示父节点在该节点所在列的前一列,在该节点所在行的上方,并且是离该节点最近的节点。
  2. 规定节点打印时占用的统一长度。否则,在打印一个长短值交替的二叉树时必然会出现格式不对齐的问题,进而产生歧义。在Java中,整型值占用长度最长的值为Integer.MIN_VALUE(即-2147483648),占用的长度为11,加上前缀和后缀之后占用的长度为13.为了在打印时更好地区分,再把前面加上两个空格,后面加上两个空格,共占用长度为17。也就是说,长度为17的空间必然可以放下任何一个32位整数,同时样式还不错。所以,打印每一个节点时,必须让每一个节点在打印时占用长度都为17,如果不足,前后都用空格补齐。
  3. 具体实现。打印的整体过程结合了二叉树先右子树、再根节点、最后左子树的递归遍历过程。如果递归到一个节点,首先遍历它的右子树。右子树遍历结束后,回到这个节点。如果这个节点所在层为l,那么先打印l×17个空格(不换行),然后开始制作该节点的打印内容,这个内容当然包括节点的值,以及确定的前后缀字符。如果该节点是其父结点的右孩子,前后缀为‘v’,如果是其父结点的左孩子,前后缀为'^',如果是头节点,前后缀为‘H’。最后在前后分别贴上数量几乎一致的空格,占用长度为17的打印内容就制作完成了。打印这个内容后换行,最后进行左子树的遍历过程。

代码:

/**
 * 直观地打印二叉树
 */
public class PrintTree {
    public class Node {
        public int value;
        public Node left;
        public Node right;

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

    public void printTree(Node head) {
        System.out.println("Binary Tree:");
        printInOrder(head, 0, "H", 17);
        System.out.println();
    }

    private void printInOrder(Node head, int height, String to, int len) {
        if (head == null) {
            return;
        }
        printInOrder(head.right, height + 1, "v", len);
        String val = to + head.value + to;
        int lenM = val.length();
        int lenL = (len - lenM) / 2;
        int lenR = len - lenM - lenL;
        val = getSpace(lenL) + val + getSpace(lenR);
        System.out.println(getSpace(height * len) + val);
        printInOrder(head.left, height + 1, "^", len);
    }

    public String getSpace(int num) {
        String space = " ";
        StringBuffer buf = new StringBuffer("");
        for (int i = 0; i < num; i++) {
            buf.append(space);
        }
        return buf.toString();
    }
}

找到二叉树中的最大搜索二叉树

题目:

给定一棵二叉树的头节点head,已知其中所有节点的值都不一样,找到含有节点最多的搜索二叉子树,并返回这棵子树的头节点。

要求:

如果节点数为N,要求时间复杂度为O(N),额外空间复杂度为O(h),h为二叉树的高度。

思考:

以节点node为头的树中,最大的搜索二叉子树只可能来自以下两种情况:

情况一:如果来自node左子树上的最大搜索二叉子树是以node.left为头的;来自node右子树上的最大搜索二叉子树是以node.right为头的;node左子树上的最大搜索二叉子树的最大值小于node.value;node右子树上的最大搜索二叉子树的最小值小于node.value,那么以节点node为头的整棵树都是搜索二叉树。

情况二:如果不满足第一种情况,说明以节点node为头的树整体不能连成搜索二叉树。这种情况下,以node为头的树上的最大搜索二叉子树是来自node的左子树上的最大搜索二叉子树和来自node的右子树上的最大搜索二叉子树之间,节点数较多的那么。

通过以上分析,求解的过程如下:

  1. 整体过程是二叉树的后序遍历。
  2. 遍历到当前节点记为cur时,先遍历cur的左子树收集4个信息,分别是左子树上最大搜索二叉子树的头节点(IBST)、节点数(ISize)、最小值(IMin)和最大值(IMax)。再遍历cur的右子树收集4个信息,分别是右子树上最大搜索二叉子树的头节点(rBST)、节点数(rSize)、最小值(rMin)和最大值(rMax)。
  3. 根据步骤2所收集的信息,判断是否满足第一种情况,如果满足第一种情况,就返回cur节点,如果满足第二种情况,就返回IBST和rBST中较大的一个。
  4. 可以使用全局变量的方式实现步骤2中收集节点数、最小值和最大值的问题。

代码:

/**
 * 找到二叉树中的最大搜索二叉子树
 */
public class BiggestSubBST {
    public class Node {
        int value;
        public Node left;
        public Node right;

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

    public Node biggestSubBST(Node head) {
        int[] record = new int[3];//存放节点数、最小值、最大值
        return posOrder(head, record);
    }

    public Node posOrder(Node head, int[] record) {
        if (head == null) {
            record[0] = 0;
            record[1] = Integer.MAX_VALUE;
            record[2] = Integer.MIN_VALUE;
            return null;
        }
        int value = head.value;
        Node left = head.left;
        Node right = head.right;
        Node lBST = posOrder(left, record);
        int lSize = record[0];
        int lMin = record[1];
        int lMax = record[2];
        Node rBST = posOrder(right, record);
        int rSize = record[0];
        int rMin = record[1];
        int rMax = record[2];
        record[1] = Math.min(lMin, value);
        record[2] = Math.max(rMax, value);
        //是否满足第一种情况,如果满足,则返回头节点
        if (left == lBST && right == rBST && lMax < value && value < rMin) {
            record[0] = lSize + rSize + 1;
            return head;
        }
        //否则,比较左子树和右子树的节点数,返回节点数最多的最大搜索二叉子树
        record[0] = Math.max(lSize, rSize);
        return lSize > rSize ? lBST : rBST;
    }
}

判断二叉树是否为平衡二叉树

题目:

平衡二叉树的性质为:要么是一棵空树,要么任何一个节点的左右子树高度差的绝对值不超过1.给定一棵二叉树的头节点head,判断这棵二叉树是否为平衡二叉树。

要求:

如果二叉树的节点数为N,要求时间复杂度为O(N).

思考:

解法的整体过程为二叉树的后序遍历,对任何一个节点node来说,先遍历node的左子树,遍历过程中收集两个信息,node的左子树是否为平衡二叉树,node的左子树最深到哪一层记为lH。如果发现node的左子树不是平衡二叉树,无须进行任何后序遍历,此过程返回什么已不重要,因为已经发现整棵树不是平衡二叉树,退出遍历过程;如果node的左子树是平衡二叉树,再遍历node的右子树,遍历过程中再收集两个信息,node的右子树是否为平衡二叉树,node的右子树最深到哪一层记为rH。如果发现node的右子树不是平衡二叉树,无须进行任何后续过程,返回什么也不重要,因为已经发现整棵树不是平衡二叉树,退出遍历过程;如果node的右子树也是平衡二叉树,就看lH和rH差的绝对值是否大于1,如果大于1,说明已经发现整棵树不是平衡二叉树,如果不大于1,则返回lH和rH较大的一个。

代码:

/**
 * 判断二叉树是否为平衡二叉树
 */
public class IsBalance {
    public class Node {
        int value;
        Node left;
        Node right;

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

    public boolean isBalance(Node head) {
        boolean[] res = new boolean[1];
        res[0] = true;
        getHeight(head, 1, res);
        return res[0];
    }

    /**
     * 在递归函数中,一旦发现不符合平衡二叉树的性质,递归过程迅速推出,此时返回什么根本不重要。
     * boolean[] res的长度为1,其功能相当于一个全局boolean变量
     * 每个节点最多遍历一次
     * @param head
     * @param level
     * @param res
     * @return
     */
    public static int getHeight(Node head, int level, boolean[] res) {
        if (head == null) {
            return level;
        }
        int lH = getHeight(head.left, level + 1, res);
        if (!res[0]) {
            return level;
        }
        int rH = getHeight(head.right, level + 1, res);
        if (!res[0]) {
            return level;
        }
        if (Math.abs(lH - rH) > 1) {
            res[0] = false;
        }
        return Math.max(lH, rH);
    }
}

根据后序数组重建搜索二叉树

题目:

给定一个整型数组arr,已知其中没有重复值,判断arr是否可能是节点值类型为整型的搜索二叉树后序遍历的结果。

进阶:

如果整型数组arr中没有重复值,且已知是一棵搜索二叉树的后序遍历结果,通过数组arr重构二叉树。

思考:

原问题的解法:二叉树的后序遍历为先左、再右、最后根的顺序,所以,如果一个数组是二叉树后序遍历的结果,那么头节点的值一定会是数组的最后一个元素。搜索二叉树的性质,所以比后续数组最后一个元素值小的数组会在数组的左边,比数组最后一个元素大的数组会在数组的右边。比如arr=[2,1,3,6,5,7,4],比4小的部分[2,1,3],比4大的部分为[6,5,7]。如果不满足这种情况,说明这个数组一定不可能是搜索二叉树后序遍历的结果。接下来数组划分成左边数组和右边数组,相等于二叉树分出了左子树和右子树,只要递归地进行如上判断即可。

进阶问题的解法原理与原问题同理,一棵树的后序数组中最后一个值为二叉树头节点的值,数组左部分都比头节点的值小,用来生成头节点的左子树,剩下的部分用来生成右子树。

代码:

/**
 * 根据后序遍历重组搜索二叉树
 */
public class IsPost {
    /**
     * 判断arr是否是节点值类型为整型的搜索二叉树后序遍历的结果
     */
    public boolean isPostArray(int[] arr) {
        if (arr == null || arr.length == 0) {
            return false;
        }
        return isPost(arr, 0, arr.length - 1);
    }

    public static boolean isPost(int[] arr, int start, int end) {
        if (start == end) {
            return true;
        }
        int less = -1;
        int more = end;
        for (int i = start; i < end; i++) {
            if (arr[end] > arr[i]) {
                less = i;
            } else {
                more = more == end ? i : more;
            }
        }
        if (less == -1 || more == end) {
            return isPost(arr, start, end - 1);
        }
        if (less != more - 1) {
            return false;
        }
        return isPost(arr, start, less) && isPost(arr, more, end - 1);
    }

    /**
     * 一棵树的后序数组中最后一个值为二叉树头节点的值,
     * 数组左部分都比头节点的值小,用来生成头节点的左子树,剩下的部分用来生成右子树。
     */
    public static class Node {
        int value;
        Node left;
        Node right;

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

    public static Node posArrayToBST(int[] posArr) {
        if (posArr == null) {
            return null;
        }
        return posToBST(posArr, 0, posArr.length - 1);
    }

    public static Node posToBST(int[] posArr, int start, int end) {
        if (start > end) {
            return null;
        }
        Node head = new Node(posArr[end]);
        int less = -1;
        int more = end;
        for (int i = start; i < end; i++) {
            if (posArr[end] > posArr[i]) {
                less = i;
            } else {
                more = end == more ? i : more;
            }
        }
        head.left = posToBST(posArr, start, less);
        head.right = posToBST(posArr, more, end - 1);
        return head;
    }
}

判断一棵二叉树是否为搜索二叉树和完全二叉树

题目:

给定一个二叉树的头节点head,已知其中没有重复值的节点,实现两个函数分别判断这棵二叉树是否是搜索二叉树和完全二叉树。

思考:

判断一棵二叉树是否为搜索二叉树,只要改写一个二叉树的中序遍历,在遍历的过程中看节点值是否都是递增的即可。这里采用时间复杂度为O(N),额外空间复杂度为O(1)的Morris中序遍历算法。Morris遍历分调整二叉树结果和恢复二叉树结构两个节点,所以,当发现节点值降序时,不能直接返回false,这么做可能会跳过恢复阶段,从而破坏二叉树的结构。

判断一棵二叉树是否是完全二叉树,可以依据以下标准:

  1. 按层遍历二叉树,每层的左边向右依次遍历所有的节点
  2. 如果当前节点有右孩子,但没有左孩子,直接返回false。
  3. 如果当前节点并不是左右孩子全有,那么之后的节点必须都为叶结点,否则返回false
  4. 遍历过程中如果不反悔false,遍历结束后返回true。

代码:

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

/**
 * 判断一棵二叉树是否为搜索二叉树和完全二叉树
 */
public class IsBSTandIsCBT {
    public class Node {
        int value;
        Node left;
        Node right;

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

    /**
     * 判断一棵二叉树是否为搜索二叉树
     *
     * @param head
     * @return
     */
    public boolean isBST(Node head) {
        if (head == null) {
            return true;
        }
        boolean res = true;
        Node pre = null;
        Node cur1 = head;
        Node cur2 = null;
        while (cur1 != null) {
            cur2 = cur1.left;
            if (cur2 != null) {
                while (cur2.right != null && cur2.right != cur1) {
                    cur2 = cur2.right;
                }
                if (cur2.right == null) {
                    cur2.right = cur1;
                    cur1 = cur1.left;
                    continue;
                } else {
                    cur2.right = null;
                }
            }
            if (pre != null && pre.value > cur1.value) {
                res = false;
            }
            pre = cur1;
            cur1 = cur1.right;
        }
        return res;
    }

    /**
     * 判断一棵二叉树是否是完全二叉树
     * @param head
     * @return
     */
    public boolean isCBT(Node head) {
        if (head == null) {
            return true;
        }
        Queue<Node> queue = new LinkedList<>();
        boolean leaf = false;
        Node l = null;
        Node r = null;
        queue.offer(head);
        while (!queue.isEmpty()) {
            head = queue.poll();
            l = head.left;
            r = head.right;
            if ((leaf && (l != null || r != null)) || (l == null && r != null)) {
                return false;
            }
            if (l != null) {
                queue.offer(l);
            }
            if (r != null) {
                queue.offer(r);
            } else {
                leaf = true;
            }
        }
        return true;
    }
}

通过有序数组生成平衡搜索二叉树

题目:

给定一个有序数组sortArr,已知其中没有重复值,用这个有序数组生成一棵平衡搜索二叉树,并且该搜索二叉树中序遍历的结果与sortArr一致。

思考:

用有序数组中最中间的数生成搜索二叉树的头节点,然后用这个数左边的数生成左子树,用右边的数生成右子树即可。

代码:

/**
 * 通过有序数组生成平衡搜索二叉树
 */
public class GenerateTree {
    public class Node{
        int value;
        Node left;
        Node right;
        public Node(int data){
            this.value=data;
        }
    }
    public Node generateTree(int[] sortArr){
        if (sortArr==null){
            return null;
        }
        return generate(sortArr,0,sortArr.length-1);
    }
    public Node generate(int[] sortArr,int start,int end){
        if (start>end){
            return null;
        }
        int mid = (start+end)/2;
        Node head = new Node(sortArr[mid]);
        head.left = generate(sortArr,start,mid-1);
        head.right = generate(sortArr,mid+1,end);
        return head;
    }
}

二叉树节点间的最大距离问题

题目:

从二叉树的节点A出发,可以向上或者向下走,但沿途的节点只能经过一次,当到达节点B时,路径上的节点数叫作A到B的距离。给定一棵二叉树的头结点head,求整棵树上节点间的最大距离。

要求:

如果二叉树的节点数为N,时间复杂度要求为O(N)。

思路:

一个以head为头的树上,最大距离只可能来自以下三种情况:

  • head的左子树上的最大距离。
  • head的右子树上的最大距离。
  • head左子树上离head.left最远的距离+1(head)+head右子树上离head.right最远的距离。

三个值中最大的那个就是整棵树中最远的距离。

根据上面的分析,算法过程如下:

  1. 整个过程为后序遍历,在二叉树的每个子树上执行步骤2.
  2. 假设子树头为head,处理head左子树,得到两个信息:左子树上的最大距离记为lMax,左子树上距离head左孩子的最远距离记为maxfromLeft。同理,处理head右孩子得到右子树上的最大距离记为rMax和距离head右孩子的最远距离记为maxfromRight。那么maxfromLeft+1+maxfromRight就是跨head节点情况下的最大距离,再与lMax和rMax比较,把三者中的最值作为head树上的最大距离返回,maxfromLeft+1就是head左子树上离head最远的点到head的距离,maxfromRight+1就是head右子树上离head最远的点到head的距离,选两者中最大的一个作为head树上距离head最远的距离返回。如何返回两个值?一个正常返回,另一个用全局变量表示。

代码:

/**
 * 二叉树节点间的最大距离问题
 */
public class MaxDistance {
    public class Node {
        int value;
        Node left;
        Node right;

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

    public int maxDistance(Node head) {
        int[] record = new int[1];
        return posOrder(head, record);
    }

    public int posOrder(Node head, int[] record) {
        if (head == null) {
            record[0] = 0;
            return 0;
        }
        int lMax = posOrder(head.left, record);
        int maxfromLeft = record[0];
        int rMax = posOrder(head.right, record);
        int maxfromRight = record[0];
        int curNodeMax = maxfromLeft + maxfromRight + 1;
        record[0] = Math.max(maxfromLeft, maxfromRight) + 1;
        return Math.max(Math.max(lMax, rMax), curNodeMax);
    }
}

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

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

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的后继节点。

思考:

先考虑一种简单的解法,时间复杂度和空间复杂度比较高。既然新类型的二叉树节点有指向父结点的指针,那么一直往上移动,自然可以找到头节点。找到头节点之后,在进行二叉树的中序遍历,生成中序遍历序列,然后在这个序列中找到node节点的下一个节点返回即可。如果二叉树的节点数为N,这种方法要把二叉树的所有节点至少遍历一遍,生成中序遍历的序列还需要大小为N的空间,所以这种方法的时间复杂度和空间复杂度都是O(N)。

最后解法不必遍历所有节点,如果node节点和node后继节点之间的实际距离为L,最优解法只用走过L个节点,时间复杂度为O(L),额外空间复杂度为O(1)。接下来详细说明最优解法是如何找到node的后继节点的:

  1. 如果node有右子树,那么后继节点就是右子树上最左边的节点。
  2. 如果node没有右子树,那么先看node是不是node父结点的左孩子,如果是左孩子,那么此时node的父结点就是node的后继节点;如果是右孩子,就向上寻找node的后继节点,假设向上移动到的节点记为s,s的父结点记为p,如果发现s是p的左孩子,那么结点p就是node节点的后继节点,否则就一直向上移动。
  3. 如果在情况2中一直向上寻找,都移动到空节点时还没有发现node的后继节点,说明node根本不存在后继节点。

情况1和情况2遍历的节点就是node到node后继节点这条路径上的点;情况3遍历的节点数也不会超过二叉树的高度。

代码:

/**
 * 在二叉树中找到一个节点的后继节点
 */
public class GetNextNode {
    public class Node {
        public int value;
        public Node left;
        public Node right;
        public Node parent;

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

    public 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 Node getLeftMost(Node node) {
        if (node == null) {
            return node;
        }
        while (node.left != null) {
            node = node.left;
        }
        return node;
    }
}

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

题目:

二叉树被记录成文件的过程叫做二叉树的序列化,通过文件内容重建原来的二叉树的过程叫做二叉树的反序列化。给定一棵二叉树的头节点head,并已知二叉树节点值类型为32位整型,请设计一种二叉树的序列化和反序列化的方案,并用代码实现。

思考:

方法一:通过先序遍历实现序列化和反序列化。

  • 先序遍历实现序列化
  1. 首先假设序列化的结果字符串为str,初始时str=""。
  2. 先序遍历二叉树,如果遇到null节点,就在str的末尾加上“#_”,“#”表示这个节点为空,节点值不存在,“_”表示一个值的结束;如果遇到不为空的节点,假设节点值为3,就在str的末尾加上“3_”。
  • 通过先序遍历序列化的结果反序列化,重构二叉树。
  1. 把结果字符串str编程字符串类型的数组,其中“_”作为字符串的分割符,记为values,数组代表一棵二叉树先序遍历的节点顺序。
  2. 用values按照先序遍历的顺序建立整棵树。

方法二:通过层遍历实现序列化和反序列化

  • 层遍历下的序列化过程
  1. 首先假设序列化的结果字符串为str,初始时str=“”.
  2. 然后实现二叉树的按层遍历,具体方式是利用队列结果,这也是图的宽度优先搜索的常见方式。
  • 层遍历的反序列化过程就是遇到“#”就生成null节点,同时不把null节点放在队列里面即可。

代码:

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

/**
 * 二叉树的序列化和反序列化
 */
public class SerialAndReserial {
    public class Node {
        int value;
        Node left;
        Node right;

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

    /**
     * 使用先序遍历进行序列化
     *
     * @param head
     * @return
     */
    public String serialByPre(Node head) {
        if (head == null) {
            return "#_";
        }
        String res = head.value + "_";
        res += serialByPre(head.left);
        res += serialByPre(head.right);
        return res;
    }

    /**
     * 基于先序遍历结果的反序列化
     *
     * @param preStr
     * @return
     */
    public Node reconByPreString(String preStr) {
        String[] values = preStr.split("_");
        Queue<String> queue = new LinkedList<>();
        for (int i = 0; i != values.length; i++) {
            queue.offer(values[i]);
        }
        return reconPreOrder(queue);
    }

    /**
     * 重构二叉树
     *
     * @param queue
     * @return
     */
    private 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;
    }

    /**
     * 使用层次遍历进行序列化
     */
    public String serialByLevel(Node head) {
        if (head == null) {
            return "#_";
        }
        String res = head.value + "_";
        Queue<Node> queue = new LinkedList<>();
        queue.offer(head);
        while (!queue.isEmpty()) {
            head = queue.poll();
            if (head.left != null) {
                res += head.left.value + "_";
                queue.offer(head.left);
            } else {
                res += "#_";
            }
            if (head.right != null) {
                res += head.right.value + "_";
                queue.offer(head.right);
            } else {
                res += "#_";
            }
        }
        return res;
    }

    /**
     * 基于层次遍历的结果反序列化
     */
    public Node reconByLevelString(String levelStr) {
        String[] values = levelStr.split("_");
        int index = 0;
        Node head = generateNodeByString(values[index++]);
        Queue<Node> queue = new LinkedList<>();
        if (head != null) {
            queue.offer(head);
        }
        Node node = null;
        while (!queue.isEmpty()) {
            node = queue.poll();
            node.left = generateNodeByString(values[index++]);
            node.right = generateNodeByString(values[index++]);
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
        return head;
    }

    private Node generateNodeByString(String val) {
        if (val.equals("#")) {
            return null;
        }
        return new Node(Integer.valueOf(val));
    }
}

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

题目:

给定一棵完全二叉树的头节点head,返回这棵树的节点个数。

要求:

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

思考:

如果完全二叉树的层数为H,具体过程为:

  1. 如果head=null,说明为空树,直接返回0;
  2. 如果不是空树,就求树的高度,求法是找到树的最左节点看能到哪一层,层数记为H。
  3. 主要逻辑是一个递归过程记为bs(node,L,H),node表示当前节点,L表示node所在的层数,H表示整棵树的层数是始终不变的。bs(node,L,H)的返回值表示以node为头的完全二叉树的节点数是多少。初始时node为头节点head,L为1,因为head在第一层,一共有H层始终不变。
  • 找到node右子树的最左节点,如果它能够达到最后一层,说明node的整棵左子树都是满二叉树,并且层数为H-1,一棵层数为H-1的满二叉树,其节点个数为2^{H-1}-1个。如果加上node节点自己,那么节点数为2^{H-1}个。此时如果再知道node右子树的节点数,就可以求出以node为头的完全二叉树上有多少个节点了。那么node右子树的节点数到底是多少呢?就是bs(node.right,L+1,H)的结果,递归去求即可。最后整体返回2^{H-1}+bs(node.right,L+1,H)。
  • 找到node右子树的最左节点,如果它没有达到最后一层,说明node的整棵右子树都是满二叉树,并且层数为H-L-1层。一棵层数为H-L-1层的满二叉树,其节点数为2^{H-L-1}-1个。如果加上node节点子集,那么节点数为2^{H-L-1}个。此时如果再知道node左子树的节点数,就可以求出以node为头的完全二叉树有多少个节点了。那么node左子树的节点数到底是所少呢?就是bs(node.left,L+1,H)的结果,递归去求即可。最后整体返回2^{H-L-1}+bs(node.left,L+1,H)。

代码:

/**
 * 统计完全二叉树的节点数
 */
public class NodeNum {
    public class Node {
        int value;
        Node left;
        Node right;

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

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

    public int bs(Node node, int L, int H) {
        if (L == H) {
            return 1;
        }
        if (mostLeftLevel(node.right, L + 1) == H) {
            return (1 << (H - L) + bs(node.right, L + 1, H));
        } else {
            return (1 << (H - L - 1)) + bs(node.left, L + 1, H);
        }
    }

    public int mostLeftLevel(Node node, int level) {
        while (node != null) {
            level++;
            node = node.left;
        }
        return level - 1;
    }
}

折纸问题

题目:

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

例如:

N=1时,打印:down

N=2时,打印:down down up

思考:

对折第一次的折痕:                                                                               下

对折第二次的折痕:                                              上                                                           下

对折第三次的折痕:                               上                                下                    上                                         下

对折第四次的折痕:                     上                 下          上                  下    上               下                  上                     下

从图中关系可以总结出:

  • 产生第i+1次折痕的过程,就是在对折i次产生的每一条折痕的左右两侧,依次插入上折痕和下折痕的过程。
  • 所有的折痕的结构是一棵满二叉树,在这棵满二叉树中,头节点为下折痕,每一棵左子树的头节点为上折痕,每一棵右子树的头节点为下折痕。
  • 从上到下打印所有折痕的方向,就是二叉树的先右、再中,最后左的中序遍历。

代码:

/**
 * 折纸问题
 */
public class PrintAllFolds {
    public static void printAllFolds(int N) {
        printProcess(1, N, true);
    }

    /**
     * 连续对折n次之后一定产生2^(n-1)条折痕
     * 时间复杂度为O(n^2),额外空间复杂度为O(1)
     *
     * @param i
     * @param N
     * @param down
     */
    private static void printProcess(int i, int N, boolean down) {
        if (i > N) {
            return;
        }
        printProcess(i + 1, N, true);
        System.out.println(down ? "down" : "up");
        printProcess(i + 1, N, false);
    }
}

猜你喜欢

转载自blog.csdn.net/ARPOSPF/article/details/81915836