今日内容:哈希表理论基础、242.有效的字母异位词、349. 两个数组的交集、202. 快乐数、1. 两数之和
代码随想录链接:代码随想录 (programmercarl.com)
哈希表
首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。
哈希表是根据关键码的值而直接进行访问的数据结构。
这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。 例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
242.有效的字母异位词
给定两个字符串
s和t,编写一个函数来判断t是否是s的字母异位词。 注意: 若s和t**中每个字符出现的次数都相同,则称s和t**互为字母异位词。
传统的方法肯定是有的,其中一个是循环其中一个数组s,找到一个字符后循环另一个数组t,找到相同的字符后,删去t中该字符,如果找不到则返回false,直到每一个数字都可以找到,且t中没有其他字符,返回true。这种方法时间复杂度O(n²)。
另一种想法是,用一个数组record去记录每一个字符的出现次数,在这道题里因为都是小写字母,所以这个记录数组record只需要26个元素,当s中出现了某个字符,那么在record中就需要在相应位置+1;反之,当t中出现了某个字符,在相应位置-1;都遍历完之后查看record的所有元素是不是都为0。
这种时候,我们可能需要申明数组每一位所对应的字符(手动建立每一条映射关系),很麻烦。
在这一题中,因为ASCII码中a-z是连续的,因此可以用各个字母到‘a’的相对距离来映射数组的下标,刚好这个相对距离是从0-25的整数,可以直接作为下标,不用另外的处理,这个时候用一条公式建立了所有的映射关系。
其实手动和用函数建立映射关系都是哈希法,区别在于哈希函数不同。
这里我就联想到哈希法可以用于保密的知识点,如果我们用一个不可逆运算的哈希函数,那么就可以起到保密的作用。因为从用户的角度,他无法知道映射关系,也逆推不出映射关系。
代码如下:
class Solution {
public boolean isAnagram(String s, String t) {
int[] record = new int[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 num: record){
if (num != 0){
return false;
}
}
return true;
}
}
这道题虽然直接看了答案,但好像对哈希法有了初步的认识,只看哈希基础知识的文章有点抽象了,虽然看懂了,但是对如何编码没有具体的概念。
349.两个数组的交集
给定两个数组
nums1和nums2,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
想法:
- 给两个nums创建哈希表,类似前面的方法,当两个表的相同位置的哈希值大于0,则记录这个值,最后将这些值放在数组里输出。
- 先用一个数组创建哈希表,将这个哈希表初始化后,用第二个数组在这个哈希表上计数。
但是这里用前一种方法创建数组太大了,而实际情况不需要这么大,我只需要根据nums1中存在的数字创建哈希表,我猜测是用到了前面提到的set或者map,于是去学习了一下。还真是。
第一种代码如下:
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] recordA = new int[1001];
int[] recordB = new int[1001];
for(int i = 0; i < nums1.length; i++){
recordA[nums1[i]]++;
}
for(int i: nums2){
recordB[i]++;
}
List<Integer> ansList = new ArrayList<>();//
for(int i = 0; i < 1001; i++){
if(recordA[i] > 0 && recordB[i] > 0){
ansList.add(i);
}
}
int index = 0;
int ans[] = new int[ansList.size()];
for( int i : ansList)
ans[index++] = i;
return ans;
}
}
这里用了列表List保存符合条件的i。
时间复杂度O(n),空间复杂度O(1)
第二种方法
和我预想的差不多,hashset可以方便的用于添加元素,能够自动生成新的映射关系。
HashSet
以下实例我们创建一个 HashSet 对象 sites,用于保存字符串元素:
HashSet<String> sites = new HashSet<String>();
添加元素用add()方法,重复的元素不会被添加 。
判断元素是否存在用contains()方法。
删除元素用remove()方法。
删除所有元素用clear()方法。
计算元素数量用size()方法。
可以用for-each来迭代HashSet中的元素。
for (String i : sites) {
System.out.println(i);
}
还有其他的方法就不列举。
具体的思路就是先把数组nums1的内容遍历出来add到一个Hashset1中,然后再遍历数组nums2,并判断Hashset1中是否包含(contain)数组nums2中的元素,如果包含,就将该元素添加到结果中,并将结果转化为数组。
tips1:需要先判断是否为空数组,这样可以直接输出空数组。
tips2:还要记得导入HashSet包
因此可以写出代码:
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> HashSet1 = new HashSet();
Set<Integer> HashSet2 = new HashSet();
for(int i: nums1){
HashSet1.add(i);
}
for(int i: nums2){
if(HashSet1.contains(i)){
HashSet2.add(i);
}
}
//存放到数组
int[] res = new int[HashSet2.size()];
int index = 0;
for(int i: HashSet2){
res[index++] = i;
}
return res;
}
}
感觉直接用list也是可以完成的,然后去搜了一下两者的区别,发现List可以存放重复的元素而HashList不会存放重复的元素。
答案还提供了更加高级的转成数组的方式,大概知道每一个是干什么的,怎么用的,但是具体的不明白。
return resSet.stream().mapToInt(x -> x).toArray();
快乐数
编写一个算法来判断一个数
n是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。 如果
n是 快乐数 就返回true;不是,则返回false。
思考:一步一步来,将一个数字拆分成一位一位的,这种时候会首先想到“ n%10 ”这种方式,然后计算出下一个n,直到 n == 1 ,代码如下:
class Solution {
public boolean isHappy(int n) {
if(n == 1){
return true;
}
while(n != 1){
int m = n;
int sum = 0;
while(m >0){
sum = sum + (m % 10) * (m % 10);
m = m/10;
}
n = sum;
}
return true;
}
}
然后我们还需要甄别出无限循环的情况,因此现在这个情况已经超时了。
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
用哈希法判断sum是否重复出现,完整代码如下:
class Solution {
public boolean isHappy(int n) {
Set<Integer> sumList = new HashSet();//申明Hashset
if(n == 1){
return true;
}
while(n != 1){
int m = n;
int sum = 0;
while(m >0){//计算平方和
sum = sum + (m % 10) * (m % 10);
m = m/10;
}
n = sum;
if(sumList.contains(sum) == true){//判断是否循环
return false;
}
sumList.add(sum);
}
return true;
}
}
官方答案是:
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
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;
}
}
快乐数真的是太快了啦
两数之和
给定一个整数数组
nums和一个整数目标值target,请你在该数组中找出 和为目标值target的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回答案。
初见思路:就是先把所有元素扔到一个HashSet里,把他的数值映射到地址上(x->x),然后就可以先随便找一个数i,通过计算target-i,去找另一个数字,如果找到了就可以输出两个值的下标,没找到的话,就把这个i从中HashList中删去,进行下一轮。可能会用到HashMap?
显然这种想法是愚蠢的,本题呢,需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
不过确实需要用到HashMap,因为本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
import java.util.HashMap;
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> sites = new HashMap<Integer,Integer>();
int index = 0;
int[] res = new int[2];
if(nums == null || nums.length == 0){
return res;
}
for(int i = 0; i < nums.length; i++){
int temp = target - nums[i];
if(sites.containsKey(temp)){
res[1] = i;
res[0] = sites.get(temp);
break;
}
sites.put(nums[i], i);
}
return res;
}
}
在思考这一题的时候,没想过key来存元素,value来存下标,长见识了。
今天的三道题把哈希表,还有数组、hashset和hashmap的情况都学了一遍,要知道什么情况用哪个。
数组 数组消耗的空间最小,简单直接有效。
HashSET 为什么用set,不用数组:
- 数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
- 如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
HashMAP 来说一说:使用数组和set来做哈希法的局限。
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
- set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
下班!