目录
第一题
题目来源
题目内容
解决方法
方法一:动态规划
这是一个典型的树形动态规划问题,可以使用递归来解决。
对于每个节点,有两种选择:偷取该节点和不偷取该节点。如果偷取该节点,则不能偷取其子节点;如果不偷取该节点,则可以选择偷取其子节点。
定义一个递归函数 rob
,它接收一个节点作为参数,并返回在该节点为根节点的子树中,小偷能够盗取的最高金额。递归函数的定义如下:
- 如果节点为空,返回0。
- 如果节点不为空,分别计算选择偷取该节点和不偷取该节点的情况下,能够盗取的最高金额。
- 偷取该节点的情况:计算偷取该节点的金额,加上偷取其左子节点和右子节点的最高金额(因为不能偷取相邻节点)。
- 不偷取该节点的情况:计算偷取其左子节点和右子节点的最高金额。
最后,在主函数中调用递归函数 rob
,传入根节点,并返回结果。
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
public class Solution {
public int rob(TreeNode root) {
int[] result = dfs(root);
return Math.max(result[0], result[1]);
}
private int[] dfs(TreeNode node) {
if (node == null) {
return new int[]{0, 0};
}
int[] left = dfs(node.left);
int[] right = dfs(node.right);
int selected = node.val + left[1] + right[1];
int notSelected = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
return new int[]{selected, notSelected};
}
}
在这个解法中,使用一个长度为2的数组来存储两种情况下的最高金额。数组中的第一个元素表示选择偷取该节点时的最高金额,第二个元素表示不选择偷取该节点时的最高金额。
- 时间复杂度:O(N),其中 N 是节点的数量,每个节点只需计算一次。
- 空间复杂度:O(N),递归调用栈的深度为树的高度,最坏情况下为 O(N)。在堆栈中存储的状态也需要 O(N) 的空间。
LeetCode运行结果:
第二题
题目来源
题目内容
解决方法
方法一:双指针
这道题可以使用双指针来解决。我们可以用两个指针分别指向数组的左端和右端,然后计算它们之间能够容纳的水量,并将当前计算出来的最大水量保存下来。
因为水的容量取决于容器的瓶颈,即容器底部的长度和高度较小的那条线段。因此,在计算当前指针对应的线段为底时,我们需要将另一个指针向中间移动来找到高度更高的线段,以获得更大的水量。
具体地,我们假设当前左指针所指的线段高度为 l,右指针所指的线段高度为 r,且 l<r。如果我们将左指针向右移动一位,那么新的左指针对应的线段高度为 l',右指针不变,那么新容器的底部长度为 r- (l'-l),高度为 min(l', r),因此容器容纳的水量为 (r-(l'-l)) * min(l', r)。假设新容器容纳的水量比原容器大,那么我们将当前容器容纳的水量更新为新容器的水量。
按照上述方式移动指针,并在每次移动后更新当前最大容量,直到两个指针相遇为止。在整个过程中,我们不断地维护最大的水量,并返回最终的最大值作为答案。
class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length - 1;
int maxArea = 0;
while (left < right) {
int area = Math.min(height[left], height[right]) * (right - left);
maxArea = Math.max(maxArea, area);
if (height[left] <= height[right]) {
++left;
} else {
--right;
}
}
return maxArea;
}
}
复杂度分析:
时间复杂度:O(n),其中 n 是数组的长度。在最坏的情况下,左指针和右指针分别移动了 n-1 次,因此总时间复杂度为 O(n)。
空间复杂度:O(1),只需要常数级别的额外空间来存储指针和变量。
LeetCode运行结果:
方法二:动态规划
除了双指针法,我们还可以尝试使用动态规划的方法解决这个问题。
- 我们定义一个二维数组 dp,其中 dp[i][j] 表示以第 i 条线和第 j 条线为边界的容器能够容纳的最大水量。
- 初始化 dp 数组的对角线元素为0,因为任意一条线段与自己构成的容器是无法容纳水的。
- 然后,我们从左到右、从下到上依次计算 dp 数组的其他元素。计算某个元素时,可以考虑当前两条线段的高度和它们之间的距离,并且选择高度较小的一条作为瓶颈,即容器的高度,并计算容器的底部长度,即线段的距离。根据容器容纳水的公式,我们可以得到 dp[i][j] = min(height[i], height[j]) * (j-i)。
- 最后,遍历整个 dp 数组,找到最大的水量,即 dp 数组的最大值。
class Solution {
public int maxArea(int[] height) {
int n = height.length;
int[][] dp = new int[n][n];
int maxArea = 0;
for (int j = 1; j < n; j++) {
for (int i = j - 1; i >= 0; i--) {
int area = Math.min(height[i], height[j]) * (j - i);
dp[i][j] = area;
maxArea = Math.max(maxArea, area);
}
}
return maxArea;
}
}
时间复杂度:O(n^2),其中 n 是数组的长度。需要遍历二维数组中的每个元素。
空间复杂度:O(n^2),需要一个二维数组来存储计算结果,空间复杂度与数组长度的平方成正比。
LeetCode运行结果:
总结:动态规划的方法在空间复杂度上较高。在面对大规模输入时,可能会超出内存限制。我们可以优先考虑双指针方法。
第三题
题目来源
题目内容
解决方法
方法一:贪心算法
要将一个整数转换为罗马数字,我们可以使用贪心算法。首先,创建两个数组,一个存储罗马数字的符号,另一个存储对应的数值。然后,从大到小遍历符号数组,每次查看给定的整数中有多少个当前符号所对应的数值,将其加入结果字符串,并用给定的整数减去已经加过的数值。重复这个过程直到给定的整数为0。
class Solution {
public String intToRoman(int num) {
int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
String[] symbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
StringBuilder sb = new StringBuilder();
int i = 0;
while (num > 0) {
if (num >= values[i]) {
sb.append(symbols[i]);
num -= values[i];
} else {
i++;
}
}
return sb.toString();
}
}
- 时间复杂度:O(1),因为我们使用固定的数组来进行转换,遍历次数最多为常数。
- 空间复杂度:O(1),只需要常数级别的额外空间来存储数组和变量。
LeetCode运行结果:
方法二:枚举
除了贪心算法外,我们还可以使用枚举类型来存储每种符号的信息,使得代码更加模块化和可维护。
首先,创建一个枚举类型Symbol,用来存储每种符号的信息,包括符号、数值、表示方式(在左边还是右边)以及特殊情况(是否需要减去另一个符号所代表的数值)。
然后,我们将所有符号按照数值从大到小排序,遍历符号数组,每次查看给定的整数中有多少个当前符号所对应的数值,将其加入结果字符串,并用给定的整数减去已经加过的数值。在遍历时还需要判断是否存在特殊情况,如果存在,则需要进行相应的处理。
public class Solution {
enum Symbol {
M(1000, "M", true),
CM(900, "CM", false),
D(500, "D", true),
CD(400, "CD", false),
C(100, "C", true),
XC(90, "XC", false),
L(50, "L", true),
XL(40, "XL", false),
X(10, "X", true),
IX(9, "IX", false),
V(5, "V", true),
IV(4, "IV", false),
I(1, "I", true);
int value;
String symbol;
boolean canRepeat;
Symbol(int value, String symbol, boolean canRepeat) {
this.value = value;
this.symbol = symbol;
this.canRepeat = canRepeat;
}
}
public String intToRoman(int num) {
StringBuilder sb = new StringBuilder();
for (Symbol s : Symbol.values()) {
while (num >= s.value) {
if (!s.canRepeat && sb.length() > 0 && sb.substring(sb.length() - 1).equals(s.symbol)) {
// 处理特殊情况
sb.deleteCharAt(sb.length() - 1);
sb.append(getSymbol(s, Symbol.values()));
} else {
// 添加普通符号
sb.append(s.symbol);
}
num -= s.value;
}
}
return sb.toString();
}
private String getSymbol(Symbol s, Symbol[] symbols) {
for (int i = 0; i < symbols.length - 1; i++) {
if (symbols[i] == s) {
return symbols[i+1].symbol;
}
}
return "";
}
}
- 时间复杂度:O(1),因为我们使用固定的枚举类型进行转换,遍历次数最多为常数。
- 空间复杂度:O(1),只需要常数级别的额外空间来存储枚举类型、字符串和变量。
LeetCode运行结果:
方法三:硬编码数字
使用硬编码的方法实现整数转罗马数字可以通过以下步骤进行:
- 定义四个数组thousands、hundreds、tens和ones,分别表示千位、百位、十位和个位上对应的罗马数字。
- 根据给定的整数,依次取出千位、百位、十位和个位上的数值。可以通过整数除以相应的倍数来获取:千位上的数值为num / 1000,百位上的数值为(num % 1000) / 100,十位上的数值为(num % 100) / 10,个位上的数值为num % 10。
- 根据取出的数值,从数组中获取相应的罗马数字。例如,千位上的数值为3,则对应的罗马数字为thousands[3]。
- 将获取到的罗马数字依次拼接到结果字符串中。
- 返回结果字符串作为整数转换后的罗马数字表示。
public class Solution {
public String intToRoman(int num) {
String[] thousands = {"", "M", "MM", "MMM"};
String[] hundreds = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"};
String[] tens = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};
String[] ones = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"};
StringBuilder sb = new StringBuilder();
sb.append(thousands[num / 1000]);// 千位上的数值
sb.append(hundreds[(num % 1000) / 100]);// 百位上的数值
sb.append(tens[(num % 100) / 10]);// 十位上的数值
sb.append(ones[num % 10]);// 个位上的数值
return sb.toString();
}
}
- 时间复杂度:O(1),因为我们进行固定次数的操作,与输入的整数大小无关。
- 空间复杂度:O(1),只需要常数级别的额外空间来存储数组、字符串和变量。
LeetCode运行结果: