剑指Offer 二叉树相关问题

1、重建二叉树

题目描述:

    输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

思路:
    根据前序遍历、中序遍历的特性,前序遍历的第一个节点,即对应根节点rootNode,该rootNode可以将中序遍历序列分为左右两个子树序列leftArray,rightArray;根据左右子树的节点值的多少,也可以将前序遍历的序列除去根节点分为左右两个子树序列。故剩下的问题即可以转化为已知子树的前序遍历序列以及中序遍历序列,来重建二叉树。

代码:

/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        TreeNode headNode = null;
        if (pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length)
            return headNode;

        // pre的第一个节点即为该子树的根节点,通过该节点将in分为左右子树
        int rootIndex = 0;
        for (int i = 0; i < pre.length; i++) {
            if (pre[0] == in[i]) {
                rootIndex = i;
                break;
            }
        }

        headNode = new TreeNode(pre[0]);
        if (rootIndex == 0) // 表示无左子树
            headNode.left = null;
        else {
            int[] left_pre = new int[rootIndex];
            int[] left_in  = new int[rootIndex];

            System.arraycopy(pre, 1, left_pre, 0, rootIndex);
            System.arraycopy(in, 0, left_in, 0, rootIndex);
            headNode.left = reConstructBinaryTree(left_pre, left_in);

        }

        // 构建右子树
        if (rootIndex == pre.length - 1)
            headNode.right = null;
        else {
            int[] right_pre = new int[pre.length - rootIndex - 1];
            int[] right_in = new int[pre.length - rootIndex - 1];

            System.arraycopy(pre, rootIndex + 1, right_pre, 0, right_pre.length);
            System.arraycopy(in, rootIndex + 1, right_in, 0, right_in.length);
            headNode.right = reConstructBinaryTree(right_pre, right_in);
        }
        return headNode;
    }
}


二、树的子结构问题
题目描述:
    输入两颗二叉树A,B,判断B是不是A的子结构。
问题分析:
    显然判断子树,只判断A树中是否包含B的根节点是不行的,比如A的一个节点a和B的根节点b的val值相同,但其左右子节点可能会不同;因此还需要继续对a和b的子树进行判断,直至确定b的所有子节点都包括在a的子树中,才表示B是A的字结构。
递归方法:
public class Solution {
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
        boolean result = false;

        if ((root1 == null) || (root2 == null))
            return result;

        // 找到相同值的情况
        if (root1.val == root2.val)
            result = isSubTree(root1, root2);
        if (!result)
            result = HasSubtree(root1.left, root2);
        if (!result)
            result = HasSubtree(root1.right, root2);

        return result;
    }

    private boolean isSubTree(TreeNode root1, TreeNode root2) {
        if (root2 == null)
            return true;
        if (root1 == null)
            return false;
        if (root1.val != root2.val)
            return false;

        return isSubTree(root1.left, root2.left) &&
                isSubTree(root1.right, root2.right);
    }
}

3、二叉树的镜像问题:

问题描述:

操作给定的二叉树,将其变换为源二叉树的镜像。 

输入描述:
二叉树的镜像定义:源二叉树 
    	    8
    	   /  \
    	  6   10
    	 / \  / \
    	5  7 9 11
    	镜像二叉树
    	    8
    	   /  \
    	  10   6
    	 / \  / \
    	11 9 7  5
问题解析:

其实很简单,只需要遍历二叉树,遍历到每一个节点时,交换左右子树的位置;


递归法:

public class Solution {
    public void Mirror(TreeNode root) {
        // 递归实现
        if (root == null) return;

        // 交换左右子树
        TreeNode tempNode = root.left;
        root.left = root.right;
        root.right = tempNode;

        // 递归广度优先遍历
        Mirror(root.left);
        Mirror(root.right);
    }
}

迭代法:

import java.util.LinkedList;
import java.util.Queue;
public class Solution {
    public void Mirror(TreeNode root) {

        Queue<TreeNode> queue = new LinkedList<TreeNode>();

        if (root != null) queue.offer(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();

            // 交换左右子树
            TreeNode temp = node.left;
            node.left = node.right;
            node.right = temp;

            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }

    }
}


 4、从上往下打印二叉树

题目描述

从上往下打印出二叉树的每个节点,同层节点从左至右打印。

问题分析:
简单的广度优先遍历,借助一个队列,将根节点入队列;弹出队列时,访问该根节点,并压入其左右结点到队列中,以此进行广度优先遍历。
(广度优先(BFS)使用队列,深度优先(DFS)使用栈)

代码:
public class Solution {
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        ArrayList<Integer> list = new ArrayList<>();
        if (root == null) return list;

        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            root = queue.poll();
            list.add(root.val);

            if (root.left != null)
                queue.offer(root.left);
            if (root.right != null)
                queue.offer(root.right);
        }
        return list;
    }
}

5、判断一个二叉搜索树的后序遍历序列
问题描述:
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
比如:{5,7,6,9,11,10,8}的结果为true;{7,4,6,5}结果为false;
问题分析:
二叉搜索树(BST(Binary Search Tree)):可以是一个空树,满足以下性质:
(1)若其左子树不为空,则左子树的所有节点的值 均小于它的根节点的值;
(2)若其右子树不为空,则右子树的所有节点的值 均大于它的根节点的值;
(3)其左右子树也为BST;
(4)没有键值相等的节点。
二叉搜索树的查找、插入、删除节点的相关代码:

满足二叉搜索树的性质,由于是后序遍历,很容易知道序列的最后一个节点为根节点root_data,则根据二叉搜索树的性质,可以将前面的序列分为左右子树两个部分,左侧键值全部小于根节点,为左子树;右侧键值全部大于根节点,为右子树;
可以通过遍历数组的方式,来获取到第一个大于root_data的值,即右子树的序列的第一个节点。
然后遍历之后的数组,是否满足所有值大于根节点的值root_data;
之后,再对左右子树重复之前的判断过程即可。

代码:

public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        if ((sequence == null) || (sequence.length == 0))
            return false;
        return VerifySquenceOfBST(sequence, 0, sequence.length - 1);
    }

    public boolean VerifySquenceOfBST(int[] sequence, int start, int end) {
        if (end - start < 1)
            return true;a

        // 获取子树的根节点
        int root_data = sequence[end];

        // 遍历获取中间节点
        int i = start;
        for (; i < end; i++) {
            if (sequence[i] > root_data)
                break;
        }

        // 判断中间节点后的节点值是否都比根节点的值要大,即是否满足二叉搜索树的定义
        for (int j = i; j < end; j++) {
            if (sequence[j] < root_data)
                return false;
        }

        // 判断左子树
        boolean left = true;
        if (i > start)
            left =  VerifySquenceOfBST(sequence, start, i - 1);

        // 判断右子树
        boolean right = true;
        if (i < end)
            right =  VerifySquenceOfBST(sequence, i , end - 1);
        return left & right;
    }
}

6、二叉搜索树转化为双向链表

问题描述:

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

问题分析:

二叉树有左右节点两个指针,而双向链表有前后节点两个指针,因此将二叉树转化成双向链表,调整指针的方向是可以实现的。


转化成一个排序的双向链表,根据BST树的形式,可以想到使用中序遍历二叉搜索树。

中序遍历即在中间访问根节点,这里根据递归的思想,假设访问根节点的时候,左子树已经是排序完成的链表,因此调整左子树的左后一个值lastNode,其right指针指向当前根节点mRoot,mRoot的left指针指向lastNode;这里就需要一个类能够记录lastNode的值。

同理完成了左子树与根节点的拼接,下面同理完成根节点与右子树的拼接即可。

代码:

public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        TreeNode lastNode = null;
        convertTree(pRootOfTree, new NodeHelp(lastNode));
        // 此时二叉树已经转化成为了双向链表
        while ((pRootOfTree != null) && (pRootOfTree.left != null)) {
            pRootOfTree = pRootOfTree.left;
        }
        return pRootOfTree;
    }

    // 辅助类,用以记录当前节点的前一个访问节点
    class NodeHelp {
        TreeNode lastNode;

        public NodeHelp(TreeNode lastNode) {
            this.lastNode = lastNode;
        }
    }

    // 使用lastNode将链表前一节点记录下来
    private void convertTree(TreeNode pRoot, NodeHelp nodeHelp) {
        if (pRoot == null)
            return;
        // 进行中序遍历
        if (pRoot.left != null)
            convertTree(pRoot.left, nodeHelp);

        // 访问根节点,转换成双向链表
        pRoot.left = nodeHelp.lastNode;
        if (nodeHelp.lastNode != null)
            nodeHelp.lastNode.right = pRoot;
        // 对lastNode重新赋值
        nodeHelp.lastNode = pRoot;
        // 访问右子树
        if (pRoot.right != null)
            convertTree(pRoot.right, nodeHelp);
    }

//    // ===== 测试函数 =========//
//    public static void main(String[] args) {
//     TreeNode node1 = new TreeNode(10);
//     TreeNode node2 = new TreeNode(6);
//     TreeNode node3 = new TreeNode(14);
//     TreeNode node4 = new TreeNode(4);
//     TreeNode node5 = new TreeNode(8);
//     TreeNode node6 = new TreeNode(12);
//     TreeNode node7 = new TreeNode(16);
//     
//     node1.left = node2;
//     node1.right = node3;
//     
//     node2.left = node4;
//     node2.right = node5;
//     
//     node3.left = node6;
//     node3.right = node7;
//     
//     Solution solution = new Solution();
//     solution.printNodes(solution.Convert(node1));
//    }
//    
//    // 输出结果
//    private void printNodes(TreeNode root) {
//     while (root!= null) {
//        System.out.println(root.val + "==>");
//        root = root.right;
//     }
//    }
}


7、二叉树中和为某一值的路径

问题描述:
输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
问题分析:
    显然这是一个深度优先遍历(DFS)的应用,在深度遍历的同时要注意将从根节点到叶子节点的路径保存下来。
    深度优先遍历通过一个Stack辅助栈很容易实现;当访问到叶子节点时,判断当前路径的和sum是否和target值相等,若相等,则保存路径;
访问完根节点后,回溯至上一个点(右节点或者其父节点的右节点),回溯上一节点,要注意将路径和sum进行更新。这里要注意,若是访问右节点,则无特别之处;若是回溯到父节点的右节点,由于父节点当前仍然保存到路径中,需要将父节点也移除。这里就需要添加一个变量来区分这两种情况。
代码:

import java.util.ArrayList;
import java.util.Stack;
class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}

public class Solution {
    // 辅助Node类,来记录该Node是否已经访问过
    class TreeNodeTag {
        TreeNode node;
        boolean isVisited = false;

        TreeNodeTag(TreeNode node) {
            this.node = node;
        }
    }

    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        // 结果List
        ArrayList<ArrayList<Integer>> result_list = new ArrayList<>();
        if (root == null)
            return result_list;
        // 暂存每一个访问节点的List
        ArrayList<Integer> router_list = new ArrayList<>();
        // 用以DFS深度优先遍历
        Stack<TreeNodeTag> stack = new Stack<>();
        stack.push(new TreeNodeTag(root));
        // 暂存每条路径的数值结果
        int cur_sum = 0;
        while (!stack.isEmpty()) {
            TreeNodeTag nodeTag = stack.peek();
            TreeNode node = nodeTag.node;
            // 如果该节点已经被访问过
            if (nodeTag.isVisited) {
                // 如果该节点已经访问过,则也应该将该节点从路径中去除
                router_list.remove(router_list.size() - 1);
                cur_sum -= node.val;
                stack.pop();
            } else {

                // 访问该节点
                cur_sum += node.val;
                router_list.add(node.val);
                nodeTag.isVisited = true;

                // 当为叶子节点时,进行判断是否满足和为target
                if ((node.left == null) && (node.right == null)) {
                    if (cur_sum == target)
                        result_list.add(new ArrayList<Integer>(router_list));
                    // 回溯
                    router_list.remove(router_list.size() - 1);
                    cur_sum -= node.val;

                    stack.pop();
                }

                // DFS
                if (node.right != null)  // 注意添加入stack的顺序,先push右节点
                    stack.push(new TreeNodeTag(node.right));
                if (node.left != null)
                    stack.push(new TreeNodeTag(node.left));
            }
        }
        return result_list;
    }

//    public static void main(String[] args) {
//     TreeNode node1= new TreeNode(10);
//     TreeNode node2= new TreeNode(5);
//     TreeNode node3= new TreeNode(12);
//     TreeNode node4= new TreeNode(4);
//     TreeNode node5= new TreeNode(7);
//     
//     node1.left = node2;
//     node1.right = node3;
//     
//     node2.left = node4;
//     node2.right = node5;
//     
//     ArrayList<ArrayList<Integer>> result_list = new ArrayList<ArrayList<Integer>>();
//     
//     result_list = new Solution().FindPath(node1, 22);
//     
//     for (ArrayList<Integer> list : result_list ) {
//        System.out.println(list);
//     }
//    }
}


拓展:DFS深度优先遍历:

使用一个Stack来实现节点的暂存,注意push的顺序,先要push右子节点,再push左子节点,才能保证正确的访问顺序。

代码:

public class Solution {
    public void DFS(TreeNode root) {
        if (root == null) return;

        Stack<TreeNode> stack = new Stack<TreeNode>();
        stack.push(root);

        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            System.out.println(node.val);

            if (node.right != null)
                stack.push(node.right);
            if (node.left != null)
                stack.push(node.left);
        }

    }
}

8、二叉树的深度
问题描述:
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
问题分析:
这一题也可以采用类似于上一题遍历路径的方法,但代码量较大,并不实用;
采用 递归的思想:
二叉树深度的本质含义即是其左子树的深度与右子树的深度之中的较大值加上1(自己本身的深度),因此递归起来很容易实现。

代码:

public class Solution {
    public int TreeDepth(TreeNode root) {
        if (root == null)
            return 0;
        int left_depth = TreeDepth(root.left);
        int right_depth = TreeDepth(root.right);

        return (left_depth > right_depth) ? (left_depth + 1) : (right_depth + 1);
    }
}

9、平衡二叉树:
问题描述:
输入一棵二叉树,判断该二叉树是否是平衡二叉树。

问题分析:
平衡二叉树(Balanced Binary Tree)(AVL树) 可以为空树,满足以下性质:
1)左子树和右子树都是AVL树
2)左子树和右子树的深度之差不超过1
AVL树节点的 平衡因子BF(Balance Factor)该节点的左子树深度与右子树深度之差;则对于平衡二叉树而言,每个节点的BF值只能为-1,0,1;当BF绝对值超过1,则必然不是AVL树。

依然采用第七题递归的思想,获取每个节点的深度值。采取书中的方法,即对分别对每个节点求取其左右子树深度,然后判断是否满足AVL性质方式,必然会造成除根节点外,其他节点都会在递归调用中被遍历很多次。
这里采用一个辅助类Info,来记录每一个节点的深度与是否满足AVL树的条件;每一次递归返回该节点的 深度信息depth与是否为AVL树isBalanced;
对于一个节点,分别求取其左右子节点的Info信息,获取其深度depth及isBalanced信息;
如果其左右子树有一个 isBalanced为false,即不满足AVL树,则整个树必然不是AVL,违反了性质1,直接将本节点的isBalanced置为false,返回给上一级父亲节点即可。
若左右子树都为AVL树,满足性质1,下面判断性质2;通过左右子树的Info类可以获知左右节点的深度,判断其BF绝对值是否小于1,若满足,则将自身深度信息与isBalanced(true)返回给父亲节点;否则直接将isBalanced置为false。

代码:
public class Solution {
    public boolean IsBalanced_Solution(TreeNode root) {
        return IsBalanced(root).isBalanced;
    }

    // 辅助类
    private class NodeInfo {
        int depth;               // 记录该节点的深度
        boolean isBalanced;      // 记录以该节点为根节点的子树是否为AVL树
        public NodeInfo (int depth, boolean isBalanced) {
            this.depth = depth;
            this.isBalanced = isBalanced;
        }
    }

    private NodeInfo IsBalanced(TreeNode node) {
        if (node == null)
            return new NodeInfo(0, true);

        // 获得左右子树的相关信息
        NodeInfo left_info = IsBalanced(node.left);
        NodeInfo right_info = IsBalanced(node.right);

        // 不符合AVL树性质的情况
        if ((!left_info.isBalanced) || (!right_info.isBalanced) ||
                (Math.abs(left_info.depth - right_info.depth) > 1))
            return new NodeInfo(0, false);
        return new NodeInfo((left_info.depth > right_info.depth) ?
                (left_info.depth + 1) : (right_info.depth + 1), true);

    }
}


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值