LeetCode 热题 100(Top-100-Liked)
1.哈希(Hash)
1.两数之和(two-sum)
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
class Solution {
public int[] twoSum(int[] nums, int target) {
// int i;
// for(i=0;i<nums.length;i++){
// for(int j=i+1;j<nums.length;j++){
// if(nums[i]+nums[j]==target){
// return new int[]{i,j};
// }
// }
// }
// return new int[]{0,0};
// HashMap类本身implements抽象接口Map
// 法一:创建了一个HashMap对象,实现了Map接口,可使用map中规定的方法和自有的方法
// Map<Integer, Integer> hashmap = new HashMap<Integer, Integer>();
// 法二
HashMap<Integer, Integer> hashmap = new HashMap<Integer, Integer>();
// 错解:HashMap hashmap = new HashMap();
// 错因:没有使用泛型参数,也就是没有指定具体的键和值的类型。这种情况下,编译器将 HashMap 对象中的键和值都作为 Object 类型处理。因为 Object 类型没有定义 get() 方法,所以不能直接使用 hashmap.get(key) 方法来获取值
for(int i=0; i<nums.length; i++){
if(hashmap.containsKey(target-nums[i])){
return new int[]{i, hashmap.get(target - nums[i])}; // get()在哈希表中根据键获取对应值
}
hashmap.put(nums[i], i); // 以数值为key,索引为value
}
return new int[0];
/**
* Map map = new HashMap();和HashMap map = new HashMap();在使用上有什么区别?
1. 类型不同
- Map map声明的变量类型是Map接口
- HashMap map声明的变量类型是HashMap类
2. 可访问的方法不同
- Map map只能调用Map接口中的方法
- HashMap map既可以调用Map接口中的方法,也可以调用HashMap类特有的方法
3. 可插入的元素类型不同
- Map map可以插入实现Map接口的任意键值对
- HashMap map只能插入实现HashMap要求的键值对类型
4. 具体实现不同
- Map map可以指向任意的Map实现类对象
- HashMap map只能指向HashMap对象
5. 扩展性不同
- Map map扩展性更好,可以指向不同的Map实现类
- HashMap map扩展性较差,只能指向HashMap
所以,如果只需要Map接口中的基本功能,则建议使用Map map,更灵活;如果需要HashMap特有的功能,则需要使用HashMap map。
*/
}
}
49.字母异位词分组(group-anagrams)
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
class Solution {
// 由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
char[] array = str.toCharArray(); // str字符串转为字符数组
Arrays.sort(array); // 排序
String key = new String(array); // 将排序后的数组元素连接为字符串,以便作为哈希表的键
// System.out.println(key);
// 根据键获取列表,如果不存在则创建新列表
if (map.containsKey(key)){
List<String> list = map.get(key); // 获取key对应的字符串列表
list.add(str); // 更新
map.put(key, list); // 把更新后的字符串列表存入哈希表
}
else{
List<String> list = new ArrayList<>();
list.add(str);
map.put(key, list);
}
System.out.println(map.values());
}
// 返回映射表中的所有值
return new ArrayList<List<String>>(map.values()); // 必须符合返回类型
// 与HashMap和Map相似:ArrayList是List的一个实现类,而List是一个接口。List只定义了一些列方法,ArrayList实现了这些方法。如果只需要List接口中的基本功能,则建议使用List list,更灵活;如果需要ArrayList特有的功能,则需要使用HashMap map。
/**
* Map map = new HashMap();和HashMap map = new HashMap();在使用上有什么区别?
1. 类型不同
- Map map声明的变量类型是Map接口
- HashMap map声明的变量类型是HashMap类
2. 可访问的方法不同
- Map map只能调用Map接口中的方法
- HashMap map既可以调用Map接口中的方法,也可以调用HashMap类特有的方法
3. 可插入的元素类型不同
- Map map可以插入实现Map接口的任意键值对
- HashMap map只能插入实现HashMap要求的键值对类型
4. 具体实现不同
- Map map可以指向任意的Map实现类对象
- HashMap map只能指向HashMap对象
5. 扩展性不同
- Map map扩展性更好,可以指向不同的Map实现类
- HashMap map扩展性较差,只能指向HashMap
所以,如果只需要Map接口中的基本功能,则建议使用Map map,更灵活;如果需要HashMap特有的功能,则需要使用HashMap map。
*/
}
}
128.最长连续序列(longest-consecutive-sequence)
给定一个未排序的整数数组 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
2.双指针(Two Pointers)
283.移动零(move-zeroes)
给定一个数组 nums
,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
// cjk:
// class Solution {
// public void moveZeroes(int[] nums) {
// // 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
// // 请注意 ,必须在不复制数组的情况下原地对数组进行操作。
// // idea:双指针遍历
// int i = 0; // 指向当前应该判断的元素,i之前均为非0
// int j = nums.length; // 指向应该移动的最后一个元素,j之后均为0
// while(i!=j-1){ // i==j-1则说明遍历完成
// if(nums[i]==0){
// // j之前的元素开始移动
// for(int k=i; k<j-1; k++){
// nums[k] = nums[k+1];
// }
// nums[--j] = 0; // j元素赋值为0
// }
// else{
// // nums[i]非0则继续移动指针
// i++;
// }
// }
// }
// }
/**
使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。
右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
注意到以下性质:左指针左边均为非零数;右指针左边直到左指针处均为零。
因此每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变。
*/
class Solution {
public void moveZeroes(int[] nums) {
int n = nums.length, left = 0, right = 0;
while (right < n) {
if (nums[right] != 0) {
// 左右指针对应的数交换:由于是待处理序列的头部和已处理序列的尾部交换,交换后右指针后移,将下一个非0值交换至尾部
swap(nums, left, right);
left++;
}
right++;
}
}
public void swap(int[] nums, int left, int right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
14.贪婪算法(Greedy Algorithm)
121.买卖股票的最佳时机(best-time-to-buy-and-sell-stock)
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
class Solution {
public int maxProfit(int[] prices) {
// 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
// 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
// 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
int pre_min = prices[0]; // 先前的最小价格
int max = 0; // 最大利润
for (int i=1; i<prices.length; i++){
if (prices[i]<=pre_min){
// 比先前的最小价格低,更新最低价格,然后跳过
pre_min = prices[i];
continue;
}
else{
if (prices[i]-pre_min>max){
max = prices[i] - pre_min;
}
}
}
return max;
}
}
55.跳跃游戏(jump-game)
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
public class Solution {
public boolean canJump(int[] nums) {
int rightmost = 0; // 最远可以到达的位置
for (int i = 0; i < nums.length; ++i) {
if (i <= rightmost) {
rightmost = Math.max(rightmost, i + nums[i]); // 更新最远可以到达的位置
if (rightmost >= nums.length - 1) {
return true;
}
}
else{ // i超出最远可以到达的位置,必失败
return false;
}
}
return false;
}
}
45.跳跃游戏(Ⅱ)(jump-game-ii)
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。
每个元素 nums[i]
表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
class Solution {
// 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
// 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
// 0 <= j <= nums[i]
// i + j < n
// 返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
// 终态法:不需要记录跳到最远位置时经过了哪些中间点,每次在上次能跳到的范围内选择一个能跳的最远的位置作为下次的起跳点
public int jump(int[] nums) {
int max_far = 0;// 目前能跳到的最远位置
int step = 0; // 跳跃次数
int end = 0; // 上次跳跃可达范围右边界(下次的最右起跳点)
for (int i = 0; i < nums.length-1; i++)
{
max_far = Math.max(max_far, i + nums[i]);
// 到达上次跳跃能到达的右边界
if (i == end)
{
end = max_far; // 目前能跳到的最远位置变成了下次起跳位置的有边界
step++; // 进入下一次跳跃
}
}
return step;
}
}
763.划分字母区间(partition-labels)
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = "eccbbbbdec"
输出:[10]
class Solution {
public List<Integer> partitionLabels(String s) {
String[] s_array = s.split(""); // 把字符串转为字符串数组
// idea: 遍历字符串,存储各字母最先/最后出现的位置,然后再次遍历字符串,当i位置及之前的字符串字母的最后位置小于i+1位置的字母的最小位置时,说明此位置为应该分段的地方
// 存储各字母最先出现的位置
Map<String, Integer> hashmap_first = new HashMap<>();
for (int i = 0; i<s_array.length; i++) {
// hashmap_first存储各字母最先出现的位置
if (!hashmap_first.containsKey(s_array[i])) {
hashmap_first.put(s_array[i], i);
}
}
// 存储各字母最后出现的位置
Map<String, Integer> hashmap_last = new HashMap<>();
for (int i = s_array.length - 1; i >= 0; i--) {
// hashmap_last存储各字母最后出现的位置
if (!hashmap_last.containsKey(s_array[i])) {
hashmap_last.put(s_array[i], i);
}
}
// 当i位置及之前的字符串字母的最后位置小于i+1位置的字母的最小位置时,说明此位置为应该分段的地方
// a的last小于d的first -> [i] < [i+1]
int min = 0; // min是当前分段的开始索引
int max = 0; // max是当前分段的结束索引
List<Integer> location = new ArrayList<>(); // 存储分段位置
for (int i = 0; i < s_array.length - 1; i++) {
max = Math.max(max, hashmap_last.get(s_array[i])); // 更新max
// 分段
if (max < hashmap_first.get(s_array[i + 1])) {
System.out.println(max);
System.out.println(s_array[i + 1]);
// System.out.println(i);
location.add(max - min + 1); // 添加长度
min = i + 1;
max = i + 1;
}
}
location.add(s_array.length - min);
return location;
}
}