今天的练习的是一个新的数据结构:二叉树
这里我不太想去说一些比较规则正式的介绍了,简单说一下我觉得比较有用和算法题目相关的,因为东西挺多的,大家如果想更详细的了解二叉树,搜索一下其他大佬们的介绍!
二叉树的分类:
主要说一下我对满二叉树和完全二叉树的区别理解:
满二叉树是指所有的叶子节点位置都有数据
完全二叉树是指,在满二叉树的基础上,允许右分支为空,只需要满足左分支有节点即可
二叉树节点:
在开始二叉树算法之前,我们需要自己构建出一个二叉树,也就是其节点
节点需要三个属性,分别是:该节点的数据,左分支节点,右分支节点
代码如下:
public class TreeNode {
/*
二叉数的节点
节点包括三个部分:
1.该节点存储的数值
2.该节点的左分支节点
3.该节点的右分支节点
*/
public Integer val;
public TreeNode left;
public TreeNode right;
public TreeNode() {
}
public TreeNode(Integer val) {
this.val = val;
}
public TreeNode(Integer val, TreeNode leftNode, TreeNode rightNode) {
this.val = val;
this.left = leftNode;
this.right = rightNode;
}
}
如果大家跟着有困难,还是先去了解一下二叉树。
二叉树遍历(递归):
链接:前序遍历
首先我们需要知道如何去遍历一个二叉树,有两种方法,一种是递归法,另一种是迭代法
对于递归算法来说,我们需要三点:
1.停止的条件
2.每次递归时必要的参数,和返回值
3.每次进行递归中的处理逻辑
对于停止条件,也就是什么时候我们停止遍历二叉树,当然是当前遍历的节点为空时,停止本次的递归
其次是参数与返回值,参数就是下一个节点,当我们处理完一个节点后,是不需要再处理其他什么的,所以忽略返回值
最后就是执行递归的逻辑:
第一步,就是对停止条件进行判断,如果达到条件便停止
第二步,将本次遍历的节点数据添加到结果集中
第三步,开始递归,将本节点的左节点或者右节点当作参数传入
代码如下:
import java.util.ArrayList;
import java.util.List;
public class RecursiveTraversalOfBinaryTree {
/*
递归遍历二叉树
*/
/*
1.传入一个TreeNode,也就是根节点,新建一个数组,将其传入递归方法
2.新建一个递归方法,分析每个节点是否存在也就是判null,如果不为null,则证明是一个节点
将其存储的数值添加到数组中,然后取出其左分支节点和右分支节点,然后进行递归
*/
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
produceBefore(root,result);
return result;
}
//前序遍历
private void produceBefore(TreeNode root,List<Integer> result){
//判断node节点是否存在
if (root == null){
return;
}
result.add(root.val);
produceBefore(root.left,result);
produceBefore(root.right,result);
}
//中序遍历
private void produce(TreeNode root,List<Integer> result){
//判断node节点是否存在
if (root == null){
return;
}
produce(root.left,result);
result.add(root.val);
produce(root.right,result);
}
}
然后说一下,前序遍历,中序遍历,后序遍历的区别,就是根节点所在的位置
前序遍历:中左右
中序遍历:左中右
后序遍历:左右中
那么我们只需要改变递归逻辑就好了,root.left就是左,root.val就根节点,root.right就右,按照上述的顺序,对应写好,就完成了每个遍历顺序
二叉树遍历(迭代):
如果使用迭代法的话,需要借用另一个数据结构,栈
栈的主要目的就是存储已经遍历过节点,而递归实际上就是将当前遍历的节点作为参数传入,所以栈也可以实现遍历二叉树
那么对应的思路就是,当我们开始遍历时,将当前节点压入到栈中,在下一次迭代时,进行弹栈处理,如果是前序遍历,则先将右节点压入再压左节点,因为栈的特点是先进后出,只有将先弹出的节点后放入,才能优先出来。而后序遍历只需要改变一下顺序,先压左节点再压右节点,它的顺序就是:中右左,最后将该结果进行反转,变成左右中就好了
中序遍历与前后序遍历不同,因为前后序遍历最先处理的都是中,也就是根节点,而中序遍历是需要先处理叶子节点,所以我们的目标应该是遍历到叶子节点再开始进行处理
所以使用的是指针的方式,通过指针定位到我们想处理的节点,然后再利用入栈弹栈来进行对应的处理。首先将指针定位到根节点,然后开始遍历,如果指针不为空,那么就一直寻找节点的左节点,并将遍历到的节点全部压入到栈中,这样,当遍历到底部时,会触发弹栈,将最后一个左节点取出,添加到结果集,此时指针还是为空,因为该节点没有右分支呀,那么还会触发弹栈,会将该节点的根节点取出,进行结果集的添加,并处理该节点的右分支。
代码如下:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
public class IterativelyTraversingBinaryTree {
/*
迭代遍历二叉树
*/
/*
利用栈来进行遍历(前序遍历)(中左右)
1.将根节点压入到栈中,然后对栈进行判断,因为栈如果不为空,则证明还有其他节点
2.从根节点开始遍历,也就是将其弹出栈,然后添加到结果数组
3.判断其右分支是否有节点,如果有则压入栈中,左侧同理,因为栈是先进后出,所以先压右再压左,保证左先出
*/
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode pop = stack.pop();
result.add(pop.val);
if (pop.right != null) {
stack.push(pop.right);
}
if (pop.left != null) {
stack.push(pop.left);
}
}
return result;
}
/*
中序遍历(指针)
1.传入根节点,对根节点进行判断,如果为空则直接返回空数组
2.栈如果不为空则证明还有节点,新建一个指针,指向根节点
进入循环,如果栈为空,或者指针为空,则证明没有节点,否则一直找下去
3.一直遍历二叉树到左分支的叶子节点,并让指针指向该节点,下次进入循环,那么指针一定为空,进行弹栈
弹栈的节点为左分支的叶子节点,并将其添加到数组中,并将指针指向该节点的右分支节点
此时指针还是空,所以进行弹栈,这时节点为其父节点,添加到数组,然后指针指向其右分支节点,也就是右分支的叶子节点
*/
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
stack.push(cur);
cur = cur.left;
} else {
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}
/*
后序遍历(左右中) 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左
*/
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode pop = stack.pop();
result.add(pop.val);
if (pop.left != null){
stack.push(pop.left);
}
if (pop.right != null){
stack.push(pop.right);
}
}
Collections.reverse(result);
return result;
}
}
二叉树的层序遍历:
链接:层序遍历
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑
思路:利用队列存储每个节点,然后处理每个节点的左右分支,并保存在队列中
提前记录队列的size,当size为0也就是处理完本次的所有节点后,将数据放入结果集,然后开始下一次也就是下一层的处理
利用队列的先进先出
1.分为两个循环,一个是层级循环,一个是节点查找
2.在大循环,也就是层级循环时,创建数组,为该层的数据
判断队列的长度,如果大于0,则证明该层有节点,开始节点查找,进入小循环
将节点取出,队列长度减一,然后添加到该层的数组中,然后判断其左分支节点和右分支节点是否存在,如果存在则添加到队列里
并将这层的数组添加到结果里
代码如下:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class TraverseBinaryTreeHierarchy {
/*
二叉树的层级遍历
*/
/*
利用队列的先进先出
1.分为两个循环,一个是层级循环,一个是节点查找
2.在大循环,也就是层级循环时,创建数组,为该层的数据
判断队列的长度,如果大于0,则证明该层有节点,开始节点查找,进入小循环
将节点取出,队列长度减一,然后添加到该层的数组中,然后判断其左分支节点和右分支节点是否存在,如果存在则添加到队列里
并将这层的数组添加到结果里
*/
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
//每层
while (!queue.isEmpty()) {
List<Integer> list = new ArrayList<>();
int size = queue.size();
//每层的每个节点
while (size > 0){
TreeNode poll = queue.poll();
list.add(poll.val);
if (poll.left!=null){
queue.offer(poll.left);
}
if (poll.right!=null){
queue.offer(poll.right);
}
//已经处理完了该层的一个节点
size--;
}
//层的所有节点已经结束了
result.add(list);
}
return result;
}
}
二叉树的右视图:
链接:右视图
如果掌握了层序遍历,利用队列特性来处理节点,那么右视图也很简单了
刚开始我以为直接拿取所有右分支节点不好了,后面一想,没有右分支,那么左分支不就成了右视图的一部分,所以还是要把左分支考虑进去
那么在层级遍历的基础上,当我们把每层的节点压入到队列后,只需要拿取队列中的最后一个节点,就是右视图中的节点了,也就是size = 1时
代码如下:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class RightVewOfBinaryTree {
/*
二叉树的右视图
*/
/*
利用队列,首先将根节点放入到队列里,然后利用层遍历,取出节点,
判断是否有左右节点,然后添加到队列里,
如果是该层的最后一个节点,那么将其添加到队列里
*/
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
while (size > 0) {
TreeNode poll = queue.poll();
if (poll.left != null) {
queue.offer(poll.left);
}
if (poll.right != null) {
queue.offer(poll.right);
}
if (size == 1) {
result.add(poll.val);
}
size--;
}
}
return result;
}
}
二叉树的最大深度:
链接:最大深度
最大深度还是和层级遍历相联系,所谓最大深度变成层级遍历的问题就是,当队列的size为0时,达到最大深度
所以我们还是利用队列,和层级遍历,每当我们进行层遍历的时候,对最大深度加1
当队列为空时,返回最大深度即可
代码如下:
import java.util.LinkedList;
import java.util.Queue;
public class TheMaximumDepthOfBinaryTree {
/*
二叉树的最大深度
*/
/*
首先还是层遍历,当队列数量为空的时候,层的深度加1
*/
public int maxDepth(TreeNode root) {
int maxDepth = 0;
if (root == null) {
return maxDepth;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
maxDepth++;
while (size > 0){
TreeNode poll = queue.poll();
if (poll.left != null) {
queue.offer(poll.left);
}
if (poll.right != null) {
queue.offer(poll.right);
}
size--;
}
}
return maxDepth;
}
}
二叉树的直径:
这道题真的是简单题吗哈哈哈我觉得有点难度,中等偏上,这个递归开始让我没搞懂,那我来说说我的理解
首先是递归的三要素:终止条件,传递的参数和返回的结果,递归的逻辑
直径是左分支最大值 + 右分支最大值
那么终止条件一定是,当遍历到最底层的叶子节点是停止,证明遍历完毕
传递的参数应该是每次递归时所需要的节点,因为直径只考虑深度不需要考虑具体的节点值
对于返回值来说,当我们递归到树的最底层的叶子节点时,实际上才是计算的开始
也就是说,是从底部往上算的,那返回的应该是自增1,因为递归到底部的叶子节点,是终止条件,返回的是0,那么它的上一个节点再次返回,应该是1,再返回应该2......这是第一点
第二点就是需要考虑节点的左右分支,因为直径是根节点的左分支最长加右分支最长,而每个节点又有左分支和右分支,所以,只需要每个节点返回的左右分支中的最大值就好了
到了递归的逻辑,就是获取每个节点的左节点深度,和右节点深度,然后相加,然后与全局最大直径比较更新。
代码如下:
import java.util.LinkedList;
import java.util.Queue;
public class TheDiameterOfBinaryTree {
/*
二叉树的直径
*/
/*
计算每个节点的深度
直径 等于 左树加右树的深度
首先对节点进行判空处理,为空则返回0
然后计算节点左树和右树节点深度
更新直径的大小,判断当前直径大小 和 节点左树加右树的深度之和 的最大值
最后返回该节点的深度
*/
private int diameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
if (root == null){
return 0;
}
calculateDepth(root);
return diameter; // 返回全局最大直径
}
private int calculateDepth(TreeNode node) {
if (node == null){
return 0;
}
int left = calculateDepth(node.left);
int right = calculateDepth(node.right);
diameter = Math.max(left + right,diameter);
return Math.max(left,right);
}
}
从前序与中序遍历序列构造二叉树:
这道题我也是看了别的大佬们写的,才看懂的,大佬写的很详细,我在上面改进了一点,更多是看起来更详细吧,还有注释
参考:【算法】从前序与中序遍历序列构建二叉树_从前序与中序遍历序列构造二叉树-CSDN博客
代码如下:
import java.util.HashMap;
import java.util.Map;
public class ConstructBinaryTreeFromPreorderAndInorder {
/*
从前序与中序遍历序列构造二叉树
*/
/*
1.根据中序遍历来进行分割,首先将中序遍历的数组放入hashmap中,key为值,value为对应索引
2.利用递归构建二叉树,将前序遍历的数组,开始值,中序遍历数组的最大索引传入,因为需要分割中序遍历的数组
3.确定递归返回值: 为根节点 因为前序和中序遍历的都是同一棵树,所以对应的子树的节点数量也一样
首先根据前序遍历数组获取根节点,然后移动前序数组的指针
根据根节点对中序遍历数组进行分割,分成
*/
private Map<Integer, Integer> inorderIndexMap;
private int preorderIndex = 0;
public TreeNode buildTree(int[] preorder, int[] inorder) {
// 创建一个哈希表来快速查找中序遍历中元素的索引
inorderIndexMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderIndexMap.put(inorder[i], i);
}
return buildTreeHelper(preorder, 0, inorder.length - 1);
}
private TreeNode buildTreeHelper(int[] preorder, int inorderStart, int inorderEnd) {
if (inorderStart > inorderEnd) {
return null;
}
// 根节点是前序遍历中的第一个元素
int rootValue = preorder[preorderIndex];
preorderIndex++;
// 在中序遍历中找到根节点的索引
int rootIndex = inorderIndexMap.get(rootValue);
// 分割中序遍历数组
int leftSubtreeSize = rootIndex - 1;
int rightSubtreeSize = rootIndex + 1;
// 递归构建左子树和右子树
TreeNode root = new TreeNode(rootValue);
root.left = buildTreeHelper(preorder, inorderStart, leftSubtreeSize);
root.right = buildTreeHelper(preorder, rightSubtreeSize, inorderEnd);
return root;
}
}
今天的练习就到这里了!二叉树的题很多,我觉得最重要的就层序遍历那块了,掌握层序就会了好多类似的题,感谢大家!