赞
踩
算法题
动态规划和贪心
买卖股票的最佳时机
最复杂的情况是限制交易k次,状态转移方程无法化简,其他情况均可化简为二维或一维动态规划。
//一次买卖
public class Solution {
public int buy(int[] price) {
int n = price.length;
int ans = 0;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for(int i = 1;i < n; i++){
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + price[i]);
dp[i][1] = Math.max(dp[i - 1][1],- price[i]);
}
return dp[i][0];
}
}
优化:只与相邻的一个状态有关,那么可以只记录前一个状态,不使用数组,空间降到O(1)
public class Solution {
public int buy(int[] price) {
int n = price.length;
int ans = 0;
int dp_i_0 = 0;
int dp_i_1 = Integer.MIN_VALUE;
for(int i = 1;i < n; i++){
dp_i_0 = Math.max(dp_i_0,dp_i_1 + price[i]);
dp_i_1 = Math.max(dp_i_1,- price[i]);
}
return dp_i_0;
}
}
//不限制次数:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
//有一天冷却期
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。
//有服务费
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
//2次买卖
原始的动态转移方程,没有可化简的地方
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
int max_k = 2;
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++) {
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) {
/* 处理 base case */
dp[i][k][0] = 0;
dp[i][k][1] = -prices[i];
continue;
}
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
}
// 穷举了 n × max_k × 2 个状态,正确。
return dp[n - 1][max_k][0];
剑指原题,剪绳子
给定一根长度为n的绳子,请把绳子剪成m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],…,k[m]。请问k[0]* k[1] * … *k[m]可能的最大乘积是多少?例如长度为8的绳子,可以剪成2,3,3段,最大乘积为18。
1.动态规划:每次剪一刀之后,剩余部分还可以继续剪,那么就是计算出所有可能的情况,取最大值。自底向上改善递归从上而下重复计算的问题。
时间复杂度O(N^2),空间复杂度O(N)
作者:星__尘
链接:https://www.nowcoder.com/discuss/428158?source_id=profile_create&channel=666
来源:牛客网
public class Solution {
public int cutRope(int target) {
if(target == 2)
return 1;
if(target == 3)
return 2;
if(target == 4)
return 4;
int[] dp = new int[target + 1];
/*
下面3行是n>=4的情况,跟n<=3不同,4可以分很多段,比如分成1、3,
这里的3可以不需要再分了,因为3分段最大才2,不分就是3。记录最大的。
*/
// 1,2,3不可拆分 越拆分越小
dp[1]=1;
dp[2]=2;
dp[3]=4; //3-->4
//用来记录最大值
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:贪心算法:可以证明,每段长度为3是最大乘积。
时间复杂度O(logN),因为乘方运算的时间复杂度是logN。当数据特别大时,只能使用贪心算法,因为动态规划枚举每个状态需要大量的空间。
public class Solution {
public int cutRope(int target) {
if(target==2){
return 1;
}else if(target==3){
return 2;
}
//pow(n,m)是求n的m次方
if(target%3==0){
return (int)Math.pow(3,target/3);
}else if(target%3==1){
return 4*(int)Math.pow(3,target/3-1);
}else {
return 2*(int)Math.pow(3,target/3);
}
}
}
接雨水(leetcode 42)
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
思路:
对于每一列来说,他能存的雨水量是他左边最高墙和右边最高墙中较低的那堵墙的高度减去自身墙的高度。所以可以用数组记录每列左右最高墙的高度,然后计算每一列可以存的雨水量。
动态规划:时间复杂度O(N),空间复杂度O(N)。
class Solution {
public int trap(int[] height) {
int len = height.length;
if(len == 0 || len == 1) return 0;
int[] left = new int[len];
int[] right = new int[len];
left[0] = height[0];
right[len - 2] = height[len - 1];
for(int i = 1;i < len - 1;i++){
left[i] = Math.max(height[i - 1],left[i - 1]);
}
for(int i = len - 2;i >= 0;i--){
right[i] = Math.max(height[i + 1],right[i + 1]);
}
int sum = 0;
for(int i = 1; i < len - 1;i++){
int min = Math.min(right[i],left[i]);
if(min > height[i])
sum = sum + (min - height[i]);
}
return sum;
}
}
柠檬水找零(LeetCode860)
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
思路:尽可能有大面值找零,也就是能用10元找零就不用2个5元。是贪心算法思想的体现。
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0,ten = 0;
for(int value : bills){
if(value == 5){
five++;
}else if(value == 10){
if(five == 0) return false;
five--;
ten++;
}else{
if(ten >= 1 && five >= 1){
ten--;
five--;
}else if(five >= 3){
five = five - 3;
}else{
return false;
}
}
}
return true;
}
}
数组
双指针遍历:解决有序数组的问题
排序数组,平方后,数组当中有多少不同的数字(相同算一个)
如果不是排序数组,可以使用hashset来保存数字的平方,重复就存不进去,那么最后就可以直接返回set的大小size即可。时间空间复杂度都是O(n)。
双指针遍历:这里是排序数组,既然有重复,肯定是有负数,0,1这些数字。平方后两头大,中间小,可以用首尾指针共同向中间扫描,扫描时去掉重复元素,同时用一个sum来记录有多少个不同数字。
时间复杂度O(N),空间复杂度O(1)。
public class Solution {
public int diffSquareNum(int nums[]) {
int n = nums.length;
if(n == 0 || nums == null){
return 0;
}
int sum = 0;
int left = 0;
int right = n - 1;
while(left <= right){
if(nums[left] + nums[right] == 0){
sum++;
int temp = nums[left];
//这里开始去掉后面重复的数字
while(left <= right && nums[left] == temp)
left++;
while(left <= right && nums[right] == -temp)
right--;
}
else if(nums[left] + nums[right] < 0){
sum++;
int temp = nums[left];
while(left <= right && nums[left] == temp)
left++;
}
else {
sum++;
int temp = nums[right];
while(left <= right && nums[right] == temp)
right--;
}
}
return sum;
}
}
一个数据先递增再递减,找出数组不重复的个数,比如 [1, 3, 9, 1],结果为3,不能使用额外空间,复杂度o(n)
作者:星__尘
链接:https://www.nowcoder.com/discuss/428158?source_id=profile_create&channel=666
来源:牛客网
public class Solution {
public int diffnum(int[] nums){
int n = nums.length;
if(n == 0 || nums == null){
return 0;
}
int left = 0;
int right = n - 1;
int sum = 0;
while(left <= right){
if(nums[left] == nums[right]){
sum++;
int temp = nums[left];
while(left <= right && nums[right] == temp)
right--;
while(left <= right && nums[left] == temp)
left++;
}
else if(nums[left] < nums[right]){
sum++;
int temp = nums[left];
while(left <= right && nums[left] == temp)
left++;
}
else{
sum++;
int temp = nums[right];
while(left <= right && nums[right] == temp)
right--;
}
}
return sum;
}
}
递增数组,找出和为k的数对
双指针遍历:用头尾两个指针,分别开始遍历,两个数字和大于k时,右指针向前移动,小于k时左指针向后移动
public class Solution{
public ArrayList findPair(int[] nums,int k){
int n = nums.length;
int i = 0;
int j = n - 1;
ArrayList list = new ArrayList<>();
while(i < j){
if(nums[i] + nums[j] < k){
i++;
}else if(num[i] + nums[j] > k){
j--;
}else{
list.add(nums[i]);
list.add(nums[j]);
i++;
j--;
}
}
return list;
}
}
给出一个数组nums,一个值k,找出数组中的两个下标 i,j 使得 nums[i] + nums[j] = k.
这个题目跟上面一题的区别就是不是有序数组,那么解题思路就可以是排序+双指针遍历,时间复杂度就因为排序升为O(NlogN)。
对于这个无序数组另一种解决办法是使用HashMap,数字作为键,下标作为值存入hashmap,然后遍历map查找符合条件的数对,map可以实现O(1)的查找,所以时间复杂度和空间复杂度都是O(N)。(这里需要注意,如果数组中有重复元素,那么这个方法就不能用,因为重复元素存入map时,后面的值(下标)会覆盖掉前面的值。
//这种实现方式无法去掉重复的pair,如结果中会同时有(3,6)和(6,3)。
public class Solution{
public ArrayList> findPair(int[] nums, int k) {
ArrayList> pairs = new ArrayList<>();
HashMap map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], i);
}
for (Map.Entry set : map.entrySet()) {
int target = k - set.getKey();
//第二个判断条件是为了去除(2,2)这种情况,即同一数字即做加数又做被加数的情况
if (map.containsKey(target) && !map.get(target).equals(set.getValue())) {
pairs.add(new Pair<>(set.getValue(), map.get(target)));
}
}
return pairs;
}
}
滑动窗口:解决连续序列问题
和为s的连续正整数序列(剑指 offer57 -II)
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]
滑动窗口:窗口左右两端都只能向右移动,当和小于sum时,j++,和大于sum时,i++,和等于sum就记录下窗口中i —j 中的序列,然后窗口继续后移,查找下一个满足条件的序列。
class Solution {
public int[][] findContinuousSequence(int target) {
int i = 1;
int j = 1;
int sum = 0;
List list = new ArrayList<>();
//序列是由小到大排列,所以如果i>target/2,那么i+i+1肯定大于target
while(i <= target/2){
if(sum < target){
sum += j;
j++;
}else if(sum > target){
sum -= i;
i++;
}else{
int[] res = new int[j - i];
for(int z = i; z < j;z++){
res[z - i] = z;
}
list.add(res);
// 左边界向右移动
sum -= i;
i++;
}
}
return list.toArray(new int[list.size()][]);
}
}
某一个大文件被拆成了N个小文件,每个小文件编号从0至N-1,相应大小分别记为S(i)。给定磁盘空间为C,试实现一个函数从N个文件中连续选出若干个文件拷贝到磁盘中,使得磁盘剩余空间最小。
滑动窗口:每次记录窗口内的总和,和小于C,记录剩余空间,再窗口右端右移,和大于C,就窗口左端右移,小于C情况下比较剩余空间取最小值。
public class Solution {
public int[] findMin(int[] s,int c){
int i = 0;
int j = 0;
int minValue = Integer.MAX_VALUE;
int sum = 0;
int left = 0;
int right = 0;
while(j <= s.length){
if(sum <= c){
j++;
sum += s[j];
minValue = Math.min(minValue,c - sum);
if(minValue == c - sum){
left = i;
right = j;
}
}else{
//i++;
//sum -= s[i];
//Young
sum -= s[i];
i++;
}
}
int[] nums = new int[right - left];
for(int k = left;k < right;k++){
nums[k - left] = s[k];
}
return nums;
}
}
给定m个不重复的字符 [a, b, c, d],以及一个长度为n的字符串tbcacbdata,问能否在这个字符串中找到一个长度为m的连续子串,使得这个子串刚好由上面m个字符组成,顺序无所谓,返回任意满足条件的一个子串的起始位置,未找到返回-1。比如上面这个例子,acbd,3.
分析:
本题的子串需要满足长度为m,字符不重复,可以使用长为m的滑动窗口遍历字符串,窗口内每个字符都要出现一次,如果符合条件,就返回窗口起始位置。
如果这里不允许使用substring,indexOf函数,可以将字符串中每个字符出现的次数存入长度为26的数组中进行比较每次数组中对应位置数量是否相等,具体可参照上面LeetCode 567题。
public class Solution {
public int checkInclusion(char[] ch,String s) {
if(ch.length > s.length()){
return -1;
}
for(int i = 0; i < s.length() - ch.length; i++){
//每次匹配长度为m的子串
if(matchs(ch,s.substring(i,i+ch.length)))
return i;
}
return -1;
}
private static boolean matchs(char[] ch,String s){
for(int i = 0; i < s.length();i++){
//返回-1说明字符串中不包含这个字符
if(s.indexOf(ch[i]) == -1)
return false;
}
return true;
}
}
哈希表辅助解决数组问题
求数组的最长连续递增数列,如:4, 200, 3, 1, 100, 2。结果是1 2 3 4,也就是说顺序可以打乱。(leetcode 128)
思路一:排序,然后再遍历一遍找最长连续递增数列,时间复杂度O(NlogN)。
思路二:使用HashMap或者HashSet,将数组元素存入set中,然后遍历set,先找到每个序列的开头元素,即num没有num-1的情况,这时从num开始循环找num+1,记录次数,通过比较找出最大长度。时间和空间复杂度都是O(n)
//这里用set,是因为set可以自动去重,用map也不影响结果
class Solution {
public int longestConsecutive(int[] nums) {
if (nums.length == 0) return 0;
HashSet set = new HashSet<>();
for(int value : nums)
set.add(value);
int longestLength = 0;
for (int num: set) {
if (!set.contains(num - 1)){
int currentNum = num;
int currentLength = 1;
while (set.contains(currentNum + 1)){
currentNum += 1;
currentLength += 1;
}
longestLength = Math.max(longestLength,currentLength);
}
}
return longestLength;
}
}
一个无序数组,从小到大找到第一个缺的数,比如[8 2 4 3 6 9 7 11 12],第一个缺的就是5 (2)
1.排序,时间复杂度O(NlogN)
2.用数组作为哈希表,将数字i放入数组中的i索引处,然后找中间没有存入数字的位置。时间和空间复杂度都是O(N)
public class
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。