938二叉搜索树的范围和
描述
给定二叉搜索树的根结点 root
,返回值位于范围 [low, high]
之间的所有结点的值的和。
示例
思路
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public int rangeSumBST(TreeNode root, int low, int high) {
if (root==null)return 0;
if (root.val<low){
return rangeSumBST(root.right,low,high);
}
if(root.val>high){
return rangeSumBST(root.left,low,high);
}
return root.val+rangeSumBST(root.right,low,high)+rangeSumBST(root.left,low,high);
}
93.复原IP地址
描述
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
示例
示例 1:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
输入:s = “0000”
输出:[“0.0.0.0”]
示例 3:
输入:s = “1111”
输出:[“1.1.1.1”]
示例 4:
输入:s = “010010”
输出:[“0.10.0.10”,“0.100.1.0”]
思路
回溯法
List<String> res = new LinkedList<String>();
int[] segments = new int[4];
public List<String> restoreIpAddresses(String s) {
segments = new int[4];
dfs(s,0,0);
return res;
}
public void dfs(String s,int segid,int segstart){
//如果找到了四段ip地址并且遍历完了字符串,表明找到一种答案
if (segid==4){
if (segstart==s.length()){
StringBuffer ipaddr = new StringBuffer();
for (int i = 0; i < 4; i++) {
ipaddr.append(segments[i]);
if (i!=3){
ipaddr.append(".");
}
}
//加入结果集
res.add(ipaddr.toString());
}
return;
}
//不到结束条件继续回溯
//没找到4端ip地址,但是已经遍历完字符串,结束回溯
if (segstart==s.length()){
return;
}
//处理前导0
if (s.charAt(segstart)=='0'){
segments[segid]=0;
dfs(s,segid+1,segstart+1);
}
//一般情况,不为0
int addr = 0;
for (int segend = segstart;segend<s.length();segend++){
addr = addr*10 + (s.charAt(segend)-'0');
if (addr>0&&addr<=255){
//符合条件
segments[segid] = addr;
dfs(s,segid+1,segend+1);
}else{
//不符合条件
break;
}
}
}
633平方数之和
描述
给定一个非负整数 c
,你要判断是否存在两个整数 a
和 b
,使得 a2 + b2 = c
。
示例
示例 1:
输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5
示例 2:
输入:c = 3
输出:false
示例 3:
输入:c = 4
输出:true
示例 4:
输入:c = 2
输出:true
示例 5:
输入:c = 1
输出:true
思路
采用一个循环,从0开始,结束的边界尽可能小,由公式可以看出,a和b不能同时比c的一半的开方还大,故而可以判定一个边界,a<= Math.ceil(Math.sqrt(c/2)),然后c-a^2,得到b,判断b的开方是否为整数,若为整数,则return true;
class Solution {
public boolean judgeSquareSum(int c) {
if(c==0||c==1)return true;
for (int a = 0; a <=Math.ceil(Math.sqrt(c/2)) ; a++) {
int b = c-a*a;
double x = Math.sqrt(b);
//if(x == (int)x)return true;
double eps = 1e-10;//精度范围
boolean flag = x-Math.floor(x)<eps;
if (flag)return true;
}
return false;
}
}
判断double是否是整数
double x;
if(x == (int)x)return true;
double x;
double eps = 1e-10;//精度范围
return x-Math.floor(x)<eps;
198打家劫舍
描述
是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思路
动态规划
dp数组记录到第i家时,偷到的最大金额。
到i家时,可以选择偷或者不偷,选择最大的金额。
动态转移方程为:dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i])
class Solution {
public int rob(int[] nums) {
if (nums.length==0)return 0;
if (nums.length==1)return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for (int i = 2; i <nums.length; i++) {
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[nums.length-1];
}
}
动态规划+滚动数组
可以使用滚动数组记录状态的变化,改善空间复杂度。
class Solution {
public int rob(int[] nums) {
if (nums.length==0)return 0;
if (nums.length==1)return nums[0];
int first = nums[0];
int second = Math.max(nums[0],nums[1]);
for (int i =2; i < nums.length; i++) {
int temp = second;
//当前i家的可得到的最大金额更新到second中,原值保留到first中
second = Math.max(first+nums[i],second);
first = temp;
}
return second;
}
}
740删除并获得点数
描述
给你一个整数数组 nums ,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除每个等于 nums[i] - 1 或 nums[i] + 1 的元素。
开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。
示例
示例 1:
输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。
示例 2:
输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。
思路
动态规划
根据题意,在选择了元素 xx 后,该元素以及所有等于 x-1 或 x+1 的元素会从数组中删去。若还有多个值为 x 的元素,由于所有等于 x-1或 x+1 的元素已经被删除,我们可以直接删除 x 并获得其点数。因此若选择了 x,所有等于 x 的元素也应一同被选择,以尽可能多地获得点数。
记元素 xx 在数组中出现的次数为 c_x,我们可以用一个数组 sum记录数组 nums 中所有相同元素之和,即 sum[x]=x⋅c 。若选择了 x,则可以获取sum[x] 的点数,且无法再选择 x−1 和 x+1。
class Solution {
public int deleteAndEarn(int[] nums) {
int maxVal = 0;
for (int val : nums) {
maxVal = Math.max(maxVal, val);
}
int[] sum = new int[maxVal + 1];
for (int val : nums) {
sum[val] += val;
}
return rob(sum);
}
public int rob(int[] nums) {
int size = nums.length;
int first = nums[0], second = Math.max(nums[0], nums[1]);
for (int i = 2; i < size; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
1473粉刷房子
描述
在一个小城市里,有 m 个房子排成一排,你需要给每个房子涂上 n 种颜色之一(颜色编号为 1 到 n )。有的房子去年夏天已经涂过颜色了,所以这些房子不可以被重新涂色。
我们将连续相同颜色尽可能多的房子称为一个街区。(比方说 houses = [1,2,2,3,3,2,1,1] ,它包含 5 个街区 [{1}, {2,2}, {3,3}, {2}, {1,1}] 。)
给你一个数组 houses ,一个 m * n 的矩阵 cost 和一个整数 target ,其中:
houses[i]:是第 i 个房子的颜色,0 表示这个房子还没有被涂色。
cost[i] [j]:是将第 i 个房子涂成颜色 j+1 的花费。
请你返回房子涂色方案的最小总花费,使得每个房子都被涂色后,恰好组成 target 个街区。如果没有可用的涂色方案,请返回 -1 。
示例
示例 1:
输入:houses = [0,0,0,0,0], cost = [[1,10],[10,1],[10,1],[1,10],[5,1]], m = 5, n = 2, target = 3
输出:9
解释:房子涂色方案为 [1,2,2,1,1],此方案包含 target = 3 个街区,分别是 [{1}, {2,2}, {1,1}]。
涂色的总花费为 (1 + 1 + 1 + 1 + 5) = 9。
示例 2:
输入:houses = [0,2,1,2,0], cost = [[1,10],[10,1],[10,1],[1,10],[5,1]], m = 5, n = 2, target = 3
输出:11
解释:有的房子已经被涂色了,在此基础上涂色方案为 [2,2,1,2,2],此方案包含 target = 3 个街区,分别是 [{2,2}, {1}, {2,2}]。
给第一个和最后一个房子涂色的花费为 (10 + 1) = 11。
示例 3:
输入:houses = [0,0,0,0,0], cost = [[1,10],[10,1],[1,10],[10,1],[1,10]], m = 5, n = 2, target = 5
输出:5
示例 4:
输入:houses = [3,1,2,3], cost = [[1,1,1],[1,1,1],[1,1,1],[1,1,1]], m = 4, n = 3, target = 3
输出:-1
解释:房子已经被涂色并组成了 4 个街区,分别是 [{3},{1},{2},{3}] ,无法形成 target = 3 个街区。
思路
动态规划
设dp(i,j,k) 表示将 [0,i] 的房子都涂上颜色,最末尾的第 i个房子的颜色为 j,并且它属于第 k 个街区时,需要的最少花费。
在进行状态转移时,我们需要考虑「第 i-1个房子的颜色」,这关系到「花费」以及「街区数量」的计算,因此我们还需要对其进行枚举。
设第 i-1i−1 个房子的颜色为 j0 ,我们可以分类讨论出不同情况下的状态转移方程:
如果 houses[i]!=−1,说明第 i个房子已经涂过颜色了。由于我们不能重复涂色,那么必须有 houses[i]=j。我们可以写出在houses[i] !=j 时的状态转移方程:
dp(i,j,k)=∞,如果 houses[i] !=-1 并且 houses[i] !=j;
这里我们用极大值 ∞ 表示不满足要求的状态,由于我们需要求出的是最少花费,因此极大值不会对状态转移产生影响。
当houses[i]=j 时,如果j=j0,那么第 i-1 个房子和第 i 个房子属于同一个街区,状态转移方程为:
dp(i,j,k)=dp(i−1,j,k),如果 houses[i]=j,j==j0;
如果 j!=j0,那么它们属于不同的街区,状态转移方程为:
dp(i,j,k)= min dp(i−1,j0,k−1),如果 houses[i]=j,j!=j0;
如果houses[i]=−1,说明我们需要将第 i个房子涂成颜色 j,花费为 cost[i] [j]。
此外的状态转移与上一类情况类似。如果j=j0,那么状态转移方程为:
dp(i,j,k)=dp(i−1,j,k)+cost[i] [j],如果 houses[i]=−1,j == j0;
如果j!=j0,那么状态转移方程为:
dp(i,j,k)= min dp(i−1,j0 ,k−1)+cost[i] [j],如果 houses[i]=−1,j!=j0;
最终的答案即为min.dp(m−1,j,target−1)。
细节
以下的细节有助于写出更简洁的代码:
我们可以将所有的状态初始化为最大值∞。在进行状态转移时,我们是选择转移中的最小值,因此∞ 不会产生影响;
两类情况下的状态转移方程十分类似,因此我们可以先不去管cost[i] [j] 的部分,在求出 dp(i,j,k) 的最小值之后,如果发现houses[i]=−1,再加上cost[i] [j] 即可;
当 k=0时,不能从包含 k-1的状态转移而来;
当 i=0时,第 0 个房子之前没有房子,因此 kk 也必须为 0。此时状态转移方程为:
dp(0,j,0)= ∞,如果 houses[i] !=−1 并且 houses[i] !=j;
dp(0,j,0)= 0,如果 houses[i] ! =−1 并且 houses[i] ==j;
dp(0,j,0)= cost[i] [j],如果 houses[i]=−1;
当 i=0且 k ! =0 时,dp(0,j,k)=∞。
class Solution {
//极大值,选择为Integer.MAX_Value/2,防止整数相加溢出问题
static final int INFTY = Integer.MAX_VALUE/2;
public int minCost(int[] houses, int[][] cost, int m, int n, int target) {
//将颜色调整为0-n
for (int i = 0; i < m; i++) {
houses[i]-=1;
}
//初始化dp数组为最大值
//dp[i][j][k]表示将0-i的房子图上颜色,第i间房子颜色为j,并且属于第k个街区
int[][][] dp = new int[m][n][target];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
Arrays.fill(dp[i][j],INFTY);
}
}
//状态转移
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; ++j) {
if (houses[i] != -1 && houses[i] != j) {
continue;
}
for (int k = 0; k < target; ++k) {
for (int j0 = 0; j0 < n; ++j0) {
if (j == j0) {
if (i == 0) {
if (k == 0) {
dp[i][j][k] = 0;
}
} else {
dp[i][j][k] = Math.min(dp[i][j][k], dp[i - 1][j][k]);
}
} else if (i > 0 && k > 0) {
dp[i][j][k] = Math.min(dp[i][j][k], dp[i - 1][j0][k - 1]);
}
}
if (dp[i][j][k] != INFTY && houses[i] == -1) {
dp[i][j][k] += cost[i][j];
}
}
}
}
int ans = INFTY;
for (int j = 0; j < n; ++j) {
ans = Math.min(ans, dp[m - 1][j][target - 1]);
}
return ans == INFTY ? -1 : ans;
}
}
10正则表达式匹配
描述
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖整个字符串 s的,而不是部分字符串。
示例
示例 1:
输入:s = “aa” p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:s = “aa” p = "a* "
输出:true
解释:因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例 3:
输入:s = “ab” p = ". * "
输出:true
解释:"." 表示可匹配零个或多个(’’)任意字符(’.’)。
示例 4:
输入:s = “aab” p = “cab”
输出:true
解释:因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。
示例 5:
输入:s = “mississippi” p = “misisp*.”
输出:false
思路
动态规划
dp[i] [j]表示s的前i个字符与p中的前j个字符能否匹配。
动态规划的边界条件为dp[0] [0]=true;最终答案为dp[m] [n],mn分别为字符串s和p的长度。
在进行状态转移时,需要考虑p的第j个字符的匹配情况:
-
p的第j个字符是一个字母,则必须在s中匹配一个相同的字母,即
dp[i] [j] = dp[i-1] [j-1],s[i]==p[j];
dp[i] [j] = false,s[i]!=s[j];
-
p的第j个字符是*,那么需要看p的第j-1位的字符,可以将第j-1位的字符匹配0-多次,即
dp[i] [j] = dp[i-1] [j] or dp[i] [j-2],s[i]==p[j-1];
dp[i] [j] = dp[i] [j-2],s[i]!=p[j-1];
-
p的第j个字符为.,则匹配任意的字母,即
dp[i] [j] = dp[i-1] [j-1];
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
boolean[][] f = new boolean[m + 1][n + 1];
f[0][0] = true;
for (int i = 0; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p.charAt(j - 1) == '*') {
f[i][j] = f[i][j - 2];
if (matches(s, p, i, j - 1)) {
f[i][j] = f[i][j] || f[i - 1][j];
}
} else {
if (matches(s, p, i, j)) {
f[i][j] = f[i - 1][j - 1];
}
}
}
}
return f[m][n];
}
public boolean matches(String s, String p, int i, int j) {
if (i == 0) {
return false;
}
if (p.charAt(j - 1) == '.') {
return true;
}
return s.charAt(i - 1) == p.charAt(j - 1);
}
}
练习一下正则表达式
public static void main(String[] args) {
//字符串与正则表达式直接匹配,boolean是否匹配
String s1 = "abcde",s2 = ".*";
System.out.println(Pattern.matches(s2,s1));
//正则表达式的捕获组,正则表达式加上了括号,一个括号一个组
String patt = "([^(a|e|o|u)]+)([a|e|o|u]+)([^(a|e|o|u)+])";
String s3 = "alanqiao";
Pattern p = Pattern.compile(patt);
Matcher m = p.matcher(s3);
//查看捕获组的数量
System.out.println(m.groupCount());
if (m.find()){
//下标为0的捕获组是完整的匹配字符串
for (int i=0;i<=m.groupCount();i++){
//打印捕获组在字符串中的下标
System.out.println("start="+m.start(i));
//打印捕获组
System.out.println("匹配串:"+m.group(i));
System.out.println();
}
}
else{
System.out.println("no match");
}
}
11盛最多水的容器
描述
给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
示例
示例1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
示例 3:
输入:height = [4,3,2,1,4]
输出:16
示例 4:
输入:height = [1,2,1]
输出:2
思路
暴力双循环,超时。
class Solution {
public int maxArea(int[] height) {
int n = height.length;
int res = 0;
for (int i = 0; i < n-1; i++) {
for (int j = i+1; j < n; j++) {
res = Math.max(res,(j-i)*Math.min(height[i],height[j]));
}
}
return res;
}
}
双指针
class Solution {
public int maxArea(int[] height) {
int l = 0,r = height.length-1;
int res = 0;
while (l<r){
int area = (r-l)*Math.min(height[l],height[r]);
res = Math.max(area,res);
if (height[l]>height[r]){
r--;
}else {
l++;
}
}
return res;
}
}
15三数之和
描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]
思路
排序+双指针
排序方便结果的去重,在需要枚举数组元素时可以考虑使用双指针进行枚举,尽量减少时间复杂度。
伪代码
nums.sort()
for first = 0 .. n-1
if first == 0 or nums[first] != nums[first-1] then
// 第三重循环对应的指针
third = n-1
for second = first+1 .. n-1
if second == first+1 or nums[second] != nums[second-1] then
// 向左移动指针,直到 a+b+c 不大于 0
while nums[first]+nums[second]+nums[third] > 0
third = third-1
// 判断是否有 a+b+c==0
check(first, second, third)
解决
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int n = nums.length;
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<List<Integer>>();
//枚举a
for (int first = 0; first < n; first++) {
//为了去重,需要考虑与前一个元素是否相同
if (first>0&&nums[first]==nums[first-1]){
continue;
}
//c对应指针在数组最右端
int third = n-1;
int target = -nums[first];
//枚举b
for (int second = first+1;second<n;second++){
//b也需要与前一位不一致
if (second>first+1&&nums[second]==nums[second-1]){
continue;
}
//需要保证b的指针在c指针的左侧
while (second<third&&nums[second]+nums[third]>target){
--third;
}
//如果指针重合,则b再增加也不会满足条件,因为数组有序
if (second==third){
break;
}
//判断是否找到一组答案
if (nums[second]+nums[third]==target){
List<Integer> list = new ArrayList<Integer>();
list.add(nums[first]);
list.add(nums[second]);
list.add(nums[third]);
res.add(list);
}
}
}
return res;
}
}
17电话号码的字母组合
描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母
示例
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
思路
回溯
基本为组合问题。考虑使用回溯法。
class Solution {
//17电话号码的字母组合
public List<String> letterCombinations(String digits) {
List<String> com = new ArrayList<String>();
if (digits.length()==0)
return com;
Map<Character,String> map = new HashMap<Character,String>();
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
//回溯结果
backtracing(com,map,digits,0,new StringBuffer());
return com;
}
public void backtracing(List<String>com,Map<Character,String>map,String digits,int index,StringBuffer tempres){
if (index==digits.length()){
com.add(tempres.toString());
}else{
char digit = digits.charAt(index);
String letters = map.get(digit);
int letterscount = letters.length();
for (int i = 0; i < letterscount; i++) {
tempres.append(letters.charAt(i));
//回溯
backtracing(com,map,digits,index+1,tempres);
//取消
tempres.deleteCharAt(index);
}
}
}
}