Android开发学算法-二叉树

420

二叉树的遍历方式:

众所周知,二叉树遍历可分为前序遍历、中序遍历、后续遍历以及层次遍历,前三个也称为深度优先遍历(dfs),使用递归的方式;最后一个也称为广度优先(bfs),使用队列的方式。

假如说有这么一棵二叉树树:

  • 前序遍历
    public void travel(TreeNode root) {
        if (root == null) return;
        System.out.print(root.val + " ");
        travel(root.left);
        travel(root.right);
    }

扩展:前序遍历还可以通过非递归的方式,也就是栈,因为栈跟递归都具有回溯的特性。代码如下:

 public void travelWithStack(TreeNode root) {
        TreeNode treeNode = root;
        Stack<TreeNode> stack = new Stack<>();
        while (treeNode != null || !stack.empty()) {
            while (treeNode != null) {
                System.out.print(treeNode.val + " ");
                stack.push(treeNode);
                treeNode = treeNode.left;
            }
            // 左子树结束,弹出当前节点,从而访问右节点
            TreeNode node = stack.pop();
            treeNode = node.right;
        }
    }

还有一种更容易理解的方式,第一步让根节点进入栈内,然后出栈,此时判断右节点、左节点是否为空,不为空的话添加到栈内,重复此操作,直到栈为空为止。(注意:之所以让右节点先进栈、左节点后进栈,是因为栈是后进先出的,这样就能先访问左节点了)

 public void travelWithStack(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.empty()) {
            TreeNode currentNode = stack.pop();
            System.out.println(currentNode.val);
            if (currentNode.right != null) {
                stack.push(currentNode.right);
            }
            if(currentNode.left != null){
                stack.push(currentNode.left);
            }
        }
    }

前序遍历可以看作指针在树上游走过程中,每到一个节点,将其打印。看leetcode617. 合并二叉树:

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
示例 1:
输入: 
	Tree 1                     Tree 2                  
          1                         2                             
         / \                       / \                            
        3   2                     1   3                        
       /                           \   \                      
      5                             4   7                  
输出: 
合并后的树:
	     3
	    / \
	   4   5
	  / \   \ 
	 5   4   7

思路:先合并根节点,然后再递归合并左右子树。前序遍历的应用

  public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
        if (t1 == null && t2 == null) return null;
        TreeNode node = new TreeNode((t1 == null ? 0 : t1.val) + (t2 == null ? 0 : t2.val));
        // 前序遍历
        node.left = mergeTrees(t1 == null ? null : t1.left, t2 == null ? null : t2.left);
        node.right = mergeTrees(t1 == null ? null : t1.right, t2 == null ? null : t2.right);
        return node;
    }

  • 中序遍历
    public void travel(TreeNode root) {
        if (root == null) return;
        travel(root.left);
        System.out.print(root.val + " ");
        travel(root.right);
    }

  • 后序遍历
    public void travel(TreeNode root) {
        if (root == null) return;
        travel(root.left);
        travel(root.right);
        System.out.print(root.val + " ");
    }

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

示例 :

          1
         / \
        2   3
       / \     
      4   5    
      
 返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]

思路:假如有一个递归方法,用来计算每一个节点的高度。对于任意一个节点来说,该节点的高度其实就是左孩子跟右孩子最大高度加1。

   private int getHeight(TreeNode node) {
        if (node == null) return 0;
        int l = getHeight(node.left);
        int r = getHeight(node.right);
        return Math.max(l, r) + 1;
    }

若此时,我传入一个根节点root,那么在求root的高度过程中,每个node的高度其实都被计算了。那么在这个过程中,来逐步更新最大直径即可。也就是两个两个节点之间的节点数-1。代码如下:

    int ans = 0; // 节点数目
    public int diameterOfBinaryTree(TreeNode root) {
        getHeight(root);
        return ans > 0 ? ans - 1 : ans;
    }
    
    /**
     * 递归函数
     * 用于求节点为node的高度
     */
    private int getHeight(TreeNode node) {
        if (node == null) return 0;
        int l = getHeight(node.left);
        int r = getHeight(node.right);
        ans = Math.max(ans, l + r + 1);
        return Math.max(l, r) + 1;
    }

  • 层次遍历
  public void travel(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (queue.size() > 0) {
            TreeNode currentNode = queue.poll();
            System.out.print(currentNode.val + " ");
            if (currentNode.left != null) {
                queue.offer(currentNode.left);
            }
            if (currentNode.right != null) {
                queue.offer(currentNode.right);
            }
        }
    }
    
   打印:       1 2 3 4 5 6 7

层次遍历使用的是队列来完成的,具体操作流程是:节点先进队列,接着节点出队列,然后判断该节点的左孩子跟右孩子是否为空,不为空的话继续添加到队列中。

  • 扩展一下,如果让每一层的节点换行打印的话,需要如何操作? 代码如下:
 public void travel(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (queue.size() > 0) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode currentNode = queue.poll();
                System.out.print(currentNode.val + " ");
                if (currentNode.left != null) {
                    queue.offer(currentNode.left);
                }
                if (currentNode.right != null) {
                    queue.offer(currentNode.right);
                }
            }
            System.out.println();
        }
    }
           1 
           2 3 
           4 5 6 7

leetcode114. 二叉树展开为链表

给定一个二叉树,原地将它展开为一个单链表。

例如,给定二叉树:

    1
   / \
  2   5
 / \   \
3   4   6

将其展开为:

1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6

解题思路如下:

  • 将左子树逐个遍历,比如上述的二叉树,考虑像将节点2的左子树移动到右边,变成2-3-4链表。
  • 再将右子树逐个展开成链表,然后将左子树已经处理好的链表接到右侧去。
  • 将左子树缓存下来之后,记得要将左子树设置为null

代码如下:


    public void flatten(TreeNode root) {
        travel(root);
    }

    /**
     * 递归函数
     * 将节点node左子树移动到右边
     */
    private TreeNode travel(TreeNode node) {
        if (node == null) return null;
        TreeNode l = travel(node.left);
        TreeNode r = travel(node.right);
        if(l != null ){
            node.right = l;
            node.left = null;
            // 要用当前的末端去链接 r
            // 即l的末端
            while ( l.right !=null){
                l = l.right;
            }
            l.right = r;
        }
        return node;
    }

leetcode236. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

236

示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。

解题思路如下:

  • 因为求最近的公共祖先,所以可以采用自底向上的方式,即后续遍历。找到的第一个公共祖先即为最近的。
  • 假如说,最近的公共祖先找到了。那么分两种情况:
  1. 节点p q必定分布在公共祖先的两侧;
  2. p、q其中一个为公共祖先,另一个在公共祖先的左侧或者右侧;

思路有了,代码就清晰了,如下:

    TreeNode resultNode;
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        checkIsContainOne(root, p, q);
        return resultNode;
    }
  
   /**
     * 判断节点node 是否包含 p 或者 q
     * 自底向上,后续遍历
     */
    private boolean checkIsContainOne(TreeNode node, TreeNode p, TreeNode q) {
        if (node == null) return false;
        boolean l = checkIsContainOne(node.left, p, q);
        boolean r = checkIsContainOne(node.right, p, q);
        if (r && l || (l && node.val == q.val || l && node.val == p.val) || (r && node.val == p.val || r && node.val == q.val)) 
        resultNode = node;
        // 当前节点node包含p或者q ,或者当前节点node的左孩子or右孩子包含p或者q,都向上返回
        return node.val == p.val || node.val == q.val || (l || r);
    }