• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • YouClaw
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

youyou-dev

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

力扣Hot100

力扣Hot100

目录
  • 力扣Hot100
    • 1、多数元素
      • 解法一:排序后取中位数
      • 解法二:哈希表计数
      • 解法三:Boyer-Moore 投票算法(最优解)
    • 2、哈希--字母异位词分组
      • 解法一:排序法
    • 3、最长连续序列
    • 4、移动零
      • 解法一:双指针(快慢指针)
      • 解法二:一次遍历 + 交换(双指针交换法)
    • 5、盛最多水的容器
      • 解法一:双指针
      • 解法二:暴力枚举(不推荐,仅用于对比)
    • 6、三数之和
      • 解法一:排序 + 双指针
      • 代码逐行解释
      • 复杂度分析
    • 7、接雨水
      • 解法

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 可以为任意值,count 为 0;

  • 我们遍历数组 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))。

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

核心思想:

  • 使用两个指针 slow 和 fast。
  • 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 循环中,left 和 right 总共移动了 n-1 次,每次循环只做常数时间的计算,因此总体 O(n)。
  • 空间复杂度 O(1):只使用了 left、right、maxArea、area 等几个变量,没有额外数组。

6、三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != 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;
    }
}

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

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3