面试之算法:二叉树遍历、左/右叶子节点和、构建二叉树、最大深度、是否平衡、将有序数组转换为二叉树、二叉树求和路径、右视图、序列化、反序列化(Java)

概述

二叉树是一个树形数据结构,每个节点最多可以有两个子节点,称为左子节点和右子节点。

二叉树的定义:

private static class TreeNode {
    private TreeNode left;
    private TreeNode right;
    private final int val;

    TreeNode(int x) {
        this.val = x;
    }
}

几种特殊的树:

  • 满二叉树:Full Tree,二叉树每个节点有 0 或 2 个子节点
  • 完美二叉树:Perfect Binary,二叉树每个节点有两个子节点,并且所有的叶子节点的深度是一样的
  • 完全二叉树:二叉树中除最后一层外其他各层的节点数均达到最大值,最后一层的节点都连续集中在最左边
  • 二叉查找树:BST,一种特殊的二叉树。其任何节点的值都大于等于左子树中的值,小于等于右子树中的值;没有键值相等的节点

二叉查找树是由n个节点随机构成,极端情况下,会退化成一个有n个节点的线性链表。此时,查询效率极低。为提高查找效率,二叉树得尽可能平衡。

平衡二叉树:AVL,带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右子树树高不超过1,和红黑树相比,它是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1)。不管执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的。AVL树适合用于插入删除次数比较少,但查找多的情况。

由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当然,如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树。

红黑树:一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是red或black。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍。它是一种弱平衡二叉树(由于是弱平衡,相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数变少,所以对于搜索、插入、删除操作多的情况下,就用红黑树。

算法面试中,关于二叉树的考点:遍历,查找,

遍历

二叉树的遍历方式有很多,包括:层序(层次)遍历,前序(先序)遍历,中序遍历,后序遍历。后面三种遍历根据是否使用递归,又分为递归遍历和非递归遍历。

二叉查找树,按照中序遍历的方式可以从小到大的顺序排序输出。

层序遍历

即广度优先搜索(BFS),可考虑利用队列来实现。BFS,核心就在于使用队列Queue来做一个while循环,在循环内除了对当前节点的处理之外,还需要将当前节点的孩子节点放入队列的尾部。


后序遍历+迭代

public static List<Integer> postOrderTraversal(TreeNode root) {
    List<Integer> list = new ArrayList<>();
    Stack<TreeNode> nodeStack = new Stack<>();
    TreeNode nodeTemp = root;
    TreeNode preNode = null;
    while (nodeTemp != null || !nodeStack.isEmpty()) {
        while (nodeTemp != null) {
            nodeStack.push(nodeTemp);
            nodeTemp = nodeTemp.left;
        }
        nodeTemp = nodeStack.peek();
        if (nodeTemp.right == null || nodeTemp.right == preNode) {
            nodeTemp = nodeStack.pop();
            list.add(nodeTemp.val);
            preNode = nodeTemp;
            nodeTemp = null;
        } else {
            nodeTemp = nodeTemp.right;
        }
    }
    return list;
}

其他

求一棵树所有左叶子节点的和

左叶子的定义,递归找到所有的子树的左叶子节点。

如果二叉树的左子树是叶子节点,要找的就是它。
如果二叉树的左子树不是一个叶子节点,则递归调用此过程去获取左子树的左叶子节点的值。
二叉树的右子树,递归调用获取右子树的左叶子节点值即可。

public class SumOfLeftLeaves {
    /**
     * 递归方式
     */
    public static int sumOfLeftLeaves(TreeNode root) {
        // 递归结束条件
        if (root == null) {
            return 0;
        }
        int left, right;
        if (root.left != null && root.left.left == null && root.left.right == null) {
            // 判断是否为左叶子节点
            left = root.left.val;
        } else {
            left = sumOfLeftLeaves(root.left);
        }
        right = sumOfLeftLeaves(root.right);
        return left + right;
    }

    /**
     * 前序遍历迭代方式
     */
    public static int sumOfLeftLeavesByPreOrder(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        int sum = 0;
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                if (root.left != null && root.left.left == null && root.left.right == null) {
                    sum += root.left.val;
                }
                root = root.left;
            }
            if (!stack.isEmpty()) {
                root = stack.pop().right;
            }
        }
        return sum;
    }

    /**
     * 层级遍历二叉树,层序遍历,广度遍历
     */
    public static int sumOfLeftLeavesByLevelOrder(TreeNode root) {
        if (root == null) {
            return 0;
        }
        Queue<TreeNode> queue = new ArrayDeque<>();
        int sum = 0;
        queue.offer(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node.left != null && node.left.left == null && node.left.right == null) {
                sum += node.left.val;
            }
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
        return sum;
    }

    /**
     * 层次遍历
     */
    public static void levelOrderTraversal(TreeNode node) {
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(node);
        while (!queue.isEmpty()) {
            node = queue.poll();
            System.out.print(node.val + " ");
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }
}

从中序与后序遍历序列构造二叉树

public static TreeNode buildTree(int[] inOrder, int[] postOrder) {
    return helper(inOrder, postOrder, postOrder.length - 1, 0, inOrder.length - 1);
}

private static TreeNode helper(int[] inOrder, int[] postOrder, int postEnd, int inStart, int inEnd) {
    if (inStart > inEnd) {
        return null;
    }
    int currentVal = postOrder[postEnd];
    TreeNode current = new TreeNode(currentVal);
    int inIndex = 0;
    for (int i = inStart; i <= inEnd; i++) {
        if (inOrder[i] == currentVal) {
            inIndex = i;
        }
    }
    TreeNode left = helper(inOrder, postOrder, postEnd - (inEnd - inIndex) - 1, inStart, inIndex - 1);
    TreeNode right = helper(inOrder, postOrder, postEnd - 1, inIndex + 1, inEnd);
    current.left = left;
    current.right = right;
    return current;
}

二叉树最大深度

也叫最大高度;思路:分别求出左右子树的最大深度,然后取其较大者。

public static int maxDepth(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int deep = 1;
    if (root.left == null && root.right == null) {
        return 1;
    }
    int leftDeep = 0;
    if (root.left != null) {
        leftDeep = 1 + maxDepth(root.left);
    }
    int rightDeep = 0;
    if (root.right != null) {
        rightDeep = 1 + maxDepth(root.right);
    }
    return deep + leftDeep > rightDeep ? leftDeep : rightDeep;
}

简化版:

public static int maxDepth1(TreeNode root) {
    if (root == null) {
        return 0;
    }
    // 注意要+1
    return Math.max(maxDepth1(root.left), maxDepth1(root.right)) + 1;
}

平衡二叉树

判断给定的二叉树是否是一颗平衡树。平衡树的定义是左右子树的高度差不超过1。

public static boolean isBalanced(TreeNode root) {
    if (root == null) {
        return true;
    }
    return (Math.abs(maxDepth(root.left) - maxDepth(root.right)) <= 1) && isBalanced(root.left)
            && isBalanced(root.right);
}

将有序数组转换为二叉搜索树

给定一个升序排序的整数数组 nums,将其转换为一棵高度平衡的二叉搜索树。
分析:给定一个有序数组,转换成二叉搜索树,即左子树小于根节点,根节点小于右子树,满足条件的二叉搜索树显然不止一种。

public static TreeNode sortedArrayToBST(int[] nums) {
    return traversal(nums, 0, nums.length - 1);
}
public static TreeNode traversal(int[] nums, int left, int right) {
    if (left > right){
        return null;
    }
    int mid = left + ((right - left) / 2);
    TreeNode node = new TreeNode(nums[mid]);
    node.left = traversal(nums, left, mid - 1);
    node.right = traversal(nums, mid + 1, right);
    return node;
}

二叉树求和路径

给定一个二叉树,和一个整数sum,求路径和为sum的方案。

分析:路径指的是从根节点到叶子节点,每次可以选择左子树或右子树。假如sum过小或过大,则不存在这样的路径;有时,存在不止一条路径。

public static List<List<Integer>> pathSum(TreeNode root, int sum) {
    if (root == null) {
        return new ArrayList<>();
    }
    List<List<Integer>> ans = new ArrayList<>();
    if (root.val == sum && root.left == null && root.right == null) {
        List<Integer> arr = new ArrayList<>();
        arr.add(root.val);
        ans.add(arr);
        return ans;
    }
    List<List<Integer>> left = pathSum(root.left, sum - root.val);
    List<List<Integer>> right = pathSum(root.right, sum - root.val);
    for (List<Integer> list : left) {
        list.add(0, root.val);
        ans.add(list);
    }
    for (List<Integer> list : right) {
        list.add(0, root.val);
        ans.add(list);
    }
    return ans;
}

二叉树中的最大路径和

二叉树的右视图

右视图的定义:从右侧看一棵树,从上到下看到的右叶子节点序列。

public static List<Integer> rightSideView(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null) {
            return list;
        }
        list.add(root.val);
        List<Integer> list1 = rightSideView(root.right);
        List<Integer> list2 = rightSideView(root.left);
        list.addAll(list1);
        if (list1.size() < list2.size()) {
            for (int i = list1.size(); i < list2.size(); i++) {
                list.add(list2.get(i));
            }
        }
        return list;
    }

序列化

思路和层序遍历没有多大差别:

public static String serialize(TreeNode root) {
    if (root == null) {
        return "[]";
    }
    ArrayList<TreeNode> queue = new ArrayList<>();
    queue.add(root);
    for (int i = 0; i < queue.size(); i++) {
        TreeNode node = queue.get(i);
        if (node == null) {
            continue;
        }
        if (node.left != null) {
            queue.add(node.left);
        }
        if (node.right != null) {
            queue.add(node.right);
        }
    }
    while (queue.get(queue.size() - 1) == null) {
        queue.remove(queue.size() - 1);
    }
    StringBuilder sb = new StringBuilder();
    sb.append("[");
    sb.append(queue.get(0));
    for (int i = 1; i < queue.size(); i++) {
        if (queue.get(i) == null) {
            sb.append(",");
            sb.append("null");
        } else {
            sb.append(",");
            sb.append(queue.get(i).val);
        }
    }
    sb.append("]");
    return sb.toString();
}

反序列化

拓展

你可能感兴趣的:(算法,算法)