vlambda博客
学习文章列表

数据结构_011_二叉树的创建和遍历

  在对二叉树进行操作之前,需要创建一棵二叉树,可以使用递归来创建一棵二叉树。

  创建一棵二叉树完毕后,需要遍历二叉树中的元素,遍历二叉树的方法大致有四种,分别是前序遍历,中序遍历,后续遍历,层次遍历

  二叉树可以有链式存储和顺序存储,顺序存储一般是针对完全二叉树。

二叉树的遍历

>>前面两节讲的是链式存储<<

  因为二叉树的创建需要了解遍历的次序,所以先了解二叉树的遍历。

  我们一般遵循先左后右的遍历习惯,也就是先左子树,再右子树,那么二叉树的遍历分为四种:前序遍历,中序遍历,后续遍历,层次遍历。不难发现,前中后的次序指的是父节点的遍历顺序。

手动创建二叉树

  在遍历二叉树之前,需要先手动创建一个二叉树。

数据结构_011_二叉树的创建和遍历

>>结点对象<<

/**
 * 二叉树的结点对象
 *
 * @param <E>
 */

public static class TreeNode<E{
    /**
     * 结点元素域
     */

    private E element;
    
    /**
     * 左子树指针
     */

    private TreeNode<E> left;
    
    /**
     * 右子树指针
     */

    private TreeNode<E> right;

    // 省略get set和构造方法
}

>>二叉树对象<<

  二叉树对象,有一个根结点root,和其他的一些遍历方法,遍历方法的实现后面介绍。

/**
 * @author: Dylan kwok GSGB
 * @date: 2021/1/4 21:22
 * 二叉树对象
 */

public class BinaryTree<E{
    /**
     * 根结点
     */

    private TreeNode<E> root;

    public BinaryTree() {
    }

    public BinaryTree(TreeNode<E> root) {
        this.root = root;
    }
}

>>手动创建一棵二叉树<<

  创建一个测试类

/**
 * @author: Dylan kwok GSGB
 * @date: 2021/1/4 21:17
 *
 * 二叉树遍历测试
 */

public class BinaryTreeErgodic {
    public static void main(String[] args) {
        // 手动创建一个二叉树
        BinaryTree<String> binaryTree = initBinaryTree();
    }

    /**
     * 手动创建一个二叉树,直观图如下
     *
     *          曹操
     *        /     \
     *      曹丕     曹植
     *     /   \       \
     *   曹睿   曹协     曹志
     *      \
     *      曹芳
     */

    private static BinaryTree<String> initBinaryTree() {
        // 在遍历前,先在内存中创建一个二叉树出来,先用呆板的创建方法
        BinaryTree<String> binaryTree = new BinaryTree<>();
        // 设置根节点等结点
        BinaryTree.TreeNode<String> root = new BinaryTree.TreeNode<>("1_曹操");
        BinaryTree.TreeNode<String> node2 = new BinaryTree.TreeNode<>("2_曹丕");
        BinaryTree.TreeNode<String> node3 = new BinaryTree.TreeNode<>("3_曹植");
        BinaryTree.TreeNode<String> node4 = new BinaryTree.TreeNode<>("4_曹睿");
        BinaryTree.TreeNode<String> node5 = new BinaryTree.TreeNode<>("5_曹协");
        BinaryTree.TreeNode<String> node6 = new BinaryTree.TreeNode<>("6_曹志");
        BinaryTree.TreeNode<String> node7 = new BinaryTree.TreeNode<>("7_曹芳");
        // 手动设置结点之间的关系
        root.setLeft(node2);
        root.setRight(node3);
        node2.setLeft(node4);
        node2.setRight(node5);
        node3.setRight(node6);
        node4.setRight(node7);
        binaryTree.setRoot(root);
        return binaryTree;
    }
}

递归前序遍历

  前序遍历:假如二叉树不为空,先访问根节点,遍历左子树,假如左子树遍历完毕或者为空才能遍历右子树。

  在二叉树BinaryTree类中增加下面两个方法。

思路:

  • 首先,我们需要判断根节点是否为空。
  • 假如不为空,且左子结点不为空,则递归遍历左子结点。
  • 假如左子树遍历完毕,且当前结点的右子节点不为空,则递归遍历右子树。

打印结果:曹操 > 曹丕 > 曹睿 > 曹芳 > 曹协 > 曹植 > 曹志。

/**
 * 前序遍历
 * 1.先访问根节点
 * 2.输出当前结点 (父节点先输出)
 * 3.假如当前结点的左子结点不为空,则递归遍历左子树
 * 4.假如左子树遍历完毕,且当前结点的右子节点不为空,则递归遍历右子树
 */

public void preOrder() {
    if (root == null) {
        return;
    }
    preOrder(root);

}

/**
 * 1_曹操 > 2_曹丕 > 4_曹睿 > 7_曹芳 > 5_曹协 > 3_曹植 > 6_曹志
 */

private void preOrder(TreeNode node) {
    // 前序遍历,先打印父结点
    System.out.println(node);
    // 假如当前结点的左子结点不为空,继续遍历左子树
    if (node.getLeft() != null) {
        preOrder(node.getLeft());
    }
    // 假如当前结点的右子结点不为空,继续遍历右子树
    if (node.getRight() != null) {
        preOrder(node.getRight());
    }
}

递归中序遍历

  中序遍历其实和前序遍历的代码90%是一样的,只是打印父节点的位置不同而已。

/**
 * 中序遍历
 *  1.先访问根节点
 *  2.假如当前结点的左子结点不为空,则递归遍历左子树
 *  3.输出当前结点 (父节点中间输出)
 *  4.假如左子树遍历完毕,且当前结点的右子节点不为空,则递归遍历右子树
 */

public void inOrder() {
    if (root == null) {
        return;
    }
    inOrder(root);
}

/**
 * 4_曹睿 > 7_曹芳 > 2_曹丕 > 5_曹协 > 1_曹操 > 3_曹植 > 6_曹志
 */

private void inOrder(TreeNode node) {
    // 假如当前结点的左子结点不为空,继续遍历左子树
    if (node.getLeft() != null) {
        inOrder(node.getLeft());
    }
    // 中序遍历,中间打印父结点
    System.out.println(node);
    // 假如当前结点的右子结点不为空,继续遍历右子树
    if (node.getRight() != null) {
        inOrder(node.getRight());
    }
}

递归后序遍历

  后序遍历其实和前序遍历的代码90%是一样的,只是打印父节点的位置不同而已。

/**
 * 后序遍历
 */

public void postOrder() {
    if (root == null) {
        return;
    }
    postOrder(root);
}

/**
 * 7_曹芳 > 4_曹睿 > 5_曹协 > 2_曹丕 > 6_曹志 > 3_曹植 > 1_曹操
 */

public void postOrder(TreeNode node) {
    // 假如当前结点的左子结点不为空,继续遍历左子树
    if (node.getLeft() != null) {
        postOrder(node.getLeft());
    }
    // 假如当前结点的右子结点不为空,继续遍历右子树
    if (node.getRight() != null) {
        postOrder(node.getRight());
    }
    // 后序遍历,最后打印父结点
    System.out.println(node);
}

层次遍历

  层次遍历,顾名思义,就是一层一层的遍历。

  首先是第一层,也就是根节点。然后第二层,第三…….,每层从左至右遍历。

  可以使用队列这种数据结构来实现二叉树的层次遍历。

思路:

  • 判断根结点是否存在,存在则继续遍历。
  • 创建一个队列数据结构的对象。
  • 先将根结点入队。
  • 进入循环,退出条件是队列为空。
    • 队首出队。
    • 假如当前结点的左子结点不为空,将左子结点入队。
    • 假如当前结点的右子结点不为空,将右子结点入队。

文字看起来是很抽象,现在看一下图解:

数据结构_011_二叉树的创建和遍历

代码:

/**
 * 层次遍历
 * 首先遍历第一层,再第二层... 从左至右
 * 使用JDK自带的队列实现层次遍历
 *
 * 1_曹操 > 2_曹丕 > 3_曹植 > 4_曹睿 > 5_曹协 > 6_曹志 > 7_曹芳
 */

public void levelOrder() {
    if (root == null) {
        // 根为空,直接退出
        return;
    }
    ArrayDeque<TreeNode<E>> deque = new ArrayDeque<>();
    deque.offer(root); // 根节点入队
    while (!deque.isEmpty()) {
        // 队首出队
        TreeNode<E> node = deque.remove();
        System.out.println(node); // 打印当前结点
        TreeNode<E> left = node.getLeft();
        TreeNode<E> right = node.getRight();
        if (left != null) {
            deque.offer(left);
        }
        if (right != null) {
            deque.offer(right);
        }
    }
}

非递归前序遍历

  二叉树的遍历操作,使用递归来实现比较优雅,不过也可以用栈这种数据结构来实现。

思路: 后面的非递归中序思路差不多

  1.初始时依次扫描根结点的所有左侧结点,访问它,并将它们一一进栈。

  2.假如某个结点没有左子结点,出栈一个结点。

  3.扫描该结点的右子结点,并将其进栈。

  4.依次扫描右子结点的所有左侧结点,并一一进栈。

  5.反复该过程直到栈为空为止。

public void preOrderStack() {
    Stack<TreeNode<E>> stack = new Stack<>();
    TreeNode<E> node = root;
    while(node != null || !stack.isEmpty()){
        if (node != null){
            System.out.println(node); // 前序遍历
            stack.push(node);
            node = node.getLeft(); // 获取左子结点
        } else {
            // 弹栈是因为没找到当前结点的左子结点,来找当前结点的右子结点
            node = stack.pop();
            node = node.getRight(); // 获取右子结点
        }
    }
}

非递归中序遍历

public void inOrderStack() {
    if(root == null) {
        // 根结点为空则直接返回
        return;
    }
    Stack<TreeNode<E>> stack = new Stack<>();
    TreeNode<E> node = root;
    // 循环退出条件 当前结点为空或栈为空
    while(node != null || !stack.isEmpty()) {
        if(node != null) {
            stack.push(node);
            node = node.getLeft();
        } else {
            node = stack.pop();
            System.out.println(node); // 中序遍历
            node = node.getRight();
        }
    }
}

非递归后序遍历

  • 初始时依次扫描根结点的所有左侧结点,并将它们一一进栈。
  • 查看栈顶元素的右结点。
    • 假如该右结点为空,或者已经访问过,则弹出栈顶结点,并访问该结点。(ps.这样做就是为了后续打印父结点)
    • 假如该右节点不为空,或者未访问过,则依次扫描右子结点的所有左侧结点,并一一进栈。
  • 反复该过程直到栈为空为止。
public void postOrderStack3(){
    Stack<TreeNode<E>> stack = new Stack<TreeNode<E>>();
    TreeNode<E> node = root;
    TreeNode<E> preNode = null;   // 记录之前遍历的右结点
    while(node != null || !stack.isEmpty()){
        if (node != null) {
            stack.push(node);
            node = node.getLeft();
        } else {
            node = stack.peek(); // 查看栈顶元素
            // 如果右结点为空,或者右结点之前遍历过,打印根结点
            if(node.getRight() == null || node.getRight() == preNode){
                System.out.println(node);
                node = stack.pop();
                preNode = node;
                node = null;
            }
            else{
                node = node.getRight();
            }
        }
    }
}

二叉树的创建

  二叉树的创建可以使用补空法。

  在得到补空之后的二叉树后,使用先序遍历得到一个序列,根据该序列来创建二叉树。

  上面补空二叉树的前序遍历的序列为:ABD##E##CF##

代码:

BinaryTree类的成员方法。

/**
 * 创建二叉树 默认是字符串等基本类型
 *
 * 输入一个 补空二叉树 的前序序列 来创建二叉树
 */

public void createBinaryTree(String preOrderStr) {
    // 创建一个扫描对象
    String[] split = preOrderStr.split(",");
    List<String> preOrderList = Arrays.stream(split).collect(Collectors.toList());
    Iterator<String> it = preOrderList.iterator();
    this.root = createBinaryTree(it);
}

private TreeNode<E> createBinaryTree(Iterator<String> it) {
    if (!it.hasNext()) {
        return null;
    }
    String temp = it.next();
    if (Objects.equals(temp, "#")) {
        return null;
    }
    // 生成父节点
    TreeNode<E> node = new TreeNode<>((E)temp); // 针对基本数据类型类转换的
    // 生成左子树
    node.setLeft(createBinaryTree(it));
    // 生成右子树
    node.setRight(createBinaryTree(it));
    return node;
}

测试:

public static void main(String[] args) {
        /* ------ 开始测试 创建 ------*/
        BinaryTree binaryTree = new BinaryTree();
        binaryTree.createBinaryTree("A,B,D,#,#,E,#,#,C,F,#,#");
        // 前序遍历创建的二叉树试下
//        binaryTree.preOrder();
//        binaryTree.inOrder();
//        // 后序遍历
//        binaryTree.postOrder();
//        // 层次遍历
        binaryTree.levelOrder(); // A B C D E F

顺序存储的二叉树

  顺序存储二叉树一般针对的是完全二叉树,假如不是完全二叉树则可能会导致大量的空间浪费。

  由完全二叉树的性质  假如根结点的索引为0,假如某个父节点的索引为n,若它的左右子结点存在的话,那么

  • 左子结点的索引为2n+1。
  • 右子结点的索引为2n+2。

要求:给一个数组,按照前序,中序,后序的遍历方式遍历数组。

public class ArrayBinaryTree<E{
    /**
     * 存储元素的数组
     */

    private Object[] elementData;

    public ArrayBinaryTree(Object[] elementData) {
        this.elementData = elementData;
    }
}

给定的数组

private static final String[] TREE_4 = {"A","B","C","D","E","F","G","H","I"};

前序遍历

/**
 * 前序遍历
 */

public void preOrder() {
    preOrder(0);
}

/**
 * 前序遍历
 */

private void preOrder(int index) {
    if (elementData == null || elementData.length <= 0) {
        return;
    }
    // 先打印父结点
    System.out.print(elementData[index]);
    int left = 2 * index + 1;
    int right = 2 * index + 2;
    // 递归打印左子树的结点
    if (left < elementData.length) {
        preOrder(left);
    }
    if (right < elementData.length) {
        preOrder(right);
    }
}

中序遍历

/**
 * 中序遍历
 */

public void inOrder() {
    inOrder(0);
}

/**
 * 中序遍历
 */

private void inOrder(int index) {
    if (elementData == null || elementData.length <= 0) {
        return;
    }
    int left = 2 * index + 1;
    int right = 2 * index + 2;
    // 递归打印左子树的结点
    if (left < elementData.length) {
        inOrder(left);
    }
    // 中间印父结点
    System.out.print(elementData[index]);
    if (right < elementData.length) {
        inOrder(right);
    }
}

后序遍历

/**
 * 后续遍历
 */

public void postOrder() {
    postOrder(0);
}

/**
 * 后续遍历
 */

private void postOrder(int index) {
    if (elementData == null || elementData.length <= 0) {
        return;
    }
    int left = 2 * index + 1;
    int right = 2 * index + 2;
    // 递归打印左子树的结点
    if (left < elementData.length) {
        postOrder(left);
    }
    if (right < elementData.length) {
        postOrder(right);
    }
    // 后打印父结点
    System.out.print(elementData[index]);
}