你好,我是林森lsjs
我的Github 地址: sqyCoder (Qiyang) · GitHub
以博文记录成长,用心打磨代码与思维


目录
[2.1 满二叉树](#2.1 满二叉树)
[2.2 完全二叉树](#2.2 完全二叉树)
[5.1 先序遍历](#5.1 先序遍历)
[5.2 中序遍历](#5.2 中序遍历)
[5.3 后序遍历](#5.3 后序遍历)
[5.4 层序遍历](#5.4 层序遍历)
[7.1 获取树中节点的个数](#7.1 获取树中节点的个数)
[7.2 获取叶子节点的个数(遍历思路)](#7.2 获取叶子节点的个数(遍历思路))
[7.3 子问题思路‑求叶子结点个数](#7.3 子问题思路‑求叶子结点个数)
[7.4 获取第K层节点的个数](#7.4 获取第K层节点的个数)
[7.5 获取二叉树的高度(你的原有代码)](#7.5 获取二叉树的高度(你的原有代码))
[7.6 检测值为value的元素是否存在,存在则返回该节点](#7.6 检测值为value的元素是否存在,存在则返回该节点)
[7.7 层序遍历【递归版,你的原有代码】](#7.7 层序遍历【递归版,你的原有代码】)
[7.8 判断一棵树是不是完全二叉树](#7.8 判断一棵树是不是完全二叉树)
一、必会概念
A ←【根结点】层次1
/ \
B C ← B、C 互为【兄弟结点】,层次2
/ \ \
D E F ← D、E 兄弟;D、F / E、F 是【堂兄弟结点】,层次3
/
G ← 层次4
-
结点的度:结点拥有子结点数量。本例:A 度 = 2;B 度 = 2;C 度 = 1;D 度 = 0;E 度 = 1;F 度 = 0;G 度 = 0。
-
树的度:整棵树所有结点度的最大值。本例最大度是 2,树的度 = 2。
-
叶子结点 (终端结点):度为 0 的结点,没有任何子结点。图中 D、F、G 都是叶子。
-
双亲(父结点):直接向上相连的前驱结点。B 是 D、E 的父结点;A 是 B、C 的父结点。
-
子结点(孩子结点):D、E 是 B 的子结点;二叉树严格区分左孩子、右孩子。
-
根结点:A,唯一没有父结点的结点;一棵树有且仅有一个根。
-
结点的层次:规定根结点层次 = 1,向下逐层 + 1。A 层次 1;B、C 层次 2;D、E、F 层次 3;G 层次 4。
-
非 叶子结点 (分支结点):度>0,拥有子结点。本例:A、B、C、E。
-
兄弟结点:拥有同一个父结点的子结点。B 和 C;D 和 E。
-
树的高度 / 深度:树内结点层次的最大值。本树最大层次为 4,树高度 = 4。
-
堂兄弟结点:结点层次相同,父结点互为兄弟(父亲不是同一个)。D、E 的父亲是 B,F 父亲是 C,B、C 是兄弟,因此 D 与 F、E 与 F 互为堂兄弟。
-
结点的祖先(以 G 举例):从根到达该结点路径上所有上层结点(不含自身):E、B、A。
-
子孙(以 B 举例):该结点所有后代(所有子、孙子等):D、E、G。
-
森林:若干棵互不相交的树的集合。删掉根 A,剩余 B 子树、C 子树,两棵独立树构成一片森林。
二、两种特殊二叉树
2.1 满二叉树
一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。
也就是说,如果一棵二叉树的层数为K,且结点总数是,则它就是满二叉树。
2.2 完全二叉树
一棵二叉树,除了最后一层之外,其余每一层的节点都必须放满;最后一层的所有节点靠左紧密连续排列,不能出现空隙。
合法完全二叉树
java
1
/ \
2 3
/ \
4 5
层数 0:1 个节点(满)层数 1:2 个节点(满)层数 2:节点 4、5 靠左连续,右侧空缺,符合完全二叉树。
不是完全二叉树
java
1
/ \
2 3
\
5
最后一层节点 5 出现在右侧,左边 4 缺失,中间有空隙,不满足。
三、一些计算(面试基本不考)
-
根结点所在层次为1。对于非空二叉树,第i层最多有2的(i-1)次方个结点,其中i大于等于1。
-
根层次为1时,深度为K的二叉树,其结点总数最多为2的K次方减1。
-
任意一棵二叉树中,叶子结点(即度为0的结点)的数量等于度为2的结点数量加1。
-
对于有n个结点的完全二叉树,其深度k等于以2为底(n+1)的对数再向上取整。
-
完全二叉树若按从上到下、从左到右的顺序编号,且编号从0开始,则:
-
当i大于0时,结点i的父结点编号为(i-1)除以2后向下取整;
-
结点i的左孩子编号为2i+1,但只有当2i+1小于n时,该左孩子才实际存在;
-
结点i的右孩子编号为2i+2,但只有当2i+2小于n时,该右孩子才实际存在。
其中n为二叉树的总结点数。
四、二叉树的存储
java
class Node {
int val;
Node left;
Node right;
Node(int val) {
this.val = val;
left = null;
right = null;
}
}
每个节点保存数值、左孩子引用、右孩子引用;适合任意形态二叉树。
五、二叉树的遍历
5.1 先序遍历
java
public static void preOrder(TreeNode root) {
// 递归终止条件:空树直接返回
if (root == null) {
return;
}
// 先访问当前根节点
System.out.print(root.val + " ");
// 递归遍历左子树
preOrder(root.left);
// 递归遍历右子树
preOrder(root.right);
}
5.2 中序遍历
java
// 中序遍历
public static void inOrder(TreeNode root) {
if (root == null) {
return;
}
// 先递归左子树
inOrder(root.left);
// 左子树走完,再访问根节点
System.out.print(root.val + " ");
// 递归遍历右子树
inOrder(root.right);
}
5.3 后序遍历
java
// 后序遍历
public static void postOrder(TreeNode root) {
if (root == null) {
return;
}
// 先递归左子树
postOrder(root.left);
// 再递归右子树
postOrder(root.right);
// 左右全部遍历完成,最后访问根节点
System.out.print(root.val + " ");
}
5.4 层序遍历
java
public static void levelOrder(TreeNode root) {
// 空树直接结束
if (root == null) {
return;
}
// 引入队列保存节点
Queue<TreeNode> queue = new LinkedList<>();
// 根节点先入队
queue.offer(root);
// 队列不为空持续循环
while (!queue.isEmpty()) {
// 取出队首节点
TreeNode cur = queue.poll();
// 访问当前节点
System.out.println(cur.val);
// 左孩子不为空,入队列
if (cur.left != null) {
queue.offer(cur.left);
}
// 右孩子不为空,入队列
if (cur.right != null) {
queue.offer(cur.right);
}
}
}
六、二叉树还原
想要还原二叉树,先抓住核心规律:先序第一个是根,后序最后一个是根,中序里根左边是左子树、右边是右子树,所以必须带上中序才能唯一还原。
拿先序 1,2,4,5,3、中序 4,2,5,1,3 来说,先取出先序第一个 1 当作根,去中序找到 1,左边 4、2、5 是左子树,右边 3 是右子树。左子树一共 3 个节点,在先序里根往后数 3 个数字 2,4,5 就是左子树片段,剩下 3 是右子树片段。接着处理左子树片段,按照先序的规则,一段先序序列首位就是当前子树的根,因此首位 2 作为根,在对应中序找到 2,左侧 4、右侧 5 分别作为左右孩子,右子树只有 3,没有后代,树就构建完成。代码依靠区间边界递归,区间起点超过终点就返回 null,用左子树节点数量切割序列。
后序加中序思路完全一样,只是根取片段最后一个元素。而先序搭配后序不能还原,没办法区分节点是左孩子还是右孩子。
七、二叉树的常见方法
7.1 获取树中节点的个数
java
public int getNodeCount(TreeNode root) {
//递归终止条件,如果当前节点为null,节点数是0
if (root == null) {
return 0;
}
// 当前根节点算1个,再加上左子树全部节点,加上右子树全部节点
return 1 + getNodeCount(root.left) + getNodeCount(root.right);
}
我们来一步步看,当传入一棵树的根节点进来。第一步先判断root是不是null,如果是空树直接返回0。如果节点不为空,1代表当前这个根节点本身,然后交给递归去算左子树一共有多少节点,再算右子树一共有多少节点,三者相加就是整棵树总节点。递归会一直拆到叶子往下的null,返回0,然后一层层向上回溯累加算出总数。
7.2 获取叶子节点的个数(遍历思路)
java
private int leafCount = 0;
public int getLeafNodeCount(TreeNode root) {
//每次调用先把计数器清零,防止上一次调用残留数据
leafCount = 0;
getLeafCountByTraversal(root);
return leafCount;
}
private void getLeafCountByTraversal(TreeNode root) {
if (root == null) {
return;
}
//叶子节点:左右都为空
if (root.left == null && root.right == null) {
leafCount++;
}
//递归访问左子树
getLeafCountByTraversal(root.left);
//递归访问右子树
getLeafCountByTraversal(root.right);
}
入口方法进来第一件事把成员变量leafCount重置为0,避免上次调用的旧数据干扰结果。调用辅助递归函数开始遍历,遇到null直接return结束这一条分支。如果当前节点左右孩子全是null,这就是叶子节点,计数器就加1。之后继续递归往左走,再递归往右走,把整棵树全部节点遍历一遍。全部遍历结束之后,leafCount里面存的就是叶子总数量,返回给调用者。
7.3 子问题思路‑求叶子结点个数
java
public int getLeafNodeCountBySub(TreeNode root) {
//空树,叶子0个
if (root == null) {
return 0;
}
//当前就是叶子节点,返回1
if (root.left == null && root.right == null) {
return 1;
}
//当前不是叶子,结果等于左子树叶子数量 + 右子树叶子数量
return getLeafNodeCountBySub(root.left) + getLeafNodeCountBySub(root.right);
}
这个版本不用成员变量,纯靠递归子问题。如果节点为空返回0。如果当前节点左右都为空,说明自己就是叶子,直接返回1。如果不是叶子,自己不能算叶子,那整棵树叶子就全部来自左子树和右子树,把左右两棵子树算出来的叶子数相加,向上返回。递归不断拆小树,回溯的时候累加得到最终叶子总数。
7.4 获取第K层节点的个数
java
public int getKLevelNodeCount(TreeNode root, int k) {
//树为空 或者k是非法负数,直接返回0
if (root == null || k <= 0) {
return 0;
}
//k等于1,代表当前就是第一层,也就是传入的根节点,节点数为1
if (k == 1) {
return 1;
}
//求左子树k‑1层节点数量 + 右子树k‑1层节点数量
return getKLevelNodeCount(root.left, k - 1) + getKLevelNodeCount(root.right, k - 1);
}
首先判断边界,如果树是空,或者k传了小于等于0的非法值,直接返回0。当k==1的时候,代表现在来到目标层,当前这个节点就是目标层节点,返回1。否则,我要找当前树第k层,等价于去找左子树的k‑1层,加上右子树的k‑1层。每往下递归一层,k就减1,直到k变成1,统计节点,回溯相加。
7.5 获取二叉树的高度(你的原有代码)
java
public int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
if (root.left == null && root.right == null) {
return 1;
}
return 1 + Math.max(getHeight(root.left), getHeight(root.right));
}
第一步,如果root等于null,空树高度是0。如果当前是叶子节点,没有左右孩子,高度直接返回1。其他情况,1代表当前节点本身这一层,递归算出左子树高度,递归算出右子树高度,取两者更大的那一个,加上当前这一层,就是整棵树高度。递归一路走到最底下叶子,再回溯向上算高度。
7.6 检测值为value的元素是否存在,存在则返回该节点
java
public TreeNode find(TreeNode root, int val) {
//空树,找不到,返回null
if (root == null) {
return null;
}
//当前节点值匹配,直接返回这个节点对象
if (root.val == val) {
return root;
}
//先去左子树查找
TreeNode leftNode = find(root.left, val);
//左子树不为null,说明左边找到了,直接返回结果,不用再跑右边
if (leftNode != null) {
return leftNode;
}
//左子树没找到,才去右子树查找并返回
return find(root.right, val);
}
如果树为空直接返回null。先判断当前根节点是不是要找的值,如果匹配直接返回当前节点。不匹配,就先递归搜索左子树,拿到leftNode。如果leftNode不等于null,代表左子树里面已经找到了目标,直接把结果返回,不再执行右子树递归。只有左子树完全搜完没找到,才进入右子树递归查找。两边都找不到最终返回null。
7.7 层序遍历
java
private List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> levelOrder(TreeNode root) {
result.clear();
if (root == null) {
return result;
}
levelOrderHelper(root, 0);
return result;
}
public void levelOrderHelper(TreeNode root, int level) {
if (root == null) {
return;
}
if (result.size() == level) {
result.add(new ArrayList<>());
}
List<Integer> curRow = result.get(level);
curRow.add(root.val);
levelOrderHelper(root.left, level + 1);
levelOrderHelper(root.right, level + 1);
}
调用levelOrder入口方法,首先调用result.clear()把成员二维列表清空,防止上一次调用残留旧数据。root为空直接返回空集合。调用辅助函数,根节点层数传0。进入helper,如果root是空直接返回。判断result.size() == level:二维list的size代表已经存了多少层,如果当前层数等于集合大小,说明还没有这一层的容器,new一个ArrayList添加进去。拿到当前层的list,把节点的值add进去。然后递归左孩子,层数+1,再递归右孩子,层数+1。递归走完,二维list里面就是按层分组的遍历结果。
7.8 判断一棵树是不是完全二叉树
java
public boolean isCompleteTree(TreeNode root) {
if (root == null) {
return true;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean meetNull = false;
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
if (cur != null) {
if (meetNull) {
return false;
}
queue.offer(cur.left);
queue.offer(cur.right);
} else {
meetNull = true;
}
}
return true;
}
空树直接判定是完全二叉树。创建队列,根节点入队。布尔变量meetNull标记有没有已经遇到过null节点。循环队列不为空,取出队首元素cur。如果cur不是null,先看标记,如果meetNull已经是true,代表前面已经出现过空节点,现在又出现真实节点,说明中间有空缺,直接返回false。把当前节点的左右孩子全部入队,就算孩子是null也直接入队 。如果取出的cur是null,就把meetNull置为true,代表后面不能再有真实节点。队列全部处理完没有触发false,循环结束返回true,代表是完全二叉树。
今天的数据结构讲解就到这了,我们下期再见!
++诸位共勉!++
