写在前面:本随笔是剑指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[
egin{matrix}
a&b&c&e\
s&f&c&s\
a&d&e&e
end{matrix}
ight]
)
矩阵中包含一条字符串"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;
}
}
如有错误,欢迎指正