速通数组:套路总结+实战练习

数组归纳

1. 滑动窗口
  • 关键字:"子串"、"连续"、"子xxx(如子数组)"
  • 例子:209. 长度最小的连续子数组76.最小覆盖子串
  • 套路:维护一个左指针l、右指针r,通过不断扩张和收缩来满足题目约束
    • 左指针扩张:右指针 r++ 往右走,把元素纳入窗口
    • 右指针收缩:如果窗口内条件不满足(如和太大/字符不满足),移动左指针l++缩小窗口


2. 双指针
  • 例子:283. 移动零11. 盛最多水的容器
  • 套路:
    • 快慢指针:右指针扫描,判断条件并交换或记录
    • 双指针夹逼:左右两端向中间收缩,更新或记录条件


3. 前缀和
  • 定义:前缀和数组 s,其中
    • s[0] = 0(类似哨兵节点)
    • s[i] = nums[0] + nums[1] + ... + nums[i-1]
  • 用途:任意区间[l, r]的和 = s[r+1] - s[l]
    • 相当于把"求区间和"的问题转化为"两个前缀和相减"
  • 例子:53. 最大子数组和238. 除自身以外数组的乘积


4. 区间问题
  • 概念: 是一类需要处理区间区间关系的题目,本质上是处理多个[l、r]的关系
  • 套路:一般用排序做预处理,因为区间没有顺序时不好比较,排好序后才能高效遍历、合并
  • 例子:56. 合并区间


5. 数组操作
  • 定义:将数组算法题中一些不具通用性的思想归为该类,主要记住方法即可
    • 189. 轮转数组:学会数组拷贝函数
    • 41. 缺失的第一个正数:原地哈希


209.长度最小的连续子数组

题目:

解答:滑动窗口

思路:滑动窗口,满足时r扩张,不满足时l收缩

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int sum = 0;
        int result = Integer.MAX_VALUE; // 记录当前最小长度

        // # 模板:右指针扩张
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];

            // # 模板:左指针收缩,如果总和大于target(满足条件)
            while (sum >= target) {
                result = Math.min(result, right - left + 1);
                sum -= nums[left++];
            }
        }
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}


76.最小覆盖子串

题目:

学习:int[]代替Set、substring()左闭右开
  1. 可以用数组代替哈希表的功能,如题有char(含大小写)的情况,直接用int[128]配合isCover()判断是否覆盖
  2. String.substring()是左闭右开

解答:滑动窗口

思路:滑动窗口,用cntScntT记录字母出现次数,如果覆盖其子串更短,则记录

class Solution {
    public String minWindow(String S, String t) {
        int[] cntS = new int[128]; // s 子串字母的出现次数
        int[] cntT = new int[128]; // t 中字母的出现次数
        for (char c : t.toCharArray()) {
            cntT[c]++;
        }

        char[] s = S.toCharArray();
        int m = s.length;
        int ansLeft = -1; // 记录要返回的l(初始化为不可能值,用于判断是否返回失败)
        int ansRight = m; // 记录要返回的r

        int left = 0;
        for (int right = 0; right < m; right++) { // #模板:扩展右指针
            cntS[s[right]]++;

            // 判断是否覆盖
            while (isCovered(cntS, cntT)) {
                // 如果找到更短的子串,记录此时的左右端点
                if (right - left < ansRight - ansLeft) {
                    ansLeft = left;
                    ansRight = right;
                }
                
                // # 模板:收缩左指针(同时收缩记录)
                cntS[s[left]]--;
                left++;
            }
        }

        return ansLeft < 0 ? "" : S.substring(ansLeft, ansRight + 1);
    }

    private boolean isCovered(int[] cntS, int[] cntT) {
        for (int i = 'A'; i <= 'Z'; i++) {
            if (cntS[i] < cntT[i]) {
                return false;
            }
        }
        for (int i = 'a'; i <= 'z'; i++) {
            if (cntS[i] < cntT[i]) {
                return false;
            }
        }
        return true;
    }
}


283.移动零:

题目:

解答:双指针-快慢指针

思路(记方法):双指针,右指针找到最近的非零元素,与左边界交换;左指针维护一个左边界(所有零在这个边界上或它右边),每次交换一个非零元素到左边界,左边界右移

class Solution {
    public void moveZeroes(int[] nums) {
        int n = nums.length, left = 0, right = 0;

        // # 模板:右指针扫描
        while (right < n) {
            if (nums[right] != 0) {
                swap(nums, left, right);
                left++;
            }
            right++;
        }
    }

    public void swap(int[] nums, int left, int right) {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
    }
}


11.盛最多水的容器

题目:

解答:双指针-双指针夹逼

思路:两指针在左右边界,每次较短一根向中间移动,并更新最大容量

  1. 为什么两个指针初始化在两边:因为这是最左和最右,而最优情况一定是这两指针之间的两个柱子
  2. 为什么每次移动较短的那根:移动哪一根都是减少1宽度,那么移动短的一根得到的结果显然更大
public class Solution {
    public int maxArea(int[] height) {
        int l = 0, r = height.length - 1;
        int ans = 0;
        while (l < r) {
            int area = Math.min(height[l], height[r]) * (r - l);
            ans = Math.max(ans, area);
            if (height[l] <= height[r]) {
                ++l;
            }
            else {
                --r;
            }
        }
        return ans;
    }
}


53.最大子数组和

题目:

解答:前缀和

思路:计算最大子数组和,只需要计算该数组的终点前缀和-起点前缀和,而起点前缀和越小越好。所以遍历数组,维护一个当前最小前缀和,遍历到的元素求前缀和并当作终点前缀和进行计算

class Solution {
    public int maxSubArray(int[] nums) {
        int ans = Integer.MIN_VALUE;
        int minPreSum = 0;
        int preSum = 0;
        for (int x : nums) {
            preSum += x; // 当前的前缀和:上一位前缀和 + x
            ans = Math.max(ans, preSum - minPreSum); // 当前子数组是否更大
            minPreSum = Math.min(minPreSum, preSum); // 维护最小前缀和
        }
        return ans;
    }
}


238.除自身以外数组的乘积

题目:

解答:"前缀和"思想

思路(记方法):answer = 左边的总乘积 * 右边的总乘积,那么维护前缀和(前缀积)数组和后缀和数组,相乘即得到答案

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] anwser = new int[n];
        
        int[] pre = new int[n]; // 前缀积
        pre[0] = 1;
        int[] suf = new int[n]; // 后缀积
        suf[n - 1] = 1;

        for (int i = 1; i < n; i++) {
            pre[i] = pre[i - 1] * nums[i - 1];
        }

        for(int i = n - 2; i >= 0; i--) {
            suf[i] = suf[i + 1] * nums[i + 1];
        }

        for (int i = 0; i < n; i++) {
            anwser[i] = pre[i] * suf[i];
        }

        return anwser;
    }
}


56.合并区间

题目:

解答:区间问题

思路:

  1. 先将intervals按左端点排序(方便后续遍历与合并)
  2. 先将第一个区间作为待合并区间,遍历数组,比较当前区间的左端点是否在合并区间之间,判断是否合并
    1. 合并:更新待合并区间的右端点
    2. 不合并:不符合条件,收集结果并将下一区间作为待合并区间

如下,把ans最后一个元素当作待合并区间

class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, (p, q) -> p[0] - q[0]); // 左端点从小到大排序
        List<int[]> ans = new ArrayList<>();
        
        for (int[] p : intervals) {
            int m = ans.size();
            
            // 如果可以合并,则判断并更新待合并区间的右端点
            if (m > 0 && p[0] <= ans.get(m - 1)[1]) {
                ans.get(m - 1)[1] = Math.max(ans.get(m - 1)[1], p[1]); 
            } else { // 不相交,无法合并,则收集结果
                ans.add(p); 
            }
        }
        return ans.toArray(new int[ans.size()][]);
    }
}


189.轮转数组

题目:

学习:数组拷贝函数System.arraycopy()

System.arraycopy(nums, l2, ans, 0, n - l2);

  • nums:源数组
  • l2:源数组的起始复制位置(即从nums[l2]开始复制)
  • ans:目标数组
  • 0:目标数组的起始粘贴位置(即粘贴到ans[0]开始)
  • n - l2:要复制的元素个数

解答:数组操作-加k取余实现轮转

思路(记方法):只需要抓住规律即可,配合取余进行赋值,用数组拷贝拷贝回原数组

class Solution {
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        int[] newArr = new int[n];
        
        for (int i = 0; i < n; ++i) {
            // +k表示轮转次数,取余表示超界时返回起点再轮转
            newArr[(i + k) % n] = nums[i];
        }
        
        System.arraycopy(newArr, 0, nums, 0, n);
    }
}


41.缺失的第一个正数

题目:

解答:数组操作-原地哈希

技巧:如果没有限制空间,可以放进哈希表并遍历得到最小正数;但是限制了空间,可以采用"原地哈希"来节省空间

思考:数组长度为n,则"最小正数"最小值为n+1,即整个数组为1 -> n

思路:遍历nums数组,如果当前nums[x]符合1 ≤ x ≤ n,而且nums[x]对应位置nums[nums[x] - 1]还没有放过,则交换xnums[x] - 1下标元素;但是,交换之后,x位置的新值也可能需要交换去别的地方,所以需要while循环,直到x位置的值不符合交换条件,才算解决了x位置,才能继续往后递归

class Solution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;

       // 1.将每个数 x 放到下标 x-1 的位置
       for (int i = 0; i < n; i++) {
            // 只放范围在(1 ≤ x ≤ n)的x,超范围的不用管
            // 已经放过的不用重复放,避免循环
            while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                int tmp = nums[nums[i] - 1];
                nums[nums[i] - 1] = nums[i];
                nums[i] = tmp;
            }
        }

        // 2.如果nums[i] != i+1,说明 i+1 就是缺失的最小正整数
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }

        // 3.如果都在正确位置,返回 n+1
        return n + 1;
    }
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值