前言
本篇是剑指Offer编程题中二叉树相关的题,共15道题。其中,题目是按照简单到困难来排序的。二叉树相关的题中,递归是很常见的。因此,得学会找规律并写出递归。
题目
二叉树的深度
第1题
题目描述
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
思路:
递归。若节点为null,则返回0(递归边界)。否则,递归调用左右子节点。最后返回左右子树深度的较大值。
实现如下:
public class Solution {
public int TreeDepth(TreeNode root) {
if(root==null)
return 0;
int leftDepth = TreeDepth(root.left);
int rightDepth = TreeDepth(root.right);
return leftDepth>=rightDepth?leftDepth+1:rightDepth+1;
}
}
二叉树的镜像
第2题
题目描述
操作给定的二叉树,将其变换为源二叉树的镜像。
输入描述:
二叉树的镜像定义:源二叉树
8
/ \
6 10
/ \ / \
5 7 9 11
镜像二叉树
8
/ \
10 6
/ \ / \
11 9 7 5
思路:
递归,交换左右子节点。节点为null是递归边界。
实现如下:
public class Solution {
public void Mirror(TreeNode root) {
if(root==null)
return;
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
Mirror(root.left);
Mirror(root.right);
}
}
判断是否平衡二叉树
第3题
题目描述
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
思路:
平衡二叉树,具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
因此,可以获取左右子树的深度,再来比较差值是否大于1。
实现如下:
public class Solution {
public boolean IsBalanced_Solution(TreeNode root) {
if(root==null)
return true;
int leftDepth = getDepth(root.left);
int rightDepth = getDepth(root.right);
return Math.abs(leftDepth-rightDepth)<=1;
}
//递归获取深度
private int getDepth(TreeNode root){
if(root==null)
return 0;
int leftDepth = getDepth(root.left);
int rightDepth = getDepth(root.right);
return leftDepth>=rightDepth?leftDepth+1:rightDepth+1;
}
}
代码这样写可以通过。但是,平衡二叉树,不是必须是二叉搜索树吗?也就是说:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。
这样,不是应该还得比较节点的大小值吗?
从上往下打印二叉树
第4题
题目描述
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
思路:
按层次遍历。使用中序遍历的话是不行的。得使用一个先进先出的队列(ArrayList也可以),用来存放节点。再使用一个ArrayList,用来存放节点值。然后遍历队列,在遍历过程中先将节点值存入list,再将节点的左右子树又添加进队列中。直至队列为空。
这里之所以把节点值放进list中,是因为题目方法要求的,否则可以直接将“把节点值放进list中”这一操作替换成“向控制台打印节点值”。但如此一来,就是紧耦合了。即如果我想往文件中输入的话,就得改这个方法了。现在,把节点值存到list中,并返回该list。然后你调用该方法获取该list,你想输出到哪就输出到哪。
实现如下:
public class Solution {
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
ArrayList<Integer> list = new ArrayList();
LinkedList<TreeNode> queue = new LinkedList();
queue.offer(root);
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(node!=null){ //不能漏,因为node节点有可能为null
list.add(node.val);
queue.offer(node.left);
queue.offer(node.right);
}
}
return list;
}
}
按之字形顺序打印二叉树
第5题
题目描述
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
思路:
这题和按层次遍历二叉树很像,按层次遍历使用的是先进先出的队列,而按之字形遍历二叉树则是使用后进先出的栈。错错错!!!!画图分析一下,就会发现只用一个栈是无法实现的,得用两个栈来实现。一个栈存的是从左往右的(奇数层),另一个是从右往左的(偶数层)。
实现如下:
注意,这段代码放到牛客网对应这道编程题的网页上提交是运行不通过的。因为网页上方法的返回值类型是ArrayList<ArrayList<Integer> >
,即把每层的数据都放在一个list中,再把各个list放到一个lists中。我这里,是把所有的数据都放在一个list里。
public ArrayList<Integer> Print(TreeNode pRoot) {
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
ArrayList<Integer> list = new ArrayList();
stack1.push(pRoot);
while(!stack1.isEmpty() || !stack2.isEmpty()){
while(!stack1.empty()){
TreeNode node = stack1.pop();
if(node!=null){
list.add(node.val);
stack2.push(node.left);
stack2.push(node.right);
}
}
while(!stack2.isEmpty()){
TreeNode node = stack2.pop();
if(node!=null){
list.add(node.val);
stack1.push(node.right);
stack1.push(node.left);
}
}
}
return list;
}
如果要把每行的数据都放在一个list中,再把这些list都装在一个大的list中,其实现也和上面的实现很相似:
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
ArrayList<ArrayList<Integer> > lists = new ArrayList();
stack1.push(pRoot);
while(!stack1.isEmpty() || !stack2.isEmpty()){
ArrayList<Integer> list1 = new ArrayList();
while(!stack1.empty()){
TreeNode node = stack1.pop();
if(node!=null){
list1.add(node.val);
stack2.push(node.left);
stack2.push(node.right);
}
}
if(!list1.isEmpty())
lists.add(list1);
ArrayList<Integer> list2 = new ArrayList();
while(!stack2.isEmpty()){
TreeNode node = stack2.pop();
if(node!=null){
list2.add(node.val);
stack1.push(node.right);
stack1.push(node.left);
}
}
if(!list2.isEmpty())
lists.add(list2);
}
return lists;
}
}
把二叉树打印成多行
第6题
题目描述
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
思路:
如果上面两题能弄懂,这题也很简单了。这道题就是按层次遍历,并且每行的数据都存储在不同的list中。思路就是使用两个队列来分别存储奇数层数据和偶数层数据。
实现如下:
public class Solution {
ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer> > lists = new ArrayList();
LinkedList<TreeNode> queue1 = new LinkedList();
LinkedList<TreeNode> queue2 = new LinkedList();
queue1.offer(pRoot);
while(!queue1.isEmpty() || !queue2.isEmpty()){
ArrayList<Integer> list1 = new ArrayList();
while(!queue1.isEmpty()){
TreeNode node = queue1.poll();
if(node!=null){
list1.add(node.val);
queue2.offer(node.left);
queue2.offer(node.right);
}
}
if(!list1.isEmpty())
lists.add(list1);
ArrayList<Integer> list2 = new ArrayList();
while(!queue2.isEmpty()){
TreeNode node = queue2.poll();
if(node!=null){
list2.add(node.val);
queue1.offer(node.left);
queue1.offer(node.right);
}
}
if(!list2.isEmpty()){
lists.add(list2);
}
}
return lists;
}
}
是否对称的二叉树
第7题
题目描述
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
思路:
判断左节点的右子节点是否和右结点的左子节点相等,以及左结点的左子节点是否和右结点的右子节点相等。镜像对称。然后递归判断。
实现如下:
public class Solution {
boolean isSymmetrical(TreeNode pRoot)
{
if(pRoot==null)
return true;
return isSymmetrical(pRoot.left,pRoot.right);
}
boolean isSymmetrical(TreeNode left,TreeNode right){
if((left==null&&right!=null) || (left!=null&&right==null))//这两行是多余的!
return false; //这两行是多余的!
if(left==null&&right==null)
return true;
if(left!=null && right!=null)
if(left.val==right.val)
return isSymmetrical(left.right,right.left)&&isSymmetrical(left.left,right.right);
return false;
}
}
简洁版本:
public class Solution {
boolean isSymmetrical(TreeNode pRoot)
{
if(pRoot==null)
return true;
return isSymmetrical(pRoot.left,pRoot.right);
}
boolean isSymmetrical(TreeNode lNode,TreeNode rNode){
if(lNode==null && rNode==null)
return true;
if(lNode!=null && rNode!=null){
return lNode.val==rNode.val && isSymmetrical(lNode.left,rNode.right) && isSymmetrical(rNode.left,lNode.right);
}
return false;
}
}
二叉搜索树变双向链表
第8题
题目描述
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
思路:
通过中序遍历实现,关键是要创建一个成员引用pre。pre指向上一个节点。通过在遍历中设置pre.right=current,current.left=pre,pre=current
,即可将二叉树转变成双向链表。
实现如下:
public class Solution {
TreeNode pre = null;
public TreeNode Convert(TreeNode pRootOfTree) {
TreeNode p = pRootOfTree;
if(p==null)
return p;
middleTraverse(p);
//通过向左遍历找到链表的首结点
while(p.left!=null)
p = p.left;
return p;
}
//中序遍历
public void middleTraverse(TreeNode cur){
if(cur==null)
return;
middleTraverse(cur.left);
//关键,重新调整节点的指针
if(pre!=null)
pre.right = cur;
cur.left = pre;
pre = cur;
middleTraverse(cur.right);
}
}
二叉树中和为某一值的路径
第9题
题目描述
输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
思路:
递归。访问节点并将节点值加到list中,直到访问到叶子节点。统计list中的数据的和是否为target。是则加到lists中。
实现如下:
public class Solution {
//lists作为全局变量,这样就不用往每个find方法传递该值了
ArrayList<ArrayList<Integer>> lists = new ArrayList();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
ArrayList<Integer> list = new ArrayList();
find(root,target,list);
return lists;
}
//递归方法
private void find(TreeNode root,int target,ArrayList<Integer> list){
//当节点为null,则直接返回
if(root==null)
return;
//否则,将节点val值加入list
list.add(root.val);
//当节点为叶子节点的时候,计算该路径是否符合要求,符合则将list加到lists中
if(root.left==null&&root.right==null){
int result =0;
for(int i=0;i<list.size();i++){
result += list.get(i);
}
if(result == target)
lists.add(list);
}
//否则,递归递归调用
else{
//注意,要重新构建容器,并将旧值存入其中
find(root.left,target,new ArrayList(list));
find(root.right,target,new ArrayList(list));
}
}
}
另一种实现,递归,也是深度遍历:
public class Solution {
private ArrayList<ArrayList<Integer>> lists = new ArrayList();
private ArrayList<Integer> list = new ArrayList();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
if(root==null)
return lists;
list.add(root.val);
target -= root.val;
if(target==0 && root.left==null && root.right==null){
lists.add(new ArrayList(list));
}
FindPath(root.left,target);
FindPath(root.right,target);
list.remove(list.size()-1);
return lists;
}
}
上一种实现是通过累加list中的节点值总和,这一个实现则是通过判断target是否为0。相比之下,这种实现更加高效。
另外,list存储的始终是当前路径的各个节点值。若访问完经过该节点路径,则会从list中剔除该节点。
因此最后有调用list.remove方法。
二叉搜索树的第k大的节点
第10题
题目描述
给定一颗二叉搜索树,请找出其中的第k大的结点。例如, 5 / \ 3 7 /\ /\ 2 4 6 8 中,按结点数值大小顺序第三个结点的值为4。
思路:
二叉搜索树的中序遍历,便是按照节点值的升序遍历。因此,可以通过中序遍历将节点放到list中。再取出k-1位置的节点,该节点便是第k大的节点。
实现如下:
public class Solution {
TreeNode KthNode(TreeNode pRoot, int k)
{
ArrayList<TreeNode> list = new ArrayList();
middleTraverse(pRoot,list);
if(k<=0 || k>list.size())
return null;
return list.get(k-1);
}
private void middleTraverse(TreeNode node,ArrayList<TreeNode> list){
if(node==null)
return;
middleTraverse(node.left,list);
list.add(node);
middleTraverse(node.right,list);
}
}
思路2:
中序遍历的结果即为升序排序的结果。现在要找第k大的节点,不就是中序遍历的第k个节点吗。那么只要找到第k个节点并返回就可以了。也就是说无需使用一个容器来盛装节点,即空间复杂度为O(1)。
实现如下:
public class Solution {
private int index = 0;
TreeNode KthNode(TreeNode pRoot, int k)
{
if(pRoot==null)
return null;
TreeNode node = null;
//处理左子树
node = KthNode(pRoot.left,k);//遍历完左子树要判断是否有找目标节点,找到则返回
if(node!=null)
return node;
//处理根节点
index++;
if(index==k)
return pRoot;
//处理右子树
node = KthNode(pRoot.right,k);//遍历完右子树要判断是否有找目标节点,找到则返回
if(node!=null)
return node;
return null;
}
}
是否二叉搜索树的后序遍历序列
第11题
题目描述
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
思路:
二叉搜索树的特点,左子树的节点均小于根节点,右子树的节点均大于根节点。
后序遍历(LRD)是二叉树遍历的一种,也叫做后根遍历,可记做左右根。
那么举例来说,
二叉搜索树
8
/ \
6 10
/ \ / \
5 7 9 11
它的后续遍历序列的数组是[5,7,6,9,11,10,8],那么根据上面说的特性,8为根节点,在数组的最后,左边部分是左子树,中间部分是右子树。分别验证左子树是否都小于根节点,右子树是否都大于根节点,若不符合,则返回false。然后对左右子树递归调用。
实现如下:
public class Solution {
public boolean VerifySquenceOfBST(int [] sequence) {
if(sequence.length==0)
return false;
return check(sequence,0,sequence.length-1);
}
private boolean check(int[] a,int l,int r){
if(l==r)
return true;
int m = r-1; //m表示的是左子树的右区间
while(m>l && a[m]>a[r]) //不要漏了m>l //找出右子树部分,那么剩下的就是属于左子树了
m--;
for(int i=l;i<m;i++) //判断左子树部分是否合法
if(a[i]>a[r])
return false;
return check(a,l,m) || check(a,m+1,r-1);
}
}
B树是否A树的子结构
第12题
题目描述
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
思路:
先找到B的根节点在A中的位置(可能有多个),然后再判断B的每个节点是否在A中都有对应的。
实现如下:
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
boolean result =false;
if(root1!=null&&root2!=null){ //如果root1为null或者root2为null,则直接返回false
if(root1.val==root2.val)
result = isSub(root1,root2);
if(!result)
result = HasSubtree(root1.left,root2); //检查左子树
if(!result)
result = HasSubtree(root1.right,root2); //检查右子树
}
return result;
}
//找到相同的节点后,调用该方法判断是否子结构
//递归
private boolean isSub(TreeNode n1,TreeNode n2){
if(n2==null)
return true;
if(n1==null)
return false;
if(n1.val==n2.val)
return isSub(n1.left,n2.left)&&isSub(n1.right,n2.right);
return false;
}
}
序列化二叉树
第13题
题目描述
请实现两个函数,分别用来序列化和反序列化二叉树
思路:
通过一种遍历方式将节点值拼成一个字符串,即序列化,反序列化时也要使用同一种遍历方式。这里采用先序遍历。
我的错误实现(正确实现在后面):
public class Solution {
String Serialize(TreeNode root) {
StringBuffer sb = new StringBuffer();
Serialize2(root,sb);
return sb.toString();
}
TreeNode Deserialize(String str) {
char ch[] = str.toCharArray();
return Deserialize2(ch);
}
private void Serialize2(TreeNode root,StringBuffer sb){
if(root==null){
sb.append("#");
return;
}
//先根遍历
sb.append(root.val); //根
Serialize2(root.left,sb); //左
Serialize2(root.right,sb); //右
}
//先序遍历反序列化
int index = -1;
private TreeNode Deserialize2(char[] ch){
index++;
if(ch[index]=='#')
return null;
TreeNode node = new TreeNode(Integer.valueOf(ch[index]));
node.left = Deserialize2(ch);
node.right = Deserialize2(ch);
return node;
}
}
还没搞懂,为什么这种实现会发生数组下标外溢。懂了再补上。
参考答案实现如下:
public class Solution {
String Serialize(TreeNode root) {
StringBuffer sb = new StringBuffer();
Serialize2(root,sb);
return sb.toString();
}
TreeNode Deserialize(String str) {
String[] s = str.split("!");
return Deserialize2(s);
}
private void Serialize2(TreeNode root,StringBuffer sb){
if(root==null){
sb.append("#!");
return;
}
//先根遍历
sb.append(root.val).append("!"); //根
Serialize2(root.left,sb); //左
Serialize2(root.right,sb); //右
}
//先序遍历,反序列化
int index = -1; //全局变量
private TreeNode Deserialize2(String[] s){
index++;
if(s[index].equals("#"))
return null;
TreeNode node = new TreeNode(Integer.valueOf(s[index]));
node.left = Deserialize2(s);
node.right = Deserialize2(s);
return node;
}
}
求二叉树的下一个节点
第14题
题目描述
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
第二次做还是没能做出来。
参考答案实现如下:
public class Solution {
public TreeLinkNode GetNext(TreeLinkNode pNode)
{
if(pNode==null)
return null;
//有右子树的情况,则返回右子树最左的节点
if(pNode.right!=null){
pNode = pNode.right;
while(pNode.left!=null)
pNode = pNode.left;
return pNode;
}
//无右子树的情况
while(pNode.next!=null){
TreeLinkNode p = pNode.next;
if(p.left==pNode)
return p;
pNode = pNode.next;
}
return null;
}
}
重建二叉树
第15题
题目描述
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
这题也没能做出来。
思路:
找规律,递归解决:
前根遍历序列{1,2,4,7,3,5,6,8}
中根遍历序列{4,7,2,1,5,3,8,6}
找根节点。很明显,前根遍历的第一个数字是根节点,即1是根节点。然后在中根遍历序列中,1左边的是左子树,1右边的是右子树。然后设置根节点的左右子节点。左右子节点从哪来呢。对左子树部分和右子树部分调用获取其根节点。
然后对左右子树递归找根节点。
实现如下:
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
if(pre.length==0)
return null;
return build(0,pre.length-1,pre,0,in.length-1,in);
}
private TreeNode build(int preL,int preR,int[] pre,int inL,int inR,int[] in){
if(preL>preR || inL>inR) //递归边界
return null;
TreeNode node = new TreeNode(pre[preL]);
for(int i=inL;i<=inR;i++){
if(pre[preL]==in[i]){
node.left = build(preL+1,preL+i-inL,pre,inL,i-1,in);
node.right = build(preL+i-inL+1,preR,pre,i+1,inR,in);
break;
}
}
return node;
}
}
总结
二叉树的题,很多都可以使用递归解决。
还得理解先序遍历、中序遍历、后序遍历之间的关系与特别之处,如知道先序遍历、中序遍历就可以确定该二叉树的结构,如二叉搜索树的中序遍历即节点值的升序访问。
第456道题是相似的题,按层次遍历和之字形遍历,使用一个或多个容器(Stack、Queue)来存储节点。
像14题求二叉树的下一个节点,这就要观察,找到规律。
而最重要的,是学会写递归。这次虽然是第二次做,但是写递归的时候总是出现各种问题。递归的书写,还得加强。