目录
二、给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
一、根据二叉树创建字符串
题目:根据二叉树创建字符串 606.根据二叉树创建字符串
给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。
空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。
思路:(以示例1和示例2为例)
1. 二叉树的前序遍历:
- 例1:1 2 4 3
- 例2:1 2 4 3
2. 每一棵树都有左右括号 (根(...)(...))
- 例1 :(1(2(4()())())(3()()))
- 例2 :(1(2()(4()()))(3()()))
3. 在步骤 2 的基础上进行括号的省略:注意要保证映射关系
(1)最外层的括号省略;
(2)二叉树的形态:
- 空树: " () " -> " "
- 只有一个结点的树: " ( 4 () () ) " -> " ( 4 ) "
- 左子树为空树的树: " ( 2 () ( 4 ) ) " -> " ( 2 () ( 4 ) ) " (不动)
- 右子树为空树的树: " ( 2 ( 4 ) () ) " -> "( 2 ( 4 ) )"
- 左右子树都不为空的树: 不存在需要省略的括号" () "
(3)省略后的字符串:
- 例1: 1(2(4))(3)
- 例2: 1(2()(4))(3)
代码:
public class Solution {
public String tree2str(TreeNode root) {
StringBuilder sb = new StringBuilder();
preorder(root,sb);
//再将最外层的括号去掉
sb.deleteCharAt(0);
sb.deleteCharAt(sb.length() - 1);
//最后转换成题目要求的 String 类型进行返回
String ans = sb.toString();
return ans;
}
//前序遍历
//因为字符串的不可变形,而我们这里需要频繁的进行插入数据,所以不能使用 String 类,而应该使用 StringBuilder 类
//在遍历的过程中要考虑二叉树的形态的 5 种情况
public void preorder(TreeNode root,StringBuilder sb) {
//1. 树为空树
if (root == null) {
sb.append("()");
return;
}
//树不为空树,先将最外面的括号输入,在 tree2str 方法里调用本方法后,再将最外面的一对括号删除
sb.append('(');
//2. 只有一个结点的情况
if (root.left == null && root.right == null) {
sb.append(root.val); //不再走左右子树了,直接到最后一个 )
} else if (root.left == null && root.right != null) {
//3. 左子树为空的情况:没有要省略的括号
sb.append(root.val);
preorder(root.left,sb);
preorder(root.right,sb);
} else if (root.left != null && root.right == null) {
//4. 右子树为空的情况:可以省略右子树的括号
//要想省略,则不遍历其右子树即可
sb.append(root.val);
preorder(root.left,sb);
} else {
//5. 最后一种情况,左右子树都不为空
sb.append(root.val);
preorder(root.left,sb);
preorder(root.right,sb);
}
//添加最外面的括号
sb.append(')');
}
}
二、给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
题目:二叉树的最近公共祖先 236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
思路:
分情况进行讨论:因为题目中明确给出了 p != q,以及 p 、q 均存在在树中,所以不讨论这种情况。
- 当 p == root,q 为其他任何结点时,公共结点为 root。
- 当 q == root,p 为其他任何结点时,公共结点为 root。
- 当 p 和 q 都在右子树(或者左子树)中时,将大问题化为小问题,依旧是查找公共祖先,只是树的规模变小了。
- 当 p 在左子树(右子树),q 在右子树(左子树)中时,公共结点为 root。
代码:
public class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (p == root || q == root ) {
return root;
}
//要判断 pq 在左子树中还是在右子树中
//另外写一个方法进行判断
boolean pInLeft = containsNode(root.left,p);
boolean qInLeft = containsNode(root.left,q);
//分情况
//pq都在左子树
if (pInLeft == true && qInLeft == true) {
return lowestCommonAncestor(root.left,p,q);
}
//pq都在右子树
if (pInLeft == false && qInLeft == false) {
return lowestCommonAncestor(root.right,p,q);
}
//此时代码执行到这里只剩下一种情况
//pq分别在左右子树中
return root;
}
public static boolean containsNode (TreeNode root, TreeNode p) {
if (root == null) {
return false;
}
//树不为空
if (root.val == p.val) {
return true;
}
boolean t = containsNode(root.left,p);
if (t == true) {
return true;
} else {
t = containsNode(root.right,p);
if (t == true) {
return true;
}
}
return false;
}
}
三、层序遍历
题目:二叉树的层序遍历 102. 二叉树的层序遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
思路:实现层序遍历需要用到队列。
层序遍历:
- 要求从上往下(每个结点负责其孩子),从左往右(先左子树再右子树)
- 使用队列 (凡是广度优先遍历,都要用到队列,保证先进先出)
- 取出一个元素,就把其相应的左右孩子放入队列中(null 的不用放入)
代码:
public class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
if (root == null) {
return ans;
}
Queue<TreeNode> nodeQueue = new LinkedList<>();
Queue<Integer> levelQueue = new LinkedList<>();
//将层数从 0 开始,可以对应 ans 中链表的下标
nodeQueue.offer(root);
levelQueue.offer(0);
//ans 中的链表按 level 分,所以每当 level 更新是,都要新建一个链表
//所以需要知道上一个结点的 level 值
int prevLevel = -1;
while (!nodeQueue.isEmpty()) {
TreeNode p = nodeQueue.poll();
int level = levelQueue.poll();
//当 level 值变化时,在 ans 中加入新链表
if (prevLevel != level) {
ans.add(new ArrayList<>());
}
//更新 prevLevel
prevLevel = level;
//此时,ans 中的链表下标刚好对应 level 值
//而此时需要根据 level 值加入结点,所以先获取 ans 中链表的下标值,再进行添加。
List<Integer> list = ans.get(level);
list.add(p.val);
if (p.left != null) {
nodeQueue.offer(p.left);
levelQueue.offer(level + 1);
}
if (p.right != null) {
nodeQueue.offer(p.right);
levelQueue.offer(level + 1);
}
}
return ans;
}
}
四、根据一棵树的前序遍历和中序遍历构造一棵二叉树
题目:根据一棵树的前序遍历和中序遍历构造一棵二叉树 105. 从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
提示:
- 1 <= preorder.length <= 3000
- inorder.length == preorder.length
- -3000 <= preorder[i], inorder[i] <= 3000
- preorder 和 inorder 均 无重复 元素
- inorder 均出现在 preorder
- preorder 保证 为二叉树的前序遍历序列
- inorder 保证 为二叉树的中序遍历序列
思路:前序遍历:根->左子树->右子树;后序遍历:左子树->右子树->根
① 根据前序遍历可以判断出树的根结点,即树的根结点为数组[ 0 ]位置的元素。
② 根据后序遍历,找到根结点,就可以判断出左右子树各有多少个元素了。左子树结点个数 = 根结点左边元素个数,右子树结点个数 = 根结点右边元素个数。
③ 同样的方式,处理左右子树。(将大规模问题转化成小规模问题)
- 左子树的前序遍历以及后序遍历(递归)
- 右子树的前序遍历以及后序遍历(递归)
④ 递归的终止条件:
- 序列长度 == 0 :空树
- 序列长度 == 1 :只有一个根节点的树
代码:有两种方式:
方法一:为了便于操作,将题目中所给的数组转化为链表。
public class Solution {
//数组不方便操作,所以写一个方法将数组转化成链表形式
public List<Integer> toList(int[] arr) {
List<Integer> ans = new ArrayList<>();
for (int e : arr) {
ans.add(e);
}
return ans;
}
//根据链表创建树的方法
public TreeNode build(List<Integer> preorder,List<Integer> inorder) {
if (preorder.size() == 0) {
//当序列元素为 0 时,说明时空树
return null;
}
//1.确定根的值
int rootVal = preorder.get(0);
TreeNode root = new TreeNode(rootVal);
//2. 根据中序遍历确定左右子树的结点个数
int leftSize = inorder.indexOf(rootVal); //左子树的结点个数刚好等于根节点的下标
int rightSize = inorder.size() - 1 - leftSize;
//3. 进行递归,切出左右子树的前序遍历和中序遍历
//这里使用了 subList() 方法, 该方法用来切割链表,传入的参数为下标值(左闭右开)
List<Integer> leftPreorder = preorder.subList(1,leftSize + 1); //左子树的前序遍历
List<Integer> leftInorder = inorder.subList(0,leftSize); //左子树的中序遍历
TreeNode leftRoot = build(leftPreorder,leftInorder);
List<Integer> rightPreorder = preorder.subList(leftSize + 1, preorder.size());
List<Integer> rightInorder = inorder.subList(leftSize + 1, inorder.size());
TreeNode rightRoot = build(rightPreorder,rightInorder);
//4. 将三个结点建立关系
root.left = leftRoot;
root.right = rightRoot;
//5. 返回根节点
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
//将数组均转换成链表
List<Integer> preorderList = toList(preorder);
List<Integer> inorderList = toList(inorder);
return build(preorderList,inorderList);
}
}
方法二:直接使用数组
public class Solution2 {
//使用数组直接进行操作
//思路是一样的,区别在于下标
//由于问题规模会不断变小,从最开始数组中截取的长度以及下标也会发生改变
//所以新写一个方法
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder, 0, preorder.length, inorder, 0, inorder.length);
}
//利用 int[] array 和 int fromIndex 和 int toIndex,左闭右开
public TreeNode build(int[] preorder, int pfrom, int pto, int[] inorder, int ifrom, int ito) {
if (pto - pfrom == 0) {
//空树
return null;
}
//1. 确定根的值
//2. 根据中序遍历确定左右子树的结点个数
//3. 进行递归,切出左右子树的前序遍历和中序遍历
//4. 将三个结点建立关系
//5. 返回根节点
int rootVal = preorder[pfrom];
TreeNode root = new TreeNode(rootVal);
//要在中序遍历中找到 rootVal 的位置,利用 for 循环遍历查找,注意循环起始条件 i 不能等于 0,而应该等于 ifrom
int index = 0;
for (int i = ifrom; i < ito; i++) {
if (inorder[i] == rootVal) {
index = i;
}
}
//此时 index 所指的元素为 rootVal 在中序遍历中的下标位置
//所以可以计算出左右子树的结点个数
int leftSize = index - ifrom;
int rightSize = ito - index;
//切出左子树的前序遍历和中序遍历
int leftPfrom = pfrom + 1;
int leftPto = leftPfrom + leftSize;
int leftIfrom = ifrom;
int leftIto = ifrom + leftSize;
//构建左子树
TreeNode left = build(preorder, leftPfrom, leftPto, inorder, leftIfrom, leftIto);
//切出右子树的前序遍历和中序遍历
int rightPfrom = pfrom + leftSize + 1;
int rightPto = pto;
int rightIfrom = ifrom + leftSize + 1;
int rightIto = ito;
//构建右子树
TreeNode right = build(preorder, rightPfrom, rightPto, inorder, rightIfrom, rightIto);
//进行联系
root.right = right;
root.left = left;
return root;
}
}
五、二叉树的构建及遍历
题目:二叉树遍历 KY11 二叉树遍历
描述:
编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
输入描述:
输入包括1行字符串,长度不超过100。
输出描述:
可能有多组测试数据,对于每组数据, 输出将输入字符串建立二叉树后中序遍历的序列,每个字符后面都有一个空格。 每个输出结果占一行。
思路:
分析题目得出:传入先序遍历的字符串,根据先序遍历构造二叉树,最后将二叉树的中序遍历按要求输出。(先序遍历 -> 二叉树 -> 中序遍历)
传入参数为字符串,可以将字符串转换为链表,使得后续的操作更加方便,所以需要一个将字符串转换为链表的方法。
其次还需要方法:①根据先序遍历构建二叉树;② 输出二叉树的中序遍历
注意:从题目中看,需要自己写一个TreeNode类,以及main方法里的输入。
代码:
public class Main {
public static class TreeNode {
public char val;
public TreeNode left;
public TreeNode right;
public TreeNode(char val) {
this.val = val;
this.left = this.right = null;
}
}
public static void main(String[] args) {
//输入先序遍历字符串
Scanner scanner = new Scanner(System.in);
String preorder = scanner.nextLine();
//转换为链表
List<Character> preorderList = toList(preorder);
//创建树
TreeNode root = buildTree(preorderList);
//树的中序遍历
inorderTraversal(root);
}
//思路:
//题目中传入的是字符串,所以首先要将字符串转化为字符链表,方便操作
//将字符链表作为参数传入目标方法中
public static List<Character> toList (String s) {
List<Character> list = new LinkedList<>();
for ( char a : s.toCharArray()) {
list.add(a);
}
return list;
}
//中序遍历 (直接输出)
public static void inorderTraversal (TreeNode root) {
if (root == null) {
return;
}
inorderTraversal(root.left);
System.out.print((char) root.val + " ");
inorderTraversal(root.right);
}
//构建二叉树:
//已知先序遍历,可以知道根结点
public static TreeNode buildTree (List<Character> preorder) {
if (preorder.isEmpty()) {
return null;
}
//将字符串的第一个元素取出来
char rootval = preorder.remove(0);
TreeNode root = new TreeNode(rootval);
//根据题意可以得出 当字符 # 时代表空树
if (rootval == '#') {
return null;
}
//构建左右子树
//因为用的是 remove 方法,所以先序队列中的元素一直在减少
//先序遍历:根->左->右
//所以当字符串的第一个根元素删除后,接下来一个元素就为左子树根结点,当左子树都处理完后,再是右子树的根结点
//运用递归调用该方法
root.left = buildTree(preorder);
root.right = buildTree(preorder);
return root;
}
}