无重复字符的最长子串
这一题是典型的滑动窗口算法,滑动窗口算法目前掌握不是很熟练,需要多刷
class Solution {
public int lengthOfLongestSubstring(String s) {
// 定义窗口初始值,窗口的计算是一个开闭的过程,滑动窗口算法需要不断的改变窗口大小来确定最终返回值
int left = 0,right = 0;
int len = s.length();
int res = 0;
// 计算重复元素的哈希表,窗口内元素和元素的出现次数相对应
Map<Character,Integer> map = new HashMap<>();
//窗口开始扩大,阈值是整个字符串的长度
while(right < len){
// 将对应的字符摘出来,放到窗口对应的哈希表里去,出现几次加几
char c = s.charAt(right);
map.put(c,map.getOrDefault(c,0) + 1);
right ++;
// 当有重复元素出现的时候,窗口开始收缩
while(map.get(c) > 1){
// 移除重复元素
char d = s.charAt(left);
left ++;
map.put(d,map.get(d) - 1);
}
// 计算窗口值
res = Math.max(res,right - left);
}
return res;
}
}
滑动窗口首先要确定两个重要的参数:窗口内计算数据的方式,收缩窗口的时机
在本题中,窗口内需要判断元素是否重复,用的是HashMap,如果要计算和或者其他什么,就要根据不同特点选择不同的数据结构;收缩时机也需要把握,窗口总是需要扩大,收缩得注意时候,比如本体,就是遇到重复的字符串才收缩
455. 分发饼干
这是个贪心的问题,贪心没有模板,主打意识流,这里就是尽量让大饼干满足大胃口的小孩,或者小饼干满足小胃口
这里是大饼干满足大胃口的解法,小饼干遍历方式是先遍历饼干,在给人
class Solution {
public int findContentChildren(int[] g, int[] s) {
// 先将两个数组变为有序数组
Arrays.sort(g);
Arrays.sort(s);
// 分别获取最大的饼干和最大的胃口「顺序反了」
int gl = g.length;
int bs = s.length - 1;
int res = 0;
// 从大到小遍历胃口
for(int i = gl - 1;i >= 0;i --){
// 对比胃口和饼干大小,bs >= 0是为了保证饼干数组不会越界
if(bs >= 0 && s[bs] >= g[i]){
bs --;
res ++;
}
}
return res;
}
}
摆动序列
也是贪心算法,摆动序列要求前后两个值的差值需要正负交替出现,其实就是计算有几个峰值,在计算峰值的过程中,我们有以下几点需要考虑:
- 峰值计算:记录前后坡度,prediff和curdiff,只要一正一负就记录峰值
- 平坡:计算三个节点,如果只要cur不等于0就是坡度有变化
- 首尾:只要首尾数字不同,就记录两次,开头一定是一个坡度的起点,默认设置res = 1
class Solution {
public int wiggleMaxLength(int[] nums) {
//如果数组的长度小于或者等于1,直接返回就行,如果等于2的话[0,0]坡度为1,不能返回2
if(nums.length <= 1){
return nums.length;
}
//记录前一个坡度和当前坡度「下一个坡度」
int prediff = 0,curdiff = 0;
int len = nums.length;
// 默认含头节点
int res = 1;
//限制一定在len - 1,否则i + 1会计算到len,数组越界
for(int i = 0;i < len - 1;i ++){
// 当前坡度是下一个节点和当前节点的差
curdiff = nums[i + 1] - nums[i];
// 前一个坡度和后一个坡度出现正负差的时候记录节点并变更坡度,=0的情况是为了考虑平坡
if((prediff >= 0 && curdiff < 0) ||( prediff <= 0 && curdiff > 0)){
res ++;
prediff = curdiff;
}
}
return res;
}
}
最大子数组和
这道题有两种选择——动态规划和贪心,我们两种算法都讲一下
贪心
贪心永远追求局部最优解,当连续和「当前元素和下一个元素之和」为负时,就立刻放弃让结果为负的那个元素,从该元素的下一个开始重新计算「因为要的是连续部分」
所以要先设定一个最终值sum,和一个临时值count,然后遇见负数连续和就重置,最终得到最大的结果
class Solution {
public int maxSubArray(int[] nums) {
// 临时值
int count = 0;
// 最终和
int sum = Integer.MIN_VALUE;
int len = nums.length;
// 遍历整个数组,时间复杂度为On
for(int i = 0;i < len;i ++){
// 临时值自增
count += nums[i];
// 取最大值
if(count > sum){
sum = count;
}
// 连续和小于等于0,重置结果
if(count <= 0){
count = 0;
}
}
return sum;
}
}
动态规划
动态规划五部曲:
- 确定dp数组及其下标的含义
- 确定递推公式
- dp数组初始化
- 确定遍历顺序
- 举例推导dp数组
针对这道题,先明确一下dp数组的含义:dp[i]表示下标到i的当前和是dp[i]
第二步确定递推公式:dp[i] = dp[i - 1] + nums[i]
第三步将数组初始化:dp[0] = nums[0]
第四步确定遍历顺序:后一步依靠前一步的结果,遍历顺序是从前向后,自顶而下
第五步开始推导
class Solution {
public int maxSubArray(int[] nums) {
if(nums.length <= 0){
return 0;
}
// 定义dp数组
int[] dp = new int[nums.length];
// 最终返回值设定为dp[0],增加了改变,如果只有0最大返回就是这个了
int res = nums[0];
// 初始化dp数组
dp[0] = nums[0];
// 前文说过定义dp推导式,
for(int i = 1;i < nums.length;i ++){
// 为啥要取最大值呢?当然是防止有负数情况出现,如果有负数了,直接赋值为不为负数的那一方
dp[i] = Math.max(dp[i - 1] + nums[i],nums[i]);
// res始终是最大
res = res > dp[i] ? res : dp[i];
}
return res;
}
}
买卖股票的最佳时机 II
这道题也能用贪心和动态规划来解,我们先不管动态规划了,先写贪心吧
这道题的逻辑简单来说,就是:涨了就卖;就是这么狂野,然后综合所有涨了就卖的,看啥时候卖赚的最多,得出最终结果
class Solution {
public int maxProfit(int[] prices) {
int res = 0;
for(int i = 1; i < prices.length;i ++){
// 这个逻辑很简单,res每次自增只要赚了就加上,亏了就加0,到最后得到的也是最大结果
res += Math.max(0,prices[i] - prices[i - 1]);
}
return res;
}
}
跳跃游戏
跳跃游戏就是看累积的长度是否可以覆盖整个数组的长度,遍历一次求最大长度就行
class Solution {
public boolean canJump(int[] nums) {
int jump = 0;
// 都是非负数,那指定能跳到
if(nums.length <= 1){
return true;
}
// 这是一个动态范围,由目前可以覆盖的范围决定
for(int i = 0;i <= jump;i ++){
// i + nums[i]求的是整体覆盖的结果
jump = Math.max(i + nums[i],jump);
// 能覆盖了
if(jump >= nums.length - 1){
return true;
}
}
return false;
}
}
跳跃游戏 II
上强度,求最少跳跃次数,又加一个参数,需要统计两次最大范围了,一次当前最大,一次下一步最大
class Solution {
public int jump(int[] nums) {
if(nums.length <= 1){
return 0;
}
// 当前覆盖范围
int curCover = 0;
// 下一步覆盖范围
int nxtCover = 0;
int res = 0;
for(int i = 0;i < nums.length;i ++){
// 求当前范围内最大的下一步覆盖范围
nxtCover = Math.max(nxtCover,i + nums[i]);
// 到了当前范围的临界点,必须走下一步
if(i == curCover){
// 只要走下一步,res必须++
res ++;
// 更新当前最大覆盖
curCover = nxtCover;
// 当前覆盖到全部了,直接跳出循环返回
if(curCover >= nums.length - 1) break;
}
}
return res;
}
}
二叉搜索树的最小绝对差
小小的刷一道二叉树,二叉树我们之前说过有两种解决方法,一种是遍历,一种是分解。
遍历
二叉搜索树最小值永远在左子树上,先中序遍历成一个有序数组,在对数组进行操作,但是这样有时间复杂度为2n,所以我们需要在第一次遍历树节点的时候计算出结果,同样,对数组进行双指针取值,对树也可以,我们需要先记录两个节点,当前节点和上一个节点,所以需要一步
pre = cur,然后再进行计算
class Solution {
// 维护的前一个节点
TreeNode pre;
// 最中结果,取最大值就设定最小,相反设定最大
int res = Integer.MAX_VALUE;
public int getMinimumDifference(TreeNode root) {
if(root == null){
return 0;
}
// 中序遍历
travsal(root);
return res;
}
public void travsal(TreeNode root){
if(root == null){
return ;
}
travsal(root.left);
// 值取差
if(pre != null){
res = Math.min(res,root.val - pre.val);
}
pre = root;
travsal(root.right);
}
}
分解
分解要利用到二叉搜索树的特性,二叉搜索树注明:左子树的节点永远小于根节点,右子树的节点永远大于根节点,所以,我们要用左子树最大的值和根节点进行差值,右子树最小的值和根节点进行取差值,再递归的将两这结果进行比较,最终得出结果
class Solution {
public int getMinimumDifference(TreeNode root) {
return helper(root,Integer.MIN_VALUE,Integer.MAX_VALUE);
}
// 设定取值为:节点 嘶设定的边界没有用到,就这样吧,能改进但是懒,可以改成树节点,对应子树的最大值和最小值
public int helper(TreeNode root,int mindiff,int maxdiff){
// 为啥要返回最大值?因为这样返回不影响比较结果,返回0那么res必定是0,干扰最终结果
if(root == null) {
return Integer.MAX_VALUE;
}
// 取最小用最大比较
int res = Integer.MAX_VALUE;
// 计算左子树最小差
if(root.left != null){
TreeNode leftMax = root.left;
while(leftMax.right != null){
leftMax = leftMax.right;
}
res =Math.min(res, root.val - leftMax.val);
}
// 右子树
if(root.right != null){
TreeNode rightMin = root.right;
while(rightMin.left != null){
rightMin = rightMin.left;
}
res = Math.min(res,rightMin.val - root.val);
}
// 向下递归
int leftDiff = helper(root.left,mindiff,maxdiff);
int rightDiff = helper(root.right,mindiff,maxdiff);
return Math.min(res,Math.min(leftDiff,rightDiff));
}
}
找出出现至少三次的最长特殊子字符串 I
昨天瞎忙,没写完,今天补起来
找出出现至少三次的最长特殊子字符串 II
今天的每日一题,被虐了,道心再次破碎,字符串死穴
结尾
还打算刷hot100、150的,先刷这些吧,把之前的进度赶上在接着刷