前提知识
题目速览
242. 有效的字母异位词
349. 两个数组的交集
202. 快乐数
1. 两数之和
454. 四数相加 II
383. 赎金信
15. 三数之和
18. 四数之和
题目详解
242. 有效的字母异位词
整体思路
法一:遍历两个字符串,建立两个哈希表,存进去每一个字符的个数,一次比较。很慢
public static boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
HashMap<Character, Integer> map1 = new HashMap<>();
HashMap<Character, Integer> map2 = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
map1.put(s.charAt(i), map1.getOrDefault(s.charAt(i), 0) + 1);
map2.put(t.charAt(i), map2.getOrDefault(t.charAt(i), 0) + 1);
}
for (Character a : map1.keySet()){
if (!map1.get(a).equals(map2.get(a))){
return false;
}
}
return true;
}
但是上面的时间复杂度和空间复杂度都很高,就解决空间复杂度来讲,可以只用一个map,当在字符串1中遇到字符时就加1,当在字符串2中遇到字符时就减1。
public static boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
HashMap<Character, Integer> map1 = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
map1.put(s.charAt(i), map1.getOrDefault(s.charAt(i), 0) + 1);
map1.put(t.charAt(i), map1.getOrDefault(t.charAt(i), 0) - 1);
}
for (Character a : map1.keySet()){
if (!map1.get(a).equals(0)){
return false;
}
}
return true;
}
这样就会减少空间复杂度。
法二:答案是用数组做的,因为哈希表查询是近似O(1),还有自己内部调整的时间,而数组是标准的O(1),且全是小写字母,只用26个字节就可以。
class Solution {
public boolean isAnagram(String s, String t) {
int[] record = new int[26];
for (char c : s.toCharArray()) {
record[c - 'a'] += 1;
}
for (char c : t.toCharArray()) {
record[c - 'a'] -= 1;
}
for (int i : record) {
if (i != 0) {
return false;
}
}
return true;
}
}
注意:
经过自己的实测,转化成s.toCharArray,比直接chatAt()更快
349. 两个数组的交集
整体思路
用一个哈希表去存一个数组中的字符,遍历另一个数组,如有在哈希表中存在就存入set集合中,除去重复
ps:下面的版本其实map直接用set集合就可以
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
HashMap<Integer, Integer> map1 = new HashMap<>();
for (int i : nums1){
map1.put(i, map1.getOrDefault(i, 0) + 1);
}
Set<Integer> set = new HashSet<>();
for (int i : nums2){
if (map1.containsKey(i)){
set.add(i);
}
}
int[] res = new int[set.size()];
int count = 0;
for (int i : set){
res[count] = i;
count++;
}
return res;
}
}
202. 快乐数
基本思路:
1.建立一个函数随时更新n
2.建立一个无重复的Set集合,如果n在里面第二次出现,就是进入了死循环
代码:
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<>();
while (n != 1 && !set.contains(n)){
set.add(n);
n = getNewNumber(n);
}
return n == 1;
}
public int getNewNumber(int n){
int res= 0;
while (n > 0){
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
1. 两数之和
基本思路:
法一:遍历
法二:自己做得时候用了两次for循环,一次是保存值与索引进入map,一次是遍历数组,查找map中是不是有对应的target - 这个值。但是其实用一次循环就可以了,因为后期如果满足条件的话,target-后期的值已经提前存进去了
public static int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> map = new HashMap<>();
int res[] = new int[2];
for (int i = 0; i < nums.length; i++){
map.put(nums[i],i);
}
for (int i = 0; i < nums.length; i++){
if (map.containsKey(target - nums[i]) && i != map.get(target - nums[i])){
res[0] = i;
res[1] = map.get(target - nums[i]);
break;
}
}
return res;
}
法三:用一次循环就可以了,因为后期如果满足条件的话,target-后期的值已经提前存进去了
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> map = new HashMap<>();
int res[] = new int[2];
for (int i = 0; i < nums.length; i++){
if (map.containsKey(target - nums[i])){
res[0] = i;
res[1] = map.get(target - nums[i]);
break;
}
map.put(nums[i], i);
}
return res;
}
454. 四数相加 II
基本思路:
- 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
- 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
- 定义int变量count,用来统计a+b+c+d = 0 出现的次数。
- 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
- 最后返回统计值 count 就可以了
代码:
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
//记录前两个数组a+b之和,和这个和出现的次数
Map<Integer, Integer> map = new HashMap<>();
int res = 0;
int n = nums1.length;
for (int a : nums1){
for (int b : nums2){
map.put((a + b), map.getOrDefault((a + b), 0) + 1);
}
}
//在后两个数组中c+d计算和,看是不是和前两个数组a+b和相加符合题意
for (int c : nums3) {
for (int d : nums4) {
if (map.containsKey(0 - (c +d))){
res += map.get(0 - (c + d));
}
}
}
return res;
}
383. 赎金信
基本思路:
- 如果ran的长度比mag大,则直接返回false
- 因为是小写字母,所以可以直接使用数组
- 记录在mag中每个字母出现的次数,对应的数组位置++
- 遍历ran,遇到对应的数组位置–
- 如果有一个位置的值小于0则返回false
代码:
public static boolean canConstruct(String ransomNote, String magazine) {
int len1 = ransomNote.length();
int len2 = magazine.length();
if (len1 > len2) return false;
int[] find = new int[26];
char[] mag = magazine.toCharArray();
char[] ran = ransomNote.toCharArray();
for (char a : mag){
find[a - 'a']++;
}
for (char b : ran){
find[b - 'a']--;
if (find[b - 'a'] < 0){
return false;
}
}
return true;
}
15. 三数之和
基本思路:
其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。
而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。
使用另一个解法:双指针法,这道题目使用双指针法 要比哈希法高效一些
-
拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
-
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i] b = nums[left] c = nums[right]。
-
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
-
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
代码:
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++){
//因为已经排好序了,当开始的时候就大于0,直接可以返回现在的结果
if (nums[i] > 0) return res;
//去重,不要nums[i] == nums[i + 1],会遗掉-1,-1,2
if (i > 0 && nums[i] == nums[i - 1]){
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left){
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0){
right--;
}else if (sum < 0){
left++;
}else {
//先加入,再去重
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
//去重,遇到原来相同的值移动
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return res;
}
18. 四数之和
基本思路:
四数之和,和15.三数之和 (opens new window)是一个思路,都是使用双指针法, 基本解法就是在15.三数之和 (opens new window)的基础上再套一层for循环。
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下表作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),
四数之和的时间复杂度是O(n^3) 。
那么一样的道理,五数之和、六数之和等等都采用这种解法。
代码:
public static List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
//下面这个需要取消,比如遇到 -5 -2 -1 target 为 -8 就会一上来就否了,
//主要原因是target可能是负值,正数越加越大,负数越加越小
/*
if (nums[i] > target) {
System.out.println(nums[i]);
return res;
}
*/
//去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < nums.length; j++) {
//去重
if (j > i + 1 && nums[j] == nums[j - 1]){
continue;
}
int left = j + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
//先加入,再去重
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
//去重,遇到原来相同的值移动
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
}
return res;
}
更进一步,充分的剪枝,时间更快
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> quadruplets = new ArrayList<List<Integer>>();
if (nums == null || nums.length < 4) {
return quadruplets;
}
Arrays.sort(nums);
int length = nums.length;
for (int i = 0; i < length - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
break;
}
if ((long) nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) {
continue;
}
for (int j = i + 1; j < length - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}
if ((long) nums[i] + nums[j] + nums[length - 2] + nums[length - 1] < target) {
continue;
}
int left = j + 1, right = length - 1;
while (left < right) {
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
quadruplets.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++;
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
}
return quadruplets;
}
}
总结
数组作为哈希表:
题目只包含小写字母,那么使用数组来做哈希最合适不过。而这种情况使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效。
set作为哈希表:
数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
map作为哈希表:
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而例如两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
map是一种<key, value>的结构,可以用key保存数值,用value在保存数值所在的下表。所以使用map最为合适。