1、二叉树构建及遍历
- 编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串:
ABC##DE#G##F###
其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
import java.util.*;
class TreeNode{
public char val;
public TreeNode left;
public TreeNode right;
public TreeNode(char val){
this.val = val;
}
}
public class Main {
public static int i = 0; // 将i定位静态,放在外面,这样每次在创建树的部分,i不会从0开始
// 根据字符串创建二叉树
public static TreeNode creatTree(String str){
if(str == null || str.length() <= 0) return null;
// 使用i遍历所给的字符串,当为字母时,创建结点
// 构建根 -》 构建根的左 -》 构建跟的右
// 构建结点之后i++;
//int i = 0; i是不能放在这里的
TreeNode root = null; // 用来存放新建的结点
if(str.charAt(i) != '#'){
root = new TreeNode(str.charAt(i)); // 根据当前i下标的字符新建一个结点
i++;
root.left = creatTree(str); // 递归左树
root.right = creatTree(str); // 递归右树
}else{
// 如果遇到#号了,i++,跳过这个
i++;
}
return root;
}
// 将创建的二叉树按中序遍历输出
public static void inOrderTraversal(TreeNode root){
if(root == null) return;
inOrderTraversal(root.left);
System.out.print(root.val + " ");
inOrderTraversal(root.right);
}
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
String str = scan.nextLine();
// 根据读入的字符串创建一个二叉树
TreeNode root = creatTree(str);
inOrderTraversal(root);
}
}
2、二叉树的分层遍历
- 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层序遍历结果:
[
[3],
[9,20],
[15,7]
]
- 需要使用到队列来进行处理
- 1、如果当前root不为空,那么root入队
- 2、判断队列是否为空?
不为空:(1)弹出队头元素,且保存并打印;(2)判断当前的cur.left和cur.right
为空: return 返回出去
public void levelOrderTraversal(TreeNode root){
if(root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); //将当前跟节点入队
while(!queue.isEmpty()){ // 只要队列不为空,就继续循环
TreeNode cur = queue.poll(); // 创建一个引用来保存出队结点的信息
System.out.print(cur.val + " "); // 打印当前出队的结点
if(cur.left != null){
queue.offer(cur.left);
}
if(cur.right != null){
queue.offer(cur.right); // 将此时所出结点的左右结点都入队
}
}
}
-
运行逻辑为下
-
上述代码是打印出来层序遍历,力扣上的题需要返回一个二维数组,需要将返回的结果保存起来,运行逻辑与上图相同,只是多了一个
list
来存放每层的元素
// 将层序遍历的结果保存在二维数组里
public List<List<Integer>> levelOrderTraversal(TreeNodee root){
List<List<Integer>> ret = new ArrayList<>();
if(root == null) return ret;
Queue<TreeNodee> queue = new LinkedList<>(); // 使用一个队列来存放分层遍历的各元素
queue.offer(root); // 将当前根节点放入队列中
while( !queue.isEmpty()){ // 只要队列不为空
List<Integer> list = new ArrayList<>(); // 存放每一层的结点
int size = queue.size(); // 因为会弹出队列,所以要将每次队列中的size都保存下来(当前队列不为空,有多少个元素)
while(size > 0){
TreeNodee cur = queue.poll(); // 当前弹出的元素;
list.add(cur.val); //将当前弹出的元素放进list中
if(cur.left != null){
queue.offer(cur.left);
}
if(cur.right != null){
queue.offer(cur.right);
}
size--; // 出去了一个元素,size--
}
// 此时队列里存放的是第二层的两个结点,不为空,进入外层while循环
// new一个新的list,size此时等于2,依次往复
// 每次走完一层,将这一层得到的list放入到ret二维数组中
ret.add(list);
}
return ret;
}
3、给定一个二叉树,找到该树中两个指定节点的最近公共祖先
- **给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” - 使用前序遍历的思想
- 1、先判断root根节点本身是不是p或q中的一个,是的话,root就是我们要找的
- 2、不是的话,分别去找根的左和根的右
- 2.1、左右两边都不为空(找到了p和q), root是公共祖先
- 2.2、左边不为空,右边为空,左边第一次找到的节点就是公共祖先
- 2.3、左边为空,右边不为空,右边第一次找到的结点就是公共祖先
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){
if(root == null) return null;
if(root == p || root == q) return root;
// 分别递归当前根节点的左边和右边
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p. q);
if(left != null && right != null) return root;
if(left != null) return left;
if(right != null) return right;
return null;
}
4、二叉搜索树转换成排序双向链表
- 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
- 以中序遍历的思路,在遍历的过程中,顺便修改right和left的指向
- 代码运行逻辑大致如下:
- 1、中序遍历,pCur肯定是先走到2这个位置,要使变成一个链表,2的前驱要指向null,后继则指向5。
- 2、先看前驱部分,我们要定义一个结点来存放我们的前驱部分,于是有了prev结点,起初它为null。
- 3、这里就让此时pCur即2的前驱为null
pCur.left = null
,并且让prev移到pCur处prev = pCur
- 4、然后根据中序遍历,下一次该到的位置是pCur.right即此时2的右孩子结点,但他为空,于是就直接返回,此时在这一次的递归中2这个节点已经走完了,于是pCur就到了上一层的5上。
- 5、此时在再次执行
pCur.left = null
,即将5节点的前驱指向2结点。 - 6、根据上面步骤,连接了2和5节点的前驱,但还没有让2的后继指向5。
- 7、接着上面的流程,此时pCur在5结点,prev在2结点,按照第3步应该要让prev移动到5节点处,但是,这时候应该让2的后继指向5,即
prev.right = pCur
,这个条件要添加到pCur.left = null
与prev = pCur
之间,但是直接加到中间在第一次递归执行时,便会因为当时prev还为null,此时的prev.right
会出现空指针异常,所以应当增加一个if条件,在prev不为空的时候执行。
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public TreeNode prev = null;
public void ConvertChild(TreeNode pCur) {
if(pCur == null) return;
ConvertChild(pCur.left);
pCur.left = prev;
if(prev != null) {
prev.right = pCur;
}
prev = pCur;
ConvertChild(pCur.right);
}
public TreeNode Convert(TreeNode pRootOfTree) {
if(pRootOfTree == null) return null;
ConvertChild(pRootOfTree);
TreeNode head = pRootOfTree;
while(head.left != null) {
head = head.left;
}
return head;
}
}
5、根据一棵树的前序遍历与中序遍历构造二叉树
- 前序遍历可以确定哪一个是根
- 中序遍历可以确定左树和右树
- 整体思路流程为:
对于前序遍历[ABCDEFGHI]
和中序遍历[HDBEIAFCG]
,此时中序遍历中inBegin
在H
位置,inEnd
在G
,根据各自遍历的特点,A这个位置肯定是最开始的root点,在中序遍历中,用A将此中序数组分成左边右边两个部分,左边就是左树,右边就是右树,此时,以左树为例[HDBEI]
,当前的中序遍历数组中inBegin
在H
位置,inEnd
在I
,在进入下一次递归建立树的时候,前序遍历的数组下标向后移动一位到B,明显B就是左树的根结点,这时在同样进行分割递归下去。详细步骤如下: - 1、判断所输入的两种遍历数组是否为空,不为空继续
- 2、通过递归创建二叉树,所需的参数是<前序遍历的数组>、<前序遍历数组的下标位置>、<中序遍历数组>、<中序遍历数组中的开始位置(inBegin)和结束位置(inEnd)>
- 3、由前序遍历数组,有了在前序遍历中根节点的下标,我们new一个新的根
TreeNode root = new TreeNdoe(preorder[preIndex])
- 4、有了根节点,找当前根节点在中序遍历数组中的位置
index
在中序遍历中找int[] inorder
,找的是相等的值int key
,范围是inBegin~inEnd
- 5、找到了当前根结点在中序遍历数组中的位置后,再将前序遍历上的下标preIndex向后移动
- 6、此时中序遍历数组中
index
的左边就是左树,右边就是右树 - 7、前序遍历数组是一直不变的,变得是前序遍历的下标,下标在每次遍历后会向后走1
- 8、中序遍历数组在一直变,每一次遍历时,针对当前
index
根节点,左树就是数组中inBegin ~ index - 1
,右树就是数组中index + ~ inEnd
- 9、分别递归中序遍历中标记处的左树和右树
- 10、返回当前树得根结点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// 前序中的0下标必然是该二叉树的根节点
// 我们在中序遍历中找出与前序中找到的根节点值相等的点,这个点就是根节点,根据中序遍历的性质,该结点左边是左树,右边是右树
// 然后根据这个结点划分
// 左边作为一个单独的树,右边作为一个单独的树
// 再找通过递归上述步骤来逐步划分
public int preIndex = 0; // 前序遍历的下标
// 要建立起一个二叉树,必然还是使用递归,通过递归子问题,构建一个一个树
public TreeNode buildTreeChild(int[] preorder, int[] inorder, int inbegin, int inend){
if(inbegin > inend) return null; //说明此时没有子树
// 两位置相等时,该节点已经是最下面的子节点了,此时在进行子树递归时,以左树为例,inend = index - 1,此时小于了inbegin
// 要将当前这个根节点生成
TreeNode root = new TreeNode(preorder[preIndex]); // preorder[preIndex] 在前序遍历中找到的根节点的值
//要在中序遍历中找到根节结点的位置
int index = findValInorder(inorder, preorder[preIndex], inbegin, inend);
// 在中序遍历内找到了根节点位置后,再在前序遍历上的下标preIndex向后移动
preIndex++;
// 此时在 index 的左边就是左树, 右边就是右树
// 前序遍历数组是一直不变的,变得是前序遍历的下标,下标在每次遍历后会向后走1个位置
// 中序遍历数组在一直变,每一遍历时,左树的位置就是 inBegin ~ index - 1的位置
root.left = buildTreeChild(preorder, inorder, inbegin, index - 1);
root.right = buildTreeChild(preorder, inorder,index + 1, inend);
return root;
}
// 要在中序遍历中找到根节结点的位置
public int findValInorder(int[] inorder, int key, int inbegin, int inend){
for(int i = inbegin; i <= inend; i++){ //因为我们在最开始传入后序遍历下标时,传给递归的就是inorder.length-1,所以这里取等于
if(inorder[i] == key){
return i; // 找到了返回i下标
}
}
return -1;
}
public TreeNode buildTree(int[] preorder, int[] inorder) { // 输入的是前序遍历和中序遍历的数组
if(preorder == null || inorder == null) return null;
if(preorder.length == 0 || inorder.length == 0) return null; //先判断给的两个数组是否合法
// 输入数组合法,则通过递归创建二叉树
return buildTreeChild(preorder, inorder, 0, inorder.length-1); // inorder.length-1表示的是中序便利的范围
}
}
6、根据一棵树的中序遍历与后序遍历构造二叉树
- 思路与前序遍历是相同的,后序遍历同样可以确定根节点的位置,只是需要从数组的后面往前走
- 注意:逆向走后序遍历的时候,递归是 先构建右子树,再构建左子树
class Solution {
public int posIndex = 0; // 前序遍历数组最后一给元素的下标,是全局要用的,所以定义在外面,赋值在主构建函数中赋值
public TreeNode buildTreeChild(int[] inorder, int[] postorder, int inbegin, int inend){
if(inbegin > inend){
return null;
}
// 新建当前根节点
TreeNode root = new TreeNode(postorder[posIndex]);
// 找到当前根节点在中序遍历数组中的位置
int index = findValInorder(inorder, postorder[posIndex], inbegin, inend);
// 将后序遍历数组的下标向前移动
posIndex--;
// 递归左树和右树
root.right = buildTreeChild(inorder, postorder, index + 1, inend);
root.left = buildTreeChild(inorder, postorder, inbegin, index - 1);
return root;
}
// 在中序遍历数组中找到根结点的下标
public int findValInorder(int[] inorder, int key, int inbegin, int inend){
for(int i = inbegin; i <= inend; i++){
if(inorder[i] == key){
return i;
}
}
return -1;
}
public TreeNode buildTree(int[] inorder, int[] postorder) { // 前序数组,后序数组
// 首先判断给定数组是否合法
if(inorder == null || postorder == null) return null;
if(inorder.length == 0 || postorder.length == 0) return null;
// 因为是从后往前走后序遍历数组,我们要拿到该数组的最后一个元素的下标
posIndex = postorder.length - 1;
// 递归创建树
return buildTreeChild(inorder, postorder, 0, inorder.length-1);
}
}
7、根据二叉树创建字符串
- 采用前序遍历的方式,将一个二叉树转换成一个由括号和整数组成的字符串。
输入: 二叉树: [1,2,3,4]
1
/ \
2 3
/
4
输出: "1(2(4))(3)"
- 以题中给的这个树为列,前序遍历,从1结点进入打印根节点,去左边,左边不为空,打印一个
(
,打印2,再向左边,不为空,打印一个(
,打印4,再向左,为空,向右,为空,4结点结束,打印一个)
,去2结点的右边,为空,不打印,2节点结束,打印一个)
,去1结点的右边,不为空,打印一个(
,打印3,3左右都是空,3结点结束,打印一个)
- 总结:
遇到左边不为空,加(
;当前树遍历完了,加)
class Solution {
public void tree2strChile(TreeNode t, StringBuilder sb){
if(t == null) return;
sb.append(t.val);
if(t.left == null){
if(t.right == null){
return;
}else{
sb.append("()");
}
}else{
sb.append("(");
tree2strChile(t.left, sb);
sb.append(")");
}
if(t.right == null){
return;
}else{
sb.append("(");
tree2strChile(t.right, sb);
sb.append(")");
}
}
public String tree2str(TreeNode t) {
StringBuilder sb = new StringBuilder();
tree2strChile(t,sb);
return sb.toString();
}
}
8、前、中、后序非递归遍历(栈)
-
二叉树递归改非递归常用的两种数据结构:栈、队列
-
(一)前序非递归遍历
1、使用栈,定义一个引用cur 以 前序遍历来将每个元素放入栈里,只要cur不为空,就入栈
2、 当cur为空时,弹出栈顶元素,用引用top来记录,此时让cur走向弹出元素的右边,即cur = top.right
3、当栈为空的时候,说明遍历完了整个二叉树
public void preOrderTraversalNor(TreeNode root){
if(root == null) return;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()){
while(cur != null){
stack.push(cur);
System.out.print(cur.val + " ");
cur = cur.left;
}
// 此时说明已经到了当前树的最左边了
TreeNode top = stack.pop();
cur = top.right;
}
}
- (二)中序非递归遍历
- 与前序不同的时是:出栈一次打印一次
public void inOrderTraversalNor(TreeNode root){
if(root == null) return;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()){
while(cur != null){
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.pop();
System.out.print(top.val + " ");
cur = top.right;
}
}
- (三)后序非递归遍历
- 在cur等于空的时候不能直接弹出栈,因为根据后序遍历的规则还要看当前根节点的右边,最后才看根节点
public void postOrderTraversalNor(TreeNode root){
if(root == null) return null;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev = null; //只要结点被打印了,就指向这个结点
while(cur != null || !stack.isEmpty()){
while(cur != null){
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if(top.right == null || top.right == prev){ // top.right == null || top.right == 打印过了
stack.pop();
System.out.print(top.val + " ");
prev = top;
}else{
cur = top.right;
}
}
}
9、递增顺序查找树:给你一个树,请你按中序遍历重新排列树,是树中最左边的结点现在是树的根,且每个节点没有左子节点,只有一个右子节点。力扣(LeetCode)
示例 :
输入:[5,3,6,2,4,null,8,1,null,null,null,7,9]
5
/ \
3 6
/ \ \
2 4 8
/ / \
1 7 9
输出:[1,null,2,null,3,null,4,null,5,null,6,null,7,null,8,null,9]
1
\
2
\
3
\
4
\
5
\
6
\
7
\
8
\
9
提示:
给定树中的结点数介于 1 和 100 之间。
每个结点都有一个从 0 到 1000 范围内的唯一整数值。
- 中序遍历+构造新的树
在树上进行中序遍历,就可以从小到大得到树上的结点,把这些节点的对应值放在数组中,此时数组中存放的值已是有序的,直接根据数据构建树
class Solution{
public TreeNode increasingBST(TreeNode root){
List<Integer> vals = new ArrayList();
inorder(root, vals); //首先根据中序遍历,得到原树中的元素(此题得到的将会是有序数组)
TreeNode ans = new TreeNode(0); // 作用相当与链表中的头节点
TreeNode cur = ans; // 用来遍历新树的下标(引用)
for(int v : vals){ // 遍历储存的数组
cur.right = new TreeNode(v); // 对当前root的right赋值
cur = cur.right; // cur移动到当前root的right子结点处
}
return ans.right; // 输出是从1开始的。0不输出
}
public void inorder(TreeNode node, List<Integer> vals){ // 标准的中序遍历结构
if(node == null) return;
inorder(node.left, vals); // 左结点
vals.add(node.val); // 添加根节点
inorder(node.right, vals); // 右结点
}
}
10、合并二叉树
- 给定两个二叉树,将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。力扣(LeetCode)
示例 1:
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7
注意: 合并必须从两个树的根节点开始。
- 深度优先搜索
从根节点开始同时遍历两个二叉树,并将对应的节点进行合并
对应的节点有三种情况,每种有不同的合并方式:
(1)如果两个二叉树的对应结点都为空,则合并后的二叉树的对应节点也为空;
(2)如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空结点;
(3)如果两个二叉树的对应节点都不为空,则合并后的二叉树的对音G节点的值为两个二叉树的对应节点的值之和。
class Solution{
public TreeNode mergeTrees(TreeNode t1, TreeNode t2){
if(t1 == null) return t2;
if(t2 == null) return t1;
TreeNode merged = new TreeNode(t1.val + t2.val);
merged.left = mergeTrees(t1.left, t2.left);
merged.right = mergeTrees(t1.right, t2.right);
return merged;
}
}
11、二叉树的最大宽度
- 使用队列来层序遍历二叉树,将每层元素入队
- 子树结点入队由公式 父节点
n
;左孩子结点(2n)
; 右孩子结点(2n+1)
public int widthOfBinaryTree(TreeNode root) {
if(root == null) return 0;
Queue<TreeNode> q = new LinkedList<>(); //记录层序遍历的顺序性
LinkedList<Integer> list = new LinkedList<>(); // 存储层次遍历后的树
q.offer(root); // 当前根入队
list.add(1); // 将当前跟放入链表,后续标记子树的下标
int res = 1; // 存放最大宽度的结果
while (!q.isEmpty()) {
int count = q.size(); //这一层的节点个数
for(int i =count; i> 0; i--) {
TreeNode cur = q.poll();
Integer curIndex = list.removeFirst();
if(cur.left != null) {
q.offer(cur.left);
list.add(curIndex * 2); // 当前节点的左子节点是2i
}
if(cur.right != null) {
q.offer(cur.right);
list.add(curIndex * 2 +1); // 当前节点的右子节点是2i+1
}
}
// for循环走一遍,说明遍历完了一层
// list中sizew为1的情况下,宽度也为1,没有必要计算。k
// 这一层的宽度是链表的最后一个元素减第一个元素
if(list.size() >= 2) {
res = Math.max(res, list.getLast() - list.getFirst() + 1);
}
}
return res;
}
12、二叉树的完全性检验
- 使用队列以层序存放各层元素
- 出队时连续出队两个Null时,则不是完全二叉树
class Solution {
public boolean isCompleteTree(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
TreeNode prevNode = root;
queue.add(root);
while(!queue.isEmpty()){
TreeNode cur = queue.poll();
if(prevNode == null && cur != null){ // 连续出队的两个为null
return false;
}
if(cur != null){
queue.add(cur.left);
queue.add(cur.right);
}
prevNode = cur;
}
return true;
}
}