目录
一、二叉树的性质
再看题之前,我们需要的先驱知识是二叉树的性质,为了方便我们做题,先简单介绍一下
1. 若规定根结点的层数为1,则一棵非空二叉树的第 i 层上最多有 2^(i-1) (i>0)个结点
2. 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 2^(k-1) (k>=0)
3. 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+1
4. 具有n个结点的完全二叉树的深度k为log2(n+1)上取整
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
- 若孩子结点的序号为i,若i>0,则双亲序号:(i-1)/2;若i=0,i为根结点编号,无双亲结点
- 若父亲结点的序号为i若 2i+1<n,则左孩子序号:2i+1,否则无左孩子;若2i+2<n,右孩子序号:2i+2,否则无右孩子
1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树
B 200
C 198
D 199答案选B
分析:n0=n2+1
2.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2
答案选A
分析:因为 n = n0+n1+n2
当完全二叉树的结点个数为偶数时
当完全二叉树的结点个数为奇数时
- 这道题中,结点个数为2n,是偶数,所以 n1=1
- 而 n = n0 + n1 + n2
- 所以 2n = n0 + n1 + n2,即 2n = n0 + 1 + n0-1
- 所以 n0 = n
3.一个具有767个节点的完全二叉树,其叶子节点个数为()
A 383
B 384
C 385
D 386
答案选B
由于该完全二叉树的结点数是奇数,所以度为1的结点的个数为0,即 n1 = 0
所以 767 = n0 + n0-1
所以n0 = 384
4.一棵完全二叉树的节点数为531个,那么这棵树的高度为( )
A 11
B 10
C 8
D 12
答案选B
2^8 - 1 < 531
2^9 - 1 < 531
2^10 - 1 > 531
二、二叉树的遍历
我们需要了解二叉树的遍历方式有以下几种:前序遍历,中序遍历,后序遍历,层序遍历
详细请见
1.某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH 。该完全二叉树的前序序列为()
A: ABDHECFG B: ABCDEFGH C: HDBEAFCG D: HDEBFGCA
答案选A
我们画成图就是这样的二叉树
2.二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为()
A: E B: F C: G D: H答案选A
3.设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为()
A: adbce B: decab C: debac D: abcde答案选D
4.某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同一层从左到右)的序列为()
A: FEDCBA B: CBAFED C: DEFCBA D: ABCDEF答案选A
这三道题比较类似,考的都是根据某两种遍历创建出二叉树
我们来看一下
1.根据先序遍历和中序遍历创建二叉树
2.根据后序遍历和中序遍历创建二叉树
3.根据前序遍历和后序遍历不能创建一个二叉树
三、二叉树OJ题
1. 检查两颗树是否相同
分析:如何判断两棵树相同?
1.两个结点都不为空或都为空
2.两个结点的左树和右树的值分别相等
两个树不相同有两种情况:
1.一个为空,一个不为空(结构不同)
2.结构虽然此时相同,但是值不同
/**
* 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 {
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q != null) return false;
if(p != null && q == null) return false;
//要么是两个都为空,要么两个都不为空
if( p == null && q == null) return true;
//此时只可能两个都不为空,需要判断val值
if(p.val != q.val){
return false;
}
//此时两个都不为空且结点的值一样
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
}
这道题的时间复杂度是:O(min(m,n))
2.另一棵树的子树
给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。
注:二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。
示例一
输入:root = [3,4,5,1,2], subRoot = [4,1,2]
输出:true
示例二
输入:root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]
输出:false
分析:如何判断subRoot是root 的子树?
- 判断subRoot和root是不是相同的两棵树
- 判断subRoot是不是root的左树的子树
- 判断subRoot是不是root的右树的子树
所以,这道题是在上一题的基础上完成的
/**
* 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 {
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q != null) return false;
if(p != null && q == null) return false;
//要么是两个都为空,要么两个都不为空
if( p == null && q == null) return true;
//此时只可能两个都不为空,需要判断val值
if(p.val != q.val){
return false;
}
//此时两个都不为空且结点的值一样
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if(root == null) return false;
if(isSameTree(root,subRoot)) return true;
if(isSubtree(root.left,subRoot)) return true;
if(isSubtree(root.right,subRoot)) return true;
return false;
}
}
这道题的时间复杂度是:O(m*n)
m是root的结点数,n是subRoot的结点数,subRoot需要跟root的每个结点都判断一次
3.二叉树的最大深度
也就是获取二叉树的高度,我们采用递归的方法
class Solution {
public int maxDepth(TreeNode root) {
if(root==null) return 0;
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
return leftHeight > rightHeight ?
leftHeight+1 : rightHeight +1 ;
}
}
4.判断一棵树是否是平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
分析:
- 要是一棵树是高度平衡的二叉树,那么这棵树的每个子树都是高度平衡的
- 采用深度遍历的方式遍历即可
- 每个节点都去求一下左右高度差是不是小于等于1,所以我们会用到上一题求高度的代码
这样写的时间复杂度是O(n^2),有n个结点,每个结点都要计算,而计算一个节点的高度的时间复杂度是O(N)
class Solution {
//求高度
public int height(TreeNode root) {
if(root==null) return 0;
int leftHeight = height(root.left);
int rightHeight = height(root.right);
return leftHeight > rightHeight ?
leftHeight+1 : rightHeight +1 ;
}
//是不是平衡:每个结点都要判断是不是平衡的,每个节点都要求左右高度差
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
int leftH = height(root.left);
int rightH = height(root.right);
return Math.abs(leftH-rightH)<=1
&& isBalanced(root.left)
&& isBalanced(root.right);
}
}
下面我们介绍一种时间复杂度为O(N)的写法
其实我们发现,在求高度的过程中,我们已经能知道是否平衡了,那我们怎么做到求高度的时候顺便就判断一下高度差是都小于等于1
class Solution {
//求高度
public int height(TreeNode root) {
if(root==null) return 0;
int leftH = height(root.left);
int rightH = height(root.right);
if(leftH>=0 && rightH>=0 && Math.abs(leftH-rightH)<=1 ){
return Math.max(rightH,leftH)+1;
}else{
return -1;
}
}
//是不是平衡:每个结点都要判断是不是平衡的,每个节点都要求左右高度差
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
return height(root) >= 0;
}
}
这个代码就是上一个的优化,是基于我们对代码的实现过程的进一步理解实现的对时间复杂度的优化
5.对称二叉树
给你一个二叉树的根节点 root
, 检查它是否轴对称。
分析:
class Solution {
public boolean isSymmetricChild (TreeNode leftTree, TreeNode rightTree){
if(leftTree==null && rightTree!=null) return false;
if(leftTree!=null && rightTree==null) return false;
if(leftTree==null && rightTree==null) return true;
if(leftTree.val != rightTree.val) return false;
return isSymmetricChild(leftTree.left,rightTree.right)
&& isSymmetricChild(leftTree.right,rightTree.left);
}
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return isSymmetricChild(root.left,root.right);
}
}
6.二叉树的构建及遍历
编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
分析:由于给定的是一个前序遍历的字符串,前序遍历是:左 -> 根 -> 右
所以我们就以前序遍历的方式去创建这棵树,由于‘#’代表空树,所以在遇到‘#’之前的结点都是根结点
所以根据这个字符串创建的二叉树为
我们需要一个 i 下标遍历字符串,当 i 不是 '#' 时,就是一个根结点,就以这个字符为值创建一个根结点,然后分别递归创建左树和右树,再中序遍历
import java.util.*;
class TreeNode{
public char val;
public TreeNode left;
public TreeNode right;
public TreeNode(char val){
this.val = val;
}
}
public class Main{
//因为递归,所以i创建在createTree函数外面
public static int i;
public static TreeNode createTree(String str){
TreeNode root = null;
if(str.charAt(i)!='#'){
root = new TreeNode(str.charAt(i));
i++;
//递归创建左树和右树
root.left = createTree(str);
root.right = createTree(str);
}else{
i++;
}
return root;
}
//中序遍历
public static void inorder(TreeNode root){
if(root==null) return;
inorder(root.left);
System.out.print(root.val+" ");
inorder(root.right);
}
public static void main (String[] args){
Scanner scan = new Scanner(System.in);
while(scan.hasNextLine()){
String str = scan.nextLine();
TreeNode root = createTree(str);
inorder(root);
}
}
}
7.二叉树的分层遍历 。
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
分析:这里我们需要用到队列,每弹出一个结点,就把它的左右结点分别入队列
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ret = new ArrayList<>();
if(root == null) return ret;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
int size = queue.size();
List<Integer> list = new ArrayList<>();//每一层
while (size!=0){
TreeNode cur = queue.poll();
list.add(cur.val);
size--;
if(cur.left!=null) queue.offer(cur.left);
if(cur.right!=null) queue.offer(cur.right);
}
ret.add(list);//每一层
}
return ret;
}
}
8.给定一个二叉树, 找到该树中两个指定节点的最近公共祖先
236. 二叉树的最近公共祖先 - 力扣(LeetCode)
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
思路一:
- 解法一:如果二叉树的表示方式是孩子双亲表示法(也就是结点中存有双亲结点的引用),那么此时这个最近公共祖先,可以被优化为求链表的交点。
- 解法二:二叉树的表示方式不是孩子双亲表示法,我们使用2个栈,分别存储从根结点到指定结点p和q的路径上的所有结点,再比较两个栈的大小,让多的栈出 两个栈元素的差值 个元素,此时两个栈中元素个数相同,这是两个栈同时开始一个一个元素地出栈,如果栈顶元素相同,那么此时这个值 就是最近公共祖先
我们以这种思路写一下代码
class Solution {
//用两个栈
/**
将从root到指定结点的路径上的所有节点都存储到栈中
*/
private boolean getPath(TreeNode root,TreeNode node,Stack<TreeNode> stack){
if(root==null || node==null) return false;
stack.push(root);
if(root == node){
return true;
}
boolean flag1 = getPath(root.left,node,stack);
if(flag1==true){
return true;
}
boolean flag2 = getPath(root.right,node,stack);
if(flag2==true){
return true;
}
stack.pop();//弹出
return false;
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){
Stack<TreeNode> stack1 = new Stack<>();
getPath(root,p,stack1);
Stack<TreeNode> stack2 = new Stack<>();
getPath(root,q,stack2);
int size1 = stack1.size();
int size2 = stack2.size();
if(size1 > size2){
int s = size1 - size2;
while(s!=0){
stack1.pop();
s--;
}
}else{
int s = size2 - size1;
while(s!=0){
stack2.pop();
s--;
}
}
while(!stack1.empty() && !stack2.empty() ){
if(stack1.peek() == stack2.peek() ){
return stack1.peek();
}else{
stack1.pop();
stack2.pop();
}
}
return null;
}
}
思路二:
如果这棵树是一棵二叉搜索树,那么怎么找公共祖先呢?首先,什么是二叉搜索树?
二叉搜索树的定义:每个结点的左边都比这个结点小,右边都比这个结点大
二叉搜索树的好处:可以帮我们判断出两个结点的分布,是分布在同侧还是分布在异侧
从思路二得到的启发:
一共有三种情况
- p,q有一个为root,则公共祖先就是p或q
- p,q在同一侧,分别去递归左侧和右侧
- p,q在不同侧,则公共祖先是root
所以我们只需要判断p,q两个结点的位置即可,我们可以用递归去搜索判断位置,那么我们就可以不需要是二叉搜索树也能解出题目
我们以这种思路写一下代码吧!
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root==null) return null;
if(p == root || q== root) return root;
//根不是,分别去找根的左边和右边
TreeNode leftRet = lowestCommonAncestor(root.left,p,q);
TreeNode rightRet = lowestCommonAncestor(root.right,p,q);
if(leftRet!=null && rightRet!=null){ //左右都有
return root;
}else if(leftRet!=null){//都在左侧,leftRet的值为先找到的结点,也就是祖先
return leftRet;
}else if(rightRet!=null){ //都在右侧
return rightRet;
}
return null;
}
}
很明显,思路二的时间复杂度更小
9. 二叉树搜索树转换成排序双向链表。
二叉搜索树与双向链表_牛客题霸_牛客网 (nowcoder.com)
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示
分析:
首先,怎么将这棵二叉搜索树排序呢?我们可以发现,二叉搜索树的中序遍历是排好序的,所以我们应该用中序遍历的思想做这道题,我们知道双向链表的每个结点是有前驱和后继的
我们发现,左是前驱,右是后继
下面我们把这个图压扁,就会惊奇地发现~
这不就是一个双向链表吗?!!
思路就是我们以中序遍历的方式去组织这样的双向链表,我们把left作为前驱,把right作为后继
每到一个结点就把它的前驱和后继都绑定好
import java.util.*;
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
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;
}
}
10. 根据一棵树的前序遍历与中序遍历构造二叉树
105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
思路:
- 定义preIndex遍历前序遍历这个数组
- 在中序遍历的 ib到 ie,找到 ri 这个的下标位置
- 此时ri左边的就是左树,ri 右边的就是右树
- 递归创建左树和递归创建右树
- 当 ib > ie 的时候,说明没有了左树或者右树
class Solution {
public int preIndex = 0;
public TreeNode buildTreeChild(int[] preorder,int[] inorder,int inbegin,int inend){
if(inbegin>inend) return null;//递归结束条件,说明此时没有左树或者右树
TreeNode root = new TreeNode(preorder[preIndex]);
//找到根结点在中序遍历当中的位置
int rootIndex = findInorderRootIndex(inorder,inbegin,inend,preorder[preIndex]);
if(rootIndex==-1) return null;
preIndex++;
root.left = buildTreeChild(preorder,inorder,inbegin,rootIndex-1);
root.right = buildTreeChild(preorder,inorder,rootIndex+1,inend);
return root;
}
private int findInorderRootIndex(int[] inorder,int inbegin,int inend,int val){
for(int i = inbegin;i<=inend;i++){
if(inorder[i]==val){
return i;
}
}
return -1;//没有val这个数据
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
return buildTreeChild(preorder,inorder,0,inorder.length-1);
}
}
11.根据一棵树的中序遍历与后序遍历构造二叉树
106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
跟上一题类似,但由于后序遍历是:左-右-根,所以每次postIndex--,并且先创建右树,再创建左树
class Solution {
public int postIndex = 0;
public TreeNode buildTreeChild(int[] postorder,int[] inorder,int inbegin,int inend){
if(inbegin>inend) return null;//递归结束条件,说明此时没有左树或者右树
TreeNode root = new TreeNode(postorder[postIndex]);
//找到根结点在中序遍历当中的位置
int rootIndex = findInorderRootIndex(inorder,inbegin,inend,postorder[postIndex]);
if(rootIndex==-1) return null;
postIndex--;
//先创建右树,再创建左树
root.right = buildTreeChild(postorder,inorder,rootIndex+1,inend);
root.left = buildTreeChild(postorder,inorder,inbegin,rootIndex-1);
return root;
}
private int findInorderRootIndex(int[] inorder,int inbegin,int inend,int val){
for(int i = inbegin;i<=inend;i++){
if(inorder[i]==val){
return i;
}
}
return -1;//没有val这个数据
}
public TreeNode buildTree(int[] inorder, int[] postorder) {
postIndex = postorder.length-1;
return buildTreeChild(postorder,inorder,0,inorder.length-1);
}
}
12.二叉树创建字符串
给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。
空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。
思路:
- 左树不为空加(
- 递归左树
- 当左树全部走完 + )
- 如果当前结点的左右都为空,就什么都不做
- 右树不为空加(
- 递归右树
- 当右树全部走完 + )
class Solution {
public void tree2strChild(TreeNode t,StringBuilder sb){
if(t == null) return;
sb.append(t.val);
if(t.left!=null){
sb.append("(");
tree2strChild(t.left,sb);
sb.append(")");
}else{
if(t.right==null){
return;
}else{
sb.append("()");
}
}
if(t.right == null){
return;
}else{
sb.append("(");
tree2strChild(t.right,sb);
sb.append(")");
}
}
public String tree2str(TreeNode root) {
if(root == null) return null;
StringBuilder sb = new StringBuilder();
tree2strChild(root,sb);
return sb.toString();
}
}
13.二叉树前序非递归遍历实现
非递归实现需要用到栈
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if(root==null) return list;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur!=null || !stack.empty()){
while (cur!=null){
stack.push(cur);
list.add(cur.val);
cur = cur.left;
}
TreeNode top = stack.pop();
cur = top.right;
}
return list;
}
}
14. 二叉树中序非递归遍历实现
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if(root==null) return list;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur!=null || !stack.empty()){
while (cur!=null){
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.pop();
list.add(top.val);
cur = top.right;
}
return list;
}
}
15. 二叉树后序非递归遍历实现
非递归实现后序遍历时需要考虑是不是有结点已经被打印了
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if(root==null) return list;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev = null;
while (cur!=null || !stack.empty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if (top.right == null || top.right == prev) {
stack.pop();
list.add(top.val);
prev = top;//记录一下最近一次被打印的结点,防止重复打印
} else {
cur = top.right;
}
}
return list;
}
}