2022-001ARTS:二叉树的遍历

48 阅读4分钟

ARTS:Algorithm、Review、Tip、Share

  • Algorithm 算法题
  • Review 英文文章
  • Tip 回想一下本周工作中学到的一个小技巧
  • Share思考一个技术观点、社会热点、一个产品或是一个困惑

Algorithm

二叉树的递归序遍历

递归序

二叉树的遍历有:先序、中序、后续三种主要方式,主要区别在任何子树的处理顺序上,具体如下。

  1. 先序:先处理头节点、再左子树、然后右子树;
  2. 中序:先处理左子树、再头节点、然后右子树;
  3. 后续:先处理左子树、再右子树、然后头节点。

虽然比较简单,但记忆起来还是比较麻烦的!实际上,我们可以通过递归的方法实现如上三种遍历形式,其实递归不仅可以作为其实现方式,更是三种遍历方式的本质!

先序、中序、后续,都是递归序加工的结果。

public static void f(Node head) {
	if (head == null) {
		return;
	}
	// 1
	f(head.left);
	// 2
	f(head.right);
	// 3
}

方法 f 的实现中包含了另外两次递归调用,分别对应左子树、右子树的处理,也意味着我们在处理每个节点的时候,都有三次处理的机会,一次是在刚进入这个节点的时候(即位置1)、二次是在执行完左子树 f(head.left) 的处理返回后的时机(即位置2)、三次是在执行完右子树 f(head.right) 的处理返回后(即位置3)。 我们将处理当前节点的过程分别放在1、2、3的位置上时,分别对应了先序、中序、后续的实现!

练习:94. 二叉树的中序遍历

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

示例 1: example-01

输入:root = [1,null,2,3] 输出:[1,3,2] 示例 2:

输入:root = [] 输出:[] 示例 3:

输入:root = [1] 输出:[1]

示例 4: example-04 输入:root = [1,2] 输出:[2,1] 示例 5: example-05 输入:root = [1,null,2] 输出:[1,2]

提示:

树中节点数目在范围 [0, 100] 内 -100 <= Node.val <= 100

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/bi…

递归解法

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> out = new LinkedList<>();
    in(root, out);
    return out;
}

private void in (TreeNode node, List<Integer> out) {
    if (node == null) {
        return;
    }
    this.in(node.left, out);
    out.add(node.val);
    this.in(node.right, out);
}

非递归序

可以明确的是,通过自己使用栈结构的方式,可以将任何递归函数改写成非递归的方式,具体到二叉树的先、中、后续遍历来看,具体实现如下:

先序遍历

先序的实现是:头、左、右的顺序,我们在入栈时,需将右子节点先入栈、而后是左子节点,这样弹出打印的时候也就是要求的顺序了:

public static void pre(Node cur) {
    System.out.print("pre order: ");
    Stack<Node> stack = new Stack<>();
    stack.push(cur);
    while (!stack.isEmpty()) {
    	// 1. 弹出并打印
        cur = stack.pop();
        System.out.print(cur.value + " ");

		// 2. 如有右孩子,压入栈
        if (cur.right != null) {
            stack.push(cur.right);
        }

		// 3. 如有左孩子,压入栈
        if (cur.left != null) {
            stack.push(cur.left);
        }
    }
    System.out.println();
}

中序遍历

我们需将整个左边界上的节点全部入栈,然后到达最左子结点,弹出并打印处理后,进入当前节点的右子树上,继续执行左边界入栈操作;没有右子树,则弹出栈中元素,执行上一级。

public static void in(Node cur) {
    System.out.print("in order: ");
    Stack<Node> stack = new Stack<>();
    while (!stack.isEmpty() || cur != null) {
    	// 左边界入栈
        if (cur != null) {
            stack.push(cur);
            cur = cur.left;
        } else {
        	// 弹出并打印处理,定位到右子树(右子树为空,则弹出栈中元素为上一级)
            cur = stack.pop();
            System.out.print(cur.value + " ");
            cur = cur.right;
        }
    }
    System.out.println();
}

后续遍历-1

与先序遍历相反,这次按照头结点、左子节点、右子节点的顺序压栈处理(s1栈),弹出时借助另外一个栈(s2)来实现顺序的转换,比如只有三个节点(root 下面跟了左子节点 left、右子节点 right)的时候,进入s1栈的顺序为:

  1. root 压入 s1;
  2. s1 弹出 root,root 压入s2;
  3. s1 压入 left;
  4. s1 压入 right;
  5. s1 弹出 right,right 压入s2;
  6. s1 弹出 left,left 压入s2;

此时s2中,right在底、right在中间、left在最上,依次弹出处理即实现后续遍历

public static void pos1(Node cur) {
    System.out.print("post1 order: ");
    Stack<Node> s1 = new Stack<>();
    Stack<Node> s2 = new Stack<>();
    s1.push(cur);
    while (!s1.isEmpty()) {
        cur = s1.pop();
        s2.push(cur);
        if (cur.left != null) {
            s1.push(cur.left);
        }
        if (cur.right != null) {
            s1.push(cur.right);
        }
    }

    while (!s2.isEmpty()) {
        System.out.print(s2.pop().value + " ");
    }
    System.out.println();
}

后续遍历-2

如果不借助另外一个栈结构,如何实现后续遍历? 使用两个变量:

  1. c 表示当前要处理的栈顶元素,首先定位到最左节点,左边界入栈;
  2. h 表示上一个处理过的节点,最开始在一个不干扰处理逻辑分支1的位置。

处理逻辑:

  1. 判断 c 的左子节点是否处理过 (h 不为 c.left, 且不为 c.right,则表示没处理过,将c.left 入栈)
  2. 判断 c 的右子节点是否处理过 (h 不为 c.right,则表示没处理过,将c.right 入栈)

左边界入栈完成后,弹出栈顶元素、并处理,此时用h标记一下当前位置,然后进入下一轮循环; c仍然取栈顶元素、判断与h(上一次处理元素)的关系,总会先处理左节点、然后因为h变为c的左子,后面会压入右子节点,右子节点完成后,才会定位到上一级,实现后续遍历的功能。

具体代码如下:

public static void pos2(Node cur) {
    if (cur == null) {
        return;
    }
    System.out.print("post2 order: ");
    Stack<Node> stack = new Stack<>();
    Node h = cur;
    Node c;
    stack.push(cur);
    while (!stack.isEmpty()) {
        c = stack.peek();
        if (c.left != null && c.left != h && c.right != h) {
            stack.push(c.left);
        } else if (c.right != null && c.right != h) {
            stack.push(c.right);
        } else {
            h = stack.pop();
            System.out.print(h.value + " ");
        }
    }

    System.out.println();
}

练习:非递归序实现leetcode94

在知道了如何通过非递归方式实现的方法后,我们可以在94题递归解法的基础上,更近一步:

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> out = new LinkedList<>();
    
    Stack<TreeNode> stack = new Stack<>();
    while (!stack.isEmpty() || root != null) {
        if (root != null) {
            stack.push(root);
            root = root.left;
        } else {
            root = stack.pop();
            out.add(root.val);
            root = root.right;
        }
    }
    return out;
}

Review

本周对 Guava-wiki 有了初步的学习,先看了Basic utilities、Collections中开头部分,对Guava的使用其实之前都是简单了解,这次希望能有一个详细的认识,熟悉好相关的工具,在实际工作中才能更有效的使用。

初步总结,见思维导图,后续会逐步完善。

Tip

在学习Guava的同时,第二遍看《EffectiveJava》这本书,上一次阅读没有好好记录笔记、总结,需要再详细看一遍,目前看完了第一章的内容。

Share

2022年1月27日:吴军老师、刘润老师视频号交流笔记,权且放在这里分享一下:

这次交流涉及三个大的主题:科技、商业、孩子教育,仅对部分印象深刻的内容整理。

  1. 科学是把钱变成知识,技术是把知识变成钱。

  2. 了解孩子的心智发育、发展 不同孩子,孩子的心理学,他能理解什么内容、认知水平随着年龄不同而不同。 小学一年级左右:同理心还不健全 11/12岁时,才会有抽象思维能力、归纳--举一反三

  3. 每个孩子都有自己的天赋 天赋三原色(三要素): 抽象 形象 语言

    长期发展:三个基础智力元素上的培养