题号 | 题目名称 |
---|---|
61 | 序列化二叉树 |
62 | 二叉搜索树的第k个节点 |
63 | 数据流中的中位数 |
64 | 滑动窗口的最大值 |
65 | 矩阵中的路径 |
66 | 机器人的运动范围 |
67 | 剪绳子 |
请实现两个函数,分别用来序列化和反序列化二叉树。
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过某种符号表示空节点(#),以 !表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
解法一: 我们根据前序遍历序列来序列化二叉树,因为前序遍历序列是从根结点开始的,且遍历顺序是中左右,方便我们依次往后遍历字符串数组,并依次生成左子树和右子树。当在遍历二叉树过程中碰到Null指针时,这些Null指针被序列化为一个特殊的字符“#”,结点之间用感叹号隔开。首先将序列化后的字符串根据分隔符分隔成字符串数组,使用一个全局变量来标识当前遍历到哪个字符。
解法一:
String Serialize(TreeNode root) {
if (root == null) return "#!";
return root.val + "!" + Serialize(root.left) + Serialize(root.right);
}
TreeNode Deserialize(String str) {
String[] strings = str.split("!");
return helper(strings);
}
int index = -1;
TreeNode helper(String[] strings) {
index++;
if (index >= strings.length) return null;
TreeNode tNode = null;
if (!strings[index].equals("#")) {
tNode = new TreeNode(Integer.parseInt(strings[index]));
tNode.left = helper(strings);
tNode.right = helper(strings);
}
return tNode;
}
给定一棵二叉搜索树,请找出其中的第k小的结点。例如,(5,3,7,2,4,6,8)中,按结点数值大小顺序第三小结点的值为4。
解法一: 递归法。因为二叉搜索树的中序遍历是有序递增的,所以可以使用递归版的中序遍历对二叉搜索树进行遍历,当遍历到第k个节点时则返回。
解法一:
int index = -1;
TreeNode res = null;
TreeNode KthNode(TreeNode pRoot, int k) {
if (pRoot == null) return null;
index = k;
inOrderTraverse(pRoot);
return res;
}
void inOrderTraverse(TreeNode root) {
if (root != null) {
inOrderTraverse(root.left);
index--;
if (index == 0) {
res = root;
return;
}
inOrderTraverse(root.right);
}
}
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
解法一: 优先队列法。设置一个小根堆和一个大根堆,在Java中使用PriorityQueue即可实现,默认是小根堆,本解法通过lambda表达式重写了比较器,实现大根堆,大根堆和小根堆各放一半的数据即可,保证大根堆中的数据量永远比小根堆中的数据量相等或多一个。当两个堆大小一样时,两个堆顶元素的平均数即为中位数;当两个堆大小不等时,由于大根堆中的元素数量会多一个,所以大根堆的堆顶元素即为中位数。在数据插入堆时,为了保证该数据放在合理的堆中,可以通过先插入大根堆,然后再将大根堆的堆顶元素取出插入小根堆的方式,使得该数据放在应有的位置。每次调整的时间复杂度为O(logn)。
解法一: O(logn)
PriorityQueue<Integer> lHeap = new PriorityQueue<>((a, b) -> b - a); //大根堆
PriorityQueue<Integer> sHeap = new PriorityQueue<>(); //小根堆
void Insert(int num) {
lHeap.add(num);
sHeap.add(lHeap.remove());
if (sHeap.size() > lHeap.size()) {
lHeap.add(sHeap.remove());
}
}
Double GetMedian() {
if (sHeap.size() == lHeap.size()) {
return (sHeap.peek() + lHeap.peek()) / 2.0;
} else {
return (double) lHeap.peek();
}
}
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{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]}。
解法一: 优先队列法。维护一个大小为size的大根堆,对数组进行遍历,每次将窗口头部元素移除并将窗口尾部元素加入堆中,然后将堆顶元素返回即可。
解法一: O(nlogk)
public static ArrayList<Integer> maxInWindows(int [] num, int size) {
PriorityQueue<Integer> queue = new PriorityQueue<>((a, b) -> b - a);
ArrayList<Integer> list = new ArrayList<>();
if (num.length < size || size == 0) {
return list;
}
for (int i = 0; i < size; i++) {
queue.offer(num[i]);
}
list.add(queue.peek());
for (int p1 = 0, p2 = size; p2 < num.length; p1++, p2++) {
queue.remove(num[p1]);
queue.offer(num[p2]);
list.add(queue.peek());
}
return list;
}
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
解法一: 回溯法。
1、根据给定数组,初始化一个标志位数组,false表示未走过,true表示已经走过。因为题目规定不能走进同一个格子两次;
2、根据行数和列数,遍历数组,先找到一个与str字符串的第一个元素相匹配的矩阵元素,进入judge,判断是否能够走通;
3、根据i和j确定当前点在一维数组matrix中的位置;
4、确定递归终止条件:越界、当前找到的矩阵值不等于数组对应位置的值、已经走过的点,这三类情况说明此路不通,返回false;
5、如果待判定字符串str的索引已经判断到了最后一位,此时说明匹配成功,返回true;
6、递归寻找当前格子周围的四个格子是否符合条件,只要有一条路满足条件则返回true;
7、如果周围的四个格子递归都没有找到路径,则说明当条路径寻找失败,将走过路径的数组flag重新标记为false。
此类问题都可以用回溯法解决,在递归过程中对数组越界进行判定是一种较为优雅的解法。
解法一:
public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
boolean[] flag = new boolean[matrix.length];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (judge(matrix, i, j, rows, cols, str, 0, flag)) {
return true;
}
}
}
return false;
}
public boolean judge(char[] matrix, int i, int j, int rows, int cols, char[] str, int k, boolean[] flag) {
int index = i * cols + j;
if (i < 0 || i >= rows || j < 0 || j >= cols || matrix[index] != str[k] || flag[index]) {
return false;
}
if (k == str.length - 1) {
return true;
}
flag[index] = true;
if (judge(matrix, i - 1, j, rows, cols, str, k + 1, flag)
|| judge(matrix, i + 1, j, rows, cols, str, k + 1, flag)
|| judge(matrix, i, j - 1, rows, cols, str, k + 1, flag)
|| judge(matrix, i, j + 1, rows, cols, str, k + 1, flag)) {
return true;
}
flag[index] = false;
return false;
}
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
解法一: 回溯法。与岛问题类似的解法。
1、根据给定的行列,创建并初始化二维数组,判断每一个位置是否满足小于threshold的要求,满足为1,不满足为0;
2、机器人从原点出发,开始递归。数组越界和数组当前值不为1说明此路走不通,返回能够到达的格子数0。注意:数组当前值不为1有两种情况——0或2。数组值为0说明该格子不满足题目中小于threshold的要求,数组值为2说明当前位置已经走过,不再重复走;
3、如果此路可以走通,则对该位置的值进行+1操作,用来标记该位置已经走过并计数;
4、对当前格子的上下左右方向能够到达的格子个数进行计数累加,并做+1操作,意味着当前格子可达。
解法一:
public static int movingCount(int threshold, int rows, int cols) {
int[][] matrix = new int[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = isAccess(threshold, i, j);
}
}
return helper(matrix, 0, 0, rows, cols);
}
public static int helper(int[][] matrix, int i, int j, int rows, int cols) {
if (i < 0 || i >= rows || j < 0 || j >= cols || matrix[i][j] != 1) {
return 0;
}
matrix[i][j]++;
return helper(matrix, i - 1, j, rows, cols)
+ helper(matrix, i + 1, j, rows, cols)
+ helper(matrix, i, j - 1, rows, cols)
+ helper(matrix, i, j + 1, rows, cols)
+ 1;
}
public static int isAccess(int threshold, int row, int col) {
int count = 0;
while (row != 0) {
count += row % 10;
row /= 10;
}
while (col != 0) {
count += col % 10;
col /= 10;
}
return count <= threshold ? 1 : 0;
}
给你一根长度为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。
解法一: 动态规划。本解法采用非递归实现,节省空间。m[i]代表着当长度为i时能够剪成若干长度的最大乘积,其实就是对组成每个长度的可能性进行遍历,比如求解长度为10的绳子,可以划分成1+9、2+8、3+7、4+6、5+5这五个子问题,这五个子问题的最大值就是m[10],而子问题的解已经在之前已经被求解。所以可以直接使用for循环遍历的方式,而不需要采用递归求解,节省对某个值多次遍历的额外空间。而之所以要对子问题求max(i, m[i]),是因为题目要求至少对绳子剪一次,所以当绳子长度为10时,我们不能直接返回10,即结果不可能是它本身。但是在求后续问题的解中,m[i]只是一个子问题,可以不再进行划分,例如我们计算12时,需要2+10这两个子问题,而对于长度为10的这段绳子,我们可以不再对它进行剪切,而是把10作为一个乘数考虑进来。
解法一: O(n²)
public int cutRope(int target) {
if (target == 1) return 1;
int[] m = new int[target + 1];
m[1] = 1;
for (int i = 2; i <= target; i++) {
int max = m[i];
for (int j = 1; j <= i / 2; j++) {
max = Math.max(max, Math.max(j, m[j]) * Math.max(i - j, m[i - j]));
}
m[i] = max;
}
return m[target];
}