欢迎指正
题解(01-10):link
题解(11-20):link
题解(21-30):link
题解(31-40):link
题解(41-50):link
题解(51-60): link
题解(61-67): link
61.序列化二叉树
描述见链接
1.解法一:序列化采用前序遍历
public class Solution {
String Serialize(TreeNode root) {
String res = "";
return Serialize(root, res);
}
private String Serialize(TreeNode root, String res) {
if (root == null) { // 序列化根节点
res += "#!";
return res;
} else {
res += root.val + "!";
}
res = Serialize(root.left, res); // 序列化左子树
res = Serialize(root.right, res); // 序列化右子树
return res;
}
TreeNode Deserialize(String str) {
if (str == null || str.length() == 0)
return null;
// 现将序列根据规则拆分成字符串数组
String[] arr = str.split("!");
return Deserialize(arr);
}
int start = -1;
private TreeNode Deserialize(String[] str) {
start ++;
// 当前字符串为空,说明是一个空节点,不用考虑
if (start < str.length && !str[start].equals("#")) {
// 将字符串数组的值转换为整数
TreeNode curr = new TreeNode(Integer.parseInt(str[start]));
curr.left = Deserialize(str); // 因为每一次递归 start 值都会增加
curr.right = Deserialize(str);
return curr;
}
return null;
}
}
62.二叉搜索树的第k个节点
题目描述: 给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
1.解法一:利用二叉搜索树的中序遍历是有序的,使用额外空间
public class Solution {
private ArrayList<TreeNode> list = new ArrayList<>();
TreeNode KthNode(TreeNode pRoot, int k) {
if (pRoot == null) return null;
inOrder(pRoot);
if (k < 1 || k > list.size()) return null;
return list.get(k - 1);
}
// 递归中序遍历
private void inOrder(TreeNode root) {
if (root != null) {
inOrder(root.left);
list.add(root);
inOrder(root.right);
}
}
}
2.解法二:不使用额外空间,使用一个计数器记录遍历的元素个数
public class Solution {
private int count = 0; // 使用count来记录中序访问了多少节点
TreeNode KthNode(TreeNode pRoot, int k) {
if (pRoot != null) {
TreeNode node = KthNode(pRoot.left, k); // 先去左边找
if (node != null) return node;
count ++; // 没找到,看看中间是不是
if (count == k) return pRoot;
node = KthNode(pRoot.right, k); // 再去右边找
if (node != null) return node;
}
return null;
}
}
63.数据流中的中位数
题目描述: 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
1.解法一:使用最大最小堆
插入的时间复杂度为O(log n)
,查找的时间复杂度O(1)
当数据保存到容器中时,可以分为两个部分,左边一部分的数据要比右边一部分的数据小。如下图所示:P1
是左边最大的数,P2
是右边最小的数,即使左右两部分数据不是有序的,我们也可以知道:左边的最大数小于右边最小的数。
由此,我们可以得到一个思路:用一个最大堆实现左边的数据存储,一个最小堆实现右边的数据存储, 向堆中插入一个数据的时间是O(log n)
,而中位数就是堆顶的数据,只需要O(1)
的时间就可以得到。
在具体的操作中:为确保中位数出现在堆顶,两个堆的数据量之差不超过1。为了实现平均分配,我们可以设定在来到的数据量是偶数时,将数据插入最小堆,否则插入最大堆。
另外,还要确保最大堆中的所有元素小于最小堆中的元素。 所以,新传来的数据要和最大堆中的最大值或最小堆中的最小值进行比较。
- 数据量为偶数插入最小堆,但是如果新来的数据小于最大堆的最大值,要先将新数据插入最大堆,将最大堆的堆顶元素弹出加入最小堆,这样才能保证最大堆的最大值小于最小堆的最小值。
- 数据量为奇数插入最大堆,同理,要是大于最小堆的最小值,那么就将其插入最小堆,然后将最小堆的堆顶元素弹出加入最大堆。
如果用其他数据结构进行存储,时间复杂度总结如下:(来源)
public class Solution {
// 优先队列默认最小堆,使用比较器构建最大堆
private PriorityQueue<Integer> minHeap = new PriorityQueue<>();
private PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
// 使用lambda表达式,上面的最大堆可以写成
// private PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
private int count = 0;
public void Insert(Integer num) {
count ++;
if (count % 2 == 0) { // 偶数,插入最小堆
if (!maxHeap.isEmpty() && num < maxHeap.peek()) { // 如果num<最大堆,那么先插入最大堆
maxHeap.add(num);
num = maxHeap.poll();
}
minHeap.add(num);
} else { // 奇数,插入最大堆
if (!minHeap.isEmpty() && num > minHeap.peek()) {
minHeap.add(num);
num = minHeap.poll();
}
maxHeap.add(num);
}
}
public Double GetMedian() {
if (minHeap.size() == maxHeap.size()) {
return (minHeap.peek() + maxHeap.peek()) / 2.0;
} else if (minHeap.size() < maxHeap.size()) {
return maxHeap.peek() / 1.0;
} else {
return minHeap.peek() / 1.0;
}
}
}
64.滑动窗口的最大值
描述见链接
1.解法一
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> res = new ArrayList<>();
if (num == null || num.length < 1 || size <= 0 || size > num.length)
return res;
// 使用一个双端队列
Deque<Integer> q = new LinkedList<>(); // 存放的是数组的下标
for (int i = 0;i < num.length;i ++) {
// 队列不为空且队列的长度超过了规定的size
while (!q.isEmpty() && q.peek() < i - size + 1) {
q.poll();
}
// 队列不为空且插入的元素比队尾元素大,那么就删除队尾,插入这个元素的索引
while (!q.isEmpty() && num[i] >= num[q.getLast()]) {
q.removeLast();
}
q.add(i);
// 只有当下标i超过size - 1,才说明开始有了滑动窗口
if (i >= size - 1) {
// q存放的是索引
res.add(num[q.peek()]);
}
}
return res;
}
}
65.矩阵中的路径
描述见链接
1.解法一:回溯法
public class Solution {
public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
if (matrix == null || matrix.length == 0 || str == null || str.length == 0)
return false;
// 首先把一维的matrix转换为二维的
char[][] mat = new char[rows][cols];
boolean[][] flag = new boolean[rows][cols];
for (int i = 0;i < rows;i ++) {
for (int j = 0;j < cols;j ++) {
mat[i][j] = matrix[i * cols + j];
}
}
boolean res = false;
for (int i = 0;i < rows;i ++) {
for (int j = 0;j < cols;j ++) {
res = hasOnePath(mat, i, j, rows, cols, flag, str, 0);
// 不能直接返回false,因为这个位置找不到,可能在下一个位置就找到了
if (res == true)
return true;
}
}
return false;
}
private boolean hasOnePath(char[][] mat, int i, int j, int rows, int cols, boolean[][] flag, char[] str, int start) {
// start代表已经找到str中第几个元素了
if (start == str.length)
return true;
if (i < 0 || i >= rows || j < 0 || j >= cols)
return false;
// 下面说明还在遍历之中
boolean res = false;
if (mat[i][j] == str[start] && flag[i][j] == false) {
flag[i][j] = true;
start ++;
// 分别取上下左右四个位置去寻找
res = hasOnePath(mat, i + 1, j, rows, cols, flag, str, start)
|| hasOnePath(mat, i - 1, j, rows, cols, flag, str, start)
|| hasOnePath(mat, i, j - 1, rows, cols, flag, str, start)
|| hasOnePath(mat, i, j + 1, rows, cols, flag, str, start);
// 如果没有找到,就可以直接回溯了,而不是直接返回值
if (res == false) {
start --;
flag[i][j] = false;
}
}
return res;
}
}
2.解法二:其实就是解法一换了个写法
public class Solution {
private char[][] mat; // 设置成全局变量,减少函数中的参数
private boolean[][] flag; // 记录这个位置是否被使用过,默认都是false
private int rows, cols;
private char[] str;
public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
if (matrix == null || str == null || rows < 1 || cols < 1) return false;
mat = new char[rows][cols];
flag = new boolean[rows][cols];
this.rows = rows;
this.cols = cols;
this.str = str;
//将一维数组转化为二维的
for (int i = 0;i < rows;i ++) {
for (int j = 0;j < cols;j ++) {
mat[i][j] = matrix[i * cols + j];
}
}
boolean res = false;
for (int i = 0;i < rows;i ++) {
for (int j = 0;j < cols;j ++) {
res = hasOnePath(i, j, 0);
if (res == true)
return true;
}
}
return false;
}
private boolean hasOnePath(int i, int j, int start) {
if (start == str.length)
return true;
if (i < 0 || i >= rows || j < 0 || j >= cols)
return false;
boolean res = false;
if (mat[i][j] == str[start] && flag[i][j] == false) {
flag[i][j] = true;
start ++;
res = hasOnePath(i + 1, j, start)
|| hasOnePath(i - 1, j, start)
|| hasOnePath(i, j + 1, start)
|| hasOnePath(i, j - 1, start);
if (res == false) {
flag[i][j] = false;
start --;
}
}
return res;
}
}
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 {
private int threshold, rows, cols;
public int movingCount(int threshold, int rows, int cols) {
if (rows < 1 || cols < 1 || threshold < 0) return 0;
boolean[][] flag = new boolean[rows][cols];
this.threshold = threshold;
this.rows = rows;
this.cols = cols;
return moveCount(0, 0, flag);
}
// 如果当前位置可达,那么将结果加一,并开始向上下左右寻找,走过的路就标为true,不用返回false
// 因为false都是不可达的
private int moveCount(int i, int j, boolean[][] flag) {
int count = 0;
if (checkReachable(i, j, flag)) {
flag[i][j] = true;
count = 1 + moveCount(i + 1, j, flag)
+ moveCount(i - 1, j, flag)
+ moveCount(i, j + 1, flag)
+ moveCount(i, j - 1, flag);
}
return count;
}
// 检查当前位置是否可达
private boolean checkReachable(int i, int j, boolean[][] flag) {
// 索引越界
if (i < 0 || i >= rows || j < 0 || j >= cols)
return false;
// 数位越阈值或当前位置已被访问过
if (digitSum(i) + digitSum(j) > threshold || flag[i][j] == true)
return false;
return true;
}
// 获取当前索引的每一位的和
private int digitSum(int num) {
int sum = 0;
while (num != 0) {
sum += num % 10;
num /= 10;
}
return sum;
}
}
67.剪绳子
描述见链接
1.解法一:动态规划
public class Solution {
public int cutRope(int target) {
if (target == 2) return 1;
if (target == 3) return 2;
int[] dp = new int[target + 1];
// 下面三个情况是在 target >= 4 时产生的
dp[1] = 1;
dp[2] = 2;
dp[3] = 3;
int res = 0;
for (int i = 4;i <= target;i ++) {
for (int j = 1;j <= i / 2;j ++) {
res = Math.max(res, dp[j] * dp[i - j]);
}
dp[i] = res;
}
return dp[target];
}
}
2.解法二:使用贪心算法
当target > 3
时,将target
尽可能的分为 3 的和将使乘积最大化。将target
分为 3 的和,可以有三种情况:
target % 3 == 0
,此时最大值为Math.pow(3, target / 3)
target % 3 == 1
,此时多出来的那个 1 可以加在一个 3 上面,所以最大值为4 * Math.pow(3, target / 3 - 1)
target % 3 == 2
,此时少的那个 1 在一个 3 里面减,由于凑不够整数个3,所以使用乘以 2 来解决,所以最大值为2 * Math.pow(3, target / 3)
public class Solution {
public int cutRope(int target) {
if (target == 2) return 1;
if (target == 3) return 2;
if (target % 3 == 0) return (int)Math.pow(3, target / 3);
if (target % 3 == 1) return 4 * (int)Math.pow(3, target / 3 - 1);
return 2 * (int)Math.pow(3, target / 3);
}
}