class Solution {
public int numDecodings(String s) {
int n = s.length();
int[] f = new int[n + 1];
f[0] = 1;
for (int i = 1; i <= n; ++i) {
if (s.charAt(i - 1) != '0') {
f[i] = f[i - 1];
}
if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26)) {
f[i] += f[i - 2];
}
}
return f[n];
}
}
题解:
使用双指针、递归回溯等方法的时间复杂度为O(n2),存在大量重复计算。
解决办法:
容斥原理,使用map记录所有数组中元素出现的次数。
从1、2、4、8到2^21遍历(i),如果 i 等于map中两个元素之和(包括相同元素)。则满足条件,计数器加1。其中两元素相同:map.get(x) * (map.get(x) - 1)/2
,两元素不同:map.get(x)*map.get(t)/2
(此处除以2是因为,顺序不同而元素相同的组合是冗余的)。将/2提取出来, res>>=1;
class Solution {
public int countPairs(int[] deliciousness) {
long res = 0;
int mod = (int)1e9+7;
int max = 1 << 22;
HashMap<Integer, Integer> map = new HashMap<>();
for (int i : deliciousness) {
map.put(i, map.getOrDefault(i, 0) + 1);
}
for (int x : map.keySet()){
for (int i = 1; i < max; i<<=1) {
int t = i-x;
if (map.containsKey(t)){
if (t==x)res += (long) map.get(x) * (map.get(x) - 1);
else res += (long) map.get(x)*map.get(t);
}
}
}
res>>=1;
return (int) (res%mod);
}
}
class Solution {
public boolean exist(char[][] board, String word) {
if (board == null || board[0] == null || word == null || word.length() == 0) return false;
boolean[][] used = new boolean[board.length][board[0].length];
boolean res = false;
//循环找到第一个元素相等的位置,多个相同的位置用 || 连接,有一个返回true即可
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (board[i][j] == word.charAt(0))
//开始递归
res = res || dfs(board, word, used, i, j, 0);
}
}
return res;
}
private boolean dfs(char[][] board, String word, boolean[][] used, int w, int h, int index) {
// 递归结束条件:word到达末尾
if (index == word.length()) return true;
//如果当前元素是word index位置的元素,且未使用过,则进入if向四周递归下一个元素
if (word.charAt(index) == board[w][h] && !used[w][h]) {
//条件满足,标记为使用过
used[w][h] = true;
//如果已经到达word的末尾,直接返回,不需要再递归
if (index + 1 == word.length()) return true;
//向四周递归,用||连接返回结果。有一个成立即可
boolean res = false;
if (w + 1 < board.length) res = dfs(board, word, used, w + 1, h, index + 1);
if (h + 1 < board[0].length) res = res || dfs(board, word, used, w, h + 1, index + 1);
if (w - 1 >= 0) res = res || dfs(board, word, used, w - 1, h, index + 1);
if (h - 1 >= 0) res = res || dfs(board, word, used, w, h - 1, index + 1);
//回溯条件:四周递归都返回false,即是死胡同,则将当前位置重置为false。回溯
if (!res) {
used[w][h] = false;
}
return res;
}
//递归结束,也没找到,返回false
return false;
}
}
给你一棵二叉搜索树,请你 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。
题解:
使用递归中序遍历二叉树,使用集合存储遍历结果。按照题目要求遍历集合连接各节点。
class Solution {
ArrayList<TreeNode> list = new ArrayList<>();
public TreeNode increasingBST(TreeNode root) {
if (root == null) return null;
dfs(root);
TreeNode head = new TreeNode(list.get(0).val);
TreeNode pre = head;
for (int i = 1; i < list.size(); i++) {
TreeNode cur = new TreeNode(list.get(i).val);
pre.right = cur;
pre = cur;
}
return head;
}
public void dfs(TreeNode node){
if (node==null)return;
dfs(node.left);
list.add(node);
dfs(node.right);
}
}
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:
输入: s = ""
输出: 0
题解:
滑动窗口。
[i,j)
从左向右滑动。i
逐步向右移动;当碰到重复字符时,将j
指向重复字符(靠左的这个重复字,而不是i
处的重复字符符)的下一个位置;res
记录当前无重复字符串的最大长度。class Solution {
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) return 0;
int length = s.length();
int res = 0;
int i = 1;
int j = 0;
while (i <= length) {
String substring = s.substring(j, i);
if (i==length||substring.contains(String.valueOf(s.charAt(i)))) {
res = Math.max(res, i-j);
if (i<length)j += substring.indexOf(s.charAt(i))+1;
}
i++;
}
return res;
}
}
c
的开方得到i
,判断i
是否为整数。0
到 i
的所有整数 k
,当 k
为某个值时,c - sqrt(k)
如果可以开平方,则返回true
;否则返回false
。Math.sqrt(j) - (int) Math.sqrt(j) == 0
class Solution {
public boolean judgeSquareSum(int c) {
int i = (int) Math.sqrt(c);
if (Math.sqrt(c)-i==0)return true;
for (int k = i; k > 0; k--) {
int j = (int) (c - Math.pow(k, 2));
if (Math.sqrt(j) - (int) Math.sqrt(j) == 0)return true;
}
return false;
}
}
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
//新建一条链表用于存储求和结果
ListNode head = new ListNode(0);
//临时节点用于遍历新建链表
ListNode tmp = head;
//保存进位值(0 或 1)
int jinwei = 0;
//直到两个链表都遍历结束
while (l1!=null || l2!=null){
//如果有一条链表已经遍历结束,则用 0 代替 null 继续遍历
int val1 = l1==null?0:l1.val;
int val2 = l2==null?0:l2.val;
//对当前两个节点的值求和
int val = val1+ val2;
//新建结果节点的值为 val ,注意要加上一次的进位值。
tmp.next = new ListNode((val + jinwei) % 10);
tmp = tmp.next;
//更新下一次的进位,仍要注意加上一次的进位值。
jinwei = (val + jinwei) / 10;
if (l1!=null)l1 = l1.next;
if (l2!=null)l2 = l2.next;
}
//当两个链表都已遍历结束。此时如果进位值还有1,则新建节点进行保存。
if (jinwei==1)tmp.next = new ListNode(1);
//返回新链表的头部。
return head.next;
}
}
示例 1:
输入:x = 123
输出:321
示例 2:
输入:x = -123
输出:-321
示例 3:
输入:x = 120
输出:21
示例 4:
输入:x = 0
输出:0
题解:
方法1:字符串反转
class Solution {
public int reverse(int x) {
if (x==0) return 0;
String s = String.valueOf(x);
StringBuilder builder = new StringBuilder(s);
StringBuilder reverse = builder.reverse();
//反转后,去除开头的 0
int index = 0;
while (reverse.charAt(index)=='0'){
index++;
}
String substring = reverse.substring(index);
//是负数的情况
if (substring.charAt(substring.length()-1)=='-'){
substring = substring.substring(0,substring.length()-1);
//大数判断,如果转换失败则说明为大数
try {
return -Integer.parseInt(substring);
}catch (Exception e){
return 0;
}
}
//不是负数的情况
try {
return Integer.parseInt(substring);
}catch (Exception e){
return 0;
}
}
}
方法2:只取最后一位
class Solution {
public int reverse(int x) {
int result = 0;
while (x != 0) {
int tmp = x % 10;
//大数判断
if (result>214748364||(result==214748364&&tmp>7))return 0;
if (result<-214748364||(result==-214748364&&tmp<-8))return 0;
result = result * 10 + tmp;
x = x / 10;
}
return result;
}
}
题解:
方法1:暴力遍历 – O(n2)
方法2:左右双指针 – O(n)
public class leecode11 {
//方法1:暴力遍历,时间复杂度:O(n2)
public int maxArea1(int[] height) {
int len = height.length;
if (len < 2) return 0;
int res = 0;
for (int left = 0; left < len; left++) {
for (int right = left + 1; right < len; right++) {
int tmp = (right - left) * Math.min(height[left], height[right]);
res = Math.max(res, tmp);
}
}
return res;
}
//方法2:左右指针,时间复杂度:O(n)
public int maxArea(int[] height) {
int left = 0, right = height.length - 1, res = 0;
while (right > left) {
res = Math.max(res, (right - left) * Math.min(height[left], height[right]));
//左右指针的移动规则:保留大的边界,移动边界小的指针
if (height[left] > height[right]){
right--;
}else {
left++;
}
}
return res;
}
}
题解:
方法1:暴力法 – O(n3)
方法2:左右指针法 – O(n2)。
nums[i]
,再使用左右指针指向nums[i]
后面的两端,数字分别为nums[l]
和nums[r]
,计算三数之和是否等于0,满足则添加到结果集。nums[i]>0
,则三数之和tmp
必然大于0;nums[i]==nums[i-1]
,则说明该数字重复,会导致重复的结果,执行continue;同样,如果tmp==0
时,nums[l]==nums[l+1]
跳过l++
,nums[r]==nums[r-1]
跳过r–;tmp>0
,则r--
;tmp<0
,则l++
;class Solution {
public List<List<Integer>> threeSum(int[] nums) {
ArrayList<List<Integer>> res = new ArrayList<>();
if (nums.length < 3) return res;
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
if (nums[i]>0)break;
int l = i + 1, r = nums.length - 1;
//去重
if (i > 0 && nums[i] == nums[i - 1]) continue;
while (r > l) {
int tmp = nums[i] + nums[l] + nums[r];
if (tmp == 0) {
res.add(Arrays.asList(nums[i], nums[l], nums[r]));
while (l < r && nums[l] == nums[l + 1]) l++;
while (l < r && nums[r] == nums[r - 1]) r--;
r--;
l++;
} else if (tmp > 0) {
r--;
} else {
l++;
}
}
}
return res;
}
}
题解:
与15题是同一类型。使用左右指针来代替暴力解法。
思路:遍历数组固定一点i
,在剩余数组中使用左右指针l
,r
进行遍历,知道两指针相遇。
int tmp = nums[i] + nums[l] + nums[r] - target;
当三点之和与target
的差最小。//移动左右指针 if (tmp == 0) { return target; } else if (tmp > 0) { r--; } else { l++; }
class Solution {
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int res = 0, l, r, dif = Integer.MAX_VALUE;
for (int i = 0; i < nums.length - 2; i++) {
l = i + 1;
r = nums.length - 1;
while (l < r) {
int tmp = nums[i] + nums[l] + nums[r] - target;
if (dif>Math.abs(tmp)){
res = nums[i] + nums[l] + nums[r];
dif = Math.abs(tmp);
}
//移动左右指针
if (tmp == 0) {
return target;
} else if (tmp > 0) {
r--;
} else {
l++;
}
}
}
return res;
}
}
class Solution {
private final String[] letter_map = {" ","*","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
private List<String> res = new ArrayList<>();
public List<String> letterCombinations(String digits) {
//边界条件
if (digits==null||digits.length()==0){
return new ArrayList<>();
}
//使用一个StringBuilder可以提高效率
iterStr(digits,new StringBuilder(),0);
return res;
}
private void iterStr(String str, StringBuilder stringBuilder, int index) {
//结束条件:到达深搜末尾,即最后一位数字。
if (index == str.length()){
res.add(stringBuilder.toString());
return;
}
char c = str.charAt(index);
int pos = c - '0';
String map_string = letter_map[pos];
//每个数字需要一层循环,用递归深搜实现循环嵌套
for(int i=0;i<map_string.length();i++) {
stringBuilder.append(map_string.charAt(i));
iterStr(str, stringBuilder, index+1);
//回溯:到达末尾后,删除最后一位,继续遍历该位的其他情况
stringBuilder.deleteCharAt(stringBuilder.length()-1);
}
}
}
题解:
优先级队列。时间复杂度:O(n*log(k)),n 是所有链表中元素的总和,k 是链表个数。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode head = new ListNode(0);
ListNode tmp = head;
PriorityQueue<ListNode> queue = new PriorityQueue<ListNode>((x,y)->(x.val-y.val));
for (ListNode node : lists) {
if (node!=null)queue.add(node);
}
while (!queue.isEmpty()){
tmp.next = queue.poll();
tmp = tmp.next;
if (tmp.next!=null)queue.add(tmp.next);
}
return head.next;
}
}
class Solution {
public int search(int[] nums, int target) {
List<Integer> list = Arrays.stream(nums).boxed().collect(Collectors.toList());
return list.indexOf(target);
}
}
方法2:二分查找。O(log n)
先寻找旋转点
。然后再根据target的大小选择在前半段
还是后半段
进行二分查找。
需要注意的点:旋转点可能是首端,旋转后数组并未变化。此时,后半段
=nums
,前半段
=null
class Solution {
public int search(int[] nums, int target) {
int rev = 0;
for (int i = 0; i < nums.length; i++) {
//寻找旋转点
//为了避免旋转点可能是首端而报越界,需添加 i + 1 < nums.length
if (i + 1 < nums.length && nums[i] > nums[i + 1]) {
rev = i + 1;
break;
}
}
int l, r;
//在前半段中二分查找
//如果旋转点在首端,则前半段为空,所以不能用nums[0]判断。而是选择必然存在的后半段末尾 nums[nums.length-1]
if (target > nums[nums.length-1]) {
l = 0;
r = rev - 1;
}
//后半段中二分查找
else {
l = rev;
r = nums.length - 1;
}
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return -1;
}
}
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int i = 1; i < n; i++) {
dp[0][i] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n+1];
dp [0] = 1;
dp [1] = 1;
for (int i = 2; i < n+1 ; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
题解:
回溯与DFS的区别:DFS 是一个劲的往某一个方向搜索,而回溯算法建立在 DFS 基础之上的,但不同的是在搜索过程中,达到结束条件后,恢复状态,回溯上一层,再次搜索。因此回溯算法与 DFS 的区别就是有无状态重置。
何时需要回溯算法:当问题需要 “回头”,以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止。
回溯算法的步骤:
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择
观察上图可得,选择列表里的数,都是选择路径(红色框)后面的数,比如[1]这条路径,他后面的选择列表只有"2、3",[2]这条路径后面只有"3"这个选择,那么这个时候,就应该使用一个参数start,来标识当前的选择列表的起始位置。也就是标识每一层的状态,因此被形象的称为"状态变量",最终函数签名如下
public void dfs(int[] nums, int start)
②找结束条件
此题非常特殊,所有路径都应该加入结果集,所以不存在结束条件。或者说当 start 参数越过数组边界的时候,程序就自己跳过下一层递归了,因此不需要手写结束条件,直接加入结果集。
③找选择列表
在①中已经提到过了,子集问题的选择列表,是上一条选择路径之后的数,即
for (int i = start; i < nums.length; i++)
④判断是否需要剪枝
从递归树中看到,路径没有重复的,也没有不符合条件的,所以不需要剪枝。
⑤做出选择(即for 循环里面的tmp.add(nums[i]);
)
⑥撤销选择
class Solution {
ArrayList<List<Integer>> res = new ArrayList<>();
ArrayList<Integer> tmp = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
if (nums == null || nums.length == 0) return res;
dfs(nums, 0);
return res;
}
public void dfs(int[] nums, int start) {
res.add(new ArrayList<>(tmp));
for (int i = start; i < nums.length; i++) {//找选择列表
tmp.add(nums[i]);//做出选择
dfs(nums, i + 1);//递归进入下一层,注意i+1,标识下一个选择列表的开始位置,最重要的一步
tmp.remove(tmp.size() - 1);//撤销选择
}
}
}
题解:
①递归树
可以发现,树中出现了大量重复的集合,②和③和第一个问题一样,不再赘述,我们直接看第四步。
④判断是否需要剪枝,需要先对数组排序,使用排序函数
显然我们需要去除重复的集合,即需要剪枝,把递归树上的某些分支剪掉。那么应去除哪些分支呢?又该如何编码呢?
观察上图不难发现,应该去除当前选择列表中,与上一个数重复的那个数,引出的分支,如 “2,2” 这个选择列表,第二个 “2” 是最后重复的,应该去除这个 “2” 引出的分支
(去除图中红色大框中的分支)
编码呢,刚刚说到是 “去除当前选择列表中,与上一个数重复的那个数,引出的分支”,说明当前列表最少有两个数,当i>start时,做选择的之前,比较一下当前数,与上一个数 (i-1) 是不是相同,相同则 continue,
⑤做出选择
⑥撤销选择
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> tmp = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
if (nums==null||nums.length==0)return res;
Arrays.sort(nums);
traceBack(nums,0);
return res;
}
public void traceBack(int[] nums, int start){
res.add(new ArrayList<>(tmp));
for (int i = start; i < nums.length; i++) {
if (i > start && nums[i]==nums[i-1])continue;//剪枝去重
tmp.add(nums[i]);
traceBack(nums, i+1);
tmp.remove(tmp.size()-1);
}
}
}
题解:
①递归树
(绿色箭头上面的是路径,红色框[]则为结果,黄色框为选择列表)
从上图看出,组合问题和子集问题一样,1,2 和 2,1 `是同一个组合,因此 **需要引入start参数标识,每个状态中选择列表的起始位置。**另外,每个状态还需要一个 sum 变量,来记录当前路径的和,函数签名如下
private void traceBack(int[] candidates, int target,int start, int sum) {
②找结束条件
由题意可得,当路径总和等于 target 时候,就应该把路径加入结果集,并 return
if (sum==target){
res.add(new ArrayList<>(tmp));
}
③找选择列表
for (int i = start; i < candidates.length; i++) {
④判断是否需要剪枝
从①中的递归树中发现,当前状态的sum大于target的时候就应该剪枝,不用再递归下去了
if (sum>target)continue;
⑤做出选择
题中说数可以无限次被选择,那么 i 就不用 +1 。即下一层的选择列表,从自身开始。并且要更新当前状态的sum。
⑥撤销选择
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> tmp = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates==null||candidates.length==0)return res;
traceBack(candidates,target,0,0);
return res;
}
private void traceBack(int[] candidates, int target,int start, int sum) {
if (sum==target){
res.add(new ArrayList<>(tmp));
}
for (int i = start; i < candidates.length; i++) {
if (sum>target)continue;
tmp.add(candidates[i]);
traceBack(candidates, target, i, sum+candidates[i]);
tmp.remove(tmp.size()-1);
}
}
}
题解:
①递归树
(最下面的叶子节点,红色【】中的就是要求的结果)
然后我们来回想一下,整个问题的思考过程,这棵树是如何画出来的。首先,我们固定1,然后只有2、3可选:如果选2,那就只剩3可选,得出结果[1,2,3];如果选3,那就只剩2可选,得出结果[1,3,2]。再来,如果固定2,那么只有1,3可选:如果选1,那就只剩3,得出结果[2,1,3]…
有没有发现一个规律:如果我们固定了(选择了)某个数,那么他的下一层的选择列表就是——除去这个数以外的其他数!!
traceBack(nums,used);
用used
来标记已经选择了的数。
②找结束条件
if (tmp.size()==nums.length){
res.add(new ArrayList<>(tmp));
return;
}
③找准选择列表
for (int i = 0; i < nums.length; i++) {
if (!used[i]){ //从给定的数中除去用过的,就是当前的选择列表
...
}
}
④判断是否需要剪枝
不需要剪枝,或者你可以认为,!used[i]已经是剪枝
⑤做出选择
for (int i = 0; i < nums.length; i++) {
if (!used[i]){
tmp.add(nums[i]);//做选择
used[i] = true;设置当前数已用
traceBack(nums,used);//进入下一层
...
}
}
⑥撤销选择
for (int i = 0; i < nums.length; i++) {
if (!used[i]){
tmp.add(nums[i]);
used[i] = true;
traceBack(nums,used);
tmp.remove(tmp.size()-1);//撤销选择
used[i] = false;//撤销选择
}
}
class Solution {
ArrayList<List<Integer>> res = new ArrayList<>();
ArrayList<Integer> tmp = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
if (nums==null||nums.length==0)return res;
boolean[] used = new boolean[nums.length];
traceBack(nums,used);
return res;
}
private void traceBack(int[] nums, boolean[] used) {
if (tmp.size()==nums.length){
res.add(new ArrayList<>(tmp));
return;
}
for (int i = 0; i < nums.length; i++) {
if (!used[i]){//从给定的数中除去用过的,就是当前的选择列表
tmp.add(nums[i]);//做选择
used[i] = true;//设置当前数已用
traceBack(nums,used);//进入下一层
tmp.remove(tmp.size()-1);//撤销选择
used[i] = false;//撤销选择
}
}
}
}
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
题解:
①递归树
可以看到,有两组是各自重复的,那么应该剪去哪条分支?首先要弄懂,重复结果是怎么来的,比如最后边的分支,选了第二个2后,,竟然还能选第一个2,从而导致最右边整条分支都是重复的
②③不再赘述,直接看④
④判断是否需要剪枝,如何编码
有了前面“子集、组合”问题的判重经验,同样首先要对题目中给出的nums
数组排序,让重复的元素并列排在一起,在if(i>start&&nums[i]==nums[i-1])
,基础上修改为if(i>0&&nums[i]==nums[i-1]&&!used[i-1])
,语义为:当i可以选第一个元素之后的元素时(因为如果i=0,即只有一个元素,哪来的重复?有重复即说明起码有两个元素或以上,i>0),**然后判断当前元素是否和上一个元素相同?**如果相同,**再判断上一个元素是否能用?**如果三个条件都满足,那么该分支一定是重复的,应该剪去
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> list = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
if (nums==null||nums.length==0)return res;
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
traceBack(nums,used);
return res;
}
private void traceBack(int[] nums, boolean[] used) {
if (list.size() == nums.length){
res.add(new ArrayList<>(list));
return;
}
for (int i = 0; i < nums.length; i++) {
//剪枝
if (i>0&&nums[i]==nums[i-1]&& !used[i - 1])continue;//剪枝,三个条件
if (!used[i]){//从给定的数中除去,用过的数,就是当前的选择列表
list.add(nums[i]);
used[i] = true;
traceBack(nums, used);
list.remove(list.size()-1);
used[i] = false;
}
}
}
}
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
题解:
与 全排列 II 一摸一样
class Solution {
List<String> res = new ArrayList<>();
StringBuilder tmp = new StringBuilder();
public String[] permutation(String s) {
if (s == null || s.length() == 0) return new String[0];
char[] chars = new char[s.length()];
s.getChars(0, s.length(), chars, 0);
Arrays.sort(chars);
boolean[] used = new boolean[chars.length];
traceBack(chars, used);
String[] resStr = new String[res.size()];
res.forEach(e -> {
resStr[res.indexOf(e)] = e;
});
return resStr;
}
private void traceBack(char[] chars, boolean[] used) {
if (tmp.length() == chars.length) {
res.add(new String(tmp));
return;
}
for (int i = 0; i < chars.length; i++) {
if (i > 0 && chars[i] == chars[i-1] && used[i-1])continue;
if (!used[i]){
tmp.append(chars[i]);
used[i] = true;
traceBack(chars, used);
tmp.deleteCharAt(tmp.length()-1);
used[i] = false;
}
}
}
}
题解:
括号可以重复选,所以选择列表不需要start指定开始位置。
回溯的流程基本不变,重要在两处剪枝check,check的剪枝逻辑:
1.第一个字符不能是)
2.当前情况下,左右括号的数量都不可以超过n
,且(的数量
>=)的数量
class Solution {
char[] source = {'(', ')'};
List<String> res = new ArrayList<>();
StringBuilder tmp = new StringBuilder();
public List<String> generateParenthesis(int n) {
if (n == 0) return res;
dfs(n);
return res;
}
private void dfs(int n) {
if (tmp.length() >= n*2) {
//添加前进行剪枝,主要判断最后一位括号。
if (check(tmp, n)) res.add(tmp.toString());
return;
}
for (int i = 0; i < source.length; i++) {
if (check(tmp, n)){//遍历时剪枝
tmp.append(source[i]);
dfs(n);
tmp.deleteCharAt(tmp.length() - 1);
}
}
}
private boolean check(StringBuilder tmp, int n) {
if (tmp.length()==0)return true;
if (tmp.charAt(0) == ')') return false;
int countLeft = 0, countRight = 0;
for (int i = 0; i < tmp.length(); i++) {
if (tmp.charAt(i) == '(') countLeft++;
else countRight++;
}
return countLeft <= n && countRight <= n && countLeft>=countRight;
}
}
题解:
递归结束条件:遍历完所有元素,且被分为>=3个数
剪枝情况:
两位以上的数字不能以0开头。
不能超过整数的最大值。
val>res.get(res.size()-1)+res.get(res.size()-2)
如果当前数已经超过,则继续下去也不会满足条件。
不剪枝的情况:res中前两个元素直接添加;后面的元素满足斐波那契数列则添加。
class Solution {
List<Integer> res = new ArrayList<>();
List<Integer> tmp = new ArrayList<>();
public List<Integer> splitIntoFibonacci(String num) {
if (num == null || num.length() == 0) return res;
dfs(num, 0);
return tmp;
}
private void dfs(String num, int start) {
if (start>=num.length()&&res.size()>=3){
tmp = new ArrayList<>(res);
return;
}
for (int i = start; i <num.length() ; i++) {
long val = Long.parseLong(num.substring(start,i+1));
if (num.charAt(start)=='0'&& i>start)break;
if (val>Integer.MAX_VALUE)break;
if (res.size()>=2&&val>res.get(res.size()-1)+res.get(res.size()-2))break;
if (res.size()<2||val==res.get(res.size()-1)+res.get(res.size()-2)){
res.add((int)val);
dfs(num, i+1);
res.remove(res.size()-1);
}
}
}
}
class Solution {
List<String> res = new ArrayList<>();
List<String> tmp = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
dfs(s, 0);
return res;
}
private void dfs(String s, int start) {
//剪枝,大于四段直接返回
if (tmp.size()>4)return;
//等于四段时,后面还有数字
if (tmp.size()==4&&start!=s.length())return;
//等于四段,且没数字了
if (tmp.size()==4){
res.add(String.join(".", tmp));
return;
}
//遍历选择列表
for (int i = start; i <s.length() ; i++) {
String subStr = s.substring(start,i+1);
//每段不能以0开头、不能小于0、大于255
if (subStr.length()>1&&subStr.startsWith("0")||subStr.length()>3)break;
int val = Integer.parseInt(subStr);
if (val<0||val>255)break;
tmp.add(subStr);
dfs(s, i+1);
tmp.remove(tmp.size()-1);
}
}
}
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
if (n <= 0) return res;
//初始化棋盘
char[][] arr = new char[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
arr[i][j] = '.';
}
}
traceBack(arr, 0, n);
return res;
}
//i:行 j:列
private void traceBack(char[][] arr, int i, int n) {
// ② 找结束条件
if (i == n) {
res.add(arrToList(arr));
return;
}
for (int j = 0; j < n; j++) { // ③ 选择列表
if (checked(arr, i, j, n)) { //④ 做出选择并剪枝
arr[i][j] = 'Q';
} else {
continue;
}
traceBack(arr, i + 1, n); // ⑤ 下一行的递归
arr[i][j] = '.'; // ⑥ 撤销选择
}
}
//判断是否满足条件
private boolean checked(char[][] arr, int i, int j, int n) {
//横向
for (int k = 0; k < n; k++) {
if (arr[i][k] == 'Q' && k != j) return false;
}
//纵向
for (int k = 0; k < i; k++) {
if (arr[k][j] == 'Q') return false;
}
//左上角
int ii = i,jj=j;
while (ii > 0 && jj > 0) {
if (arr[--ii][--jj] == 'Q') return false;
}
//右上角
while (i > 0 && j < n-1) {
if (arr[--i][++j] == 'Q') return false;
}
return true;
}
private List<String> arrToList(char[][] arr) {
ArrayList<String> list = new ArrayList<>();
StringBuilder builder = new StringBuilder();
int length = arr.length;
for (int i = 0; i < length; i++) {
for (int j = 0; j < length; j++) {
builder.append(arr[i][j]);
}
list.add(builder.toString());
builder = new StringBuilder();
}
return list;
}
}
class Solution {
List<List<String>> res = new ArrayList<>();
List<String> tmp = new ArrayList<>();
public List<List<String>> partition(String s) {
if (s == null || s.length() == 0) return res;
traceBack(s, 0);
return res;
}
private void traceBack(String s, int start) {
if (start >= s.length()) { //递归结束条件
res.add(new ArrayList<>(tmp));
return;
}
for (int i = start; i < s.length(); i++) { //选择列表
String s1 = s.substring(start, i+1); //选择、剪枝
if (isPalindrome(s1)) {
tmp.add(s1);
} else {
continue;
}
traceBack(s, i + 1); //递归
tmp.remove(tmp.size() - 1); //撤销选择
}
}
private boolean isPalindrome(String s) {
StringBuilder builder = new StringBuilder(s);
return s.equals(builder.reverse().toString());
}
}
像这种配对抵消的问题,一般可以使用辅助栈先进后出的特点进行求解。题目需要求解最长有效括号的长度,需要注意的是,有效括号不仅包括题中示例:()()()
这种连续的非嵌套括号,还包括这种套娃型括号((()))
都属于有效括号。
(
还是 )
。(
,因为括号长度都是按右括号来决定。)
。(
且栈顶元素是右括号 )
res
记录最长有效括号的长度:每次出栈时说明出现了括号配对,此时需要记录res
。每次出栈后,有两种情况:
i + 1
res
)、栈顶元素之后(长度为i-stack.peek()
)的括号都成功配对。所以,取二者的较大值:Math.max(res,i-stack.peek())
。 遍历完 s 的所有元素后,返回res
即可。class Solution {
public int longestValidParentheses(String s) {
if (s == null || s.length() < 2) return 0;
int res = 0;
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
if (stack.isEmpty()||s.charAt(stack.peek())==')'||s.charAt(i)=='(')stack.push(i);
else {
stack.pop();
res = Math.max(res, stack.isEmpty()?i+1:i-stack.peek());
}
}
return res;
}
}
题目:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
题解:
动态规划(转换为 0-1 背包问题)
基本思路:
每个物品考虑 选 或 不选,一个一个物品考虑,一点一点扩大容量的范围。本题与 0-1 背包问题有一个很大的不同,即:
boolean[][] dp = new boolean[length][target + 1]
; dp[i][j] = true or false
i
:遍历到的物品当前位置;j
:动态背包容量dp[i][j]
: [0,i
]区间的元素之和是否可以等于j
nums[i]
和目标值j
关系可分为:
nums[i]方程为: dp[i][j] = dp[i-1][j-nums[i]]||dp[i-1][j];
其中dp[i-1][j]
为true的情况: 只需要[0,i-1]的元素就可以等于 j,不需要再加 nums[i]
dp[i-1][j-nums[i]]
为true的情况: 表示[0,i-1]的元素可以达到 j-nums[i],加上 nums[i],则刚好是[0,i]个元素可以达到 j
nums[i]==j
方程为: dp[i][j] = true;
nums[i]>j)
方程为: dp[i][j] = dp[i-1][j]
class Solution {
public boolean canPartition(int[] nums) {
int length = nums.length;
int sum = Arrays.stream(nums).sum();
if (length < 2 || sum % 2 == 1) return false;
int target = sum / 2;
boolean[][] dp = new boolean[length][target + 1];
for (int j = 0; j < target; j++) {
if (nums[0] == j)dp[0][j] = true;
}
for (int i = 1; i < length; i++) {
for (int j = 0; j <= target; j++) {
if (nums[i]<j){
//dp[i-1][j]为true: 只需要[0,i-1]的元素就可以等于 j,不需要再加 nums[i]
//dp[i-1][j-nums[i]]为true: 表示[0,i-1]的元素可以达到 j-nums[i],加上 nums[i],则刚好是[0,i]个元素可以达到 j
dp[i][j] = dp[i-1][j-nums[i]]||dp[i-1][j];
}
if (nums[i]==j){ //那就只要 nums[i],扔掉其他的所有元素,直接为true
dp[i][j] = true;
}
if (nums[i]>j) dp[i][j] = dp[i-1][j]; //此时,肯定不能考虑把 nums[i]加进来了
}
}
return dp[length-1][target];
}
}
dp[len][target]
,len
为数组的长度,target
为目标值。如果需要返回方案数则定义为整形数组,如果是判断是否可行(例如上题),则定义为布尔数组。但一般背包问题是选与不选:0、num[i],而这道题是必须选,或加或减:+num[i]、-num[i]。因此定义dp数组的大小也发生了变化。int[][] dp = new int[len][2*sum+1];
。[2*sum+1]
这里主要是考虑到数组元素的和>目标值的话,可能会发生数组越界异常。那为什么不设为sum+1
的大小呢?这可以用状态转移方程解释。。dp[i][j] = dp[i-1][j+nums[i]]+dp[i-1][j-nums[i]]
,注意,一般在写方程的时候都要留意下标越界异常。
i-1
:遍历从 i=1 开始, i = 0的情况比较简单,我们可以在第三步进行显式初始化。因此不会发生越界。j+nums[i]
,为了防止越界,我们才把dp[0]的大小设为 2*sum+1。解释了第一步的问题。j-nums[i]
,此处越界主要是它可能会小于0,而数组的下标是从0开始的正整数。但是,从题意来看,目标值确实也可以为负数。那怎么办呢,我们不可能让数组的下标为负数啊。实际上,目标值的正负并不影响有多少种方法可以得到它。比如:arr={1,1},target=2。那么只有一种方案:+1+1;那target=-2呢,target互为相反数的话,把求取过程全部取反不就行了。因此,得出结论dp[i-1][j-nums[i]]==dp[i-1][Math.abs(j-nums[i])]
成立。修改状态转移方程来避免越界。for (int j = 0; j <= sum; j++) { if (nums[0]==j)dp[0][j]=1; }
if (nums[0]==0)dp[0][0]=2;
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int len = nums.length;
target=target<0?-target:target;
int sum = Arrays.stream(nums).sum();
if (sum<target)return 0;
int[][] dp = new int[len][2*sum+1];
//初始化
for (int j = 0; j <= sum; j++) {
if (nums[0]==j)dp[0][j]=1;
}
if (nums[0]==0)dp[0][0]=2;
for (int i = 1; i < len; i++) {
for (int j = 0; j <= sum; j++) {
dp[i][j] = dp[i-1][j+nums[i]]+dp[i-1][Math.abs(j-nums[i])];
}
}
return dp[len-1][target];
}
}
题解:
与0-1背包问题不同,本题每个物品可以拿无限多次,而01背包每个物品只有一个,只需要判断拿或者不拿。因此状态转移方程略有不同。而且一般背包问题要求总值小于目标值,而本题是恰好等于。
定状态:
int[][] dp = new int[coins.length][amount + 1];
dp[i][j]
表示[0,i]
硬币总值为 j
时(恰好等于)的最小硬币数。
状态转移方程:
dp[i][j]=Math.min(dp[i][j],dp[i][j-coins[i]]+1);
因为每个硬币可以重复选,所以dp的状态可以从两种状态转移而来:
dp[i][j]
: 有两层含义,第一种含义[0,i]
硬币中,coins[i]
已经取过但不用关心取了几个,且这次不取了,所以状态还是dp[i][j]
。第二种含义,coins[i]
还没取,这次也同样不取,此时dp[i][j]=dp[i-1][j]
,所以每次状态转移方程之前都需要把dp[i][j] = dp[i-1][j];
先抄过来。dp[i][j-coins[i]]+1)
,表示取coins[i]
。比较好理解。dp[i][0] = 0;
表示目标值为 0 时,硬币数量也为0;if(j%coins[0]==0)dp[0][j]=j/coins[0];
只用第一种硬币。class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[][] dp = new int[coins.length][amount + 1];
for (int i = 0; i < coins.length; i++) {
for (int j = 0; j <= amount; j++) {
dp[i][j] = max;
}
}
for (int i = 0; i <coins.length; i++) {
dp[i][0] = 0;
}
for(int j=0;j<=amount;j++){
if(j%coins[0]==0)dp[0][j]=j/coins[0];
}
for (int i = 1; i < coins.length; i++) {
for (int j = 0; j <= amount; j++) {
dp[i][j] = dp[i-1][j];
if (j>=coins[i])
dp[i][j]=Math.min(dp[i][j],dp[i][j-coins[i]]+1);
}
}
return dp[coins.length-1][amount] > amount ? -1 : dp[coins.length-1][amount];
}
}
题解:
同样是完全背包问题,只是需要求解所有的组合方案。因此,dp状态不变,状态转移方程略有不同。最大的区别在于初始化.
定状态:
int[][] dp = new int[coins.length][amount + 1];
写方程:
dp[i][j]=dp[i][j]+dp[i][j-coins[i]];
初始化:
if (j%coins[0]==0)dp[0][j]=1;
因为是恰好等于目标值,所以需要使用 if 进行限制。不管硬币用了多少种,组合只有 1 个。所以,初始化为1.
class Solution {
public int change(int amount, int[] coins) {
if (amount==0)return 1;
int[][] dp = new int[coins.length][amount + 1];
for (int j = 0; j <=amount ; j++) {
if (j%coins[0]==0)dp[0][j]=1;
}
for (int j = 0; j <= amount; j++) {
for (int i = 1; i < coins.length; i++) {
dp[i][j] = dp[i-1][j];
if (j>=coins[i])
dp[i][j]=dp[i][j]+dp[i][j-coins[i]];
}
}
return dp[coins.length-1][amount];
}
}
class Solution {
public int combinationSum4(int[] nums, int target) {
int len = target;
int[][] dp = new int[len+1][target + 1];
dp[0][0] = 1;
int res = 0;
for (int i = 1; i <= len; i++) {
for (int j = 0; j <= target; j++) {
for (int num : nums) {
if(j>=num)dp[i][j] += dp[i - 1][j - num];
}
}
res += dp[i][target];
}
return res;
}
}
题解:
本题与上题为同一类题目,列表中的元素可以无限取用,属于完全背包问题。同时,字符串自带排列效果。因此和上题一样为排列完全背包问题。
对于排列完全背包问题,dp 数组的定义方式一般为:
dp[i][j]
从列表中选 i
个元素(记录的是实际使用到的元素个数),可重复选。使得目标值为 j ; 而对于组合背包问题,dp数组的定义 i
为列表中[0,i]的元素(记录的是实际使用到的元素种类数)。
定状态:boolean[][] dp = new boolean[len + 1][s.length() + 1];
实际使用的元素个数最多为字符串的长度,即每个元素都是单个字符,所以 len = s.length
。j 的定义为s的[0,j]
部分字符串。
写方程: 所选择的最后一个元素可能是 list 种的任意一个。所以:
dp[i][j] = dp[i-1][j-list[0]] || dp[i-1][j-list[1]] || ... || dp[i-1][j-list[list.size()]]
。当然,前提是该元素能和字符串的最后一部分匹配上:s.startsWith(word, j-word.length())
为true.
初始化::目标值为0时总为true, 初始化第一列 dp[i][0] = true;
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int len = s.length();
//len+1:可以选择最多的 list 中的元素,
boolean[][] dp = new boolean[len + 1][s.length() + 1];
for (int i = 0; i <= len; i++) {
dp[i][0] = true;
}
for (int j = 1; j <= s.length(); j++) {
for (int i = 1; i <= len; i++) {
for (String word : wordDict) {
if (j >= word.length() && s.startsWith(word, j-word.length()))
dp[i][j] |= dp[i - 1][j - word.length()];
}
}
}
return dp[len][s.length()];
}
}
题解:
不要关注冷冻期!不要关注冷冻期!不要关注冷冻期!
只关注卖出的那一天!只关注卖出的那一天!只关注卖出的那一天!
题目中定义的“冷冻期”=卖出的那一天的后一天,题目设置冷冻期的意思是,如果昨天卖出了,今天不可买入,那么关键在于哪一天卖出,只要在今天想买入的时候判断一下前一天是不是刚卖出,即可,所以关键的一天其实是卖出的那一天,而不是卖出的后一天。
定状态::分为持有、不持有。不持有又可以分为:未卖出不持有(压根就没买)、卖出不持有。因此,共有三种状态。
dp[i][0]
dp[i][1]
dp[i][2]
写方程::
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2])
:今天未卖出不持有可能是昨天就已经是未卖出不持有,也有可能是昨天刚卖。
dp[i][1] = Math.max(dp[i-1][1],dp[i][0]-prices[i])
:今天持有可能是昨天就已经持有,也有可能是由于昨天刚买入。(因为冷冻期的存在,这里不能是昨天卖出不持有dp[i-1][2]
后今天再买入)
dp[i][2] = dp[i-1][1]+prices[i]
:今天卖出,那昨天一定是持有了
初始化:
dp[0][0] = 0;
dp[0][1] = -prices[0];
买入是,花钱了收入肯定是负的,卖出时才为正
dp[0][2] = 0;
class Solution {
public int maxProfit(int[] prices) {
if (prices.length<1)return 0;
int[][] dp = new int[prices.length][3];
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
dp[i][2] = dp[i-1][1]+prices[i];
}
return Math.max(dp[prices.length-1][0],dp[prices.length-1][2]);
}
}
dp[i][1] = Math.max(dp[i-1][1],Math.max(dp[i-1][0]-prices[i]-fee,dp[i-1][2]-prices[i]-fee));
dp[i-1][1]
dp[i-1][0]-prices[i]-fee
(买入时扣除手续费)dp[i-1][2]-prices[i]-fee
(买入时扣除手续费)class Solution {
public int maxProfit(int[] prices, int fee) {
if (prices.length<1)return 0;
int[][] dp = new int[prices.length][3];
//未卖出未持有
dp[0][0] = 0;
//持有
dp[0][1] = -prices[0]-fee;
//卖出未持有
dp[0][2] = 0;
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2]);
dp[i][1] = Math.max(dp[i-1][1],Math.max(dp[i-1][0]-prices[i]-fee,dp[i-1][2]-prices[i]-fee));
dp[i][2] = dp[i-1][1]+prices[i];
}
return Math.max(dp[prices.length-1][0],dp[prices.length-1][2]);
}
}
题解:
此题和上面两题有一个明显的不同:最多完成 2 笔交易.
因此状态需要额外加一个维度来记录当前的交易次数。
定状态:dp[i][0][k]
:第 i -1天(下标从0开始),持有,且交易次数为 k(k<=2);dp[i][1][k]
:第i-1天,未持有,且交易次数为 k (k<=2)
写方程:
dp[i][0][0] = 0
:未持有且交易次数为0,那表明从来没买过,收益一直为0。dp[i][0][1] = Math.max(dp[i-1][0][1],dp[i-1][1][0]+prices[i])
:今天未持有且交易过1次,可能昨天也是这样,也可能恰好今天是第1次交易dp[i-1][1][0]+prices[i]dp[i][0][2] = Math.max(dp[i-1][0][2],dp[i-1][1][1]+prices[i])
:今天未持有且交易过2次,可能昨天也是这样,也可能恰好今天是第2次交易dp[i-1][1][1]+prices[i]dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][0][0]-prices[i])
:今天持有且没有交易过,可能昨天也是这样,也可能恰好今天第1次买入dp[i-1][0][0]-prices[i]dp[i][1][1] = Math.max(dp[i-1][1][1],dp[i-1][0][1]-prices[i])
:今天持有且交易过1次,可能昨天也是这样,也可能恰好今天第2次买入dp[i-1][0][1]-prices[i]dp[i][1][2] = MIN_VALUE
: 交易次数已经用完,但还持有,这种情况不能发生。dp[0][0][0] = 0;
第0天未买入dp[0][1][0] = -prices[0];
第0天买入dp[0][0][1] = MIN_VALUE;
dp[0][0][2] = MIN_VALUE;
dp[0][1][1] = MIN_VALUE;
dp[0][1][2] = MIN_VALUE;
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length <= 1) return 0;
int[][][] dp = new int[prices.length][2][3];
int MIN_VALUE = Integer.MIN_VALUE / 2;
dp[0][0][0] = 0;
dp[0][1][0] = -prices[0];
dp[0][0][1] = MIN_VALUE;
dp[0][0][2] = MIN_VALUE;
dp[0][1][1] = MIN_VALUE;
dp[0][1][2] = MIN_VALUE;
for (int i = 1; i < prices.length; i++) {
dp[i][0][0] = 0;
dp[i][0][1] = Math.max(dp[i-1][0][1],dp[i-1][1][0]+prices[i]);
dp[i][0][2] = Math.max(dp[i-1][0][2],dp[i-1][1][1]+prices[i]);
dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][0][0]-prices[i]);
dp[i][1][1] = Math.max(dp[i-1][1][1],dp[i-1][0][1]-prices[i]);
dp[i][1][2] = MIN_VALUE;//不可能
}
return Math.max(0,Math.max(dp[prices.length-1][0][1],dp[prices.length-1][0][2]));
}
}
题解:
上一题目的一摸一样,只不过把2换成了k。思路解法一致。
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices.length<1)return 0;
int[][][] dp = new int[prices.length][2][k+1];
final int MINVAL = Integer.MIN_VALUE/2;
for (int i = 0; i <= k; i++) {
dp[0][0][i] = MINVAL;
dp[0][1][i] = MINVAL;
if (i==0){
dp[0][0][i] = 0;
dp[0][1][i] = -prices[0];
}
}
for (int i = 1; i < prices.length; i++) {
for (int j = 0; j <= k; j++) {
if (j==0) dp[i][0][j] = 0;
else dp[i][0][j] = Math.max(dp[i-1][0][j],dp[i-1][1][j-1]+prices[i]);
if (j==k) dp[i][1][j] = MINVAL;
else dp[i][1][j] = Math.max(dp[i-1][1][j],dp[i-1][0][j]-prices[i]);
}
}
int res = 0;
for (int i = 0; i <= k; i++) {
res = Math.max(res, dp[prices.length-1][0][i]);
}
return res;
}
}
三指针法,left2 - left1;
为开头0的个数,即right每向右移动一位,有多少个子数组。
class Solution {
//三指针法
public int numSubarraysWithSum(int[] nums, int goal) {
int res = 0;
int sum1 = 0, sum2 = 0;
int left1 = 0, left2 = 0, right = 0;
while (right < nums.length) {
sum1 += nums[right];
while ((left1 <= right && sum1 > goal)) {
sum1 -= nums[left1];
left1++;
}
sum2 += nums[right];
while (left2 <= right && sum2 >= goal) {
sum2 -= nums[left2];
left2++;
}
res += left2 - left1;
right++;
}
return res;
}
}
题解:
使用优先级队列,优先出队最大元素。统计最大元素出队个数,
if (val == count) {//如果出队列元素和个数相同,则 h = count;结束循环。
h = count;
break;
} else if (val < count) { //如果出列元素值小于count,则 h = count-1;
h = count - 1;
break;
}else {//出列元素大于count,h=count;继续循环。
h=count;
}
class Solution {
public int hIndex(int[] citations) {
int h = 0;
int count = 0;
PriorityQueue<Integer> queue = new PriorityQueue<>(((o1, o2) -> (o2 - o1)));
for (int i = 0; i < citations.length; i++) {
queue.add(citations[i]);
}
int size = queue.size();
for (int i = 0; i < size; i++) {
count++;
Integer val = queue.poll();
if (val == count) {
h = count;
break;
} else if (val < count) {
h = count - 1;
break;
}else {
h=count;
}
}
return h;
}
}
class Solution {
public int minDepth(TreeNode root) {
if (root==null) return 0;
if (root.left == null && root.right == null)return 1;
int m1 = minDepth(root.left);
int m2 = minDepth(root.right);
if (root.left==null||root.right==null)return m1+m2+1;
return Math.min(m1, m2)+1;
}
}
题解:
先求左右子树的最大深度,根据最大深度的差值是否大于1判断,然后递归判断左子树和右子树。都成立则返回true。
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) return true;
return Math.abs(dfs(root.left) - dfs(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
public int dfs(TreeNode node) {
if (node == null) return 0;
return Math.max(dfs(node.left), dfs(node.right)) + 1;
}
}
class Solution {
int res = 0;
public int diameterOfBinaryTree(TreeNode root) {
compare(root);
return res-1;
}
//最大直径就是左右子树的最大深度的和
public void compare(TreeNode node) {
if (node != null) {
res = Math.max(res, dfs(node.left) + dfs(node.right) + 1);
compare(node.left);
compare(node.right);
}
}
//树的深度
public int dfs(TreeNode node) {
if (node == null) return 0;
return Math.max(dfs(node.left), dfs(node.right)) + 1;
}
}
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
targetSum-=root.val;
if (targetSum==0&&root.left==null&&root.right==null)return true;
return hasPathSum(root.left, targetSum)|| hasPathSum(root.right, targetSum);
}
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
if (root == null) return res;
dfs(root, targetSum);
return res;
}
private void dfs(TreeNode root, int targetSum) {
if (root==null)return;
targetSum -= root.val;
path.add(root.val);
if (targetSum == 0 && root.left == null && root.right == null) {
res.add(new ArrayList<>(path));
}
dfs(root.left, targetSum);
dfs(root.right, targetSum);
path.remove(path.size() - 1);
}
}
class Solution {
int res = 0;
public int pathSum(TreeNode root, int targetSum) {
traversal(root, targetSum);
return res;
}
private void traversal(TreeNode root, int targetSum){
if (root==null)return;
dfs(root,targetSum);
if (root.left!=null)traversal(root.left, targetSum);
if (root.right!=null)traversal(root.right, targetSum);
}
private void dfs(TreeNode root, int targetSum) {
if (root==null)return;
targetSum-=root.val;
if (targetSum==0)res++;
dfs(root.left, targetSum);
dfs(root.right,targetSum);
}
}