【数据结构与算法】——二叉树
前言
二叉树是一种重要的数据存储结构,与二叉树相关的算法也有很多,本文简单介绍二叉树的先序遍历、中序遍历、后序遍历、层序遍历,四种遍历方式的递归及非递归解法;求解二叉树的高度;判断一棵二叉树是否为平衡二叉树 、完全二叉树 。本文涉及的二叉树定义为:
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. 而特殊二叉树的判断和求二叉树高度,则需要进行层序遍历,在层序遍历的基础之上加上各自特殊性质的逻辑判断即可完全求解。
- 点赞
- 收藏
- 关注作者
评论(0)