文章目录
本篇文章参考于左程云算法课程,部分详细讲解于左程云算法课程视频
二叉树的遍历
在解决二叉树问题时,很多时候需要对二叉树进行遍历,根据遍历的结果进而解决问题。因此,二叉树的遍历对解决二叉树相关问题显得尤为重要。二叉树的遍历方法大致分为三种:深度优先遍历、广度优先遍历、Morris遍历。
一、深度优先遍历
深度优先遍历分为:先序遍历、中序遍历、后序遍历,二对于这三种遍历方式又分别有递归形式与非递归形式的实现。
1. 先序遍历
先序遍历的流程为:
- 访问本节点
- 访问左孩子
- 访问右孩子
(1) 递归形式
public void preOrderRecur(Node head){
if(head==null) return;
System.out.print(head.value);
preOrderRecur(head.left);
preOrderRecur(head.right);
}
(2) 非递归形式
public void preOrderNotRecur(Node head){
if(head!=null){
Stack<Node> stack=new Stack<Node>();
stack.add(head);
while(!stack.isEmpty()){
head=stack.pop();
System.out.print(head.value);
if(head.right!=null) stack.push(head.riight);
if(head.left!=null) stack.push(head.left);
}
}
}
非递归形式先序遍历的主要思想为:
- 将本节点(头节点)入栈
- 栈顶元素出栈,记为curNode
- 将curNode右孩子入栈
- 将curNode左孩子入栈
- 循环入栈、出栈过程,直至栈空
2. 中序遍历
先序遍历的流程为:
- 访问左孩子
- 访问本节点
- 访问右孩子
(1) 递归形式
public void inOrderRecur(Node head){
if(head==null) return;
preOrderRecur(head.left);
System.out.print(head.value);
preOrderRecur(head.right);
}
(2) 非递归形式
public void inOrderNotRecur(Node root){
Stack<Node> stack=new Stack<>();
while (root!=null||!stack.isEmpty()){
if (root!=null){
stack.push(root);
root=root.left;
}else {
root=stack.pop();
System.out.println(root.val);
root=root.right;
}
}
}
非递归形式中序遍历的主要思想为:
若当前节点curNode非空,则入栈
不断将curNode左孩子,及其左子孙节点入栈,直至左子孙节点为空
当左子孙节点为空,栈顶元素出栈即为curNode
寻找curNode右子树
循环流程,直至栈空
3. 后序遍历
先序遍历的流程为:
- 访问左孩子
- 访问右孩子
- 访问本节点
(1) 递归形式
public void posOrderRecur(Node head)
if(head==null) return;
preOrderRecur(head.left);
preOrderRecur(head.right);
System.out.print(head.value);
}
(2) 非递归形式
public void posOrderNotRecur(Node root){
if (root!=null){
Stack<Node> stack=new Stack<>();
stack.push(root);
Node curNode=null;
while (!stack.isEmpty()){
curNode=stack.peek();
if (curNode.left!=null&&root!=curNode.left&&root!=curNode.right) stack.push(curNode.left);
else if (curNode.right!=null&&root!=curNode.right) stack.push(curNode.right);
else {
System.out.println(stack.pop().val);
root=curNode;
}
}
}
}
非递归形式后序遍历的主要思想为:
- 根节点入栈
- 若栈顶元素curNode左孩子不为空且最新出栈元素不是curNode的左右子元素
- curNode左孩子入栈
- 若栈顶元素curNode右孩子不为空且最新出栈元素不是粗人Node的右孩子
- curNode右孩子入栈
- 否则,栈顶元素出栈
- 循环入栈、出栈直至栈空
二、广度优先遍历
广度优先遍历即是按二叉树的层级遍历每个层级的元素
public void levelOrder(Node root){
if (root!=null){
Queue<Node> queue=new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()){
root=queue.poll();
System.out.println(root.val);
if (root.left!=null) queue.add(root.left);
if (root.right!=null) queue.add(root.right);
}
}
}
层序遍历的主要思想为:
- 根节点入队
- 队首元素出队记为curNode
- curNode左孩子不为空,将curNode左孩子入队
- curNode右孩子不为空,将curNode右孩子入队
- 循环入队、出队,直至队空
三、Morris遍历
Morris遍历相较与广度优先遍历和深度优先遍历,显著的降低了空间复杂度和时间复杂度。因此,当优化某个需将二叉树遍历遍历的算法时,可选择Morris遍历。由于Morris遍历时改变了某些节点的指向,因此该遍历方法不宜用于不能改变二叉树内容的问题。
public void morris(Node head){
if(head==null) return;
Node cur=head;
Node mostRight=null;
while(cur!=null){
mostRight=cur.left;//mostRight为cur的左孩子
if(mostRight!=null){//cur存在左子树
while(mostRight.right!=null&&mostRight.right!=cur){//获取左子树最右节点
mostRight=mostRight.right;
}
if(mostRight.right==null){//第一次到cur
mostRight.right=cur;
cur=cur.left;
continue;
}else{//mostRight.right==cur即是第二次到cur
mostRight.right=null;
}
}
cur=cur.right;
}
}
Morris遍历的本质实际为实现了线索二叉树。
Morris遍历的主要思想为:(假定cur为头节点)
- 如果cur没有左孩子,cur向右移动(cur=cur.right)
- 如果cur有左孩子,找到其左子树上最右的节点mostRight;
- 如果mostRight的右指针指向null,则让该节点右指针指向cur,然后cur向左移动(cur=cur.left)
- 如果mostRight的右指针指向cur,则让该节点右指针指向null,然后cur向右移动(cur=cur.right)
- cur为空时遍历停止
1. Morris先序遍历
public void morrisPreOrder(Node head){
if(head==null) return;
Node cur=head;
Node mostRight=null;
while(cur!=null){
mostRight=cur.left;//mostRight为cur的左孩子
if(mostRight!=null){//cur存在左子树
while(mostRight.right!=null&&mostRight.right!=cur){//获取左子树最右节点
mostRight=mostRight.right;
}
if(mostRight.right==null){//第一次到cur
System.out.print(cur.value);
mostRight.right=cur;
cur=cur.left;
continue;
}else{//mostRight.right==cur即是第二次到cur
mostRight.right=null;
}
}else{//cur不存在左子树
System.out.print(cur.value);
}
cur=cur.right;
}
}
Morris先序遍历的思想为:在Morris遍历的基础上,无论能够到达当前节点一次或两次,均在首次到达时打印。
2. Morris中序遍历
public void morrisInOrder(Node head){
if(head==null) return;
Node cur=head;
Node mostRight=null;
while(cur!=null){
mostRight=cur.left;//mostRight为cur的左孩子
if(mostRight!=null){//cur存在左子树
while(mostRight.right!=null&&mostRight.right!=cur){//获取左子树最右节点
mostRight=mostRight.right;
}
if(mostRight.right==null){//第一次到cur
mostRight.right=cur;
cur=cur.left;
continue;
}else{//mostRight.right==cur即是第二次到cur
mostRight.right=null;
}
}
System.out.print(cur.value);
cur=cur.right;
}
}
Morris先序遍历的思想为:在Morris遍历的基础上
- 当某个节点只能到达一次是,直接打印
- 当某个节点能到达两次时,在第二次到达时打印
3. Morris后序遍历
public void printEdge(Node X){//逆序打印以X为头的树的右边界
Node tail=reverseEdge(X);
Node cur=tail;
while(cur!=null){
System.out.print(cur.value);
cur=cur.right;
}
reverseEdge(tail);
}
public 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;
}
public void morrisPosOrder(Node head){
if(head==null) return;
Node cur=head;
Node mostRight=null;
while(cur!=null){
mostRight=cur.left;//mostRight为cur的左孩子
if(mostRight!=null){//cur存在左子树
while(mostRight.right!=null&&mostRight.right!=cur){//获取左子树最右节点
mostRight=mostRight.right;
}
if(mostRight.right==null){//第一次到cur
mostRight.right=cur;
cur=cur.left;
continue;
}else{//mostRight.right==cur即是第二次到cur
mostRight.right=null;
printEdge(cur.left);//逆序打印该节点左树的右边界
}
}
cur=cur.right;
}
printEdge(head);//打印整棵树的右边界
}
Morris先序遍历的思想为:在Morris遍历的基础上,仅当能到达两次的节点且为第二次到达该节点,则逆序打印该节点左树的右边界,最后打印整棵树的右边界。
二叉树问题解决思路
在解决二叉树的相关问题时,首先可以考虑是否可以通过二叉树的遍历解决,而后再考虑是否可以通过分解问题的思路解决(将问题分解为多个子问题,利用递归解决)。
一、 遍历解决
针对与本题,不难想到可以利用二叉树的层序遍历解决
public List<Integer> rightSideView(TreeNode root) {
List<Integer> list=new ArrayList<>();
Queue<TreeNode> queue=new LinkedList<>();
if (root==null) return list;
queue.add(root);
while (!queue.isEmpty()){
int size=queue.size();
for (int i=0;i<size;i++){
TreeNode curNode=queue.poll();
if (i==size-1){//该层最右侧元素
list.add(curNode.val);
}
if (curNode.left!=null) queue.add(curNode.left);
if (curNode.right!=null) queue.add(curNode.right);
}
}
return list;
}![]()
对于本题,不难想到可以利用二叉树的先序遍历解决
public void preOrder(TreeNode root,ArrayList<TreeNode> list){
if (root==null) return;
list.add(root);
preOrder(root.left,list);
preOrder(root.right,list);
}
public void flatten(TreeNode root) {
ArrayList<TreeNode> nodeList=new ArrayList<>();
preOrder(root,nodeList);
if (nodeList.size()>0) {
TreeNode curNode = nodeList.get(0);
for (int i = 1; i < nodeList.size(); i++) {
TreeNode temp = nodeList.get(i);
curNode.left = null;
curNode.right = temp;
curNode = temp;
}
}
}
二、非遍历问题解决模板
本模板并不适用于所有的二叉树问题。适用于再二叉树递归时,无需考虑左右子树的具体情况,只需对左右子树所提供的某一特定信息进行操作,并进行判断解决。
流程大致如下:
- 罗列问题可能的所有情况
- 罗列判断标准
- 确定所需信息
- 获取左右子树判断信息
- 对左右子树的信息进行判断
- 根据左右子树的信息结合本节点情况,返回本子树的信息
public void fun(){
process(root);
}
public InfoType process(TreeNode root){
if(root==null) {
//当节点为null时,返回的信息
}
InfoType left=process(root.left);
//从左子树获取信息
InfoType right=process(root.right);
//从右子树获取信息
//根据left、right信息对问题的标准进行判断
return new InfoType();
//根据左右子树的信息返回该节点的信息
}
以判断平衡二叉树为例:
- 罗列问题可能的所有情况
- 左右子树均为平衡二叉树,但左右子树高度差大于1
- 左右子树均不为平衡二叉树
- 左右子树仅一个为平衡二叉树
- 罗列判断标准
- 该节点左子树为平衡二叉树
- 该节点右子树为平衡二叉树
- 左右子树高度差小于等于1
- 确定所需信息
- 该子树是否为平衡二叉树
- 该子树的高度
- 获取左右子树判断信息
- 依据判断标准对左右子树的信息进行判断
- 根据左右子树的信息结合本节点情况,返回本子树的信息
class ReturnType{
public boolean isBalanced;
public int height;
public ReturnType(boolean isB,int hei){
isBalanced=isB;
height=hei;
}
}
public boolean isBalanced(TreeNode root){
return process(root).isBalanced;
}
public ReturnType process(TreeNode root){
if(root==null) return new ReturnType(true,0);
ReturnType leftData=process(root.left);
ReturnType rightData=process(root.right);
int height=Math.max(leftData.height,right.height)+1;
boolean isBalanced=leftData.isBalanced && rightData.isBalanced && Math.abs(leftData.height-right.height)<2;
return new ReturnType(isBalanced,height);
}
依据该模板对于本问题可以得出以下分析:
- 罗列问题可能的所有情况
- 左右子树均为二叉搜索树,且左右子树于本节点满足二叉搜索树要求
- 左右子树均为二叉搜索树,但左右子树于本节点不满足二叉搜索树要求
- 左右子树均不为二叉搜索树
- 罗列判断标准
- 左右子树均为二叉搜索树
- 左子树最大值小于本节点
- 右子树最小值大于本节点
- 确定所需信息
- 是否为二叉搜索树
- 左子树最大值
- 右子树最小值
- 获取左右子树判断信息
- 依据判断标准对左右子树的信息进行判断
- 根据左右子树的信息结合本节点情况,返回本子树的信息
public class Info{
public boolean isSearch;
public Long max;//左子树最大值
public Long min;//右子树最小值
public Info(boolean isSearch,Long max,Long min){
this.isSearch=isSearch;
this.max=max;
this.min=min;
}
}
public Info process(TreeNode root){
if (root==null) return new Info(true,Long.MIN_VALUE,Long.MAX_VALUE);
Info left=process(root.left);
Info right=process(root.right);
boolean isSearch=false;
Long max= Math.max(root.val, right.max);
Long min= Math.min(root.val, left.min);
if (left.isSearch&& right.isSearch&& root.val> left.max&&root.val< right.min){
isSearch=true;
return new Info(isSearch,max,min);
}else return new Info(isSearch,max,min);
}
public boolean isValidBST(TreeNode root) {
return process(root).isSearch;
}