【剑指Offer】英文版新增面试题(中)

本文深入探讨了二叉树的多种算法,包括查找中序遍历下一个结点、判断二叉树是否对称、多行打印、之字形打印、序列化与反序列化、查找第k小结点及数据流中位数等,提供了详细的分析和Java代码实现。

二叉树的下一个结点

题目描述

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
树的结点定义如下

public class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}

分析

在这里插入图片描述
所有的结点根据其有无右子树,可以分为四种情况。之所以不考虑其左子树是因为是中序遍历,即使有左子树,左子树中的结点都在该节点之前遍历完了,不会对下一个结点产生影响。

四种情况分别为:
Case1: 给定的结点有右子树,右子树中存在结点有左孩子
下一个节点就是右子树中的最左结点。

示例: 结点 b
b有右孩子e,e有左孩子g,g为b的右子树中的最左结点,所以b的下一个结点为g。

Case2: 给定的结点有右子树,右子树中不存在结点有左孩子
下一个节点该结点的右孩子。

示例: 结点 c
c有右孩子f,f有右孩子i,所以b的右子树中不存在结点有左孩子,所以c的下一个结点是f。

Case3: 给定的结点无右子树,且该结点是其父结点的孩子、
下一个结点就是其父结点。

示例: 结点 d
d没有右孩子,但是d是b的左孩子,所以d的下一个结点是b。

Case4: 给定的结点无右子树,且该结点是其父结点的孩子,则向上追溯,直到其某个祖先结点是其父结点的左孩子,则下个结点是该祖先结点的父结点。若追溯不到满足条件的结点,则表示中序遍历结束。

示例1: 结点 h
h没有右子树,但h是e的右孩子,继续向上追溯,e是b的右孩子,直到追溯到b是a的左孩子,则h的下个结点是a;
示例2: 结点 i
i没有右子树,但是i是f的右孩子,继续向上追溯,f是c的右孩子,c是a的右孩子,而a是根节点,所以i后面没有节点了。

在这里插入图片描述

Java代码

public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode)
    {
        if(pNode == null)
            return null;
        
        TreeLinkNode p = pNode;
        
        if(pNode.right != null){
            pNode = pNode.right;
            while(pNode.left==null && pNode.right!=null) 
                pNode = pNode.right;
            // case 1
            if(pNode.left != null)
                p = pNode.left;
            // case 2
            else
                p = p.right;
        }
        else if(pNode.next != null) {
            if(pNode.next.left == pNode)
                return pNode.next;
            else{
                while(pNode.next!=null && pNode.next.left!=pNode)
                    pNode = pNode.next;
                // case 3
                if(pNode.next != null)
                    p = pNode.next;
                // case 4
                else
                    p =  null;
            }
        }
        // 该结点为根节点,且没有右孩子
        else
            p = null;
        return p;
    }
}

对称的二叉树

题目描述

请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

树的结点的定义:

public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}

分析

如下图所示
在这里插入图片描述
第一棵树是对称的,第二棵和第三棵不是对称的。
已知树的三种遍历方法:先序遍历、中序遍历、后序遍历都是先访问左子结点,再访问右子结点,以先序遍历为例,第一棵树的遍历结果为:8657675。

若定义一种新的遍历方式,按照“根-右-左”的顺序进行遍历,结果为:8657875,和先序遍历的结果是一样的。

注意:对于像第三棵树这样的不是完美二叉树,将空结点用“null”代替。

Java代码

public class Solution {
    boolean isSymmetrical(TreeNode pRoot)
    {
        if(pRoot == null)
            return true;
        return symmetrical(pRoot, pRoot);
    }
    
    boolean symmetrical(TreeNode p1, TreeNode p2){
    	// 形状
    	// 1. p1和p2都为空
        if(p1==null && p2==null)
            return true;
        // 2. p1或p2一个为空一个不为空;
        if(p1==null || p2==null)
            return false;
        // 数值
        if(p1.val != p2.val)
            return false;
        
        return symmetrical(p1.left, p2.right) && symmetrical(p1.right, p2.left);
    }
}

把二叉树打印成多行

题目描述

从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
树的结点的定义:

public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}

分析

在这里插入图片描述
有两种方法,第一种方法是递归,第二种方法类似于层次遍历。

方法1:递归
递归的时候判断当前结点所在的深度,如果深度增加了,就将ArrayList进行扩容。

方法2:层次遍历
每遍历一个层次的结点的时候,就依次记录下其左右结点,并统计下一层的结点总数。

Java代码

方法1:递归

import java.util.ArrayList;
public class Solution {
    ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> res =  new ArrayList<ArrayList<Integer>>();
        visit(pRoot, 0, res);
        return res;
    }
    
    public void visit(TreeNode root, int depth, ArrayList<ArrayList<Integer>> res){
        if(root == null)
            return;
        if(depth >= res.size())
            res.add(new ArrayList<Integer>());
        res.get(depth).add(root.val);
        
        visit(root.left, depth+1, res);
        visit(root.right, depth+1, res);
    }
}

方法2:层次遍历

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
public class Solution {
    ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> res =  new ArrayList<ArrayList<Integer>>();
        if(pRoot == null)
            return res;
        // 队列用于层次遍历
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        // list用于记录当前行的结果
        ArrayList<Integer> list = new ArrayList<Integer>();
        queue.add(pRoot);
        int cntOfCurLayer = 1;
        int cntOfNextLayer = 0;
        while(!queue.isEmpty()){
            // 层次遍历
            TreeNode temp = queue.remove();
            if(temp.left != null){
                queue.add(temp.left);
                cntOfNextLayer++;
            }
            if(temp.right != null){
                queue.add(temp.right);
                cntOfNextLayer++;
            }

            list.add(temp.val);
            cntOfCurLayer--;
            if(cntOfCurLayer == 0){
                res.add(list);
                list = new ArrayList<Integer>();
                cntOfCurLayer = cntOfNextLayer;
                cntOfNextLayer = 0;
            }
        }
        return res;
    }
}

按之字形打印二叉树

题目描述

请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
树的结点的定义:

public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}

分析

使用两个辅助栈s1、s2和一个整型flag。
flag=0用来标志从左向右读,flag=1表示从右向左读。

初始化,将根节点入栈s1,flag=0;

flag=0时,从s1中依次pop出结点temp,再将temp的左右孩子入栈s2,由于下一轮是从右向左读,且stack是先进后出,所以先入栈左孩子,再入栈右孩子。

flag=1时,从s2中依次pop出结点temp,与上述相反,先入栈右孩子,再入栈左孩子。

在这里插入图片描述
输出结果为{{8}, {10, 6}(5,7,9,4)}

Java代码

public class Solution {
    public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
        if(pRoot == null)
            return res;

        int flag = 0;
        Stack<TreeNode> s1 = new Stack<TreeNode>();
        Stack<TreeNode> s2 = new Stack<TreeNode>();
        s1.push(pRoot);

        ArrayList<Integer> list = new ArrayList<Integer>();
        while(!s1.empty() || !s2.empty()){
            if(flag == 0){
                while(!s1.empty()){
                    TreeNode temp = s1.pop();
                    list.add(temp.val);
                    if(temp.left != null)
                        s2.push(temp.left);
                    if(temp.right != null)
                        s2.push(temp.right);
                }
            }
            else{
                while(!s2.empty()){
                    TreeNode temp = s2.pop();
                    list.add(temp.val);
                    if(temp.right != null)
                        s1.push(temp.right);
                    if(temp.left != null)
                        s1.push(temp.left);
                }
            }
            res.add(list);
            list = new ArrayList<Integer>();
            flag = 1 - flag;
        }
        return res;
    }
}

序列化二叉树

题目描述

请实现两个函数,分别用来序列化和反序列化二叉树。

  1. 对于序列化:使用前序遍历,递归的将二叉树的值转化为字符,并且在每次二叉树的结点不为空时,在转化val所得的字符之后添加一个’ , '作为分割。对于空节点则以 ‘#’ 代替。
  2. 对于反序列化:按照前序顺序,递归的使用字符串中的字符创建一个二叉树。

树的结点的定义:

public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}

分析

在这里插入图片描述
使用递归的思想。

  • 序列化的时候,当一个结点不是空结点就将其添加到序列中,并递归处理其左孩子和右孩子。如果结点为空,则将"#"添加到序列中。
  • 反序列化的时候,将字符串处理根据逗号split成String数组,然后进行递归处理。如果读取的字符串不为“#”,表示这个结点存在,然后递归处理其左孩子和右孩子。否则说明这是个空结点,返回null。

注意:在序列化的时候,使用StringBuilder而不是String来传递参数。可参考:String,StringBuffer与StringBuilder的区别??

Java代码

public class Solution {
    String Serialize(TreeNode root) {
        if(root == null)
            return "";
        StringBuilder str = new StringBuilder();
        serializeCore(root, str);
        return str.toString();
    }
    void serializeCore(TreeNode root, StringBuilder str){
        if(root == null){
            str.append("#,");
            return;
        }
        str.append(root.val);
        str.append(",");
        serializeCore(root.left, str);
        serializeCore(root.right, str);
    }

    TreeNode Deserialize(String str) {
        if(str.length() == 0)
            return null;
        String[] strs = str.split(",");
        return deserializeCore(strs);
    }
    int idx = -1;
    TreeNode deserializeCore(String[] strs){
        idx++;
        if(!strs[idx].equals("#")){
            TreeNode root = new TreeNode(0);
            root.val = Integer.parseInt(strs[idx]);
            root.left = deserializeCore(strs);
            root.right = deserializeCore(strs);
            return root;
        }
        return null;
    }
}

二叉搜索树的第k个结点

题目描述

给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

分析

在这里插入图片描述
二叉搜索树的中序遍历结果是从小到大排序的,所以二叉搜索树的第k小的结点就是其中序遍历的第k个元素。

Java代码

public class Solution {
    int i = 0;
    TreeNode KthNode(TreeNode pRoot, int k)
    {
        if(pRoot != null){
            TreeNode node = KthNode(pRoot.left, k);
            if(node != null)
                return node;
            i++;
            if(i == k)
                return pRoot;
            node = KthNode(pRoot.right, k);
            if(node != null)
                return node;
        }
        return null;
    }
}

数据流中的中位数

题目描述

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

分析

参考:剑指Offer 【面试题63-数据流中的中位数】
使用最大堆和最小堆来过滤数据。
最大堆用来存放较小的数字,从大到小排列,则其根节点是较小数字中的最大值;
最小堆用来存放较大的数字,从小到大排列,则其根节点是较大数字中的最小值。
保证最小堆中的元素都大于最大堆的元素。

当读入一个新的数据前,

  • 已经读取了偶数个数据,则将新数据放到最大堆中,然后再将最大堆中的最大值放到最小堆中,中位数是最小堆的根元素;
  • 已经读取了奇数个数据,则将新数据放到最小堆中,然后再将最小堆中的最小值放到最大堆中,中位数是两个根元素的平均值;

示例:假设数据流为[5,2,3,4,1,6,7,0,8],
在这里插入图片描述
在Java中,使用PriorityQueue来表示最大堆和最小堆。

堆是树的应用,虽然没有直接用到树的结构,但是这个方法和使用平衡的二叉搜索树AVL的时间复杂度一样:

  • 插入的时间复杂度为O(logn)
  • 得到中位数的时间复杂度为O(1)。

Java代码

import java.util.PriorityQueue;
import java.util.Comparator;

public class Solution {
    Comparator<Integer> comparator = new Comparator<Integer>() {
        @Override
        public int compare(Integer x, Integer y) {
            return y - x;
        }
    };
    private PriorityQueue<Integer> maxHeap = new PriorityQueue<>(comparator);
    private PriorityQueue<Integer> minHeap = new PriorityQueue<>();

    int readyCnt = 0;
    public void Insert(Integer num) {
        if(readyCnt%2 == 0){
            maxHeap.offer(num);
            minHeap.offer(maxHeap.poll());
        }
        else{
            minHeap.offer(num);
            maxHeap.offer(minHeap.poll());
        }
        readyCnt++;
    }

    public Double GetMedian() {
        if(readyCnt%2 == 0)
            return new Double(maxHeap.peek() + minHeap.peek()) / 2;
        else
            return new Double(minHeap.peek());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值