253.会议室Ⅱ
带解锁
279.完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...
)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:输入: n = 12 输出: 3 解释: 12 = 4 + 4 + 4.
示例 2:输入: n = 13 输出: 2 解释: 13 = 4 + 9.
分析:动态规划,建立dp数组,存储和为n的最小步数。例如n=12,上一步可以由11+1,8+4,3+9得来,那么如何选择呢,当然是选择到11、8、3的最小步数。11可以由10+1,7+4,2+9得来,8可以由7+1,4+4得来,3可以由2+1得来。依次递归。
1 class Solution { 2 public int numSquares(int n) { 3 int[] dp = new int[n+1]; 4 dp[0] = 0; 5 for(int i=1;i<=n;i++){ 6 dp[i] = i;//最大步数,1+1+1+1…得来,是为了下一步取最小值做准备 7 for(int j=1;j*j<=i;j++){ 8 dp[i] = Math.min(dp[i], dp[i-j*j]+1); 9 } 10 } 11 return dp[n]; 12 } 13 }
283.移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:输入: [0,1,0,3,12] 输出: [1,3,12,0,0].
说明:必须在原数组上操作,不能拷贝额外的数组。尽量减少操作次数。
分析:依次遍历数组,将非零数据往前挪,剩下的全设置为0.
1 class Solution { 2 public void moveZeroes(int[] nums) { 3 int index = 0; 4 for(int i=0;i<nums.length;i++){ 5 if(nums[i]!=0){//index一直小于i 6 nums[index++] = nums[i]; 7 } 8 } 9 while(index < nums.length){ 10 nums[index++] = 0; 11 } 12 } 13 }
287.寻找重复数
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:输入: [1,3,4,2,2] 输出: 2
示例 2:输入: [3,1,3,4,2] 输出: 3
说明:不能更改原数组(假设数组是只读的)。只能使用额外的 O(1) 的空间。时间复杂度小于 O(n2) 。数组中只有一个重复的数字,但它可能不止重复出现一次。
分析:1.排序(不改变原数组,则不能使用) 2.集合set(空间复杂度O(N),不能使用) 3.如果数组没有重复元素,那么索引0~n-1对应着数值1~n。如果含有重复元素,比如[3,1,3,4,2], nums[0]=3, nums[3]=4, nums[4]=2, nums[2]=3, nums[3]=4…出现循环情况,因为有两处index对应着3。可以利用链表内是否有环的方法来进行操作,设置快慢指针,快指针一次走两步,慢指针走一步。
设链表(数组)共有 a+b个节点,其中 链表头部到链表入口 有 a 个节点(不计链表入口节点), 链表环 有 b个节点(这里需要注意,a 和 b是未知数);设两指针分别走了 f,s 步,则有:
fast 走的步数是slow步数的 2 倍,即 f=2s(解析: fast 每轮走 2 步)
fast 比 slow多走了 n 个环的长度,即 f=s+nb ;( 解析: 双指针都走过 aaa 步,然后在环内绕圈直到重合,重合时 fast 比 slow 多走 环的长度整数倍 );
以上两式相减得:f=2nb,s=nb,即fast和slow 指针分别走了 2n,n个 环的周长 (注意: n 是未知数,不同链表的情况不同)。
链表走到环入口需要走k=a+nb,那么我们再让slow走a步就可以了,但是我们不知道a是多少。这里再使用双指针,此指针从头指针和slow一起走a+mb步,那么两个指针相遇位置一定是在入口节点位置。
1 class Solution { 2 public int findDuplicate(int[] nums) { 3 if(nums == null || nums.length <= 1){ 4 return -1; 5 } 6 int slow = nums[0]; 7 int fast = nums[nums[0]]; 8 while(slow != fast){ 9 slow = nums[slow]; 10 fast = nums[nums[fast]]; 11 } 12 int head = 0; 13 while(head != slow){ 14 slow = nums[slow]; 15 head = nums[head]; 16 } 17 return head; 18 } 19 }
法二:二分法:mid是中间索引值,如果mid左边对应比mid小的数的个数大于mid(count(num<mid)),则左移,否则右移。
1 class Solution { 2 public int findDuplicate(int[] nums) { 3 int left = 0, right = nums.length-1; 4 while(left < right){ 5 int mid = left + (right-left)/2, count = 0; 6 for(int num:nums){ 7 if(num <= mid){ 8 count ++; 9 } 10 } 11 if(count > mid){ 12 right = mid; 13 }else{ 14 left = mid+1; 15 } 16 } 17 return left; 18 } 19 }
297.二叉树的序列化与反序列化
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
示例: 你可以将以下二叉树:
1
/
2 3
/
4 5
序列化为 "[1,2,3,null,null,4,5]"
分析:按照前序遍历存入字符串,如果遇到空节点则拼接",null"。
1 /** 2 * Definition for a binary tree node. 3 * public class TreeNode { 4 * int val; 5 * TreeNode left; 6 * TreeNode right; 7 * TreeNode(int x) { val = x; } 8 * } 9 */ 10 import java.util.*; 11 public class Codec { 12 13 // Encodes a tree to a single string. 14 //StringBuilder快 15 public String serialize(TreeNode root) { 16 StringBuilder res = serialize(root, new StringBuilder()); 17 return res.toString(); 18 } 19 public StringBuilder serialize(TreeNode root, StringBuilder res){ 20 if(root == null){ 21 res.append("null,"); 22 return res; 23 } 24 res.append(root.val); 25 res.append(","); 26 res = serialize(root.left, res); 27 res = serialize(root.right,res); 28 return res; 29 } 30 // Decodes your encoded data to tree. 31 public TreeNode deserialize(String data) { 32 Queue<String> queue = new LinkedList(); 33 Collections.addAll(queue, data.split(",")); 34 return deserialize(queue); 35 } 36 public TreeNode deserialize(Queue<String> queue){ 37 String str = queue.poll(); 38 if(str.equals("null")){ 39 return null; 40 } 41 TreeNode root = new TreeNode(Integer.parseInt(str)); 42 root.left = deserialize(queue); 43 root.right = deserialize(queue); 44 return root; 45 } 46 } 47 48 // Your Codec object will be instantiated and called as such: 49 // Codec codec = new Codec(); 50 // codec.deserialize(codec.serialize(root));
300.最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:输入: [10,9,2,5,3,7,101,18], 输出: 4 ,解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。 你算法的时间复杂度应该为 O(n2) 。
分析:动态规划,dp[]表示迄今为止最长上升子序列长度。nums[j]是从i往左遍历小于nums[i]的数(可能不止一个),则dp[i] = Max{dp[j]}+1.时间复杂度O(N^2)。 复杂度问题出现在找出dp[j]的过程中O(N),如果是用二分法可减少复杂度。
二分+DP:建立一个tails数组(一定是单调递增的)和整数res记录最长上升子序列。
很具小巧思。新建数组 cell
,用于保存最长上升子序列。对原序列进行遍历,将每位元素二分插入 cell
中。如果 cell
中元素都比它小,将它插到最后,否则,用它覆盖掉比它大的元素中最小的那个。总之,思想就是让 cell
中存储比较小的元素。这样,cell
未必是真实的最长上升子序列,但长度是对的。
1 class Solution { 2 public int lengthOfLIS(int[] nums) { 3 int[] temp = new int[nums.length]; 4 int res = 0; 5 for(int num:nums){ 6 int i=0,j=res; 7 while(i<j){ 8 int mid = (i+j)/2; 9 if(num <= temp[mid]){ 10 j = mid; 11 }else{ 12 i = mid+1; 13 } 14 } 15 temp[i] = num; 16 if(res==j) res++; 17 } 18 return res; 19 } 20 }
301.删除无效的括号
删除最小数量的无效括号,使得输入的字符串有效,返回所有可能的结果。说明: 输入可能包含了除 ( 和 ) 以外的字符。
示例 1:输入: "()())()" 输出: ["()()()", "(())()"]
示例 2:输入: "(a)())()" 输出: ["(a)()()", "(a())()"]
示例 3:输入: ")(" 输出: [""]
分析:
①首先确定可以删除的左括号和右括号数目,这样在递归是可以判断是否要删除。如何确定数目呢,因为本体要删除最小数量,那么每当有一对()出现时,就会减少要删除的数量。当左括号出现时,left++。当右括号出现时,判断是否有多的左括号与它凑成一对(即判断left是否大于0),如果有,那么left--,要删除的左括号数目减一,如果没有,那么要删除的右括号数目加一,即right++;
②其次使用递归回溯(一次递归前添加字符,结束后删除字符)
那么什么时候要删除括号呢?当left>0&&char=='('时删除左括号,当right>0&&char==')'时删除右括号,其他时候只要拼接字符串即可。
那么什么时候该把拼接好的字符串添加到结果集合中呢?即当遍历完字符串并且待删除的括号数目都等于0,既可以将当前拼接的字符串添加到集合中。
③设置一个函数isValia判断当前子字符串是否是一个有效字符串。
1 class Solution { 2 public List<String> removeInvalidParentheses(String s) { 3 List<String> res = new ArrayList(); 4 //1.首先确定待删除左括号和右括号数目 5 int left=0,right=0; 6 for(int i=0;i<s.length();i++){ 7 char ch = s.charAt(i); 8 if(ch=='('){ 9 left++; 10 }else if(ch==')'){ 11 if(left>0){ 12 left--; 13 }else{//没有待删左括号并且多了一个右括号, 14 right++; 15 } 16 } 17 } 18 dfs(s,0,left,right,new StringBuilder(s),res); 19 return res; 20 } 21 public void dfs(String s, int start, int left, int right, StringBuilder sb, List<String> res){ 22 if(left==0&&right==0&&isValid(sb.toString())){ 23 res.add(sb.toString()); 24 return; 25 } 26 for(int i=start;i<s.length();i++){ 27 if(i!=start&&s.charAt(i)==s.charAt(i-1)) continue;//如果有连续的,只要对第一个进行操作就行 28 if(s.charAt(i)=='(' || s.charAt(i)==')'){ 29 sb = new StringBuilder(s); 30 sb.deleteCharAt(i); 31 if(left>0&&s.charAt(i)=='('){ 32 dfs(sb.toString(),i,left-1,right,sb,res); 33 } 34 if(right>0&&s.charAt(i)==')'){ 35 dfs(sb.toString(),i,left,right-1,sb,res); 36 } 37 } 38 } 39 } 40 //判断字符是否有效 41 public boolean isValid(String s){ 42 int count=0; 43 char[] chs = s.toCharArray(); 44 for(char ch:chs){ 45 if(ch=='(') count++; 46 if(ch==')') count--; 47 if(count<0) return false; 48 } 49 return count==0; 50 } 51 }
309.最佳买卖股票时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:输入: [1,2,3,0,2] 输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
分析:动态规划,建立一个二维数组dp[i][j],j只有两种取值,即0表示第i天没有持有股票,1表示持有股票。
第i天没有持有股票:①今天没有买入,休息②昨天买入今天卖出
d[i][0] = Max{ dp[i-1][0], dp[i-1][1] + prices[i]}
第i天持有股票:①昨天买入,今天休息②前天卖出,今天买入
d[i][1] = Max{ dp[i-1][1], dp[i-2][0] - prices[i]}
最终结果求dp[n][0],最后一天没有持有的状况。
1 class Solution { 2 public int maxProfit(int[] prices) { 3 int len = prices.length; 4 if(len<=1) return 0; 5 int[][] dp = new int[len][2]; 6 dp[0][0] = 0; 7 dp[0][1] = -prices[0]; 8 dp[1][0] = Math.max(dp[0][0], dp[0][1] +prices[1]); 9 dp[1][1] = Math.max(dp[0][1], dp[0][0]- prices[1]); 10 for(int i=2;i<len;i++){ 11 dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); 12 dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i]); 13 } 14 return dp[len-1][0]; 15 } 16 }
312.戳气球
有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。现在要求你戳破所有的气球。每当你戳破一个气球 i 时,你可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。求所能获得硬币的最大数量。
说明:你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
示例:输入: [3,1,5,8] 输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
分析:数组、状态=》动态规划。每次从前往后戳气球会影响左右数组元素,状态太多了。如果从最后一个被戳的开始分析呢?假设最后一个被戳的是5,那么之前的状态时[3 1] 和[8 ],5将数组分成两个部分,可得状态转移公式:
dp[i][j] = Max{dp[i][j], dp[i][k-1]+dp[k+1][j]+nums[i]*nums[k]*nums[j]} i<=k<=j, dp[i][j]表示区间i~j能够获得的最大硬币数,k表示这个区间内最后戳破的气球。
例如3158最优情况是:先排除315,8是最后一个1*8*1,然后放出3,倒数第二个戳3,倒数第三个戳5,第一个戳1。
①循环定义长度len from 1 to n=nums.length
②循环定义遍历的范围 1 to n-len+1,此时j的位置j=i+len-1
③在当前情况下,选取位置k气球,当作是最后一次点爆,i<=k<=j,此时硬币数dp[i][k-1] + dp[k+1][j] + nums[i]*nums[k]*nums[j],选取最大值。返回dp[1][n]
1 class Solution { 2 public int maxCoins(int[] nums) { 3 int n = nums.length; 4 //为了方便计算,省去边界情况,在原数组左右加一个1 5 int[] num = new int[n+2]; 6 Arrays.fill(num,1); 7 for(int i=1;i<=n;i++){ 8 num[i] = nums[i-1]; 9 } 10 //初始化dp数组 11 int[][] dp = new int[n+2][n+2]; 12 for(int len=1;len<=n;len++){ 13 for(int i=1;i<=n-len+1;i++){ 14 int j = i+len-1;//维持长度为len的小区间 15 for(int k=i;k<=j;k++){ 16 dp[i][j] = Math.max(dp[i][j], dp[i][k-1]+dp[k+1][j]+num[i-1]*num[k]*num[j+1]); 17 } 18 } 19 } 20 return dp[1][n]; 21 } 22 }
322.零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:输入: coins = [1, 2, 5], amount = 11,输出: 3, 解释: 11 = 5 + 5 + 1
示例 2:输入: coins = [2], amount = 3 ,输出: -1
说明:你可以认为每种硬币的数量是无限的。
分析:动态规划,dp[i]表示金额i所需要换取的次数。可以dp[i]递推关系只和coins中的元素相关,例如coins = [1, 2, 5],那么dp[i]只归根结底与dp[1],dp[2],dp[5]相关。
1 class Solution { 2 public int coinChange(int[] coins, int amount) { 3 if(amount == 0) return 0; 4 int[] dp = new int[amount+1]; 5 for(int i=1;i<=amount;i++){ 6 dp[i] = Integer.MAX_VALUE;//初始设为最大值 7 for(int j=0;j<coins.length;j++){ 8 int remain = i-coins[j];//当前剩下的钱 9 if(remain>=0&&dp[remain]!=Integer.MAX_VALUE){//如果比当前coins[j]大,并且可换 10 dp[i] = Math.min(dp[i], dp[remain]+1); 11 } 12 } 13 } 14 return dp[amount]==Integer.MAX_VALUE?-1:dp[amount]; 15 } 16 }