剑指Offer/61-67


61. 序列化二叉树

题目描述
请实现两个函数,分别用来序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
例如,我们可以把一个只有根节点为1的二叉树序列化为"1,",然后通过自己的函数来解析回这个二叉树

解题思路:队列(层次遍历实现)

public class Solution {
    String Serialize(TreeNode root) {
        StringBuilder result = new StringBuilder();
        if(root == null){
            return null;
        }
        //先访问根节点
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        result.append(String.valueOf(root.val)+"!");
        while(!queue.isEmpty()){
            TreeNode node = queue.poll();
            if(node.left != null){
                result.append(String.valueOf(node.left.val)+"!");
                queue.offer(node.left);
            }else{
                result.append("#!");
            }
            if(node.right != null){
                result.append(String.valueOf(node.right.val) + "!");
                queue.offer(node.right);
            }else{
                result.append("#!");
            }
        }
        return result.toString();
  }
    TreeNode Deserialize(String str) {
       TreeNode root = null;
        if(str == null || str.length() == 0){
            return null;
        }
        String[] strs = str.split("!");
        //先建立根节点
        root = new TreeNode(Integer.parseInt(strs[0]));
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);//把需要插入左右结点的元素放在队列中管理,一旦左右子树建立之后就从队列中删除
        int index = 1;
        while(!queue.isEmpty() && index < strs.length){
            TreeNode temp = queue.poll();
            if(!strs[index].equals("#")){
                TreeNode newLeft = new TreeNode(Integer.parseInt(strs[index]));
                temp.left = newLeft;
                queue.offer(newLeft);
            }
            index++;
            if(!strs[index].equals("#")){
                TreeNode newRight = new TreeNode(Integer.parseInt(strs[index]));
                temp.right = newRight;
                queue.offer(newRight);
            }
            index++;
        }
        return root;
  }
}

62. 二叉搜索树的第k个结点

题目描述
给定一棵二叉搜索树,请找出其中的第k小的结点。

解题思路:
方法1:利用中序遍历,得到的即为有序序列,第k个节点即为所要求解的。
方法2:单函数的方法,剪枝操作。

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

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

    }

}
*/
import java.util.ArrayList;
public class Solution {
//     private int i = 0;//计数器
//     private TreeNode result = null;
//     TreeNode KthNode(TreeNode pRoot, int k)
//     {
//        OrderTraversal(pRoot, k);
//         return result;
//     }
//     public void OrderTraversal(TreeNode pRoot, int k){
//         if(pRoot == null)
//             return;
//         OrderTraversal(pRoot.left, k);
//         if(++i == k){
//             result = pRoot;
//             return;
//         }
//         OrderTraversal(pRoot.right, k);
//     }
// }
    
    //单函数法
     private int i = 0;
     TreeNode KthNode(TreeNode pRoot, int k)//本题单函数:代码短些,不过逻辑复杂些
     {
         if(pRoot == null)
             return null;
         TreeNode node1 = KthNode(pRoot.left, k);//左子树分支总返回给node1【左子树不断向下递归】
         if(node1 != null)
             return node1;//将node1 return给上一层
         if(++i == k)
             return pRoot;//【一旦找到,就会剪枝、logn时间迅速向上】
         TreeNode node2 = KthNode(pRoot.right, k);//右子树分支总返回给node2
         if(node2 != null)
             return node2;
         return null;//【此分支结束(结束向下),向上一层返回(不是向最上层返回)】 
//递归类似于金字塔,一层层向上返回,最顶层返回最终result
    }
}

63. 数据流中的中位数

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

解题思路:使用两个堆,利用两个堆的堆顶解题

(1) 将读入的数据分为数量几乎相同的两部分,一部分数字小,一部分数字大,小的部分采用大顶堆存放,大的部分采用小顶堆存放。
(2) 这样两个堆的堆顶就是整个数据流中,最中间的两个数。
①当总个数为偶数时,是两个堆的数目相同,则中位数=大顶堆的最大数字和小顶堆的最小数字的平均值;
②当总个数为奇数时,使小顶堆的个数比大顶堆多1,则中位数为小顶堆的最小数组

插入的步骤:
(1)若已读取的个数为偶数(包括0)时,两个堆的数目已经相同,再插入一个数时,应该选一个数插入到小顶堆中,从而实现小顶堆的个数多1.
    但是不能直接插入到小顶堆(必须选择一个较大的数插入到小顶堆中,但是这个插入的数不一定满足要求),
    所以这个数要和大顶堆的最大数(先进大顶堆)打群架,谁赢了(谁大)谁进小顶堆
(2)若已读取的个数为奇数时,小顶堆的个数多1,所以要将某数字插入到大顶堆中,此时方法与上面类似,新进来的数要和小顶堆的堆顶(最小值)
    打一架,打输的(更小的那个数)进入大顶堆。
public class Solution {
    //PriorityQueue默认是一个小顶堆
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();//小顶堆
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>(){
        @Override
        public int compare(Integer i1, Integer i2){
            return i2-i1;
        }
    });
    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]}。
窗口大于数组长度的时候,返回空

解题思路:双端队列

public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size){
        ArrayList<Integer> result = new ArrayList<>();
        if(num == null || num.length == 0 || size == 0 || size > num.length)
            return result;
        Deque<Integer> deque = new LinkedList<>();
        for(int i = 0; i < size; i++){
            //依次与队尾元素相比较
            while(!deque.isEmpty() && num[i] >= num[deque.peekLast()]){
                deque.pollLast();
            }
            //存放当前值的下标
            deque.offer(i);
        }
        //队首元素就是该滑动窗口的最大值
        result.add(num[deque.peekFirst()]);
        for(int i = size; i < num.length; i++){
            while(!deque.isEmpty() && num[i] >= num[deque.peekLast()]){
                deque.pollLast();
            }
            //判断下标是否越界
            if(!deque.isEmpty() && deque.peekFirst() <= i - size){
                deque.pollFirst();//越界的情况下 出队列
            }
            deque.offer(i);
            result.add(num[deque.peekFirst()]);
        }
        return result;
    }
}

65. 矩阵中的路径

题目描述
在这里插入图片描述

解题思路:DFS递归的回溯剪枝思想
(1) 根据给定数组,初始化一个标志位数组flag,初始化为false,表示未走过,true表示已经走过,不能走第二次
(2) 根据行数和列数,遍历数组,先找到一个与str字符串的第一个元素相匹配的矩阵元素(进入help函数)
(3) 根据i和j先确定一维数组的位置,因为给定的matrix是一个一维数组
(4) 确定递归终止条件:越界,当前找到的矩阵值不等于数组对应位置的值,已经走过的,这三类情况,都直接false,说明这条路不通
(5) 若k,就是待判定的字符串str的索引已经判断到了最后一位,此时说明是匹配成功的
(6)下面就是本题的精髓,递归不断地寻找周围四个格子是否符合条件,只要有一个格子符合条件,就继续再找这个符合条件的格子的四周是否存在符合条件的格子,直到k到达末尾或者不满足递归条件就停止。
(7)走到这一步,说明本次是不成功的,我们要还原一下标志位数组index处的标志位,进入下一轮的判断。

public class Solution {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str){
        int[] flag = new int[rows * cols];//flag表明那些曾经在路径中出现过的节点的坐标
        for(int i = 0; i < rows; i++){
            for(int j = 0; j < cols; j++){
                if(help(matrix, rows, cols, str, 0, flag, i, j))
                    return true;
            }
        }
        return false;
    }
    public boolean help(char[] matrix, int rows, int cols, char[] str, int cur, int[] flag, int i, int j){
        int index = i*cols + j;//index是(i,j)在一维数组中所对应的位置   (i,j)为所在的格子位置
        if(i < 0 || i >= rows || j < 0 || j >= cols || flag[index] == 1 || matrix[index] != str[cur])
            return false;
        //matrix[index] = str[cur]的时候
        cur = cur + 1;
        if(cur == str.length)//当字符串中所有字符都匹配时返回true
            return true;
        flag[index] = 1;//标记,指明该格子不能再进
        if(help(matrix, rows, cols, str, cur, flag, i-1, j) ||
              help(matrix, rows, cols, str, cur, flag, i+1, j) ||
              help(matrix, rows, cols, str, cur, flag, i, j-1) ||
              help(matrix, rows, cols, str, cur, flag, i, j+1))
                   return true;
         flag[index] = 0;
         return false;
    }
}

66. 机器人的运动范围

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

解题思路:DFS递归的回溯剪枝思想
我们需要全局变量:标志数组;需要一个函数来计算行坐标和列坐标的数位之和;终止条件包括三种情况:越界、重复、行坐标和列坐标的数位之和超过k

public class Solution {
    public int movingCount(int threshold, int rows, int cols)
    {
        int flag[][] = new int[rows][cols];//
        return helper(0, 0, rows, cols, flag, threshold);
    }
    private int helper(int i,int j,int rows,int cols,int[][] flag,int threshold){
        //判断坐标是否符合条件,该路径是否还可以再走
        if(i<0 || i>=rows || j<0 || j>=cols || numSum(i)+numSum(j) > threshold || flag[i][j] == 1)
            return 0;
        flag[i][j] = 1;//标记该位置已被访问
        return helper(i-1 , j, rows, cols, flag, threshold)+
               helper(i+1, j, rows, cols, flag, threshold)+
               helper(i, j-1, rows, cols, flag, threshold)+
               helper(i, j+1, rows, cols, flag, threshold) + 1;
    }
    //计算一个数的数位之和
    private int numSum(int i){
        int sum = 0;
        do{
            sum += i%10;
        }while((i=i/10)>0);
        return sum;
    }
}

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。

解题思路:
假设k[0]-k[m]中有x个2,y个3,则有:
2x+3y=k
其乘积为:
在这里插入图片描述
这是一个随y递增的函数,因此要保证最后的乘积最大,则y需要尽可能大,既3的个数要尽可能多

public class Solution {
        //贪婪算法
        if(target <= 3){
            return target-1;
        }
        int timesOf3 = target/3;
        if(target % 3 == 1){//余数为1时,应该将1*3分解成2*2
            timesOf3 = timesOf3 - 1;
        }
        int timesOf2 = (target - timesOf3*3)/2;
        return (int)Math.pow(3,timesOf3)*(int)Math.pow(2,timesOf2);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值