基础知识:
哈希是什么: 快速!找出集合里的元素。时间复杂度 O(1)
哈希函数 hash function: 把其他数值格式转化成 index 也就是hashCode
hashCode: 是通过特定编码方式,可以将其他数据格式转化为不同的数值 (🌰 将学生名字映射在哈希表的index上,用来找学生属于哪个学校)但是如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
hashCode 的index 大于 tableSize:此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作。
哈希碰撞:两个数值都映射到了hash表的同一个index下。---解决办法:拉链法和线性探测法。
拉链法:发生冲突的元素都被存储在链表中。就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间
线性探测法:一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
常见3种哈希结构: 数组,set(集合),map (映射)
- 我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset
- map中,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现
什么是 流 “Stream”:在Java中,Stream API 是Java 8中引入的一个重要特性,它提供了一种高效且简洁的方法来处理数据序列。流可以看作是数据项的序列,并提供了许多操作以对这些数据进行计算。流API借鉴了函数式编程的思想,允许以声明式的方式处理数据,即表达做什么,而不是怎么做。
流操作通常会形成一个管道,其中包含一系列的中间操作(如筛选、映射),以及一个终端操作(如收集、求和)来产生一个结果或副作用。
- 无状态: 流操作应该是无状态的,避免使用外部变量。
- 单次使用: 流只能被消费一次。一旦一个终端操作被执行,流就会关闭。
➡️流的创建:
- 从集合创建: 可以从集合(如List、Set)通过
stream()
方法创建流。 - 从数组创建: 通过
Arrays.stream(T[] array)
方法。 - 静态工厂方法: 如
Stream.of(T...)
,IntStream.range(int startInclusive, int endExclusive)
等
➡️流的操作:
- 中间操作: 中间操作返回流本身,并允许多个操作链接在一起。常见的中间操作包括
filter
(过滤)、map
(映射)、sorted
(排序)等。 - 终端操作: 终端操作返回一个结果或产生一个副作用。常见的终端操作包括
forEach
(遍历)、collect
(收集到集合)、reduce
(归约)、sum
(求和)等。
➡️并行流:
- 并行处理: 流可以非常简单地转换为并行模式,以利用多核处理器的优势,仅需调用
parallel()
方法。 - 注意事项: 并行流使用共享的
ForkJoinPool
,并且并不总是比顺序流快,特别是在数据量较小或计算不密集时
242.有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true
示例 2: 输入: s = "rat", t = "car" 输出: false
说明: 你可以假设字符串只包含小写字母。
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']++; //遍历s字符串,统计字符出现次数在哈希表上以递增记录
}
for(int i =0; i <t.length(); i++){
record[t.charAt(i)-'a']--; //遍历t字符串,统计字符出现次数在哈希表上以递减记录
}
for(int count:record){
if(count!=0){
return false; //统计所有的正负值相加,如果不为0,则s和t的字母数量对应不上
}
} return true; // // record数组所有元素都为零0,说明字符串s和t是字母异位词
}
}
349. 两个数组的交换
set 不是万能用法,因为比数组慢。直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
Java中用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> 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;
}
}
resSet.stream()
: resSet
是一个 Set<Integer>
集合。stream()
方法将这个集合转换为一个流 (Stream<Integer>
),便于后续的操作。
mapToInt
属于中间操作(Intermediate Operation)的一种。这些操作是构建流水线的关键部分,用于转换和处理流中的元素,而不会触发任何实际的数据处理,这意味着它们是惰性的。
mapToInt
方法用于将对象流转换为一个原始的 int
流(IntStream
)。这个方法接受一个函数作为参数,这个函数应用于流中的每个元素,并将它们映射为 int
类型的值。
它主要用于将非原始类型(如 Integer
、String
等)的流元素转换成原始类型 int
的值。这对于性能优化是有好处的,因为原始类型流比对象流更加高效。
List<String> strings = Arrays.asList("Hello", "World");
int[] lengths = strings.stream().mapToInt(String::length).toArray();
在这个例子中,mapToInt
方法将 strings
流中的每个 String
元素映射到它们的长度(一个 int
值),然后 toArray()
方法将这个 IntStream
转换成一个 int
数组。
这里的 x -> x
是一个 lambda 表达式,表示对于流中的每一个元素 x
(这里的 x
是 Integer
类型),直接返回它的 int
值。因为 Integer
类型可以自动拆箱为 int
,所以这个表达式实际上是在进行自动拆箱。
.toArray()
: 这个方法将流中的元素收集到一个数组中。因为前面使用了 mapToInt
,所以生成的是一个 int
类型的数组,而不是 Integer
类型的数组
😁Set
是一个表示元素集合的接口,其中每个元素都是唯一的(不允许重复)。是<Integer>
一个泛型类型参数,指示这Set
将包含类型的元素Integer
。
这set1
可用于存储唯一整数的集合,通常用于各种操作,例如检查某些值是否存在、添加新值、删除值和迭代值。HashSet
当主要操作涉及搜索元素时,使用是特别有利的,因为此类操作的平均时间复杂度为 O(1)。
new HashSet<>()
:这部分代码是a的实例化HashSet
,是接口的具体实现Set
。HashSet
由哈希表(实际上是一个实例)支持HashMap
。它不保证其元素的顺序,这意味着迭代顺序可以与插入顺序不同。该<>
运算符称为菱形运算符,告诉编译器从上下文推断泛型类型,在本例中为Integer
。
😁寻找交点:for (int i : nums2) { if (set1.contains(i)) { resSet.add(i); } }
:该循环迭代nums2
。对于 中的每个元素nums2
,它检查该元素是否存在于set1
(其中包含 的元素nums1
)中。如果该元素同时存在于两个数组中,则将其添加到 中resSet
,后者存储交集
方法二:用HashMap 写
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash1 = new int[1002];
int[] hash2 = new int[1002];
for(int i : nums1)
hash1[i]++;
for(int i : nums2)
hash2[i]++; //这些循环迭代nums1和nums2,增加相应哈希数组中每个元素的计数
List<Integer> resList = new ArrayList<>(); //创建一个List命名resList来存储在两个数组中找到的元素。
for(int i = 0; i < 1002; i++) //寻找交集元素
if(hash1[i] > 0 && hash2[i] > 0) //如果在hash1和hash2中都找到某个元素,i则将其添加到。hash1hash2resList
resList.add(i);
int index = 0;
int res[] = new int[resList.size()];
for(int i : resList)
res[index++] = i;
return res;
}
}
202.快乐数
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
出现了 要快速找到一个元素(sum)是否出现在集合里的情况。----就用到HashMap
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
while (n != 1 && !record.contains(n)) { //n变成1:这意味着找到了一个快乐数,因为根据快乐数的定义,不断替换为其数字平方和的过程最终会得到1。
//record.contains(n)返回true:这意味着当前的数字n之前已经出现过,表明进入了一个无限循环,因此这个数不是快乐数。
record.add(n); //这行代码将当前的数字n添加到HashSet record中。HashSet用于记录所有出现过的数字,以便检测是否进入了无限循环。
n = getNextNumber(n); //这行代码调用getNextNumber方法,该方法接收当前的数字n,计算并返回下一个数字。
}
return n == 1;
}
private int getNextNumber(int n) {
int res = 0;
while (n > 0) {
int temp = n % 10; //这行代码用于获取数字n的最后一位。操作符%表示取模,即取余数。因此,n % 10会得到n的个位数
res += temp * temp; //这行代码计算得到的个位数的平方,并将其累加到变量res中。res是用来存储每一位数平方和的变量。
n = n / 10; //这行代码用于移除数字n的最后一位。操作符/表示整数除法。因此,n / 10会把n最右边的数字(即个位数)去掉,例如,如果n是123,那么执行n / 10之后,n会变成12。
}
return res;
} //整个过程是将数字n的每一位数字分离出来,计算其平方,然后将这些平方数累加。
}
两数之和
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
解题 既需要下标又需要值 所以用Hash (数组空间局限而且会有浪费,set 是集合只能存key不能存value)
拿着一个数字(存进Hash表里的键值对)去找另一个符合条件的元素加入HashMap中
接下来需要明确两点:
- map用来做什么
- map中key和value分别表示什么
map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)
接下来是map中key和value分别表示什么。
这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。
那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
if(nums == null || nums.length == 0){
return res;
}
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
int temp = target - nums[i]; // 对于每个元素 nums[i],计算 target 与其差值 temp
if(map.containsKey(temp)){ //检查 map 中是否存在一个键(即一个数),它的值等于 temp。如果存在,表示我们找到了两个数,它们的和等于 target。
res[1] = i; //如果找到了这样的一个数,将其索引(i)赋值给 res[1]。这表示数组 res 的第二个元素是当前遍历到的数组 nums 的索引
res[0] = map.get(temp); //同时,获取 temp 在 map 中对应的值,即另一个加数的索引,赋值给 res[0]。这表示数组 res 的第一个元素是与当前元素配对的另一个元素的索引。
break; //找到这两个数后,退出循环,因为任务已完成
}
map.put(nums[i], i); // 如果没找到匹配对,就把访问过的元素和下标加入到map中
}
return res;
}