小小算法——Day1

82 阅读8分钟

无重复字符的最长子串

这一题是典型的滑动窗口算法,滑动窗口算法目前掌握不是很熟练,需要多刷

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;
    }
}

摆动序列

也是贪心算法,摆动序列要求前后两个值的差值需要正负交替出现,其实就是计算有几个峰值,在计算峰值的过程中,我们有以下几点需要考虑:

  1. 峰值计算:记录前后坡度,prediff和curdiff,只要一正一负就记录峰值
  2. 平坡:计算三个节点,如果只要cur不等于0就是坡度有变化
  3. 首尾:只要首尾数字不同,就记录两次,开头一定是一个坡度的起点,默认设置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;
    }
}

动态规划

动态规划五部曲:

  1. 确定dp数组及其下标的含义
  2. 确定递推公式
  3. dp数组初始化
  4. 确定遍历顺序
  5. 举例推导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的,先刷这些吧,把之前的进度赶上在接着刷