剑指offer第61-67题
  • 61、序列化二叉树

  • 62、二叉搜索树的第K个结点

  • 63、数据流中的中位数

  • 64、滑动窗口的最大值

  • 65、矩阵中的路径

  • 66、机器人的运动范围

  • 67、剪绳子

  • 总结

61、序列化二叉树

题目描述: 请实现两个函数,分别用来序列化和反序列化二叉树

二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 !表示一个结点值的结束(value!)。

二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树

例如,我们可以把一个只有根节点为1的二叉树序列化为"1,",然后通过自己的函数来解析回这个二叉树

思路:这道题的目的很明确,就是给定一棵二叉树,能够序列化为一个字符串;给定一个字符串,能够反序列化一棵二叉树。

  • 序列化为字符串
秒杀剑指offer(终篇)_中位数

根据题意,序列化的结果为1!2!4!#!#!5!#!#!3!6!#!#!7!#!#!

序列化相对简单些,只需要在遇到null的时候添加#!号,遇到数值添加!即可

  • 反序列化二叉树

反序列化的时候,由于采用的是先序遍历,此时如果遇到了#号,我们知道左边结束了,要开启右边,如果再次遇到#,表示当前整个部分的左边结束了要开始右子树。。依次类推。

代码实现

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

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

    }

}
*/
public class Solution {
    String Serialize(TreeNode root) {
        if(root==null) return "";
        return helpSerialize(root,new StringBuilder()).toString();
        
  }
    private StringBuilder helpSerialize(TreeNode root,StringBuilder s){
        if(root ==null) return s;
        s.append(root.val).append("!");
        if(root.left!=null){
            helpSerialize(root.left,s);
        }else{
            s.append("#!");
        }
        if(root.right!=null){
            helpSerialize(root.right,s);
        }else{
            s.append("#!");
        }
        return s;
    }
    //设置全局主要是遇到#号时需要直接前进并返回null
    private int index=0;
    TreeNode Deserialize(String str) {
       if(str==null||str.length()==0) return null;
        String[] split=str.split("!");
        return helpDeserialize(split);
  }
    private TreeNode helpDeserialize(String[] strings){
        if(strings[index].equals("#")){
            index++;//数据前进
            return null;
        }
        TreeNode root=new TreeNode(Integer.valueOf(strings[index]));
        index++;//index++到达下一个需要反序列化的值
        root.left=helpDeserialize(strings);
        root.right=helpDeserialize(strings);
        return root;
    }
}

62、二叉搜索树的第K个结点

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

思路:二叉搜索树的中序遍历的顺序打印与二叉排序树相同,所以我们直接可以采用中序遍历进行查找,返回即可

代码

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

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

    }

}
*/
public class Solution {
     private TreeNode target = null;
    private int k1 = 0;
    TreeNode KthNode(TreeNode pRoot, int k)
    {
        k1 = k;
        getKthNode(pRoot);
        return target;
    }
     
    private void getKthNode(TreeNode pRoot){
        if(pRoot == null || k1 <= 0){
            return;
        }
        getKthNode(pRoot.left);
        k1--;
        if(k1 == 0){
            target = pRoot;
            return;
        }
        getKthNode(pRoot.right);
    }
}

63、数据流中的中位数

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

思路:

  • 先用java集合PriorityQueue来设置一个小顶堆和大顶堆

  • 主要的思想是:因为要求的是中位数,那么这两个堆,大顶堆用来存较小的数,从大到小排列;

  • 小顶堆存较大的数,从小到大的顺序排序*,显然中位数就是大顶堆的根节点与小顶堆的根节点和的平均数。

  • 保证:小顶堆中的元素都大于等于大顶堆中的元素,所以每次塞值,并不是直接塞进去,而是从另一个堆中poll出一个最大(最小)的塞值

  • 当数目为偶数的时候,将这个值插入大顶堆中,再将大顶堆中根节点(即最大值)插入到小顶堆中;

  • 当数目为奇数的时候,将这个值插入小顶堆中,再讲小顶堆中根节点(即最小值)插入到大顶堆中;

  • 取中位数的时候,如果当前个数为偶数,显然是取小顶堆和大顶堆根结点的平均值;如果当前个数为奇数,显然是取小顶堆的根节点

    理解了上面所述的主体思想,下面举个例子辅助验证一下。

    例如,传入的数据为:[5,2,3,4,1,6,7,0,8],那么按照要求,输出是"5.00 3.50 3.00 3.50 3.00 3.50 4.00 3.50 4.00 "

    那么整个程序的执行流程应该是(用min表示小顶堆,max表示大顶堆):

  • 5先进入大顶堆,然后将大顶堆中最大值放入小顶堆中,此时min=[5],max=[无],avg=[5.00]

  • 2先进入小顶堆,然后将小顶堆中最小值放入大顶堆中,此时min=[5],max=[2],avg=[(5+2)/2]=[3.50]

  • 3先进入大顶堆,然后将大顶堆中最大值放入小顶堆中,此时min=[3,5],max=[2],avg=[3.00]

  • 4先进入小顶堆,然后将小顶堆中最小值放入大顶堆中,此时min=[4,5],max=[3,2],avg=[(4+3)/2]=[3.50]

  • 1先进入大顶堆,然后将大顶堆中最大值放入小顶堆中,此时min=[3,4,5],max=[2,1],avg=[3/00]

  • 6先进入小顶堆,然后将小顶堆中最小值放入大顶堆中,此时min=[4,5,6],max=[3,2,1],avg=[(4+3)/2]=[3.50]

  • 7先进入大顶堆,然后将大顶堆中最大值放入小顶堆中,此时min=[4,5,6,7],max=[3,2,1],avg=[4]=[4.00]

  • 0先进入小顶堆,然后将小顶堆中最大值放入小顶堆中,此时min=[4,5,6,7],max=[3,2,1,0],avg=[(4+3)/2]=[3.50]

  • 8先进入大顶堆,然后将大顶堆中最小值放入大顶堆中,此时min=[4,5,6,7,8],max=[3,2,1,0],avg=[4.00]

代码

import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
    //小顶堆,默认为从小到大排列
    private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>();
    
    //大顶堆
    private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new Comparator<Integer>() {
        //重写排序方法,从大到小
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    
    //记录偶数个还是奇数个
    int count = 0;
    //每次插入小顶堆的是当前大顶堆中最大的数
    //每次插入大顶堆的是当前小顶堆中最小的数
    //这样保证小顶堆中的数永远大于等于大顶堆中的数
    //中位数就可以方便地从两者的根结点中获取了
    public void Insert(Integer num) {
        //个数为偶数的话,则先插入到大顶堆,然后将大顶堆中最大的数插入小顶堆中
        if(count % 2 == 0){
            maxHeap.offer(num);
            int max = maxHeap.poll();
            minHeap.offer(max);
        }else{
            //个数为奇数的话,则先插入到小顶堆,然后将小顶堆中最小的数插入大顶堆中
            minHeap.offer(num);
            int min = minHeap.poll();
            maxHeap.offer(min);
        }
        count++;
    }
    public Double GetMedian() {
        //当前为偶数个,则取小顶堆和大顶堆的堆顶元素求平均
        if(count % 2 == 0){
            return new Double(minHeap.peek() + maxHeap.peek())/2;
        }else{
            //当前为奇数个,则直接从小顶堆中取元素即可
            return new Double(minHeap.peek());
        }
    }
}

64、滑动窗口的最大值

题目描述:给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5};针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个:     {[2,3,4],2,6,2,5,1},  {2,[3,4,2],6,2,5,1},      {2,3,[4,2,6],2,5,1},      {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1},      {2,3,4,2,6,[2,5,1]}。

思路:

  • 用一个大顶堆,保存当前滑动窗口中的数据。滑动窗口每次移动一格,就将前面一个数出堆,后面一个数入堆。
  • 使用双指针

代码一

import java.util.*;
public class Solution {
 public PriorityQueue<Integer> maxQueue = new PriorityQueue<Integer>((o1,o2)->o2-o1);//大顶堆
    public ArrayList<Integer> result = new ArrayList<Integer>();//保存结果
    public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
        if(num==null || num.length<=0 || size<=0 || size>num.length){
            return result;
        }
        int count=0;
        for(;count<size;count++){//初始化滑动窗口
            maxQueue.add(num[count]);
        }
        while(count<num.length){//对每次操作,找到最大值(用优先队列的大顶堆),然后向后滑动(出堆一个,入堆一个)
            result.add(maxQueue.peek());
            maxQueue.remove(num[count-size]);
            maxQueue.add(num[count]);
            count++;
        }
        result.add(maxQueue.peek());//最后一次入堆后没保存结果,这里额外做一次即可
 
        return result;
    }
}

代码二

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
        ArrayList<Integer> result=new ArrayList<>();
        int max=0;
        if(num.length<=0||size<=0||size>num.length){
            return result;
        }
        for(int i=0;i<=num.length-size;i++){
            max=num[i];
            for(int j=i;j<size+i;j++){
                if(max<num[j]){
                    max=num[j];
                }
            }
            result.add(max);
        }
        return result;
    }
}

65、矩阵中的路径

题目描述

秒杀剑指offer(终篇)_中位数_02

思路

0.根据给定数组,初始化一个标志位数组,初始化为false,表示未走过,true表示已经走过,不能走第二次

1.根据行数和列数,遍历数组,先找到一个与str字符串的第一个元素相匹配的矩阵元素,进入judge

2.根据i和j先确定一维数组的位置,因为给定的matrix是一个一维数组

3.确定递归终止条件:越界,当前找到的矩阵值不等于数组对应位置的值,已经走过的,这三类情况,都直接false,说明这条路不通

4.若k等于字符串长度,就是待判定的字符串str的索引已经判断到了最后一位,此时说明是匹配成功的

5.下面就是本题的精髓,递归不断地寻找周围四个格子是否符合条件,只要有一个格子符合条件,就继续再找这个符合条件的格子的四周是否存在符合条件的格子,直到k到达末尾或者不满足递归条件就停止。

6.走到这一步,说明本次是不成功的,我们要还原一下标志位数组index处的标志位,进入下一轮的判断。

代码

public class Solution {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
    {
      //标志位,初始化为false
        boolean[] flag = new boolean[matrix.length];
        for(int i=0;i<rows;i++){
            for(int j=0;j<cols;j++){
                 //循环遍历二维数组,找到起点等于str第一个元素的值,再递归判断四周是否有符合条件的----回溯法
                 if(judge(matrix,i,j,rows,cols,flag,str,0)){
                     return true;
                 }
            }
        }
        return false;
    }
    
//judge(初始矩阵,索引行坐标i,索引纵坐标j,矩阵行数,矩阵列数,待判断的字符串,字符串索引初始为0即先判断字符串的第一位)
    private boolean judge(char[] matrix,int i,int j,int rows,int cols,boolean[] flag,char[] str,int k){
        //先根据i和j计算匹配的第一个元素转为一维数组的位置
        int index = i*cols+j;
        //递归终止条件
        if(i<0 || j<0 || i>=rows || j>=cols || matrix[index] != str[k] || flag[index] == true)
            return false;
        //若k已经到达str末尾了,说明之前的都已经匹配成功了,直接返回true即可
        if(k == str.length-1)
            return true;
        //要走的第一个位置置为true,表示已经走过了
        flag[index] = true;
         
        //回溯,递归寻找,每次找到了就给k加一,找不到,还原
        if(judge(matrix,i-1,j,rows,cols,flag,str,k+1) ||
           judge(matrix,i+1,j,rows,cols,flag,str,k+1) ||
           judge(matrix,i,j-1,rows,cols,flag,str,k+1) ||
           judge(matrix,i,j+1,rows,cols,flag,str,k+1)  )
        {
            return true;
        }
        //走到这,说明这一条路不通,还原,再试其他的路径
        flag[index] = false;
        return false;
    }

}

66、机器人的运动范围

题目描述:地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

思路:

1.从(0,0)开始走,每成功走一步标记当前位置为true,然后从当前位置往四个方向探索,

返回1 + 4 个方向的探索值之和。

2.探索时,判断当前节点是否可达的标准为:

1)当前节点在矩阵内;

2)当前节点未被访问过;

3)当前节点满足limit限制。

代码

public class Solution {
    public int movingCount(int threshold, int rows, int cols)
    {
        //采用回溯递归方法
        boolean[][] visited=new boolean[rows][cols];
        return countingSteps(threshold,rows,cols,0,0,visited);
        
    }
    
    public int countingSteps(int limit,int rows,int cols,int r,int c,boolean[][] visited){
        //查询递归条件
           if (r < 0 || r >= rows || c < 0 || c >= cols
                || visited[r][c] || bitSum(r) + bitSum(c) > limit)  return 0;
        visited[r][c] = true;
          return countingSteps(limit,rows,cols,r - 1,c,visited)
                + countingSteps(limit,rows,cols,r,c - 1,visited)
                + countingSteps(limit,rows,cols,r + 1,c,visited)
                + countingSteps(limit,rows,cols,r,c + 1,visited)
                +1;
    }
    
    public int bitSum(int t){
        int count=0;
        while(t!=0){
            count+=t%10;
            t=t/10;
        }
        return count;
    }
}

67、剪绳子

题目描述:给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1,m<=n),每段绳子的长度记为k[1],...,k[m]。请问k[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

思路:可以采用贪心算法,每一步都去的最优解,那么最后的解也就是最优解。

①target<=3,m是必须要分段的,3的最大分段乘积为2,2的最大分段乘积为1

②taeget>=4,可以用一个for循环进行比较字段的值,并把最大值存起来

public class Solution {
    public int cutRope(int target) {
     if(target==2){
            return 1;
        }
     // n<=3的情况,m>1必须要分段,例如:3必须分成1、2;1、1、1 ,n=3最大分段乘积是2,
        if(target==3){
            return 2;
        }
        int[] dp=new int[target+1];
         /*
        下面3行是n>=4的情况,跟n<=3不同,4可以分很多段,比如分成1、3,
        这里的3可以不需要再分了,因为3分段最大才2,不分就是3。记录最大的。
         */
        dp[1]=1;
        dp[2]=2;
        dp[3]=3;
        int res=0;
        /**
   第一个循环从4开始,没啥问题吧。第二个循环为什么j<=i/2是因为1*3和3*1是一样的,没必要计算在内,只要计算到1*3和2*2就好了。然后就是取最大,1*3   最大是3,2*2最大是4,那么dp[4]=res就是4。
          */
        for(int i=4;i<=target;i++){
            for(int j=1;j<=i/2;j++){
                res=Math.max(res,dp[j]*dp[i-j]);
            }
             dp[i]=res;
        }
        return dp[target];
    }
}

总结

剑指offer67到题目到这里就全部结束,小编将会在下一篇文章中整理成册分享给大家。耐得住辛苦,接得住机会。

秒杀剑指offer(终篇)_字符串_03