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);
}
}