今天的题涉及到了集合 分别用数组 set 和map 对集合很生疏 要时刻记住一旦出现判断一个元素是否出现过 就要想到用哈希
集合的一些基本知识
这里推荐看以下博客 讲的很全
List:List就是数组啊 链表那些 主要记住有序 可以重复
Set:集合无序 不允许重复 通过计算添加元素的hashcode来判断存储位置
Set的常用方法包括
add()
用来添加元素、remove()
用来删除元素、contains()
用来判断是否包含某个元素、isEmpty()
用来判断集合是否为空、size()
用来获取集合中元素的数量等等。
Map:存储键值对 key-value
添加元素:使用
put()
方法向Map中添加元素,语法:map.put(key, value)
,其中key
为键对象,value
为值对象。获取元素:使用
get()
方法通过键来获取对应的值,语法:map.get(key)
,其中key为键对象。
获取所有的key用keySet()方法 会返回一个set
删除元素:使用remove()
方法通过键来删除对应的键值对,语法:map.remove(key)
,其中key为键对象。判断是否包含键或值:使用
containsKey()
、containsValue()
方法来判断Map中是否包含指定的键或值,语法:map.containsKey(key)
或map.containsValue(value)
,其中key为键对象,value为值对象。
这里要再强调一下 集合中的泛型里面只能添加对象或者包装类 也就是里面只能放引用数据类型(Integer等)
有效的字母异位词
若 s
和 t
中每个字符出现的次数都相同,则称 s
和 t
互为字母异位词。
思路
由于一共我们只有26个字母,用数组来作为存储出现的次数是很快捷的方式,下标就按字母在在字母表的顺序
定义一个数组叫做record用来上记录字符串s里字符出现的次数。
需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
代码
class Solution {
public boolean isAnagram(String s, String t) {
int[] count = new int[26];
for (int j = 0; j < s.length(); j++) {
count[s.charAt(j) - 'a']++; //charAt方法是根据给的索引返回对应的char
}
for (int j = 0; j < t.length(); j++) {
count[t.charAt(j) - 'a']--;
}
for (int cnt : count) {
if (cnt != 0) { //只要不是0 是正数或者负数一定代表他们不是异位词
return false;
}
}
return true;
}
}
由于本人写题是忘了foreach迭代 这里再介绍一下:
for-each循环常用于遍历数组或集合, 格式是for(元素类型 变量名 : 数组或集合) 其中,元素类型代表数组或集合中元素的类型,变量名代表每次迭代获取到的当前元素。 例如Set(String) 那就是for(String str : Set) 能直接获取到里面的元素
总结
这道题不难想到用数组来存放次数 关于集合用到数组一般较为容易 常用于数组的下表非常好确定的情况
两个数组的交集
本题使用set 可以想象数学中的集合 不允许重复
思路
首先找交集 是不是还是相当于找一个元素是不是在别的集合中出现 所以还是用哈希 其次为什么要用set 是因为题目要求没有重复元素 因此正好我们可以利用set的去重特性
代码
import java.util.HashSet;
import java.util.Set;
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
return new int[0];
}
Set<Integer> set1 = new HashSet<>();
Set<Integer> resSet = new HashSet<>();
//遍历数组1
for (int i : nums1) {
set1.add(i);
}
//遍历数组2的过程中判断哈希表中是否存在该元素
for (int i : nums2) {
if (set1.contains(i)) {
resSet.add(i);
}
}
//方法1:将结果集合转为数组
return resSet.stream().mapToInt(x -> x).toArray();
//方法2:另外申请一个数组存放setRes中的元素,最后返回数组
int[] arr = new int[resSet.size()];
int j = 0;
for(int i : resSet){
arr[j++] = i;
}
return arr;
}
}
关于set和数组
set和数组各自都有什么优缺点呢
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
HashSet
HashSet
是基于哈希表的Set
实现,其内部实际上是使用HashMap
来存储元素的。
优点
- 处理冲突:
HashSet
(通过HashMap
)有效地处理哈希冲突,使用链表或红黑树来存储相同哈希值的元素。 - 常数时间操作:理想情况下,
HashSet
提供常数时间的添加、删除、包含等操作性能,这使得它非常适合需要快速查找的场景。 - 动态扩容:
HashSet
可以根据元素的增加自动扩容,无需手动管理底层数据结构的大小。
缺点
- 空间效率:由于使用了额外的数据结构(如链表或红黑树),以及为了减少哈希冲突而采取的扩容机制,
HashSet
可能会使用比纯数组更多的内存。 - 性能下降:在极端情况下(如大量元素具有相同的哈希值时),
HashSet
的性能可能会接近O(n)
。
基于数组的哈希表
在一些简单的情况下,我们可能会直接使用数组来实现哈希表,通过计算元素哈希值的模运算确定其在数组中的位置。
优点
- 内存效率:直接使用数组作为底层数据结构,可能比
HashSet
更节省内存,特别是在元素数量较少时。 - 简单直接:对于简单的用途,基于数组的哈希表实现起来较为直接和简单,容易理解。
缺点
- 处理冲突:基于数组的哈希表需要自己实现冲突处理机制,如开放寻址或链表法,这可能增加实现的复杂性。
- 固定大小:数组的大小是固定的,这意味着哈希表的容量是有限的,一旦达到容量限制,性能会急剧下降,且需要手动或通过复杂的逻辑来实现扩容。
- 性能变化:当哈希表填满时,性能会下降,查找、插入和删除操作的时间复杂度可能增加到
O(n)
,特别是在没有有效冲突解决机制的情况下。
总之,HashSet
提供了一个高度优化且易于使用的哈希表实现,适用于大多数需要使用集合的场景,特别是当元素的添加、删除和查找操作频繁时。而基于数组的哈希表可能在某些特定场景下更有优势,尤其是在元素数量较少、内存使用受限或者可以容忍简单冲突处理机制时。在选择使用哪种实现时,应考虑具体的应用需求和限制。
快乐数
思路
这道题最开始做的时候 就不知道应该什么时候结束循环 后来才明白原来题目说的无限循环指的是sum会重复出现 这样才会无限循环 那一看到这个出现二字 很容易想到又要用哈希表了 这里我们选择用HashSet避免重复
代码
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
//n等于1 证明是快乐数
while (n != 1 && !record.contains(n)) {//遇到重复的就退出 肯定不是快乐数
record.add(n);
n = getNextNumber(n);
}
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;
}
}
两数之和
思路
本质上还是找元素是否在集合中出现 遍历数组 那么target减去该元素的值就是我们要找的数 如果集合中有 那证明可以找到
因为题目要求返回下标 用map是最合适的 返回key即可
代码
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = new int[2];
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])) {
result[0] = i;
result[1] = map.get(target - nums[i]);
}
map.put(nums[i], i);
}
return result;
}
}
最开始我想的是一趟遍历把元素先都加到map里 然后再进行第二次遍历 这样的时间耗时很显然没有上述代码好 上述代码是边添加边判断 而且同样能把所有元素的组合给判断一次 每一个map中的元素 都和他之前的进行匹配