力扣Hot100
力扣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)详解
countMap.getOrDefault(num, 0):从countMap中获取键num对应的值(即该元素已出现的次数)。如果num这个键还不存在于 map 中,则返回默认值0。- 将获取到的次数加
1,得到新的出现次数。 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>作为值,并存入映射。 - 如果已存在,则直接返回已有的列表。
- 如果不存在,则使用第二个参数(一个 lambda 表达式
- 返回值是一个
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
思路
- 将所有数字存入
HashSet,实现 O(1) 的快速查找。 - 遍历数组中的每个数字
num:- 只有当
num - 1不在集合中时,才认为num可能是一个连续序列的起点。 - 从起点开始,不断检查
num + 1,num + 2... 是否在集合中,计算当前序列长度。 - 更新全局最长序列长度。
- 只有当
- 虽然有两层循环,但每个数字只会作为起点被访问一次,且在向后查找时每个数字最多被访问一次,整体时间复杂度为 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 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。

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

输入: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
核心思路是 “木桶效应”:每个柱子上方能接多少雨水,取决于它左边最高柱子和右边最高柱子中较矮的那个(短板),再减去它自己的高度。
具体步骤:
- 预处理左侧最高
从左向右遍历,left[i]记录下标0..i中柱子的最大高度(包括自己)。 - 预处理右侧最高
从右向左遍历,right[i]记录下标i..n-1中柱子的最大高度。 - 计算总水量
对于每个位置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;
}
}
浙公网安备 33010602011771号