力扣Hot100(1)

力扣Hot100

1、多数元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入:nums = [3,2,3]
输出:3

示例 2:

输入:nums = [2,2,1,1,1,2,2]
输出:2

进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

解法一:排序后取中位数

因为出现次数总是大于 ⌊ n/2 ⌋,所以可以先排序,排序完之后找到中间数,即

import java.util.Arrays;
public int majorityElement(int[] nums) {
    Arrays.sort(nums);
    return nums[nums.length / 2];  // 多数元素必然占据中间位置
}

时间复杂度 O(n log n),空间复杂度 O(1)

解法二:哈希表计数

遍历数组一次,用哈希表记录每个元素出现的次数,当某个元素的出现次数超过 n/2 时,它就是我们要找的多数元素

import java.util.HashMap;
import java.util.Map;

public int majorityElement(int[] nums) {
    Map<Integer,Integer> countMap = new HashMap<>();
    int n = nums.length;
    //使用增强型 for 循环(for-each)遍历数组 nums,每次循环将当前元素赋值给变量 num。
    for(int num:nums){
        countMap.put(num,countMap.getOrDefault(num,0)+1);
         // 一旦某个元素出现次数超过 n/2,立即返回(剪枝)
        if (countMap.get(num) > n / 2) {
            return num;
        }
    }
    return -1;
}

时间复杂度 O(n),空间复杂度 O(n)

countMap.put(num,countMap.getOrDefault(bum,0)+1)详解

  1. countMap.getOrDefault(num, 0):从 countMap 中获取键 num 对应的值(即该元素已出现的次数)。如果 num 这个键还不存在于 map 中,则返回默认值 0
  2. 将获取到的次数加 1,得到新的出现次数。
  3. countMap.put(num, ...):将 num 作为键,新的次数作为值存回 countMap 中。如果该键已存在,则新值会覆盖旧值;如果不存在,则添加新键值对。

解法三:Boyer-Moore 投票算法(最优解)

核心思想:相互抵消

假设数组中有两类元素:多数元素 M(出现次数 > n/2)和其他元素 O(总数 < n/2)。把众数记为 +1,把其他数记为 −1,将它们全部加起来,显然和大于 0

算法步骤

  • 假设有一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count0

  • 我们遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断 x:

  • 如果 x 与 candidate 相等,那么计数器 count 的值增加 1;

  • 如果 x 与 candidate 不等,那么计数器 count 的值减少 1。

  • 当count=0时,更换下一个数为新的众数,重复上述步骤

  • 在遍历完成后,此时的candidate 即为整个数组的众数。

public int majorityElement(int[] nums) {
        int candidate = 0;  //整型变量 candidate,用于记录当前候选的多数元素。初始值可以设为任意值
        int count = 0;  //整型变量 count,用于记录当前候选元素出现的次数。
        for (int num : nums) {
            if(count == 0) candidate = num;
            count += (num == candicate)? 1 : -1;  
        }
    return candidate;
}

eg:

nums:      [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
candidate:  7  7  7  7  7  7   5  5   5  5  5  5   7  7  7  7
count:      1  2  1  2  1  0   1  0   1  2  1  0   1  2  3  4

上述示例作者:力扣官方题解
链接:https://leetcode.cn/problems/majority-element/solutions/146074/duo-shu-yuan-su-by-leetcode-solution/
来源:力扣(LeetCode)

2、哈希--字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

示例 1:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]

输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

解释:

在 strs 中没有字符串可以通过重新排列来形成 "bat"。
字符串 "nat" 和 "tan" 是字母异位词,因为它们可以重新排列以形成彼此。
字符串 "ate" ,"eat" 和 "tea" 是字母异位词,因为它们可以重新排列以形成彼此。
示例 2:

输入: strs = [""]

输出: [[""]]

示例 3:

输入: strs = ["a"]

输出: [["a"]]

核心思路是字母异位词(anagram) 指的是两个单词包含的字母种类和数量完全相同,只是排列顺序不同。因此,我们可以为每个单词生成一个唯一的“签名”,然后将拥有相同签名的单词归为一组。

解法一:排序法

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        //创建一个哈希表,健为字符串,值为字符串列表
         Map<String, List<String>> map = new HashMap<>();
         for (String s : strs) {
            //将当前字符串 s 转换成字符数组。
            char[] chars = s.toCharArray();
            Arrays.sort(chars);
            //排序后的字符串s作为一个健
            String key = new String(chars);
            //将原字符串s作为一个值加入值列表
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
        }
     //map.values() 返回映射中所有值(即各个分组列表)的集合
     return new ArrayList<>(map.values());
   }
}
  • map.computeIfAbsent(key, k -> new ArrayList<>()):检查哈希表中是否已存在 key 对应的值。
    • 如果不存在,则使用第二个参数(一个 lambda 表达式 k -> new ArrayList<>())创建一个新的空 ArrayList<String> 作为值,并存入映射。
    • 如果已存在,则直接返回已有的列表。
  • 返回值是一个 List<String>(无论新创建还是已存在)。
  • 然后对这个列表调用 .add(s),将当前原始字符串 s 添加到该组中。

上述方法时间复杂度为 O(N * K log K),其中 N 是单词数量,K 是单词的最大长度;空间复杂度 O(N * K),用于存储所有单词和哈希表。

3、最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:

输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
示例 3:

输入:nums = [1,0,1,2]
输出:3

思路

  1. 将所有数字存入 HashSet,实现 O(1) 的快速查找。
  2. 遍历数组中的每个数字 num
    • 只有当 num - 1 不在集合中时,才认为 num 可能是一个连续序列的起点。
    • 从起点开始,不断检查 num + 1, num + 2 ... 是否在集合中,计算当前序列长度。
    • 更新全局最长序列长度。
  3. 虽然有两层循环,但每个数字只会作为起点被访问一次,且在向后查找时每个数字最多被访问一次,整体时间复杂度为 O(n)。
import java.util.HashSet;
import java.util.Set;

public class Solution {
    public int longestConsecutive(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }

        // 将所有数字加入哈希集合,便于 O(1) 查找
        Set<Integer> numSet = new HashSet<>();
        for (int num : nums) {
            numSet.add(num);
        }

        int longestStreak = 0;

        for (int num : numSet) {
            // 只有当 num-1 不在集合中时,才以 num 为起点向后查找
            if (!numSet.contains(num - 1)) {
                int currentNum = num;
                int currentStreak = 1;

                while (numSet.contains(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }
                longestStreak = Math.max(longestStreak, currentStreak);
            }
        }

        return longestStreak;
    }
}

4、移动零

问题分析

给定一个整数数组 nums,要求将所有的 0 移动到数组末尾,同时保持非零元素的原有顺序。必须原地修改数组,不能使用额外的数组(即空间复杂度为 O(1))。

解法一:双指针(快慢指针)

核心思想

  • 使用两个指针 slowfast
  • fast 指针遍历数组,遇到非零元素就把它放到 slow 指向的位置,然后 slow 后移一位。
  • 遍历结束后,slow 之前的位置都已经放好了非零元素,且顺序不变。slow 之后的位置全部赋值为 0 即可。

时间复杂度:O(n),只需一次遍历
空间复杂度:O(1),只用了两个指针

public void moveZeroes(int[] nums) {
    // 如果数组为空或长度为1,无需处理,直接返回
    if (nums == null || nums.length <= 1) {
        return;
    }
    
    int slow = 0; // 指向下一个非零元素应该存放的位置
    
    // 第一次遍历:将所有非零元素移到数组前部
    for (int fast = 0; fast < nums.length; fast++) {
        if (nums[fast] != 0) {         //如果当前元素不是零。
            nums[slow] = nums[fast];   //把它放到慢指针的位置。
            slow++;					   //慢指针后移一位,准备接收下一个非零元素。
        }
    }
    //第一个循环结束后,所有非零元素已经按顺序放在了 `[0, slow-1]` 区间。
    // 第二次遍历:slow 之后的位置全部补零
    for (int i = slow; i < nums.length; i++) {
        nums[i] = 0;
    }
}

解法二:一次遍历 + 交换(双指针交换法)

核心思想

  • 同样使用双指针,但一次遍历完成。
  • slow 指向已处理好的非零元素的下一个位置(类似解法一)。
  • fast 遍历数组,当遇到非零元素时,与 slow 位置的元素交换,然后 slow++
  • 这样遍历结束后,所有零自然被交换到数组末尾。

优点:只需一次循环,减少了第二次赋值操作。
时间复杂度:O(n)
空间复杂度:O(1)

public void moveZeroes(int[] nums) {
    if (nums == null || nums.length <= 1) return;
    
    int slow = 0;
    for (int fast = 0; fast < nums.length; fast++) {
        if (nums[fast] != 0) {
            // 交换 nums[fast] 和 nums[slow]
            int temp = nums[fast];
            nums[fast] = nums[slow];
            nums[slow] = temp;
            slow++;
        }
    }
}

逐行解释

  • 前两行同解法一。
  • int slow = 0;:慢指针指向当前已放置非零区域的下一位置(初始为0)。
  • for (int fast = 0; fast < nums.length; fast++):遍历每个元素。
  • if (nums[fast] != 0):遇到非零元素。
  • int temp = nums[fast]; ...:交换 nums[fast]nums[slow]
  • slow++;:慢指针后移,扩大非零区域。
  • 交换操作将零向右推,非零向左归位,一次遍历完成。

5、盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

img

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

输入:height = [1,1]
输出:1

解法一:双指针

  1. 初始化双指针
    左指针 l 指向数组的第一个元素(最左边的线),右指针 r 指向数组的最后一个元素(最右边的线)。此时两根线之间的距离最大,容器宽度最大。
  2. 计算当前面积
    对于当前指针位置 (l, r),容器的高度由两条线中较短的那条决定(水会溢出),宽度为 r - l,因此面积 = min(height[l], height[r]) * (r - l)
  3. 更新最大面积
    ans 记录所有面积中的最大值。
  4. 移动指针
    • 如果 height[l] <= height[r],说明左边线较短,移动左指针向右一步l++)。
    • 否则(左边线比右边线高),移动右指针向左一步r--)。
      移动指针后,宽度减少 1,但有机会让容器的高度变高(因为原来较短的线可能被换掉)。
  5. 重复步骤 2-4,直到 l >= r(两根线重合或交错),此时所有可能的容器都已考虑。
  6. 返回 ans,即最大水量。

时间复杂度:O(n),只遍历一次
空间复杂度:O(1)

public class Solution {
    public int maxArea(int[] height) {
        int l = 0, r = height.length - 1;
        int ans = 0;   //初始化变量 ans 用于记录当前找到的最大水量,初始为 0。
        while (l < r) {
            //当左指针小于右指针时循环(即两根线之间有距离,可以形成容器)。如果 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;
    }
}

解法二:暴力枚举(不推荐,仅用于对比)

思路:枚举所有 (i, j) 组合,计算面积,记录最大值。

时间复杂度:O(n²)
空间复杂度:O(1)

public int maxAreaBruteForce(int[] height) {
    int max = 0;
    for (int i = 0; i < height.length; i++) {
        for (int j = i + 1; j < height.length; j++) {
            int area = Math.min(height[i], height[j]) * (j - i);
            max = Math.max(max, area);
        }
    }
    return max;
}

该解法在 n 较大时性能极差,不推荐使用。

复杂度分析(以双指针解法为例)

  • 时间复杂度 O(n)while 循环中,leftright 总共移动了 n-1 次,每次循环只做常数时间的计算,因此总体 O(n)。
  • 空间复杂度 O(1):只使用了 leftrightmaxAreaarea 等几个变量,没有额外数组。

6、三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

解法一:排序 + 双指针

时间复杂度 O(n²),空间复杂度 O(1)

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        if (nums == null || nums.length < 3) return result;    //边界检查:数组为空或长度不足 3 直接返回空
        // 1. 排序
        Arrays.sort(nums);
        int n = nums.length;

        // 2. 固定第一个数
        for (int i = 0; i < n - 2; i++) {
            // 去重:跳过重复的 nums[i]
            if (i > 0 && nums[i] == nums[i - 1]) continue;

            int target = -nums[i];
            int left = i + 1;        //初始化左右指针
            int right = n - 1;

            // 双指针查找两数之和等于 target
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum == target) {
                    // 找到一组解
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    // 移动指针并跳过重复元素
                    while (left < right && nums[left] == nums[left + 1]) left++;        //跳过重复的 `left` 值
                    while (left < right && nums[right] == nums[right - 1]) right--;     //跳过重复的 right 值
                    left++;
                    right--;
                } else if (sum < target) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return result;
   }
}

代码逐行解释

代码 含义
List<List<Integer>> result = new ArrayList<>(); 存储结果三元组列表
`if (nums == null
Arrays.sort(nums); 升序排序,便于双指针移动和去重
for (int i = 0; i < n - 2; i++) 固定第一个数,范围到倒数第三个元素
if (i > 0 && nums[i] == nums[i - 1]) continue; 跳过重复的 nums[i],避免结果重复
int target = -nums[i]; 需要在 i 右侧找到两个数之和等于 -nums[i](即三数和为 0)
int left = i + 1, right = n - 1; 初始化左右指针
while (left < right) 双指针遍历
int sum = nums[left] + nums[right]; 计算当前两数之和
if (sum == target) 找到符合条件的三元组
result.add(Arrays.asList(nums[i], nums[left], nums[right])); 添加到结果列表
while (left < right && nums[left] == nums[left + 1]) left++; 跳过重复的 left
while (left < right && nums[right] == nums[right - 1]) right--; 跳过重复的 right
left++; right--; 继续寻找下一组可能解
else if (sum < target) left++; 和太小,左指针右移增大和
else right--; 和太大,右指针左移减小和

复杂度分析

  • 时间复杂度:O(n²)
    外层循环 O(n),内层双指针 O(n),总 O(n²)。
  • 空间复杂度:O(1)(忽略输出结果),排序使用 O(log n) 的栈空间(Java 的 Arrays.sort 对于基本类型是双轴快排,空间 O(log n))。

7、接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

img

输入:height = [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 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

核心思路是 “木桶效应”:每个柱子上方能接多少雨水,取决于它左边最高柱子右边最高柱子中较矮的那个(短板),再减去它自己的高度。

具体步骤:

  1. 预处理左侧最高
    从左向右遍历,left[i] 记录下标 0..i 中柱子的最大高度(包括自己)。
  2. 预处理右侧最高
    从右向左遍历,right[i] 记录下标 i..n-1 中柱子的最大高度。
  3. 计算总水量
    对于每个位置 i
    • 该位置的水位上限 = min(left[i], right[i])
    • 该位置的实际水量 = 上限 - height[i](若为负数则取 0,但这里保证不会负,因为左右最大都 ≥ 自身)
      总水量 = 所有位置的水量之和。

代码中用了一个等效技巧:先累加所有位置的 min(left[i], right[i]) 得到 sum1,再累加原始高度 sum2,最后 sum1 - sum2 就是总水量。

时间 O(n),空间 O(n)

解法

class Solution {
    public int trap(int[] height) {
        // 1. 预处理:计算每个柱子左侧的最高高度(包括自身)
        int[] left = new int[height.length];
        // 2. 预处理:计算每个柱子右侧的最高高度(包括自身)
        int[] right = new int[height.length];
        
        int temp = 0;
        // 从左向右扫描,left[i] 记录 height[0..i] 中的最大值
        for (int i = 0; i < height.length; i++) {
            if (temp < height[i]) {
                temp = height[i];
                left[i] = temp;
            } else {
                left[i] = temp;
            }
        }
        
        // 从右向左扫描,right[i] 记录 height[i..n-1] 中的最大值
        temp = height[height.length - 1];
        for (int i = height.length - 1; i >= 0; i--) {
            if (temp < height[i]) {
                temp = height[i];
                right[i] = temp;
            } else {
                right[i] = temp;
            }
        }
        
        // 3. 计算总水量 = Σ min(left[i], right[i]) - Σ height[i]
        int sum1 = 0, sum2 = 0;
        for (int i = 0; i < height.length; i++) {
            // sum1 累加每个位置能接水的“上限”(即左右最高中的较小值)
            sum1 += left[i] < right[i] ? left[i] : right[i];
            // sum2 累加柱子的原始高度
            sum2 += height[i];
        }
        // 总接水量 = 上限总和 - 柱子体积总和
        return sum1 - sum2;
    }
}

8、无重复字符的最长字串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

思路

本题要求找出字符串中不含重复字符的最长子串的长度。常用的高效解法是滑动窗口

  1. 使用两个指针 leftright 表示当前窗口的左右边界,初始都指向字符串开头。
  2. 用一个 HashSet(或数组,因为字符集有限)来记录当前窗口内出现过的字符。
  3. 移动右指针 right 遍历整个字符串:
    • 如果 s.charAt(right) 不在集合中,说明当前窗口没有重复,将其加入集合,并更新最长长度 maxLen = max(maxLen, right - left + 1)
    • 如果字符已在集合中,说明窗口内出现重复。此时需要移动左指针 left,逐个将窗口最左边的字符从集合中移除,直到重复字符被移除为止。然后再将当前字符加入集合。
  4. 继续右移 right,直到遍历完所有字符。

时间复杂度:O(n),每个字符最多被左右指针各访问一次。
空间复杂度:O(min(n, |字符集|)),最坏情况需要存储整个字符串的字符。

逐步骤过程
字符串:a b c a b c b b
索引: 0 1 2 3 4 5 6 7

  1. right=0,字符 = a
    窗口:[a]
    集合:{a}
    长度:1 → max=1
  2. right=1,字符 = b
    窗口:[a,b]
    集合:{a,b}
    长度:2 → max=2
  3. right=2,字符 = c
    窗口:[a,b,c]
    集合:{a,b,c}
    长度:3 → max=3
  4. right=3,字符 = a
    a 已存在,left 右移:
    移除 s [0]=a,left=1
    加入 a
    窗口:[b,c,a]
    长度:3 → max 不变
  5. right=4,字符 = b
    b 已存在,left 右移:
    移除 s [1]=b,left=2
    加入 b
    窗口:[c,a,b]
    长度:3 → max 不变
  6. right=5,字符 = c
    c 已存在,left 右移:
    移除 s [2]=c,left=3
    加入 c
    窗口:[a,b,c]
    长度:3 → max 不变
  7. right=6,字符 = b
    b 已存在,连续移动 left:
    移除 s [3]=a → left=4
    移除 s [4]=b → left=5
    加入 b
    窗口:[c,b]
    长度:2
  8. right=7,字符 = b
    b 已存在,连续移动 left:
    移除 s [5]=c → left=6
    移除 s [6]=b → left=7
    加入 b
    窗口:[b]
    长度:1

解法:

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 哈希集合,记录当前窗口里出现过的字符
        Set<Character> set = new HashSet<>();
        int left = 0, maxLen = 0;
        
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            
            // 如果当前字符已经在窗口里,就不断移动左指针,删掉左边字符
            while (set.contains(c)) {
                set.remove(s.charAt(left));
                left++;
            }
            
            // 把当前字符加入窗口
            set.add(c);
            
            // 更新最长子串长度
            maxLen = Math.max(maxLen, right - left + 1);
        }
        
        return maxLen;
    }
}

9、找到字符串中所有字母异位词

给定两个字符串 sp,找到 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 互为字母异位词(即字符种类和个数相同,顺序可以不同)的子串,并返回这些子串的起始索引。

核心方法:滑动窗口 + 字符计数数组。

  • 因为题目中字符串只包含小写字母(通常如此),可以用长度为 26 的数组分别记录 p 中每个字母的出现次数,以及当前滑动窗口中每个字母的出现次数。
  • 滑动窗口的长度固定为 p.length(),在 s 上从左向右滑动,每次比较两个计数数组是否相同。相同则说明当前窗口是 p 的一个异位词,记录起始索引。
  • 初始窗口为 s[0..pLen-1],先比较一次。然后每次滑动:移除窗口最左边的字符,加入窗口右边的新字符,再比较。

时间复杂度:O(n),其中 n 为 s 的长度(每次滑动只更新两个字符的计数,比较数组是 O(26)=O(1))。
空间复杂度:O(1),只用了固定大小的数组。

解法:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        // 1. 先拿到两个字符串的长度
        int sLen = s.length(), pLen = p.length();

        // 2. 边界提前判断:s比p短,不可能有异位词,直接返回空列表
        if (sLen < pLen) {
            return new ArrayList<Integer>();
        }

        // 3. 定义核心变量:结果集合、两个计数数组
        List<Integer> ans = new ArrayList<Integer>();   // 存符合条件的起始索引
        int[] sCount = new int[26];    // 记录当前滑动窗口内的字母出现次数
        int[] pCount = new int[26];    // 记录p的字母出现次数(固定的标准答案)

        // 4. 初始化计数:一次循环同时统计p的计数、s第一个窗口的计数
        for (int i = 0; i < pLen; ++i) {
            ++sCount[s.charAt(i) - 'a'];   // 给s窗口的对应字母计数+1
            ++pCount[p.charAt(i) - 'a'];   // 给p的对应字母计数+1
        }

        // 5. 检查第一个窗口(起始索引0)是否符合要求
        if (Arrays.equals(sCount, pCount)) {
            ans.add(0);
        }

        // 6. 滑动窗口主循环:遍历所有剩余的窗口
        for (int i = 0; i < sLen - pLen; ++i) {
            // 6.1 移除窗口最左侧的字符(滑出窗口,计数-1)
            --sCount[s.charAt(i) - 'a'];
            // 6.2 加入窗口右侧的新字符(滑入窗口,计数+1)
            ++sCount[s.charAt(i + pLen) - 'a'];

            // 6.3 检查当前窗口是否匹配,匹配就记录起始索引
            if (Arrays.equals(sCount, pCount)) {
                ans.add(i + 1);
            }
        }

        // 7. 返回最终结果
        return ans;
    }
}

10、和为K的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

示例 1:

输入:nums = [1,1,1], k = 2
输出:2

示例 2:

输入:nums = [1,2,3], k = 3
输出:2

解法一:枚举
最直接的思路:枚举所有可能的子数组,计算每个子数组的和,判断是否等于 k。
通过固定子数组的起点 start,然后从该起点向前(或向后)累加元素,得到所有以 start 结尾的子数组的和。
这里代码采用的是 从终点向前累加 的方式:外层循环遍历每个位置作为终点 start(变量名虽为 start,实际逻辑是终点),内层循环从当前终点向前累加到数组开头,依次计算以每个位置结尾的子数组和。

public class Solution {
    public int subarraySum(int[] nums, int k) {
        // 记录符合条件的子数组数量,初始为0
        int count = 0;
        // 外层循环:start 表示子数组的结束位置(反向定义,非常规写法)
        for (int start = 0; start < nums.length; ++start) {
            // 每轮重置当前子数组的和
            int sum = 0;
            // 内层循环:从 start 往前遍历,枚举所有以 start 结尾的子数组
            for (int end = start; end >= 0; --end) {
                // 累加元素
                sum += nums[end];
                // 如果和等于k,计数+1
                if (sum == k) {
                    count++;
                }
            }
        }
        // 返回最终结果
        return count;
    }
}

解法二:前缀和 + 哈希表优化
思路
利用前缀和的概念:定义 pre[i] 为数组前 i 个元素的和(即 nums[0] + ... + nums[i-1])。
那么子数组 [j, i] 的和等于 pre[i+1] - pre[j]。
要求 pre[i+1] - pre[j] = k 等价于 pre[j] = pre[i+1] - k。

因此,遍历数组时,维护当前前缀和 pre,并记录每个前缀和出现的次数(哈希表)。对于每个位置 i,需要知道在 i 之前有多少个前缀和等于 pre - k,这些前缀和对应的起点就能与当前 i 构成和为 k 的子数组。
初始时哈希表中放入 {0: 1},表示空前缀和为 0 出现了一次,用于处理从数组开头开始的子数组。

这是本题标准最优解法,时间复杂度 O(n),空间复杂度 O(n)。

原理

前缀和 pre[i] = 数组前 i 个元素的和子数组 [j, i] 的和 = pre[i] - pre[j-1] = k→ 等价于:pre[j-1] = pre[i] - k用哈希表记录前缀和出现的次数,快速统计答案。

public class Solution {
    public int subarraySum(int[] nums, int k) {
        // count:最终符合条件的子数组个数
        // pre:当前的前缀和(累加和)
        int count = 0, pre = 0;
        // 哈希表:key=前缀和,value=这个前缀和出现的次数
        HashMap < Integer, Integer > mp = new HashMap < > ();
        // 初始化:前缀和为0出现1次(关键:处理前缀和直接等于k的情况)
        mp.put(0, 1);
        
        // 遍历数组每一个元素
        for (int i = 0; i < nums.length; i++) {
            // 计算当前前缀和 = 之前的前缀和 + 当前元素
            pre += nums[i];
            
            // 如果哈希表中存在 (当前前缀和 - k)
            // 说明:之前有前缀和 = pre - k → 两段之差=k,找到符合条件的子数组
            if (mp.containsKey(pre - k)) {
                // 累加 该前缀和出现的次数 到结果中
                count += mp.get(pre - k);
            }
            
            // 更新哈希表:当前前缀和出现次数+1
            mp.put(pre, mp.getOrDefault(pre, 0) + 1);
        }
        // 返回总个数
        return count;
    }
}

posted on 2026-04-14 14:36  U~U  阅读(48)  评论(0)    收藏  举报