整理:Java二叉树遍历(递归、迭代、Morris)原创图解+代码 |
您所在的位置:网站首页 › java二叉树层次遍历 › 整理:Java二叉树遍历(递归、迭代、Morris)原创图解+代码 |
本文不讨论二叉树层次遍历 刷题的时候看到一些二叉树遍历的解法,整理在这里作为笔记,也分享给大家 语言是 Java 的,我会采取代码+图解+说明形式来尽可能讲明白每种遍历方式 目录 一些准备 树节点类代码(TreeNode) 树节点类图解 工程文件结构 工程文件说明 递归解法(RecursiveTraversal) 递归解法复杂度 前序(递归) 中序(递归) 后序(递归) 迭代解法(IterativeTraversal) 迭代解法复杂度 前序(迭代,单层循环) 前序(迭代,双层循环) 中序(迭代,双层循环) 后序(迭代,单层循环) 后序(迭代,双层循环) 5种迭代解法图解 Morris 解法(MorrisTraversal) Morris 解法复杂度 前序(Morris) 中序(Morris) 一些准备为了便于整理和展示,我建立了一个简单的纯 Java 工程来测试各种解法 树节点类代码(TreeNode)一个简单的树节点类: public class TreeNode { int val; TreeNode left; TreeNode right; TreeNode() { } TreeNode(int val) { this.val = val; } TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; } } 树节点类图解这是一棵二叉树,它拥有四个树节点,左侧虚线框内是它的简化图 工程文件结构 工程文件说明 TreeNode:树节点类RecursiveTraversal:递归遍历二叉树的各种实现IterativeTraversal:迭代遍历二叉树的各种实现MorrisTraversal:Morris 遍历二叉树的各种实现 递归解法(RecursiveTraversal) 二叉树天然是一种递归结构,因此递归解法是最简洁的,代码量最少 简洁不一定可读性好;对于不熟悉递归思想的人来说,递归解的可读性并不高显而易见,递归解法总是会消耗大量栈空间 而且,Java 编译器不支持尾递归优化(这与 Java 语言的设计哲学和实现细节有关)另一方面,递归导致的大量函数调用,会产生一定不必要的开销 递归解法复杂度 时间复杂度空间复杂度O(n)O(n) 前序(递归) 根左右 public static void preOrder(TreeNode root) { if (root == null) return; System.out.print(root.val + " "); preOrder(root.left); preOrder(root.right); } 中序(递归)左根右 public static void inOrder(TreeNode root) { if (root == null) return; inOrder(root.left); System.out.print(root.val + " "); inOrder(root.right); } 后序(递归)左右根 public static void postOrder(TreeNode root) { if (root == null) return; postOrder(root.left); postOrder(root.right); System.out.print(root.val + " "); } 迭代解法(IterativeTraversal) 使用辅助数据结构(自定义栈)代替系统栈,有效降低栈空间消耗和溢出风险避免了大量函数调用所产生的开销代码复杂度相对较高 然而,这并不意味着迭代解法更难以理解 迭代解法复杂度 时间复杂度空间复杂度O(n)O(n) 前序(迭代,单层循环)需要区分的一点是: 递归中,进入的是子树迭代中,压入的是节点尽管子树和节点看起来都是一个 TreeNode,但它们之间存在一些区别! 另外,这里使用 LinkedList(双向链表),而非 Stack 因为 Stack 底层是基于数组实现的,对于频繁插入和删除的场景下,并不太适合 注意:选择数组或链表实现栈的优劣取决于二叉树的分布特点 对于单层循环方式,我们利用栈的后进先出特点,要先压入右子节点,再压入左子节点 public static void preOrder1(TreeNode root) { // 前序单层循环 if (root == null) return; Deque stack = new LinkedList(); stack.push(root); while (!stack.isEmpty()) { TreeNode node = stack.pop(); System.out.print(node.val + " "); if (node.right != null) stack.push(node.right); if (node.left != null) stack.push(node.left); } } 前序(迭代,双层循环)内循环中压入子树的所有左子节点 public static void preOrder2(TreeNode root) { // 前序双层循环 if (root == null) return; TreeNode node = root; Deque stack = new LinkedList(); while (!stack.isEmpty() || node != null) { while (node != null) { System.out.print(node.val + " "); stack.push(node); node = node.left; } node = stack.pop(); node = node.right; } } 中序(迭代,双层循环)对于单层循环方式,我们必须先把根节点压入栈中,所以它无法实现中序遍历 内循环中压入子树的所有左子节点 public static void inOrder(TreeNode root) { // 中序双层循环 if (root == null) return; TreeNode node = root; Deque stack = new LinkedList(); while (!stack.isEmpty() || node != null) { while (node != null) { stack.push(node); node = node.left; } node = stack.pop(); System.out.print(node.val + " "); node = node.right; } } 后序(迭代,单层循环)对于单层循环方式,后序遍历可以采用一个讨巧的办法:逆序输出 这需要额外的开销(对于下面的代码,既有辅助栈的空间开销,也有逆序的时间开销) public static void postOrder1(TreeNode root) { // 后序单层循环 if (root == null) return; Deque stack1 = new LinkedList(); Deque stack2 = new LinkedList(); stack1.push(root); while (!stack1.isEmpty()) { TreeNode node = stack1.pop(); stack2.push(node); if (node.left != null) stack1.push(node.left); if (node.right != null) stack1.push(node.right); } while (!stack2.isEmpty()) { System.out.print(stack2.pop().val + " "); } } 后序(迭代,双层循环)内循环中压入子树的所有左子节点 public static void postOrder2(TreeNode root) { // 后序双层循环 if (root == null) return; TreeNode node = root; TreeNode prev = null; Deque stack = new LinkedList(); while (!stack.isEmpty() || node != null) { while (node != null) { stack.push(node); node = node.left; } node = stack.pop(); if (node.right == null || node.right == prev) { System.out.print(node.val + " "); prev = node; node = null; } else { stack.push(node); node = node.right; } } } 5种迭代解法图解我绘制了5种解法的核心代码 N-S 图,以便于横向对比 Morris 解法(MorrisTraversal) 本质上,Morris 解法利用了二叉树节点中指向 null 的 left 和 right 属性(空闲指针),将原本的二叉树改造成临时线性结构,从而实现空间复杂度 O(1) 的遍历在传统的线索二叉树中,我们一般需要使用左右 tag 来标识指向的是左右节点还是前驱或后继节点;Morris 解法可以看作是一种不使用 tag 的线索化二叉树方法Morris 解法不能直接适用于后序遍历,需要额外的操作才能实现(本文不展示)Morris 解法在遍历过程中修改了树的结构,因此不是线程安全的 Morris 解法复杂度 时间复杂度空间复杂度O(n)O(1) 前序(Morris) public static void preOrder(TreeNode root) { if (root == null) return; TreeNode cur1 = root; TreeNode cur2 = null; while (cur1 != null) { cur2 = cur1.left; if (cur2 != null) { // 找到左子树的最右叶子节点 while (cur2.right != null && cur2.right != cur1) { cur2 = cur2.right; } if (cur2.right == null) { cur2.right = cur1; System.out.print(cur1.val + " "); cur1 = cur1.left; continue; } else { cur2.right = null; } } else { System.out.print(cur1.val + " "); } cur1 = cur1.right; } } 中序(Morris) public static void inOrder(TreeNode root) { if (root == null) return; TreeNode cur1 = root; TreeNode cur2 = null; while (cur1 != null) { cur2 = cur1.left; if (cur2 != null) { // 找到左子树的最右叶子节点 while (cur2.right != null && cur2.right != cur1) { cur2 = cur2.right; } if (cur2.right == null) { cur2.right = cur1; cur1 = cur1.left; continue; } else { cur2.right = null; } } System.out.print(cur1.val + " "); cur1 = cur1.right; } } |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |