目录
补充知识:HashMap(或哈希表)的大小通常为2^n的原因
1. 哈希表的概念
散列表(Hash table,也叫哈希表),是根据关键码值(key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
即:给每份数据分配一个编号,放入表格(数组)。通过建立编号与表格索引的关系,将来就可以通过编号快速查找数据。
(1)理想情况编号当是唯一的,数组能够容纳所有数据
(2)现实是不能说为了容纳所有数据造一个超大数组,编号也有可能重复
解决:(1)有限长度的数组,以【拉链】(链表)方式存储数据;(2)允许编号适当重复,通过数据自身来进行区分。
2. LeetCode真题之两数之和
题目描述:
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6 输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6 输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
- 只会存在一个有效答案
进阶:你可以想出一个时间复杂度小于 O(n2)
的算法吗?
01 暴力解法——枚举
思路:先枚举一个数x,然后寻找数组中是否存在target-x。使用两层for循环对nums数组进行遍历,第一层for循环固定初始位置,即第一个数x,从左往右进行遍历,第二层for循环用于寻找与第一层for循环对应变量的满足条件的值,即第二个数target-x,如果有就返回,没有则继续进行遍历。
Java代码实现
class Solution {
public int[] twoSum(int[] nums, int target) {
int len = nums.length;
// 定义数组存储要返回的结果
int[] result = new int[2];
for (int i = 0; i < len; i++) {
// 定义要查询的配对变量
int tar = target - nums[i];
for(int j = i + 1; j < len; j++){
if(nums[j] == tar){
result[0] = i;
result[1] = j;
return result;
}
}
}
return null;
}
}
02 哈希表
思路:先循环遍历数组,获取每个数字x,然后以target - x作为key到hash表查找,若没找到,将x作为key,它的索引作为value放到hash表中,若找到了,返回x以及与它配对的索引。
对于每一个x,先查询hash表中是否存在target - x,然后再将x插入到哈希表中,这样做可以保证不会让x与自己匹配。
Java代码实现
class Solution {
public static int[] twoSum(int[] nums, int target) {
// 定义一个hash表,键和值都是整数类型
HashMap<Integer, Integer> map = new HashMap<>();
// 遍历数组,获取每个数字x
for (int i = 0; i < nums.length; i++) {
int x = nums[i];
if (map.containsKey(target - x)) {
// 找到了,返回x以及与它配对的索引
return new int[]{i, map.get(target - x)};
} else {
// 没找到,将x作为key,它的索引作为value放到hash表中
map.put(x, i);
}
}
return new int[0];
}
}
补充知识:HashMap(或哈希表)的大小通常为2^n的原因
主要涉及以下几个方面:
- 哈希函数的效率和冲突减少:哈希表通过哈希函数将键映射到一个数组的索引上。当数组的大小是2^n时,哈希函数的计算和索引定位变得非常高效。假设数组大小为2^n,哈希函数生成的哈希值可以通过简单的按位操作来计算索引,例如,对于一个哈希值hash和数组大小N=2^n,索引计算可以是:index=hash&(N-1),这里按位与&操作,比除法和取模运算要高效得多。通过这种方式,可以有效地减少哈希冲突。
- 内存对齐和缓存友好:数组大小是2^n可以更好地利用内存对齐和缓存优化,现代计算机的内存和缓存系统通常是按块和对齐方式来设计的。当数组的大小是2^n时,数据结构会更好的符合硬件的内存对齐要求,从而提高内存访问速度。
- 简化计算和优化性能:使用2^n作为数组大小使得一些计算更为简单和高效。例如,计算哈希值的哈希表索引只需进行按位与操作,而不是求模,这在硬件上实现起来更快。这种优化有助于提高哈希表的整体性能,特别是在处理大量数据时。
- 均匀分布哈希值:哈希表的大小是2^n有助于均匀分布哈希值,减少哈希冲突。当哈希表的大小是2^n时,哈希值的低位比特更能均匀分布,从而减少了哈希冲突的可能性。均匀的哈希值分布有助于保持哈希表的性能稳定。
总结:哈希表的大小通常是2^n的原因在于提高哈希函数的效率、优化内存对齐和缓存友好性、简化计算以及减少哈希冲突。通过这些优化措施,哈希表在处理大量数据时能够提供更好的性能和更高的效率。
3. LeetCode真题之无重复字符的最长子串
题目描述:
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是"abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是"b",所以其长度为 1。
示例 3:
输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是"wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104
s
由英文字母、数字、符号和空格组成
01 滑动窗口+哈希表
思路1:根据题中给出的实例,我们可以从左到右一一列举符合条件的结果,从第一个位置a开始最长不含有重复字符的字符串为abc,从b开始为bca,依次类推,可以发现如果我们依次递增枚举子串的开始位置,那么子串的结束位置也是递增的。原因为,如果选择字符串中的第m个位置为开始位置,得到一个不包含重复字符的最长子串的结束位置为n,那么选择第m+1个字符作为起始位置时,从m+1到n的字符很显然是不重复的,并且由于去掉了原本的第m个字符,可以尝试继续增大n,直到右侧出现了重复字符为止。很显然,可以利用滑动窗口的策略去解题。
- 可以使用左右指针表示字符串中的某个子串的左右边界,其中左指针代表枚举的开始位置,右指针代表满足条件的最长子串的结束位置。
- 在每一步的操作中,通过将左指针向右移动一格,表示每次枚举的起始位置,然后不断的移动右指针,但需要保证左右指针的区间中没有重复的字符。在右指针移动结束后,这个子串就对应以左指针开始,不包含重复字符的最长子串,记录其长度。
- 枚举结束后,找到的最长子串的长度即为答案。
在以上的流程中,还有一个关键的点——判断重复字符 ,使用哈希集合来判断是否有重复字符,java中的HashMap和HashSet。
Java代码实现
public static int lengthOfLongestSubString1(String s) {
// 定义hash集合,记录字符是否出现过
HashMap<Character, Integer> map = new HashMap<>();
int result = 0;
// begin 表示枚举的开始位置,end表示每次枚举要寻找的结束位置
for (int begin = 0, end = 0; begin < s.length(); begin++) {
// begin不为0,说明移动了初始位置,要将前面存在map集合中的字符删除
if(begin != 0){
// 初始指针向右移动一格,删除一个字符
map.remove(s.charAt(begin - 1), begin - 1);
}
// map中不存在重复字符,不断的右移结束指针
while (end < s.length() && !map.containsKey(s.charAt(end))) {
map.put(s.charAt(end), end);
end++;
}
// System.out.println(s.substring(begin, end));
// 由于结束while循环时,end指针的位置是满足条件的子串的后一位,因此不需额外+1
result = Math.max(result, end - begin);
}
return result;
}
思路1结果打印
思路2:从左到右依次访问字符串,比如题中的abcabcbb,初始进入这个窗口为abc时满足题目要求,当再进入a,窗口变成了abca。此时不满足要求,所以要对这个窗口进行移动,如何进行移动呢?只要把窗口的左边的元素移出来就可以了,直到满足题目的要求,一直维持这样的窗口,找出队列出现最长的长度即为答案。时间复杂度为O(n)。此时利用滑动窗口+哈希表,来实现,首先定义两个变量表示开始和结束位置,用hash表检查重复字符,然后从左向右查看每个字符,如果: 没遇到重复字符,调整end;遇到重复的字符,调整begin,并将当前字符放入hash表,end - begin + 1是当前子串的长度。
Java代码实现
/*
实现要点:
* 使用begin和end表示子串开始和结束位置
* 用hash表检查重复字符
* 从左向右查看每个字符,如果:
- 没遇到重复字符,调整end
- 遇到重复的字符,调整begin
- 将当前字符放入hash表
* end - begin + 1是当前子串的长度
*/
public static int lengthOfLongestSubString(String s){
int begin = 0;
HashMap<Character, Integer> map = new HashMap<>();
int result = 0;
for (int end = 0; end < s.length(); end++) {
// 获取每个字符
char ch = s.charAt(end);
if(map.containsKey(ch)){
// 防止begin回退,所以要比较一下
begin = Math.max(begin, map.get(ch) + 1);
map.put(ch, end);
}else{
map.put(ch, end);
}
// System.out.println(s.substring(begin, end + 1));
result = Math.max(result, (end - begin + 1));
}
return result;
}
思路2 结果打印
4. LeetCode真题之字母异位词分组
题目描述:
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"] 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""] 输出: [[""]]
示例 3:
输入: strs = ["a"] 输出: [["a"]]
提示:
1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i]
仅包含小写字母
01 排序+哈希表
思路:根据题目可知,字母异位词,指的是字符串中包含的字母相同。最初看到这道题,想到的是获取每个字母的ascii码,然后相加;或者给从a~z的字母进行编号,然后求和,但是这样做会出问题,不同的字母组合之和可能是相同的。
由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的。因此,利用字符串中包含的字母相同的特点,使用哈希表存储每一组字母异位词,哈希表的键为一组字母异位词的标志,哈希表的值为一组字母异位词列表。
Java代码实现
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 定义hash集合,存储字母异位词,key为排序后的字母组合String类型,value为字母异位词List集合
HashMap<String, List<String>> map = new HashMap<>();
// 遍历字符串数组中的每个元素
for(String str: strs){
// 对字符串进行排序,得到hash中的key
char[] chars = str.toCharArray();
Arrays.sort(chars);
// 将排序后的字符转换成String类型
String key = new String(chars);
// 获取key对应的values列表
List<String> list = map.get(key);
// 如果哈希表中没有对应的list
if(list == null){
// 创建一个新的空的集合
list = new ArrayList<>();
// 放入哈希表中
map.put(key, list);
}
// 如果已经存在,加到对应的list集合中即可
list.add(str);
}
return new ArrayList<>(map.values());
}
}
02 计数+哈希表
思路:进一步优化代码,基于互为字母异位词的两个字符串包含相同的字符的特点,可知两个字符串中字母出现的次数应该是完全相同的,可以将每个字母出现的次数使用字符串来表示,作为哈希表的键。根据题目中说仅包含小写字母,可以使用长度为26的数组记录每个字母出现的次数。
Java代码实现
// 由于hash表中的key,要求要重写hashCode的equals方法,整数数组不能直接作为hash表中的key
static class ArrayKey {
// 新建一个类封装整数数组
int[] key = new int[26];
// 重写hashCode的equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ArrayKey arrayKey = (ArrayKey) o;
return Arrays.equals(key, arrayKey.key);
}
@Override
public int hashCode() {
return Arrays.hashCode(key);
}
// 把字符串变成整数数组,即key
public ArrayKey(String str) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
key[ch - 'a']++;
}
}
}
// 计数加hash表
public static List<List<String>> groupAnagrams(String[] strs) {
// 定义hash表,key为定义的类中的ArrayKey类型
HashMap<ArrayKey, List<String>> map = new HashMap<>();
// 遍历数组中的每个元素
for(String str: strs){
// 获取每个字符串对应的key
ArrayKey key = new ArrayKey(str);
// 获取key对应的values列表
List<String> list = map.get(key);
if(list == null){
// 创建一个新的空的集合
list = new ArrayList<>();
// 放入哈希表中
map.put(key, list);
}
// 如果已经存在,加到对应的list集合中即可
list.add(str);
}
return new ArrayList<>(map.values());
}
5. LeetCode真题之存在重复元素
题目描述:
给你一个整数数组 nums
。如果任一值在数组中出现 至少两次 ,返回 true
;如果数组中每个元素互不相同,返回 false
。
示例 1:
输入:nums = [1,2,3,1] 输出:true
示例 2:
输入:nums = [1,2,3,4] 输出:false
示例 3:
输入:nums = [1,1,1,3,3,4,3,2,4,2] 输出:true
提示:
1 <= nums.length <= 105
-109 <= nums[i] <= 109
01 哈希表/暴力解法
思路:这题是leetcode真题中的简单题,思路也比较简单,使用hash表(hashMap和hashSet都可)对整数数组中的重复元素进行判断即可。
暴力解法即使用两层for去判断,逐个比较是否有重复元素,如果存在返回true,不存在返回false,在此就不写实现代码了。官方题解还有一种先对数组进行排序,再判断相邻两个元素是否重复进行解题。
Java代码实现
public static boolean containsDuplicate(int[] nums){
// 创建hashSet集合,用来判断是否存在重复元素
HashSet<Integer> set = new HashSet<>();
// 遍历数组中的元素
for (int num: nums) {
// 如果set集合中包含num
if(set.contains(num)){
// 存在重复元素返回true
return true;
}
// 将元素添加到set中
set.add(num);
}
// 遍历完成,说明不存在重复元素,返回false
return false;
}
注意:这里还可以对代码进行优化,看以下add的源码,set的add方法是插入成功,返回true,否则返回false,可以利用这个特点替换contains方法进行判断
更新后的代码实现:
class Solution {
public boolean containsDuplicate(int[] nums) {
// 创建hashSet集合,用来判断是否存在重复元素
HashSet<Integer> set = new HashSet<>();
// 遍历数组中的元素
for (int num: nums) {
// 插入失败说明已经存在重复元素,返回true
if(!set.add(num)){
return true;
}
}
// 遍历完成,说明不存在重复元素,返回false
return false;
}
}
补充知识:
01 HashSet和HashMap的区别
- 用途不同
HashSet是一个基于哈希表的集合,用于存储不重复的元素,它不存储键值对。它实际上是基于HashMap实现的,之存储了键,值都设置为同一个特殊值(默认为null)HashMap也是一个哈希表的集合,用于存储键值对。允许根据键来查找值,因此在存储和检索键值对方面更加灵活。
- 数据结构不同
HashSet内部使用哈希表(或哈希集合)来存储元素。哈希表是一个无序的数据结构,元素之间没有特定的顺序。HashMap内部也使用哈希表,但它存储键值对,其中键和值之间有关联关系,HashMap具有键的集合和值的集合,键是唯一的,值可以重复。
- 元素类型不同
HashSet存储的是单一的元素类型,如整数、字符串等,用于存储不重复的对象,通过元素的哈希码来判断重复性。HashMap存储键值对,键和值可以是不同类型的对象,键用于检索值。
- 方法不同
HashSet提供了添加、删除、查找元素的方法,如add(), remove(), contains()等,没有提供根据键查找值的方法。HashMap提供了添加键值对、删除键值对、根据键查找值的方法,如put(), remove(), get()等,可以根据键来查找对应的值。
02 HashSet的优缺点
优点:
- 唯一性:HashSet确保存储的元素不重复,适合用于去重
- 快速查找:HashSet提供了快速的元素查找,因为它使用哈希表
- 无序性:HashSet不保证元素的存储顺序,适合不需要关心顺序的场景
缺点
- 不支持键值对:HashSet只存储单一的元素类型,不支持键值对的存储
- 无法存储关联数据:无法将额外的数据与元素关联,智能存储元素本身
03 HashMap的优缺点
优点:
- 键值对存储:HashMap可以存储键值对,允许将关联数据存储在一起
- 快速查找:HashMap提供了快速的键查找值的能力,适合需要根据键查找值的场景
- 灵活性:HashMap提供了更多的功能,如替换值、遍历键值对等
缺点
- 复杂性:相对于HashSet,HashMap的使用可能更加复杂,因为它需要处理键值对的关系
- 额外的内存消耗:HashMap存储键值对,因此需要额外的内存空间
04 HashSet和HashMap的适用场景
- HashSet的适用场景
数据去重:当需要存储一组数据,但不关心顺序和关联信息,只关心数据是否重复时,使用HashSet是合适的,例如,存储一组唯一的用户名或标签。
集合运算:HashSet适合用于集合运算,如求交集、并集、差集等。
- HashMap的适用场景
键值存储:当需要将数据与关联的键一起存储时,使用HashMap是合适的。例如,存储学生的成绩,其中学生名是键,成绩是值。
数据索引:HashMap适合用于构建索引,提供快速查找能力,例如,建立一个电话簿,根据姓名查找电话号码。
需要键值对的功能:如果你需要存储关联数据,并且需要使用键来查找值、替换值或遍历键值对,那么HashMap是最好的选择。