二叉树的相关算法题无疑是笔试、面试中考察最频繁的知识点了,所以在你准备笔试、面试前一定要对其进行掌握。本篇文章主要介绍几个二叉树相关的基础算法
,同样这也是我们务必要理解掌握的,并且要能写出来的。
值得说明的是,看懂这几个算法的实现,你要具备二叉树的一些基础知识
,比如:什么是二叉树的广度优先搜索、深度搜索,什么是先序遍历、中序遍历以及后序遍历,什么是二叉树的深度、宽度以及二叉树的叶子节点是怎么定义的。
其次,你最好也具备递归算法
的思想,因为二叉树相关的好多算法都运用了递归的思想,所以你要是理解什么是递归,对看懂这篇文章有极大的帮助。
广度优先搜索
二叉树的广度优先搜索(Breadth First Search,BFS)通常是借助队列
实现的。
根据返回值类型的不同,这里给了两种形式:
public class BFS {
// 第一种:以数组的方式返回结果
public int[] levelOrder1(TreeNode root) {
// 如果根节点为空,返回空数组
if (root == null) return new int[0];
// 声明一个队列,用来存储树的节点
Queue<TreeNode> queue = new LinkedList<>();
// 声明一个链表,用来存储节点值
ArrayList<Integer> ans = new ArrayList<>();
// 根节点进队列
queue.add(root);
// !queue.isEmpty()
// 但队列不为空时,继续访问队列
while (queue.size() != 0) {
// 取出下一个要访问的节点,称为当前节点
TreeNode curr = queue.poll();
// 访问当前节点的值,并添加到ans链表中
ans.add(curr.val);
// 向队列中添加左右子树节点
// 先添加左子树
if (curr.left != null) queue.add(curr.left);
// 再添加右子树
if (curr.right != null) queue.add(curr.right);
}
// 下面代码的作用是将链表ans的结果传给数组,并返回。
int size = ans.size();
int[] result = new int[size];
for (int i = 0; i < size; i++) {
result[i] = ans.get(i);
}
return result;
}
// 第二种:以List集合返回 层先方法
public List<List<Integer>> levelOrder2(TreeNode root) {
// 这里先创建一个结果链表
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> nodeQueue = new LinkedList<>();
nodeQueue.add(root);
while(!nodeQueue.isEmpty()) {
//这里是每一层创建一个链表,用来存放当前一层的节点值
ArrayList<Integer> ans = new ArrayList<>();
// 循环当前队列的所有节点,即为树的该层所有的节点
for (int i = nodeQueue.size(); i > 0; i--) {
TreeNode curr = nodeQueue.poll();
ans.add(curr.val);
// 这里相当于是把下一层节点放入队列中
if (curr.left != null) nodeQueue.add(curr.left);
if (curr.right != null) nodeQueue.add(curr.right);
}
// 将存放每一层的节点值的链表添加到结果链表中
res.add(ans);
}
// 直接返回结果
return res;
}
}
深度优先搜索
二叉树的深度优先搜索(Depth First Search,DFS)按照不同的遍历顺序可以分为:先序遍历、中序遍历以及后序遍历。
不管那种遍历方式都可以借助栈
来实现。
先序遍历
很好理解,代码中给出了详细的备注解释。中序遍历
稍微有点难理解,但是只要你深刻理解了中序遍历的访问顺序,再看代码的实现是不难的。后序遍历
是在先序遍历
的基础上,加以修改:- 首先,先序遍历是:中左右
- 然后,调换下顺序:中右左(这个很容易实现,只需要调整左右子树的访问顺序即可)
- 最后,反转下数组:左右中(这正是
后序遍历
的顺序)
public class DFS {
// 1、先序遍历
public int[] preorderTraversal(TreeNode root) {
// 如果根节点为空,返回空数组
if (root == null) return new int[0];
// 声明一个栈,用来存放各节点
Stack<TreeNode> stack = new Stack<>();
// 声明一个链表,用来存放各节点的值
ArrayList<Integer> ans = new ArrayList<>();
// 根节点压栈
stack.push(root);
// 当栈不为空时,证明树的节点还未访问完,继续访问
while (!stack.isEmpty()) {
// 当前节点弹栈
TreeNode node = stack.pop();
// 访问当前节点的值,并添加到链表中年
ans.add(node.val);
// 先压栈右节点,再压栈左节点
// 为什么先压右节点,再压左节点?这里自己动手画一画流程图就清楚明白了
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
// 下面代码的作用是将链表ans的结果传给数组,并返回。
int size = ans.size();
int[] result = new int[size];
for (int i = 0; i < size; i++) {
result[i] = ans.get(i);
}
return result;
}
// 2、中序遍历
public int[] inorderTraversal(TreeNode root) {
if (root == null) return new int[0];
Stack<TreeNode> stack = new Stack<>();
ArrayList<Integer> ans = new ArrayList<>();
// 中序遍历,需要对当前节点进行判断,所以先声明一个当前节点
TreeNode curr = root;
// 如果当前节点不为空,或者栈不为空,都进入循环,为什么?
// 第一次是因为栈空,但当前节点为root不为空
// 中间两者都满足,都不为空
// 当到叶子节点时,当前节点为空,但是栈不为空
// 直到访问了最后一个叶子节点,两个都为空了,也就结束了
while (curr != null || !stack.isEmpty()) {
if (curr != null){
// 当前节点不为空,压栈
// 这里是因为中序遍历,所以要先访问左子树的最后一个节点
// 所以要先压栈,不能访问
stack.push(curr);
// 当前节点压栈以后,指向自己的左子树
curr = curr.left;
} else {
// 如果当前节点为空了,证明到了叶子节点,
// 这时在栈中弹出最顶的节点,即为最左的一个节点
curr = stack.pop();
// 访问节点,并将值添加链表中
ans.add(curr.val);
// 指向右子树
curr = curr.right;
}
}
// 下面代码的作用是将链表ans的结果传给数组,并返回。
int size = ans.size();
int[] result = new int[size];
for (int i = 0; i < size; i++) {
result[i] = ans.get(i);
}
return result;
}
// 3、后序遍历
// 明白了先序遍历,后序遍历就简单跟多了
public int[] postorderTraversal(TreeNode root) {
if (root == null) return new int[0];
Stack<TreeNode> stack = new Stack<>();
ArrayList<Integer> temp = new ArrayList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
temp.add(node.val);
if (node.left != null) stack.push(node.left);
if (node.right != null) stack.push(node.right);
}
int size = temp.size();
int[] result = new int[size];
for (int i = 0; i < size; i++) {
// 反向赋值
result[i] = temp.get(size - i - 1);
}
return result;
}
}
二叉树的宽度
二叉树的最大宽度可以基于广度优先搜索
实现,话不多说,直接上代码:
public class Solution {
public int widthOfBinaryTree(TreeNode root) {
// 不说了
if(root == null) return 0;
// 用来存放树的节点
Queue<TreeNode> nodeQueue = new LinkedList<>();
// 记录最大的宽度
int width = 0;
nodeQueue.add(root);
while (!nodeQueue.isEmpty()) {
// 重点就是这一句代码
// 每次对比最大值和该层的宽度,选择两者的最大值
width = Math.max(nodeQueue.size(), width);
for (int i = nodeQueue.size(); i > 0; i--) {
TreeNode curr = nodeQueue.poll();
// 左右子树添加到队列
if (curr.left != null) nodeQueue.add(curr.left);
if (curr.right != null) nodeQueue.add(curr.right);
}
}
return width;
}
}
二叉树的深度
二叉树的最大深度可以基于递归的思想
实现,直接上代码:
public class Solution1 {
// 利用深度优先 - 迭代法
public int maxDepth(TreeNode root) {
// 如果根节点为空,直接返回0
if (root == null) return 0;
// 根节点不为空,那就遍历左右子树,求左右子树的最大值
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
// 最后返回的就是左右子树高度的最大值+1
return Math.max(leftHeight, rightHeight) + 1;
}
}
二叉树的叶子节点数
二叉树的叶子节点数也可以基于递归的思想
实现,上代码:
public class Solution {
public int countOfLeaf(TreeNode root) {
if (root == null) return 0;
// 这里不管是||还是&&,都是可以实现的
// 需要思考一下,为什么?
// if (root.left == null && root.right == null) return 1;
if (root.left == null || root.right == null) return 1;
// 返回左右子树的叶子节点数的和
return countOfLeaf(root.left) + countOfLeaf(root.right);
}
}
总结:这里仅介绍了二叉树中最基础的算法,当然这也是你进阶二叉树其他算法的基础,请务必要理解掌握。同时实现上算法的方式很多,你可以有其他的方式实现,相信自己。