算法数据结构:二叉树

398 阅读5分钟

1、什么是二叉树(Binary Tree)

关于树,有几个比较常用的概念你需要掌握,那就是:根节点、叶子节点、父节点、子节点、兄弟节点,还有节点的高度、深度、层数,以及树的高度。

二叉树,顾名思义,每个节点最多有两个子节点,分别是左子节点和右子节点。不过,一个二叉树中并不要求每个节点都必须有两个节点,有的节点只有左子节点,有的节点只有右子节点,还有的节点左右子节点都为空。如下图所示

上图中有两个比较特殊的二叉树,分别是编号 2 和编号 3 这两个。

其中编号 2 的二叉树中的叶子节点全都在最低层,除了叶子节点之外,每个节点都有左右两个子结点,这种二叉树叫做满二叉树

编号 3 的二叉树中,叶子节点都在最低下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其它层的节点个数都达到了最大,这种二叉树叫做完全二叉树

2、如何存储一棵二叉树

想要存储一棵二叉树,通常两种方法,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。

2.1、链式存储法

每个节点有三个字段,其中一个存储数据,另外两个是指向左右子结点的指针。我们只要通过根节点,就可以通过左右子结点的指针,把整棵树都串起来,如图所示。大部分二叉树都是通过这种结构来实现的。

2.2、基于数组的顺序存储法

用数组的顺序存储法实现二叉树,当前节点的下标为 i,左子节点的下标为i * 2,右子节点的下标为2 * i + 1,这里i从 1 开始,也就是根节点的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置)。如图所示,通过这种方式,我们只要知道根节点存储的位置,这样就可以通过下标计算,把整棵二叉树穿起来了。

上图的二叉树是一颗完全二叉树,所以仅仅浪费了一个下标为 0 的存储位置。如果是非完全二叉树,会浪费比较多的数组存储空间。如图所示

当二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式,因为数组的存储方式并不需要像链式存储法那样,需要存储额外的左右子节点的指针。这里我们通常会想到,堆的底层实现就是一棵完全二叉树,最常用的存储方式就是数组。

3、二叉树的遍历

如何将所有节点都遍历打印出来呢?经典的方法有三种,前序遍历中序遍历后序遍历。其中前、中、后序,表示的是当前节点与它左右子树节点遍历打印的先后顺序。

  1. 前序遍历,对于树中的任意节点,先打印当前节点,然后打印它的左子树,最后打印它的右子树
  2. 中序遍历,对于树中的任意节点,先打印它的左子树,然后打印当前节点,最后打印它的右子树
  3. 后序遍历,对于树中的任意节点,先打印它的左子树,然后打印它的右子树,最后打印当前节点

3.1、递归实现
/**
 * @description: 二叉树遍历 递归
 * @author: erlang
 * @since: 2020-11-25 22:54
 */
public class RecursiveTraversalBT {

    /**
     * 前序遍历
     *
     * @param root 待遍历二叉树的根节点
     */
    public static void pre(TreeNode root) {
        if (root == null) {
            return;
        }

        System.out.print(root.value + "  ");
        pre(root.left);
        pre(root.right);
    }

    /**
     * 中序遍历
     *
     * @param root 待遍历二叉树的根节点
     */
    public static void in(TreeNode root) {
        if (root == null) {
            return;
        }

        in(root.left);
        System.out.print(root.value + "  ");
        in(root.right);
    }

    /**
     * 后序遍历
     *
     * @param root 待遍历二叉树的根节点
     */
    public static void post(TreeNode root) {
        if (root == null) {
            return;
        }

        post(root.left);
        post(root.right);
        System.out.print(root.value + "  ");
    }

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

        System.out.print("pre-order: ");
        pre(head);
        System.out.println("\n========");
        System.out.print("in-order: ");
        in(head);
        System.out.println("\n========");
        System.out.print("post-order: ");
        post(head);
        System.out.println("\n========");

    }
}

3.2、栈实现
import java.util.Stack;

/**
 * @description: 二叉树遍历 非递归
 * @author: erlang
 * @since: 2020-11-25 22:54
 */
public class UnRecursiveTraversalBT {

    /**
     * 前序遍历
     * 1、弹栈,打印
     * 2、如有右,压入右
     * 3、如有左,压入左
     *
     * @param root 待遍历二叉树的根节点
     */
    public static void pre(TreeNode root) {
        System.out.print("pre-order: ");
        if (root == null) {
            return;
        }
        Stack<TreeNode> stack = new Stack<>();
        // 先压入根节点
        stack.add(root);
        while (!stack.isEmpty()) {
            root = stack.pop();
            // 1、弹栈,打印
            System.out.print(root.value + "  ");
            // 2、如有右,压入右
            if (root.right != null) {
                stack.push(root.right);
            }
            // 3、如有左,压入左
            if (root.left != null) {
                stack.push(root.left);
            }
        }

    }

    /**
     * 中序遍历
     * 1、整条左边界依次入栈
     * 2、不满足 1 时,弹出节点并打印
     * 3、到右子树上继续执行 1
     * @param root 待遍历二叉树的根节点
     */
    public static void in(TreeNode root) {
        System.out.print("in-order: ");
        if (root == null) {
            return;
        }

        Stack<TreeNode> stack = new Stack<>();
        while (!stack.isEmpty() || root != null) {
            if (root != null) {
                // 1、整条左子树依次入栈
                stack.push(root);
                root = root.left;
            } else {
                // 2、不满足 1 时,弹出节点并打印
                root = stack.pop();
                System.out.print(root.value + "  ");
                // 3、到右子树上继续执行 1
                root = root.right;
            }
        }
    }

    /**
     * 后序遍历
     * 1、弹栈,收集到 res 栈中,
     * 2、如有左,压入左
     * 3、如有右,压入右
     * 4、最后打印 res
     *
     * @param root 待遍历二叉树的根节点
     */
    public static void post1(TreeNode root) {
        System.out.print("post-order1: ");
        if (root == null) {
            return;
        }
        Stack<TreeNode> stack = new Stack<>();
        Stack<TreeNode> res = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            root = stack.pop();
            res.push(root);
            if (root.left != null) {
                stack.push(root.left);
            }
            if (root.right != null) {
                stack.push(root.right);
            }
        }
        while (!res.isEmpty()) {
            System.out.print(res.pop().value + "  ");
        }

    }

    /**
     * 后序遍历
     *
     * @param root 待遍历二叉树的根节点
     */
    public static void post2(TreeNode root) {
        System.out.print("post-order2: ");
        if (root == null) {
            return;
        }

        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.peek();
            if (cur.left != null && root != cur.left && root != cur.right) {
                // 左树未处理的时候,处理左树
                // 当前节点的左子树不为空,且上次打印的节点不是右子树也不是左子树
                // 则说明当前节点是第一次访问
                // 即当前节点未打印过
                stack.push(cur.left);
            } else if (cur.right != null && root != cur.right) {
                // 右树未处理的时候,处理右树
                // 当前节点的右子树不为空,且上次打印的节点不是右子树
                // 则说明当前节点是第一次访问
                // 即当前节点未打印过
                stack.push(cur.right);
            } else {
                // 当前节点的左右子树都被访问过后,打印当前节点
                System.out.print(stack.pop().value + "  ");
                // 记录上次打印的节点
                root = cur;
            }
        }
    }

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

        pre(head);
        System.out.println("\n========");
        in(head);
        System.out.println("\n========");
        post1(head);
        System.out.println("\n========");
        post2(head);
        System.out.println("\n========");

    }
}

3.3、二叉树实现
/**
 * @description: 二叉树
 * @author: erlang
 * @since: 2020-11-25 22:56
 */
public class TreeNode {

    public int value;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(int v) {
        value = v;
    }
}

4、小结

二叉树的每个节点最多有两个子节点,分别是左子节点和右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和完全二叉树。满二叉树又是完全二叉树的一种特殊情况。

二叉树既可以用链式存储,也可以用数组顺序存储。数组顺序存储的方式比较适合完全二叉树,其他类型的二叉树用数组存储会比较浪费存储空间。除此之外,二叉树里非常重要的操作就是前、中、后序遍历操作,遍历的时间复杂度是 O(n)。