前言
记录 LeetCode 刷题时遇到的哈希表相关题目,第一篇
242.有效的字母异位词
能用数组作为哈希表就不用 map,否则就是浪费
/*直接用数组作为哈希表,不使用map,不过使用了两个数组分别记录s和t
public boolean isAnagram(String s, String t) {
char[] record = new char[26];
char[] record2 = new char[26];
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i) - 'a']++;
}
for (int i = 0; i < t.length(); i++) {
record2[t.charAt(i) - 'a']++;
}
for (int i = 0; i < 26; i++) {
if(record[i] != record2[i]){
return false;
}
}
return true;
}*/
/*====既然是两个字符串进行匹配,所以就用一个数组来记录s中的字符个数====
======情况,然后遍历t的时候让数组中对应字符个数减一即可====
=====最后看数组中有没有不是0的就行====*/
public boolean isAnagram(String s, String t) {
char[] record = new char[26];
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i) - 'a']++;
}
for (int i = 0; i < t.length(); i++) {
record[t.charAt(i) - 'a']--;
}
for (int i = 0; i < 26; i++) {
if(record[i] != 0){
return false;
}
}
return true;
}
49.字母异位词分组
//首先要找到什么值作为哈希表的键值,这里以排序后的字符串作为键值,
//因为=========字母异位词排序后长得肯定是一样的=========
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String, List<String>> map = new HashMap<>();
for(String str : strs){
//将字符串放到另一个字符数组中才不会破坏原字符串
char[] chars = str.toCharArray();
//使用Arrays工具类中的sort()方法排序
Arrays.sort(chars);
//使用传入字符数组的String()构造函数生成排序后的字符串
String key = new String(chars);
List<String> list = map.getOrDefault(key, new ArrayList<>());
list.add(str);
map.put(key,list);
}
Collection<List<String>> lists = map.values();
//可以构造函数传入一个Collection集合来构造list
ArrayList<List<String>> lists1 = new ArrayList<>(lists);
return lists1;
}
438.找到字符串中所有字母异位词 - 哈希表+滑动窗口
没有充分使用滑动窗口的做法:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
ArrayList<Integer> result = new ArrayList<>();
int lengthS = s.length();
int lengthP = p.length();
int i;
for(i = 0;;i++){
int temp = i + lengthP - 1;
//p的长度小于s才能找到子串,否则直接结束算法
if(temp < lengthS){
String substring = s.substring(i, temp + 1);
//每次截取与p长度相同的子串与p比较是否异位
if(isAnagram(substring, p)){
result.add(i);
//设计下面这个循环的目的是:当前遍历到了一个符合条件的子串,
//于是马上右移一位,判断下一个子串是否符合条件,
//因为只需要判断被减去的最左边那个字符跟新加入的最右边那个字符是否相等即可
while (true){
i += 1;
temp = i + lengthP - 1;
if(temp < lengthS && s.charAt(temp) == s.charAt(i - 1)){
result.add(i);
}else {
break;
}
}
}
}else {
break;
}
}
return result;
}
//判断异位的方法,即 242.有效的字母异位词 的方法
public boolean isAnagram(String s, String t) {
int lengthT = t.length();
char[] record = new char[26];
for (int i = 0; i < lengthT; i++) {
record[s.charAt(i) - 'a']++;
record[t.charAt(i) - 'a']--;
}
for (int i = 0; i < 26; i++) {
if(record[i] != 0){
return false;
}
}
return true;
}
}
上面的算法中调用 isAnagram() 方法次数较多,而且每次调用是对当前遍历到的 s 子串以及 p 从头遍历一遍计算字符个数,非常浪费时间。其实从整个 for 循环可以感觉到跟滑动窗口其实是一样的,只不过 没有做到的是 在判断当前遍历到的子串与 p 是否异位时,上一个子串的字符个数统计是可以利用的,而不用重新遍历整个子串来得到
public List<Integer> findAnagrams(String s, String p) {
int sLength = s.length();
int pLength = p.length();
List<Integer> res = new ArrayList<>();
if(sLength < pLength) return res;
int[] pHash = new int[26];
int[] sHash = new int[26];
//初始化一个记录
for(int i = 0; i < pLength; i++){
pHash[p.charAt(i) - 'a']++;
sHash[s.charAt(i) - 'a']++;
}
//判断数组内容是否相同,是则该子串与p异位
if(Arrays.equals(sHash, pHash)){
res.add(0);
}
//窗口向右移动一位,减掉最左边的字符,加上最右边的字符。i指向窗口的右边界
for(int i = pLength; i < sLength; i++){
sHash[s.charAt(i - pLength) - 'a']--; //减掉最左边字符
sHash[s.charAt(i) - 'a']++; //加上最右边字符
if(Arrays.equals(sHash, pHash)){ //判断当前窗口所代表的子串是否和p异位
res.add(i - pLength + 1); //异位的话添加该子串的首下标
}
}
return res;
}
383.赎金信
//与242基本上是一样的,只不过magazine中的每种字符的数量可以是多于ransomNote的
public boolean canConstruct(String ransomNote, String magazine) {
char[] record = new char[26];
for (int i = 0; i < magazine.length(); i++) {
record[magazine.charAt(i) - 'a']++;
}
for (int i = 0; i < ransomNote.length(); i++) {
if (record[ransomNote.charAt(i) - 'a'] == 0){
return false;
}
record[ransomNote.charAt(i) - 'a']--;
}
return true;
}
349.两个数组的交集
数值是离散的,而且范围较大,用数组来做哈希表可能会造成空间的极大浪费,而且遍历数组时会有大部分时间用在访问无效的 bucket
根据 Set (add 了 Set 中已有元素时,不会再次 add) 的性质,本题用 Set 最合适
public int[] intersection(int[] nums1, int[] nums2) {
HashSet<Integer> set = new HashSet<>();
HashSet<Integer> set1 = new HashSet<>();
for (int i : nums1) {
set.add(i);
}
for (int i : nums2) {
if(set.contains(i)){
set1.add(i);
}
}
int[] ints = new int[set1.size()];
int index = -1;
for (Integer integer : set1) {
ints[++index] = integer;
}
return ints;
}
350.两个数组的交集 II
public int[] intersect(int[] nums1, int[] nums2) {
HashMap<Integer, Integer> map = new HashMap<>();
//由于不知道交集的数量的多少,可以先设置为一个较大的大小。
//交集大小肯定不会超过num1跟num2的大小,这里设为num1的大小
int[] result = new int[nums1.length];
int index = -1;
for (int i : nums1) {
map.put(i,map.getOrDefault(i,0) + 1);
}
for (int i : nums2) {
//如果获取不到,说明第一个数组中没有这个值,让tem等于-1,不做处理
Integer tem = map.getOrDefault(i, -1);
//tem大于0,说明第一个数组中也有这个值,所以把i放入结果数组,同时使map中i对应的个数减一
if(tem > 0){
result[++index] = i;
map.put(i,--tem);
//如果tem为0,可以直接移除掉这个数的记录
}else if(tem == 0){
map.remove(i);
}
}
return Arrays.copyOfRange(result, 0, index + 1);
}
202.快乐数
题目说的 “也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数”,这个运算过程既然会无限循环,那么就是有数会重复出现,重复计算,所以判断有没有数第二次出现就可以了
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
while (n != 1 && !record.contains(n)) {
record.add(n);
n = getNextNumber(n);
}
//运算终止条件为得到1或者遇到重复的数,只要此时n为1就符合题意
return n == 1;
}
private int getNextNumber(int n) {
int res = 0;
while (n > 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
}
128.最长连续序列
使用枚举的话,思路是枚举数组中的每个元素 num,然后依次判断 num + 1,num + 2,…,num + k 是否在数组中,同时记录连续序列的长度。枚举完所有元素,所有连续序列长度中的最大值就是答案
优化的点在于,假设我们已经知道从 num 开始的连续序列的长度,那就没有必要再算以 num + 1 开始的连续序列的长度了,因为以 num + 1 开始的连续序列的长度肯定小于从 num 开始的连续序列的长度。也就是说,对于当前枚举到的元素 num,要先判断 num - 1 是否在数组中,不在才进行计算后续连续序列长度的操作
所以,对于数组中每一个连续序列,只有序列中的第一个元素才需要 “进行计算后续连续序列长度的操作”。代码:
public int longestConsecutive(int[] nums) {
Set<Integer> numSet = new HashSet<Integer>();
//根据用例,相同的元素不计入连续序列中,所以直接用Set存储,提高后续的查找效率
for (int num : nums) {
numSet.add(num);
}
int max = 0;
for (int num : numSet) {
if (!numSet.contains(num - 1)) {
int currentNum = num;
int currentLen = 1;
while (numSet.contains(++currentNum)) {
currentLen += 1;
}
max = Math.max(max, currentLen);
}
}
return max;
}