树的概念及树的深度
基础概念
“
树
” ——
递归的数据结构
由根节点和它的孩子,以及孩子的孩子等等组成
其中没有子节点的节点 叫做叶子节点。如 EFCG ,都是叶子节点。节点的度:指拥有的子节点个数。如 A 的度是 3 , B 的度是 2 , D 的度是 1 ,叶子节点的度都是 0 。节点的关系:子节点是父节点的孩子节点,父节点也是子节点的双亲节点。有相同父节点的节点为兄弟节点。如 A 是 B 的双亲节点, B 是 A 的孩子节点, B 和 C 是兄弟节点。节点的层次: A 在第一层, BCD 在第二层, EFG 在第三层树的深度:是最大层次数,即为 3 。
“
二叉树
”
,最多可以分出两个叉的树。
https://leetcode-cn.com/problems/er-cha-shu-de-shen-du-lcof/
剑指 Offffer 55 - I. 二叉树的深度难度简单 59输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。例如:给定二叉树 [3,9,20,null,null,15,7]
3/ \9 20/ \15 7返回它的最大深度 3 。
public static int maxDepth(TreeNode root) {
// 树的深度 左子树的深度和右子树的深度中 更大的值 +1
if (root == null) return 0;
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
二叉树的分类和比较
二叉树的分类
满二叉树
所有叶子节点都在同一层 所有分支节点(非叶子节点)都有左右子树每一层的节点个数 (i 表示层数) 2^ ( i-1 )个节点对于深度为 h 的树,共有节点 2^h - 1
斜树
所有的子节点都向一个方向倾斜,只分叉出左子树或右子树
完全二叉树
对于高度
/
深度为
h
的二叉树而言,
h-1
层都是满的,
h
层节点在左侧连续排列,空位都在右侧。
满二叉树一定是完全二叉树,反之未必。
将完全二叉树的节点进行编号,对于编号为
k
的节点:
1
) 父节点就是
k/2
2)
如果有孩子节点,先有左孩子
2k
,后有右孩子
2k+1
【构造出完全二叉树】
// 0 1 2 3 4
// 2k+1 2k+2
private static int[] array = {1, 2, 3, 4, 5};
// 存储生成的节点
private static List<TreeNode> nodeList = new LinkedList<>();
public static TreeNode createTree() {
// 构造节点
for (int i = 0; i < array.length; i++) {
TreeNode node = new TreeNode(array[i]);
nodeList.add(node);
}
// 构造节点之间的关系
for (int i = 0; i < nodeList.size() / 2; i++) {
TreeNode node = nodeList.get(i);
node.left = nodeList.get(i * 2 + 1);
// 最后一个父节点 可能没有右孩子 需要额外判断
if (i * 2 + 2 < nodeList.size()) {
node.right = nodeList.get(i * 2 + 2);
}
}
return nodeList.get(0);
}
【相同的树】
https://leetcode-cn.com/problems/same-tree/
100. 相同的树给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。示例 1:输入 : 1 1/ \ / \2 3 2 3[1,2,3], [1,2,3]输出 : true示例 2:输入 : 1 1/ \2 2[1,2], [1,null,2]输出 : false示例 3:输入 : 1 1/ \ / \2 1 1 2[1,2,1], [1,1,2]输出 : false
如何判断是否相同?1 )两棵树,如果有左右子树,树 A 的左子树 == 树 B 的左子树,同时树 A 的右子树 == 树 B 的右子树2 )如果两棵树都是空,是相同的3 )如果一棵为空,一棵不为空,是不同的4 )如果数值不同,也是不同的
public boolean isSameTree(TreeNode p, TreeNode q) {
// 递归出口
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
if (p.val != q.val) {
return false;
}
// 递归规律
return isSameTree(p.left, q.left)
&& isSameTree(p.right, q.right);
}
二叉树的遍历之递归实现
遍历
遍历:对树中的每个节点都访问一次,且只访问一次。深度优先遍历 DFS = Deep First Search广度优先遍历 BFS = Breath First Search先序遍历(前序遍历): 根节点 —> 左子树 -> 右子树中序遍历:左子树 —> 根节点 -> 右子树后序遍历:左子树 -> 右子树 —> 根节点
1
、递归实现方式
// 先序 中序 和后序的遍历
public static void preOrder(TreeNode node) {
// 递归出口
if (node == null) return;
System.out.print(node.val + " ");
preOrder(node.left);
preOrder(node.right);
}
public static void inOrder(TreeNode node) {
if (node == null) return;
inOrder(node.left);
System.out.print(node.val + " ");
inOrder(node.right);
}
public static void postOrder(TreeNode node) {
if (node == null) return;
postOrder(node.left);
postOrder(node.right);
System.out.print(node.val + " ");
}
2
、广度优先遍历
通过队列实现 从根节点开始存储到队列中对队列元素的处理是 将队头节点的孩子存入队列中,取出队头节点直到队列为空 所有节点处理完成 同时节点的顺序是按照层级的
public static void bfs(TreeNode node) {
if (node == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(node);
while (queue.size() > 0) {
// 取出队首元素
TreeNode node1 = queue.poll();
System.out.print(node1.val + " ");
// 如果有左孩子 或者右孩子 将其存入队列
if (node1.left != null) {
queue.offer(node1.left);
}
if (node1.right != null) {
queue.offer(node1.right);
}
}
}
前序和中序遍历之非递归实现
非递归实现方式
递归 —— 有去有回先解决子问题 再基于子问题 解决当前问题也可以理解为 是解决有依赖关系的多个问题fifib(3) A -> fifib(2) B -> fifib(1) C调用关系开始处理 A因为 A 依赖 B 开始处理 B因为 B 依赖 C 开始处理 CC 处理完成B 处理完成A 处理完成后进先出的处理关系 -- 栈
a
) 先序和中序遍历的非递归处理分析
public static void preOrderByLoop(TreeNode node) {
Stack<TreeNode> stack = new Stack<>();
// 使用指针 记录遍历到哪个节点
TreeNode p = node;
while (p != null || !stack.isEmpty()) {
// 入栈 把当前能读到的所有左孩子 存入栈中
while (p != null) {
System.out.print(p.val + " ");
stack.push(p);
p = p.left;
}
// 1 2 4 8
// 1 2 9
// 1 5
// 3 6
// 7
// 出栈 弹出栈顶元素 并找到其右孩子
if (!stack.isEmpty()) {
p = stack.pop();
p = p.right;
}
}
}
先序是入栈时打印 中序是出栈时打印
public static void inOrderByLoop(TreeNode node) {
Stack<TreeNode> stack = new Stack<>();
// 使用指针 记录遍历到哪个节点
TreeNode p = node;
while (p != null || !stack.isEmpty()) {
// 入栈 把当前能读到的所有左孩子 存入栈中
while (p != null) {
stack.push(p);
p = p.left;
}
// 出栈 弹出栈顶元素 并找到其右孩子
if (!stack.isEmpty()) {
p = stack.pop();
System.out.print(p.val + " ");
p = p.right;
}
}
}
后序遍历之非递归实现
后序遍历的非递归方式
经过的路径 左 - 根 - 右后序遍历时 根节点不先从栈中弹出 要在右子树被遍历后 再弹出如何判断右子树被遍历完成通过记录上一次遍历的节点如果上一次遍历的是当前节点的右子树 代表此根节点也可以被遍历出来入栈顺序 1 2 4 8 9 5 3 6 7出栈顺序 8 4 9 2 5 1 6 3 71 2 4 81 2 91 53 67入栈顺序 1 2 4 8 9 5 3 6 7出栈顺序 8 9 4 5 2 6 7 3 11 2 4 81 2 41 2 4 91 2 41 21 2 51 21 3 61 3 71 31节点出栈的逻辑有两种情况:1 )当前节点是叶子节点2 )上一次出栈的节点是当前节点的右孩子
public static void postOrderByLoop(TreeNode node) {
Stack<TreeNode> stack = new Stack<>();
// 使用指针 记录遍历到哪个节点
TreeNode p = node;
// 记录上一次访问的节点
TreeNode prev = null;
while (p != null || !stack.isEmpty()) {
// 入栈 把当前能读到的所有左孩子 存入栈中
while (p != null) {
stack.push(p);
p = p.left;
}
// 出栈 弹出栈顶元素 并找到其右孩子
if (!stack.isEmpty()) {
// 节点出栈的逻辑有两种情况:
// 1)当前节点是叶子节点
// 2)上一次出栈的节点是当前节点的右孩子
p = stack.pop();
if (p.right == null || prev == p.right){
System.out.print(p.val + " ");
prev = p;
p = null;
}else {
// 不满足出栈条件 是因为节点有右孩子 且右孩子没有被访问到
stack.push(p);
p = p.right;
}
}
}
}
经典应用之对称二叉树
对称二叉树
https://leetcode-cn.com/problems/symmetric-tree/
101. 对称二叉树给定一个二叉树,检查它是否是镜像对称的。例如,二叉树 [1,2,2,3,4,4,3] 是对称的。1/ \2 2/ \ / \3 4 4 3但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的 :1/ \2 2\ \3 3如何将数组转化为二叉树1/ \2 2/ \ / \3 4 4 30 1 2 3 4 5 6Integer[] arr = {1,2,2,3,4,4,3}索引 元素 左子树索引 右子树索引0 1 1 21 2 3 42 2 5 6......i 2 i+1 2 i+2判断是否对称将树拆解为 左子树 A 右子树 BA 的左子树和 B 的右子树要对称A 的右子树和 B 的左子树要对称非递归的方式 —— 广度优先遍历存入左子树和右子树 再取出两个子树 判断是否对称
public static boolean isSymmetric(TreeNode root) {
// 排除空树的情况
if (root == null) return true;
// 拆分成左右子树 递归判断
return isMirror(root.left, root.right);
}
// 将树拆解为 左子树A 右子树B
// A的左子树和B的右子树要对称
// A的右子树和B的左子树要对称
public static boolean isMirror(TreeNode node1, TreeNode node2) {
// 递归的出口
if (node1 == null && node2 == null) return true;
// 判断是否不对称
if (node1 == null || node2 == null) return false;
if (node1.val != node2.val) return false;
return isMirror(node1.left, node2.right)
&& isMirror(node1.right, node2.left);
}
public static boolean isSymmetric1(TreeNode root) {
if (root == null) return true;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root.left);
queue.offer(root.right);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
// 递归的出口
if (node1 == null && node2 == null) continue;
// 判断是否不对称
if (node1 == null || node2 == null) return false;
if (node1.val != node2.val) return false;
queue.offer(node1.left);
queue.offer(node2.right);
queue.offer(node1.right);
queue.offer(node2.left);
}
return true;
}
经典应用之翻转二叉树
翻转二叉树
https://leetcode-cn.com/problems/invert-binary-tree
翻转一棵二叉树。示例:输入:4/ \2 7/ \ / \1 3 6 9输出:4/ \7 2/ \ / \9 6 3 1
翻转的过程方式一:(1)4/ \2 7/ \ / \1 3 6 9(2)4/ \7 2/ \ / \6 9 1 3(3)4/ \7 2/ \ / \9 6 3 1先将左右子树交换 然后再分别 翻转左右子树
方式二:(1)4/ \2 7/ \ / \1 3 6 9(2)4/ \2 7/ \ / \3 1 6 9(3)4/ \7 2/ \ / \6 9 3 1(4)4/ \7 2/ \ / \9 6 3 1先翻转左子树 然后交换左右子树 再翻转左子树(原来的右子树)
方式三:(1)4/ \2 7/ \ / \1 3 6 9(2)4/ \2 7/ \ / \3 1 6 9(3)4/ \2 7/ \ / \3 1 9 6(4)4/ \7 2/ \ / \9 6 3 1
先翻转左子树
再翻转右子树
最后交换左右子树
分别对应
前序、中序、后序遍历
// 前序遍历
public static TreeNode invertTree(TreeNode root) {
// 递归的出口
if (root == null) return null;
// 方案一
// 先将左右子树交换 然后再分别 翻转左右子树
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
// 中序遍历
public static TreeNode invertTree1(TreeNode root) {
// 递归的出口
if (root == null) return null;
// 方案二
// 先翻转左子树 然后交换左右子树 再翻转左子树(原来的右子树)
invertTree1(root.left);
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree1(root.left);
return root;
}
// 后序遍历
public static TreeNode invertTree2(TreeNode root) {
// 递归的出口
if (root == null) return null;
invertTree2(root.left);
invertTree2(root.right);
// 方案三
// 先翻转左子树 再翻转右子树 最后交换左右子树
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
return root;
}
// 广度优先遍历
public static TreeNode invertTree3(TreeNode root) {
if (root == null) return null;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (queue.size() > 0) {
// 取出队首元素
TreeNode current = queue.poll();
// 交换此节点的左右子树
TreeNode tmp = current.left;
current.left = current.right;
current.right = tmp;
// 如果有左孩子 或者右孩子 将其存入队列
if (current.left != null) {
queue.offer(current.left);
}
if (current.right != null) {
queue.offer(current.right);
}
}
return root;
}
根据遍历结果构造二叉树
根据遍历结果构造二叉树
只能通过前序和中序,或者中序和后续唯一确认一棵二叉树,而前序和后序无法做到。1 )前序和中序结果倒推二叉树思路:先根据前序结果,找到根节点找到中序结果中根节点的位置,确认左右子树的个数切出左子树的前序和中序结果,再按照此方法推进,直到推导出叶子节点的位置,反倒推出整棵树。0 1 2 3 41 2 3 4 53 4 2 1 5根节点在中序中的位置是 3 (左子树的大小)leftSize = 3左子树的前序和中序切分的区间是左闭右开2 3 4 [1,leftSize+1 )3 4 2 [0,leftSize)右子树的前序和中序5 [leftSize+1,end)5 [leftSize+1,end)
public static TreeNode buildTree(int[] preOrder, int[] inOrder) {
// 递归出口
if (preOrder.length == 0) return null;
// 在前序结果中 找到根节点
int rootValue = preOrder[0];
TreeNode root = new TreeNode(rootValue);
// 查找根节点在中序结果中的位置
int leftSize = find(inOrder, rootValue);
// 切分出左子树的前序和中序结果
// 使用 Arrays.copyOfRange方法 三个参数 原始数组 起始位置 终止位置
// [起始位置,终止位置)
//
// 2 3 4 [1,leftSize+1)
//
// 3 4 2 [0,leftSize)
int[] leftPreOrder = Arrays.copyOfRange(preOrder, 1, leftSize + 1);
int[] leftInOrder = Arrays.copyOfRange(inOrder, 0, leftSize);
root.left = buildTree(leftPreOrder, leftInOrder);
// 切分出右子树的前序和中序结果
//
// 5 [leftSize+1,end)
// 5 [leftSize+1,end)
int[] rightPreOrder = Arrays.copyOfRange(preOrder, leftSize + 1,
preOrder.length);
int[] rightInOrder = Arrays.copyOfRange(inOrder, leftSize + 1,
inOrder.length);
root.right = buildTree(rightPreOrder, rightInOrder);
return root;
}
public static int find(int[] array, int value) {
for (int i = 0; i < array.length; i++) {
if (array[i] == value) {
return i;
}
}
return -1;
}
2
)中序和后序结果倒推二叉树
0 1 2 3 43 4 2 1 54 3 2 5 1根节点在中序中的位置是 3 (左子树的大小)leftSize = 3左子树的中序和后序切分的区间是左闭右开3 4 5 [0,leftSize)4 3 2 [0,leftSize)右子树的中序和后序5 [leftSize+1,end)5 [leftSize,end-1)
public static TreeNode buildTree1(int[] inOrder, int[] postOrder) {
// 递归出口
if (inOrder.length == 0 || postOrder.length == 0) return null;
// 在前序结果中 找到根节点
int rootValue = postOrder[postOrder.length - 1];
TreeNode root = new TreeNode(rootValue);
// 查找根节点在中序结果中的位置
int leftSize = find(inOrder, rootValue);
// 切分出左子树的中序和后序结果
// 使用 Arrays.copyOfRange方法 三个参数 原始数组 起始位置 终止位置
// [起始位置,终止位置)
//
// 3 4 5 [0,leftSize)
// 4 3 2 [0,leftSize)
int[] leftInOrder = Arrays.copyOfRange(inOrder, 0, leftSize);
int[] leftPostOrder = Arrays.copyOfRange(postOrder, 0, leftSize);
root.left = buildTree1(leftInOrder, leftPostOrder);
// 切分出右子树的中序和后序结果
//
// 5 [leftSize+1,end)
// 5 [leftSize,end-1)
int[] rightInOrder = Arrays.copyOfRange(inOrder, leftSize + 1,
inOrder.length);
int[] rightPostOrder = Arrays.copyOfRange(postOrder, leftSize,
postOrder.length - 1);
root.right = buildTree1(rightInOrder, rightPostOrder);
return root;
}