【数据结构与算法】——二叉树

举报
雪月清 发表于 2022/05/19 10:32:00 2022/05/19
【摘要】 二叉树是一种重要的数据存储结构,与二叉树相关的算法也有很多,本文简单介绍二叉树的先序遍历、中序遍历、后序遍历、层序遍历,四种遍历方式的递归及非递归解法、求解二叉树的高度、判断一棵二叉树是否为平衡二叉树 、完全二叉树。

前言

二叉树是一种重要的数据存储结构,与二叉树相关的算法也有很多,本文简单介绍二叉树的先序遍历中序遍历后序遍历层序遍历,四种遍历方式的递归及非递归解法;求解二叉树的高度;判断一棵二叉树是否为平衡二叉树完全二叉树 。本文涉及的二叉树定义为:

 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;
     }
  }

一、二叉树的四种遍历

1.先序遍历

先序遍历也称前序遍历,遍历顺序为 根结点 -> 左子树 -> 右子树 简称 根左右
在这里插入图片描述
根据先序遍历的特点,先访问根结点然后访问根结点的左子树,访问左子树时左子树成为新的根结点继续重复先序遍历的过程,因此该二叉树的前序遍历顺序为 4 -> 2 -> 1 -> 3 -> 7 -> 6 ->9

递归解法

class Solution {
    //list集合存储二叉树遍历先序遍历结点的值
    List <Integer> list = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
          
          if(root == null) return list;
          //先访问结点,并将结点的值加入list集合
          list.add(root.val);
        //遍历根结点的左子树
          preorderTraversal(root.left);
       //遍历根结点的右子树
          preorderTraversal(root.right);

          return list;
    }
}

非递归解法

class Solution {
      public List<Integer> preorderTraversal(TreeNode root) {
            //list集合存储二叉树遍历先序遍历结点的值
      List<Integer> list = new ArrayList<Integer>();
      if (root == null) {
		return list;
	}
          //创建栈存储结点
      Stack<TreeNode> stack = new Stack<TreeNode>();
      TreeNode curNode = root;
          // curNode 指向正在访问的结点
      while(!stack.isEmpty() || curNode !=null) {

    	  if (curNode != null) {
              //遍历访问 压栈
              list.add(curNode.val);
			stack.push(curNode);
			//遍历左子树
              curNode = curNode.left; 
		}else {
              //左子树为空 栈顶结点出栈访问右子树
			curNode = stack.pop();
            curNode =curNode.right;
		} 
      }
      return list;
  }
}

2.中序遍历

中序遍历,遍历顺序为 左子树 -> 根结点 -> 右子树 简称 左根右
在这里插入图片描述
根据中序遍历的特点,一直遍历二叉树根结点的左子树,待左子树为空,再返回访问根结点,最后遍历访问右子树,因此该二叉树的中序遍历顺序为 1 -> 2 -> 3 ->4 ->6 ->7 ->9

递归解法

class Solution {
    //list集合存储二叉树中序遍历结点的值
    List<Integer> list = new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
     
     if(root == null) return list;
     //先一直遍历左子树
     inorderTraversal(root.left);
     //访问结点,并将结点的值加入list集合  
        list.add(root.val);
     //遍历右子树
     inorderTraversal(root.right);
     return list;

    }
}

非递归解法

class Solution {
    //list集合存储二叉树中序遍历结点的值
    public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> list = new ArrayList<Integer>();
		  if (root == null) {
			return list;
		}
        //创建栈存储结点
		  Stack< TreeNode> stack = new Stack<TreeNode>();
		  TreeNode curNode =root;
        //临时结点指向正在访问的结点
		  while(!stack.isEmpty() || curNode != null ) {
			  if(curNode != null) {
                  //一直向左子树遍历压栈
				  stack.push(curNode);
				curNode = curNode.left;  
			  }else {
                  //左子树为空 栈顶结点出栈访问 随后curNode指向右节点
				  curNode = stack.pop();
				  list.add(curNode.val);
				  curNode = curNode.right;
			  }					  
		  }
		  return list;
    }
}

3.后序遍历

后序遍历,遍历顺序为 左子树 -> 右子树 -> 根结点 简称 左右根
在这里插入图片描述
根据后序遍历的特点,一直遍历二叉树根结点的左子树,左子树为空时再去遍历访问右子树,待右子树也为空时,才醉后遍历访问根结点,该二叉树后序遍历结果为 1 -> 3 -> 2 -> 6 -> 9 ->7 -> 4

递归解法

class Solution {
    //list集合存储二叉树后序遍历结点的值
    List <Integer> list = new ArrayList<>();
  public List<Integer> postorderTraversal(TreeNode root) {
      if(root == null) return list;
      //遍历左子树
    postorderTraversal(root.left);
    //遍历右子树
    postorderTraversal(root.right);
 // 访问结点,并将结点的值加入list集合
    list.add(root.val);
    return list;
    }
}

非递归解法

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        //list集合存储二叉树后序遍历结点的值
         List <Integer> list = new ArrayList<>();
         //创建栈存储结点
         Stack<TreeNode> stack = new Stack();
        //临时结点 cur 指向当前访问结点
           TreeNode cur = root;
        //临时结点 pre 指向上一次访问的结点
           TreeNode pre = null;
     while(!stack.isEmpty() || cur != null){
        while(cur != null){
            stack.push(cur);
            cur = cur.left;
        }
    //cur 指向栈顶结点 但不出栈 此时cur 的左子树为空
        cur = stack.peek();
        if(cur.right == null || cur.right == pre){
        //如果cur 的右子树也为空 或者 cur的右子树已经访问过了,
        //则栈顶结点真正出栈 pre指向栈顶结点也就是要访问的结点进行记录  
        //最后cur所指向的栈顶结点已经访问过了,cur置为空
            pre = stack.pop();
            list.add(pre.val);
           cur = null;
       
        }else{
            //右节点不为空 且右子树没访问过,cur就遍历右子树
            cur = cur.right;
        }
    }
 return list;
  
    }
}

pre指向上一次访问结点的作用: 由于后序遍历是 左右根 只有遍历了根结点才知晓左子树,左子树为空时,再遍历根结点然后访问右子树。只有左右子树都为空或者已经访问过,才访问根结点。所以根结点遍历了两次,且根结点右子树为空或者已经访问过,才访问根结点。


4. 层序遍历

层序遍历,遍历顺序为一层层进行遍历,层序遍历比较符合我们平时直观上的遍历。

在这里插入图片描述
根据层序遍历的特点,该二叉树层序遍历的结果为 4 -> 2 -> 7 ->1 -> 3 ->6 ->9 ,根据层序遍历的先后顺序我们可以观察到,每次访问一个结点就要记录这个结点的左右子树,待本层访问结束就会按照记录的先后顺序继续访问下一层,符合队列的先进先出特点,因此我们用队列进行辅助遍历。

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
		//记录二叉树某一层结点个数,从根结点 1 开始
		 int levelSize = 1;
		   List<List<Integer>> res = new ArrayList<List<Integer>>();
         if(root == null) return res;
        //list集合存储每一层的结点的值
		   List<Integer> list = new ArrayList<Integer>();
		   //创建队列 存储结点
		   Queue<TreeNode> queue =new LinkedList<>();
       //根结点入队
		   queue.offer(root);
        //层序遍历
		   while(!queue.isEmpty()) {
		   //队头结点出队
			   TreeNode node = queue.poll();
			   //本层结点数 -1
			   levelSize--;
			   list.add(node.val);
			   if(node.left != null) {
				   queue.offer(node.left);
			   }
			   if(node.right != null) {
				   queue.offer(node.right);
			   }
          //每一层访问结束 数的高度 + 1  levelSize等于下一层的结点个数
			  if(levelSize == 0) {
				  res.add(list);
				  levelSize = queue.size();
                   list = new ArrayList<Integer>();
			  }   
		   }
		   return res;
		    }
}

二叉树前序、中序、后序、层序遍历 时间复杂度为 O(n) , n为二叉树结点个数。
空间复杂度如果单是遍历,前、中、后序遍历都是O(h), h为二叉树高度,但是用list集合存储遍历结果 所以空间复杂度为O(n), n为二叉树结点个数,层序遍历空间复杂度为O(n) 。


二、二叉树的高度及特殊二叉树判断

1. 二叉树的高度

求解二叉树的高度,即为求根结点的高度,方法分为递归与非递归两种

递归解法

public int height(TreeNode root) {
		if(root == null)
			return 0;
		return 1 + Math.max(height(root.left), height(root.right));
	}

非递归解法

public int  height(TreeNode root) {
    if(root == null)
			return 0;
		//记录二叉树的高度
		int height = 0;
		//记录二叉树某一层结点个数,从根结点 1 开始
		int  levelSize = 1;
		Queue<TreeNode> queue = new LinkedList<>();
		//根结点入队
		queue.offer(root);
		//层序遍历
		while(!queue.isEmpty()) {
			TreeNode node = queue.poll();
			levelSize--;
			if(node.left != null) {
				queue.offer(node.left);
			}
			if(node.right != null) {
				queue.offer(node.right);
			}
			//每一层访问结束 数的高度 + 1  levelSize等于下一层的结点个数
			if(levelSize == 0) {
				height++;
				levelSize = queue.size();
			}
		}
		return height;
	}

时间复杂度:O(n) 空间复杂度:O(n) n为二叉树结点个数


2.判断一棵二叉树是否为平衡二叉树

平衡二叉树 : 二叉树中每个节点 的左右两个子树的高度差的绝对值不超过 1

采用类似后序遍历的递归方式,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断当前结点的根结点是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回 -1。如果存在一棵子树不平衡,则整个二叉树一定不平衡。

class Solution {
    public boolean isBalanced(TreeNode root) {
   return height(root) >=0 ;
    }

    public int height(TreeNode node){
        if(node == null)
        return 0;
        //遍历左子树,遍历右子树
      int l = height(node.left), r = height(node.right);
      //如果此结点的子树是平衡的,返回此结点的高度 左右子树最大高度 + 1 
      if(l >= 0 && r >=0 && Math.abs(l-r) <= 1 ){
          return Math.max(l,r) + 1;
      }else{
          return -1;
      }
    }
}

时间复杂度:O(n) 空间复杂度:O(n) n为二叉树结点个数


3.判断一棵二叉树是否为完全二叉树

完全二叉树:

public boolean isComplete(TreeNode root) {
		if(root == null)
			return false;
		
		boolean leaf = false;
		Queue<TreeNode> queue = new LinkedList<>();
		//根结点入队
		queue.offer(root);
		//层序遍历
		while(!queue.isEmpty()) {
			
			TreeNode node = queue.poll();
			//此节点应该是叶子结点,但是实际如果不是叶子结点 ,leaf为true 说明这个二叉树不是完全二叉树
			if(leaf && (node.left != null || node.right !=null)) {
				return false;
			}
			
		  if (node.left != null) {
			queue.offer(node.left);
		}else if(node.right == null) {
			//左结点为空,右结点不为空 二叉树一定不是完全二叉树
			return false;
		}
		  
		  if (node.right != null) {
			queue.offer(node.right);
		}else {
			//左结点为空或者左结点不为空 && 右结点一定为空  若是完全二叉树,此结点应该为叶子结点
			leaf = true;
		}
	}
		return true;
   }

时间复杂度:O(n) 空间复杂度:O(n) n为二叉树结点个数


总结

1. 二叉树的前序、中序、后序遍历的非递归解法,观察发现其实是一个模板,根据这个模板结合三种遍历各自特有的性质,就可以写出三种遍历的非递归代码。


if(root == null)
    return;
//临时结点cur指向当前访问的结点
TreeNode curNode = root;

//后序遍历需要再加一个临时结点指向上一次遍历的结点
while(栈非空 || curNode != null)
{
    if(curNode != null){
        
    }else{
        
    }
    
}

2. 而特殊二叉树的判断和求二叉树高度,则需要进行层序遍历,在层序遍历的基础之上加上各自特殊性质的逻辑判断即可完全求解。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。