这里写目录标题
一 基础函数
1.1 切片操作
//将nums只保存第0到第slow位元素
nums = Arrays.copyOfRange(nums,0,slow+1)
例题1 赎金信
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例 1:
输入:ransomNote = “a”, magazine = “b”
输出:false
示例 2:
输入:ransomNote = “aa”, magazine = “ab”
输出:false
示例 3:
输入:ransomNote = “aa”, magazine = “aab”
输出:true
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) {
return false;
}
int[] cnt = new int[26];
for (char c : magazine.toCharArray()) {
cnt[c - 'a']++;
}
for (char c : ransomNote.toCharArray()) {
cnt[c - 'a']--;
if(cnt[c - 'a'] < 0) {
return false;
}
}
return true;
}
}
二 基础题型
2.1 快慢指针应用 (经典题目就是 原地修改数组)
2.1.1 删除有序数组中的重复项
给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k 。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
思路:
我们让慢指针 slow 走在后面,快指针 fast 走在前面探路,找到一个不重复的元素就告诉 slow 并让 slow 前进一步。这样当 fast 指针遍历完整个数组 nums 后,nums[0…slow] 就是不重复元素
代码:
class Solution {
public int removeDuplicates(int[] nums) {
int len = nums.length;
int slow = 0;
int fast = 1;
while(fast<len){
if(nums[fast] == nums[slow]){
fast++;
}else if(nums[fast] != nums[slow]){
slow++;
nums[slow] = nums[fast];
fast++;
}
}
nums = Arrays.copyOfRange(nums,0,slow+1);
return slow+1;
}
}
2.1.2 删除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
思路:
如果 fast 遇到需要去除的元素,则直接跳过,否则就告诉 slow 指针,并让 slow 前进一步。
代码
class Solution {
public int removeElement(int[] nums, int val) {
int fast = 0, slow = 0;
while (fast < nums.length) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
2.1.3 删除有序数组中的重复项 II
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。
思路以及代码:
本题解法依然使用快慢指针技巧,在之前的解法中添加一个 count 变量记录每个数字重复出现的次数
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length == 0) {
return 0;
}
// 快慢指针,维护 nums[0..slow] 为结果子数组
int slow = 0, fast = 0;
// 记录一个元素重复的次数
int count = 0;
while (fast < nums.length) {
if (nums[fast] != nums[slow]) {
// 此时,对于 nums[0..slow] 来说,nums[fast] 是一个新的元素,加进来
slow++;
nums[slow] = nums[fast];
} else if (slow < fast && count < 2) {
// 此时,对于 nums[0..slow] 来说,nums[fast] 重复次数小于 2,也加进来
slow++;
nums[slow] = nums[fast];
}
fast++;
count++;
if (fast < nums.length && nums[fast] != nums[fast - 1]) {
// fast 遇到新的不同的元素时,重置 count
count = 0;
}
}
// 数组长度为索引 + 1
return slow + 1;
}
}
2.1.4 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
思路以及代码:
先移除所有 0,然后把最后的元素都置为 0,就相当于移动 0 的效果。
具体请复习 原地修改数组的解法
class Solution {
public void moveZeroes(int[] nums) {
int p = removeZero(nums);
for(;p<nums.length;p++){
nums[p] = 0;
}
}
public int removeZero(int[] nums){
int fast = 0;
int slow = 0;
while(fast < nums.length){
if(nums[fast]!=0){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
2.2 滑动窗口(解决子数组问题,比如让你寻找符合某个条件的最长/最短子数组)
滑动窗口算法技巧主要用来解决子数组问题,比如让你寻找符合某个条件的最长/最短子数组。
基本套路:
left 指针在后,right 指针在前,两个指针中间的部分就是「窗口」,算法通过扩大和缩小「窗口」来解决某些问题。
1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
2、我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,就好像一条毛毛虫,一伸一缩,不断向右滑动,这就是「滑动窗口」这个名字的来历。
int left = 0, right = 0;
while (left < right && right < nums.size()) {
// 增大窗口
window.add(nums[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.remove(nums[left]);
left++;
}
}
2.2.1 两数之和 II - 输入有序数组
给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。
以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2]
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3]
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
思路:
只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 就可以调整 sum 的大小
代码:
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left = 0;
int right = numbers.length-1;
int[] result = new int[2];
while(left<right){
if(numbers[left]+numbers[right] > target){
right--;
}else if(numbers[left]+numbers[right] < target){
left++;
}else{
result[0] = left+1;
result[1] = right+1;
return result;
}
}
return result;
}
}
2.2.2 最长回文子串
给你一个字符串 s,找到 s 中最长的回文
子串
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
思路:
如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。因此要考虑两种情况:单中心或者双中心;
因此最终思路就是:
for 0 <= i < len(s):
找到以 s[i] 为中心的回文串
找到以 s[i] 和 s[i+1] 为中心的回文串
更新答案
代码:
class Solution {
public String longestPalindrome(String s) {
String res = "";
for(int i = 0;i<s.length();i++){
String s1 = huiwen(s,i,i);
String s2 = huiwen(s,i,i+1);
if(s1.length() > res.length()){
res = s1;
}
if(s2.length() > res.length()){
res = s2;
}
}
return res;
}
//寻找以l和r为中心的最长回文串
public String huiwen(String s,int l,int r){
while(l>=0 && r<s.length() && s.charAt(l) == s.charAt(r)){
l--;
r++;
}
return s.substring(l+1,r);
}
}
2.2.3 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长
子串
的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
思路:
创建一个更新计数器window
当 window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了。
另外,要在收缩窗口完成后更新 res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复。
代码:
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character,Integer> windows = new HashMap();
int left = 0;
int right = 0;
int res = 0;
while(right < s.length()){
char c = s.charAt(right);
right++;
windows.put(c,windows.getOrDefault(c,0)+1);
//判断是不是要缩小窗口
while(windows.get(c) > 1){
char b = s.charAt(left);
left++;
//缩小窗口,将原来的值-1
windows.put(b,windows.get(b)-1);
}
res = Math.max(res,right-left);
}
return res;
}
}
2.2.4 拆炸弹(循环数组)
你有一个炸弹需要拆除,时间紧迫!你的情报员会给你一个长度为 n 的 循环 数组 code 以及一个密钥 k 。
为了获得正确的密码,你需要替换掉每一个数字。所有数字会 同时 被替换。
如果 k > 0 ,将第 i 个数字用 接下来 k 个数字之和替换。
如果 k < 0 ,将第 i 个数字用 之前 k 个数字之和替换。
如果 k == 0 ,将第 i 个数字用 0 替换。
由于 code 是循环的, code[n-1] 下一个元素是 code[0] ,且 code[0] 前一个元素是 code[n-1] 。
给你 循环 数组 code 和整数密钥 k ,请你返回解密后的结果来拆除炸弹!
示例 1:
输入:code = [5,7,1,4], k = 3
输出:[12,10,16,13]
解释:每个数字都被接下来 3 个数字之和替换。解密后的密码为 [7+1+4, 1+4+5, 4+5+7, 5+7+1]。注意到数组是循环连接的。
示例 2:
输入:code = [1,2,3,4], k = 0
输出:[0,0,0,0]
解释:当 k 为 0 时,所有数字都被 0 替换。
示例 3:
输入:code = [2,4,9,3], k = -2
输出:[12,5,6,13]
解释:解密后的密码为 [3+9, 2+3, 4+2, 9+4] 。注意到数组是循环连接的。如果 k 是负数,那么和为 之前 的数字。
思路以及代码:
其实就考了下如何循环遍历数组的问题:
class Solution {
public int[] decrypt(int[] code, int k) {
int[] code2 = new int[code.length];
for(int i = 0;i<code.length;i++){
code2[i] = code[i];
}
if(k == 0){
Arrays.fill(code,0);
}else if(k > 0){
for(int i = 0;i<code.length;i++){
code[i] = 0;
int j = (i+1)%code.length;
int k2 = k;
while(k2 > 0){
code[i] += code2[j];
j = (j+1)%code.length;
k2--;
}
}
}else if(k < 0){
for(int i = 0;i<code.length;i++){
code[i] = 0;
int j = (code.length+i-1)%code.length;
int k2 = 0-k;
while(k2 > 0){
code[i] += code2[j];
j = (code.length+j-1)%code.length;
k2--;
}
}
}
return code;
}
}
2.2.5 存在重复元素 II
给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,1], k = 3
输出:true
示例 2:
输入:nums = [1,0,1,1], k = 1
输出:true
示例 3:
输入:nums = [1,2,3,1,2,3], k = 2
输出:false
思路以及代码:
这题考察滑动窗口技巧,你维护一个大小为 k 的滑动窗口滑过整个数组,滑动的过程中计算窗口中是否存在重复元素。
使用滑动窗口算法需要搞清楚以下几个问题:
1、什么时候应该扩大窗口?
2、什么时候应该缩小窗口?
3、什么时候得到一个合法的答案?
本题很简单直接,以上三个问题的答案是:
1、当窗口大小小于 k 时,扩大窗口。
2、当窗口大小大于 k 时,缩小窗口。
3、当窗口大小等于 k 且发现窗口中存在重复元素时,返回 true。
代码如下:
class Solution {
public boolean containsNearbyDuplicate(int[] nums, int k) {
int left = 0;
int right = 0;
Set<Integer> windows = new HashSet();
while(right < nums.length){
//如果满足 扩大窗口
if(windows.contains(nums[right])){
return true;
}
windows.add(nums[right]);
right++;
//缩小窗口
if(right - left > k){
windows.remove(nums[left]);
left++;
}
}
return false;
}
}
2.2.6 三数之和(秒杀nSum问题 :排序+双指针+递归)
首先,对于2Sum,先给数组从小到大排序,然后双指针 left和 right分别在数组开头和结尾,这样就可以控制 nums[left] 和 nums[right] 这两数之和的大小:如果你想让它俩的和大一些,就让 left++,如果你想让它俩的和小一些,就让 right–
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 记录索引 lo 和 hi 最初对应的值
int left = nums[lo], right = nums[hi];
if (sum < target) lo++;
else if (sum > target) hi--;
else {
res.push_back({left, right});
// 跳过所有重复的元素
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
那么对于3Sum呢?
其实还是分治的思想,现在我们想找和为 target 的三个数字,那么对于第一个数字,可能是什么?nums 中的每一个元素 nums[i] 都有可能!
那么,确定了第一个数字之后,剩下的两个数字可以是什么呢?其实就是和为 target - nums[i] 的两个数字呗,那不就是 twoSum 函数解决的问题么🤔其实nSum问题也可以这样解
因此,总代码如下:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
return nSum(nums,3,0,0);
}
public List<List<Integer>> nSum(int[] nums,int n,int start,int target){
List<List<Integer>> res = new ArrayList();
//至少是 2Sum,且数组大小不应该小于 n
if(n < 2 || nums.length < 2){
return res;
}
//2Sum的话,直接头尾双指针即可
if(n == 2){
int left = start;
int right = nums.length -1;
while(left < right){
int sum = nums[left] + nums[right];
int oldleft = nums[left];
int oldright = nums[right];
if(sum < target){
while(left<right && nums[left] == oldleft){
left++;
}
}else if(sum > target){
while(left<right && nums[right] == oldright){
right--;
}
}else{
res.add(new ArrayList<>(Arrays.asList(oldleft,oldright)));
while(left<right && nums[left] == oldleft){
left++;
}
while(left<right && nums[right] == oldright){
right--;
}
}
}
}else{
//如果n>2,则先确定第一个数,再递归计算(n-1)Sum的结果
for(int i = start;i < nums.length;i++){
List<List<Integer>> sub = nSum(nums,n-1,i+1,target-nums[i]);
for(List<Integer> arr:sub){
arr.add(nums[i]);
res.add(arr);
}
while(i<nums.length-1 && nums[i] == nums[i+1]){
i++;
}
}
}
return res;
}
}
2.2.7 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 连续
子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
思路以及代码:
本题比较简单,直接上代码了:
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int right = 0;
int windowsSum = 0;
int res = Integer.MAX_VALUE;
while(right < nums.length){
//扩大窗口
windowsSum+=nums[right];
right++;
//缩小窗口
while(windowsSum >= target && left < right){
res = Math.min(res,right-left);
windowsSum-=nums[left];
left++;
}
}
return res==Integer.MAX_VALUE?0:res;
}
}
2.2 前缀和数组(快速、频繁地计算一个索引区间内的元素之和)
其实这种方法是对暴力算法的优化,具体看例题:
2.2.1 区域和检索 - 数组不可变
给定一个整数数组 nums,处理以下类型的多个查询:
计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left <= right
实现 NumArray 类:
NumArray(int[] nums) 使用数组 nums 初始化对象
int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + … + nums[right] )
示例 1:
输入:
[“NumArray”, “sumRange”, “sumRange”, “sumRange”]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]
解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))
提示:最多调用 10的四次方 次 sumRange 方法
思路及解题
几乎是个人都能写出下面的代码:
class NumArray {
private int[] nums;
public NumArray(int[] nums) {
this.nums = nums;
}
public int sumRange(int left, int right) {
int res = 0;
for (int i = left; i <= right; i++) {
res += nums[i];
}
return res;
}
}
但是效率很差,因为 sumRange 方法会被频繁调用,而它的时间复杂度是 O(N),其中 N 代表 nums 数组的长度。
这道题的最优解法是使用前缀和技巧,将 sumRange 函数的时间复杂度降为 O(1),说白了就是不要在 sumRange 里面用 for 循环。
具体的思路就是new 一个新的数组 preSum 出来,preSum[i] 记录 nums[0…i-1] 的累加和,由于NumArray方法就执行一次,那么我们可以在这个方法里面new出来新数组,在sumRange 方法只需要查即可。
class NumArray {
private int sum_nums[];
public NumArray(int[] nums) {
sum_nums = new int[nums.length+1];
sum_nums[0] = 0;
for(int i = 0;i<nums.length;i++){
sum_nums[i+1] = sum_nums[i] + nums[i];
}
}
public int sumRange(int left, int right) {
return sum_nums[right+1]-sum_nums[left];
}
}
2.2.2 二维区域和检索 - 矩阵不可变
给定一个二维矩阵 matrix,以下类型的多个请求:
计算其子矩形范围内元素的总和,该子矩阵的 左上角 为 (row1, col1) ,右下角 为 (row2, col2) 。
实现 NumMatrix 类:
NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
int sumRegion(int row1, int col1, int row2, int col2) 返回 左上角 (row1, col1) 、右下角 (row2, col2) 所描述的子矩阵的元素 总和 。
示例 1:
输入:
[“NumMatrix”,“sumRegion”,“sumRegion”,“sumRegion”]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
输出:
[null, 8, 11, 12]
解释:
NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)
思路与解题
注意任意子矩阵的元素和可以转化成它周边几个大矩阵的元素和的运算:
而这四个大矩阵有一个共同的特点,就是左上角都是 (0, 0) 原点。
那么做这道题更好的思路和一维数组中的前缀和是非常类似的,我们可以维护一个二维 preSum 数组,专门记录以原点为顶点的矩阵的元素之和,就可以用几次加减运算算出任何一个子矩阵的元素和:
class NumMatrix {
//preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和
private int[][] ma_sum;
public NumMatrix(int[][] matrix) {
int n = matrix.length;
int m = matrix[0].length;
ma_sum = new int[n+1][m+1];
if(n == 0 || m == 0){
return;
}
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;j++){
ma_sum[i][j] = ma_sum[i-1][j] + ma_sum[i][j-1] + matrix[i-1][j-1] - ma_sum[i-1][j-1];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
return ma_sum[row2+1][col2+1] - ma_sum[row1][col2+1] - ma_sum[row2+1][col1] + ma_sum[row1][col1];
}
}
2.2.3 除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n) 时间复杂度内完成此题
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
思路以及代码:
其实读完题目可以基本确定使用后前缀和,但是需要思考一下下,不能用除法,所以应该分别计算从左向右和从右向左的两个前缀和:
class Solution {
int[] answer;
public int[] productExceptSelf(int[] nums) {
int[] right = new int[nums.length];
int[] left = new int[nums.length];
//从左到右前缀积 left[i]是nums[0...i]的元素积
left[0] = nums[0];
for(int i = 1;i<nums.length;i++){
left[i] = left[i-1]*nums[i];
}
//从右到左的元素积 right[i]是nums[i...n-1]的元素积;
right[nums.length-1] = nums[nums.length-1];
for(int i = nums.length-2;i>=0;i--){
right[i] = right[i+1]*nums[i];
}
answer = new int[nums.length];
answer[0] = right[1];
answer[nums.length-1] = left[nums.length-2];
for(int i = 1;i<nums.length-1;i++){
answer[i] = left[i-1] * right[i+1];
}
return answer;
}
}
或者:
class Solution {
public int[] productExceptSelf(int[] nums) {
int[] left = new int[nums.length];
int[] right = new int[nums.length];
//left[i]表示nums[0...i-1]的前缀积
left[0] = 1;
for(int i = 1; i < nums.length; i++) {
left[i] = left[i - 1] * nums[i - 1];
}
//right[i]表示nums[i-1...n-1]的前缀和
right[nums.length - 1] = 1;
for(int i = nums.length - 2; i >= 0; i--) {
right[i] = right[i + 1] * nums[i + 1];
}
int[] answer = new int[nums.length];
for(int i = 0; i < nums.length; i++) {
answer[i] = left[i] * right[i];
}
return answer;
}
}
2.2.4 和为 K 的子数组(数组有负数,用哈希表解决)
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列.
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
思路以及代码:
首先,假如没有负数,就可以用滑动窗口直接秒
但有负数,就要考虑前缀和解决,最要命的是,要求的不是连续元素的和为k的,而是随机的,这个怎么解决呢?
我们可以建立前缀和和前缀和数量的映射
假如preSum[i](Nums[0…i-1]的前缀和)为10,题目要求的k为5,那么是不是只要在[0…i]之间存在前缀和为5的区间(区间长度可以为1),就可以说明在[0…i-1]之间存在随机元素和为5的子数组,并且,子数组数量就是[0…i]之间前缀和为5的数量
class Solution {
public int subarraySum(int[] nums, int k) {
int[] preSum = new int[nums.length+1];
//存储前缀和以及前缀和出现次数的映射
Map<Integer,Integer> count = new HashMap();
preSum[0] = 0;
count.put(0,1);
int res = 0;
for(int i = 1;i<=nums.length;i++){
//preSum[i]表示nums(0...i-1)的和
preSum[i] = preSum[i-1]+nums[i-1];
int need = preSum[i] - k;
//nums[0..i-1]前缀和和k相差need,看看在(0...i-1)之间存不存在前缀和为need的
//如果存在减去即说明可以构成
if(count.containsKey(need)){
res += count.get(need);
}
//当前前缀和加入哈希表
if(!count.containsKey(preSum[i])){
count.put(preSum[i],1);
}else{
count.put(preSum[i],count.get(preSum[i])+1);
}
}
return res;
}
}
2.2.5 路径总和 III(二叉树+负数的前缀和)
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
提示:
二叉树的节点个数的范围是 [0,1000]
-109 <= Node.val <= 109
-1000 <= targetSum <= 1000
思路以及代码:
DFS遍历树,之后计算路径和(前缀和),并且设置Map记录之前出现过的前缀和和前缀和数量的映射即可
class Solution {
Map<Integer,Integer> count = new HashMap();
int res = 0;
int pathSum;
int targetSum;
public int pathSum(TreeNode root, int targetSum) {
if(root == null){
return 0;
}
this.pathSum = 0;
this.targetSum = targetSum;
this.count.put(0,1);
traverse(root);
return res;
}
public void traverse(TreeNode root){
if(root == null){
return;
}
pathSum+=root.val;
res += count.getOrDefault(pathSum - targetSum,0);
count.put(pathSum,count.getOrDefault(pathSum,0)+1);
traverse(root.left);
traverse(root.right);
count.put(pathSum,count.get(pathSum)-1);
pathSum-=root.val;
}
}
2.3 差分数组(主要适用场景是频繁对原始数组的某个区间的元素进行增减)
比如说,我给你输入一个数组 nums,然后又要求给区间 nums[2…6] 全部加 1,再给 nums[3…9] 全部减 3,再给 nums[0…4] 全部加 2,再给…
一通操作猛如虎,然后问你,最后 nums 数组的值是什么?
这里就需要差分数组的技巧,类似前缀和技巧构造的 preSum 数组,我们先对 nums 数组构造一个 diff 差分数组,diff[i] 就是 nums[i] 和 nums[i-1] 之差:
代码实现:
int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
通过这个 diff 差分数组是可以反推出原始数组 nums 的,代码逻辑如下:
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
这样构造差分数组 diff,就可以快速进行区间增减的操作,如果你想对区间 nums[i…j] 的元素全部加 3,那么只需要让 diff[i] += 3,然后再让 diff[j+1] -= 3 即可:
原理很简单,回想 diff 数组反推 nums 数组的过程,diff[i] += 3 意味着给 nums[i…] 所有的元素都加了 3,然后 diff[j+1] -= 3 又意味着对于 nums[j+1…] 所有元素再减 3,那综合起来,是不是就是对 nums[i…j] 中的所有元素都加 3 了?
所以总和代码如下:
// 差分数组工具类
class Difference {
// 差分数组
private int[] diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i, j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
public int[] result() {
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
2.3.1 航班预订统计
里有 n 个航班,它们分别从 1 到 n 进行编号。
有一份航班预订表 bookings ,表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi] 意味着在从 firsti 到 lasti (包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。
请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。
示例 1:
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
输出:[10,55,45,25,25]
解释:
航班编号 1 2 3 4 5
预订记录 1 : 10 10
预订记录 2 : 20 20
预订记录 3 : 25 25 25 25
总座位数: 10 55 45 25 25
因此,answer = [10,55,45,25,25]
示例 2:
输入:bookings = [[1,2,10],[2,2,15]], n = 2
输出:[10,25]
解释:
航班编号 1 2
预订记录 1 : 10 10
预订记录 2 : 15
总座位数: 10 25
因此,answer = [10,25]
代码:
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] nums = new int[n];
difference differen = new difference(nums);
for(int[] book:bookings){
int i = book[0] - 1;
int j = book[1] - 1;
int val = book[2];
differen.increment(i,j,val);
}
return differen.result();
}
}
class difference{
private int[] diff;
public difference(int[] nums){
assert nums.length > 0;
diff = new int[nums.length];
diff[0] = nums[0];
for(int i = 1;i<nums.length;i++){
diff[i] = nums[i] - diff[i-1];
}
}
public void increment(int i,int j,int val){
diff[i] += val;
if((j+1)<diff.length){
diff[j+1] -= val;
}
}
public int[] result(){
int[] res = new int[diff.length];
res[0] = diff[0];
for(int i = 1;i<diff.length;i++){
res[i] = res[i-1] + diff[i];
}
return res;
}
}
2.3.2 拼车
车上最初有 capacity 个空座位。车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向)
给定整数 capacity 和一个数组 trips , trip[i] = [numPassengersi, fromi, toi] 表示第 i 次旅行有 numPassengersi 乘客,接他们和放他们的位置分别是 fromi 和 toi 。这些位置是从汽车的初始位置向东的公里数。
当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false。
示例 1:
输入:trips = [[2,1,5],[3,3,7]], capacity = 4
输出:false
示例 2:
输入:trips = [[2,1,5],[3,3,7]], capacity = 5
输出:true
代码
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
int[] nums = new int[10000];
Difference difference = new Difference(nums);
int maxs = 0;
for(int[] s:trips){
int val = s[0];
int i = s[1];
//到j其实已经下车了 注意!!!
int j = s[2] - 1;
if(j > maxs){
maxs = j;
}
if(i > j){
return false;
}
difference.increment(i,j,val);
}
int[] res = difference.result();
for (int i = 0; i < res.length; i++) {
if (capacity < res[i]) {
return false;
}
}
return true;
}
}
class Difference{
public int[] diff;
public Difference(int[] nums){
diff = new int[nums.length];
diff[0] = nums[0];
}
public void increment(int i,int j,int val){
diff[i] += val;
diff[j+1] -= val;
}
public int[] result(){
int[] res = new int[diff.length];
res[0] = diff[0];
for(int i = 1;i<diff.length;i++){
res[i] = res[i-1] + diff[i];
}
return res;
}
}
2.4 二分查找的边界搜索(让你求吃香蕉的「最小速度」,让你求轮船的「最低运载能力」,求最值的过程,必然解决的是搜索一个边界的过程)
2.4.1 寻找左侧边界的二分搜索
假设输入的数组 nums = [1,2,3,3,3,5,7],想搜索的元素 target = 3,那么算法就会返回索引 2
如果 target 不存在,搜索左侧边界的二分搜索返回的索引是大于 target 的最小索引。
注意:
1.次每循环的「搜索区间」是 [left, right) 左闭右开,因此right = nums.length,(因为nums.length其实是越界的,所以才可以满足右边开区间)
2.while(left < right) 终止的条件是 left == right,此时搜索区间 [left, left) 为空,所以可以正确终止。
3.为什么 left = mid + 1,right = mid ?
答:因为我们的「搜索区间」是 [left, right) 左闭右开,所以当 nums[mid] 被检测之后,下一步应该去 mid 的左侧或者右侧区间搜索,即 [left, mid) 或 [mid + 1, right)。
4.为什么该算法能够搜索左侧边界?
答:关键在于对于 nums[mid] == target 这种情况的处理:
if (nums[mid] == target)
right = mid;
可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
代码:
int left_bound(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意
while (left < right) { // 注意
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
return left;
}
2.4.2 寻找右侧边界的二分搜索
假设输入的数组 nums = [1,2,3,3,3,5,7],想搜索的元素 target = 3,那么算法就会返回索引 4。
注意:
- 为什么这个算法能够找到右侧边界?
类似地,关键点还是这里:if (nums[mid] == target) { left = mid + 1;
当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的左边界 left,使得区间不断向右靠拢,达到锁定右侧边界的目的。
- 为什么最后返回 left - 1 而不像左侧边界的函数,返回 left?而且我觉得这里既然是搜索右侧边界,应该返回 right 才对?
答:
// 增大 left,锁定右侧边界
if (nums[mid] == target) {
left = mid + 1;
// 这样想: mid = left - 1
代码:
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left - 1; // 注意
}
2.4.3 例题1 在 D 天内送达包裹的能力
二分搜索的套路比较固定,如果遇到一个算法问题,能够确定 x, f(x), target 分别是什么,并写出单调函数 f 的代码,那么就可以运用二分搜索的思路求解。
传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。
传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。
返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。
示例 1:
输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10
请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。
示例 2:
输入:weights = [3,2,2,4,1,4], days = 3
输出:6
解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4
示例 3:
输入:weights = [1,2,3,1,1], days = 4
输出:3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1
思路:
船的运载能力就是自变量 x,运输天数和运载能力成反比,所以可以定义 f(x) 表示 x 的运载能力下需要的运输天数,target 显然就是运输天数 D,我们要在 f(x) == D 的约束下,算出船的最小载重 x
代码:
class Solution {
//查找左边界
public int shipWithinDays(int[] weights, int days) {
int left = 0;
int right = 0;
for(int w:weights){
left = Math.max(left, w);
right += w;
}
while(left<right){
int mid = left + (right - left)/2;
if(f(weights,mid) == days){
right = mid;
}else if(f(weights,mid) > days){
left = mid + 1;
}else if(f(weights,mid) < days){
right = mid;
}
}
return left;
}
//定义f(x) 其中x为最大载运量 f(x)为当最大载运量为x时,最少几天弄完
//注意!!! f和x成反比
public int f(int[] weights,int x){
int wei = x;
int day = 1;
for(int i = 0;i<weights.length;i++){
if (weights[i] > wei){
day+=1;
wei = x;
}
if(weights[i] <= wei){
wei -= weights[i];
}
}
return day;
}
}
例题2 爱吃香蕉的珂珂
珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。
珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
示例 1:
输入:piles = [3,6,7,11], h = 8
输出:4
示例 2:
输入:piles = [30,11,23,4,20], h = 5
输出:30
示例 3:
输入:piles = [30,11,23,4,20], h = 6
输出:23
思路:
珂珂吃香蕉的速度 K 就是自变量 x,吃完所有香蕉所需的时间就是单调函数 f(x),target 就是吃香蕉的时间限制 H。我们需要调整 x,使得 f(x) 尽可能接近 target,也就是说,我们需要找到最小的 x,使得 f(x) <= target
代码:
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int left = 1;
int right = -10000;
for(int p:piles){
//left = Math.min(left,p);
right = Math.max(right,p);
}
while(left < right){
int mid = left + (right - left) / 2;
if(f(piles,mid) == h){
right = mid;
}else if(f(piles,mid) > h){
left = mid+1;
}else if(f(piles,mid) < h){
right = mid;
}
}
return left;
}
//其中x是一次吃几根 f(x)是吃完需要多少时间
//x和f成反比
public int f(int[] p,int x){
int times = 0;
for(int i = 0;i<p.length;i++){
if(p[i] <= x){
times++;
continue;
}
if(p[i] > x){
int y = p[i] / x;
int z = p[i] % x;
if(z == 0){
times += y;
}else{
times += y+1;
}
}
}
return times;
}
}
2.4 滑动窗口框架最终篇
// 注意:java 代码由 chatGPT🤖 根据我的 cpp 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码不保证正确性,仅供参考。如有疑惑,可以参照我写的 cpp 代码对比查看。
import java.util.HashMap;
import java.util.Map;
public class Main {
/* 滑动窗口算法框架 */
public static void slidingWindow(String s) {
// 用合适的数据结构记录窗口中的数据,根据具体场景变通
// 比如说,我想记录窗口中元素出现的次数,就用 map
// 我想记录窗口中的元素和,就用 int
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
while (right < s.length()) {
// c 是将移入窗口的字符
char c = s.charAt(right);
window.put(c, window.getOrDefault(c, 0) + 1);
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
// ...
/*** debug 输出的位置 ***/
// 注意在最终的解法代码中不要 print
// 因为 IO 操作很耗时,可能导致超时
System.out.printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (left < right && window needs shrink) {
// d 是将移出窗口的字符
char d = s.charAt(left);
window.put(d, window.get(d) - 1);
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
// ...
}
}
}
public static void main(String[] args) {
slidingWindow("your string here");
}
}
2.4.1 最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释:最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’。
示例 2:
输入:s = “a”, t = “a”
输出:“a”
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = “a”, t = “aa”
输出: “”
解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
思路:
2、我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
那么可以会问?right找到尽头怎么确定返回值呢?可以在最开始设置一个start和len,来记录left最后一次移动的位置和left right之间的距离,即最终的答案:最小覆盖子串
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = Integer.MAX_VALUE;
代码:
class Solution {
public String minWindow(String s, String t) {
Map<Character,Integer> windows = new HashMap();
Map<Character,Integer> need = new HashMap();
for(char ct:t.toCharArray()){
need.put(ct,need.getOrDefault(ct,0)+1);
}
//temp存储 有几个单词已经和t匹配了
int temp = 0;
int left = 0;
int right = 0;
//记录最小覆盖子串的长度和起始
int start = 0, len = Integer.MAX_VALUE;
while(right < s.length()){
char c = s.charAt(right);
right++;
if(need.containsKey(c)){
windows.put(c,windows.getOrDefault(c,0)+1);
if(windows.get(c).equals(need.get(c))){
temp++;
}
}
//当[left,right)区间内已经满足t了 开始收缩 直到收缩到窗口的字符串不满足要求 停止移动
while(temp == need.size()){
if(right - left < len){
start = left;
len = right - left;
}
char l = s.charAt(left);
//收缩
left++;
if(need.containsKey(l)){
if(need.get(l).equals(windows.get(l))){
temp--;
}
windows.put(l, windows.get(l) - 1);
}
}
}
if(len == Integer.MAX_VALUE){
return "";
}else{
return s.substring(start,start+len);
}
}
}
2.4.2 字符串的排列
给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。
示例 1:
输入:s1 = “ab” s2 = “eidbaooo”
输出:true
解释:s2 包含 s1 的排列之一 (“ba”).
示例 2:
输入:s1= “ab” s2 = “eidboaoo”
输出:false
思路:
既然要判断s2中是否包含s1的排列
那么思考两个问题:
- left什么时候移动?
答:因为排列嘛,显然长度应该是一样的,例如s1是ab,那么当left = 0,right = 3时,表示windows有三个字符,不管有没有ab,一定多余一个字符,那么我们就可以移动left,也就是本题移动 left 缩小窗口的时机是窗口大小大于 t.size() 时。 - 什么时候返回true?
答:进入移动left的while循环时,如果vaild和need.size()相同,那么表示windows中已经包含了ab的排列,即可返回true。
(即 刚好可以进入left的循环(windows有两个字符),且vaild == need.size (这两个字符还敲恰好时=是a和b) 就返回true)
代码:
class Solution {
public boolean checkInclusion(String s1, String s2) {
Map<Character,Integer> need = new HashMap<>();
Map<Character,Integer> windows = new HashMap<>();
for(char s:s1.toCharArray()){
need.put(s,need.getOrDefault(s,0)+1);
}
int left = 0;
int right = 0;
int vaild = 0;
while(right < s2.length()){
char c = s2.charAt(right);
right++;
if(need.containsKey(c)){
windows.put(c,windows.getOrDefault(c,0)+1);
if(windows.get(c).equals(need.get(c))){
vaild++;
}
}
while(right - left >= s1.length()){
if(vaild == need.size()){
return true;
}
char c1 = s2.charAt(left);
left++;
if (need.containsKey(c1)) {
if (windows.get(c1).equals(need.get(c1))){
vaild--;
}
windows.put(c1, windows.getOrDefault(c1, 0) - 1);
}
}
}
return false;
}
}
2.4.3 找到字符串中所有字母异位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。
示例 2:
输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的异位词。
思路
这道题其实就是找s中p的排列,找到后将left加入list,找到所有p的排列后,返回list
还是思考两个问题:
-
left什么时候进行移动?
答:当right - left >= p.length()时,也就是windows中的字符数量恰好等于p,或者windows中的字符有多余的,left就进行移动 -
什么时候加入list?
答:当判断出在windows的字符数量大于等于p的字符数量 ,并且p中的字符已经全部凑齐(vaild等于need中key的数量时),将left加入list。
代码:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
Map<Character,Integer> need = new HashMap();
Map<Character,Integer> windows = new HashMap();
for(char pc:p.toCharArray()){
need.put(pc,need.getOrDefault(pc,0)+1);
}
int left = 0;
int right = 0;
int vaild = 0;
List<Integer> list = new ArrayList();
while(right < s.length()){
char c = s.charAt(right);
right++;
if(need.containsKey(c)){
windows.put(c,windows.getOrDefault(c,0) + 1);
if(need.get(c).equals(windows.get(c))){
vaild++;
}
}
while(right - left >= p.length()){
if(vaild == need.size()){
list.add(left);
}
char c1 = s.charAt(left);
left++;
if (need.containsKey(c1)) {
if (windows.get(c1).equals(need.get(c1))) {
vaild--;
}
windows.put(c1, windows.get(c1) - 1);
}
}
}
return list;
}
}
三 例题
3.1 验证回文串(字符串双指针)
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
字母和数字都属于字母数字字符。
给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。
示例 1:
输入: s = “A man, a plan, a canal: Panama”
输出:true
解释:“amanaplanacanalpanama” 是回文串。
示例 2:
输入:s = “race a car”
输出:false
解释:“raceacar” 不是回文串。
示例 3:
输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 “” 。
由于空字符串正着反着读都一样,所以是回文串。
思路以及代码:
思路非常简单,先把字符串的空格、数字、符号去掉,之后大写转小写;再用两端向中心的双指针进行判断即可:
class Solution {
public boolean isPalindrome(String s) {
StringBuilder sb = new StringBuilder();
for(int i = 0;i<s.length();i++){
char c = s.charAt(i);
if(Character.isLetterOrDigit(c)){
sb.append(Character.toLowerCase(c));
}
}
s = sb.toString();
int left = 0;
int right = s.length() - 1;
while(left < right){
if(s.charAt(left) != s.charAt(right)){
return false;
}
left++;
right--;
}
return true;
}
}
3.2 找出字符串中第一个匹配项的下标(双指针)
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:haystack = “sadbutsad”, needle = “sad”
输出:0
解释:“sad” 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = “leetcode”, needle = “leeto”
输出:-1
解释:“leeto” 没有在 “leetcode” 中出现,所以返回 -1
思路以及代码:
两个指针分别指向haystack 和 needle 的首字符,依次向后遍历匹配。
- 当needle的第一个字符开始匹配到之后两个指针都右移,如果needle 的指针移到了最后时表明匹配成功,直接返回 l-r+1,若刚开始没匹配到,就右移haystack的指针,直到第一个字符匹配成功或者haystack指针移动到末尾结束。
- 当匹配到中间失败时,needle指针归零,haystack指针移动到第一个匹配成功位置的下一位。
class Solution {
public int strStr(String haystack, String needle) {
int l = 0,r = 0;
while(r <= needle.length()-1 && l <= haystack.length()-1){
while(r <= needle.length()-1 && l <= haystack.length()-1 && needle.charAt(r) == haystack.charAt(l)){
r++;
if(r == needle.length()){
return l-r+1;
}
l++;
}
if(r == 0){
l++;
}else if(r != 0 ){
l = l-r+1;
r = 0;
}
}
return -1;
}
}
3.3 O(1) 时间插入、删除和获取随机元素
实现RandomizedSet 类:
RandomizedSet() 初始化 RandomizedSet 对象
bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。
bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。
int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。
你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。
示例:
输入
[“RandomizedSet”, “insert”, “remove”, “insert”, “getRandom”, “remove”, “insert”, “getRandom”]
[[], [1], [2], [2], [], [1], [2], []]
输出
[null, true, false, true, 2, true, false, 2]
解释
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。
randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。
randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。
randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
思路以及代码:
注意满足每个函数的 平均 时间复杂度为 O(1),这样就不能对数组进行遍历,因此需要一个map来记录val和index的对应关系:
class RandomizedSet {
List<Integer> nums;
//记录每个元素在nums中的索引
Map<Integer, Integer> valToIndex;
public RandomizedSet() {
nums = new ArrayList();
valToIndex = new HashMap();
}
public boolean insert(int val) {
//val存在
if(valToIndex.containsKey(val)){
return false;
}
//val不存在
valToIndex.put(val,nums.size());
nums.add(val);
return true;
}
public boolean remove(int val) {
//val不存在
if(!valToIndex.containsKey(val)){
return false;
}
//val存在
int index = valToIndex.get(val);
//将最后一个元素的索引与index互换
valToIndex.put(nums.get(nums.size()-1),index);
//交换val和最后一个元素
Collections.swap(nums,index,nums.size()-1);
//nums删除val
nums.remove(nums.size()-1);
valToIndex.remove(val);
return true;
}
public int getRandom() {
return nums.get((int)(Math.random() * nums.size()));
}
}
3.4 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
思路以及代码:
找下规律,在区间有序的情况下,如果某个区间(a,b)的b大于等于下个区间的a,就表示可以进行合并
因此,本题思路就是先按a进行区间排序,再进行整合
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> res = new ArrayList();
//按照第一个元素的大小进行排序
Arrays.sort(intervals,(a,b)->{
return a[0] - b[0];
});
res.add(intervals[0]);
for(int i = 1;i<intervals.length;i++){
int[] cur = intervals[i];
int[] last = res.getLast();
//后面的第0个值小于前面的第一个值 则合并
if(cur[0] <= last[1]){
res.getLast()[1] = Math.max(last[1],cur[1]);
}else{
res.add(cur);
}
}
return res.toArray(new int[0][0]);
}
}