剑指Offer系列之题61~题67(完结)

写在前面:本随笔是剑指Offer系列最后一篇。后续会记录一些Java、Spring、数据库、分布式等方面的内容。

61.序列化二叉树

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

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

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

递归;非递归。选择相应的遍历顺序。


1.递归:

public class Solution {
    int index=-1;
    String Serialize(TreeNode root) {
        //以 !表示结点值的结束,#表示空节点
        StringBuffer sb=new StringBuffer();
        if(root==null){
            sb.append("#!");
            return sb.toString();
        }
        //前序遍历
        sb.append(root.val+"!");
        sb.append(Serialize(root.left));
        sb.append(Serialize(root.right));
        return sb.toString();
  }
    TreeNode Deserialize(String str) {
        index++;//依次后移判断对应结点
        TreeNode root=null;
        String[] node=str.split("!");
        if(!node[index].equals("#")){
            root=new TreeNode(Integer.valueOf(node[index]));
            root.left=Deserialize(str);
            root.right=Deserialize(str);
        }
        return root;
  }
}

2.非递归(层次遍历):

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

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
import java.util.Queue;
import java.util.LinkedList;
public class Solution {
    int index=-1;
    String Serialize(TreeNode root) {
        //以 !表示结点值的结束,#表示空节点
        if(root==null)
            return "#!";
        Queue<TreeNode> queue=new LinkedList<>();
        StringBuffer sb=new StringBuffer();
        int width=0;//宽度,当前层结点个数
        int count=0;//当前层已遍历结点数
        TreeNode temp=null;//存储遍历的结点
        queue.offer(root);
        sb.append(root.val+"!");
        while(!queue.isEmpty()){
            width=queue.size();
            while(count<width){//遍历当前层
                temp=queue.poll();
                count++;
                //在入队时,直接将值添加到字符串
                if(temp.left!=null){
                    queue.offer(temp.left);
                    sb.append(temp.left.val+"!");
                }else{
                    sb.append("#!");
                }
                if(temp.right!=null){
                    queue.offer(temp.right);
                    sb.append(temp.right.val+"!");
                }else{
                    sb.append("#!");
                }
            }
            count=0;
        }
        return sb.toString();
  }
    TreeNode Deserialize(String str) {
        TreeNode head = null;
        if(str == null || str.length() == 0)
            return head;
        String[] nodes = str.split("!");
        TreeNode[] treeNodes = new TreeNode[nodes.length];
        for(int i=0; i<nodes.length; i++){//将字符串数组转换为结点数组
            if(!nodes[i].equals("#"))
                treeNodes[i] = new TreeNode(Integer.valueOf(nodes[i]));
        }
        for(int i=0,j=1;j<treeNodes.length;i++){
            //i代表要连接左右子树的父节点,j表示左右子树
            if(treeNodes[i]!=null){
                treeNodes[i].left=treeNodes[j++];
                treeNodes[i].right=treeNodes[j++];
            }
        }
        return treeNodes[0];
  }
}

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

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

辅助栈:递归将结点按右根左的顺序入栈,最后从栈顶到栈底是升序排列。


1.辅助栈:

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

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

    }

}
*/
import java.util.Stack;
public class Solution {
    Stack<TreeNode> stack=new Stack<>();
    TreeNode KthNode(TreeNode pRoot, int k)
    {
        // 根大于左,小于右;最左子节点是最小值  中序遍历
        if(pRoot==null || k<1)
            return null;
        //从最左子节点开始计数
        helper(pRoot);
        TreeNode temp=null;
        while(!stack.empty() && k>0){
            temp=stack.pop();
            k--;
        }
        if(k>0)//当k大于结点个数时
            return null;
        return temp;
    }
    //从右到左入栈,从栈顶到栈底是升序
    void helper(TreeNode root){
        if(root!=null){
            if(root.right!=null)
                helper(root.right);//将右子节点入栈
            stack.push(root);//将根入栈
            if(root.left!=null)
                helper(root.left);//将左子节点入栈
        }
    }
}

2.非递归:

假设有二叉搜索树为 2 3 4,求第三小的结点,以下过程为:

3入栈,2入栈(最左子节点),然后2出栈,不等,又因为2的右结点为空,所以3出栈,不等,但3的右结点非空,所以4入栈,然后4出栈。

import java.util.Stack;
public class Solution {
    TreeNode KthNode(TreeNode pRoot, int k)
    {
        // 根大于左,小于右;最左子节点是最小值  中序遍历
        if(pRoot==null || k<1)
            return null;
        //从最左子节点开始计数
        TreeNode temp=pRoot;
        Stack<TreeNode> stack=new Stack<>();
        int count=0;
        while(temp!=null || !stack.isEmpty()){
            if(temp!=null){//一直入左子节点,直到空
                stack.push(temp);
                temp=temp.left;
            }else{
                temp=stack.pop();//第一次出栈是最小值
                count++;
                if(count==k)
                    return temp;
                temp=temp.right;//赋为右结点,若非空则将入栈即栈顶,此时左 根已遍历,下一个遍历结点即该右结点;空则接着出栈顶元素
            }
        }
        return null;
    }
}

63.数据流中的中位数

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

暴力解(时空复杂度过高):ArrayList存储数字,然后赋值给数组排序,返回中位数;

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


1.暴力解:

import java.util.Arrays;
import java.util.ArrayList;
public class Solution {
    ArrayList<Integer> res=new ArrayList<>();
    public void Insert(Integer num) {
        res.add(num);
    }
    public Double GetMedian() {
        if(res.isEmpty())
            return null;
        double arr[]=new double[res.size()];
        for(int i=0;i<arr.length;++i){
            arr[i]=(double)res.get(i);
        }
        Arrays.sort(arr);//排序
        if(arr.length%2!=0){//奇数个数
            return arr[arr.length/2];
        }
        return (arr[arr.length/2-1]+arr[arr.length/2])/2;//偶数个数
    }
}

2.堆排序(大顶堆、小顶堆):

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>(15, 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]}。

暴力解;每次移动后遍历当前滑动窗口;

双端队列:对新来的元素k,将其与双端队列中的元素相比较 1)前面比k小的,直接移出队列(因为不再可能成为后面滑动窗口的最大值了!),2)前面比k大的X,比较两者下标,判断X是否已不在窗口之内,不在了,直接移出队列队列的第一个元素是滑动窗口中的最大值


1.暴力解:

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
        //窗口里的最大值
        ArrayList<Integer> res=new ArrayList<>();
        if(num.length<=0 || size<=0)//异常输入
            return res;
        int fir=0;//起始位置
        int last=size-1;//末尾
        int max=0;
        if(size>num.length)//若只存在一个滑动窗口
            return res;
        while(last<num.length){
            max=num[fir];
            for(int i=fir;i<=last;++i){
                if(num[i]>max)
                    max=num[i];
            }
            res.add(max);
            fir++;
            last++;
        }
        return res;
    }
}

2.利用双端队列:

import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
        //窗口里的最大值
        ArrayList<Integer> res=new ArrayList<>();
        if(num.length<=0 || size<=0)//异常输入
            return res;
        Deque<Integer> q=new LinkedList<>();
        for(int i=0;i<num.length;++i){
            int flag=i-size+1;//flag是当前位置的前size处的下标,用来判断队列内个数是否达到了size个
            if(q.isEmpty())//若队列空,将当前元素的下标入队,表示该元素在当前滑动窗口内
                q.add(i);
            else if(flag>q.peekFirst())//此时队列内元素下标个数超过滑动窗口大小
                q.pollFirst();//弹出队首元素
            while(!q.isEmpty() &&  num[q.peekLast()]<= num[i])//若队尾元素小于当前元素,该步删除队列内比当前元素小的元素
                q.pollLast();//将队尾元素弹出,即删除比当前元素小的元素的下标
            q.add(i);//将当前元素下标入队
            if(flag>=0)//说明当前是滑动窗口
                res.add(num[q.peekFirst()]);//队首下标即当前滑动窗口内最大元素的下标
        }
        return res;
    }
}

65.矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如
\( \left[ \begin{matrix} a&b&c&e\\ s&f&c&s\\ a&d&e&e \end{matrix} \right] \)
矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

回溯:找到第一个相同的然后开始递归判断,递归过程中如果不符合条件返回false同时将当前位的访问状态重置


1.回溯:

public class Solution {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
    {
        int count=0;//第几个字符
        boolean flag[]=new boolean[matrix.length];
        if(rows<1 || cols<1 || str.length<1)
            return false;
        for(int i=0;i<rows;++i){
            for(int j=0;j<cols;++j){
                if(helper(matrix,rows,cols,i,j,str,count,flag))
                    return true;
            }
        }
        return false;
    }
    boolean helper(char[] matrix,int rows,int cols,int row,int col,char[] str,int count,boolean[] flag){
        //如果超出范围/不等/已访问都返回false;
        if(row<0 || col<0 || row==rows || col==cols || matrix[row*cols+col]!=str[count]||flag[row*cols+col])
            return false;
        //若count=str长度,说明全部匹配成功
        if(count==str.length-1)
            return true;
        //非空且相等时继续对比下一位
        flag[row*cols+col]=true;//设为已访问
        //递归寻找,找到了就count+1
        if(helper(matrix,rows,cols,row+1,col,str,count+1,flag) ||
                helper(matrix,rows,cols,row-1,col,str,count+1,flag) ||
                helper(matrix,rows,cols,row,col-1,str,count+1,flag) ||
                helper(matrix,rows,cols,row,col+1,str,count+1,flag)){
            return true;
        }
        //上面语句不通过说明未找到,回溯,将该位置为未访问
        flag[row*cols+col]=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.递归:

public class Solution {
    public int movingCount(int threshold, int rows, int cols)
    {
        boolean flag[]=new boolean[rows*cols];
        if(rows<=0 || cols<=0 || threshold<0)
            return 0;
        int count=helper(rows,cols,0,0,threshold,flag);
        return count;
    }

    int helper(int rows,int cols,int row,int col,int k,boolean[] flag){
        //退出条件
        if(row<0 || col<0 || row==rows || col==cols || flag[row*cols+col] || !checkSum(row,col,k))
            return 0;
        flag[row*cols+col]=true;//置为已访问
        return helper(rows,cols,row-1,col,k,flag)+
            helper(rows,cols,row+1,col,k,flag)+
            helper(rows,cols,row,col-1,k,flag)+
            helper(rows,cols,row,col+1,k,flag)+1;
    }
    //检查数位和
    boolean checkSum(int row,int col,int k){
        int sum=0;
        while(row!=0 || col!=0){
            if(row!=0){
                sum+=row%10;
                row=row/10;
            }
            if(col!=0){
                sum+=col%10;
                col/=10;
            }
        }
        if(sum>k)
            return false;
        return true;
    }
}

2.非递归:

public class Solution {
    public int movingCount(int threshold, int rows, int cols)
    {
        if(rows<=0 || cols<=0 || threshold<0)
            return 0;
        int count=0;
        //遍历每个点
        for(int i=0;i<rows;++i){
            for(int j=0;j<cols;++j){
                if(checkSum(i,j,threshold))//满足条件
                    count++;
                else if(rows==1 || cols==1)//当只有一行或一列时,遇到不满足的点就退出
                    return count;
            }
        }
        return count;
    }

    //检查数位和
    boolean checkSum(int row,int col,int k){
        int sum=0;
        while(row!=0 || col!=0){
            if(row!=0){
                sum+=row%10;
                row=row/10;
            }
            if(col!=0){
                sum+=col%10;
                col/=10;
            }
        }
        if(sum>k)
            return false;
        return true;
    }

67.剪绳子

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

输入描述:输入一个数n,意义见题面。(2 <= n <= 60)

找到规律:当都是2和3时乘积最大,且3的个数大于2的个数。

5<2*3,6<3*3,比6更大的数字我们就更不用考虑了,肯定要继续分。


1.:

public class Solution {
    public int cutRope(int target) {
        //m>1,n>1
        //根据规律可看出是 2 和 3的乘积
        int multi=1;
        //小值判断
        if(target<=3)
            return target-1;
        while(target>4){//只有5才可分为 2+3,比5小的直接当作一段时乘积最大
            target-=3;
            multi*=3;
        }
        return multi*target;
    }
}

如有错误,欢迎指正

posted @ 2020-04-17 14:36  雨落成尘  阅读(234)  评论(0编辑  收藏  举报