目录
方法一:广度优先搜索 (以下解释来自leetcode官方题解)
分析二叉树的前序,中序,后序的遍历步骤
1.层序遍历
方法一:广度优先搜索 (迭代)
简化步骤:
初始化队列 q,并将根节点 root 加入到队列中;
当队列不为空时:
队列中弹出节点 node,加入到结果中;
如果左子树非空,左子树加入队列;
如果右子树非空,右子树加入队列;
由于题目要求每一层保存在一个子数组中,所以我们额外加入了 level 保存每层的遍历结果,并使用 for 循环来实现。
注意点:
终止条件:遍历结束的时候就是队列为空时;
节点完全访问结束:节点值已经被访问且该节点的左右子树也被加入队列。
队列中保存的恰好是即将要访问的这层队列的全部元素。
即将要访问的这层节点到底有多少个元素,恰好是当前队列的长度。
看个图例用以说明:
广度优先(迭代)需要用队列作为辅助结构,我们先将根节点放到队列中,然后不断遍历队列。
首先拿出根节点,如果左子树/右子树不为空,就将他们放入队列中。第一遍处理完后,根节点已经从队列中拿走了,而根节点的两个孩子已放入队列中了,现在队列中就有两个节点 2 和 5。
第二次处理,会将 2 和 5 这两个节点从队列中拿走,然后再将 2 和 5 的子节点放入队列中,现在队列中就有三个节点 3,4,6。
我们把每层遍历到的节点都放入到一个结果集中,最后返回这个结果集就可以了。
public List<List<Integer>> levelOrder(TreeNode root) {
//要返回的结果集合
List<List<Integer>> res = new ArrayList<>();
if(root == null){
return res;
}
//借助队列实现遍历过程
Deque<TreeNode> queue = new LinkedList<>();
queue.offer(root);
//每次进入循环时,队列中保存了即将处理的所有元素
while(!queue.isEmpty()){
//保存当前层的元素
List<Integer> level = new ArrayList<>();
//取出当前层的所有元素添加进level
int levelCount = queue.size();
// for(int i = 0;i < queue.size();i++){//这样会错
for(int i = 0;i < levelCount;i++){
//队列中弹出节点node,加入到结果中
TreeNode node = queue.poll();
level.add(node.val);
if(node.left != null){
queue.offer(node.left);
}
if(node.right != null){
queue.offer(node.right);
}
}
res.add(level);
}
return res;
}
方法二:递归
用广度优先处理是很直观的,可以想象成是一把刀横着切割了每一层,但是深度优先遍历就不那么直观了。
我们开下脑洞,把这个二叉树的样子调整一下,摆成一个田字形的样子。田字形的每一层就对应一个 list。
按照深度优先的处理顺序,会先访问节点 1,再访问节点 2,接着是节点 3。
之后是第二列的 4 和 5,最后是第三列的 6。
每次递归的时候都需要带一个 index(表示当前的层数),也就对应那个田字格子中的第几行,如果当前行对应的 list 不存在,就加入一个空 list 进去。
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
if(root==null) {
return res;
}
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
//将根节点放入队列中,然后不断遍历队列
queue.add(root);
while(queue.size()>0) {
//获取当前队列的长度,这个长度相当于 当前这一层的节点个数
int size = queue.size();
ArrayList<Integer> tmp = new ArrayList<Integer>();
//将队列中的元素都拿出来(也就是获取这一层的节点),放到临时list中
//如果节点的左/右子树不为空,也放入队列中
for(int i=0;i<size;++i) {
TreeNode t = queue.remove();
tmp.add(t.val);
if(t.left!=null) {
queue.add(t.left);
}
if(t.right!=null) {
queue.add(t.right);
}
}
//将临时list加入最终返回结果中
res.add(tmp);
}
return res;
}
2.前序遍历
2.1先输出当前节点(初始的时候是root节点)
2.2如果左子节点不为空,则递归继续前序遍历
2.3如果右子节点不为空,则递归继续前序遍历
3.中序遍历
3.1如果当前节点的左子节点不为空,则递归中序遍历
3.2输出当前节点
3.3如果当前节点的右子节点不为空,则递归中序遍历
4.后序遍历
4.1如果当前节点的左子节点不为空,则递归后序遍历
4.2如果当前节点的右子节点不为空,则递归后序遍历
4.3输出当前节点
规律总结:看输出父节点的顺序,就确定是前序、中序还是后序
如果你按照 根节点 -> 左孩子 -> 右孩子 的方式遍历,即「先序遍历」,每次先遍历根节点,遍历结果为 1 2 4 5 3 6 7;
同理,如果你按照 左孩子 -> 根节点 -> 右孩子 的方式遍历,即「中序序遍历」,遍历结果为 4 2 5 1 6 3 7;
如果你按照 左孩子 -> 右孩子 -> 根节点 的方式遍历,即「后序序遍历」,遍历结果为 4 5 2 6 7 3 1;
最后,层序遍历就是按照每一层从左向右的方式进行遍历,遍历结果为 1 2 3 4 5 6 7。
递归解法
由于层次遍历的递归解法不是主流,因此只介绍前三种的递归解法
前序遍历--递归
public List<Integer> preorderTraversal(TreeNode root) {
//递归
List<Integer> list = new ArrayList<>();
preOrder(root,list);
return list;
}
public void preOrder(TreeNode root,List<Integer> list){
if(root == null){
return;
}
list.add(root.val);
preOrder(root.left,list);
preOrder(root.right,list);
}
中序遍历--递归
public List<Integer> inorderTraversal(TreeNode root) {
//递归
List<Integer> list = new LinkedList<>();
inOrder(root,list);
return list;
}
public void inOrder(TreeNode root,List<Integer> list){
if(root == null){
return;
}
inOrder(root.left,list);
list.add(root.val);
inOrder(root.right,list);
}
后序遍历--递归
public List<Integer> postorderTraversal(TreeNode root) {
//递归
List<Integer> list = new LinkedList<>();
postOrder(root,list);
return list;
}
public void postOrder(TreeNode root,List<Integer> list){
if(root == null){
return;
}
postOrder(root.left,list);
postOrder(root.right,list);
list.add(root.val);
}
三种递归遍历的总结:递归终止的条件为碰到空节点。
迭代解法
前序遍历--迭代
核心思想:(在写三种遍历方式时,借助栈这个结构,保证做到不重不漏不出错。)
1.每拿到一个节点就把它保存在栈中
2.继续对这个节点的左子树重复过程1,直到左子树为空
3.因为保存在栈中的节点都遍历了左子树但是没有遍历右子树,所以对栈中节点出栈并对它的右子树重复过程1
4.直到遍历完所有节点
public List<Integer> preorderTraversal(TreeNode root) {
//迭代
List<Integer> list = new ArrayList<>();
if(root == null){
return list;
}
Deque<TreeNode> stack = new LinkedList<TreeNode>();
//临时节点,帮助遍历二叉树
TreeNode node = root;
//栈的作用是用来短暂的保存遍历节点的值,以助于最后值的返回
while(!stack.isEmpty() || node != null){
while(node != null){
list.add(node.val);
stack.push(node);
node = node.left;
}
node = stack.pop();
node = node.right;
}
return list;
}
中序遍历--迭代
public List<Integer> inorderTraversal(TreeNode root) {
//中序遍历:左根右,借助栈栈这个结构,第二次访问根节点时才能输出根节点值
//为了知道啥时候是第二次访问,引入了cur引用,从root开始一路向左子树走到头(null) - 第一次访问
//此时栈顶保存了最后一个没有左子树的节点(第二次访问)
List<Integer> list = new ArrayList<>();
if(root == null){
return list;
}
Deque<TreeNode> stack = new LinkedList<>();
//当前走到的节点
TreeNode cur = root;
while(cur != null || !stack.isEmpty()){
//不管三七二十一,先一路向左走到根儿~
while(cur != null){
stack.push(cur);
cur = cur.left;
}
//此时cur为空,说明走到了null,此时栈顶就存放了左树为空的节点
cur = stack.pop();
list.add(cur.val);
//继续访问右子树
cur = cur.right;
}
return list;
}
和前序遍历的代码完全相同,只是在出栈的时候才将父节点 的值加入到结果中。
后序遍历--迭代
public List<Integer> postorderTraversal(TreeNode root) {
//后序遍历:第三次访问根节点时才能输出根节点的值
// 为了知道啥时候是第三次访问(左树,右树都访问结束,再次回到根节点时,才叫第三次访问)
// 引入prev引用
List<Integer> res = new ArrayList<>();
if(root == null){
return res;
}
TreeNode cur = root;
Deque<TreeNode> stack = new ArrayDeque<>();
//上一个完全处理过的节点(左右根都处理完毕的节点)
TreeNode prev = null;
while(cur != null || !stack.isEmpty()){
//先一路向左走到最左
while(cur != null){
stack.push(cur);
cur = cur.left;
}
//此时左树为空,cur取栈顶元素,第二次访问
cur = stack.pop();
//判断右树是否为空或者被我们访问过(第三次访问root)
if(cur.right == null || prev == cur.right){
res.add(cur.val);
//当前节点cur就是最后处理的根节点,更新prev引用,变为cur
prev = cur;
// cur如果不置空:死循环重复遍历左子树
cur = null;
}else{
//此时右树不为空且没有处理过,需要把根节点再压入栈中,继续处理右子树
stack.push(cur);
cur = cur.right;
}
}
return res;
}
三种迭代解法的总结:
前序遍历和后序遍历之间的关系:
前序遍历顺序为:根 -> 左 -> 右
后序遍历顺序为:左 -> 右 -> 根
如果1: 将前序遍历中节点插入结果链表尾部的逻辑,修改为将节点插入结果链表的头部
那么结果链表就变为了:右 -> 左 -> 根
如果2: 将遍历的顺序由从左到右修改为从右到左,配合如果1
那么结果链表就变为了:左 -> 右 -> 根
这刚好是后序遍历的顺序
基于这两个思路,想一下如何处理:
修改前序遍历代码中,节点写入结果链表的代码,将插入队尾修改为插入队首
修改前序遍历代码中,每次先查看左节点再查看右节点的逻辑,变为先查看右节点再查看左节点
前序遍历和中序遍历之间的关系:
和前序遍历的代码完全相同,只是在出栈的时候才将父节点的值加入到结果中。
三种遍历的特点:
二叉树遍历的应用
中序遍历的应用:中序遍历二叉搜索树得到升序排列。
后序遍历的应用:后序在数学表达中被广泛使用。 编写程序来解析后缀表示法更为容易(后缀表达式)。
可以使用中序遍历轻松找出原始表达式。 但是程序处理这个表达式时并不容易,因为必须检查操作的优先级。如果想对这棵树进行后序遍历,使用栈来处理表达式会变得更加容易。 每遇到一个操作符,就可以从栈中弹出栈顶的两个元素,计算并将结果返回到栈中。
层序遍历的应用:广度优先搜索是一种广泛运用在树或图这类数据结构中,遍历或搜索的算法。 该算法从一个根节点开始,首先访问节点本身。 然后遍历它的相邻节点,其次遍历它的二级邻节点、三级邻节点,以此类推。
当我们在树中进行广度优先搜索时,我们访问的节点的顺序是按照层序遍历顺序的。
Morris遍历
遍历特点:Morris 遍历利用了树中大量空闲指针的特性
当前节点cur,一开始cur来到整树头
1)cur无左树,cur = cur.right(cur右移)
2)cur有左树,找到左树最右节点,mostright;此时我们又可以分为两种情况,一种是叶子节点添加 right 指针的情况,一种是去除叶子节点 right 指针的情况
A.mostright 的右指针指向null的mostright.right = cur, cur = cur.left(cur左移)
B.mostright 的右指针指向cur的mostright.right = null(为了防止重复执行,则需要去掉 right 指针), cur = cur.right(cur右移)
当cur == null时,整个过程结束。
遍历特点:有左树节点必遍历到两次,没有左树的节点必遍历到一次
public static void morris(Node head){
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while(cur != null){
//cur有没有左树
mostRight = cur.left;
if(mostRight != null){//有左树的情况
//找到cur左树上,真实的最右节点
//前者说明是第一次来到当前的cur,后者说明是第二次来到当前的cur
while(mostRight.right != null && mostRight.right != cur){
mostRight = mostRight.right;
}
//从while中出来,mostRight一定是cur左树上的最右节点
if(mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue;//结束的是外层的循环!!!!!!!!!!!!!
}else{//走到这里意味着:mostRight.right == cur
mostRight.right = null;
}
}
//cur没有左树
cur = cur.right;
}
}
空间复杂度:利用空闲的指针,使用了两个变量完成了遍历,空间复杂度是常数级别的
时间复杂度:
morris--前序遍历
第一次来到一个节点,就打印;第二次来到这个节点,不打印
public static void morrisPre(Node head){
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while(cur != null){
mostRight = cur.left;
if(mostRight != null){
while(mostRight.right != null && mostRight.right != cur){
mostRight = mostRight.right;
}
if(mostRight.right == null){
mostRight.right = cur;
System.out.print(cur.value + " ");
cur = cur.left;
continue;
}else{
mostRight.right = null;
}
}else{
System.out.print(cur.value + " ");
}
cur = cur.right;
}
System.out.println();
}
morris--中序遍历
对于能回到自己两次的节点,第二次时打印,对于只能来到自己一次的节点,直接打印
只要一个节点要往右移动,就打印
public static void morrisIn(Node head){
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while(cur != null){
mostRight = cur.left;
if(mostRight != null){
while(mostRight.right != null && mostRight.right != cur){
mostRight = mostRight.right;
}
if(mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue;
}else{
mostRight.right = null;
}
}
System.out.print(cur.value + " ");
cur = cur.right;
}
System.out.println();
}
morris--后序遍历:
public static void morrisPos(Node head) {
if(head == null) {
return;
}
Node cur = head;
Node mostRight = null;
while(cur != null) {
mostRight = cur.left;
if(mostRight != null) {
while(mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if(mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
continue;
}else {
mostRight.right = null;
printEdge(cur.left);//逆序打印左树的右边界
}
}
cur = cur.right;
}
printEdge(head);//最后打印整棵树的右边界
System.out.println();
}
public static void printEdge(Node head) {
Node tail = reverseEdge(head);
Node cur = tail;
while(cur != null) {
System.out.print(cur.value + " ");
cur = cur.right;
}
reverseEdge(tail);
}
private static Node reverseEdge(Node from) {
Node pre = null;
Node next = null;
while(from != null) {
next = from.right;
from.right = pre;
pre = from;
from = next;
}
return pre;
}
Morris后序遍历比较复杂,可以看看相关的视频讲解--左神算法系列。