【递归+迭代详解】二叉树的morris遍历、层序遍历、前序遍历、中序遍历、后序遍历

本文详细介绍了二叉树的四种遍历方法:层序遍历、前序遍历、中序遍历和后序遍历,包括递归和迭代两种实现方式,并分析了它们的特点和应用场景。此外,还探讨了Morris遍历法,一种利用空闲指针提高效率的空间复杂度为O(1)的遍历方法。
摘要由CSDN通过智能技术生成

目录

分析二叉树的前序,中序,后序的遍历步骤

1.层序遍历

方法一:广度优先搜索  (以下解释来自leetcode官方题解)

方法二:递归

2.前序遍历

3.中序遍历

4.后序遍历

递归解法

前序遍历--递归

中序遍历--递归

后序遍历--递归

三种递归遍历的总结:递归终止的条件为碰到空节点。

迭代解法

前序遍历--迭代

中序遍历--迭代

后序遍历--迭代

三种迭代解法的总结:

Morris遍历

morris--前序遍历

morris--中序遍历

morris--后序遍历:

分析二叉树的前序,中序,后序的遍历步骤

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后序遍历比较复杂,可以看看相关的视频讲解--左神算法系列。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值