Loading

二叉树遍历和延伸

之前为了求快,认为递归和动态规划是一类的,导致做了不少递归的题只能想到思路,但就是没法代码实现出来!!!

现在学习二叉树,我觉得对学习递归真的很有帮助!

二叉树的基础:遍历二叉树

二叉树遍历是基础,如果我们不能遍历二叉树,又如何对二叉树的节点进行操作呢?

二叉树的节点:

/**
 * @author keboom
 * @date 2021/4/30
 */
public class Node {
    public int value;
    public Node left;
    public Node right;

    public Node(int data) {
        this.value = data;
    }

    /**
     *          10
     *     12        15
     *  4     7    5    18
     * @return
     */
    public static Node getTestTree() {
        Node node10 = new Node(10);
        Node node5 = new Node(5);
        Node node15 = new Node(15);
        Node node4 = new Node(4);
        Node node7 = new Node(7);
        Node node12 = new Node(12);
        Node node18 = new Node(18);

        node10.left = node12;
        node10.right = node15;
        node12.left = node4;
        node12.right = node7;
        node15.left = node5;
        node15.right = node18;

        return node10;
    }
}

getTestTree此方法为了方便测试,不用理会。

递归遍历

    public void preOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        System.out.println(head.value + " ");
        preOrderRecur(head.left);
        preOrderRecur(head.right);
    }

    public void inOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        inOrderRecur(head.left);
        System.out.println(head.value + " ");
        inOrderRecur(head.right);
    }

    public void posOrderRecur(Node head) {
        if (head == null) {
            return;
        }
        posOrderRecur(head.left);
        posOrderRecur(head.right);
        System.out.println(head.value + " ");
    }

以上分别为先序,中序和后序遍历。

我认为这三种遍历的区别:

  • 先序遍历:先打印,再向下递归

  • 中序和后序:先递归到最下面,再打印

只看简单的遍历太简单不直观,请看下面的几道题

接下里的几道题目在求解过程中我会跟二叉树的遍历做一些联系,我认为做这些联系是有助于解题的😄

非递归遍历

    public void preOrderUnRecur(Node head) {
        System.out.println("pre-order: ");
        if (head != null) {
            Stack<Node> stack = new Stack<>();
            stack.add(head);
            while (!stack.isEmpty()) {
                head = stack.pop();
                System.out.print(head.value + " ");
                if (head.right != null) {
                    stack.push(head.right);
                }
                if (head.left != null) {
                    stack.push(head.left);
                }
            }
        }
        System.out.println();
    }

    public void inOrderUnRecur(Node head) {
        System.out.println("in-order: ");
        if (head != null) {
            Stack<Node> stack = new Stack<>();
            while (!stack.isEmpty() || head != null) {
                if (head != null) {
                    stack.push(head);
                    head = head.left;
                } else {
                    head = stack.pop();
                    System.out.print(head.value + " ");
                    head = head.right;
                }
            }
        }
        System.out.println();
    }

	/**
	* 先将头放入s2,然后将右子树放入s2,最后将左子树放入s2
	*/
    public void posOrderUnRecur1(Node head) {
        System.out.println("pos-order: ");
        if (head != null) {
            Stack<Node> s1 = new Stack<>();
            Stack<Node> s2 = new Stack<>();
            s1.push(head);
            while (!s1.isEmpty()) {
                head = s1.pop();
                s2.push(head);
                if (head.left != null) {
                    s1.push(head.left);
                }
                if (head.right != null) {
                    s1.push(head.right);
                }
            }
            while (!s2.isEmpty()) {
                System.out.print(s2.pop().value + " ");
            }
        }
        System.out.println();
    }

递归的前中后序遍历看起来挺有规律,但是非递归的就不太一样了,我是找不到啥规律。

非递归的前序遍历和中序遍历都模拟了递归调用栈。

打印边缘节点

题目:

给定一棵二叉树的头节点 head,按照标准实现二叉树的边界节点的逆时针打印。

1.头节点为边界节点。
2.叶节点为边界节点。
3.如果节点在其所在的层中是最左的或最右的,那么该节点也是边界节点。

image-20210517093814189

打印结果为:1,2,4,7,11,13,14,15,16,12,10,6,3

思路:

  • 获得树高
  • 依据树高构建Node[][] edgeMap = new Node[height][2]edgeMap是存储二叉树每一层的最左和最右的边缘节点
  • 有了edgeMap后便可以打印左边缘节点
  • 打印如14,15这样的非边缘节点,但为叶结点
  • 从下向上打印右边缘节点

代码:

二叉树遍历是基础,以下代码我会跟二叉树的前中后序遍历做一些联系,如果读者觉着这样的联系并不好,那就忽略吧。

获得树高

    /**
     * 获得二叉树的高度
     * 通过不断向下递归查找,找到最大高度
     * @param head
     * @param height
     * @return
     */
    private int getHeight(Node head, int height) {
        if (head == null) {
            return height;
        }
        return Math.max(getHeight(head.left, height + 1), getHeight(head.right, height + 1));
    }

此方法我认为类似后序遍历。向左右递归,得到左右的树高,然后在做比较取大一点的。

符合先左右递归,然后操作。

获得edgeMap

此方法将每一层的左右边缘节点存储到edgeMap

    /**
     * edgeMap是一个二维数组,记录树的每一层最左和最右边缘节点。
     * @param h
     * @param l
     * @param edgeMap
     */
    private void setEdgeMap(Node h, int l, Node[][] edgeMap) {
        if (h == null) {
            return;
        }
        edgeMap[l][0] = edgeMap[l][0] == null ? h : edgeMap[l][0];
        edgeMap[l][1] = h;
        setEdgeMap(h.left, l + 1, edgeMap);
        setEdgeMap(h.right, l + 1, edgeMap);
    }

这个一看就符合先序遍历。

先操作edgeMap,然后递归。

        edgeMap[l][0] = edgeMap[l][0] == null ? h : edgeMap[l][0];
        edgeMap[l][1] = h;

上面两行代码挺巧妙的,仔细思考先序遍历的过程和规律,发现这两行代码正好就能求的左右边缘。

打印非边缘节点的叶结点

    /**
     * 打印不是边缘节点的叶节点
     * @param h
     * @param l
     * @param m
     */
    private void printLeafNotInMap(Node h, int l, Node[][] m) {
        if (h == null) {
            return;
        }
        // 首先要是叶子结点,并且不是左右边缘
        if (h.left == null && h.right == null && h != m[l][0] && h != m[l][1]) {
            System.out.print(h.value + " ");
        }
        printLeafNotInMap(h.left, l + 1, m);
        printLeafNotInMap(h.right, l + 1, m);
    }

这个也是先序遍历了。

符合先操作,后递归。

先序遍历每个节点,判断其是叶结点但不为边缘节点。

主方法

    /**
     * @param head
     */
    public void printEdge1(Node head) {
        if (head == null) {
            return;
        }
        int height = getHeight(head, 0);
        Node[][] edgeMap = new Node[height][2];
        setEdgeMap(head, 0, edgeMap);
        for (int i = 0; i != edgeMap.length; i++) {
            System.out.print(edgeMap[i][0].value + " ");
        }
        printLeafNotInMap(head, 0, edgeMap);
        for (int i = edgeMap.length - 1; i != -1; i--) {
            if (edgeMap[i][0] != edgeMap[i][1]) {
                System.out.print(edgeMap[i][1].value + " ");
            }
        }
        System.out.println();
    }

主方法与思路一致,不多说了。

完整代码:Gitee

序列化二叉树

题目:

将一棵二叉树通过先序遍历序列化成字符串。再通过字符串反序列化出二叉树。

image-20210520210255882

序列化结果为字符串:“10!12!4!#!#!7!#!#!15!5!#!#!18!#!#!”

思路:

以!为分隔符,空指针用#,在先序遍历的过程中构造一个字符串,然后返回。

代码:

    /**
     * 通过先序遍历来序列化
     * @param head
     * @return
     */
    public String serialByPre(Node head) {
        if (head == null) {
            return "#!";
        }
        String res = head.value + "!";
        res += serialByPre(head.left);
        res += serialByPre(head.right);
        return res;
    }

看看那!!!多么像先序遍历打印二叉树的代码啊!!!

以下为反序列化:

    /**
     * 先序遍历反序列化
     * @param preStr
     * @return
     */
    public Node reconByPreString(String preStr) {
        String[] values = preStr.split("!");
        LinkedList<String> queue = new LinkedList<>();
        for (int i = 0; i < values.length; i++) {
            queue.offer(values[i]);
        }
        return reconPreOrder(queue);
    }

    /**
     * 正好模拟了先序遍历的过程
     * @param queue
     * @return
     */
    private Node reconPreOrder(Queue<String> queue) {
        String value = queue.poll();
        if (value.equals("#")) {
            return null;
        }
        Node head = new Node(Integer.valueOf(value));
        head.left = reconPreOrder(queue);
        head.right = reconPreOrder(queue);
        return head;
    }

地址:gitee

打印二叉树

题目:

image-20210520210255882

打印结果:

image-20210520212036696

"H"表示头结点,"v"表示此节点为父节点的右节点,"^"表示为父节点的左节点。

思路:

先打印右子树,再打印左子树。这样的打印结果顺时针旋转90度就是二叉树直观形状了。

上图的打印结果,第一行为最右节点,第二行为最右节点的父节点,第三行为左兄弟节点,如此规律那就是一个中序遍历啊!!

代码:

printTree方法为主函数,printInOrder会以中序遍历来打印节点,只不过先打印的是右子树,len为一个节点所占的字符串长度。getSpace用来凑空字符串。

    public void printTree(Node head) {
        System.out.println("Binary Tree: ");
        printInOrder(head, 0, "H", 17);
        System.out.println();
    }

    public void printInOrder(Node head, int height, String to, int len) {
        if (head == null) {
            return;
        }
        printInOrder(head.right, height + 1, "v", len);
        String val = to + head.value + to;
        int lenM = val.length();
        int lenL = (len - lenM) / 2;
        int lenR = len - lenM - lenL;
        val = getSpace(lenL) + val + getSpace(lenR);
        System.out.println(getSpace(height * len) + val);
        printInOrder(head.left, height + 1, "^", len);
    }

    private String getSpace(int num) {
        String space = " ";
        StringBuffer buf = new StringBuffer("");
        for (int i = 0; i < num; i++) {
            buf.append(space);
        }
        return buf.toString();
    }

这就是中序遍历,只不过中间我们需要记录当前遍历到树的当前高度等信息。

地址:gitee

判断 t1 树是否包含 t2 树全部的拓扑结构

题目:

image-20210520214117843

第一棵树包含第二棵树全部的拓扑结构,返回true

思路:

  1. 我们需要先找到节点2
  2. 然后从节点2开始比较所有节点是否相等

代码:

contains方法让我们先遍历t1的节点,先找到我们上面所说的“节点2”check方法则判断t2的每个节点值是否与t1对应。

    public boolean contains(Node t1, Node t2) {
        if (t2 == null) {
            return true;
        }
        if (t1 == null) {
            return false;
        }
        // 因为t1不是一开头就跟t2匹配,所以也需要contains递归去找
        // 只要找到t1的子节点与t2相等的,则进行check递归,由于或运算只要有一个true,则结果为true
        return check(t1, t2) || contains(t1.left, t2) || contains(t1.right, t2);
    }

    private boolean check(Node h, Node t2) {
        if (t2 == null) {
            return true;
        }
        // 此还可判断t1子节点是否与t2头结点相等呢!
        if (h == null || h.value != t2.value) {
            return false;
        }
        return check(h.left, t2.left) && check(h.right, t2.right);
    }

上面的就跟正常的递归遍历形式上不太一样,但是核心思想还是在通过左右递归遍历二叉树。

地址:gitee

以后还有很多二叉树题目都是在二叉树递归的基础上加大难度,比如在递归过程中需要记录高度或者其他信息,这是对于递归的参数而言。对于返回值来说,打印类的题目自然就是void,判断类的题目自然就是boolean,构造二叉树自然就是Node,返回二叉树高度自然就是int,这是对于递归的返回值来说。对于终止条件最常见的就是head == null,当然了要视情况而定。然后就是左递归,右递归,一坨操作。如果我们需要立即进行操作,就是说从传进来一个头结点我们就操作他,然后在递归左节点和右节点,那么就类似先序遍历。如果我们需要先递归到二叉树的最左下,然后在操作之,那么就需要用中序或者后序遍历了。

posted @ 2021-05-18 12:44  KeBoom  阅读(76)  评论(1编辑  收藏  举报