前言
这是我平时在LeetCode上刷的一些题,有时间慢慢整理一下吧。
1、哈希
1.1 两数之和
Leetcode: 1. 两数之和
难度:简单
题目:
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
方法一:(暴力枚举)
对于大部分简单题都可以使用暴力枚举通过
首先我们可以用循环遍历数组中的每个元素 x,然后嵌套循环检查是否存在 target - x。需要注意的是,对于当前元素 x,之前的元素都已经和其匹配过,无需再进行匹配。所以我们只需在 x 后面的元素中寻找 target - x。
因为需要两次嵌套循环,所以时间复杂度为:O(N2)
空间复杂度:O(1)
代码:略
方法二:(哈希)
对于嵌套循环检查是否存在 target - x,我们可以创建一个HashMap来简化查询。
package com.hot100.test001;
import java.util.HashMap;
import java.util.Map;
public class Solution {
/**
* 1. 两数之和
* 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
* 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
*
* */
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
if (hashtable.containsKey(target - nums[i])) {
return new int[]{hashtable.get(target - nums[i]), i};
}
hashtable.put(nums[i], i);
}
return new int[0];
}
}
因为使用了HashMap,查找target - x的时间复杂度降低到从 O(N) 降低到 O(1)。 同时空间复杂度提高到了O(N),哈希表的开销。
时间复杂度为:O(N)
空间复杂度:O(N)
1.2 字母异位词分组
LeetCode: 49. 字母异位词分组
难度:中等
题目:
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
方法一:(排序)
思路:遍历每个字符串,对于每个字符串,得到该字符串所在的一组字母异位词的标志,将当前字符串加入该组字母异位词的列表中。遍历全部字符串之后,哈希表中的每个键值对即为一组字母异位词。
要将字母异位词组合在一起,可以使用哈希表来存储每个单词的排序后形式,并将相同排序后形式的单词放在同一个列表中。
import java.util.*;
public class Solution {
/**
* 49. 字母异位词分组
* 给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
* 字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
*
* 示例 1:
* 输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
* 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
*
* */
public List<List<String>> groupAnagrams(String[] strs) {
// 创建一个哈希表,用于存储排序后形式的单词以及对应的单词列表
Map<String, List<String>> map = new HashMap<>();
// 遍历字符串数组
for (String str : strs) {
// 将当前单词进行排序,得到排序后形式
char[] charArray = str.toCharArray();
Arrays.sort(charArray);
String sortedStr = new String(charArray);
// 如果哈希表中已经存在这个排序后形式的单词,则将当前单词添加到对应的列表中
if (map.containsKey(sortedStr)) {
map.get(sortedStr).add(str);
} else {
// 如果不存在,则创建一个新的列表,并将当前单词添加到列表中
List<String> newList = new ArrayList<>();
newList.add(str);
map.put(sortedStr, newList);
}
}
// 将哈希表中的值(即各个单词列表)转换为结果列表
List<List<String>> result = new ArrayList<>(map.values());
return result;
}
}
遍历字符串数组需要 O(n) 的时间复杂度。
对于每个单词,将其排序需要 O(k * log(k)) 的时间复杂度,其中 k 是单词的长度。
因此,时间复杂度为 O(n * k * log(k))。
哈希表中存储了每个单词的排序后形式以及对应的单词列表,所以主要取决于哈希表的大小。
哈希表中最多包含 n 个键值对,每个键值对的键长度为 k,因此空间复杂度为 O(n * k)。
时间复杂度:O(nklogk)
空间复杂度:O(nk)
方法二:(计数)
思路:
创建一个哈希表,用于存储每个单词的字符计数形式以及对应的单词列表。遍历字符串数组,对于数组中的每个单词,统计其字符出现的次数,生成一个唯一的计数形式。
将计数形式作为键,将当前单词添加到对应的单词列表中。
最后,将哈希表中的所有单词列表取出作为结果列表返回。
import java.util.*;
public class Solution {
/**
* LeetCode 热题 100 (no.2)
* 49. 字母异位词分组
* 给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
* 字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
*
* 示例 1:
* 输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
* 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
*
* */
public List<List<String>> groupAnagrams(String[] strs) {
// 创建一个哈希表,用于存储计数形式的单词以及对应的单词列表
Map<String, List<String>> map = new HashMap<>();
// 遍历字符串数组
for (String str : strs) {
// 统计当前单词中每个字符出现的次数,生成计数形式
int[] count = new int[26]; // 26个字母的计数数组
for (char c : str.toCharArray()) {
count[c - 'a']++; // 统计字符出现的次数
}
// 将计数形式转换为字符串,作为哈希表的键
StringBuilder keyBuilder = new StringBuilder();
for (int num : count) {
keyBuilder.append('#').append(num); // 使用#分隔数字,构造唯一的计数形式字符串
}
String key = keyBuilder.toString();
// 如果哈希表中已经存在这个计数形式的单词,则将当前单词添加到对应的列表中
if (map.containsKey(key)) {
map.get(key).add(str);
} else {
// 如果不存在,则创建一个新的列表,并将当前单词添加到列表中
List<String> newList = new ArrayList<>();
newList.add(str);
map.put(key, newList);
}
}
// 将哈希表中的值(即各个单词列表)转换为结果列表
List<List<String>> result = new ArrayList<>(map.values());
return result;
}
}
对于每个单词,无需排序只统计其字符出现的次数需要 O(k) 的时间复杂度
因此,时间复杂度为 O(nk)。
时间复杂度:O(nk)
空间复杂度:O(nk)
1.3 最长连续序列
LeetCode:128. 最长连续序列
难度:中等
题目:
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
方法:(哈希)
思路:
1)创建一个哈希表,用于存储每个数字以及它们所在连续序列的长度。
2)遍历数组,对于数组中的每个数字,检查其左右相邻的数字是否存在于哈希表中,如果存在,3)则更新当前数字所在连续序列的长度为左右相邻数字所在序列长度之和加一,并更新左右端点的序列长度。
4)在遍历过程中不断更新最长连续序列的长度。返回最终的最长连续序列的长度。
import java.util.*;
public class Solution {
/**
* 128. 最长连续序列
* 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
* 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
*
* 示例 1:
* 输入:nums = [100,4,200,1,3,2]
* 输出:4
* 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
*
* */
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// 创建哈希表,用于存储每个数字以及它们所在连续序列的长度
Map<Integer, Integer> map = new HashMap<>();
int maxLength = 0;
// 遍历数组
for (int num : nums) {
// 如果当前数字已经存在于哈希表中,则跳过
if (map.containsKey(num)) {
continue;
}
// 查找左右相邻的数字所在的连续序列长度
int left = map.getOrDefault(num - 1, 0);
int right = map.getOrDefault(num + 1, 0);
// 计算当前数字所在连续序列的长度,并更新左右端点的序列长度
int currentLength = left + right + 1;
map.put(num, currentLength);
map.put(num - left, currentLength); // 更新左端点
map.put(num + right, currentLength); // 更新右端点
// 更新最长连续序列的长度
maxLength = Math.max(maxLength, currentLength);
}
return maxLength;
}
}
这个算法的时间复杂度为 O(n),在遍历过程中,对于数组中的每个数字,我们只需要 O(1) 的时间复杂度来查找其左右相邻的数字是否存在于哈希表中
时间复杂度为 O(n)。
空间复杂度为 O(n)
2、双指针
2.1 移动零
LeetCode:283. 移动零
难度:简单
题目:
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums =[0,1,0,3,12]
输出:[1,3,12,0,0]
方法:(双指针)
思路:
1)使用两个指针 left
和 right
,初始时都指向数组的开头。
2)遍历数组,当遇到非零元素时,将其交换到 left
指针指向的位置,并将 left
指针右移。
3)遍历完成后,left
指针左侧的所有元素都是非零元素,而 left
指针右侧的所有元素都是 0。将 left
指针右侧的所有元素设置为 0 即可。
public class Solution {
/**
* 283. 移动零
* 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
*
* 示例 1:
* 输入: nums = [0,1,0,3,12]
* 输出: [1,3,12,0,0]
* */
public void moveZeroes(int[] nums) {
if (nums == null || nums.length == 0) {
return;
}
int left = 0; // 左指针,用于指向当前非零元素应该放置的位置
int right = 0; // 右指针,用于遍历数组
// 遍历数组
while (right < nums.length) {
// 如果当前元素非零,则将其交换到 left 指针指向的位置
if (nums[right] != 0) {
swap(nums, left, right);
left++; // left 指针右移
}
right++; // right 指针右移
}
// 将 left 指针右侧的所有元素设置为 0
for (int i = left; i < nums.length; i++) {
nums[i] = 0;
}
}
// 辅助函数,用于交换数组中两个元素的位置
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
这个算法只需要一次遍历数组,并且没有使用额外的空间
时间复杂度为 O(n)
空间复杂度为 O(1)
2.2 盛水最多的容器
LeetCode:11. 盛最多水的容器
难度:中等
题目:
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。
说明:你不能倾斜容器。
方法一:(双指针)
思路:
1)使用两个指针 left 和 right 分别指向数组的首尾位置。
2)计算当前左右指针所形成的容器的容量,容量等于两个指针之间的距离乘以较小的高度,即 min(height[left], height[right]) * (right - left)。
3)将左右指针中较小高度的那一条线段向中间移动,因为如果保留较大高度的那一条线段,容器的容量不会增加,而且容器的宽度减小了,因此容量只会减小。
public class Solution {
/**
* LeetCode 热题 100 (no.5)
* 11. 盛最多水的容器
*
* 给定一个长度为 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。
* */
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxArea = 0;
while (left < right) {
int minHeight = Math.min(height[left], height[right]);
int area = minHeight * (right - left);
maxArea = Math.max(maxArea, area);
// 如果下一个垂线更高,则移动指针
if (height[left] < height[right]) {
while (left < right && height[left] <= minHeight) {
left++;
}
} else {
while (left < right && height[right] <= minHeight) {
right--;
}
}
}
return maxArea;
}
}
时间复杂度为 O(n)
空间复杂度为 O(1)
2.3 三数之和
LeetCode:15. 三数之和
难度:中等
题目:
给你一个整数数组 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] 。 注意,输出的顺序和三元组的顺序并不重要。
方法一(排序+双指针)
思路:
1)首先对数组进行排序,这样可以将重复的元素放在一起,在移动双指针时,要注意去重。
2)遍历排序后的数组,固定一个数 nums[i],并使用双指针 left 和 right 分别指向 i+1 和数组的最后一个元素。
3)在固定 nums[i] 的情况下,利用双指针 left 和 right 在剩余数组中寻找和为 0 - nums[i] 的两个数。
4)如果找到了满足条件的两个数,将它们和 nums[i] 组成的三元组加入结果集中。
import java.util.*;
public class Solution {
/**
* 15. 三数之和
* 给你一个整数数组 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] 。
* 注意,输出的顺序和三元组的顺序并不重要。
* */
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
int len = nums.length;
// 首先对数组进行排序
Arrays.sort(nums);
for (int i = 0; i < len - 2; i++) {
// 如果当前数和前一个数相同,则跳过,避免重复
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int target = -nums[i]; // 目标和为 0 - nums[i]
int left = i + 1;
int right = len - 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++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
// 继续寻找下一个不重复的解
left++;
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
return result;
}
}
时间复杂度:O(n^2)
首先对数组进行排序的时间复杂度为 O(nlogn),排序后,使用双指针法遍历数组的时间复杂度为 O(n)。在双指针法中,对于每个固定的元素 nums[i],左右指针最多遍历数组一次,所以总体时间复杂度为 O(n^2)。
空间复杂度:O(logn)
2.4 接雨水
LeetCode:42. 接雨水
难度:困难
题目:给定 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 个单位的雨水(蓝色部分表示雨水)。
方法一:(双指针)
思路:
1)首先遍历数组,找到最高的柱子,并记录其索引。这个柱子将数组分为左右两部分。
2)从左右两侧向最高柱子方向遍历数组,同时维护两个指针 left 和 right。
3)在遍历过程中,维护两个变量 left_max 和 right_max,分别表示左侧和右侧已经遍历过的柱子的最大高度。
4)对于当前柱子,可以计算当前柱子上的水量为 min(left_max, right_max) - height[i](左侧和右侧最大高度的较小值减去当前柱子的高度。)
5)将当前柱子上的水量累加到总水量中,并更新 left_max 和 right_max。
public class Solution {
/**
* 42. 接雨水
*给定 n 个非负整数表示每个宽度为 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 个单位的雨水(蓝色部分表示雨水)。
* @param height
* @return
*/
public int trap(int[] height) {
// 如果输入为空或者数组长度小于等于2,无法形成容器,返回0
if (height == null || height.length <= 2) {
return 0;
}
int n = height.length;
int left = 0, right = n - 1; // 左右指针,分别指向数组的两端
int left_max = 0, right_max = 0; // 左右最大高度
int total_water = 0; // 总水量
// 使用双指针法,左右指针向中间遍历
while (left < right) {
if (height[left] < height[right]) { // 如果左侧高度小于右侧
if (height[left] >= left_max) { // 如果当前柱子高度大于左侧最大高度
left_max = height[left]; // 更新左侧最大高度
} else {
total_water += left_max - height[left]; // 计算当前柱子上的水量并累加到总水量中
}
left++; // 左指针右移
} else { // 如果右侧高度小于或等于左侧
if (height[right] >= right_max) { // 如果当前柱子高度大于右侧最大高度
right_max = height[right]; // 更新右侧最大高度
} else {
total_water += right_max - height[right]; // 计算当前柱子上的水量并累加到总水量中
}
right--; // 右指针左移
}
}
return total_water; // 返回总水量
}
}
算法只需要一次遍历数组,并且只使用了常数级别的额外空间
时间复杂度为 O(n)
空间复杂度为 O(1)。
方法二:(动态规划)
这道题目还可以使用动态规划来完成
思路:
1)计算左边最高柱子高度数组 leftMaxHeight
:
遍历数组 height
,从左往右,对于每个位置 i
,leftMaxHeight[i]
表示从数组起始位置到当前位置 i
的最高柱子高度。这个过程使用动态规划来计算,leftMaxHeight[i]
的值取决于 leftMaxHeight[i-1]
和 height[i]
的较大值,即 leftMaxHeight[i] = max(leftMaxHeight[i-1], height[i])
。
2)计算右边最高柱子高度数组 rightMaxHeight
:
同样地,遍历数组 height
,从右往左,对于每个位置 i
,rightMaxHeight[i]
表示从数组末尾位置到当前位置 i
的最高柱子高度。这个过程同样使用动态规划来计算,rightMaxHeight[i]
的值取决于 rightMaxHeight[i+1]
和 height[i]
的较大值,即 rightMaxHeight[i] = max(rightMaxHeight[i+1], height[i])
。
3)遍历整个数组 height
,对于每个位置 i
,计算当前位置的盛水量。盛水量等于 min(rightMaxHeight[i], leftMaxHeight[i]) - height[i]
,即当前位置左右两侧最大柱子高度的较小值减去当前柱子高度。累加所有位置的盛水量,即得到最终的总盛水量 maxArea
。
public class Solution {
/**
* 42. 接雨水
*给定 n 个非负整数表示每个宽度为 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 个单位的雨水(蓝色部分表示雨水)。
* @param height
* @return
*/
public int trap(int[] height) {
int len=height.length;
if(len==0)
return 0;
// 左边最高柱子
int[] leftMaxHeight=new int[len];
leftMaxHeight[0]=height[0];
for (int i = 1; i <len ; i++) {
leftMaxHeight[i]=Math.max(leftMaxHeight[i-1],height[i]);
}
// 右边最高柱子
int[] rightMaxHeight=new int[len];
rightMaxHeight[len-1]=height[len-1];
for (int i = len-2; i>=0 ; i--) {
rightMaxHeight[i]=Math.max(rightMaxHeight[i+1],height[i]);
}
int maxArea=0;
for (int i = 0; i < len; i++) {
maxArea+=Math.min(rightMaxHeight[i],leftMaxHeight[i])-height[i];
}
return maxArea;
}
public static void main(String[] args) {
Solution solution=new Solution();
int[] height ={0,1,0,2,1,0,1,3,2,1,2,1};
int trap = solution.trap(height);
System.out.println(trap);
}
}
因为需要遍历数组两次来计算左右最高柱子高度数组,需要额外的空间来存储左右最高柱子高度数组。
时间复杂度为 O(n)
空间复杂度为 O(n),