Day05
242.有效的字母异位词
哈希表简介
哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表)
数组就是一张哈希表,哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素
一般哈希表都是用来快速判断一个元素是否出现集合里
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
![](https://i-blog.csdnimg.cn/blog_migrate/16a640b7c2c62d5f0657262aa8bb22b5.png)
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
接下来哈希碰撞登场
![](https://i-blog.csdnimg.cn/blog_migrate/2d52e7395cebf03bc3d1240036487565.png)
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。
哈希法牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
力扣题目链接
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
思路
首先想到哈希表,逻辑是这样的:
使用一个数组arr来接收字符出现的次数
可以先遍历第一个字符串,看每个位置的字符,把数组对应元素下标加一
遍历第二个字符串,把数组对应元素下标减一
遍历数组,看元素是否都为0,只要有一个位置不是0,就返回false
技巧在于,不需要记忆下来每个字母的ASCII码,下标是与字母a的差即可
时间复杂度O(n),空间复杂度O(26)
其实也可以排序后直接比较字符串大小,相等才行!
时间复杂度O(nlogn),
排序的时间复杂度为 O(nlogn),比较两个字符串是否相等时间复杂度为 O(n),因此总体时间复杂度为 O(nlog n+n)=O(nlogn)。
排序需要 O(nlogn) 的空间复杂度。注意,在某些语言(比如 Java & JavaScript)中字符串是不可变的,因此我们需要额外的 O(n)的空间来拷贝字符串。
代码
class Solution {
public boolean isAnagram(String s, String t) {
int[] arr = new int[26];//仅考虑小写字母,这个数组长度是26即可,下标是和'a'的差
char c;
for (int i = 0; i < s.length(); i++) {
c = s.charAt(i);
arr[c - 'a']++;
}
for (int i = 0; i < t.length(); i++) {
c = t.charAt(i);
arr[c - 'a']--;
}
for (int count : arr){
if (count != 0){
return false;
}
}
return true;
}
}
class Solution2 {
public boolean isAnagram(String s, String t) {
char[] chars1 = s.toCharArray();
char[] chars2 = t.toCharArray();
Arrays.sort(chars1);
Arrays.sort(chars2);
return Arrays.equals(chars1,chars2);
}
}
349. 两个数组的交集
力扣题目链接
题意:给定两个数组,编写一个函数来计算它们的交集。
Java中的HashSet
HashSet是基于HashMap来实现的,实现了Set接口,同时还实现了序列化和可克隆化。而集合(Set)是不允许重复值的。
所以HashSet是一个没有重复元素的集合,但不保证集合的迭代顺序,所以随着时间元素的顺序可能会改变。
由于HashSet是基于HashMap来实现的,所以允许空值,不是线程安全的。
思路
这个题用数组处理就有点复杂了,考虑使用HashMap处理
先遍历第一个数组,把元素存入set中,因为HashMap元素不能重复,所以直接存就好
然后遍历第二个数组,如果set中也有这个元素,那这个元素就是交集的元素,存入结果集resset中
之后把resset转为int类型的数组即可
注意,set可以直接转为array,但是存放的元素是Object类型的,不能在数组层面进行强转
需要遍历数组,取出每个元素,转换数据类型之后,再放入int类型的array中
时间复杂度O(m+n)。计算:遍历两个数组,是O(m+n),判断某个元素是否在HashMap中,是O(1)
空间复杂度:O(m+n),生成了两个HashMap
也可以试试数组,只不过有点耗内存
代码
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
HashSet<Integer> set = new HashSet<>();
HashSet<Integer> resset = new HashSet<>();
for (int i = 0; i < nums1.length; i++) {
set.add(nums1[i]);//set元素不重复
}
for (int i : nums2){
if (set.contains(i)){
resset.add(i);//如果set中有元素,加入resset中
}
}
Object[] array = resset.toArray();//把resset转为int[]
int[] res = new int[array.length];
for (int i = 0; i < array.length; i++) {
res[i] = (int) array[i];
}
return res;
}
}
class Solution1 {
public int[] intersection(int[] nums1, int[] nums2) {
int[] arr = new int[1005];
Set<Integer> set = new HashSet<>();
int num;
for (int i = 0; i < nums1.length; i++) {
num = nums1[i];
arr[num] = 1;
}
for (int i = 0; i < nums2.length; i++) {
num = nums2[i];
if (arr[num] == 1){
set.add(num);
}
}
Object[] array = set.toArray();
int[] res = new int[array.length];
for (int i = 0; i < array.length; i++) {
res[i] = (int)array[i];
}
return res;
}
}
202.快乐数
力扣题目链接
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。
思路
这道题把我坑了
首先需要封装一个私有方法,传入n,输出每个位置数字的平方和
逻辑是,不断对10取模,把n每次除10,得到每个数位的数字,然后不断累加
取模是只要末位,除10是去除末位
然后使用一个HashSet不断接收n,如果某次计算后n为1,就返回true;如果发现集合中存在n,就退出循环,返回false
题目中说到,如果这个数字会导致无限重复,那么就可能是死循环,这是一个很重要的点,也是
判断的根本条件,也就是说 当我们发现拆分下去之后数字的平方和加起来和之前的某一次相等
那么就可以判断他不是一个快乐数可以直接返回false
查找给定数字的下一个值的成本为 O(logn)
代码
class Solution {
public boolean isHappy(int n) {
HashSet<Integer> set = new HashSet<>();
while (!set.contains(n)){
set.add(n);
n = getNextNumber(n);//这两行代码不要反了,不然加完再判断,直接跳出循环
if (n == 1){
return true;
}
}
return false;
}
private int getNextNumber(int n){
int temp;
int res = 0;
while (n > 0){
temp = (n % 10) * (n % 10);//不断对10取模,取出末位数字
res += temp;
n = n / 10;//n/10是让n不断去除末位数字
}
return res;
}
}
1. 两数之和
力扣题目链接
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
思路
两层for循环很简单,但不是最优解
i遍历数组,j遍历i后所有元素,如果遇到和为target,返回两个下标即可
这个题不能用set
的确需要比对元素是否出现过,但输出的不是元素,而是索引,因此用map比较合适
循环数组
每次都要看map中有没有key,有就返回下标对应的数组
注意,返回的是下标,因此需要返回value,用map中的get方法即可
这也是为什么下标是value,因为HashMap不能根据value拿到key,只能根据key拿到value
Java中一个key对应一个value,但一个value可以对应很多个key
没有就存放键值对,key是元素,value是下标
map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下表,这样才能找到与当前元素相匹配的(也就是相加等于target)
接下来是map中key和value分别表示什么。
这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。
那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下表}。
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素比配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
代码
class Solution {
public int[] twoSum(int[] nums, int target) {
for (int 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 null;
}
}
class Solution1 {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])){
return new int[]{map.get(target - nums[i]),i};
}
map.put(nums[i],i);
}
return null;
}
}
补充:HashMap中也可以根据value获得key,只不过复杂一些
public static List<String> getKeyList(HashMap<String, String> map, String value) {
List<String> keyList = new ArrayList();
for (String getKey : map.keySet()) {
if (map.get(getKey).equals(value)) {
keyList.add(getKey);
}
}
return keyList;
}