热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

树、二叉树的前中后层序遍历(递归、非递归Java实现)

文章目录

文章目录

  • 1. 二叉树的前中后序遍历(递归,非递归)以及层序遍历
    • 1.1 二叉树前中后遍历的递归解法
    • 1.2 二叉树前中后遍历的非递归解法
      • 1.2.1 非递归的前序遍历:(LeetCode 144)
      • 1.2.2 非递归的中序遍历:(LeetCode 94)
      • 1.2.3 非递归的后序遍历:(LeetCode 145)
    • 1.3 层序遍历:(LeetCode 102、103、107)
  • 2 树的前中后层序遍历
    • 2.1 前序遍历:(LeetCode 589)
    • 2.2 中序遍历:
    • 2.3 后序遍历:(LeetCode 590)
    • 2.4 层序遍历:(LeetCode 429)

1. 二叉树的前中后序遍历(递归,非递归)以及层序遍历

1.1 二叉树前中后遍历的递归解法

树的前中后序的遍历这个是很常见的问题,其递归做法相对简单,这里直接贴代码:

// 前序遍历
public static void preorder(TreeNode treeNode) {
if (treeNode == null) return;
System.out.print(treeNode.val + " ");
preorder(treeNode.left);
preorder(treeNode.right);
}

// 中序遍历
public static void inorder(TreeNode treeNode) {
if (treeNode == null) return;
inorder(treeNode.left);
System.out.print(treeNode.val + " ");
inorder(treeNode.right);
}
// 后序遍历
public static void postorder(TreeNode treeNode) {
if (treeNode == null) return;
postorder(treeNode.left);
postorder(treeNode.right);
System.out.print(treeNode.val + " ");
}

从上面的代码中可以看到,树的前中后序遍历代码结构基本相同,差距只要在何时输出根值,前序遍历一遍历到节点时,先输出根植,中序是遍历完左子节点,后序是最后才输出,理解起来很容易。

1.2 二叉树前中后遍历的非递归解法

接下来我们来看看如何用非递归的方式,实现前中后序遍历:

1.2.1 非递归的前序遍历:(LeetCode 144)

在这里插入图片描述
前序遍历:34、35、29、25、33、42、40、39
根据树前序遍历的特点,我们很容易将其与栈的特点相结合,这里简单附上代码:

// 前序遍历的非递归解法
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> lists = new ArrayList<>();
if (root == null) return lists;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode temp = null;
while (!stack.isEmpty()) {
temp = stack.pop();
lists.add(temp.val);
// 这里注意,要先压入右子节点,再压入左节点
if (temp.right != null) {
stack.push(temp.right);
}
if (temp.left != null) {
stack.push(temp.left);
}
}
return lists;
}

1.2.2 非递归的中序遍历:(LeetCode 94)

在这里插入图片描述
中序遍历:25、29、35、33、34、40、42、39
这个实现的思路,是按照中序遍历的过程:先找到最左的子树,然后返回其父节点,然后遍历右子树,这个返回的动作可以让我们联想到使用栈这个数据结构,实现代码:

// 二叉树非递归的中序遍历
public List<Integer> inorderTraversal(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root, temp = null;
List<Integer> lists = new ArrayList<>();

// 判断条件:所有栈为空,且节点指向为空,即所有节点已经完成遍历
while (!stack.isEmpty() || node != null) {
// 向左搜索,寻找最左的节点,即中序遍历的第一个节点
while (node != null) {
stack.add(node);
node = node.left;
}
// 对每一个节点进行判断
if (!stack.empty()) {
// 获取当前节点
temp = stack.pop();
// 遍历该节点
lists.add(temp.val);
// 如果该节点为内部节点,则按中序遍历的顺序,遍历其右子节点
node = temp.right;
}
}
return lists;
}

1.2.3 非递归的后序遍历:(LeetCode 145)

非递归的后序遍历比较麻烦,但如果我们发现以下规律,就会变得简单:
例如:
在这里插入图片描述
对于后序遍历,结果:25、29、33、35、40、39、42、34
后序遍历的方法,就是先左子树,后右子树,再最后根节点,如果反过来,顺序就变成了:先根节点,后右子树,再左子树,是不是跟先序遍历有点像呢?就是先序遍历根节点遍历之后,下一个是遍历左子树,而下一个再遍历右子树。我们按这种反过来的顺序进行遍历,结果:34、42、39、40、35、33、29、25。很容易发现跟上面后序遍历的结果刚好相反。
因此,后序遍历可以变为:先遍历根节点,后遍历右子树,再遍历左子树,的结果的逆序,我们修改先序遍历的代码,可以得到:

public List<Integer> postorderTraversal_(TreeNode root) {
LinkedList<Integer> lists = new LinkedList<>();
if (root == null) return lists;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode temp = null;
while (!stack.isEmpty()) {
temp = stack.pop();
lists.addFirst(temp.val);
if (temp.left != null) {
stack.push(temp.left);
}
if (temp.right != null) {
stack.push(temp.right);
}
}
return lists;
}

这里使用了LinkedList,目的是为了让我们每一次插入结果,都可以插入到当前结果的最前面,相当于实现逆序的过程。

1.3 层序遍历:(LeetCode 102、103、107)

层序遍历,就是按照每一层的顺序,一层一层的访问,例如:
在这里插入图片描述
层序遍历的结果为:[[34], [35, 42], [29, 33, 40, 39], [25]]

// 根据层序遍历的特点,我们很容易想到使用队列,利用广度优先遍历的方式,进行遍历
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
List<List<Integer>> lists = new ArrayList<>();
List<Integer> arrays = new ArrayList<>();
TreeNode temp = null;
while (!queue.isEmpty()) {
int n = queue.size();
// 这里通过读取队列的元素,获取这一层有多少个元素
for (int i = 0; i < n; i++) {
temp = queue.poll();
arrays.add(temp.val);
if (temp.left != null) {
queue.add(temp.left);
}
if (temp.right != null) {
queue.add(temp.right);
}
}
// 将每一层的数据放入到链表中
lists.add(new ArrayList<>(arrays));
arrays.clear();
}
return lists;
}

2 树的前中后层序遍历

上面讲的都是二叉树,那对于一棵普通的树,其前中后序遍历,是如何呢:

2.1 前序遍历:(LeetCode 589)

// 递归实现
public List<Integer> preorder(Node root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
helper(result, root);
return result;
}
private void helper(List<Integer> list, Node root) {
list.add(root.val);
for (Node node: root.children
) {
helper(list, node);
}
}

// 非递归的N叉树前序遍历
public List<Integer> preorder(Node root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Stack<Node> stack = new Stack<>();
stack.add(root);
while (!stack.isEmpty()) {
Node temp = stack.pop();
result.add(temp.val);
// 注意这里要入栈的顺序是从右边最后一个子树开始
for (int i = temp.children.size() - 1; i >= 0; i--) {
stack.add(temp.children.get(i));
}
}
return result;

2.2 中序遍历:

发现 LeetCode 没有N叉树的中序遍历,关于中序遍历,我们要注意:先遍历最左的一个子树,然后根节点,然后才是遍历其他子节点。
在这里插入图片描述
遍历结果:5、3、6、1、2、4

2.3 后序遍历:(LeetCode 590)

在这里插入图片描述
后序遍历:5、6、3、2、4、1
规律:遍历每个节点,先遍历节点的子节点,最后输入根节点。
这里可以参考二叉树的后序遍历,也使用逆转考虑的方法,先遍历根节点,然后按逆序访问孩子节点,最后将结果进行逆序处理。

// 递归实现:
public List<Integer> postorder(Node root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
helper(result, root);
return result;
}
private void helper(List<Integer> list, Node root) {
for (Node node : root.children
) {
helper(list, node);
}
list.add(root.val);
}
// 非递归:
public List<Integer> postorder(Node root) {
List<Integer> list = new ArrayList<>();
if (root == null) return list;

Stack<Node> stack = new Stack<>();
stack.add(root);
while (!stack.isEmpty()) {
Node temp = stack.pop();
result.add(temp.val);
stack.addAll(temp.children);
}
// 将结果进行逆序
Collections.reverse(list);
return list;
}

2.4 层序遍历:(LeetCode 429)

// N叉树的层序遍历
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> lists = new ArrayList<>();
if (root == null) {
return lists;
}

Queue<Node> queue = new LinkedList<>();
queue.add(root);
List<Integer> list = new ArrayList<>();
Node temp = null;
while (!queue.isEmpty()) {
int levelSize = queue.size();
for (int i = 0; i < levelSize; i++) {
temp = queue.poll();
// 跟二叉树的层序遍历的区别在于,这里加入的是所有子树
queue.addAll(temp.children);
list.add(temp.val);
}
lists.add(new ArrayList<>(list));
list.clear();
}

return lists;
}

推荐阅读
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 栈和队列的共同处和不同处
    本文主要介绍了栈和队列的共同处和不同处。栈和队列都是由几个数据特性相同的元素组成的有限序列,也就是线性表。队列是限定仅在表的一端插入元素、在另一端删除元素的线性表,遵循先进先出的原则。栈是限定仅在表尾进行插入或删除操作的线性表,遵循后进先出的原则。 ... [详细]
  • EzPP 0.2发布,新增YAML布局渲染功能
    EzPP发布了0.2.1版本,新增了YAML布局渲染功能,可以将YAML文件渲染为图片,并且可以复用YAML作为模版,通过传递不同参数生成不同的图片。这个功能可以用于绘制Logo、封面或其他图片,让用户不需要安装或卸载Photoshop。文章还提供了一个入门例子,介绍了使用ezpp的基本渲染方法,以及如何使用canvas、text类元素、自定义字体等。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
author-avatar
wjb201212
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有